6 Commits

Author SHA1 Message Date
olivier 3c7b6741f5 fix: calendar disappeared for existing users
Shallow Object.assign on settings meant filterTags (new in 1.5.0) was
undefined for anyone with prior saved settings. Accessing .length on
undefined threw, killing the entire redraw before calendar could render.

- Defensive fallback: filterTags || []
- Deep merge on loadSettings for recentFiles, calendar, display
2026-06-22 10:18:37 -04:00
olivier abf4d6ccf7 feat: configurable filter tags for recent files
New 'Filter tags' setting in Recent Files tab. Enter tag names one per line
to pin specific tags as filter pills. Leave empty to auto-detect from
frontmatter 'type' property (existing behavior).
2026-06-22 10:15:54 -04:00
olivier 141121878f feat: add About tab to settings with version and plugin description 2026-06-22 10:07:22 -04:00
olivier 6a89660cf4 release v1.4.1 2026-06-22 10:01:47 -04:00
olivier 44c2dd2abc fix: enforce recent files limit when maxItems setting changes
The limit was only applied on file-open and plugin load, not when the user
changed the maxItems setting in the settings tab. Now enforceRecentFilesLimit()
is called on every settings change via the onSettingsChange callback.
2026-06-22 10:00:45 -04:00
olivier 49c8da9863 feat: persistent recent files history + fix limit enforcement + fix settings data loss
- Recent files are now persisted to disk and survive restarts
- Max items limit is applied on load (in case setting was reduced)
- Omitted paths regex filtering is now wired up in addToRecentFiles
- Fixed saveAndRefresh() overwriting entire plugin data on settings change
- Debounced disk writes (300ms) to avoid excessive saves on rapid opens
2026-06-21 22:36:03 -04:00
8 changed files with 182 additions and 30 deletions
+17 -12
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "waypoint-sidebar",
"name": "Waypoint Sidebar",
"version": "1.3.0",
"version": "1.5.1",
"minAppVersion": "0.16.3",
"description": "Calendar, recent files, and custom bookmarks sidebar.",
"author": "Olivier",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "waypoint",
"version": "1.3.0",
"version": "1.5.1",
"description": "Calendar, recent files, and custom bookmarks sidebar.",
"main": "main.js",
"scripts": {
+57 -2
View File
@@ -16,12 +16,14 @@ import { BookmarkItem, WaypointData } from 'src/models/bookmark';
const DEFAULT_DATA: WaypointData = {
bookmarks: [],
recentFiles: [],
};
export default class WaypointPlugin extends Plugin {
public settings: WaypointSettings;
public waypointData: WaypointData;
public recentFiles: { path: string; basename: string }[] = [];
private recentFilesSaveTimer: ReturnType<typeof setTimeout> | null = null;
async onload(): Promise<void> {
console.debug('Waypoint: loading plugin v' + this.manifest.version);
@@ -41,7 +43,10 @@ export default class WaypointPlugin extends Plugin {
this.app,
this,
this.settings,
() => this.redrawAll(),
() => {
this.enforceRecentFilesLimit();
this.redrawAll();
},
));
// ── Commands ──
@@ -194,6 +199,10 @@ export default class WaypointPlugin extends Plugin {
const saved = await this.loadData() as Record<string, unknown> | null;
const s = (saved?.settings || {}) as Partial<WaypointSettings>;
this.settings = Object.assign({}, DEFAULT_SETTINGS, s);
// Deep merge nested objects that might be missing new fields
this.settings.recentFiles = Object.assign({}, DEFAULT_SETTINGS.recentFiles, s.recentFiles || {});
this.settings.calendar = Object.assign({}, DEFAULT_SETTINGS.calendar, s.calendar || {});
this.settings.display = Object.assign({}, DEFAULT_SETTINGS.display, s.display || {});
}
async saveSettings(): Promise<void> {
@@ -206,14 +215,47 @@ export default class WaypointPlugin extends Plugin {
const saved = await this.loadData() as Record<string, unknown> | null;
const d = (saved?.waypointData || {}) as Partial<WaypointData>;
this.waypointData = Object.assign({}, DEFAULT_DATA, d);
// Load persisted recent files
this.recentFiles = this.waypointData.recentFiles || [];
// Apply current limit (in case maxItems was reduced since last save)
if (this.recentFiles.length > this.settings.recentFiles.maxItems) {
this.recentFiles = this.recentFiles.slice(0, this.settings.recentFiles.maxItems);
this.waypointData.recentFiles = this.recentFiles;
}
}
/**
* Trim recent files to the current maxItems limit and persist.
* Called when the maxItems setting changes.
*/
enforceRecentFilesLimit(): void {
if (this.recentFiles.length > this.settings.recentFiles.maxItems) {
this.recentFiles = this.recentFiles.slice(0, this.settings.recentFiles.maxItems);
this.persistRecentFiles();
}
}
async saveWaypointData(): Promise<void> {
// Sync recentFiles into waypointData before saving
this.waypointData.recentFiles = this.recentFiles;
const all = (await this.loadData()) as Record<string, unknown> || {};
all.waypointData = this.waypointData;
await this.saveData(all);
}
/**
* Persist recent files to disk (debounced to avoid excessive writes on rapid opens).
*/
persistRecentFiles(): void {
this.waypointData.recentFiles = this.recentFiles;
if (this.recentFilesSaveTimer) clearTimeout(this.recentFilesSaveTimer);
this.recentFilesSaveTimer = setTimeout(() => {
this.saveWaypointData();
}, 300);
}
// ── Recent Files ──
private onFileOpen(file: TFile): void {
@@ -225,14 +267,26 @@ export default class WaypointPlugin extends Plugin {
}
addToRecentFiles(file: TFile): void {
// Apply omitted paths filter
if (this.settings.recentFiles.omittedPaths.length > 0) {
for (const pattern of this.settings.recentFiles.omittedPaths) {
try {
if (new RegExp(pattern).test(file.path)) return;
} catch {
// Invalid regex, skip
}
}
}
this.recentFiles = this.recentFiles.filter(f => f.path !== file.path);
this.recentFiles.unshift({ path: file.path, basename: file.basename });
// Prune
// Apply max items limit
if (this.recentFiles.length > this.settings.recentFiles.maxItems) {
this.recentFiles = this.recentFiles.slice(0, this.settings.recentFiles.maxItems);
}
this.persistRecentFiles();
this.broadcastRedraw();
}
@@ -241,6 +295,7 @@ export default class WaypointPlugin extends Plugin {
if (entry) {
entry.path = file.path;
entry.basename = (file as TFile).basename || file.name.replace(/\.[^/.]+$/, '');
this.persistRecentFiles();
this.broadcastRedraw();
}
+1
View File
@@ -13,4 +13,5 @@ export interface BookmarkItem {
export interface WaypointData {
bookmarks: BookmarkItem[];
recentFiles: { path: string; basename: string }[];
}
+70 -3
View File
@@ -1,11 +1,11 @@
import { Setting, PluginSettingTab, App, Plugin } from 'obsidian';
import { Setting, PluginSettingTab, App, Plugin, setIcon } from 'obsidian';
import { WaypointSettings, DEFAULT_SETTINGS, PeriodNoteSettings } from 'src/settings';
export class WaypointSettingTab extends PluginSettingTab {
private plugin: Plugin;
private settings: WaypointSettings;
private onSettingsChange: () => void;
private activeTab: 'calendar' | 'periodic' | 'recent' | 'display' = 'calendar';
private activeTab: 'calendar' | 'periodic' | 'recent' | 'display' | 'about' = 'calendar';
constructor(app: App, plugin: Plugin, settings: WaypointSettings, onSettingsChange: () => void) {
super(app, plugin);
@@ -25,6 +25,7 @@ export class WaypointSettingTab extends PluginSettingTab {
{ key: 'periodic' as const, label: 'Periodic Notes' },
{ key: 'recent' as const, label: 'Recent Files' },
{ key: 'display' as const, label: 'Display' },
{ key: 'about' as const, label: 'About' },
];
for (const tab of tabs) {
@@ -53,6 +54,9 @@ export class WaypointSettingTab extends PluginSettingTab {
case 'display':
this.renderDisplayTab(tabContent);
break;
case 'about':
this.renderAboutTab(tabContent);
break;
}
}
@@ -216,6 +220,21 @@ export class WaypointSettingTab extends PluginSettingTab {
this.saveAndRefresh();
};
});
const filterDesc = new DocumentFragment();
filterDesc.appendText('Tags to show as filter pills above the file list. One per line. Leave empty to auto-detect from frontmatter `type` property.');
new Setting(container)
.setName('Filter tags')
.setDesc(filterDesc)
.addTextArea((text) => {
text.inputEl.setAttr('rows', 4);
text.setPlaceholder('meeting\nperson\nproject');
text.setValue(this.settings.recentFiles.filterTags.join('\n'));
text.inputEl.onblur = () => {
this.settings.recentFiles.filterTags = text.getValue().split('\n').filter(t => t.trim());
this.saveAndRefresh();
};
});
}
// ═══════════════════════════════
@@ -294,7 +313,55 @@ export class WaypointSettingTab extends PluginSettingTab {
}
private async saveAndRefresh(): Promise<void> {
await this.plugin.saveData(this.settings);
await (this.plugin as any).saveSettings();
this.onSettingsChange();
}
// ═══════════════════════════════
// About tab
// ═══════════════════════════════
private renderAboutTab(container: HTMLElement): void {
const version = this.plugin.manifest.version;
const header = container.createDiv();
header.style.display = 'flex';
header.style.alignItems = 'center';
header.style.gap = '12px';
header.style.marginBottom = '16px';
const icon = header.createDiv();
icon.style.display = 'flex';
icon.style.alignItems = 'center';
icon.style.justifyContent = 'center';
icon.style.width = '48px';
icon.style.height = '48px';
icon.style.borderRadius = '12px';
icon.style.background = 'var(--interactive-accent)';
icon.style.color = 'var(--text-on-accent)';
icon.style.fontSize = '24px';
setIcon(icon, 'compass');
const titleGroup = header.createDiv();
const title = titleGroup.createEl('h2', { text: 'Waypoint Sidebar' });
title.style.margin = '0';
title.style.lineHeight = '1.2';
const subtitle = titleGroup.createDiv({ text: `v${version}` });
subtitle.style.color = 'var(--text-muted)';
subtitle.style.fontSize = 'var(--font-ui-small)';
const desc = container.createDiv();
desc.style.marginBottom = '20px';
desc.style.lineHeight = '1.6';
desc.style.color = 'var(--text-normal)';
desc.innerHTML = [
`<p><strong>Waypoint</strong> is a sidebar plugin that brings three essential panels into one view:</p>`,
`<ul style="padding-left: 20px; margin: 8px 0;">`,
`<li><strong>Calendar</strong> — a monthly grid for your periodic notes (daily, weekly, monthly, quarterly, yearly). Click any day, week, or month to open or create the corresponding note.</li>`,
`<li><strong>Recent Files</strong> — a list of recently opened files with type filtering, drag-and-drop, and right-click actions.</li>`,
`<li><strong>Bookmarks</strong> — custom bookmarks with icons, groups, nesting, and drag-and-drop reordering. Separate from Obsidian's native bookmarks.</li>`,
`</ul>`,
`<p style="color: var(--text-muted); font-size: var(--font-ui-small);">Made by Olivier. Licensed under MIT.</p>`,
].join('\n');
}
}
+2
View File
@@ -17,6 +17,7 @@ export interface RecentFilesSettings {
updateOn: 'file-open' | 'file-edit';
omittedPaths: string[];
omittedTags: string[];
filterTags: string[]; // tags to show as filter pills (empty = auto-detect from frontmatter)
}
export interface DisplaySettings {
@@ -79,6 +80,7 @@ export const DEFAULT_SETTINGS: WaypointSettings = {
updateOn: 'file-open',
omittedPaths: [],
omittedTags: [],
filterTags: [],
},
display: {
rowSize: 26,
+26 -4
View File
@@ -241,7 +241,26 @@ export class WaypointView extends ItemView {
}
// ── Type filter bar ──
const typeCounts: Record<string, number> = {};
const configuredTags = this.plugin.settings.recentFiles.filterTags || [];
let typeCounts: Record<string, number> = {};
if (configuredTags.length > 0) {
// Use configured tags — count occurrences in recent files
for (const tag of configuredTags) {
typeCounts[tag] = 0;
}
for (const file of this.plugin.recentFiles) {
const tfile = this.app.vault.getAbstractFileByPath(file.path);
if (tfile instanceof TFile) {
const cache = this.app.metadataCache.getFileCache(tfile);
const type = cache?.frontmatter?.type;
if (type && typeof type === 'string' && typeCounts.hasOwnProperty(type)) {
typeCounts[type]++;
}
}
}
} else {
// Auto-detect from frontmatter `type` property
for (const file of this.plugin.recentFiles) {
const tfile = this.app.vault.getAbstractFileByPath(file.path);
if (tfile instanceof TFile) {
@@ -252,6 +271,7 @@ export class WaypointView extends ItemView {
}
}
}
}
if (Object.keys(typeCounts).length > 0) {
const filterBar = section.createDiv({ cls: 'waypoint-recent-filter' });
@@ -262,7 +282,9 @@ export class WaypointView extends ItemView {
this.redraw();
});
// Type pills
const sortedTypes = Object.entries(typeCounts).sort(([,a], [,b]) => b - a);
const sortedTypes = configuredTags.length > 0
? Object.entries(typeCounts) // preserve configured order
: Object.entries(typeCounts).sort(([,a], [,b]) => b - a); // sort by count
for (const [type, count] of sortedTypes) {
const pill = filterBar.createSpan({ cls: `waypoint-recent-pill${this.recentFilesFilter === type ? ' is-active' : ''}` });
pill.setText(`${type} ${count}`);
@@ -304,7 +326,7 @@ export class WaypointView extends ItemView {
removeBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.plugin.recentFiles = this.plugin.recentFiles.filter(f => f.path !== file.path);
this.plugin.saveSettings();
this.plugin.persistRecentFiles();
this.redraw();
});
setTooltip(navFile, file.path);
@@ -384,7 +406,7 @@ export class WaypointView extends ItemView {
} else {
new Notice('Cannot find file');
this.plugin.recentFiles = this.plugin.recentFiles.filter(f => f.path !== file.path);
this.plugin.saveSettings();
this.plugin.persistRecentFiles();
this.redraw();
}
}