Initial commit: Waypoint Obsidian plugin with calendar, recent files, and bookmarks
This commit is contained in:
+352
@@ -0,0 +1,352 @@
|
||||
// ── Main plugin entry ──
|
||||
|
||||
import {
|
||||
Plugin,
|
||||
WorkspaceLeaf,
|
||||
ItemView,
|
||||
Notice,
|
||||
TFile,
|
||||
TAbstractFile,
|
||||
moment,
|
||||
} from 'obsidian';
|
||||
import { WaypointSettings, DEFAULT_SETTINGS, PeriodNoteSettings } from 'src/settings';
|
||||
import { WaypointSettingTab } from 'src/settings-tab';
|
||||
import { WaypointView, WAYPOINT_VIEW_TYPE } from 'src/views/waypoint-view';
|
||||
import { BookmarkItem, WaypointData } from 'src/models/bookmark';
|
||||
|
||||
const DEFAULT_DATA: WaypointData = {
|
||||
bookmarks: [],
|
||||
};
|
||||
|
||||
export default class WaypointPlugin extends Plugin {
|
||||
public settings: WaypointSettings;
|
||||
public waypointData: WaypointData;
|
||||
public recentFiles: { path: string; basename: string }[] = [];
|
||||
|
||||
async onload(): Promise<void> {
|
||||
console.debug('Waypoint: loading plugin v' + this.manifest.version);
|
||||
|
||||
// Load persisted data
|
||||
await this.loadSettings();
|
||||
await this.loadWaypointData();
|
||||
|
||||
// Register the sidebar view
|
||||
this.registerView(
|
||||
WAYPOINT_VIEW_TYPE,
|
||||
(leaf: WorkspaceLeaf) => new WaypointView(leaf, this),
|
||||
);
|
||||
|
||||
// Register settings tab
|
||||
this.addSettingTab(new WaypointSettingTab(
|
||||
this.app,
|
||||
this,
|
||||
this.settings,
|
||||
() => this.redrawAll(),
|
||||
));
|
||||
|
||||
// ── Commands ──
|
||||
|
||||
this.addCommand({
|
||||
id: 'waypoint-open-view',
|
||||
name: 'Open Waypoint sidebar',
|
||||
callback: async () => {
|
||||
const leaves = this.app.workspace.getLeavesOfType(WAYPOINT_VIEW_TYPE);
|
||||
if (leaves.length > 0) {
|
||||
await this.app.workspace.revealLeaf(leaves[0]);
|
||||
} else {
|
||||
const leaf = this.app.workspace.getLeftLeaf(false);
|
||||
if (leaf) {
|
||||
await leaf.setViewState({ type: WAYPOINT_VIEW_TYPE });
|
||||
await this.app.workspace.revealLeaf(leaf);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'waypoint-add-bookmark',
|
||||
name: 'Add current file as Waypoint bookmark',
|
||||
callback: async () => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file) {
|
||||
new Notice('No active file');
|
||||
return;
|
||||
}
|
||||
this.addBookmark(file.path, file.basename, 'file');
|
||||
new Notice(`Bookmarked: ${file.basename}`);
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'waypoint-go-to-today',
|
||||
name: 'Go to today\'s daily note',
|
||||
callback: async () => {
|
||||
await this.openPeriodNote('day', moment());
|
||||
},
|
||||
});
|
||||
|
||||
// ── Events ──
|
||||
|
||||
this.registerEvent(
|
||||
this.app.workspace.on('file-open', (file: TFile | null) => {
|
||||
if (file) this.onFileOpen(file);
|
||||
}),
|
||||
);
|
||||
|
||||
this.registerEvent(
|
||||
this.app.vault.on('create', () => this.onVaultChange()),
|
||||
);
|
||||
this.registerEvent(
|
||||
this.app.vault.on('delete', () => this.onVaultChange()),
|
||||
);
|
||||
this.registerEvent(
|
||||
this.app.vault.on('rename', (file, oldPath) => this.onRename(file, oldPath)),
|
||||
);
|
||||
|
||||
// Auto-open view on first load
|
||||
this.app.workspace.onLayoutReady(() => {
|
||||
const leaves = this.app.workspace.getLeavesOfType(WAYPOINT_VIEW_TYPE);
|
||||
if (leaves.length === 0) {
|
||||
const leaf = this.app.workspace.getLeftLeaf(false);
|
||||
if (leaf) {
|
||||
leaf.setViewState({ type: WAYPOINT_VIEW_TYPE });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Midnight refresh (check every 10 min)
|
||||
let lastDate = new Date().toDateString();
|
||||
this.registerInterval(
|
||||
window.setInterval(() => {
|
||||
const currentDate = new Date().toDateString();
|
||||
if (currentDate !== lastDate) {
|
||||
lastDate = currentDate;
|
||||
this.redrawAll();
|
||||
}
|
||||
}, 600000), // 10 min
|
||||
);
|
||||
}
|
||||
|
||||
async onunload(): Promise<void> {
|
||||
this.app.workspace.detachLeavesOfType(WAYPOINT_VIEW_TYPE);
|
||||
}
|
||||
|
||||
// ── Settings ──
|
||||
|
||||
async loadSettings(): Promise<void> {
|
||||
const saved = await this.loadData() as Record<string, unknown> | null;
|
||||
const s = (saved?.settings || {}) as Partial<WaypointSettings>;
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, s);
|
||||
}
|
||||
|
||||
async saveSettings(): Promise<void> {
|
||||
const all = (await this.loadData()) as Record<string, unknown> || {};
|
||||
all.settings = this.settings;
|
||||
await this.saveData(all);
|
||||
}
|
||||
|
||||
async loadWaypointData(): Promise<void> {
|
||||
const saved = await this.loadData() as Record<string, unknown> | null;
|
||||
const d = (saved?.waypointData || {}) as Partial<WaypointData>;
|
||||
this.waypointData = Object.assign({}, DEFAULT_DATA, d);
|
||||
}
|
||||
|
||||
async saveWaypointData(): Promise<void> {
|
||||
const all = (await this.loadData()) as Record<string, unknown> || {};
|
||||
all.waypointData = this.waypointData;
|
||||
await this.saveData(all);
|
||||
}
|
||||
|
||||
// ── Recent Files ──
|
||||
|
||||
private onFileOpen(file: TFile): void {
|
||||
if (this.settings.recentFiles.updateOn === 'file-edit') {
|
||||
// We'll handle this via quick-preview in a future refinement
|
||||
return;
|
||||
}
|
||||
this.addToRecentFiles(file);
|
||||
}
|
||||
|
||||
addToRecentFiles(file: TFile): void {
|
||||
this.recentFiles = this.recentFiles.filter(f => f.path !== file.path);
|
||||
this.recentFiles.unshift({ path: file.path, basename: file.basename });
|
||||
|
||||
// Prune
|
||||
if (this.recentFiles.length > this.settings.recentFiles.maxItems) {
|
||||
this.recentFiles = this.recentFiles.slice(0, this.settings.recentFiles.maxItems);
|
||||
}
|
||||
|
||||
this.broadcastRedraw();
|
||||
}
|
||||
|
||||
private onRename(file: TAbstractFile, oldPath: string): void {
|
||||
const entry = this.recentFiles.find(f => f.path === oldPath);
|
||||
if (entry) {
|
||||
entry.path = file.path;
|
||||
entry.basename = (file as TFile).basename || file.name.replace(/\.[^/.]+$/, '');
|
||||
this.broadcastRedraw();
|
||||
}
|
||||
|
||||
// Update bookmark file paths
|
||||
this.updateBookmarkPath(oldPath, file.path);
|
||||
}
|
||||
|
||||
private onVaultChange(): void {
|
||||
this.broadcastRedraw();
|
||||
}
|
||||
|
||||
// ── Bookmarks ──
|
||||
|
||||
addBookmark(filePath: string, label: string, type: 'file' | 'group' | 'separator' | 'spacer' = 'file', icon = 'file'): BookmarkItem {
|
||||
const id = `bm-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
||||
const item: BookmarkItem = {
|
||||
id,
|
||||
type,
|
||||
label,
|
||||
filePath: type === 'file' ? filePath : '',
|
||||
icon,
|
||||
children: [],
|
||||
collapsed: false,
|
||||
indent: 0,
|
||||
};
|
||||
this.waypointData.bookmarks.push(item);
|
||||
this.saveWaypointData();
|
||||
this.broadcastRedraw();
|
||||
return item;
|
||||
}
|
||||
|
||||
removeBookmark(id: string): void {
|
||||
const removeRecursive = (items: BookmarkItem[]): boolean => {
|
||||
const idx = items.findIndex(i => i.id === id);
|
||||
if (idx >= 0) {
|
||||
items.splice(idx, 1);
|
||||
return true;
|
||||
}
|
||||
for (const item of items) {
|
||||
if (item.children && removeRecursive(item.children)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
removeRecursive(this.waypointData.bookmarks);
|
||||
this.saveWaypointData();
|
||||
this.broadcastRedraw();
|
||||
}
|
||||
|
||||
updateBookmark(id: string, updates: Partial<BookmarkItem>): void {
|
||||
const findRecursive = (items: BookmarkItem[]): BookmarkItem | null => {
|
||||
for (const item of items) {
|
||||
if (item.id === id) return item;
|
||||
if (item.children) {
|
||||
const found = findRecursive(item.children);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const item = findRecursive(this.waypointData.bookmarks);
|
||||
if (item) {
|
||||
Object.assign(item, updates);
|
||||
this.saveWaypointData();
|
||||
this.broadcastRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
private updateBookmarkPath(oldPath: string, newPath: string): void {
|
||||
const updateRecursive = (items: BookmarkItem[]) => {
|
||||
for (const item of items) {
|
||||
if (item.filePath === oldPath) {
|
||||
item.filePath = newPath;
|
||||
}
|
||||
if (item.children) updateRecursive(item.children);
|
||||
}
|
||||
};
|
||||
updateRecursive(this.waypointData.bookmarks);
|
||||
}
|
||||
|
||||
// ── Period note creation/opening ──
|
||||
|
||||
async openPeriodNote(period: 'day' | 'week' | 'month' | 'quarter' | 'year', date: moment.Moment): Promise<void> {
|
||||
// Map period to settings and date format
|
||||
type PeriodConfig = {
|
||||
settings: PeriodNoteSettings;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const configs: Record<string, PeriodConfig> = {
|
||||
day: { settings: this.settings.daily, label: 'Daily' },
|
||||
week: { settings: this.settings.weekly, label: 'Weekly' },
|
||||
month: { settings: this.settings.monthly, label: 'Monthly' },
|
||||
quarter: { settings: this.settings.quarterly, label: 'Quarterly' },
|
||||
year: { settings: this.settings.yearly, label: 'Yearly' },
|
||||
};
|
||||
|
||||
const config = configs[period];
|
||||
if (!config) return;
|
||||
|
||||
const { settings: periodSettings } = config;
|
||||
const filename = date.format(periodSettings.nameFormat) + '.md';
|
||||
const fullPath = periodSettings.folder
|
||||
? `${periodSettings.folder}/${filename}`
|
||||
: filename;
|
||||
|
||||
// Check if exists
|
||||
let file = this.app.vault.getFileByPath(fullPath);
|
||||
if (!file) {
|
||||
// Create it from template
|
||||
try {
|
||||
// Try to find template file
|
||||
const templatePath = periodSettings.templateFile + '.md';
|
||||
const templateFile = this.app.vault.getFileByPath(templatePath);
|
||||
|
||||
if (templateFile) {
|
||||
const templateContent = await this.app.vault.read(templateFile);
|
||||
file = await this.app.vault.create(fullPath, templateContent);
|
||||
} else {
|
||||
// Fallback: create with minimal frontmatter
|
||||
const content = `---\ntype: ${periodSettings.typeProperty}\ndate: ${date.format('YYYY-MM-DD')}\n---\n\n`;
|
||||
file = await this.app.vault.create(fullPath, content);
|
||||
}
|
||||
} catch (err) {
|
||||
new Notice(`Failed to create ${config.label.toLowerCase()} note: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
new Notice(`Created ${config.label.toLowerCase()} note: ${filename}`);
|
||||
}
|
||||
|
||||
if (file) {
|
||||
const leaf = this.app.workspace.getLeaf(false);
|
||||
await leaf.openFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Redraw ──
|
||||
|
||||
private redrawAll(): void {
|
||||
this.broadcastRedraw();
|
||||
}
|
||||
|
||||
private broadcastRedraw(): void {
|
||||
const leaves = this.app.workspace.getLeavesOfType(WAYPOINT_VIEW_TYPE);
|
||||
for (const leaf of leaves) {
|
||||
if (leaf.view instanceof WaypointView) {
|
||||
leaf.view.redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all markdown files that exist on a specific date.
|
||||
* Used to show note indicators on the calendar.
|
||||
*/
|
||||
async getNotesForDate(dateStr: string): Promise<TFile[]> {
|
||||
return this.app.vault.getFiles().filter(f =>
|
||||
f.extension === 'md' && f.basename === dateStr,
|
||||
);
|
||||
}
|
||||
|
||||
async hasNoteForDate(dateStr: string): Promise<boolean> {
|
||||
return this.app.vault.getFiles().some(f =>
|
||||
f.extension === 'md' && f.basename === dateStr,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user