diff --git a/src/features/custom-notes/manager.ts b/src/features/custom-notes/manager.ts new file mode 100644 index 0000000..dda13e9 --- /dev/null +++ b/src/features/custom-notes/manager.ts @@ -0,0 +1,71 @@ +import { Notice, TFile } from "obsidian"; +import type LatexReferencer from "../../main"; + +export class CustomNoteManager { + private registeredCommandIds: string[] = []; + + constructor(private plugin: LatexReferencer) {} + + onLoad() { + this.registerCommands(); + } + + registerCommands() { + // 1. Unregister all previously registered commands + const appCommands = (this.plugin.app as any).commands; + if (appCommands && typeof appCommands.removeCommand === "function") { + for (const cmdId of this.registeredCommandIds) { + try { + appCommands.removeCommand(cmdId); + } catch (e) { + console.error("Failed to remove custom note command", cmdId, e); + } + } + } + this.registeredCommandIds = []; + + // 2. Register new commands + const customNotes = this.plugin.settings.customNoteHotkeys || []; + for (const item of customNotes) { + if (!item.notePath) continue; + + const displayName = item.name || item.notePath.split("/").pop()?.replace(/\.md$/, "") || item.notePath; + const commandId = `open-custom-note-${item.id}`; + const fullCommandId = `${this.plugin.manifest.id}:${commandId}`; + + const commandConfig: any = { + id: commandId, + name: `Open custom note: ${displayName}`, + callback: () => { + this.openCustomNote(item.notePath); + } + }; + + if (item.hotkeyKey && item.hotkeyKey.trim()) { + commandConfig.hotkeys = [ + { + modifiers: item.hotkeyModifiers || [], + key: item.hotkeyKey.trim() + } + ]; + } + + this.plugin.addCommand(commandConfig); + this.registeredCommandIds.push(fullCommandId); + } + } + + async openCustomNote(notePath: string) { + let file = this.plugin.app.vault.getAbstractFileByPath(notePath); + if (!file && !notePath.endsWith(".md")) { + file = this.plugin.app.vault.getAbstractFileByPath(notePath + ".md"); + } + + if (file instanceof TFile) { + const leaf = this.plugin.app.workspace.getLeaf("tab"); + await leaf.openFile(file); + } else { + new Notice(`Custom note not found at path: ${notePath}. Please verify your configuration in settings.`); + } + } +} diff --git a/src/features/custom-notes/modal.ts b/src/features/custom-notes/modal.ts new file mode 100644 index 0000000..9941842 --- /dev/null +++ b/src/features/custom-notes/modal.ts @@ -0,0 +1,25 @@ +import { App, FuzzyMatch, FuzzySuggestModal, TFile } from "obsidian"; + +export class NoteSuggestModal extends FuzzySuggestModal { + constructor(app: App, private onSelect: (file: TFile) => void) { + super(app); + this.setPlaceholder("Search note path..."); + } + + getItems(): TFile[] { + return this.app.vault.getMarkdownFiles(); + } + + getItemText(item: TFile): string { + return item.path; + } + + renderSuggestion(item: FuzzyMatch, el: HTMLElement): void { + el.createEl("div", { text: item.item.basename }); + el.createEl("small", { text: item.item.path, attr: { style: "color: var(--text-muted);" } }); + } + + onChooseItem(item: TFile, evt: MouseEvent | KeyboardEvent): void { + this.onSelect(item); + } +} diff --git a/src/features/settings/settings.ts b/src/features/settings/settings.ts index 9ec2623..6937d44 100644 --- a/src/features/settings/settings.ts +++ b/src/features/settings/settings.ts @@ -63,6 +63,17 @@ export interface PluginSettings { // Zotero Cleanup zoteroCleanDirectories: string; + + // Custom Note Hotkeys + customNoteHotkeys: CustomNoteHotkey[]; +} + +export interface CustomNoteHotkey { + id: string; + notePath: string; + name: string; + hotkeyModifiers: Modifier[]; + hotkeyKey: string; } export const DEFAULT_SETTINGS: Required = { @@ -122,4 +133,7 @@ export const DEFAULT_SETTINGS: Required = { // Zotero Cleanup zoteroCleanDirectories: "", + + // Custom Note Hotkeys + customNoteHotkeys: [], }; diff --git a/src/features/settings/tab.ts b/src/features/settings/tab.ts index 6cae806..1fc4d6e 100644 --- a/src/features/settings/tab.ts +++ b/src/features/settings/tab.ts @@ -1,6 +1,7 @@ import { App, PluginSettingTab, Setting, TextAreaComponent } from "obsidian"; import LatexReferencer from "../../main"; import { NUMBER_STYLES } from "./settings"; +import { NoteSuggestModal } from "../custom-notes/modal"; export class MathSettingTab extends PluginSettingTab { constructor(app: App, public plugin: LatexReferencer) { @@ -288,6 +289,164 @@ export class MathSettingTab extends PluginSettingTab { textArea.inputEl.setAttr("rows", 3); }); + containerEl.createEl("h2", { text: "Custom Note Hotkeys" }); + containerEl.createEl("p", { + text: "Configure hotkeys to quickly open specific notes in your vault. You can define optional default hotkeys here, and further customize or rebind them within Obsidian's global 'Hotkeys' settings.", + cls: "setting-item-description" + }); + + new Setting(containerEl) + .setName("Add new hotkey mapping") + .setDesc("Create a new shortcut command to open a specific note.") + .addButton(btn => btn + .setButtonText("+ Add hotkey mapping") + .setCta() + .onClick(async () => { + if (!this.plugin.settings.customNoteHotkeys) { + this.plugin.settings.customNoteHotkeys = []; + } + this.plugin.settings.customNoteHotkeys.push({ + id: Date.now().toString(), + notePath: "", + name: "", + hotkeyModifiers: ["Mod"], + hotkeyKey: "" + }); + await this.plugin.saveSettings(); + this.plugin.customNoteManager.registerCommands(); + this.display(); + }) + ); + + const hotkeys = this.plugin.settings.customNoteHotkeys || []; + hotkeys.forEach((item, index) => { + const hotkeyContainer = containerEl.createEl("div", { + cls: "custom-note-hotkey-item", + attr: { + style: "border: 1px solid var(--background-modifier-border); padding: 15px; margin-bottom: 15px; border-radius: 8px; background-color: var(--background-primary-alt);" + } + }); + + const titleRow = hotkeyContainer.createEl("div", { + attr: { style: "display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;" } + }); + titleRow.createEl("strong", { text: `Hotkey Mapping #${index + 1}` }); + + const deleteBtn = titleRow.createEl("button", { + text: "Delete", + cls: "mod-warning", + attr: { style: "background-color: var(--background-modifier-error); color: var(--text-on-accent);" } + }); + deleteBtn.addEventListener("click", async () => { + hotkeys.splice(index, 1); + await this.plugin.saveSettings(); + this.plugin.customNoteManager.registerCommands(); + this.display(); + }); + + new Setting(hotkeyContainer) + .setName("Friendly name") + .setDesc("A clear label for the Obsidian command (e.g. 'Daily Planner').") + .addText(text => text + .setPlaceholder("e.g. My Note") + .setValue(item.name) + .onChange(async value => { + item.name = value; + await this.plugin.saveSettings(); + this.plugin.customNoteManager.registerCommands(); + }) + ); + + const pathSetting = new Setting(hotkeyContainer) + .setName("Note path") + .setDesc("The relative vault path to the target markdown note."); + + pathSetting.addText(text => { + text.setPlaceholder("e.g. Folder/My Note.md") + .setValue(item.notePath) + .onChange(async value => { + item.notePath = value; + await this.plugin.saveSettings(); + this.plugin.customNoteManager.registerCommands(); + }); + + pathSetting.addButton(btn => btn + .setIcon("search") + .setTooltip("Browse/Search vault notes") + .onClick(() => { + new NoteSuggestModal(this.app, async (file) => { + text.setValue(file.path); + item.notePath = file.path; + await this.plugin.saveSettings(); + this.plugin.customNoteManager.registerCommands(); + }).open(); + }) + ); + }); + + const hotkeySetting = new Setting(hotkeyContainer) + .setName("Default hotkey") + .setDesc("Select modifiers and input a key (e.g., '1', 'a') to assign a default shortcut."); + + const isMod = item.hotkeyModifiers.includes("Mod"); + const isAlt = item.hotkeyModifiers.includes("Alt"); + const isShift = item.hotkeyModifiers.includes("Shift"); + + hotkeySetting.addToggle(toggle => toggle + .setTooltip("Ctrl / Cmd (Mod)") + .setValue(isMod) + .onChange(async value => { + if (value) { + if (!item.hotkeyModifiers.includes("Mod")) item.hotkeyModifiers.push("Mod"); + } else { + item.hotkeyModifiers = item.hotkeyModifiers.filter(m => m !== "Mod"); + } + await this.plugin.saveSettings(); + this.plugin.customNoteManager.registerCommands(); + }) + ); + hotkeySetting.controlEl.createSpan({ text: "Ctrl/Cmd ", attr: { style: "margin-right: 15px; font-size: 0.9em;" } }); + + hotkeySetting.addToggle(toggle => toggle + .setTooltip("Alt") + .setValue(isAlt) + .onChange(async value => { + if (value) { + if (!item.hotkeyModifiers.includes("Alt")) item.hotkeyModifiers.push("Alt"); + } else { + item.hotkeyModifiers = item.hotkeyModifiers.filter(m => m !== "Alt"); + } + await this.plugin.saveSettings(); + this.plugin.customNoteManager.registerCommands(); + }) + ); + hotkeySetting.controlEl.createSpan({ text: "Alt ", attr: { style: "margin-right: 15px; font-size: 0.9em;" } }); + + hotkeySetting.addToggle(toggle => toggle + .setTooltip("Shift") + .setValue(isShift) + .onChange(async value => { + if (value) { + if (!item.hotkeyModifiers.includes("Shift")) item.hotkeyModifiers.push("Shift"); + } else { + item.hotkeyModifiers = item.hotkeyModifiers.filter(m => m !== "Shift"); + } + await this.plugin.saveSettings(); + this.plugin.customNoteManager.registerCommands(); + }) + ); + hotkeySetting.controlEl.createSpan({ text: "Shift ", attr: { style: "margin-right: 15px; font-size: 0.9em;" } }); + + hotkeySetting.addText(text => text + .setPlaceholder("Key (e.g. 1, a)") + .setValue(item.hotkeyKey || "") + .onChange(async value => { + item.hotkeyKey = value; + await this.plugin.saveSettings(); + this.plugin.customNoteManager.registerCommands(); + }) + ); + }); new Setting(containerEl).setName("Debug").setHeading(); new Setting(containerEl) diff --git a/src/main.ts b/src/main.ts index 2d1adc8..6bbcbc7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -28,6 +28,7 @@ import { checkAndFixCalloutMath } from 'utils/fixer'; import { traverseFolder } from "./features/export-pdf/utils"; import { SnippetManager } from 'features/snippets/manager'; import { processZoteroCleanup } from 'features/zotero-cleanup'; +import { CustomNoteManager } from 'features/custom-notes/manager'; const isDev = process.env.NODE_ENV === "development"; @@ -37,6 +38,7 @@ export default class LatexReferencer extends Plugin { editorExtensions: Extension[]; internalProviders: Provider[] = []; snippetManager: SnippetManager; + customNoteManager: CustomNoteManager; async onload() { await this.loadSettings(); @@ -47,6 +49,10 @@ export default class LatexReferencer extends Plugin { this.snippetManager = new SnippetManager(this); this.snippetManager.onLoad(); + // Custom Notes + this.customNoteManager = new CustomNoteManager(this); + this.customNoteManager.onLoad(); + this.addSettingTab(new MathSettingTab(this.app, this)); // Commands