From 3ea2b36990025429eb3447825d4e1ab134c4edc8 Mon Sep 17 00:00:00 2001 From: "Samir L. Boulema" Date: Thu, 9 Jul 2026 22:01:07 +0200 Subject: [PATCH] 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