mirror of
https://github.com/ebullient/obsidian-deck-notes.git
synced 2026-07-22 06:40:43 +00:00
206 lines
6.1 KiB
TypeScript
206 lines
6.1 KiB
TypeScript
import { type Editor, Notice, Plugin, TFile, TFolder } from "obsidian";
|
|
import type { Card, DeckNotesData, DeckNotesSettings } from "./@types/settings";
|
|
import { DeckNotesApi } from "./dn-Api";
|
|
import { CardParser } from "./dn-CardParser";
|
|
import { DEFAULT_SETTINGS } from "./dn-Constants";
|
|
import { FlashcardModal } from "./dn-Modal";
|
|
import { DeckNotesSettingsTab } from "./dn-SettingsTab";
|
|
|
|
export default class DeckNotesPlugin extends Plugin {
|
|
settings!: DeckNotesSettings;
|
|
data!: DeckNotesData;
|
|
cachedCards: Card[] = [];
|
|
cardParser!: CardParser;
|
|
api!: DeckNotesApi;
|
|
|
|
async onload() {
|
|
console.log("Loading Deck Notes plugin");
|
|
|
|
await this.loadSettings();
|
|
this.cardParser = new CardParser(this.app);
|
|
|
|
this.addSettingTab(new DeckNotesSettingsTab(this.app, this));
|
|
|
|
// Defer initial card scan, API, and command registration to avoid blocking startup
|
|
this.app.workspace.onLayoutReady(async () => {
|
|
await this.scanCards();
|
|
|
|
// Initialize API
|
|
this.api = new DeckNotesApi(this);
|
|
if (!window.deckNotes) {
|
|
window.deckNotes = {};
|
|
}
|
|
window.deckNotes.api = this.api;
|
|
|
|
// Command: Show Random Card
|
|
this.addCommand({
|
|
id: "show-card",
|
|
name: "Show Card",
|
|
callback: () => {
|
|
this.showFlashCard();
|
|
},
|
|
});
|
|
|
|
// Command: Embed Card
|
|
this.addCommand({
|
|
id: "embed-card",
|
|
name: "Embed Card",
|
|
editorCallback: (editor) => {
|
|
this.embedCard(editor);
|
|
},
|
|
});
|
|
|
|
// Command: Refresh Cards
|
|
this.addCommand({
|
|
id: "refresh-cards",
|
|
name: "Refresh Cards",
|
|
callback: async () => {
|
|
await this.scanCards();
|
|
new Notice("Flash cards refreshed");
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
onunload() {
|
|
console.log("Unloading Deck Notes plugin");
|
|
|
|
// Clear API reference
|
|
if (window.deckNotes) {
|
|
window.deckNotes.api = undefined;
|
|
}
|
|
}
|
|
|
|
async loadSettings() {
|
|
const data = await this.loadData();
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, data);
|
|
this.data = data || { cardViews: {} };
|
|
}
|
|
|
|
async saveSettings() {
|
|
await this.saveData({
|
|
...this.settings,
|
|
cardViews: this.data.cardViews,
|
|
});
|
|
await this.scanCards();
|
|
}
|
|
|
|
async scanCards() {
|
|
this.cachedCards = [];
|
|
|
|
for (const cardPath of this.settings.cardPaths) {
|
|
const abstractFile = this.app.vault.getAbstractFileByPath(cardPath);
|
|
|
|
if (abstractFile instanceof TFolder) {
|
|
await this.scanFolder(abstractFile);
|
|
} else if (abstractFile instanceof TFile) {
|
|
const cards = await this.cardParser.parseFile(abstractFile);
|
|
this.cachedCards.push(...cards);
|
|
} else {
|
|
console.warn(`Card path not found: ${cardPath}`);
|
|
}
|
|
}
|
|
|
|
console.log(`Scanned ${this.cachedCards.length} cards`);
|
|
}
|
|
|
|
private async scanFolder(folder: TFolder) {
|
|
for (const child of folder.children) {
|
|
if (child instanceof TFile && child.extension === "md") {
|
|
const cards = await this.cardParser.parseFile(child);
|
|
this.cachedCards.push(...cards);
|
|
} else if (child instanceof TFolder) {
|
|
await this.scanFolder(child);
|
|
}
|
|
}
|
|
}
|
|
|
|
selectCard(deckTag?: string): Card | null {
|
|
let pool = this.cachedCards;
|
|
|
|
if (deckTag) {
|
|
// Hierarchical matching: "activities" matches "activities/morning"
|
|
pool = pool.filter((c) =>
|
|
c.tags.some(
|
|
(tag) => tag === deckTag || tag.startsWith(`${deckTag}/`),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (pool.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
// least-recent: sort by lastSeen timestamp
|
|
const sorted = pool.slice().sort((a, b) => {
|
|
const aTime = this.data.cardViews[a.key] || 0;
|
|
const bTime = this.data.cardViews[b.key] || 0;
|
|
if (aTime === bTime) {
|
|
// consistent, pseudo-randomizing sort
|
|
return a.hash.localeCompare(b.hash);
|
|
}
|
|
return aTime - bTime;
|
|
});
|
|
|
|
return this.settings.selectionMode === "random"
|
|
? pool[Math.floor(Math.random() * pool.length)]
|
|
: sorted[0];
|
|
}
|
|
|
|
recordView(cardKey: string) {
|
|
this.data.cardViews[cardKey] = Date.now();
|
|
this.saveData({
|
|
...this.settings,
|
|
cardViews: this.data.cardViews,
|
|
});
|
|
}
|
|
|
|
showFlashCard() {
|
|
const deckTag = this.settings.defaultDeckTag || undefined;
|
|
const card = this.selectCard(deckTag);
|
|
|
|
if (!card) {
|
|
new Notice("No cards available. Check your settings.");
|
|
return;
|
|
}
|
|
|
|
new FlashcardModal(this.app, this, card, deckTag).open();
|
|
}
|
|
|
|
createEmbedText(deck: string | undefined): string | null {
|
|
const deckTag = deck || this.settings.defaultDeckTag;
|
|
const card = this.selectCard(deckTag);
|
|
|
|
if (!card) {
|
|
return null;
|
|
}
|
|
|
|
// Create collapsible callout with card content
|
|
const contentLines = card.content
|
|
.split("\n")
|
|
.map((line) => `> ${line}`);
|
|
|
|
const embedText = [
|
|
`> [!${this.settings.calloutType}]- ${card.heading}`,
|
|
...contentLines,
|
|
].join("\n");
|
|
|
|
// Record as viewed
|
|
if (this.settings.trackViews) {
|
|
this.recordView(card.key);
|
|
}
|
|
|
|
return embedText;
|
|
}
|
|
|
|
embedCard(editor: Editor, deck: string | undefined = undefined) {
|
|
const embedText = this.createEmbedText(deck);
|
|
|
|
if (!embedText) {
|
|
new Notice("No cards available. Check your settings.");
|
|
return;
|
|
}
|
|
|
|
editor.replaceSelection(embedText);
|
|
}
|
|
}
|