ob2qualkard v0.2 test alpha
This commit is contained in:
192
src/main.ts
192
src/main.ts
@@ -1,114 +1,132 @@
|
||||
// *************************************************************************
|
||||
// * (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 {
|
||||
Editor,
|
||||
MarkdownView,
|
||||
MarkdownFileInfo,
|
||||
Modal,
|
||||
Notice,
|
||||
Plugin,
|
||||
} from 'obsidian';
|
||||
import {
|
||||
Ob2qualkardSettings,
|
||||
DEFAULT_SETTINGS,
|
||||
MyPluginSettings,
|
||||
SampleSettingTab,
|
||||
Ob2qualkardSettingTab,
|
||||
} from './settings';
|
||||
|
||||
// Remember to rename these classes and interfaces!
|
||||
|
||||
export default class MyPlugin extends Plugin {
|
||||
settings!: MyPluginSettings;
|
||||
export default class Ob2qualkardPlugin extends Plugin {
|
||||
settings!: Ob2qualkardSettings;
|
||||
// Board name -> project id, resolved once per session (cleared on save).
|
||||
private boardCache = new Map<string, number>();
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
this.addSettingTab(new Ob2qualkardSettingTab(this.app, this));
|
||||
|
||||
// This creates an icon in the left ribbon.
|
||||
this.addRibbonIcon('dice', 'Sample', (_evt: MouseEvent) => {
|
||||
// Called when the user clicks the icon.
|
||||
new Notice('This is a notice!');
|
||||
});
|
||||
|
||||
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
|
||||
const statusBarItemEl = this.addStatusBarItem();
|
||||
statusBarItemEl.setText('Status bar text');
|
||||
|
||||
// This adds a simple command that can be triggered anywhere
|
||||
this.addCommand({
|
||||
id: 'open-modal-simple',
|
||||
name: 'Open modal (simple)',
|
||||
callback: () => {
|
||||
new SampleModal(this.app).open();
|
||||
},
|
||||
});
|
||||
// This adds an editor command that can perform some operation on the current editor instance
|
||||
this.addCommand({
|
||||
id: 'replace-selected',
|
||||
name: 'Replace selected content',
|
||||
editorCallback: (
|
||||
editor: Editor,
|
||||
_ctx: MarkdownView | MarkdownFileInfo,
|
||||
) => {
|
||||
editor.replaceSelection('Sample editor command');
|
||||
},
|
||||
});
|
||||
// This adds a complex command that can check whether the current state of the app allows execution of the command
|
||||
this.addCommand({
|
||||
id: 'open-modal-complex',
|
||||
name: 'Open modal (complex)',
|
||||
checkCallback: (checking: boolean) => {
|
||||
// Conditions to check
|
||||
const markdownView =
|
||||
this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (markdownView) {
|
||||
// If checking is true, we're simply "checking" if the command can be run.
|
||||
// If checking is false, then we want to actually perform the operation.
|
||||
if (!checking) {
|
||||
new SampleModal(this.app).open();
|
||||
}
|
||||
|
||||
// This command will only show up in Command Palette when the check function returns true
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
id: 'push-note-to-qualkard',
|
||||
name: 'Push this note to QualKard',
|
||||
editorCallback: (editor) => void this.pushNote(editor),
|
||||
});
|
||||
|
||||
// This adds a settings tab so the user can configure various aspects of the plugin
|
||||
this.addSettingTab(new SampleSettingTab(this.app, this));
|
||||
|
||||
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
|
||||
// Using this function will automatically remove the event listener when this plugin is disabled.
|
||||
this.registerDomEvent(activeDocument, 'click', (_evt: MouseEvent) => {
|
||||
new Notice('Click');
|
||||
});
|
||||
|
||||
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
|
||||
this.registerInterval(
|
||||
window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000),
|
||||
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)),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
onunload() {}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign(
|
||||
{},
|
||||
DEFAULT_SETTINGS,
|
||||
(await this.loadData()) as Partial<MyPluginSettings>,
|
||||
(await this.loadData()) as Partial<Ob2qualkardSettings>,
|
||||
);
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
}
|
||||
|
||||
class SampleModal extends Modal {
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.setText('Woah!');
|
||||
// The board name may have changed; re-resolve its id on the next push.
|
||||
this.boardCache.clear();
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
// 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<number | null> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
144
src/settings.ts
144
src/settings.ts
@@ -1,38 +1,132 @@
|
||||
import { App, PluginSettingTab, Setting } from 'obsidian';
|
||||
import MyPlugin from './main';
|
||||
// *************************************************************************
|
||||
// * (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 *
|
||||
// *************************************************************************
|
||||
|
||||
export interface MyPluginSettings {
|
||||
mySetting: string;
|
||||
import { App, PluginSettingTab, Setting, SettingDefinitionItem } from 'obsidian';
|
||||
import Ob2qualkardPlugin from './main';
|
||||
|
||||
export interface Ob2qualkardSettings {
|
||||
baseUrl: string;
|
||||
username: string;
|
||||
apiToken: string;
|
||||
board: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: MyPluginSettings = {
|
||||
mySetting: 'default',
|
||||
export const DEFAULT_SETTINGS: Ob2qualkardSettings = {
|
||||
baseUrl: 'https://organon.beco.cc',
|
||||
username: 'drbeco',
|
||||
apiToken: '',
|
||||
board: '',
|
||||
};
|
||||
|
||||
export class SampleSettingTab extends PluginSettingTab {
|
||||
plugin: MyPlugin;
|
||||
interface FieldSpec {
|
||||
key: keyof Ob2qualkardSettings;
|
||||
name: string;
|
||||
desc: string;
|
||||
placeholder: string;
|
||||
password?: boolean;
|
||||
}
|
||||
|
||||
constructor(app: App, plugin: MyPlugin) {
|
||||
// A single source of truth for the four text fields, shared by the modern
|
||||
// declarative API and the imperative fallback below.
|
||||
const FIELDS: FieldSpec[] = [
|
||||
{
|
||||
key: 'baseUrl',
|
||||
name: 'Kanboard URL',
|
||||
desc: 'Base URL of your Kanboard instance (without the /jsonrpc.php path).',
|
||||
placeholder: 'https://organon.beco.cc',
|
||||
},
|
||||
{
|
||||
key: 'username',
|
||||
name: 'API username',
|
||||
desc: 'Kanboard user whose personal API token is used.',
|
||||
placeholder: 'drbeco',
|
||||
},
|
||||
{
|
||||
key: 'apiToken',
|
||||
name: 'API token',
|
||||
desc: 'Personal API token for that user. Stored in plain text in this vault (data.json) - do not commit or publicly sync it.',
|
||||
placeholder: 'paste token',
|
||||
password: true,
|
||||
},
|
||||
{
|
||||
key: 'board',
|
||||
name: 'Study board',
|
||||
desc: 'Display name of the QualKard-enabled board that cards are pushed to.',
|
||||
placeholder: 'Direito',
|
||||
},
|
||||
];
|
||||
|
||||
export class Ob2qualkardSettingTab extends PluginSettingTab {
|
||||
plugin: Ob2qualkardPlugin;
|
||||
|
||||
constructor(app: App, plugin: Ob2qualkardPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
// Modern declarative settings (Obsidian 1.13+): rendered and indexed for
|
||||
// settings search. Ignored on older versions, which use display() below.
|
||||
getSettingDefinitions(): SettingDefinitionItem[] {
|
||||
return FIELDS.map(
|
||||
(f): SettingDefinitionItem => ({
|
||||
name: f.name,
|
||||
desc: f.desc,
|
||||
control: {
|
||||
type: 'text',
|
||||
key: f.key,
|
||||
defaultValue: DEFAULT_SETTINGS[f.key],
|
||||
placeholder: f.placeholder,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
getControlValue(key: string): unknown {
|
||||
return this.plugin.settings[key as keyof Ob2qualkardSettings];
|
||||
}
|
||||
|
||||
async setControlValue(key: string, value: unknown): Promise<void> {
|
||||
const text = typeof value === 'string' ? value : '';
|
||||
this.plugin.settings[key as keyof Ob2qualkardSettings] = text;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
|
||||
// Imperative fallback for Obsidian < 1.13 (e.g. 1.12.x). Not called on
|
||||
// 1.13+ once getSettingDefinitions() returns a non-empty array.
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Settings #1')
|
||||
.setDesc("It's a secret")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder('Enter your secret')
|
||||
.setValue(this.plugin.settings.mySetting)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.mySetting = value;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
this.containerEl.empty();
|
||||
for (const f of FIELDS) {
|
||||
new Setting(this.containerEl)
|
||||
.setName(f.name)
|
||||
.setDesc(f.desc)
|
||||
.addText((text) => {
|
||||
text
|
||||
.setPlaceholder(f.placeholder)
|
||||
.setValue(this.plugin.settings[f.key])
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings[f.key] = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
if (f.password) text.inputEl.type = 'password';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user