From 3ea2b36990025429eb3447825d4e1ab134c4edc8 Mon Sep 17 00:00:00 2001 From: "Samir L. Boulema" Date: Thu, 9 Jul 2026 22:01:07 +0200 Subject: [PATCH 1/5] feat: Add collection viewing --- .github/workflows/workflow.yml | 2 +- README.md | 31 +- main.ts | 30 +- manifest.json | 2 +- package-lock.json | 758 ++++----------------------------- package.json | 5 +- src/collection-modal.ts | 21 + src/collection-renderer.ts | 142 ++++++ src/collection.ts | 69 +-- src/scryfall.ts | 50 ++- styles.css | 33 +- versions.json | 3 +- 12 files changed, 417 insertions(+), 729 deletions(-) create mode 100644 src/collection-modal.ts create mode 100644 src/collection-renderer.ts 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..b2041fb 100644 --- a/main.ts +++ b/main.ts @@ -1,14 +1,18 @@ import { App, FuzzySuggestModal, Plugin, PluginSettingTab, Setting, TFolder, Vault } from "obsidian"; import { + CardCollection, DEFAULT_COLLECTION_COUNT_COLUMN, DEFAULT_COLLECTION_FOLDER_PATH, DEFAULT_COLLECTION_NAME_COLUMN, + syncCollections, syncCounts, } 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"; const DEFAULT_SETTINGS: ObsidianPluginMtgSettings = { collection: { @@ -32,6 +36,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 +48,25 @@ 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.cardCounts = await syncCounts(vault, this.settings); + this.collections = await syncCollections(vault, this.settings); } } }); this.app.workspace.onLayoutReady(async () => { this.cardCounts = await syncCounts(vault, this.settings); + this.collections = await syncCollections(vault, this.settings); + }); + + this.addCommand({ + id: "view-collection", + name: "View collection", + callback: () => { + new CollectionModal(this.app, this.collections).open(); + }, }); this.registerMarkdownPostProcessor((element) => { @@ -79,6 +92,13 @@ export default class ObsidianPluginMtg extends Plugin { } })(); }); + + this.registerMarkdownCodeBlockProcessor( + "mtg-collection", + (_source, el) => { + void renderCollection(el, this.collections); + } + ); } onunload() {} diff --git a/manifest.json b/manifest.json index c095488..32bed05 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "mtg-deck", "name": "MTG Deck", - "version": "1.13.106", + "version": "1.14.123", "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..4609846 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mtg-deck", - "version": "1.13.106", + "version": "1.14.123", "description": "Display your MTG decks and card lists in your notes.", "main": "main.js", "scripts": { @@ -37,7 +37,8 @@ "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" }, "allowScripts": { "esbuild@0.28.1": true 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..9b43b64 --- /dev/null +++ b/src/collection-renderer.ts @@ -0,0 +1,142 @@ +import { CardCollection, nameToId } from "./collection"; +import { CardData, fetchCardDataFromScryfall } 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 fetchCardDataFromScryfall( + 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..13f02a8 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,50 @@ 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 ( vault: Vault, settings: ObsidianPluginMtgSettings ): Promise => { - // Sync collection - const collectionContents: string[] = await processCollectionFiles( - vault, - settings - ); + const collections = await processCollectionFiles(vault, settings); + return collections.reduce((acc, collection) => { + Object.entries(collection.counts).forEach(([name, count]) => { + acc[name] = (acc[name] ?? 0) + count; + }); + return acc; + }, {} as CardCounts); +}; - // Create consolidated collection dictionary - return createCardCountsMapping(collectionContents, settings); +export const syncCollections = async ( + vault: Vault, + settings: ObsidianPluginMtgSettings +): Promise => { + return processCollectionFiles(vault, settings); }; \ No newline at end of file diff --git a/src/scryfall.ts b/src/scryfall.ts index d3f07b0..99627b6 100644 --- a/src/scryfall.ts +++ b/src/scryfall.ts @@ -1,6 +1,8 @@ import { nameToId } from "./collection"; import { promiseWrappedRequest } from "./http"; +const cardDataCache: Record = {}; + export type CardIdentifier = | { name: string; @@ -181,6 +183,52 @@ export const getMultipleCardData = async ( return request(params); }; +export const fetchCardDataFromScryfall = 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 !cardDataCache[key]; + }); + + if (uncachedCards.length > 0) { + const batches: CardIdentifier[][] = []; + let currentBatch: CardIdentifier[] = []; + batches.push(currentBatch); + + uncachedCards.forEach(identifier => { + if (currentBatch.length === MAX_SCRYFALL_BATCH_SIZE) { + batches.push(currentBatch); + currentBatch = []; + } + currentBatch.push(identifier); + }); + batches.push(currentBatch); + + let fetched = 0; + const total = distinctCards.length; + + for (const batch of batches) { + const result = await getMultipleCardData(batch); + result.data.forEach((card: CardData) => { + if (card.name) { + cardDataCache[nameToId(card.name)] = card; + } + if (card.printed_name) { + cardDataCache[nameToId(card.printed_name)] = card; + } + }); + fetched += batch.length; + onProgress?.(fetched, total); + } + } + + return cardDataCache; +}; + /** * Fetches card data from the Scryfall API for a list of distinct cards. * Cards are fetched in batches of {@link MAX_SCRYFALL_BATCH_SIZE} to respect @@ -193,7 +241,7 @@ export const getMultipleCardData = async ( * @param distinctCards - List of distinct card identifiers to fetch * @returns A record of card data indexed by normalized card name */ -export const fetchCardDataFromScryfall = async ( +export const fetchFromScryfall = async ( distinctCards: CardIdentifier[] ): Promise> => { const batches: CardIdentifier[][] = []; 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..6e7f97e 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.123": "1.5.7" } \ No newline at end of file From 7157869306eae5319b1980592ebeb8f39032b682 Mon Sep 17 00:00:00 2001 From: Samir Boulema Date: Fri, 10 Jul 2026 13:20:08 +0200 Subject: [PATCH 2/5] feat: Change from session cache, to data cache --- main.ts | 80 +++++++++++-------- src/cache.ts | 34 ++++++++ src/collection-renderer.ts | 4 +- src/collection.ts | 5 ++ src/scryfall.ts | 158 ++++++++++++++++--------------------- 5 files changed, 156 insertions(+), 125 deletions(-) create mode 100644 src/cache.ts diff --git a/main.ts b/main.ts index b2041fb..5188127 100644 --- a/main.ts +++ b/main.ts @@ -1,18 +1,21 @@ -import { App, FuzzySuggestModal, Plugin, PluginSettingTab, Setting, TFolder, Vault } from "obsidian"; +import { App, FuzzySuggestModal, Plugin, PluginSettingTab, Setting, TFolder } from "obsidian"; import { - CardCollection, - DEFAULT_COLLECTION_COUNT_COLUMN, - DEFAULT_COLLECTION_FOLDER_PATH, - DEFAULT_COLLECTION_NAME_COLUMN, - syncCollections, + CardCollection, + CardCounts, + DEFAULT_COLLECTION_COUNT_COLUMN, + DEFAULT_COLLECTION_FOLDER_PATH, + DEFAULT_COLLECTION_NAME_COLUMN, + hashCollectionContents, + processCollectionFiles, + syncCollections, syncCounts, } 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: { @@ -50,15 +53,24 @@ export default class ObsidianPluginMtg extends Plugin { if (file.name.endsWith(".csv")) { const collectionFolderPath = this.settings.collection?.folderPath || ""; if (file.parent?.path.startsWith(collectionFolderPath)) { - this.cardCounts = await syncCounts(vault, this.settings); this.collections = await syncCollections(vault, this.settings); + this.cardCounts = await syncCounts(vault, this.settings); + clearCache(); } } }); this.app.workspace.onLayoutReady(async () => { - this.cardCounts = await syncCounts(vault, this.settings); + const fileContents = await processCollectionFiles(vault, this.settings); + const hash = hashCollectionContents(fileContents); + + const savedData = await this.loadData(); + if (savedData?.collectionHash === hash && savedData?.cardDataCache) { + loadCache(savedData.cardDataCache); + } + this.collections = await syncCollections(vault, this.settings); + this.cardCounts = await syncCounts(vault, this.settings); }); this.addCommand({ @@ -101,41 +113,39 @@ export default class ObsidianPluginMtg extends Plugin { ); } - onunload() {} + onunload() { + void (async () => { + const fileContents = await processCollectionFiles( + this.app.vault, + this.settings + ); + const hash = hashCollectionContents(fileContents); + 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, + }); } } 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-renderer.ts b/src/collection-renderer.ts index 9b43b64..c729722 100644 --- a/src/collection-renderer.ts +++ b/src/collection-renderer.ts @@ -1,5 +1,5 @@ import { CardCollection, nameToId } from "./collection"; -import { CardData, fetchCardDataFromScryfall } from "./scryfall"; +import { CardData, fetchCardDataFromScryfallCached } from "./scryfall"; const renderRows = ( tbody: HTMLElement, @@ -107,7 +107,7 @@ export const renderCollection = async ( // Fetch card data and re-render const identifiers = allCardNames.map(name => ({ name })); - const cardDataByCardId = await fetchCardDataFromScryfall( + const cardDataByCardId = await fetchCardDataFromScryfallCached( identifiers, (fetched, total) => { progressEl.textContent = `${fetched} / ${total}`; diff --git a/src/collection.ts b/src/collection.ts index 13f02a8..99eef79 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -108,4 +108,9 @@ export const syncCollections = async ( settings: ObsidianPluginMtgSettings ): Promise => { return processCollectionFiles(vault, settings); +}; + +export const hashCollectionContents = (contents: CardCollection[]): string => { + const combined = contents.map(c => Object.entries(c.counts).join("")).join(""); + return `${contents.length}-${combined.length}`; }; \ No newline at end of file diff --git a/src/scryfall.ts b/src/scryfall.ts index 99627b6..b14c536 100644 --- a/src/scryfall.ts +++ b/src/scryfall.ts @@ -1,16 +1,15 @@ import { nameToId } from "./collection"; import { promiseWrappedRequest } from "./http"; - -const cardDataCache: Record = {}; +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; @@ -23,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; @@ -52,7 +50,7 @@ export interface CardFace { } export interface CardData { - object?: string; // card + object?: string; id?: string; oracle_id?: string; multiverse_ids?: number[]; @@ -183,52 +181,6 @@ export const getMultipleCardData = async ( return request(params); }; -export const fetchCardDataFromScryfall = 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 !cardDataCache[key]; - }); - - if (uncachedCards.length > 0) { - const batches: CardIdentifier[][] = []; - let currentBatch: CardIdentifier[] = []; - batches.push(currentBatch); - - uncachedCards.forEach(identifier => { - if (currentBatch.length === MAX_SCRYFALL_BATCH_SIZE) { - batches.push(currentBatch); - currentBatch = []; - } - currentBatch.push(identifier); - }); - batches.push(currentBatch); - - let fetched = 0; - const total = distinctCards.length; - - for (const batch of batches) { - const result = await getMultipleCardData(batch); - result.data.forEach((card: CardData) => { - if (card.name) { - cardDataCache[nameToId(card.name)] = card; - } - if (card.printed_name) { - cardDataCache[nameToId(card.printed_name)] = card; - } - }); - fetched += batch.length; - onProgress?.(fetched, total); - } - } - - return cardDataCache; -}; - /** * Fetches card data from the Scryfall API for a list of distinct cards. * Cards are fetched in batches of {@link MAX_SCRYFALL_BATCH_SIZE} to respect @@ -241,42 +193,72 @@ export const fetchCardDataFromScryfall = async ( * @param distinctCards - List of distinct card identifiers to fetch * @returns A record of card data indexed by normalized card name */ -export const fetchFromScryfall = async ( - distinctCards: CardIdentifier[] +export const fetchCardDataFromScryfall = async ( + distinctCards: CardIdentifier[] ): 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 cardDataInBatches: ScryfallResponse[] = await Promise.all( + batches.map((batch) => getMultipleCardData(batch)) + ); - const cardDataByCardId: Record = {}; + const cardDataByCardId: Record = {}; - 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; + cardDataInBatches.forEach((batch) => { + batch.data.forEach((card: CardData) => { + if (card.name) { + cardDataByCardId[nameToId(card.name)] = card; } - }); - }); + if (card.printed_name) { + cardDataByCardId[nameToId(card.printed_name)] = card; + } + }); + }); - return cardDataByCardId; + 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); + Object.entries(freshData).forEach(([key, data]) => { + setCachedCardData(key, data); + }); + } + + onProgress?.(distinctCards.length, distinctCards.length); + + return getCachedCardData(); }; \ No newline at end of file From 8664b8eb3c3fc19da75e1e2f370897578aab0a86 Mon Sep 17 00:00:00 2001 From: "Samir L. Boulema" Date: Sun, 12 Jul 2026 21:57:35 +0200 Subject: [PATCH 3/5] feat: Select parent folders of collection folders --- main.ts | 24 +++++++++++------------- src/collection.ts | 22 +++++++++------------- src/scryfall.ts | 26 +++++++++++++++----------- 3 files changed, 35 insertions(+), 37 deletions(-) diff --git a/main.ts b/main.ts index 5188127..3a0d521 100644 --- a/main.ts +++ b/main.ts @@ -6,9 +6,8 @@ import { DEFAULT_COLLECTION_FOLDER_PATH, DEFAULT_COLLECTION_NAME_COLUMN, hashCollectionContents, - processCollectionFiles, + mergeCollections, syncCollections, - syncCounts, } from "src/collection"; import { renderDecklist } from "src/renderer"; import { ObsidianPluginMtgSettings } from "src/settings"; @@ -54,23 +53,22 @@ export default class ObsidianPluginMtg extends Plugin { const collectionFolderPath = this.settings.collection?.folderPath || ""; if (file.parent?.path.startsWith(collectionFolderPath)) { this.collections = await syncCollections(vault, this.settings); - this.cardCounts = await syncCounts(vault, this.settings); + this.cardCounts = mergeCollections(this.collections); clearCache(); } } }); this.app.workspace.onLayoutReady(async () => { - const fileContents = await processCollectionFiles(vault, this.settings); - const hash = hashCollectionContents(fileContents); + 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.collections = await syncCollections(vault, this.settings); - this.cardCounts = await syncCounts(vault, this.settings); }); this.addCommand({ @@ -115,11 +113,7 @@ export default class ObsidianPluginMtg extends Plugin { onunload() { void (async () => { - const fileContents = await processCollectionFiles( - this.app.vault, - this.settings - ); - const hash = hashCollectionContents(fileContents); + const hash = hashCollectionContents(this.collections); const data = await this.loadData() ?? {}; await this.saveData({ @@ -346,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/src/collection.ts b/src/collection.ts index 99eef79..7dc4c1f 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -90,19 +90,6 @@ export const processCollectionFiles = async ( )).filter((c): c is CardCollection => c !== null); }; -export const syncCounts = async ( - vault: Vault, - settings: ObsidianPluginMtgSettings -): Promise => { - const collections = await processCollectionFiles(vault, settings); - return collections.reduce((acc, collection) => { - Object.entries(collection.counts).forEach(([name, count]) => { - acc[name] = (acc[name] ?? 0) + count; - }); - return acc; - }, {} as CardCounts); -}; - export const syncCollections = async ( vault: Vault, settings: ObsidianPluginMtgSettings @@ -113,4 +100,13 @@ export const syncCollections = async ( 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 b14c536..5f50b78 100644 --- a/src/scryfall.ts +++ b/src/scryfall.ts @@ -194,7 +194,8 @@ 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[] = []; @@ -210,14 +211,14 @@ export const fetchCardDataFromScryfall = async ( batches.push(currentBatch); - const cardDataInBatches: ScryfallResponse[] = await Promise.all( - batches.map((batch) => getMultipleCardData(batch)) - ); - const cardDataByCardId: Record = {}; + let fetched = 0; + const total = distinctCards.length; - cardDataInBatches.forEach((batch) => { - batch.data.forEach((card: CardData) => { + for (const batch of batches) { + const result: ScryfallResponse = await getMultipleCardData(batch); + + result.data.forEach((card: CardData) => { if (card.name) { cardDataByCardId[nameToId(card.name)] = card; } @@ -225,7 +226,10 @@ export const fetchCardDataFromScryfall = async ( cardDataByCardId[nameToId(card.printed_name)] = card; } }); - }); + + fetched += batch.length; + onProgress?.(fetched, total); + } return cardDataByCardId; }; @@ -252,13 +256,13 @@ export const fetchCardDataFromScryfallCached = async ( }); if (uncachedCards.length > 0) { - const freshData = await fetchCardDataFromScryfall(uncachedCards); + const freshData = await fetchCardDataFromScryfall(uncachedCards, onProgress); Object.entries(freshData).forEach(([key, data]) => { setCachedCardData(key, data); }); + } else { + onProgress?.(distinctCards.length, distinctCards.length); } - onProgress?.(distinctCards.length, distinctCards.length); - return getCachedCardData(); }; \ No newline at end of file From 1f47703a9bfa5d35c32896f67ad50bea24291ae9 Mon Sep 17 00:00:00 2001 From: "Samir L. Boulema" Date: Mon, 13 Jul 2026 09:39:03 +0200 Subject: [PATCH 4/5] chore: Update dependencies --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 4609846..913a3ca 100644 --- a/package.json +++ b/package.json @@ -20,21 +20,21 @@ "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": "6.0.3", From cfa11ece3b39322ace60a40f3ad4ce69938dbb2e Mon Sep 17 00:00:00 2001 From: "Samir L. Boulema" Date: Mon, 13 Jul 2026 09:42:13 +0200 Subject: [PATCH 5/5] chore: Bump version --- manifest.json | 2 +- package.json | 2 +- versions.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/manifest.json b/manifest.json index 32bed05..c1d8394 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "mtg-deck", "name": "MTG Deck", - "version": "1.14.123", + "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.json b/package.json index 913a3ca..c0a9473 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mtg-deck", - "version": "1.14.123", + "version": "1.14.128", "description": "Display your MTG decks and card lists in your notes.", "main": "main.js", "scripts": { diff --git a/versions.json b/versions.json index 6e7f97e..e10ce11 100644 --- a/versions.json +++ b/versions.json @@ -28,5 +28,5 @@ "1.12.88": "1.1.0", "1.13.105": "1.5.7", "1.13.106": "1.5.7", - "1.14.123": "1.5.7" + "1.14.128": "1.5.7" } \ No newline at end of file