diff --git a/.editorconfig b/.editorconfig new file mode 100755 index 0000000..a3ebc62 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +max_line_length = 80 +quote_style = single +trim_trailing_whitespace = true + +[{*.js, *.json, *.ts}] +quote_style = double diff --git a/.gitignore b/.gitignore index da93af9..c679350 100644 --- a/.gitignore +++ b/.gitignore @@ -1,33 +1,34 @@ -# Intellij -*.iml -.idea - -# npm -node_modules -package-lock.json - -# build -main.js -*.js.map -docs/test-vault/.obsidian/plugins/flashcards/main.js -docs/test-vault/.obsidian/plugins/flashcards/manifest.json - -# scripts -move.sh - -# Visual Studio Code -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace - -# Local History for Visual Studio Code -.history/ - -# Github -.DS_Store - -# Test-vault -docs/test-vault/.obsidian/workspace +# Obsidian +data.json + +# IntelliJ +.idea/ +*.iml + +# npm +node_modules/ +package-lock.json + +# Build +main.js + +# Scripts +move.sh + +# Visual Studio Code +.vscode/ +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/ + +# macOS +.DS_Store + +# Test vault +docs/test-vault/.obsidian/*.json +!docs/test-vault/.obsidian/community-plugins.json +docs/test-vault/.obsidian/plugins/flashcards-obsidian/* +!docs/test-vault/.obsidian/plugins/flashcards-obsidian/.hotreload +!docs/test-vault/.obsidian/plugins/hot-reload/* diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5e97a9e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "standard.enable": false +} diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 0508d30..b826fbe 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,10 +1,16 @@ # Contributing + Contributions via bug reports, bug fixes, are welcome. If you have ideas about features to be implemented, please open an issue so we can discuss the best way to implement it. ## How to build? + You need to pull the repository, install the dependencies with `node` and then build with the command `npm run dev`. It will automatically move the files into the `docs/test-vault` and hot reload the plugin. $ git clone git@github.com:reuseman/flashcards-obsidian.git $ cd flashcards-obsidian ~/flashcards-obsidian$ npm install - ~/flashcards-obsidian$ npm run dev \ No newline at end of file + ~/flashcards-obsidian$ npm run dev + +### Hot Reload + +You need to download `main.js` and `manifest.json` from https://github.com/pjeby/hot-reload into `docs/test-vault/.obsidian/plugins/hot-reload` if you want to use the hot reload functionality mentioned above. diff --git a/docs/test-vault/.obsidian/app.json b/docs/test-vault/.obsidian/app.json deleted file mode 100644 index bf21f23..0000000 --- a/docs/test-vault/.obsidian/app.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "legacyEditor": false, - "livePreview": true -} \ No newline at end of file diff --git a/docs/test-vault/.obsidian/appearance.json b/docs/test-vault/.obsidian/appearance.json deleted file mode 100644 index 990f337..0000000 --- a/docs/test-vault/.obsidian/appearance.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "baseFontSize": 16 -} \ No newline at end of file diff --git a/docs/test-vault/.obsidian/core-plugins.json b/docs/test-vault/.obsidian/core-plugins.json deleted file mode 100644 index ab1d511..0000000 --- a/docs/test-vault/.obsidian/core-plugins.json +++ /dev/null @@ -1,15 +0,0 @@ -[ - "file-explorer", - "global-search", - "switcher", - "graph", - "backlink", - "page-preview", - "note-composer", - "command-palette", - "editor-status", - "markdown-importer", - "word-count", - "open-with-default-app", - "file-recovery" -] \ No newline at end of file diff --git a/docs/test-vault/.obsidian/hotkeys.json b/docs/test-vault/.obsidian/hotkeys.json deleted file mode 100644 index 9e26dfe..0000000 --- a/docs/test-vault/.obsidian/hotkeys.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/docs/test-vault/.obsidian/plugins/flashcards/.hotreload b/docs/test-vault/.obsidian/plugins/flashcards-obsidian/.hotreload similarity index 100% rename from docs/test-vault/.obsidian/plugins/flashcards/.hotreload rename to docs/test-vault/.obsidian/plugins/flashcards-obsidian/.hotreload diff --git a/docs/test-vault/.obsidian/plugins/flashcards/data.json b/docs/test-vault/.obsidian/plugins/flashcards/data.json deleted file mode 100644 index 5701d93..0000000 --- a/docs/test-vault/.obsidian/plugins/flashcards/data.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "contextAwareMode": true, - "sourceSupport": false, - "codeHighlightSupport": false, - "inlineID": false, - "contextSeparator": " > ", - "deck": "Default", - "flashcardsTag": "card", - "inlineSeparator": "::", - "inlineSeparatorReverse": ":::", - "defaultAnkiTag": "obsidian", - "ankiConnectPermission": false -} \ No newline at end of file diff --git a/docs/test-vault/.obsidian/plugins/hot-reload/main.js b/docs/test-vault/.obsidian/plugins/hot-reload/main.js new file mode 100644 index 0000000..0502508 --- /dev/null +++ b/docs/test-vault/.obsidian/plugins/hot-reload/main.js @@ -0,0 +1,115 @@ +const {Plugin, Notice} = require("obsidian"); +const fs = require("fs"); + +const watchNeeded = window.process.platform !== "darwin" && window.process.platform !== "win32"; + +module.exports = class HotReload extends Plugin { + + statCache = new Map(); // path -> Stat + + onload() { this.app.workspace.onLayoutReady( this._onload.bind(this) ); } + + async _onload() { + this.pluginReloaders = {}; + this.inProgress = null; + await this.getPluginNames(); + this.reindexPlugins = this.debouncedMethod(500, this.getPluginNames); + this.registerEvent( this.app.vault.on("raw", this.onFileChange.bind(this)) ); + this.watch(".obsidian/plugins"); + await this.checkVersions(); + this.addCommand({ + id: "scan-for-changes", + name: "Check plugins for changes and reload them", + callback: () => this.checkVersions() + }) + } + + watch(path) { + if (this.app.vault.adapter.watchers.hasOwnProperty(path)) return; + const realPath = [this.app.vault.adapter.basePath, path].join("/"); + const lstat = fs.lstatSync(realPath); + if (lstat && (watchNeeded || lstat.isSymbolicLink()) && fs.statSync(realPath).isDirectory()) { + this.app.vault.adapter.startWatchPath(path, false); + } + } + + async checkVersions() { + const base = this.app.plugins.getPluginFolder(); + for (const dir of Object.keys(this.pluginNames)) { + for (const file of ["manifest.json", "main.js", "styles.css", ".hotreload"]) { + const path = `${base}/${dir}/${file}`; + const stat = await app.vault.adapter.stat(path); + if (stat) { + if (this.statCache.has(path) && stat.mtime !== this.statCache.get(path).mtime) { + this.onFileChange(path); + } + this.statCache.set(path, stat); + } + } + } + } + + async getPluginNames() { + const plugins = {}, enabled = new Set(); + for (const {id, dir} of Object.values(app.plugins.manifests)) { + this.watch(dir); + plugins[dir.split("/").pop()] = id; + if ( + await this.app.vault.exists(dir+"/.git") || + await this.app.vault.exists(dir+"/.hotreload") + ) enabled.add(id); + } + this.pluginNames = plugins; + this.enabledPlugins = enabled; + } + + onFileChange(filename) { + if (!filename.startsWith(this.app.plugins.getPluginFolder()+"/")) return; + const path = filename.split("/"); + const base = path.pop(), dir = path.pop(); + if (path.length === 1 && dir === "plugins") return this.watch(filename); + if (path.length != 2) return; + const plugin = dir && this.pluginNames[dir]; + if (base === "manifest.json" || base === ".hotreload" || base === ".git" || !plugin) return this.reindexPlugins(); + if (base !== "main.js" && base !== "styles.css") return; + if (!this.enabledPlugins.has(plugin)) return; + const reloader = this.pluginReloaders[plugin] || ( + this.pluginReloaders[plugin] = this.debouncedMethod(750, this.requestReload, plugin) + ); + reloader(); + } + + requestReload(plugin) { + const next = this.inProgress = this.reload(plugin, this.inProgress); + next.finally(() => {if (this.inProgress === next) this.inProgress = null;}) + } + + async reload(plugin, previous) { + const plugins = this.app.plugins; + try { + // Wait for any other queued/in-progress reloads to finish + await previous; + await plugins.disablePlugin(plugin); + console.debug("disabled", plugin); + // Ensure sourcemaps are loaded (Obsidian 14+) + const oldDebug = localStorage.getItem("debug-plugin"); + localStorage.setItem("debug-plugin", "1"); + try { + await plugins.enablePlugin(plugin); + } finally { + // Restore previous setting + if (oldDebug === null) localStorage.removeItem("debug-plugin"); else localStorage.setItem("debug-plugin", oldDebug); + } + console.debug("enabled", plugin); + new Notice(`Plugin "${plugin}" has been reloaded`); + } catch(e) {} + } + + debouncedMethod(ms, func, ...args) { + var timeout; + return () => { + if (timeout) clearTimeout(timeout); + timeout = setTimeout( () => { timeout = null; func.apply(this, args); }, ms); + } + } +} \ No newline at end of file diff --git a/package.json b/package.json index b58eeae..d86a0e9 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "An Anki integration.", "main": "main.js", "scripts": { - "dev": "rollup --config rollup.config.js -w --environment BUILD:dev", + "dev": "cp manifest.json docs/test-vault/.obsidian/plugins/flashcards-obsidian/ && rollup --config rollup.config.js -w --environment BUILD:dev", "build": "rollup --config rollup.config.js --environment BUILD:production", "test": "jest" }, diff --git a/src/services/anki.ts b/src/services/anki.ts index 3f04a4a..7109f9e 100644 --- a/src/services/anki.ts +++ b/src/services/anki.ts @@ -139,7 +139,11 @@ export class Anki { return await this.invoke("cardsInfo", 6, { cards: ids }); } - public async getCards(ids: number[]) { + public async findNotes(deckName: string) { + return await this.invoke("findNotes", 6, { query: `deck:"${deckName}"` }); + } + + public async notesInfo(ids: number[]) { return await this.invoke("notesInfo", 6, { notes: ids }); } @@ -304,7 +308,7 @@ export class Anki { }, ], }, - + } const obsidianSpaced = { diff --git a/src/services/cards.ts b/src/services/cards.ts index 238ea86..678b4e6 100644 --- a/src/services/cards.ts +++ b/src/services/cards.ts @@ -60,7 +60,10 @@ export class CardsService { let deckName = ""; if (parseFrontMatterEntry(frontmatter, "cards-deck")) { deckName = parseFrontMatterEntry(frontmatter, "cards-deck"); - } else if (this.settings.folderBasedDeck && activeFile.parent.path !== "/") { + } else if ( + this.settings.folderBasedDeck && + activeFile.parent.path !== "/" + ) { // If the current file is in the path "programming/java/strings.md" then the deck name is "programming::java" deckName = activeFile.parent.path.split("/").join("::"); } else { @@ -81,8 +84,14 @@ export class CardsService { globalTags = this.parseGlobalTags(this.file); // TODO with empty check that does not call ankiCards line const ankiBlocks = this.parser.getAnkiIDsBlocks(this.file); + const ankiIDs = this.getAnkiIDs(ankiBlocks); + const ankiNotes = await this.anki.findNotes(deckName); + // const inObsidianNotAnki = ankiIDs.filter((x) => !ankiNotes.includes(x)); + // const inAnkiNotObsidian = ankiNotes.filter((x) => !ankiIDs.includes(x)); + const inBoth = ankiIDs.filter((x) => ankiNotes.includes(x)); + const ankiCards = ankiBlocks - ? await this.anki.getCards(this.getAnkiIDs(ankiBlocks)) + ? await this.anki.notesInfo(inBoth) : undefined; const cards: Card[] = this.parser.generateFlashcards(