// ************************************************************************* // * (C)opyright 2026 by Ruben Carlo Benante * // * * // * This program is free software; you can redistribute it and/or modify * // * it under the terms of the GNU General Public License as published by * // * the Free Software Foundation, either version 3 of the License, or * // * (at your option) any later version. * // * * // * This program is distributed in the hope that it will be useful, * // * but WITHOUT ANY WARRANTY; without even the implied warranty of * // * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * // * GNU General Public License for more details. * // * * // * You should have received a copy of the GNU General Public License * // * along with this program. If not, see http://www.gnu.org/licenses/. * // * * // * Contact author at: * // * Ruben Carlo Benante * // * rcb@beco.cc * // ************************************************************************* import { Editor, Notice, Plugin } from 'obsidian'; import { parseQualcardBlocks, selectBlockToPush } from './card'; import { getProjectIdByName, createTask } from './kanboard'; import { Ob2qualkardSettings, DEFAULT_SETTINGS, Ob2qualkardSettingTab, } from './settings'; export default class Ob2qualkardPlugin extends Plugin { settings!: Ob2qualkardSettings; // Board name -> project id, resolved once per session (cleared on save). private boardCache = new Map(); async onload() { await this.loadSettings(); this.addSettingTab(new Ob2qualkardSettingTab(this.app, this)); this.addCommand({ id: 'push-note-to-qualkard', name: 'Push this note to QualKard', editorCallback: (editor) => void this.pushNote(editor), }); this.registerEvent( this.app.workspace.on('editor-menu', (menu, editor) => { menu.addItem((item) => item .setTitle('Push this note to QualKard') .setIcon('upload') .onClick(() => void this.pushNote(editor)), ); }), ); } async loadSettings() { this.settings = Object.assign( {}, DEFAULT_SETTINGS, (await this.loadData()) as Partial, ); } async saveSettings() { await this.saveData(this.settings); // The board name may have changed; re-resolve its id on the next push. this.boardCache.clear(); } // Parse the note, pick the card to push (sole block, or the one under the // cursor), validate it, then create the Kanboard task. One-way, no dedup. private async pushNote(editor: Editor) { const sel = selectBlockToPush( parseQualcardBlocks(editor.getValue()), editor.getCursor().line, ); if (sel.reason === 'none') { new Notice('No qualcard block in this note'); return; } if (sel.reason === 'ambiguous') { new Notice( 'Several qualcard blocks; place the cursor inside the one to push', ); return; } const block = sel.block; if (!block || !block.front) { new Notice('This qualcard block has no prompt (front)'); return; } if (!block.hasAnswer) { new Notice('This qualcard block has no answer, so it was skipped'); return; } if (!this.settings.apiToken || !this.settings.board) { new Notice( 'Set the API token and study board in settings first', ); return; } try { const projectId = await this.resolveBoard(); if (projectId === null) { new Notice(`Board "${this.settings.board}" not found`); return; } const taskId = await createTask( this.settings, projectId, block.front, block.back, ); new Notice(`Card created (task #${taskId})`); } catch (e) { const msg = e instanceof Error ? e.message : 'push failed'; console.error('ob2qualkard: push failed', e); new Notice(msg); } } private async resolveBoard(): Promise { const name = this.settings.board; const cached = this.boardCache.get(name); if (cached !== undefined) return cached; const id = await getProjectIdByName(this.settings, name); if (id !== null) this.boardCache.set(name, id); return id; } }