diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index abe6453..03aba27 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -10,7 +10,7 @@ permissions: contents: read env: - version: '1.13.${{ github.run_number }}' + version: '1.14.${{ github.run_number }}' nodeVersion: '22' jobs: diff --git a/README.md b/README.md index 7cc399d..591a645 100644 --- a/README.md +++ b/README.md @@ -137,8 +137,8 @@ The following rules are validated in the section footer: - **Card legality** — flags banned/not legal/restricted cards - **Deck size** — flags when the deck is too small or too large for the format - **Sideboard size** — flags when the sideboard exceeds the maximum for the format -- **Number of copies** - flags when a card is included more than the allowed times (4/1), handles cards like Hare Apparent! -- **Commander rules** - flags when a card is not a legal commander, or cards in the deck that or outside of the color identity +- **Number of copies** — flags when a card is included more than the allowed times (4/1), handles cards like Hare Apparent! +- **Commander rules** — flags when a card is not a legal commander, or cards in the deck that are outside of the color identity # ⚙️ Settings @@ -183,13 +183,11 @@ Supported show values: # 📦 Collection Tracking -This plugin expects your collection to be stored as CSV files with the extension `.mtg.collection.csv` by default. +This plugin tracks how many copies of each card you own by reading CSV files from a configurable folder. Cards where you don't own enough copies are highlighted in red, and a count shows owned vs. needed (e.g. `2 / 4`). A **Buylist** section is automatically shown at the bottom when you're missing cards. -These files are expected to be properly formed CSVs with a Name and a Count column. Column names can be configured through the settings. +Collection CSV files must have at least two columns: one for the card name and one for the count. Column names are configurable in settings and default to `Name` and `Count`. Multiple CSV files are supported — the plugin merges all files in the configured folder and its subfolders. -Cards where you don't own enough copies are highlighted in red, and a count shows owned vs. needed (e.g. `2 / 4`). A **Buylist** section is automatically shown at the bottom when you're missing cards. - -## Example CSV Files +## Example CSV File ``` Name,Count @@ -199,17 +197,30 @@ Delver of Secrets // Insectile Aberration,8 Ledger Shredder,5 ``` -Note that your collection will consist of the merged result of all of your CSV files. +## Viewing Your Collection + +You can view your entire collection in two ways: + +**Command** — open the command palette and run `MTG Deck: View collection`. This opens a modal with your full collection, showing counts per collection file, mana costs, and rarity indicators. A search box lets you filter by card name. + +**Code block** — embed your collection in any note using the `mtg-collection` code block: + +````markdown +```mtg-collection +``` +```` + +This renders an inline table with all your cards, per-file counts, a grand total column, and a search filter. Card data is loaded from Scryfall and a progress indicator shows loading status. # 🔍 Obsidian Plugin Scorecard ## Network Requests -This plugin makes network requests to the [Scryfall API](https://scryfall.com/docs/api) to fetch card data, images, and prices. Requests are only made when rendering a decklist code block and are batched to minimize the number of calls. No user data is sent to Scryfall — only card names and set codes are used to identify cards. +This plugin makes network requests to the [Scryfall API](https://scryfall.com/docs/api) to fetch card data, images, and prices. Requests are only made when rendering a decklist or collection code block and are batched to minimize the number of calls. Fetched card data is cached for the duration of the session. No user data is sent to Scryfall — only card names and set codes are used to identify cards. ## Vault Access -This plugin enumerates files in your vault to find collection CSV files in the configured collection folder. Only files with the `.csv` extension in the configured folder are read. No other vault files are accessed. +This plugin enumerates files in your vault to find collection CSV files in the configured collection folder and its subfolders. Only files with the `.csv` extension are read. No other vault files are accessed. ## CSS diff --git a/main.ts b/main.ts index 8b43e33..3a0d521 100644 --- a/main.ts +++ b/main.ts @@ -1,14 +1,20 @@ -import { App, FuzzySuggestModal, Plugin, PluginSettingTab, Setting, TFolder, Vault } from "obsidian"; +import { App, FuzzySuggestModal, Plugin, PluginSettingTab, Setting, TFolder } from "obsidian"; import { - DEFAULT_COLLECTION_COUNT_COLUMN, - DEFAULT_COLLECTION_FOLDER_PATH, - DEFAULT_COLLECTION_NAME_COLUMN, - syncCounts, + CardCollection, + CardCounts, + DEFAULT_COLLECTION_COUNT_COLUMN, + DEFAULT_COLLECTION_FOLDER_PATH, + DEFAULT_COLLECTION_NAME_COLUMN, + hashCollectionContents, + mergeCollections, + syncCollections, } from "src/collection"; import { renderDecklist } from "src/renderer"; import { ObsidianPluginMtgSettings } from "src/settings"; -import { CardCounts } from "src/collection"; import { parseCodeBlockOptions, applyShowOverrides } from "src/code-block-options"; +import { CollectionModal } from "src/collection-modal"; +import { renderCollection } from "src/collection-renderer"; +import { loadCache, getCache, clearCache } from "src/cache"; const DEFAULT_SETTINGS: ObsidianPluginMtgSettings = { collection: { @@ -32,6 +38,7 @@ export default class ObsidianPluginMtg extends Plugin { // This keeps a record of the collection in memory cardCounts: CardCounts; + collections: CardCollection[] = []; async onload() { await this.loadSettings(); @@ -43,17 +50,33 @@ export default class ObsidianPluginMtg extends Plugin { vault.on("modify", async (file) => { if (file.name.endsWith(".csv")) { - const settings = this.settings; - const collectionFolderPath = - settings.collection?.folderPath || ""; - if (file.parent?.path === collectionFolderPath) { - this.cardCounts = await syncCounts(vault, settings); + const collectionFolderPath = this.settings.collection?.folderPath || ""; + if (file.parent?.path.startsWith(collectionFolderPath)) { + this.collections = await syncCollections(vault, this.settings); + this.cardCounts = mergeCollections(this.collections); + clearCache(); } } }); this.app.workspace.onLayoutReady(async () => { - this.cardCounts = await syncCounts(vault, this.settings); + this.collections = await syncCollections(vault, this.settings); + this.cardCounts = mergeCollections(this.collections); + + const hash = hashCollectionContents(this.collections); + + const savedData = await this.loadData(); + if (savedData?.collectionHash === hash && savedData?.cardDataCache) { + loadCache(savedData.cardDataCache); + } + }); + + this.addCommand({ + id: "view-collection", + name: "View collection", + callback: () => { + new CollectionModal(this.app, this.collections).open(); + }, }); this.registerMarkdownPostProcessor((element) => { @@ -79,43 +102,44 @@ export default class ObsidianPluginMtg extends Plugin { } })(); }); + + this.registerMarkdownCodeBlockProcessor( + "mtg-collection", + (_source, el) => { + void renderCollection(el, this.collections); + } + ); } - onunload() {} + onunload() { + void (async () => { + const hash = hashCollectionContents(this.collections); + const data = await this.loadData() ?? {}; + + await this.saveData({ + ...data, + settings: this.settings, + collectionHash: hash, + cardDataCache: getCache(), + }); + })(); + } async loadSettings() { + const data = await this.loadData(); this.settings = Object.assign( {}, DEFAULT_SETTINGS, - await this.loadData() as Partial + data?.settings as Partial ); } async saveSettings() { - await this.saveData(this.settings); - } - - async renderDecklist(vault: Vault, source: string, el: HTMLElement, format: string | null = null) { - // Sync card counts once if they haven't been already - if (!this.cardCounts) { - this.cardCounts = await syncCounts(vault, this.settings); - } - - try { - await renderDecklist( - el, - source, - this.cardCounts, - this.settings, - format - ); - } catch (err) { - console.error(err); - el.createDiv({ - text: String(err), - cls: "obsidian-plugin-mtg-error", - }); - } + const data = await this.loadData() ?? {}; + await this.saveData({ + ...data, + settings: this.settings, + }); } } @@ -316,6 +340,10 @@ class FolderSuggestModal extends FuzzySuggestModal { this.app.vault.getFiles().forEach((file) => { if (file.extension === "csv" && file.parent) { folders.set(file.parent.path, file.parent); + + if (file.parent.parent) { + folders.set(file.parent.parent.path, file.parent.parent); + } } }); diff --git a/manifest.json b/manifest.json index c095488..c1d8394 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "mtg-deck", "name": "MTG Deck", - "version": "1.13.106", + "version": "1.14.128", "minAppVersion": "1.5.7", "description": "Display your MTG decks and card lists in your notes.", "author": "Samir Boulema", diff --git a/package-lock.json b/package-lock.json index f726c1b..ce62c13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mtg-deck", - "version": "1.13.106", + "version": "1.14.123", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mtg-deck", - "version": "1.13.106", + "version": "1.14.123", "license": "MIT", "devDependencies": { "@eslint/eslintrc": "^3.3.5", @@ -22,11 +22,12 @@ "jest": "^30.4.2", "jest-environment-jsdom": "^30.4.1", "jsdom": "^29.1.1", - "obsidian": "*", + "obsidian": "latest", "prettier": "3.9.4", "ts-jest": "^29.0.0", "tslib": "2.8.1", - "typescript": "^7.0.2" + "typescript": "6.0.3", + "typescript-eslint": "^8.63.0" }, "engines": { "node": ">=20" @@ -2513,45 +2514,6 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/project-service": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz", - "integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.63.0", - "@typescript-eslint/types": "^8.63.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz", - "integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { "version": "8.63.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz", @@ -2577,81 +2539,6 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz", - "integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.63.0", - "@typescript-eslint/tsconfig-utils": "8.63.0", - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/visitor-keys": "8.63.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz", - "integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.63.0", - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/typescript-estree": "8.63.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -2662,22 +2549,6 @@ "node": ">= 4" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/parser": { "version": "8.63.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz", @@ -2703,7 +2574,7 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": { + "node_modules/@typescript-eslint/project-service": { "version": "8.63.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz", "integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==", @@ -2725,7 +2596,25 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": { + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz", + "integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { "version": "8.63.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz", "integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==", @@ -2742,7 +2631,21 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "node_modules/@typescript-eslint/types": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz", + "integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { "version": "8.63.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz", "integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==", @@ -2770,7 +2673,7 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/parser/node_modules/balanced-match": { + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", @@ -2780,7 +2683,7 @@ "node": "18 || 20 || >=22" } }, - "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", @@ -2793,7 +2696,7 @@ "node": "18 || 20 || >=22" } }, - "node_modules/@typescript-eslint/parser/node_modules/minimatch": { + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", @@ -2809,15 +2712,17 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/scope-manager": { + "node_modules/@typescript-eslint/utils": { "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz", - "integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz", + "integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==", "dev": true, "license": "MIT", "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.63.0", "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/visitor-keys": "8.63.0" + "@typescript-eslint/typescript-estree": "8.63.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2825,20 +2730,10 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz", - "integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { @@ -2872,346 +2767,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@typescript/typescript-aix-ppc64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", - "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-darwin-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", - "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-darwin-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", - "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-freebsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", - "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-freebsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", - "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-arm": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", - "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", - "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-loong64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", - "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-mips64el": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", - "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-ppc64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", - "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-riscv64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", - "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-s390x": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", - "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", - "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-netbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", - "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-netbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", - "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-openbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", - "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-openbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", - "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-sunos-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", - "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-win32-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", - "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-win32-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", - "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16.20.0" - } - }, "node_modules/@ungap/structured-clone": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.2.tgz", @@ -5525,136 +5080,6 @@ "undici-types": "~5.26.4" } }, - "node_modules/eslint-plugin-obsidianmd/node_modules/@typescript-eslint/project-service": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz", - "integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.63.0", - "@typescript-eslint/types": "^8.63.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/eslint-plugin-obsidianmd/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz", - "integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/eslint-plugin-obsidianmd/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz", - "integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.63.0", - "@typescript-eslint/tsconfig-utils": "8.63.0", - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/visitor-keys": "8.63.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/eslint-plugin-obsidianmd/node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/eslint-plugin-obsidianmd/node_modules/@typescript-eslint/utils": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz", - "integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.63.0", - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/typescript-estree": "8.63.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/eslint-plugin-obsidianmd/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/eslint-plugin-obsidianmd/node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/eslint-plugin-obsidianmd/node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -5938,30 +5363,6 @@ "node": ">=14.17" } }, - "node_modules/eslint-plugin-obsidianmd/node_modules/typescript-eslint": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz", - "integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.63.0", - "@typescript-eslint/parser": "8.63.0", - "@typescript-eslint/typescript-estree": "8.63.0", - "@typescript-eslint/utils": "8.63.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, "node_modules/eslint-plugin-obsidianmd/node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -10629,38 +10030,41 @@ } }, "node_modules/typescript": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", - "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { - "tsc": "bin/tsc" + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "engines": { - "node": ">=16.20.0" + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz", + "integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.63.0", + "@typescript-eslint/parser": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/utils": "8.63.0" }, - "optionalDependencies": { - "@typescript/typescript-aix-ppc64": "7.0.2", - "@typescript/typescript-darwin-arm64": "7.0.2", - "@typescript/typescript-darwin-x64": "7.0.2", - "@typescript/typescript-freebsd-arm64": "7.0.2", - "@typescript/typescript-freebsd-x64": "7.0.2", - "@typescript/typescript-linux-arm": "7.0.2", - "@typescript/typescript-linux-arm64": "7.0.2", - "@typescript/typescript-linux-loong64": "7.0.2", - "@typescript/typescript-linux-mips64el": "7.0.2", - "@typescript/typescript-linux-ppc64": "7.0.2", - "@typescript/typescript-linux-riscv64": "7.0.2", - "@typescript/typescript-linux-s390x": "7.0.2", - "@typescript/typescript-linux-x64": "7.0.2", - "@typescript/typescript-netbsd-arm64": "7.0.2", - "@typescript/typescript-netbsd-x64": "7.0.2", - "@typescript/typescript-openbsd-arm64": "7.0.2", - "@typescript/typescript-openbsd-x64": "7.0.2", - "@typescript/typescript-sunos-x64": "7.0.2", - "@typescript/typescript-win32-arm64": "7.0.2", - "@typescript/typescript-win32-x64": "7.0.2" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/uglify-js": { diff --git a/package.json b/package.json index bc62725..c0a9473 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mtg-deck", - "version": "1.13.106", + "version": "1.14.128", "description": "Display your MTG decks and card lists in your notes.", "main": "main.js", "scripts": { @@ -20,24 +20,25 @@ "author": "Samir Boulema", "license": "MIT", "devDependencies": { - "@eslint/eslintrc": "^3.3.5", + "@eslint/eslintrc": "^3.3.6", "@eslint/js": "^10.0.1", "@jest/globals": "^30.4.1", "@types/node": "^26.1.1", "@typescript-eslint/eslint-plugin": "^8.63.0", "@typescript-eslint/parser": "8.63.0", "esbuild": "0.28.1", - "eslint": "^10.6.0", + "eslint": "^10.7.0", "eslint-plugin-obsidianmd": "^0.4.1", "globals": "^17.7.0", "jest": "^30.4.2", "jest-environment-jsdom": "^30.4.1", "jsdom": "^29.1.1", "obsidian": "latest", - "prettier": "3.9.4", + "prettier": "3.9.5", "ts-jest": "^29.0.0", "tslib": "2.8.1", - "typescript": "^7.0.2" + "typescript": "6.0.3", + "typescript-eslint": "^8.63.0" }, "allowScripts": { "esbuild@0.28.1": true diff --git a/src/cache.ts b/src/cache.ts new file mode 100644 index 0000000..249c187 --- /dev/null +++ b/src/cache.ts @@ -0,0 +1,34 @@ +import { CardData } from "./scryfall"; + +interface CachedCardData { + data: CardData; +} + +let cardDataCache: Record = {}; + +export const loadCache = (data: Record): void => { + cardDataCache = data; +}; + +export const getCache = (): Record => { + return cardDataCache; +}; + +export const clearCache = (): void => { + cardDataCache = {}; +}; + +export const getCachedCardData = (): Record => { + return Object.fromEntries( + Object.entries(cardDataCache) + .map(([key, entry]) => [key, entry.data]) + ); +}; + +export const setCachedCardData = (key: string, data: CardData): void => { + cardDataCache[key] = { data }; +}; + +export const hasCachedCardData = (key: string): boolean => { + return !!cardDataCache[key]; +}; \ No newline at end of file diff --git a/src/collection-modal.ts b/src/collection-modal.ts new file mode 100644 index 0000000..a03ece1 --- /dev/null +++ b/src/collection-modal.ts @@ -0,0 +1,21 @@ +import { App, Modal } from "obsidian"; +import { CardCollection, CardCounts } from "./collection"; +import { renderCollection } from "./collection-renderer"; + +export class CollectionModal extends Modal { + collections: CardCollection[]; + + constructor(app: App, collections: CardCollection[]) { + super(app); + this.collections = collections; + } + + onOpen() { + this.modalEl.addClass("collection-modal"); + void renderCollection(this.contentEl, this.collections); + } + + onClose() { + this.contentEl.empty(); + } +} \ No newline at end of file diff --git a/src/collection-renderer.ts b/src/collection-renderer.ts new file mode 100644 index 0000000..c729722 --- /dev/null +++ b/src/collection-renderer.ts @@ -0,0 +1,142 @@ +import { CardCollection, nameToId } from "./collection"; +import { CardData, fetchCardDataFromScryfallCached } from "./scryfall"; + +const renderRows = ( + tbody: HTMLElement, + entries: string[], + collections: CardCollection[], + cardDataByCardId: Record, + filter: string +) => { + tbody.empty(); + + entries + .filter(name => name.includes(filter.toLowerCase())) + .forEach(name => { + const cardInfo = cardDataByCardId[nameToId(name)]; + const row = tbody.createEl("tr"); + + // Name cell with rarity dot and hyperlink + const nameCell = row.createEl("td", { cls: "max" }); + nameCell.createSpan({ cls: `card-rarity ${cardInfo?.rarity ?? ""}` }); + const nameEl = nameCell.createSpan({ cls: "card-name" }); + + if (cardInfo?.scryfall_uri) { + const link = nameEl.createEl("a"); + link.href = cardInfo.scryfall_uri; + link.textContent = cardInfo.printed_name ?? cardInfo.name ?? name; + } else { + nameEl.textContent = cardInfo?.printed_name ?? cardInfo?.name ?? name; + } + + // Mana cost cell + const costEl = row.createEl("td").createSpan({ cls: "card-cost" }); + const manaCost = cardInfo?.mana_cost ?? cardInfo?.card_faces?.[0]?.mana_cost; + + if (manaCost) { + manaCost + .split("//") + .map(part => part.trim()) + .forEach((part, index) => { + if (index > 0) { + costEl.createSpan({ cls: "card-cost-divider" }); + } + part + .replace(/\//g, "") + .split("{") + .slice(1) + .forEach(symbol => { + costEl.createEl("img", { + attr: { + src: `https://svgs.scryfall.io/card-symbols/${symbol.slice(0, -1)}.svg`, + width: 18, + height: 18, + }, + }); + }); + }); + } + + // Per-collection count cells + let total = 0; + collections.forEach(c => { + const count = c.counts[name] ?? 0; + total += count; + row.createEl("td", { text: count > 0 ? `${count}` : "-" }); + }); + + // Total cell + row.createEl("td", { text: `${total}` }); + }); +}; + +export const renderCollection = async ( + root: HTMLElement, + collections: CardCollection[] +): Promise => { + const containerEl = root.createDiv({ cls: "decklist" }); + + const searchEl = containerEl.createEl("input", { + attr: { type: "text", placeholder: "Search cards..." }, + cls: "collection__search", + }); + + const table = containerEl.createEl("table"); + const thead = table.createEl("thead"); + const headerRow = thead.createEl("tr"); + headerRow.createEl("th", { text: "Name", cls: "max" }); + headerRow.createEl("th", { text: "Cost" }); + collections.forEach(c => { + headerRow.createEl("th", { text: c.fileName }); + }); + headerRow.createEl("th", { text: "Total" }); + + const tbody = table.createEl("tbody"); + + // Build sorted list of all unique card names across all collections + const allCardNames = Array.from( + new Set(collections.flatMap(c => Object.keys(c.counts))) + ).sort((a, b) => a.localeCompare(b)); + + // Render skeleton immediately + renderRows(tbody, allCardNames, collections, {}, ""); + + const loadingEl = containerEl.createDiv({ cls: "collection__loading" }); + const progressEl = loadingEl.createSpan({ text: `0 / ${allCardNames.length}` }); + loadingEl.createDiv({ cls: "collection__spinner" }); + + // Fetch card data and re-render + const identifiers = allCardNames.map(name => ({ name })); + const cardDataByCardId = await fetchCardDataFromScryfallCached( + identifiers, + (fetched, total) => { + progressEl.textContent = `${fetched} / ${total}`; + } + ); + + loadingEl.remove(); + + renderRows(tbody, allCardNames, collections, cardDataByCardId, ""); + + // Footer with per-collection totals and grand total + const tfoot = table.createEl("tfoot", { cls: "decklist__section-totals" }); + const footRow = tfoot.createEl("tr"); + footRow.createEl("td", { text: "Total", cls: "max" }); + footRow.createEl("td"); + + collections.forEach(c => { + const total = Object.values(c.counts).reduce((acc, v) => acc + v, 0); + footRow.createEl("td", { text: `${total}` }); + }); + + const grandTotal = collections.reduce( + (acc, c) => acc + Object.values(c.counts).reduce((a, v) => a + v, 0), + 0 + ); + footRow.createEl("td", { text: `${grandTotal}` }); + + // Search filter + searchEl.addEventListener("input", () => { + renderRows(tbody, allCardNames, collections, cardDataByCardId, searchEl.value); + }); +}; \ No newline at end of file diff --git a/src/collection.ts b/src/collection.ts index 0c1f725..7dc4c1f 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -1,9 +1,16 @@ -import { TFile, Vault } from "obsidian"; +import { Vault } from "obsidian"; import { parseCsvFile } from "./csv"; import { ObsidianPluginMtgSettings } from "./settings"; export type CardCounts = Record; +export interface CardCollection { + fileName: string; + counts: CardCounts; +} + +export type CollectionData = CardCollection[]; + export const DEFAULT_COLLECTION_FOLDER_PATH = "Collections"; export const DEFAULT_COLLECTION_NAME_COLUMN = "Name"; export const DEFAULT_COLLECTION_COUNT_COLUMN = "Count"; @@ -55,42 +62,51 @@ export const createCardCountsMapping = ( export const processCollectionFiles = async ( vault: Vault, settings: ObsidianPluginMtgSettings -): Promise => { - const folder = vault.getFolderByPath( - settings.collection.folderPath - ); +): Promise => { + const folderPath = settings.collection.folderPath; - if (!folder) { + if (!folderPath) { return []; } - return ( - ( - await Promise.all( - folder.children - .filter( - (file): file is TFile => - file instanceof TFile && - file.extension === "csv" - ) - .map((file) => vault.cachedRead(file).catch(() => "")) - ) - ) - // remove unreadable files - .filter((s) => s.length) + const files = vault.getFiles().filter( + file => file.extension === "csv" && + file.parent?.path.startsWith(folderPath) ); + + if (files.length === 0) { + return []; + } + + return (await Promise.all( + files.map(async file => { + const content = await vault.cachedRead(file).catch(() => ""); + if (!content.length) return null; + return { + fileName: file.basename, + counts: createCardCountsMapping([content], settings), + }; + }) + )).filter((c): c is CardCollection => c !== null); }; -export const syncCounts = async ( +export const syncCollections = async ( vault: Vault, settings: ObsidianPluginMtgSettings -): Promise => { - // Sync collection - const collectionContents: string[] = await processCollectionFiles( - vault, - settings - ); +): Promise => { + return processCollectionFiles(vault, settings); +}; - // Create consolidated collection dictionary - return createCardCountsMapping(collectionContents, settings); +export const hashCollectionContents = (contents: CardCollection[]): string => { + const combined = contents.map(c => Object.entries(c.counts).join("")).join(""); + return `${contents.length}-${combined.length}`; +}; + +export const mergeCollections = (collections: CardCollection[]): CardCounts => { + return collections.reduce((acc, collection) => { + Object.entries(collection.counts).forEach(([name, count]) => { + acc[name] = (acc[name] ?? 0) + count; + }); + return acc; + }, {} as CardCounts); }; \ No newline at end of file diff --git a/src/scryfall.ts b/src/scryfall.ts index d3f07b0..5f50b78 100644 --- a/src/scryfall.ts +++ b/src/scryfall.ts @@ -1,14 +1,15 @@ import { nameToId } from "./collection"; import { promiseWrappedRequest } from "./http"; +import { getCachedCardData, setCachedCardData, hasCachedCardData } from "./cache"; export type CardIdentifier = - | { - name: string; - } - | { - set: string; - collector_number: string; - }; + | { + name: string; + } + | { + set: string; + collector_number: string; + }; export interface RequestOptions { url: string; @@ -21,11 +22,10 @@ export interface RequestOptions { export type Request = (options: RequestOptions) => Promise; -// This is the maximum number of cards that can be requested at the same time export const MAX_SCRYFALL_BATCH_SIZE = 75; export interface CardFace { - object?: string; // card_face + object?: string; name?: string; mana_cost?: string; type_line?: string; @@ -50,7 +50,7 @@ export interface CardFace { } export interface CardData { - object?: string; // card + object?: string; id?: string; oracle_id?: string; multiverse_ids?: number[]; @@ -194,41 +194,75 @@ export const getMultipleCardData = async ( * @returns A record of card data indexed by normalized card name */ export const fetchCardDataFromScryfall = async ( - distinctCards: CardIdentifier[] + distinctCards: CardIdentifier[], + onProgress?: (fetched: number, total: number) => void ): Promise> => { - const batches: CardIdentifier[][] = []; - let currentBatch: CardIdentifier[] = []; - batches.push(currentBatch); + const batches: CardIdentifier[][] = []; + let currentBatch: CardIdentifier[] = []; + batches.push(currentBatch); - distinctCards.forEach((identifier: CardIdentifier) => { - if (currentBatch.length === MAX_SCRYFALL_BATCH_SIZE) { - batches.push(currentBatch); - currentBatch = []; - } - currentBatch.push(identifier); - }); + distinctCards.forEach((identifier: CardIdentifier) => { + if (currentBatch.length === MAX_SCRYFALL_BATCH_SIZE) { + batches.push(currentBatch); + currentBatch = []; + } + currentBatch.push(identifier); + }); - batches.push(currentBatch); + batches.push(currentBatch); - const cardDataInBatches: ScryfallResponse[] = await Promise.all( - batches.map((batch) => getMultipleCardData(batch)) - ); + const cardDataByCardId: Record = {}; + let fetched = 0; + const total = distinctCards.length; - const cardDataByCardId: Record = {}; + for (const batch of batches) { + const result: ScryfallResponse = await getMultipleCardData(batch); - cardDataInBatches.forEach((batch) => { - batch.data.forEach((card: CardData) => { - if (card.name) { - const cardId = nameToId(card.name); - cardDataByCardId[cardId] = card; - } - - if (card.printed_name) { - const cardId = nameToId(card.printed_name); - cardDataByCardId[cardId] = card; + result.data.forEach((card: CardData) => { + if (card.name) { + cardDataByCardId[nameToId(card.name)] = card; } - }); - }); + if (card.printed_name) { + cardDataByCardId[nameToId(card.printed_name)] = card; + } + }); - return cardDataByCardId; + fetched += batch.length; + onProgress?.(fetched, total); + } + + return cardDataByCardId; +}; + +/** + * Fetches card data from the Scryfall API, using a persistent cache. + * Only cards not already in the cache are fetched. The cache is keyed by + * normalized card name and persists across renders for the duration of + * the session or until the collection changes. + * + * @param distinctCards - List of distinct card identifiers to fetch + * @param onProgress - Optional callback called when all cards are resolved + * @returns A record of card data indexed by normalized card name + */ +export const fetchCardDataFromScryfallCached = async ( + distinctCards: CardIdentifier[], + onProgress?: (fetched: number, total: number) => void +): Promise> => { + const uncachedCards = distinctCards.filter(identifier => { + const key = "name" in identifier + ? identifier.name + : `${identifier.set}-${identifier.collector_number}`; + return !hasCachedCardData(key); + }); + + if (uncachedCards.length > 0) { + const freshData = await fetchCardDataFromScryfall(uncachedCards, onProgress); + Object.entries(freshData).forEach(([key, data]) => { + setCachedCardData(key, data); + }); + } else { + onProgress?.(distinctCards.length, distinctCards.length); + } + + return getCachedCardData(); }; \ No newline at end of file diff --git a/styles.css b/styles.css index cf714f9..3f6ccb6 100644 --- a/styles.css +++ b/styles.css @@ -77,10 +77,6 @@ width: 18px; } -.card-image { - border-radius: 8px; -} - span.card-name { flex: 2; } @@ -90,6 +86,11 @@ span.comment { font-style: italic; } +.card-image { + border-radius: 8px; + width: 200px; +} + .card-image-container { display: none; position: fixed; @@ -228,3 +229,27 @@ span.comment { font-size: 0.85em; font-weight: bold; } + +.collection__search { + width: 100%; + margin-bottom: 8px; + padding: 4px 8px; +} + +.collection__loading { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 0; + color: var(--text-muted); + font-size: 0.9em; +} + +.collection__spinner { + width: 16px; + height: 16px; + border: 2px solid var(--background-modifier-border); + border-top-color: var(--interactive-accent); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} \ No newline at end of file diff --git a/versions.json b/versions.json index bbccd0b..e10ce11 100644 --- a/versions.json +++ b/versions.json @@ -27,5 +27,6 @@ "1.11.80": "0.15.0", "1.12.88": "1.1.0", "1.13.105": "1.5.7", - "1.13.106": "1.5.7" + "1.13.106": "1.5.7", + "1.14.128": "1.5.7" } \ No newline at end of file