diff --git a/README.md b/README.md index ed99bc8..a8c072d 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ It showcases a list of your calendar events. When you click on an event, it crea In the plugin settings, you can customize: - Note location in your vault +- **Folder path template** for organizing notes in date-based subfolders - Note title format - Default template with variables like: - {{event_title}} @@ -68,6 +69,39 @@ In the plugin settings, you can customize: - {{location}} - {{source}} +### Organizing Notes with Folder Templates + +MemoChron supports flexible folder organization using customizable templates. You can organize your event notes into date-based subfolders automatically. + +**Available Template Variables:** +- `{YYYY}` - 4-digit year (2025) +- `{YY}` - 2-digit year (25) +- `{MM}` - 2-digit month (06) +- `{M}` - 1-digit month (6) +- `{MMM}` - 3-letter month abbreviation (Jun) +- `{MMMM}` - Full month name (June) +- `{DD}` - 2-digit day (15) +- `{D}` - 1-digit day (15) +- `{DDD}` - 3-letter day abbreviation (Mon) +- `{DDDD}` - Full day name (Monday) +- `{Q}` - Quarter number (2) +- `{source}` - Calendar source name +- `{event_title}` - Event title (sanitized for file names) + +**Example Templates:** +- `{YYYY}/{MMM}` → `2025/Jun/` +- `{YYYY}-{MM}` → `2025-06/` +- `{source}/{YYYY}/{MMM}` → `Work Calendar/2025/Jun/` +- `{YYYY}/Q{Q}` → `2025/Q2/` +- `{MMM} {YYYY}` → `Jun 2025/` + +**How to Use:** +1. Go to Settings > MemoChron +2. Find the "Folder path template" setting +3. Enter your desired template pattern +4. Use the live preview to see how your template will look +5. Leave empty to save all notes in the same folder (default behavior) + ## Configuration ### Settings @@ -77,6 +111,7 @@ In the plugin settings, you can customize: - **Hide Calendar**: Show only the agenda view without the month calendar grid - **Refresh Interval**: Set how often calendar data updates - **Note Location**: Set the default folder for event notes +- **Folder Path Template**: Organize notes in date-based subfolders with customizable patterns - **Note Title Format**: Customize how note titles are generated - **Template**: Customize the default note template - **Tags**: Set default tags for event notes diff --git a/src/services/NoteService.ts b/src/services/NoteService.ts index ae59192..be0157b 100644 --- a/src/services/NoteService.ts +++ b/src/services/NoteService.ts @@ -14,6 +14,22 @@ interface EventTemplateVariables { description: string; } +interface FolderTemplateVariables { + YYYY: string; + YY: string; + MM: string; + M: string; + MMM: string; + MMMM: string; + DD: string; + D: string; + DDD: string; + DDDD: string; + Q: string; + source: string; + event_title: string; +} + export class NoteService { private static readonly NOTES_SECTION_MARKER = "## 📝 Notes"; private static readonly FRONTMATTER_DELIMITER = "---"; @@ -63,10 +79,18 @@ export class NoteService { } private buildFilePath(event: CalendarEvent): string { - const { noteLocation, noteTitleFormat } = this.settings; + const { noteLocation, noteTitleFormat, folderPathTemplate } = this.settings; const normalizedPath = normalizePath(noteLocation); const title = this.formatTitle(noteTitleFormat, event); - return normalizePath(`${normalizedPath}/${title}.md`); + + // If folderPathTemplate is empty, use the old behavior + if (!folderPathTemplate.trim()) { + return normalizePath(`${normalizedPath}/${title}.md`); + } + + // Apply folder template to create subfolder structure + const subfolderPath = this.applyFolderTemplate(folderPathTemplate, event); + return normalizePath(`${normalizedPath}/${subfolderPath}/${title}.md`); } private async ensureParentFolder(filePath: string): Promise { @@ -298,6 +322,56 @@ export class NoteService { } } + private applyFolderTemplate(template: string, event: CalendarEvent): string { + const variables = this.getFolderTemplateVariables(event); + return this.parseFolderTemplate(template, variables); + } + + private getFolderTemplateVariables(event: CalendarEvent): FolderTemplateVariables { + const date = event.start; + const monthNames = [ + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + ]; + const monthAbbreviations = [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" + ]; + const dayNames = [ + "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" + ]; + const dayAbbreviations = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + + const year = date.getFullYear(); + const month = date.getMonth(); + const day = date.getDate(); + const dayOfWeek = date.getDay(); + const quarter = Math.floor(month / 3) + 1; + + return { + YYYY: year.toString(), + YY: year.toString().slice(-2), + MM: (month + 1).toString().padStart(2, "0"), + M: (month + 1).toString(), + MMM: monthAbbreviations[month], + MMMM: monthNames[month], + DD: day.toString().padStart(2, "0"), + D: day.toString(), + DDD: dayAbbreviations[dayOfWeek], + DDDD: dayNames[dayOfWeek], + Q: quarter.toString(), + source: this.sanitizeFileName(event.source), + event_title: this.sanitizeFileName(event.title), + }; + } + + private parseFolderTemplate(template: string, variables: FolderTemplateVariables): string { + return Object.entries(variables).reduce((result, [key, value]) => { + const pattern = new RegExp(`\\{${key}\\}`, "g"); + return result.replace(pattern, value); + }, template); + } + private sanitizeFileName(str: string): string { return str.replace(/[\\/:*?"<>|]/g, "-"); } diff --git a/src/settings/SettingsTab.ts b/src/settings/SettingsTab.ts index c264f92..863ae02 100644 --- a/src/settings/SettingsTab.ts +++ b/src/settings/SettingsTab.ts @@ -52,6 +52,7 @@ export class SettingsTab extends PluginSettingTab { new Setting(this.containerEl).setName("Notes").setHeading(); this.renderNoteLocation(); + this.renderFolderPathTemplate(); this.renderNoteTitleFormat(); this.renderNoteDateFormat(); this.renderDefaultFrontmatter(); @@ -318,6 +319,122 @@ export class SettingsTab extends PluginSettingTab { }); } + private renderFolderPathTemplate(): void { + const templateSetting = new Setting(this.containerEl) + .setName("Folder path template") + .setDesc("Organize notes in date-based subfolders. Leave empty to save all notes in the same folder."); + + templateSetting.descEl.createEl("br"); + templateSetting.descEl.createEl("small", { + text: "Available variables: {YYYY}, {YY}, {MM}, {M}, {MMM}, {MMMM}, {DD}, {D}, {DDD}, {DDDD}, {Q}, {source}, {event_title}" + }); + templateSetting.descEl.createEl("br"); + templateSetting.descEl.createEl("small", { + text: "Examples: {YYYY}/{MM}, {YYYY}-{MMM}, {source}/{YYYY}/{MMM}" + }); + + templateSetting.addText((text) => + text + .setPlaceholder("{YYYY}/{MMM}") + .setValue(this.plugin.settings.folderPathTemplate) + .onChange(async (value) => { + this.plugin.settings.folderPathTemplate = value; + await this.plugin.saveSettings(); + }) + ); + + // Add preview container + const previewContainer = templateSetting.controlEl.createDiv({ + cls: "memochron-template-preview", + }); + this.updateTemplatePreview(previewContainer, this.plugin.settings.folderPathTemplate); + + // Update preview when input changes + const textInput = templateSetting.controlEl.querySelector('input') as HTMLInputElement; + if (textInput) { + textInput.addEventListener('input', () => { + this.updateTemplatePreview(previewContainer, textInput.value); + }); + } + } + + private updateTemplatePreview(container: HTMLElement, template: string): void { + container.empty(); + + if (!template.trim()) { + container.createEl("small", { + text: "Preview: Notes will be saved directly in the note location folder", + cls: "memochron-preview-text" + }); + return; + } + + // Create a sample date for preview + const sampleDate = new Date(); + const sampleEvent = { + title: "Sample Meeting", + start: sampleDate, + end: sampleDate, + source: "Work Calendar" + }; + + try { + const previewPath = this.generatePreviewPath(template, sampleEvent); + container.createEl("small", { + text: `Preview: ${previewPath}/`, + cls: "memochron-preview-text" + }); + } catch (error) { + container.createEl("small", { + text: "Invalid template format", + cls: "memochron-preview-error" + }); + } + } + + private generatePreviewPath(template: string, event: any): string { + const monthNames = [ + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + ]; + const monthAbbreviations = [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" + ]; + const dayNames = [ + "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" + ]; + const dayAbbreviations = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + + const date = event.start; + const year = date.getFullYear(); + const month = date.getMonth(); + const day = date.getDate(); + const dayOfWeek = date.getDay(); + const quarter = Math.floor(month / 3) + 1; + + const variables = { + YYYY: year.toString(), + YY: year.toString().slice(-2), + MM: (month + 1).toString().padStart(2, "0"), + M: (month + 1).toString(), + MMM: monthAbbreviations[month], + MMMM: monthNames[month], + DD: day.toString().padStart(2, "0"), + D: day.toString(), + DDD: dayAbbreviations[dayOfWeek], + DDDD: dayNames[dayOfWeek], + Q: quarter.toString(), + source: event.source.replace(/[\\/:*?"<>|]/g, "-"), + event_title: event.title.replace(/[\\/:*?"<>|]/g, "-"), + }; + + return Object.entries(variables).reduce((result, [key, value]) => { + const pattern = new RegExp(`\\{${key}\\}`, "g"); + return result.replace(pattern, value); + }, template); + } + private renderDefaultTags(): void { new Setting(this.containerEl) .setName("Default tags") diff --git a/src/settings/types.ts b/src/settings/types.ts index 7e9d882..7d2c274 100644 --- a/src/settings/types.ts +++ b/src/settings/types.ts @@ -29,6 +29,7 @@ export interface MemoChronSettings { noteTemplate: string; firstDayOfWeek: number; // 0 = Sunday, 1 = Monday, etc. hideCalendar: boolean; + folderPathTemplate: string; // Template for organizing notes in date-based subfolders } export const DEFAULT_SETTINGS: MemoChronSettings = { @@ -54,4 +55,5 @@ export const DEFAULT_SETTINGS: MemoChronSettings = { `, firstDayOfWeek: DEFAULT_FIRST_DAY_OF_WEEK, hideCalendar: false, + folderPathTemplate: "", // Empty by default for backwards compatibility }; diff --git a/styles.css b/styles.css index c982d20..88143e3 100644 --- a/styles.css +++ b/styles.css @@ -334,6 +334,21 @@ background: var(--background-modifier-hover); } +/* Template Preview */ +.memochron-template-preview { + margin-top: var(--memochron-padding-xs); +} + +.memochron-preview-text { + color: var(--text-muted); + font-style: italic; +} + +.memochron-preview-error { + color: var(--text-error); + font-style: italic; +} + /* Mobile Specific Styles */ @media screen and (max-width: 768px) { .memochron-calendar-grid {