caffa_Obsidian-Current-Fold.../main.ts

960 lines
32 KiB
TypeScript
Raw Permalink Normal View History

import { App, Notice, Plugin, PluginSettingTab, Setting, TFile } from 'obsidian';
2024-03-23 02:57:56 +00:00
import { ItemView, WorkspaceLeaf } from "obsidian";
import { MarkdownView } from "obsidian";
import { TFolder } from "obsidian";
2022-04-15 18:13:31 +00:00
2024-03-23 02:57:56 +00:00
interface CurrentFolderNotesDisplaySettings {
excludeTitlesFilter: string;
includeTitleFilter: string;
prettyTitleCase: boolean;
includeSubfolderNotes: boolean;
2025-03-08 17:41:17 +00:00
includeCurrentFileOutline: boolean;
includeListFileOutlines: boolean;
styleMode: 'minimal' | 'fancy' | 'neobrutalist';
2025-03-08 12:54:03 +00:00
showNavigation: boolean;
2025-03-08 18:21:42 +00:00
biggerText: boolean; // Add this line
biggerTextMobileOnly: boolean; // Add this new setting
2025-05-17 11:18:12 +00:00
maxFilesDisplay: number; // Add new setting for max files to display
2022-04-15 18:13:31 +00:00
}
2025-03-08 17:41:17 +00:00
const DEFAULT_SETTINGS: Partial<CurrentFolderNotesDisplaySettings> = {
excludeTitlesFilter: '_index',
includeTitleFilter: '',
prettyTitleCase: true,
includeSubfolderNotes: false,
2025-03-08 17:41:17 +00:00
includeCurrentFileOutline: true,
includeListFileOutlines: false,
2025-03-08 12:54:03 +00:00
styleMode: 'fancy',
2025-03-08 18:21:42 +00:00
showNavigation: false,
biggerText: false,
2025-05-17 11:18:12 +00:00
biggerTextMobileOnly: false,
maxFilesDisplay: 100 // Default to showing 100 files
2022-04-15 18:13:31 +00:00
}
2024-03-23 02:57:56 +00:00
export default class CurrentFolderNotesDisplay extends Plugin {
settings: CurrentFolderNotesDisplaySettings;
2025-03-08 12:54:03 +00:00
public leaves: WorkspaceLeaf[] = [];
2022-04-15 18:13:31 +00:00
fileChangeHandler(file: TFile) {
2025-03-08 12:54:03 +00:00
if (file instanceof TFile) {
this.load();
}
}
2022-04-15 18:13:31 +00:00
async onload() {
await this.loadSettings();
2025-03-08 17:41:17 +00:00
2025-03-08 18:21:42 +00:00
// Load custom styles
this.loadStyles();
2025-03-08 17:41:17 +00:00
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new CurrentFolderNotesDisplaySettingTab(this.app, this));
2025-03-08 17:41:17 +00:00
// add a panel to the right sidebar - view
2025-03-08 17:41:17 +00:00
this.registerView(VIEW_TYPE_CURRENT_FOLDER_NOTES_DISPLAY, (leaf) => new CurrentFolderNotesDisplayView(leaf, this));
2025-03-08 17:41:17 +00:00
// Add a ribbon icon
this.addRibbonIcon('folder', 'Activate folder notes display', () => {
2024-03-23 02:57:56 +00:00
this.activateView();
2022-04-15 18:13:31 +00:00
});
// Add a command to open the view
2024-03-23 05:49:16 +00:00
this.addCommand({
id: 'activate-folder-notes-display',
name: 'Open Pane',
2024-03-23 05:49:16 +00:00
callback: () => {
this.activateView();
}
});
2025-03-12 04:53:52 +00:00
// Add debouncing for refreshView
let refreshTimeout: NodeJS.Timeout | null = null;
const debouncedRefresh = () => {
if (refreshTimeout) clearTimeout(refreshTimeout);
refreshTimeout = setTimeout(() => {
this.refreshView();
}, 300); // Wait 300ms before refreshing
};
// Use debounced refresh for all file events
this.registerEvent(this.app.workspace.on('file-open', debouncedRefresh));
this.registerEvent(this.app.vault.on('delete', debouncedRefresh));
this.registerEvent(this.app.vault.on('create', debouncedRefresh));
this.registerEvent(this.app.vault.on('rename', debouncedRefresh));
2022-04-15 18:13:31 +00:00
}
async onunload() {
console.log('unloading plugin');
// Clean up by detaching any leaves created by this plugin
const leavesToDetach = this.app.workspace.getLeavesOfType(VIEW_TYPE_CURRENT_FOLDER_NOTES_DISPLAY);
leavesToDetach.forEach(leaf => {
leaf.detach();
});
// Clear the leaves array
this.leaves = [];
2022-04-15 18:13:31 +00:00
}
2024-03-23 02:57:56 +00:00
async activateView() {
const { workspace } = this.app;
// Clean up existing leaves first to prevent duplicates
const existingLeaves = workspace.getLeavesOfType(VIEW_TYPE_CURRENT_FOLDER_NOTES_DISPLAY);
// Keep only the first leaf if multiple exist
if (existingLeaves.length > 1) {
for (let i = 1; i < existingLeaves.length; i++) {
existingLeaves[i].detach();
}
}
2024-03-23 02:57:56 +00:00
let leaf: WorkspaceLeaf | null = null;
if (existingLeaves.length) {
2024-03-23 02:57:56 +00:00
// A leaf with our view already exists, use that
leaf = existingLeaves[0];
2024-03-23 02:57:56 +00:00
} 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) {
2024-12-11 06:23:28 +00:00
await leaf.setViewState({ type: VIEW_TYPE_CURRENT_FOLDER_NOTES_DISPLAY, active: true });
// Only add to leaves array if it's a new leaf
2024-12-11 06:23:28 +00:00
this.leaves.push(leaf);
2024-03-23 02:57:56 +00:00
}
}
if (!leaf) {
new Notice('Could not create a new leaf for the view');
return;
}
workspace.revealLeaf(leaf);
}
2024-03-23 05:46:45 +00:00
async refreshView() {
const { workspace } = this.app;
2025-03-11 07:32:03 +00:00
// console.log("[CFN] Refresh view triggered");
2024-03-23 05:46:45 +00:00
const leaves = workspace.getLeavesOfType(VIEW_TYPE_CURRENT_FOLDER_NOTES_DISPLAY);
2025-03-11 07:32:03 +00:00
// console.log("[CFN] Found leaves:", leaves.length);
if (leaves.length > 1) {
2025-03-11 07:32:03 +00:00
// console.log("[CFN] Multiple leaves found, cleaning up extras");
for (let i = 1; i < leaves.length; i++) {
leaves[i].detach();
}
}
if (leaves.length === 1) {
2025-03-11 07:32:03 +00:00
// console.log("[CFN] Updating existing view");
2025-06-12 15:53:03 +00:00
const leaf = leaves[0];
const view = leaf.view;
// Check if view is an instance of CurrentFolderNotesDisplayView
if (view && view instanceof CurrentFolderNotesDisplayView) {
await view.displayNotesInCurrentFolder();
} else {
// If the view doesn't have the expected method, recreate it
console.log("[CFN] View is not a CurrentFolderNotesDisplayView, recreating view");
leaf.detach();
await this.activateView();
}
} else if (leaves.length === 0) {
2025-03-11 07:32:03 +00:00
// console.log("[CFN] No view found, creating new one");
2024-03-23 05:46:45 +00:00
this.activateView();
}
}
2022-04-15 18:13:31 +00:00
async loadSettings() {
2025-03-08 17:41:17 +00:00
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
2022-04-15 18:13:31 +00:00
}
async saveSettings() {
await this.saveData(this.settings);
}
2025-03-08 18:21:42 +00:00
// Add this method to load styles
loadStyles() {
// Load styles from the plugin's styles.css
const styleEl = document.createElement('style');
styleEl.id = 'current-folder-notes-styles';
document.head.appendChild(styleEl);
2025-03-08 18:21:42 +00:00
// Register the stylesheet to be removed when the plugin unloads
this.register(() => styleEl.remove());
2025-03-08 18:21:42 +00:00
// Load the CSS file from the plugin directory
this.loadData().then(data => {
// You can add default styles here if needed
const defaultStyles = `
.folder-notes-style { font-weight: 500; }
.folder-notes-style.current-file { font-weight: bold; }
.hover-style-file { text-decoration: underline; }
.hover-style-heading { font-style: italic; }
/* Add more default styles as needed */
`;
styleEl.textContent = defaultStyles;
});
}
2022-04-15 18:13:31 +00:00
}
2024-03-23 02:57:56 +00:00
export const VIEW_TYPE_CURRENT_FOLDER_NOTES_DISPLAY = "current-folder-notes-view";
export class CurrentFolderNotesDisplayView extends ItemView {
plugin: CurrentFolderNotesDisplay;
2024-03-27 07:42:01 +00:00
constructor(leaf: WorkspaceLeaf, plugin: CurrentFolderNotesDisplay) {
2024-03-23 02:57:56 +00:00
super(leaf);
this.plugin = plugin;
2022-04-15 18:13:31 +00:00
}
2024-03-23 02:57:56 +00:00
getViewType() {
return VIEW_TYPE_CURRENT_FOLDER_NOTES_DISPLAY;
}
getDisplayText() {
2024-03-27 07:42:01 +00:00
return "Current Folder Notes";
}
getIcon(): string {
return 'folder';
2024-03-23 02:57:56 +00:00
}
async onOpen() {
await this.displayNotesInCurrentFolder();
}
async onClose() {
// Remove this leaf from the plugin's leaves array to avoid memory leaks
const index = this.plugin.leaves.indexOf(this.leaf);
if (index > -1) {
this.plugin.leaves.splice(index, 1);
}
}
// main.ts
// Function to create clickable headings
createClickableHeadings(container: HTMLElement, currentFileContent: string, currentFilePath: string, addExtraHeadingCSS: boolean): void {
2025-03-12 04:53:52 +00:00
// Set a maximum limit on the file content to process for safety
const MAX_CONTENT_SIZE = 500000; // 500KB max processing size
if (currentFileContent.length > MAX_CONTENT_SIZE) {
currentFileContent = currentFileContent.substring(0, MAX_CONTENT_SIZE);
// Add a note that content was truncated
container.createEl('p', {
cls: 'content-truncated-note',
text: 'Note is very large. Only showing headings from the first portion.'
});
}
2025-03-12 04:53:52 +00:00
// Use a more efficient regex approach for large files
const headingRegex = /^(#+)\s+(.*)$/gm;
let match;
let headingCount = 0;
const MAX_HEADINGS = 100; // Limit the number of headings displayed
// Process headings in batches for very large files
try {
while ((match = headingRegex.exec(currentFileContent)) !== null && headingCount < MAX_HEADINGS) {
const headingLevel = match[1].length;
let headingText = match[2];
// Use extractAlias to get the alias from the heading text
headingText = this.extractAlias(headingText);
// Add a right arrow symbol to the heading text
let headingLabel = '→ ' + headingText;
const p: HTMLElement = container.createEl('p', { text: headingLabel });
p.classList.add('basic-heading');
p.classList.add(`heading-level-${headingLevel}`);
if (addExtraHeadingCSS) {
p.classList.add('extra-heading-style');
}
2025-03-12 04:53:52 +00:00
p.addEventListener('click', (event) => {
// Prevent default behavior
event.preventDefault();
2025-03-12 04:53:52 +00:00
// Use the openLinkText method to navigate to the heading
this.app.workspace.openLinkText('#' + headingText, currentFilePath);
2025-03-12 04:53:52 +00:00
// Clear any text selection
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
}
2025-03-12 04:53:52 +00:00
});
// Add hover effect
p.onmouseover = () => {
p.classList.add('hover-style-heading');
}
// Remove hover effect when not hovering
p.onmouseout = () => {
p.classList.remove('hover-style-heading');
}
2025-03-12 04:53:52 +00:00
headingCount++;
}
// Indicate if there are more headings not shown
if (headingCount >= MAX_HEADINGS) {
container.createEl('p', {
cls: 'more-headings-note',
text: '... more headings available (not shown)'
});
}
} catch (err) {
console.error("Error processing headings:", err);
container.createEl('p', {
cls: 'error-note',
text: 'Error processing headings'
});
}
}
// Function to extract alias from heading text
extractAlias(headingText: string): string {
const matches = headingText.match(/\[\[.*\|(.*?)\]\]/);
return matches ? matches[1] : headingText;
}
2025-03-08 12:54:03 +00:00
private getShortNoteName(basename: string): string {
// Try to find T numbers first (e.g., T1, T23)
const tMatch = basename.match(/T(\d+)/);
if (tMatch) return `T${tMatch[1]}`;
2025-03-08 12:54:03 +00:00
// Try to find Y numbers next (e.g., Y1, Y2023)
const yMatch = basename.match(/Y(\d+)/);
if (yMatch) return `Y${yMatch[1]}`;
2025-03-08 12:54:03 +00:00
// Try to find any numbers
const numberMatch = basename.match(/\d+/);
if (numberMatch) return numberMatch[0];
2025-03-08 12:54:03 +00:00
// Try to find text after dash
const dashMatch = basename.match(/-\s*(.+)$/);
if (dashMatch) return dashMatch[1];
2025-03-08 12:54:03 +00:00
// If nothing else matches, return the basename
return basename;
}
2024-03-23 05:46:45 +00:00
async displayNotesInCurrentFolder(): Promise<void> {
2025-03-11 07:32:03 +00:00
// console.log("[CFN] Starting displayNotesInCurrentFolder");
2025-03-08 12:54:03 +00:00
const container = this.containerEl.children[1] as HTMLElement;
2024-03-23 02:57:56 +00:00
container.empty();
2025-03-08 12:54:03 +00:00
// Get display settings
2025-03-12 04:53:52 +00:00
// const displayMode = this.plugin.settings.displayMode; // Removed
2025-03-08 12:54:03 +00:00
const styleMode = this.plugin.settings.styleMode;
const showNavigation = this.plugin.settings.showNavigation;
2024-03-23 02:57:56 +00:00
2025-03-08 12:54:03 +00:00
// Create header
const headerContainer = container.createDiv({ cls: 'folder-view-header' });
headerContainer.createEl("h6", { text: "Current Folder Notes" });
// Get current file info and folder path
2024-03-23 02:57:56 +00:00
const activeFile = this.app.workspace.getActiveFile();
2025-03-11 07:32:03 +00:00
// console.log("[CFN] Active file:", activeFile?.path);
2024-03-23 02:57:56 +00:00
const currentFilePath = activeFile ? activeFile.path : '';
let parentFolderPath = ''; // Don't default to root
if (currentFilePath) {
const lastSlashIndex = currentFilePath.lastIndexOf('/');
parentFolderPath = lastSlashIndex > 0 ? currentFilePath.substring(0, lastSlashIndex) : '';
2025-03-11 07:32:03 +00:00
// console.log("[CFN] Parent folder path from current file:", parentFolderPath);
} else if (this.app.workspace.getActiveViewOfType(MarkdownView)) {
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
2025-03-11 07:32:03 +00:00
// console.log("[CFN] Found active view:", activeView?.file?.path);
if (activeView?.file) {
const viewFilePath = activeView.file.path;
const lastSlashIndex = viewFilePath.lastIndexOf('/');
parentFolderPath = lastSlashIndex > 0 ? viewFilePath.substring(0, lastSlashIndex) : '';
2025-03-11 07:32:03 +00:00
// console.log("[CFN] Parent folder path from view:", parentFolderPath);
}
}
// Show warning if no valid folder path found
if (!parentFolderPath) {
const warningDiv = container.createDiv({ cls: 'empty-state-message' });
warningDiv.createEl('p', {
text: 'Please open a note in a folder to view related notes.',
cls: 'empty-state-highlight'
});
warningDiv.createEl('p', {
text: 'The root folder cannot be shown to prevent performance issues with large vaults.',
cls: 'empty-state-subtext'
2025-03-08 12:54:03 +00:00
});
return;
2025-03-08 12:54:03 +00:00
}
2024-03-23 02:57:56 +00:00
// Show current folder path
const pathDisplay = headerContainer.createEl('div', {
cls: 'folder-path-display',
text: parentFolderPath
});
pathDisplay.style.fontSize = 'var(--font-smaller)';
pathDisplay.style.color = 'var(--text-muted)';
pathDisplay.style.marginBottom = '10px';
pathDisplay.style.wordBreak = 'break-word';
2025-03-08 12:54:03 +00:00
// Get and filter files
let folder = this.app.vault.getAbstractFileByPath(parentFolderPath);
2025-03-11 07:32:03 +00:00
// console.log("[CFN] Found folder:", folder?.path);
2025-03-08 12:54:03 +00:00
let files: TFile[] = [];
if (folder instanceof TFolder) {
2025-03-08 12:54:03 +00:00
files = folder.children.filter((file: any) => file instanceof TFile) as TFile[];
2025-03-11 07:32:03 +00:00
// console.log("[CFN] Initial files count:", files.length);
}
2025-03-08 12:54:03 +00:00
files = this.applyFilters(files, parentFolderPath);
2025-03-11 07:32:03 +00:00
// console.log("[CFN] Files after filtering:", files.length);
// console.log("[CFN] Current filters - Include:", this.plugin.settings.includeTitleFilter, "Exclude:", this.plugin.settings.excludeTitlesFilter);
2025-03-08 12:54:03 +00:00
if (files.length === 0) {
2025-03-08 17:41:17 +00:00
this.showEmptyState(container, activeFile, currentFilePath, files);
2025-03-08 12:54:03 +00:00
return;
}
2025-03-08 12:54:03 +00:00
// Sort files by name/sequence
const sequenceWithPrefixOrLongest = (str: string) => {
const tMatches = str.match(/T(\d+)/);
if (tMatches) return parseInt(tMatches[1]);
2025-03-08 12:54:03 +00:00
const yMatches = str.match(/Y(\d+)/);
if (yMatches) return parseInt(yMatches[1]) + 1000;
const matches = str.match(/\d+/g) || [];
return Math.max(...matches.map(numStr => parseInt(numStr)), 0);
};
2025-03-08 12:54:03 +00:00
files.sort((a, b) => sequenceWithPrefixOrLongest(a.basename) - sequenceWithPrefixOrLongest(b.basename));
const currentFileIndex = files.findIndex(file => file.path === currentFilePath);
2024-03-23 02:57:56 +00:00
2025-03-08 12:54:03 +00:00
// Create main content container
const mainContent = container.createDiv({ cls: 'main-content' });
2025-03-08 12:54:03 +00:00
// Show navigation section if enabled and we have a current file
if (showNavigation && currentFileIndex !== -1) {
const navigationSection = mainContent.createDiv({ cls: 'navigation-section' });
2025-03-08 12:54:03 +00:00
// Navigation header with prev/next
const navHeader = navigationSection.createDiv({ cls: 'navigation-header' });
2025-03-08 12:54:03 +00:00
// Previous note
if (currentFileIndex > 0) {
const prevNote = files[currentFileIndex - 1];
const prevLink = navHeader.createDiv({ cls: 'nav-link prev-note' });
const prevIcon = prevLink.createSpan({ cls: 'nav-icon' });
prevIcon.innerHTML = `<svg viewBox="0 0 100 100" class="arrow-left" width="16" height="16"><path fill="currentColor" stroke="currentColor" d="M 60,20 L 30,50 L 60,80"></path></svg>`;
prevLink.createSpan({ cls: 'nav-direction', text: 'Previous' });
const prevTitle = prevLink.createSpan({
cls: 'nav-title',
2025-03-08 12:54:03 +00:00
text: this.getShortNoteName(prevNote.basename)
});
2025-03-08 12:54:03 +00:00
prevLink.setAttribute('aria-label', `Previous: ${prevNote.basename}`);
prevLink.addEventListener('click', () => {
2025-05-17 11:28:51 +00:00
// Use the full path to handle duplicate file names
this.app.workspace.openLinkText(prevNote.path, '');
2025-03-08 12:54:03 +00:00
});
}
2025-03-08 12:54:03 +00:00
// Next note
if (currentFileIndex < files.length - 1) {
const nextNote = files[currentFileIndex + 1];
const nextLink = navHeader.createDiv({ cls: 'nav-link next-note' });
nextLink.createSpan({ cls: 'nav-direction', text: 'Next' });
const nextIcon = nextLink.createSpan({ cls: 'nav-icon' });
nextIcon.innerHTML = `<svg viewBox="0 0 100 100" class="arrow-right" width="16" height="16"><path fill="currentColor" stroke="currentColor" d="M 40,20 L 70,50 L 40,80"></path></svg>`;
const nextTitle = nextLink.createSpan({
cls: 'nav-title',
2025-03-08 12:54:03 +00:00
text: this.getShortNoteName(nextNote.basename)
});
nextLink.setAttribute('aria-label', `Next: ${nextNote.basename}`);
nextLink.addEventListener('click', () => {
2025-05-17 11:28:51 +00:00
// Use the full path to handle duplicate file names
this.app.workspace.openLinkText(nextNote.path, '');
2025-03-08 12:54:03 +00:00
});
}
2025-03-08 12:54:03 +00:00
2025-03-12 04:53:52 +00:00
// Current note outline - LAZY LOAD
2025-03-08 17:41:17 +00:00
if (this.plugin.settings.includeCurrentFileOutline) {
2025-03-08 12:54:03 +00:00
const currentFile = files[currentFileIndex];
2025-03-12 04:53:52 +00:00
// Create the outline section first
const outlineSection = navigationSection.createDiv({ cls: 'outline-section' });
2025-03-12 04:53:52 +00:00
// Add current note title
outlineSection.createEl('div', {
cls: 'current-note-title',
text: currentFile.basename
});
outlineSection.createEl('div', {
cls: 'folder-section-header',
text: 'CURRENT NOTE OUTLINE'
});
// Add a loading indicator
const loadingEl = outlineSection.createEl('div', {
cls: 'loading-indicator',
text: 'Loading outline...'
});
// Load content asynchronously
setTimeout(async () => {
const fileContent = await this.app.vault.read(currentFile);
if (fileContent) {
// Remove loading indicator
loadingEl.remove();
// Create the headings
this.createClickableHeadings(outlineSection, fileContent, currentFile.path, true);
} else {
loadingEl.setText('No content found');
}
}, 10);
}
2024-03-23 02:57:56 +00:00
2025-03-08 12:54:03 +00:00
// Add separator
mainContent.createDiv({ cls: 'section-separator' });
}
2025-03-08 12:54:03 +00:00
// Always show flat list view
2025-03-08 17:41:17 +00:00
const listContainer = mainContent.createDiv({ cls: 'notes-flat-list' });
listContainer.createEl('div', {
2025-03-08 12:54:03 +00:00
cls: 'folder-section-header',
text: 'FOLDER NOTES'
});
2025-03-12 04:53:52 +00:00
// OPTIMIZE: Limit the number of files displayed at once to prevent memory issues
2025-05-17 11:18:12 +00:00
const MAX_FILES_DISPLAY = this.plugin.settings.maxFilesDisplay; // Use the setting value
2025-03-12 04:53:52 +00:00
const displayedFiles = files.slice(0, MAX_FILES_DISPLAY);
const hasMoreFiles = files.length > MAX_FILES_DISPLAY;
// Process files in batches to prevent UI freezing
const BATCH_SIZE = 20;
const processBatch = async (startIdx: number) => {
const endIdx = Math.min(startIdx + BATCH_SIZE, displayedFiles.length);
for (let i = startIdx; i < endIdx; i++) {
const file = displayedFiles[i];
const isCurrentFile = file.path === currentFilePath;
const fileContainer = listContainer.createDiv({
cls: isCurrentFile ? 'file-container current' : 'file-container'
});
this.createFileLink(fileContainer, file, currentFilePath, parentFolderPath);
2025-03-12 04:53:52 +00:00
// Only load outlines if explicitly enabled
if (this.plugin.settings.includeListFileOutlines) {
// Add a placeholder initially
const outlinePlaceholder = fileContainer.createEl('div', {
cls: 'outline-placeholder',
text: 'Loading outline...'
});
2025-03-12 04:53:52 +00:00
// Load outline in the next animation frame
requestAnimationFrame(async () => {
try {
const fileContent = await this.app.vault.read(file);
outlinePlaceholder.remove();
if (fileContent) {
this.createClickableHeadings(fileContainer, fileContent, file.path, isCurrentFile);
}
} catch (err) {
console.error("Error loading outline:", err);
outlinePlaceholder.setText("Failed to load outline");
}
});
}
}
2025-03-12 04:53:52 +00:00
// Process next batch if needed
if (endIdx < displayedFiles.length) {
setTimeout(() => processBatch(endIdx), 50);
}
// Show message if we're not displaying all files
if (hasMoreFiles && endIdx === displayedFiles.length) {
listContainer.createEl('div', {
cls: 'more-files-message',
text: `Showing ${MAX_FILES_DISPLAY} of ${files.length} notes. Use filters to narrow results.`
});
}
};
// Start processing the first batch
processBatch(0);
2025-03-08 12:54:03 +00:00
// Apply display classes
2025-03-12 04:53:52 +00:00
container.classList.add('compact-mode'); // Always use compact mode
// container.classList.toggle('expanded-mode', displayMode === 'expanded'); // Removed
2025-03-08 12:54:03 +00:00
container.classList.toggle('minimal-style', styleMode === 'minimal');
container.classList.toggle('fancy-style', styleMode === 'fancy');
container.classList.toggle('neobrutalist-style', styleMode === 'neobrutalist');
const shouldApplyBiggerText = this.plugin.settings.biggerText &&
2025-03-08 18:21:42 +00:00
(!this.plugin.settings.biggerTextMobileOnly || (this.app as any).isMobile);
container.classList.toggle('bigger-text', shouldApplyBiggerText);
2025-03-08 17:41:17 +00:00
2025-03-08 12:54:03 +00:00
}
// Helper function to create file links with consistent styling
createFileLink(container: HTMLElement, file: any, currentFilePath: string, parentFolderPath: string): void {
const p = container.createEl('p');
const a = p.createEl('a', { text: file.basename });
2025-03-08 12:54:03 +00:00
if (this.plugin.settings.prettyTitleCase) {
a.innerText = a.innerText.replace(/\w\S*/g, function(txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
});
}
2025-03-08 12:54:03 +00:00
// Apply styling
a.className = 'folder-notes-style';
2025-03-08 12:54:03 +00:00
// If current file, add special styling
if (file.path === currentFilePath) {
a.className += ' current-file';
a.innerText = '\u2605 ' + a.innerText;
}
2025-03-08 12:54:03 +00:00
// Add hover effects
a.onmouseover = () => a.classList.add('hover-style-file');
a.onmouseout = () => a.classList.remove('hover-style-file');
2025-03-08 12:54:03 +00:00
// Add click handler to open the file
a.addEventListener('click', () => {
2025-05-17 11:28:51 +00:00
// Use the full path instead of just basename to handle duplicate file names
this.app.workspace.openLinkText(file.path, '');
2025-03-08 12:54:03 +00:00
});
}
2025-03-08 12:54:03 +00:00
// Helper method to show empty state
2025-03-08 17:41:17 +00:00
private showEmptyState(container: HTMLElement, activeFile: TFile | null, currentFilePath: string, files: TFile[]): void {
2025-03-08 12:54:03 +00:00
const emptyStateDiv = container.createDiv({ cls: 'empty-state-message' });
2025-03-08 17:41:17 +00:00
if (files.length === 0) {
emptyStateDiv.createEl('p', {
2025-03-08 17:41:17 +00:00
text: 'No notes found. If you have recently added or moved notes, Obsidian might still be indexing them. Please wait a moment and try again.',
cls: 'empty-state-highlight'
});
}
2025-03-08 12:54:03 +00:00
if (this.plugin.settings.includeTitleFilter) {
emptyStateDiv.createEl('p', {
2025-03-08 12:54:03 +00:00
text: `🔍 No notes match the current filter "${this.plugin.settings.includeTitleFilter}".`,
cls: 'empty-state-highlight'
});
} else if (this.plugin.settings.excludeTitlesFilter) {
emptyStateDiv.createEl('p', {
2025-03-08 12:54:03 +00:00
text: `All notes are currently filtered out by the exclude pattern "${this.plugin.settings.excludeTitlesFilter}"`,
});
} else {
emptyStateDiv.createEl('p', {
2025-03-08 12:54:03 +00:00
text: 'This folder is empty',
});
}
2025-03-08 12:54:03 +00:00
if (currentFilePath && activeFile) {
emptyStateDiv.createEl('p', {
text: 'Showing outline of current note:',
cls: 'empty-state-subtext'
});
2025-03-12 04:53:52 +00:00
// Add a loading indicator
const loadingEl = emptyStateDiv.createEl('div', {
cls: 'loading-indicator',
text: 'Loading outline...'
2025-03-08 12:54:03 +00:00
});
2025-03-12 04:53:52 +00:00
// Load content asynchronously with timeout to prevent blocking UI
setTimeout(async () => {
try {
const fileContent = await this.app.vault.read(activeFile);
if (fileContent) {
// Remove loading indicator
loadingEl.remove();
// Create the headings with a limited subset of content if it's large
const contentSize = fileContent.length;
if (contentSize > 100000) { // If content is very large (>100KB)
const truncatedContent = fileContent.substring(0, 100000);
this.createClickableHeadings(container as HTMLElement, truncatedContent, currentFilePath, true);
container.createEl('p', {
text: 'Note is very large. Only showing first portion of headings.',
cls: 'empty-state-subtext'
});
} else {
this.createClickableHeadings(container as HTMLElement, fileContent, currentFilePath, true);
}
} else {
loadingEl.setText('No content found');
}
} catch (err) {
console.error("Error loading outline:", err);
loadingEl.setText("Failed to load outline");
}
}, 10);
2025-03-08 12:54:03 +00:00
}
}
2025-03-08 12:54:03 +00:00
private applyFilters(files: TFile[], parentFolderPath: string): TFile[] {
2025-03-12 04:53:52 +00:00
// Create a single-pass filter function to avoid multiple array iterations
return files.filter(file => {
// Skip files in subfolders if that setting is disabled
if (!this.plugin.settings.includeSubfolderNotes &&
file.path.substring(parentFolderPath.length + 1).includes('/')) {
return false;
}
const lowerBasename = file.basename.toLowerCase();
2025-03-12 04:53:52 +00:00
// Check exclude filter
const excludeFilter = this.plugin.settings.excludeTitlesFilter;
if (excludeFilter && excludeFilter.length > 0) {
const excludeWords = excludeFilter.split(/[,\s]+/).filter(word => word.trim().length > 0)
.map(word => word.trim().toLowerCase());
if (excludeWords.length > 0 &&
excludeWords.some(word => lowerBasename.includes(word))) {
return false;
}
}
2025-03-12 04:53:52 +00:00
// Check include filter
const includesFilter = this.plugin.settings.includeTitleFilter;
if (includesFilter && includesFilter.length > 0) {
const includeWords = includesFilter.split(/[,\s]+/).filter(word => word.trim().length > 0)
.map(word => word.trim().toLowerCase());
if (includeWords.length > 0 &&
!includeWords.some(word => lowerBasename.includes(word))) {
return false;
}
}
2025-03-12 04:53:52 +00:00
return true;
});
2022-04-15 18:13:31 +00:00
}
}
2024-03-23 02:57:56 +00:00
class CurrentFolderNotesDisplaySettingTab extends PluginSettingTab {
plugin: CurrentFolderNotesDisplay;
2022-04-15 18:13:31 +00:00
2024-03-23 02:57:56 +00:00
constructor(app: App, plugin: CurrentFolderNotesDisplay) {
2022-04-15 18:13:31 +00:00
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
containerEl.empty();
2025-03-08 12:54:03 +00:00
// Add some CSS for styling the settings
containerEl.createEl('style', {
text: `
.settings-section {
margin-bottom: 24px;
border-bottom: 1px solid var(--background-modifier-border);
padding-bottom: 16px;
}
.settings-section-header {
margin-bottom: 12px;
font-size: 16px;
font-weight: 600;
color: var(--text-normal);
}
.settings-section-description {
margin-bottom: 12px;
font-size: 13px;
color: var(--text-muted);
}
.setting-item {
border-top: none !important;
padding-top: 12px !important;
}
.setting-item-description {
font-size: 12px !important;
}
`
});
2022-04-15 18:13:31 +00:00
2025-03-08 12:54:03 +00:00
// Title section with plugin description
const titleEl = containerEl.createDiv({ cls: 'settings-section' });
titleEl.createEl('h2', { text: 'Current Folder Notes Settings' });
titleEl.createEl('p', {
2025-03-08 12:54:03 +00:00
cls: 'settings-section-description',
text: 'Configure how notes from the current folder are displayed in the panel.'
});
2025-03-08 12:54:03 +00:00
// Filter settings section
const filtersEl = containerEl.createDiv({ cls: 'settings-section' });
filtersEl.createEl('h3', { cls: 'settings-section-header', text: '📂 Filter Settings' });
filtersEl.createEl('p', {
2025-03-08 12:54:03 +00:00
cls: 'settings-section-description',
text: 'Control which notes appear in the folder notes panel.'
});
2025-03-08 12:54:03 +00:00
new Setting(filtersEl)
.setName('Exclude titles filter')
2025-03-08 12:54:03 +00:00
.setDesc('Notes containing these words will be hidden. Separate multiple words with commas.')
2022-04-15 18:13:31 +00:00
.addText(text => text
2025-03-08 12:54:03 +00:00
.setPlaceholder('_index, draft, template')
.setValue(this.plugin.settings.excludeTitlesFilter)
.onChange(async (value) => {
this.plugin.settings.excludeTitlesFilter = value;
await this.plugin.saveSettings();
}));
2025-03-08 12:54:03 +00:00
new Setting(filtersEl)
.setName('Include titles filter')
.setDesc('Only notes containing these words will be shown. Leave empty to show all notes. Separate multiple words with commas.')
.addText(text => text
2025-03-08 12:54:03 +00:00
.setPlaceholder('chapter, section, lesson')
.setValue(this.plugin.settings.includeTitleFilter)
2022-04-15 18:13:31 +00:00
.onChange(async (value) => {
this.plugin.settings.includeTitleFilter = value;
2022-04-15 18:13:31 +00:00
await this.plugin.saveSettings();
}));
2025-03-08 12:54:03 +00:00
// Display options section
const displayEl = containerEl.createDiv({ cls: 'settings-section' });
displayEl.createEl('h3', { cls: 'settings-section-header', text: '🖥️ Display Options' });
displayEl.createEl('p', {
2025-03-08 12:54:03 +00:00
cls: 'settings-section-description',
text: 'Control how notes and their contents are displayed in the panel.'
});
new Setting(displayEl)
.setName('Show navigation')
.setDesc('Show navigation links for previous and next notes.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.showNavigation)
.onChange(async (value) => {
this.plugin.settings.showNavigation = value;
await this.plugin.saveSettings();
this.plugin.refreshView();
}));
2025-03-08 12:54:03 +00:00
new Setting(displayEl)
.setName('Pretty title case')
2025-03-08 12:54:03 +00:00
.setDesc('Convert note titles to Title Case for better readability.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.prettyTitleCase)
.onChange(async (value) => {
this.plugin.settings.prettyTitleCase = value;
await this.plugin.saveSettings();
}));
2025-03-08 12:54:03 +00:00
new Setting(displayEl)
.setName('Style mode')
.setDesc('Choose between minimal, fancy, and neobrutalist styles.')
2025-03-08 12:54:03 +00:00
.addDropdown(dropdown => dropdown
.addOption('minimal', 'Minimal')
.addOption('fancy', 'Fancy')
.addOption('neobrutalist', 'Neobrutalist')
2025-03-08 12:54:03 +00:00
.setValue(this.plugin.settings.styleMode)
.onChange(async (value) => {
this.plugin.settings.styleMode = value as 'minimal' | 'fancy' | 'neobrutalist';
2025-03-08 12:54:03 +00:00
await this.plugin.saveSettings();
this.plugin.refreshView();
2025-03-08 12:54:03 +00:00
}));
2025-03-08 18:21:42 +00:00
new Setting(displayEl)
.setName('Bigger text')
.setDesc('Make all the text bigger for easier clicking.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.biggerText)
.onChange(async (value) => {
this.plugin.settings.biggerText = value;
await this.plugin.saveSettings();
this.plugin.refreshView();
}));
// Add this after the existing biggerText setting
new Setting(displayEl)
.setName('Mobile-only bigger text')
.setDesc('Only apply bigger text when using Obsidian on mobile devices.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.biggerTextMobileOnly)
.onChange(async (value) => {
this.plugin.settings.biggerTextMobileOnly = value;
await this.plugin.saveSettings();
this.plugin.refreshView();
}));
2025-03-08 12:54:03 +00:00
// Content options section
const contentEl = containerEl.createDiv({ cls: 'settings-section' });
contentEl.createEl('h3', { cls: 'settings-section-header', text: '📝 Content Options' });
contentEl.createEl('p', {
2025-03-08 12:54:03 +00:00
cls: 'settings-section-description',
text: 'Configure what content is included in the folder notes panel.'
});
2025-03-08 12:54:03 +00:00
new Setting(contentEl)
.setName('Include subfolder notes')
2025-03-08 12:54:03 +00:00
.setDesc('Show notes from subfolders within the current folder.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.includeSubfolderNotes)
.onChange(async (value) => {
this.plugin.settings.includeSubfolderNotes = value;
await this.plugin.saveSettings();
}));
new Setting(contentEl)
.setName('Show outline in current file section')
.setDesc('Display headings from the currently active file at the top of the panel.')
.addToggle(toggle => toggle
2025-03-08 17:41:17 +00:00
.setValue(this.plugin.settings.includeCurrentFileOutline)
.onChange(async (value) => {
2025-03-08 17:41:17 +00:00
this.plugin.settings.includeCurrentFileOutline = value;
await this.plugin.saveSettings();
}));
2025-03-08 12:54:03 +00:00
new Setting(contentEl)
2025-03-08 17:41:17 +00:00
.setName('Show outline in files list')
.setDesc('Display headings under each file in the folder notes list.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.includeListFileOutlines)
.onChange(async (value) => {
this.plugin.settings.includeListFileOutlines = value;
await this.plugin.saveSettings();
}));
2025-05-17 11:18:12 +00:00
new Setting(contentEl)
.setName('Maximum files to display')
.setDesc('Limit the number of files shown in the folder notes list. Use a lower value for better performance in large folders.')
.addSlider(slider => slider
.setLimits(10, 500, 10)
.setValue(this.plugin.settings.maxFilesDisplay)
.setDynamicTooltip()
.onChange(async (value) => {
this.plugin.settings.maxFilesDisplay = value;
await this.plugin.saveSettings();
this.plugin.refreshView();
}));
2025-03-08 12:54:03 +00:00
// About section with help text
const aboutEl = containerEl.createDiv({ cls: 'settings-section' });
aboutEl.createEl('h3', { cls: 'settings-section-header', text: ' About' });
aboutEl.createEl('p', {
2025-03-08 12:54:03 +00:00
cls: 'settings-section-description',
text: 'Current Folder Notes displays notes and outlines from the current folder for quick navigation. Changes to settings will apply immediately.'
});
2025-03-08 12:54:03 +00:00
// Add a refresh button to explicitly refresh the view
new Setting(aboutEl)
.setName('Refresh panel')
.setDesc('Manually refresh the Current Folder Notes panel to apply changes.')
.addButton(button => button
.setButtonText('Refresh Now')
.setCta()
.onClick(() => {
this.plugin.refreshView();
new Notice('Current Folder Notes panel refreshed');
}));
2022-04-15 18:13:31 +00:00
}
}
2025-03-08 18:21:42 +00:00