caffa_Obsidian-Current-Fold.../main.ts
2025-06-12 23:53:03 +08:00

959 lines
32 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { App, Notice, Plugin, PluginSettingTab, Setting, TFile } from 'obsidian';
import { ItemView, WorkspaceLeaf } from "obsidian";
import { MarkdownView } from "obsidian";
import { TFolder } from "obsidian";
interface CurrentFolderNotesDisplaySettings {
excludeTitlesFilter: string;
includeTitleFilter: string;
prettyTitleCase: boolean;
includeSubfolderNotes: boolean;
includeCurrentFileOutline: boolean;
includeListFileOutlines: boolean;
styleMode: 'minimal' | 'fancy' | 'neobrutalist';
showNavigation: boolean;
biggerText: boolean; // Add this line
biggerTextMobileOnly: boolean; // Add this new setting
maxFilesDisplay: number; // Add new setting for max files to display
}
const DEFAULT_SETTINGS: Partial<CurrentFolderNotesDisplaySettings> = {
excludeTitlesFilter: '_index',
includeTitleFilter: '',
prettyTitleCase: true,
includeSubfolderNotes: false,
includeCurrentFileOutline: true,
includeListFileOutlines: false,
styleMode: 'fancy',
showNavigation: false,
biggerText: false,
biggerTextMobileOnly: false,
maxFilesDisplay: 100 // Default to showing 100 files
}
export default class CurrentFolderNotesDisplay extends Plugin {
settings: CurrentFolderNotesDisplaySettings;
public leaves: WorkspaceLeaf[] = [];
fileChangeHandler(file: TFile) {
if (file instanceof TFile) {
this.load();
}
}
async onload() {
await this.loadSettings();
// Load custom styles
this.loadStyles();
// 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));
// Add a ribbon icon
this.addRibbonIcon('folder', 'Activate folder notes display', () => {
this.activateView();
});
// Add a command to open the view
this.addCommand({
id: 'activate-folder-notes-display',
name: 'Open Pane',
callback: () => {
this.activateView();
}
});
// 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));
}
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 = [];
}
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();
}
}
let leaf: WorkspaceLeaf | null = null;
if (existingLeaves.length) {
// A leaf with our view already exists, use that
leaf = existingLeaves[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 });
// Only add to leaves array if it's a new leaf
this.leaves.push(leaf);
}
}
if (!leaf) {
new Notice('Could not create a new leaf for the view');
return;
}
workspace.revealLeaf(leaf);
}
async refreshView() {
const { workspace } = this.app;
// console.log("[CFN] Refresh view triggered");
const leaves = workspace.getLeavesOfType(VIEW_TYPE_CURRENT_FOLDER_NOTES_DISPLAY);
// console.log("[CFN] Found leaves:", leaves.length);
if (leaves.length > 1) {
// console.log("[CFN] Multiple leaves found, cleaning up extras");
for (let i = 1; i < leaves.length; i++) {
leaves[i].detach();
}
}
if (leaves.length === 1) {
// console.log("[CFN] Updating existing view");
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) {
// console.log("[CFN] No view found, creating new one");
this.activateView();
}
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
// 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);
// Register the stylesheet to be removed when the plugin unloads
this.register(() => styleEl.remove());
// 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;
});
}
}
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 "Current Folder Notes";
}
getIcon(): string {
return 'folder';
}
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 {
// 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.'
});
}
// 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');
}
p.addEventListener('click', (event) => {
// Prevent default behavior
event.preventDefault();
// Use the openLinkText method to navigate to the heading
this.app.workspace.openLinkText('#' + headingText, currentFilePath);
// Clear any text selection
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
}
});
// 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');
}
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;
}
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]}`;
// Try to find Y numbers next (e.g., Y1, Y2023)
const yMatch = basename.match(/Y(\d+)/);
if (yMatch) return `Y${yMatch[1]}`;
// Try to find any numbers
const numberMatch = basename.match(/\d+/);
if (numberMatch) return numberMatch[0];
// Try to find text after dash
const dashMatch = basename.match(/-\s*(.+)$/);
if (dashMatch) return dashMatch[1];
// If nothing else matches, return the basename
return basename;
}
async displayNotesInCurrentFolder(): Promise<void> {
// console.log("[CFN] Starting displayNotesInCurrentFolder");
const container = this.containerEl.children[1] as HTMLElement;
container.empty();
// Get display settings
// const displayMode = this.plugin.settings.displayMode; // Removed
const styleMode = this.plugin.settings.styleMode;
const showNavigation = this.plugin.settings.showNavigation;
// Create header
const headerContainer = container.createDiv({ cls: 'folder-view-header' });
headerContainer.createEl("h6", { text: "Current Folder Notes" });
// Get current file info and folder path
const activeFile = this.app.workspace.getActiveFile();
// console.log("[CFN] Active file:", activeFile?.path);
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) : '';
// console.log("[CFN] Parent folder path from current file:", parentFolderPath);
} else if (this.app.workspace.getActiveViewOfType(MarkdownView)) {
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
// 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) : '';
// 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'
});
return;
}
// 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';
// Get and filter files
let folder = this.app.vault.getAbstractFileByPath(parentFolderPath);
// console.log("[CFN] Found folder:", folder?.path);
let files: TFile[] = [];
if (folder instanceof TFolder) {
files = folder.children.filter((file: any) => file instanceof TFile) as TFile[];
// console.log("[CFN] Initial files count:", files.length);
}
files = this.applyFilters(files, parentFolderPath);
// console.log("[CFN] Files after filtering:", files.length);
// console.log("[CFN] Current filters - Include:", this.plugin.settings.includeTitleFilter, "Exclude:", this.plugin.settings.excludeTitlesFilter);
if (files.length === 0) {
this.showEmptyState(container, activeFile, currentFilePath, files);
return;
}
// Sort files by name/sequence
const sequenceWithPrefixOrLongest = (str: string) => {
const tMatches = str.match(/T(\d+)/);
if (tMatches) return parseInt(tMatches[1]);
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);
};
files.sort((a, b) => sequenceWithPrefixOrLongest(a.basename) - sequenceWithPrefixOrLongest(b.basename));
const currentFileIndex = files.findIndex(file => file.path === currentFilePath);
// Create main content container
const mainContent = container.createDiv({ cls: 'main-content' });
// Show navigation section if enabled and we have a current file
if (showNavigation && currentFileIndex !== -1) {
const navigationSection = mainContent.createDiv({ cls: 'navigation-section' });
// Navigation header with prev/next
const navHeader = navigationSection.createDiv({ cls: 'navigation-header' });
// 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',
text: this.getShortNoteName(prevNote.basename)
});
prevLink.setAttribute('aria-label', `Previous: ${prevNote.basename}`);
prevLink.addEventListener('click', () => {
// Use the full path to handle duplicate file names
this.app.workspace.openLinkText(prevNote.path, '');
});
}
// 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',
text: this.getShortNoteName(nextNote.basename)
});
nextLink.setAttribute('aria-label', `Next: ${nextNote.basename}`);
nextLink.addEventListener('click', () => {
// Use the full path to handle duplicate file names
this.app.workspace.openLinkText(nextNote.path, '');
});
}
// Current note outline - LAZY LOAD
if (this.plugin.settings.includeCurrentFileOutline) {
const currentFile = files[currentFileIndex];
// Create the outline section first
const outlineSection = navigationSection.createDiv({ cls: 'outline-section' });
// 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);
}
// Add separator
mainContent.createDiv({ cls: 'section-separator' });
}
// Always show flat list view
const listContainer = mainContent.createDiv({ cls: 'notes-flat-list' });
listContainer.createEl('div', {
cls: 'folder-section-header',
text: 'FOLDER NOTES'
});
// OPTIMIZE: Limit the number of files displayed at once to prevent memory issues
const MAX_FILES_DISPLAY = this.plugin.settings.maxFilesDisplay; // Use the setting value
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);
// 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...'
});
// 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");
}
});
}
}
// 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);
// Apply display classes
container.classList.add('compact-mode'); // Always use compact mode
// container.classList.toggle('expanded-mode', displayMode === 'expanded'); // Removed
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 &&
(!this.plugin.settings.biggerTextMobileOnly || (this.app as any).isMobile);
container.classList.toggle('bigger-text', shouldApplyBiggerText);
}
// 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 });
if (this.plugin.settings.prettyTitleCase) {
a.innerText = a.innerText.replace(/\w\S*/g, function(txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
});
}
// Apply styling
a.className = 'folder-notes-style';
// If current file, add special styling
if (file.path === currentFilePath) {
a.className += ' current-file';
a.innerText = '\u2605 ' + a.innerText;
}
// Add hover effects
a.onmouseover = () => a.classList.add('hover-style-file');
a.onmouseout = () => a.classList.remove('hover-style-file');
// Add click handler to open the file
a.addEventListener('click', () => {
// Use the full path instead of just basename to handle duplicate file names
this.app.workspace.openLinkText(file.path, '');
});
}
// Helper method to show empty state
private showEmptyState(container: HTMLElement, activeFile: TFile | null, currentFilePath: string, files: TFile[]): void {
const emptyStateDiv = container.createDiv({ cls: 'empty-state-message' });
if (files.length === 0) {
emptyStateDiv.createEl('p', {
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'
});
}
if (this.plugin.settings.includeTitleFilter) {
emptyStateDiv.createEl('p', {
text: `🔍 No notes match the current filter "${this.plugin.settings.includeTitleFilter}".`,
cls: 'empty-state-highlight'
});
} else if (this.plugin.settings.excludeTitlesFilter) {
emptyStateDiv.createEl('p', {
text: `All notes are currently filtered out by the exclude pattern "${this.plugin.settings.excludeTitlesFilter}"`,
});
} else {
emptyStateDiv.createEl('p', {
text: 'This folder is empty',
});
}
if (currentFilePath && activeFile) {
emptyStateDiv.createEl('p', {
text: 'Showing outline of current note:',
cls: 'empty-state-subtext'
});
// Add a loading indicator
const loadingEl = emptyStateDiv.createEl('div', {
cls: 'loading-indicator',
text: 'Loading outline...'
});
// 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);
}
}
private applyFilters(files: TFile[], parentFolderPath: string): TFile[] {
// 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();
// 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;
}
}
// 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;
}
}
return true;
});
}
}
class CurrentFolderNotesDisplaySettingTab extends PluginSettingTab {
plugin: CurrentFolderNotesDisplay;
constructor(app: App, plugin: CurrentFolderNotesDisplay) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
containerEl.empty();
// 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;
}
`
});
// Title section with plugin description
const titleEl = containerEl.createDiv({ cls: 'settings-section' });
titleEl.createEl('h2', { text: 'Current Folder Notes Settings' });
titleEl.createEl('p', {
cls: 'settings-section-description',
text: 'Configure how notes from the current folder are displayed in the panel.'
});
// Filter settings section
const filtersEl = containerEl.createDiv({ cls: 'settings-section' });
filtersEl.createEl('h3', { cls: 'settings-section-header', text: '📂 Filter Settings' });
filtersEl.createEl('p', {
cls: 'settings-section-description',
text: 'Control which notes appear in the folder notes panel.'
});
new Setting(filtersEl)
.setName('Exclude titles filter')
.setDesc('Notes containing these words will be hidden. Separate multiple words with commas.')
.addText(text => text
.setPlaceholder('_index, draft, template')
.setValue(this.plugin.settings.excludeTitlesFilter)
.onChange(async (value) => {
this.plugin.settings.excludeTitlesFilter = value;
await this.plugin.saveSettings();
}));
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
.setPlaceholder('chapter, section, lesson')
.setValue(this.plugin.settings.includeTitleFilter)
.onChange(async (value) => {
this.plugin.settings.includeTitleFilter = value;
await this.plugin.saveSettings();
}));
// Display options section
const displayEl = containerEl.createDiv({ cls: 'settings-section' });
displayEl.createEl('h3', { cls: 'settings-section-header', text: '🖥️ Display Options' });
displayEl.createEl('p', {
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();
}));
new Setting(displayEl)
.setName('Pretty title case')
.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();
}));
new Setting(displayEl)
.setName('Style mode')
.setDesc('Choose between minimal, fancy, and neobrutalist styles.')
.addDropdown(dropdown => dropdown
.addOption('minimal', 'Minimal')
.addOption('fancy', 'Fancy')
.addOption('neobrutalist', 'Neobrutalist')
.setValue(this.plugin.settings.styleMode)
.onChange(async (value) => {
this.plugin.settings.styleMode = value as 'minimal' | 'fancy' | 'neobrutalist';
await this.plugin.saveSettings();
this.plugin.refreshView();
}));
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();
}));
// Content options section
const contentEl = containerEl.createDiv({ cls: 'settings-section' });
contentEl.createEl('h3', { cls: 'settings-section-header', text: '📝 Content Options' });
contentEl.createEl('p', {
cls: 'settings-section-description',
text: 'Configure what content is included in the folder notes panel.'
});
new Setting(contentEl)
.setName('Include subfolder notes')
.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
.setValue(this.plugin.settings.includeCurrentFileOutline)
.onChange(async (value) => {
this.plugin.settings.includeCurrentFileOutline = value;
await this.plugin.saveSettings();
}));
new Setting(contentEl)
.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();
}));
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();
}));
// About section with help text
const aboutEl = containerEl.createDiv({ cls: 'settings-section' });
aboutEl.createEl('h3', { cls: 'settings-section-header', text: ' About' });
aboutEl.createEl('p', {
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.'
});
// 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');
}));
}
}