Skip to content

Commit 0c84d4e

Browse files
Add notes missing anki deck to status bar
1 parent 7d78520 commit 0c84d4e

File tree

2 files changed

+150
-2
lines changed

2 files changed

+150
-2
lines changed

src/main.ts

Lines changed: 146 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
1-
import { Editor, Notice, Plugin } from "obsidian";
1+
import { Editor, Modal, Notice, Plugin, TFile, setIcon } from "obsidian";
22
import {
33
DEFAULT_SETTINGS,
44
AnkiLinkSettings,
55
AnkiLinkSettingsTab,
66
} from "./settings";
77
import { syncVaultNotes } from "./syncUtil";
8+
import { FC_PREAMBLE_P } from "./regexUtil";
9+
10+
const ANKI_LINK_ICON = "circle-question-mark";
811

912
export default class AnkiLink extends Plugin {
1013
settings!: AnkiLinkSettings;
14+
private statusBarItemEl!: HTMLElement;
15+
private statusBarRefreshToken = 0;
1116

1217
async onload() {
1318
await this.loadSettings();
1419

1520
this.addRibbonIcon(
16-
"circle-question-mark",
21+
ANKI_LINK_ICON,
1722
"Sample",
1823
async (_evt: MouseEvent) => {
1924
await this.runSyncAndNotify();
@@ -36,6 +41,28 @@ export default class AnkiLink extends Plugin {
3641
},
3742
});
3843

44+
this.statusBarItemEl = this.addStatusBarItem();
45+
this.statusBarItemEl.addClass("anki-link-status");
46+
this.statusBarItemEl.addClass("mod-clickable");
47+
this.registerDomEvent(this.statusBarItemEl, "click", () => {
48+
void this.showMissingDeckNotesModal();
49+
});
50+
void this.refreshStatusBar();
51+
52+
this.registerEvent(this.app.workspace.on("file-open", () => {
53+
void this.refreshStatusBar();
54+
}));
55+
this.registerEvent(this.app.vault.on("modify", (file) => {
56+
const activeFile = this.app.workspace.getActiveFile();
57+
if (activeFile?.path !== file.path) return;
58+
void this.refreshStatusBar();
59+
}));
60+
this.registerEvent(this.app.metadataCache.on("changed", (file) => {
61+
const activeFile = this.app.workspace.getActiveFile();
62+
if (activeFile?.path !== file.path) return;
63+
void this.refreshStatusBar();
64+
}));
65+
3966
this.addSettingTab(new AnkiLinkSettingsTab(this.app, this));
4067
}
4168

@@ -100,4 +127,121 @@ export default class AnkiLink extends Plugin {
100127

101128
return "Unknown error";
102129
}
130+
131+
private async refreshStatusBar(): Promise<void> {
132+
const refreshToken = ++this.statusBarRefreshToken;
133+
const activeFile = this.app.workspace.getActiveFile();
134+
if (activeFile?.extension !== "md") {
135+
this.setStatusBarState("Anki: -");
136+
return;
137+
}
138+
139+
const hasFlashcards = await this.fileHasFlashcards(activeFile);
140+
if (refreshToken !== this.statusBarRefreshToken) return;
141+
142+
const configuredDeck = this.getConfiguredDeck(activeFile);
143+
if (!hasFlashcards) {
144+
this.setStatusBarState("Anki: no cards");
145+
return;
146+
}
147+
if (!configuredDeck) {
148+
this.setStatusBarState("Anki: ⚠ deck missing", true);
149+
return;
150+
}
151+
this.setStatusBarState(`Anki: ${configuredDeck}`);
152+
}
153+
154+
private async fileHasFlashcards(file: TFile): Promise<boolean> {
155+
const content = await this.app.vault.read(file);
156+
return content
157+
.split("\n")
158+
.some((line) => FC_PREAMBLE_P.test(line));
159+
}
160+
161+
private async showMissingDeckNotesModal(): Promise<void> {
162+
const loadingNotice = new Notice("Checking notes for missing Anki deck...", 0);
163+
try {
164+
const missingDeckFiles = await this.findNotesMissingDeckNames();
165+
new MissingDeckNotesModal(this, missingDeckFiles).open();
166+
} finally {
167+
loadingNotice.hide();
168+
}
169+
}
170+
171+
private async findNotesMissingDeckNames(): Promise<TFile[]> {
172+
const markdownFiles = this.app.vault.getMarkdownFiles();
173+
const missingDeckFiles: TFile[] = [];
174+
for (const file of markdownFiles) {
175+
if (this.getConfiguredDeck(file)) continue;
176+
if (await this.fileHasFlashcards(file)) {
177+
missingDeckFiles.push(file);
178+
}
179+
}
180+
return missingDeckFiles;
181+
}
182+
183+
private getConfiguredDeck(file: TFile): string | undefined {
184+
const frontmatter = this.app.metadataCache.getFileCache(file)?.frontmatter;
185+
if (!frontmatter) return undefined;
186+
for (const [key, value] of Object.entries(frontmatter)) {
187+
if (key.toLowerCase() !== "anki deck") continue;
188+
if (typeof value !== "string") continue;
189+
const trimmed = value.trim();
190+
if (trimmed.length > 0) {
191+
return trimmed;
192+
}
193+
}
194+
return undefined;
195+
}
196+
197+
private setStatusBarState(text: string, isWarning = false): void {
198+
this.statusBarItemEl.empty();
199+
this.statusBarItemEl.setAttribute("aria-label", text);
200+
this.statusBarItemEl.setAttribute("title", text);
201+
setIcon(this.statusBarItemEl, ANKI_LINK_ICON);
202+
this.statusBarItemEl.style.color = isWarning ? "var(--text-error)" : "";
203+
}
204+
}
205+
206+
class MissingDeckNotesModal extends Modal {
207+
constructor(
208+
private readonly plugin: AnkiLink,
209+
private readonly files: TFile[],
210+
) {
211+
super(plugin.app);
212+
}
213+
214+
override onOpen(): void {
215+
const { contentEl } = this;
216+
contentEl.empty();
217+
contentEl.createEl("h3", { text: "Notes missing Anki deck" });
218+
219+
if (this.files.length === 0) {
220+
contentEl.createEl("p", { text: "No notes with flashcards are missing an Anki deck." });
221+
return;
222+
}
223+
224+
contentEl.createEl("p", {
225+
text: `${this.files.length} note${this.files.length === 1 ? "" : "s"} found.`,
226+
});
227+
const listEl = contentEl.createEl("ul", { cls: "anki-link-missing-deck-list" });
228+
for (const file of this.files) {
229+
const itemEl = listEl.createEl("li");
230+
const linkEl = itemEl.createEl("a", { text: file.path });
231+
linkEl.href = "#";
232+
linkEl.addEventListener("click", (event) => {
233+
event.preventDefault();
234+
void this.openFile(file);
235+
});
236+
}
237+
}
238+
239+
override onClose(): void {
240+
this.contentEl.empty();
241+
}
242+
243+
private async openFile(file: TFile): Promise<void> {
244+
await this.plugin.app.workspace.getLeaf(true).openFile(file);
245+
this.close();
246+
}
103247
}

styles.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ If your plugin does not need CSS, delete this file.
1010
--callout-color: 2, 122, 255;
1111
--callout-icon: lucide-circle-question-mark;
1212
}
13+
14+
.anki-link-missing-deck-list li {
15+
margin-top: 0.25rem;
16+
}

0 commit comments

Comments
 (0)