diff --git a/.vite/lib/manifest.js b/.vite/lib/manifest.js index 54aa712..3a3181a 100644 --- a/.vite/lib/manifest.js +++ b/.vite/lib/manifest.js @@ -96,21 +96,16 @@ class ManifestProcessor { singleOrMultipleHostnames = [singleOrMultipleHostnames]; } + const matchPatterReplacer = ManifestProcessor.#createHostnameReplacementReduce(singleOrMultipleHostnames); + this.#manifestObject.host_permissions = singleOrMultipleHostnames.map(hostname => `*://*.${hostname}/`); this.#manifestObject.content_scripts?.forEach(entry => { - entry.matches = entry.matches.reduce((resultMatches, originalMatchPattern) => { - for (const updatedHostname of singleOrMultipleHostnames) { - resultMatches.push( - originalMatchPattern.replace( - /\*:\/\/\*\.[a-z]+\.[a-z]+\//, - `*://*.${updatedHostname}/` - ), - ); - } + entry.matches = entry.matches.reduce(matchPatterReplacer, []); - return resultMatches; - }, []); + if (entry.exclude_matches) { + entry.exclude_matches = entry.exclude_matches.reduce(matchPatterReplacer, []); + } }) } @@ -148,6 +143,32 @@ class ManifestProcessor { } ); } + + /** + * @param {string|(string[])} singleOrMultipleHostnames + * @return {function(string[], string): string[]} + */ + static #createHostnameReplacementReduce(singleOrMultipleHostnames) { + return ( + /** + * @param {string[]} resultMatches + * @param {string} originalMatchPattern + * @return {string[]} + */ + (resultMatches, originalMatchPattern) => { + for (const updatedHostname of singleOrMultipleHostnames) { + resultMatches.push( + originalMatchPattern.replace( + /\*:\/\/\*\.[a-z]+\.[a-z]+\//, + `*://*.${updatedHostname}/` + ), + ); + } + + return resultMatches; + } + ); + } } /** @@ -186,6 +207,7 @@ export function loadManifest(filePath) { /** * @typedef {Object} ContentScriptsEntry * @property {string[]} matches + * @property {string[]} exclude_matches * @property {string[]|undefined} js * @property {string[]|undefined} css */ diff --git a/.vite/pack-extension.js b/.vite/pack-extension.js index a5cf79d..903602c 100644 --- a/.vite/pack-extension.js +++ b/.vite/pack-extension.js @@ -3,6 +3,7 @@ import path from "path"; import { buildScriptsAndStyles } from "./lib/content-scripts.js"; import { extractInlineScriptsFromIndex } from "./lib/index-file.js"; import { normalizePath } from "vite"; +import fs from "fs"; /** * Build addition assets required for the extension and pack it into the directory. @@ -84,12 +85,55 @@ export async function packExtension(settings) { break; default: - console.warn('No replacement set up for site: ' + process.env.SITE); + if (process.env.SITE) { + console.warn('No replacement set up for site: ' + process.env.SITE); + } } manifest.passVersionFromPackage(path.resolve(settings.rootDir, 'package.json')); manifest.saveTo(path.resolve(settings.exportDir, 'manifest.json')); + const iconsDirectory = path.resolve(settings.exportDir, 'icons'); + + switch (process.env.SITE) { + case "derpibooru": + case "tantabus": + const siteIconsDirectory = path.resolve(iconsDirectory, process.env.SITE); + + if (!fs.existsSync(siteIconsDirectory)) { + console.warn(`Can't find replacement icons for site ${process.env.SITE}`); + break; + } + + console.log(`Found replacement icons for ${process.env.SITE}, swapping them...`); + + fs.readdirSync(siteIconsDirectory).forEach(fileName => { + const originalIconPath = path.resolve(settings.exportDir, fileName); + const replacementIconPath = path.resolve(siteIconsDirectory, fileName); + + if (!fs.existsSync(originalIconPath)) { + console.warn(`Original icon not found: ${originalIconPath}`) + return; + } + + fs.rmSync(originalIconPath); + fs.cpSync(replacementIconPath, originalIconPath); + + console.log(`Replaced: ${path.relative(settings.rootDir, replacementIconPath)} → ${path.relative(settings.rootDir, originalIconPath)}`); + }); + + break; + } + + if (fs.existsSync(iconsDirectory)) { + console.log('Cleaning up icon replacements directory'); + + fs.rmSync(iconsDirectory, { + recursive: true, + force: true, + }); + } + extractInlineScriptsFromIndex(path.resolve(settings.exportDir, 'index.html')); } diff --git a/manifest.json b/manifest.json index 492076b..e8a851e 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name": "Furbooru Tagging Assistant", "description": "Small experimental extension for slightly quicker tagging experience. Furbooru Edition.", - "version": "0.6.1", + "version": "0.7.0", "browser_specific_settings": { "gecko": { "id": "furbooru-tagging-assistant@thecore.city", @@ -50,11 +50,16 @@ "matches": [ "*://*.furbooru.org/images/*" ], + "exclude_matches": [ + "*://*.furbooru.org/images/new", + "*://*.furbooru.org/images/new?*" + ], "js": [ "src/content/tags-editor.ts" ], "css": [ - "src/styles/content/tags-editor.scss" + "src/styles/content/tags-editor.scss", + "src/styles/content/tag-presets.scss" ] }, { @@ -87,6 +92,17 @@ "css": [ "src/styles/content/posts.scss" ] + }, + { + "matches": [ + "*://*.furbooru.org/images/new" + ], + "js": [ + "src/content/upload.ts" + ], + "css": [ + "src/styles/content/tag-presets.scss" + ] } ], "action": { diff --git a/package-lock.json b/package-lock.json index 49fa2ce..8f4a5ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,33 +1,33 @@ { "name": "furbooru-tagging-assistant", - "version": "0.6.1", + "version": "0.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "furbooru-tagging-assistant", - "version": "0.6.1", + "version": "0.7.0", "dependencies": { "@fortawesome/fontawesome-free": "^7.2.0", "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.53.0", + "@sveltejs/kit": "^2.55.0", "amd-lite": "^1.0.1", "lz-string": "^1.5.0", - "sass": "^1.97.3", - "svelte": "^5.53.3" + "sass": "^1.98.0", + "svelte": "^5.53.12" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^6.2.4", "@types/chrome": "^0.1.37", - "@types/node": "^25.3.0", - "@vitest/coverage-v8": "^4.0.18", + "@types/node": "^25.5.0", + "@vitest/coverage-v8": "^4.1.0", "cheerio": "^1.2.0", "cross-env": "^10.1.0", "jsdom": "^28.1.0", - "svelte-check": "^4.4.3", + "svelte-check": "^4.4.5", "typescript": "^5.9.3", "vite": "^7.3.1", - "vitest": "^4.0.18" + "vitest": "^4.1.0" } }, "node_modules/@acemir/cssom": { @@ -96,13 +96,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -112,9 +112,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { @@ -860,9 +860,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", - "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -873,9 +873,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", - "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -886,9 +886,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", - "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -899,9 +899,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", - "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -912,9 +912,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", - "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -925,9 +925,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", - "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -938,9 +938,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", - "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -951,9 +951,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", - "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -964,9 +964,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", - "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -977,9 +977,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", - "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -989,10 +989,23 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", - "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], @@ -1003,9 +1016,22 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", - "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -1016,9 +1042,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", - "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -1029,9 +1055,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", - "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -1042,9 +1068,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", - "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -1055,9 +1081,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", - "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -1068,9 +1094,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", - "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -1080,10 +1106,36 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", - "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -1094,9 +1146,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", - "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -1106,10 +1158,23 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", - "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -1120,9 +1185,9 @@ ] }, "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, "node_modules/@sveltejs/acorn-typescript": { @@ -1143,9 +1208,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.53.0", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.53.0.tgz", - "integrity": "sha512-Brh/9h8QEg7rWIj+Nnz/2sC49NUeS8g3Qd9H5dTO3EbWG8vCEUl06jE+r5jQVDMHdr1swmCkwZkONFsWelGTpQ==", + "version": "2.55.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.55.0.tgz", + "integrity": "sha512-MdFRjevVxmAknf2NbaUkDF16jSIzXMWd4Nfah0Qp8TtQVoSp3bV4jKt8mX7z7qTUTWvgSaxtR0EG5WJf53gcuA==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -1153,7 +1218,7 @@ "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", - "devalue": "^5.6.3", + "devalue": "^5.6.4", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", @@ -1204,12 +1269,12 @@ } }, "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.0.tgz", - "integrity": "sha512-iwQ8Z4ET6ZFSt/gC+tVfcsSBHwsqc6RumSaiLUkAurW3BCpJam65cmHw0oOlDMTO0u+PZi9hilBRYN+LZNHTUQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz", + "integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==", "license": "MIT", "dependencies": { - "debug": "^4.4.1" + "obug": "^2.1.0" }, "engines": { "node": "^20.19 || ^22.12 || >=24" @@ -1278,9 +1343,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", - "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -1294,29 +1359,29 @@ "license": "MIT" }, "node_modules/@vitest/coverage-v8": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", - "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.0.tgz", + "integrity": "sha512-nDWulKeik2bL2Va/Wl4x7DLuTKAXa906iRFooIRPR+huHkcvp9QDkPQ2RJdmjOFrqOqvNfoSQLF68deE3xC3CQ==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.0.18", - "ast-v8-to-istanbul": "^0.3.10", + "@vitest/utils": "4.1.0", + "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", - "magicast": "^0.5.1", + "magicast": "^0.5.2", "obug": "^2.1.1", - "std-env": "^3.10.0", + "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.0.18", - "vitest": "4.0.18" + "@vitest/browser": "4.1.0", + "vitest": "4.1.0" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1325,17 +1390,17 @@ } }, "node_modules/@vitest/expect": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", - "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", + "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", "dev": true, "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.0.0", + "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "chai": "^6.2.1", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "chai": "^6.2.2", "tinyrainbow": "^3.0.3" }, "funding": { @@ -1343,13 +1408,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", - "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.18", + "@vitest/spy": "4.1.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -1358,7 +1423,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -1370,9 +1435,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", - "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", "dev": true, "license": "MIT", "dependencies": { @@ -1383,13 +1448,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", - "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", + "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.18", + "@vitest/utils": "4.1.0", "pathe": "^2.0.3" }, "funding": { @@ -1397,13 +1462,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", - "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", + "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.18", + "@vitest/pretty-format": "4.1.0", + "@vitest/utils": "4.1.0", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1412,9 +1478,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", - "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", "dev": true, "license": "MIT", "funding": { @@ -1422,13 +1488,14 @@ } }, "node_modules/@vitest/utils": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", - "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.18", + "@vitest/pretty-format": "4.1.0", + "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" }, "funding": { @@ -1482,15 +1549,15 @@ } }, "node_modules/ast-v8-to-istanbul": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.10.tgz", - "integrity": "sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", + "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", - "js-tokens": "^9.0.1" + "js-tokens": "^10.0.0" } }, "node_modules/axobject-query": { @@ -1601,6 +1668,13 @@ "node": ">=6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.6.0", "license": "MIT", @@ -1725,6 +1799,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1764,9 +1839,9 @@ } }, "node_modules/devalue": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz", - "integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==", + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", + "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", "license": "MIT" }, "node_modules/dom-serializer": { @@ -1848,9 +1923,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true, "license": "MIT" }, @@ -2057,7 +2132,9 @@ } }, "node_modules/immutable": { - "version": "5.0.3", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", "license": "MIT" }, "node_modules/is-extglob": { @@ -2149,9 +2226,9 @@ } }, "node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", "dev": true, "license": "MIT" }, @@ -2239,6 +2316,279 @@ "node": ">=6" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss/node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/locate-character": { "version": "3.0.0", "license": "MIT" @@ -2270,14 +2620,14 @@ } }, "node_modules/magicast": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", - "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, @@ -2334,7 +2684,8 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true }, "node_modules/nanoid": { "version": "3.3.11", @@ -2464,9 +2815,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "funding": [ { "type": "opencollective", @@ -2524,9 +2875,9 @@ } }, "node_modules/rollup": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", - "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -2539,26 +2890,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.46.2", - "@rollup/rollup-android-arm64": "4.46.2", - "@rollup/rollup-darwin-arm64": "4.46.2", - "@rollup/rollup-darwin-x64": "4.46.2", - "@rollup/rollup-freebsd-arm64": "4.46.2", - "@rollup/rollup-freebsd-x64": "4.46.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", - "@rollup/rollup-linux-arm-musleabihf": "4.46.2", - "@rollup/rollup-linux-arm64-gnu": "4.46.2", - "@rollup/rollup-linux-arm64-musl": "4.46.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", - "@rollup/rollup-linux-ppc64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-musl": "4.46.2", - "@rollup/rollup-linux-s390x-gnu": "4.46.2", - "@rollup/rollup-linux-x64-gnu": "4.46.2", - "@rollup/rollup-linux-x64-musl": "4.46.2", - "@rollup/rollup-win32-arm64-msvc": "4.46.2", - "@rollup/rollup-win32-ia32-msvc": "4.46.2", - "@rollup/rollup-win32-x64-msvc": "4.46.2", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, @@ -2579,13 +2935,13 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.97.3", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", - "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz", + "integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==", "license": "MIT", "dependencies": { "chokidar": "^4.0.0", - "immutable": "^5.0.2", + "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { @@ -2687,9 +3043,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", "dev": true, "license": "MIT" }, @@ -2707,9 +3063,9 @@ } }, "node_modules/svelte": { - "version": "5.53.3", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.3.tgz", - "integrity": "sha512-pRUBr6j6uQDgBi208gHnGRMykw0Rf2Yr1HmLyRucsvcaYgIUxswJkT93WZJflsmezu5s8Lq+q78EoyLv2yaFCg==", + "version": "5.53.12", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.12.tgz", + "integrity": "sha512-4x/uk4rQe/d7RhfvS8wemTfNjQ0bJbKvamIzRBfTe2eHHjzBZ7PZicUQrC2ryj83xxEacfA1zHKd1ephD1tAxA==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", @@ -2721,7 +3077,7 @@ "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", - "devalue": "^5.6.3", + "devalue": "^5.6.4", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", @@ -2734,9 +3090,9 @@ } }, "node_modules/svelte-check": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.3.tgz", - "integrity": "sha512-4HtdEv2hOoLCEsSXI+RDELk9okP/4sImWa7X02OjMFFOWeSdFF3NFy3vqpw0z+eH9C88J9vxZfUXz/Uv2A1ANw==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.5.tgz", + "integrity": "sha512-1bSwIRCvvmSHrlK52fOlZmVtUZgil43jNL/2H18pRpa+eQjzGt6e3zayxhp1S7GajPFKNM/2PMCG+DZFHlG9fw==", "dev": true, "license": "MIT", "dependencies": { @@ -3063,9 +3419,9 @@ } }, "node_modules/vitefu": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", - "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz", + "integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==", "license": "MIT", "workspaces": [ "tests/deps/*", @@ -3073,7 +3429,7 @@ "tests/projects/workspace/packages/*" ], "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" }, "peerDependenciesMeta": { "vite": { @@ -3082,31 +3438,31 @@ } }, "node_modules/vitest": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", - "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", + "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.0.18", - "@vitest/mocker": "4.0.18", - "@vitest/pretty-format": "4.0.18", - "@vitest/runner": "4.0.18", - "@vitest/snapshot": "4.0.18", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "es-module-lexer": "^1.7.0", - "expect-type": "^1.2.2", + "@vitest/expect": "4.1.0", + "@vitest/mocker": "4.1.0", + "@vitest/pretty-format": "4.1.0", + "@vitest/runner": "4.1.0", + "@vitest/snapshot": "4.1.0", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", - "std-env": "^3.10.0", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -3122,12 +3478,13 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.18", - "@vitest/browser-preview": "4.0.18", - "@vitest/browser-webdriverio": "4.0.18", - "@vitest/ui": "4.0.18", + "@vitest/browser-playwright": "4.1.0", + "@vitest/browser-preview": "4.1.0", + "@vitest/browser-webdriverio": "4.1.0", + "@vitest/ui": "4.1.0", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -3156,6 +3513,9 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, diff --git a/package.json b/package.json index e2c0439..3e0b22a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "furbooru-tagging-assistant", - "version": "0.6.1", + "version": "0.7.0", "private": true, "type": "module", "scripts": { @@ -16,24 +16,24 @@ }, "dependencies": { "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.53.0", + "@sveltejs/kit": "^2.55.0", "@fortawesome/fontawesome-free": "^7.2.0", "amd-lite": "^1.0.1", "lz-string": "^1.5.0", - "sass": "^1.97.3", - "svelte": "^5.53.3" + "sass": "^1.98.0", + "svelte": "^5.53.12" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^6.2.4", "@types/chrome": "^0.1.37", - "@types/node": "^25.3.0", - "@vitest/coverage-v8": "^4.0.18", + "@types/node": "^25.5.0", + "@vitest/coverage-v8": "^4.1.0", "cheerio": "^1.2.0", "cross-env": "^10.1.0", "jsdom": "^28.1.0", - "svelte-check": "^4.4.3", + "svelte-check": "^4.4.5", "typescript": "^5.9.3", "vite": "^7.3.1", - "vitest": "^4.0.18" + "vitest": "^4.1.0" } } diff --git a/src/app.d.ts b/src/app.d.ts index ffec69b..8281fc6 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,7 +1,8 @@ // See https://kit.svelte.dev/docs/types#app // for information about these interfaces -import MaintenanceProfile from "$entities/MaintenanceProfile"; +import type TaggingProfile from "$entities/TaggingProfile"; import type TagGroup from "$entities/TagGroup"; +import type TagEditorPreset from "$entities/TagEditorPreset"; declare global { /** @@ -37,8 +38,9 @@ declare global { ); interface EntityNamesMap { - profiles: MaintenanceProfile; + profiles: TaggingProfile; groups: TagGroup; + presets: TagEditorPreset; } interface ImageURIs { diff --git a/src/assets/icon/README.md b/src/assets/icon/README.md new file mode 100644 index 0000000..3e715cb --- /dev/null +++ b/src/assets/icon/README.md @@ -0,0 +1,57 @@ +# Extension Icon + +This folder contains original resources used to make an icon for the extension. Since I'm not really an icon designer, I +ended up just composing the icon from sites logos + the shorthand name of the extension with fancy font. Nothing +special. + +## Sources + +All resources used for composing an icon are stored here as copies just to not lose anything. Original assets are +sourced from the following places: + +- [Derpibooru Logo](https://github.com/derpibooru/philomena/blob/40ffb1b75bd0d96db24fa7c84bce36fcb7f2935f/assets/static/favicon.svg) +- [Furbooru Logo](https://github.com/furbooru/philomena/blob/cbfde406de34734403c06952bcaca51db6df1390/assets/static/favicon.svg) +- [Tantabus Logo](https://github.com/tantabus-ai/philomena/blob/285a7666ae4be46ac4da36bbc9ac8fda9e5c0fc3/assets/static/favicon.svg) +- [RoundFeather Font](https://drive.google.com/file/d/18ggNplAZNYtO4eNtMUpv3XpkeOAxSkxm/view?usp=sharing) + - Made by [allorus162](https://bsky.app/profile/allorus162.bsky.social) + - [Original Bluesky post](https://bsky.app/profile/allorus162.bsky.social/post/3mfqntff4j22i) + +## Rendering + +**Note:** You don't need to do anything to pack current version of icon to the extension. All icons are already pre-rendered and +placed into the `static` directory. + +For now, any change to the icons will require manual re-rendering of PNG versions of the logos used when packing +extension for the release. All you need is to open `/src/assets/icon/icon.svg` in software like Inskape, hide the +currently opened logo and toggle the required one and save it into `icon256.png`, `icon128.png`, `icon48.png` and +`icon16.png`. + +For the font on the bottom-right to work, you will need to install it from the file +`src/assets/icon/fonts/roundfeather-regular-1.001.ttf` (or you can download and install it from the source link). + +You should render them into `/static` directory in the following structure: + +- Place Furbooru icons into `/static` directory +- Then add same icons for Derpibooru and Tantabus into `/static/icons/depribooru` and `/static/icons/tantabus` + respectively. + +Resulting structure will look like this: + +``` +static/ + icons/ + derpibooru/ + icon16.png + icon48.png + icon128.png + icon256.png + tantabus/ + icon16.png + icon48.png + icon128.png + icon256.png + icon16.png + icon48.png + icon128.png + icon256.png +``` diff --git a/src/assets/icon/favicons/derpibooru.svg b/src/assets/icon/favicons/derpibooru.svg new file mode 100644 index 0000000..2dec1d7 --- /dev/null +++ b/src/assets/icon/favicons/derpibooru.svg @@ -0,0 +1,2 @@ + + diff --git a/src/assets/icon/favicons/furbooru.svg b/src/assets/icon/favicons/furbooru.svg new file mode 100644 index 0000000..0d5f3a7 --- /dev/null +++ b/src/assets/icon/favicons/furbooru.svg @@ -0,0 +1,2 @@ + + diff --git a/src/assets/icon/favicons/tantabus.svg b/src/assets/icon/favicons/tantabus.svg new file mode 100644 index 0000000..6c07350 --- /dev/null +++ b/src/assets/icon/favicons/tantabus.svg @@ -0,0 +1,41 @@ + + diff --git a/src/assets/icon/fonts/roundfeather-regular-1.001.ttf b/src/assets/icon/fonts/roundfeather-regular-1.001.ttf new file mode 100644 index 0000000..73030eb Binary files /dev/null and b/src/assets/icon/fonts/roundfeather-regular-1.001.ttf differ diff --git a/src/assets/icon/icon.svg b/src/assets/icon/icon.svg new file mode 100644 index 0000000..48e85b2 --- /dev/null +++ b/src/assets/icon/icon.svg @@ -0,0 +1,115 @@ + +PTA diff --git a/src/components/features/GroupView.svelte b/src/components/features/GroupView.svelte index 3b6c698..b2142dc 100644 --- a/src/components/features/GroupView.svelte +++ b/src/components/features/GroupView.svelte @@ -1,6 +1,8 @@ -
- Group Name: -
{group.settings.name}
-
+ + {group.settings.name} + {#if sortedTagsList.length} -
- Tags: + -
- {#each sortedTagsList as tagName} - {tagName} - {/each} -
+
-
+ {/if} {#if sortedPrefixes.length} -
- Prefixes: + -
- {#each sortedPrefixes as prefixName} - {prefixName}* - {/each} -
+
-
+ {/if} {#if sortedSuffixes.length} -
- Suffixes: + -
- {#each sortedSuffixes as suffixName} - *{suffixName} - {/each} -
+
-
+ {/if} - - diff --git a/src/components/features/PresetView.svelte b/src/components/features/PresetView.svelte new file mode 100644 index 0000000..18c24ca --- /dev/null +++ b/src/components/features/PresetView.svelte @@ -0,0 +1,20 @@ + + + + {preset.settings.name} + + + + diff --git a/src/components/features/ProfileView.svelte b/src/components/features/ProfileView.svelte index d39f965..715f0eb 100644 --- a/src/components/features/ProfileView.svelte +++ b/src/components/features/ProfileView.svelte @@ -1,8 +1,10 @@ -
- Profile: -
{profile.settings.name}
-
-
- Tags: -
- {#each sortedTagsList as tagName} - {tagName} - {/each} -
-
- - + + {profile.settings.name} + + + + diff --git a/src/components/tags/TagsList.svelte b/src/components/tags/TagsList.svelte new file mode 100644 index 0000000..5ee590b --- /dev/null +++ b/src/components/tags/TagsList.svelte @@ -0,0 +1,23 @@ + + +
+ {#each tags as tagName} +
{prepend || ''}{tagName}{append || ''}
+ {/each} +
+ + diff --git a/src/components/ui/DetailsBlock.svelte b/src/components/ui/DetailsBlock.svelte new file mode 100644 index 0000000..aec0d36 --- /dev/null +++ b/src/components/ui/DetailsBlock.svelte @@ -0,0 +1,30 @@ + + +
+ {#if title?.length} + {title}: + {/if} +
+ {@render children?.()} +
+
+ + diff --git a/src/content/components/TagsForm.ts b/src/content/components/TagsForm.ts deleted file mode 100644 index ab79c8c..0000000 --- a/src/content/components/TagsForm.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { BaseComponent } from "$content/components/base/BaseComponent"; -import { getComponent } from "$content/components/base/component-utils"; -import { emit, on, type UnsubscribeFunction } from "$content/components/events/comms"; -import { EVENT_FETCH_COMPLETE } from "$content/components/events/booru-events"; -import { EVENT_FORM_EDITOR_UPDATED } from "$content/components/events/tags-form-events"; - -export class TagsForm extends BaseComponent { - protected init() { - // Site sending the event when form is submitted vie Fetch API. We use this event to reload our logic here. - const unsubscribe = on( - this.container, - EVENT_FETCH_COMPLETE, - () => this.#waitAndDetectUpdatedForm(unsubscribe), - ); - } - - #waitAndDetectUpdatedForm(unsubscribe: UnsubscribeFunction): void { - const elementContainingTagEditor = this.container - .closest('#image_tags_and_source') - ?.parentElement; - - if (!elementContainingTagEditor) { - return; - } - - const observer = new MutationObserver(() => { - const tagsFormElement = elementContainingTagEditor.querySelector('#tags-form'); - - if (!tagsFormElement || getComponent(tagsFormElement)) { - return; - } - - const tagFormComponent = new TagsForm(tagsFormElement); - tagFormComponent.initialize(); - - const fullTagEditor = tagFormComponent.parentTagEditorElement; - - if (fullTagEditor) { - emit(document.body, EVENT_FORM_EDITOR_UPDATED, fullTagEditor); - } else { - console.info('Tag form is not in the tag editor. Event is not sent.'); - } - - observer.disconnect(); - unsubscribe(); - }); - - observer.observe(elementContainingTagEditor, { - subtree: true, - childList: true, - }); - - // Make sure to forcibly disconnect everything after a while. - setTimeout(() => { - observer.disconnect(); - unsubscribe(); - }, 5000); - } - - get parentTagEditorElement(): HTMLElement | null { - return this.container.closest('.js-tagsauce') - } - - /** - * Collect all the tag categories available on the page and color the tags in the editor according to them. - */ - refreshTagColors() { - const tagCategories = this.#gatherTagCategories(); - const editableTags = this.container.querySelectorAll('.tag'); - - for (const tagElement of editableTags) { - // Tag name is stored in the "remove" link and not in the tag itself. - const removeLink = tagElement.querySelector('a'); - - if (!removeLink) { - continue; - } - - const tagName = removeLink.dataset.tagName; - - if (!tagName || !tagCategories.has(tagName)) { - continue; - } - - const categoryName = tagCategories.get(tagName)!; - - tagElement.dataset.tagCategory = categoryName; - tagElement.setAttribute('data-tag-category', categoryName); - } - } - - /** - * Collect list of categories from the tags on the page. - * @return - */ - #gatherTagCategories(): Map { - const tagCategories: Map = new Map(); - - for (const tagElement of document.querySelectorAll('.tag[data-tag-name][data-tag-category]')) { - const tagName = tagElement.dataset.tagName; - const tagCategory = tagElement.dataset.tagCategory; - - if (!tagName || !tagCategory) { - console.warn('Missing tag name or category!'); - continue; - } - - tagCategories.set(tagName, tagCategory); - } - - return tagCategories; - } - - static watchForEditors() { - document.body.addEventListener('click', event => { - const targetElement = event.target; - - if (!(targetElement instanceof HTMLElement)) { - return; - } - - const tagEditorWrapper = targetElement.closest('#image_tags_and_source'); - - if (!tagEditorWrapper) { - return; - } - - const refreshTrigger = targetElement.closest('.js-taginput-show, #edit-tags') - - if (!refreshTrigger) { - return; - } - - const tagFormElement = tagEditorWrapper.querySelector('#tags-form'); - - if (!tagFormElement) { - return; - } - - let tagEditor = getComponent(tagFormElement); - - if (!tagEditor || !(tagEditor instanceof TagsForm)) { - tagEditor = new TagsForm(tagFormElement); - tagEditor.initialize(); - } - - (tagEditor as TagsForm).refreshTagColors(); - }); - } -} diff --git a/src/content/components/events/booru-events.ts b/src/content/components/events/booru-events.ts index 05d9e57..236cc66 100644 --- a/src/content/components/events/booru-events.ts +++ b/src/content/components/events/booru-events.ts @@ -1,5 +1,15 @@ export const EVENT_FETCH_COMPLETE = 'fetchcomplete'; +export const EVENT_RELOAD = 'reload'; + +/** + * Custom data for the reload event on plain editor textarea. Philomena doesn't send anything on this event. + */ +export interface ReloadCustomOptions { + skipTagColorRefresh?: boolean; + skipTagRefresh?: boolean; +} export interface BooruEventsMap { [EVENT_FETCH_COMPLETE]: null; // Site sends the response, but extension will not get it due to isolation. + [EVENT_RELOAD]: ReloadCustomOptions|null; } diff --git a/src/content/components/events/comms.ts b/src/content/components/events/comms.ts index c31bae0..7127a25 100644 --- a/src/content/components/events/comms.ts +++ b/src/content/components/events/comms.ts @@ -4,13 +4,15 @@ import type { FullscreenViewerEventsMap } from "$content/components/events/fulls import type { BooruEventsMap } from "$content/components/events/booru-events"; import type { TagsFormEventsMap } from "$content/components/events/tags-form-events"; import type { TagDropdownEvents } from "$content/components/events/tag-dropdown-events"; +import type { PresetBlockEventsMap } from "$content/components/events/preset-block-events"; type EventsMapping = MaintenancePopupEventsMap & FullscreenViewerEventsMap & BooruEventsMap & TagsFormEventsMap - & TagDropdownEvents; + & TagDropdownEvents + & PresetBlockEventsMap; type EventCallback = (event: CustomEvent) => void; export type UnsubscribeFunction = () => void; diff --git a/src/content/components/events/fullscreen-viewer-events.ts b/src/content/components/events/fullscreen-viewer-events.ts index 60b9c55..bc9a284 100644 --- a/src/content/components/events/fullscreen-viewer-events.ts +++ b/src/content/components/events/fullscreen-viewer-events.ts @@ -1,4 +1,4 @@ -import type { FullscreenViewerSize } from "$lib/extension/settings/MiscSettings"; +import type { FullscreenViewerSize } from "$lib/extension/preferences/MiscPreferences"; export const EVENT_SIZE_LOADED = 'size-loaded'; diff --git a/src/content/components/events/maintenance-popup-events.ts b/src/content/components/events/maintenance-popup-events.ts index fe6e61a..95c780f 100644 --- a/src/content/components/events/maintenance-popup-events.ts +++ b/src/content/components/events/maintenance-popup-events.ts @@ -1,4 +1,4 @@ -import type MaintenanceProfile from "$entities/MaintenanceProfile"; +import type TaggingProfile from "$entities/TaggingProfile"; export const EVENT_ACTIVE_PROFILE_CHANGED = 'active-profile-changed'; export const EVENT_MAINTENANCE_STATE_CHANGED = 'maintenance-state-change'; @@ -7,7 +7,7 @@ export const EVENT_TAGS_UPDATED = 'tags-updated'; type MaintenanceState = 'processing' | 'failed' | 'complete' | 'waiting'; export interface MaintenancePopupEventsMap { - [EVENT_ACTIVE_PROFILE_CHANGED]: MaintenanceProfile | null; + [EVENT_ACTIVE_PROFILE_CHANGED]: TaggingProfile | null; [EVENT_MAINTENANCE_STATE_CHANGED]: MaintenanceState; [EVENT_TAGS_UPDATED]: Map | null; } diff --git a/src/content/components/events/preset-block-events.ts b/src/content/components/events/preset-block-events.ts new file mode 100644 index 0000000..4723d61 --- /dev/null +++ b/src/content/components/events/preset-block-events.ts @@ -0,0 +1,10 @@ +export const EVENT_PRESET_TAG_CHANGE_APPLIED = 'preset-tag-changed'; + +export interface PresetTagChange { + addedTags?: Set; + removedTags?: Set; +} + +export interface PresetBlockEventsMap { + [EVENT_PRESET_TAG_CHANGE_APPLIED]: PresetTagChange; +} diff --git a/src/content/components/FullscreenViewer.ts b/src/content/components/extension/FullscreenViewer.ts similarity index 95% rename from src/content/components/FullscreenViewer.ts rename to src/content/components/extension/FullscreenViewer.ts index bf2ed23..616acdf 100644 --- a/src/content/components/FullscreenViewer.ts +++ b/src/content/components/extension/FullscreenViewer.ts @@ -1,5 +1,5 @@ import { BaseComponent } from "$content/components/base/BaseComponent"; -import MiscSettings, { type FullscreenViewerSize } from "$lib/extension/settings/MiscSettings"; +import MiscPreferences, { type FullscreenViewerSize } from "$lib/extension/preferences/MiscPreferences"; import { emit, on } from "$content/components/events/comms"; import { EVENT_SIZE_LOADED } from "$content/components/events/fullscreen-viewer-events"; @@ -53,8 +53,8 @@ export class FullscreenViewer extends BaseComponent { this.#imageElement.addEventListener('load', this.#onLoaded.bind(this)); this.#sizeSelectorElement.addEventListener('click', event => event.stopPropagation()); - FullscreenViewer.#miscSettings - .resolveFullscreenViewerPreviewSize() + FullscreenViewer.#preferences + .fullscreenViewerSize.get() .then(this.#onSizeResolved.bind(this)) .then(this.#watchForSizeSelectionChanges.bind(this)); } @@ -179,7 +179,7 @@ export class FullscreenViewer extends BaseComponent { #watchForSizeSelectionChanges() { let lastActiveSize = this.#sizeSelectorElement.value; - FullscreenViewer.#miscSettings.subscribe(settings => { + FullscreenViewer.#preferences.subscribe(settings => { const targetSize = settings.fullscreenViewerSize; if (!targetSize || lastActiveSize === targetSize) { @@ -202,7 +202,7 @@ export class FullscreenViewer extends BaseComponent { } lastActiveSize = targetSize; - void FullscreenViewer.#miscSettings.setFullscreenViewerPreviewSize(targetSize); + void FullscreenViewer.#preferences.fullscreenViewerSize.set(targetSize as FullscreenViewerSize); }); } @@ -289,7 +289,7 @@ export class FullscreenViewer extends BaseComponent { return url.endsWith('.mp4') || url.endsWith('.webm'); } - static #miscSettings = new MiscSettings(); + static #preferences = new MiscPreferences(); static #offsetProperty = '--offset'; static #opacityProperty = '--opacity'; diff --git a/src/content/components/ImageShowFullscreenButton.ts b/src/content/components/extension/ImageShowFullscreenButton.ts similarity index 71% rename from src/content/components/ImageShowFullscreenButton.ts rename to src/content/components/extension/ImageShowFullscreenButton.ts index b2dc612..25422a8 100644 --- a/src/content/components/ImageShowFullscreenButton.ts +++ b/src/content/components/extension/ImageShowFullscreenButton.ts @@ -1,8 +1,8 @@ import { BaseComponent } from "$content/components/base/BaseComponent"; import { getComponent } from "$content/components/base/component-utils"; -import MiscSettings from "$lib/extension/settings/MiscSettings"; -import { FullscreenViewer } from "$content/components/FullscreenViewer"; -import type { MediaBoxTools } from "$content/components/MediaBoxTools"; +import MiscPreferences from "$lib/extension/preferences/MiscPreferences"; +import { FullscreenViewer } from "$content/components/extension/FullscreenViewer"; +import type { MediaBoxTools } from "$content/components/extension/MediaBoxTools"; export class ImageShowFullscreenButton extends BaseComponent { #mediaBoxTools: MediaBoxTools | null = null; @@ -10,8 +10,6 @@ export class ImageShowFullscreenButton extends BaseComponent { protected build() { this.container.innerText = '🔍'; - - ImageShowFullscreenButton.#miscSettings ??= new MiscSettings(); } protected init() { @@ -27,14 +25,14 @@ export class ImageShowFullscreenButton extends BaseComponent { this.on('click', this.#onButtonClicked.bind(this)); - if (ImageShowFullscreenButton.#miscSettings) { - ImageShowFullscreenButton.#miscSettings.resolveFullscreenViewerEnabled() + if (ImageShowFullscreenButton.#preferences) { + ImageShowFullscreenButton.#preferences.fullscreenViewer.get() .then(isEnabled => { this.#isFullscreenButtonEnabled = isEnabled; this.#updateFullscreenButtonVisibility(); }) .then(() => { - ImageShowFullscreenButton.#miscSettings?.subscribe(settings => { + ImageShowFullscreenButton.#preferences?.subscribe(settings => { this.#isFullscreenButtonEnabled = settings.fullscreenViewer ?? true; this.#updateFullscreenButtonVisibility(); }) @@ -58,6 +56,15 @@ export class ImageShowFullscreenButton extends BaseComponent { ?.show(imageLinks); } + static create(): HTMLElement { + const element = document.createElement('div'); + element.classList.add('media-box-show-fullscreen'); + + new ImageShowFullscreenButton(element); + + return element; + } + static #viewer: FullscreenViewer | null = null; static #resolveViewer(): FullscreenViewer { @@ -76,14 +83,5 @@ export class ImageShowFullscreenButton extends BaseComponent { return viewer; } - static #miscSettings: MiscSettings | null = null; -} - -export function createImageShowFullscreenButton() { - const element = document.createElement('div'); - element.classList.add('media-box-show-fullscreen'); - - new ImageShowFullscreenButton(element); - - return element; + static #preferences = new MiscPreferences(); } diff --git a/src/content/components/MediaBoxTools.ts b/src/content/components/extension/MediaBoxTools.ts similarity index 52% rename from src/content/components/MediaBoxTools.ts rename to src/content/components/extension/MediaBoxTools.ts index 662dfe6..0c05ee5 100644 --- a/src/content/components/MediaBoxTools.ts +++ b/src/content/components/extension/MediaBoxTools.ts @@ -1,14 +1,14 @@ import { BaseComponent } from "$content/components/base/BaseComponent"; import { getComponent } from "$content/components/base/component-utils"; -import { MaintenancePopup } from "$content/components/MaintenancePopup"; +import { TaggingProfilePopup } from "$content/components/extension/profiles/TaggingProfilePopup"; import { on } from "$content/components/events/comms"; import { EVENT_ACTIVE_PROFILE_CHANGED } from "$content/components/events/maintenance-popup-events"; -import type { MediaBoxWrapper } from "$content/components/MediaBoxWrapper"; -import type MaintenanceProfile from "$entities/MaintenanceProfile"; +import type { MediaBox } from "$content/components/philomena/MediaBox"; +import type TaggingProfile from "$entities/TaggingProfile"; export class MediaBoxTools extends BaseComponent { - #mediaBox: MediaBoxWrapper | null = null; - #maintenancePopup: MaintenancePopup | null = null; + #mediaBox: MediaBox | null = null; + #maintenancePopup: TaggingProfilePopup | null = null; init() { const mediaBoxElement = this.container.closest('.media-box'); @@ -34,7 +34,7 @@ export class MediaBoxTools extends BaseComponent { component.initialize(); } - if (!this.#maintenancePopup && component instanceof MaintenancePopup) { + if (!this.#maintenancePopup && component instanceof TaggingProfilePopup) { this.#maintenancePopup = component; } } @@ -42,33 +42,33 @@ export class MediaBoxTools extends BaseComponent { on(this, EVENT_ACTIVE_PROFILE_CHANGED, this.#onActiveProfileChanged.bind(this)); } - #onActiveProfileChanged(profileChangedEvent: CustomEvent) { + #onActiveProfileChanged(profileChangedEvent: CustomEvent) { this.container.classList.toggle('has-active-profile', profileChangedEvent.detail !== null); } - get maintenancePopup(): MaintenancePopup | null { + get maintenancePopup(): TaggingProfilePopup | null { return this.#maintenancePopup; } - get mediaBox(): MediaBoxWrapper | null { + get mediaBox(): MediaBox | null { return this.#mediaBox; } -} -/** - * Create a maintenance popup element. - * @param childrenElements List of children elements to append to the component. - * @return The maintenance popup element. - */ -export function createMediaBoxTools(...childrenElements: HTMLElement[]): HTMLElement { - const mediaBoxToolsContainer = document.createElement('div'); - mediaBoxToolsContainer.classList.add('media-box-tools'); + /** + * Create a maintenance popup element. + * @param childrenElements List of children elements to append to the component. + * @return The maintenance popup element. + */ + static create(...childrenElements: HTMLElement[]): HTMLElement { + const mediaBoxToolsContainer = document.createElement('div'); + mediaBoxToolsContainer.classList.add('media-box-tools'); - if (childrenElements.length) { - mediaBoxToolsContainer.append(...childrenElements); + if (childrenElements.length) { + mediaBoxToolsContainer.append(...childrenElements); + } + + new MediaBoxTools(mediaBoxToolsContainer); + + return mediaBoxToolsContainer; } - - new MediaBoxTools(mediaBoxToolsContainer); - - return mediaBoxToolsContainer; } diff --git a/src/content/components/extension/presets/EditorPresetsBlock.ts b/src/content/components/extension/presets/EditorPresetsBlock.ts new file mode 100644 index 0000000..80cba5c --- /dev/null +++ b/src/content/components/extension/presets/EditorPresetsBlock.ts @@ -0,0 +1,102 @@ +import { BaseComponent } from "$content/components/base/BaseComponent"; +import TagEditorPreset from "$entities/TagEditorPreset"; +import PresetTableRow from "$content/components/extension/presets/PresetTableRow"; +import { createFontAwesomeIcon } from "$lib/dom-utils"; +import { sortEntitiesByField } from "$lib/utils"; + +export default class EditorPresetsBlock extends BaseComponent { + #presetsTable = document.createElement('table'); + #presetBlocks: PresetTableRow[] = []; + #tags: Set = new Set(); + + protected build() { + this.container.classList.add('block', 'hidden', 'tag-presets'); + this.container.style.marginTop = 'var(--block-spacing)'; + + const header = document.createElement('div'); + header.classList.add('block__header'); + + const headerTitle = document.createElement('div'); + headerTitle.classList.add('block__header__title') + headerTitle.textContent = ' Presets'; + + const content = document.createElement('div'); + content.classList.add('block__content'); + + this.#presetsTable.append( + document.createElement('thead'), + document.createElement('tbody'), + ); + + this.#presetsTable.tHead?.append( + EditorPresetsBlock.#createRowWithTableHeads([ + 'Name', + 'Tags', + 'Actions' + ]), + ); + + headerTitle.prepend(createFontAwesomeIcon('layer-group')); + header.append(headerTitle); + content.append(this.#presetsTable); + + this.container.append( + header, + content, + ); + } + + protected init() { + TagEditorPreset.readAll() + .then(this.#refreshPresets.bind(this)) + .then(() => TagEditorPreset.subscribe(this.#refreshPresets.bind(this))); + } + + toggleVisibility(shouldBeVisible: boolean | undefined = undefined) { + this.container.classList.toggle('hidden', shouldBeVisible); + } + + updateTags(tags: Set) { + this.#tags = tags; + + for (const presetBlock of this.#presetBlocks) { + presetBlock.updateTags(tags); + } + } + + #refreshPresets(presetsList: TagEditorPreset[]) { + if (this.#presetBlocks.length) { + for (const block of this.#presetBlocks) { + block.remove(); + } + } + + for (const preset of sortEntitiesByField(presetsList, "name")) { + const block = PresetTableRow.create(preset); + this.#presetsTable.tBodies[0]?.append(block.container); + block.initialize(); + block.updateTags(this.#tags); + + this.#presetBlocks.push(block); + } + } + + static create(): EditorPresetsBlock { + return new EditorPresetsBlock( + document.createElement('div') + ); + } + + static #createRowWithTableHeads(columnNames: string[]): HTMLTableRowElement { + const rowElement = document.createElement('tr'); + + for (const columnName of columnNames) { + const columnHeadElement = document.createElement('th'); + columnHeadElement.textContent = columnName; + + rowElement.append(columnHeadElement); + } + + return rowElement; + } +} diff --git a/src/content/components/extension/presets/PresetTableRow.ts b/src/content/components/extension/presets/PresetTableRow.ts new file mode 100644 index 0000000..61bcafd --- /dev/null +++ b/src/content/components/extension/presets/PresetTableRow.ts @@ -0,0 +1,129 @@ +import { BaseComponent } from "$content/components/base/BaseComponent"; +import type TagEditorPreset from "$entities/TagEditorPreset"; +import { emit } from "$content/components/events/comms"; +import { EVENT_PRESET_TAG_CHANGE_APPLIED } from "$content/components/events/preset-block-events"; +import { createFontAwesomeIcon } from "$lib/dom-utils"; + +export default class PresetTableRow extends BaseComponent { + #preset: TagEditorPreset; + #tagsList: HTMLElement[] = []; + #applyAllButton = document.createElement('button'); + #removeAllButton = document.createElement('button'); + + constructor(container: HTMLElement, preset: TagEditorPreset) { + super(container); + + this.#preset = preset; + } + + protected build() { + this.#tagsList = this.#preset.settings.tags + .toSorted((a, b) => a.localeCompare(b)) + .map(tagName => { + const tagElement = document.createElement('span'); + tagElement.classList.add('tag'); + tagElement.textContent = tagName; + tagElement.dataset.tagName = tagName; + + return tagElement; + }); + + const nameCell = document.createElement('td'); + nameCell.textContent = this.#preset.settings.name; + + const tagsCell = document.createElement('td'); + + const tagsListContainer = document.createElement('div'); + tagsListContainer.classList.add('tag-list'); + tagsListContainer.append(...this.#tagsList); + + tagsCell.append(tagsListContainer); + + const actionsCell = document.createElement('td'); + + const actionsContainer = document.createElement('div'); + actionsContainer.classList.add('flex', 'flex--gap-small'); + + this.#applyAllButton.classList.add('button', 'button--state-success', 'button--bold'); + this.#applyAllButton.append(createFontAwesomeIcon('circle-plus')); + this.#applyAllButton.title = 'Add all tags from this preset into the editor'; + + this.#removeAllButton.classList.add('button', 'button--state-danger', 'button--bold'); + this.#removeAllButton.append(createFontAwesomeIcon('circle-minus')); + this.#removeAllButton.title = 'Remove all tags from this preset from the editor'; + + actionsContainer.append( + this.#applyAllButton, + this.#removeAllButton, + ); + + actionsCell.append(actionsContainer); + + this.container.append( + nameCell, + tagsCell, + actionsCell, + ); + } + + protected init() { + for (const tagElement of this.#tagsList) { + tagElement.addEventListener('click', this.#onTagClicked.bind(this)); + } + + this.#applyAllButton.addEventListener('click', this.#onApplyAllClicked.bind(this)); + this.#removeAllButton.addEventListener('click', this.#onRemoveAllClicked.bind(this)); + } + + #onTagClicked(event: Event) { + const targetElement = event.currentTarget; + + if (!(targetElement instanceof HTMLElement)) { + return; + } + + const tagName = targetElement.dataset.tagName; + const isMissing = targetElement.classList.contains(PresetTableRow.#tagMissingClassName); + + emit(this, EVENT_PRESET_TAG_CHANGE_APPLIED, { + [isMissing ? 'addedTags' : 'removedTags']: new Set([tagName]) + }); + } + + #onApplyAllClicked(event: Event) { + event.preventDefault(); + event.stopPropagation(); + + emit(this, EVENT_PRESET_TAG_CHANGE_APPLIED, { + addedTags: new Set(this.#preset.settings.tags), + }); + } + + #onRemoveAllClicked(event: Event) { + event.preventDefault(); + event.stopPropagation(); + + emit(this, EVENT_PRESET_TAG_CHANGE_APPLIED, { + removedTags: new Set(this.#preset.settings.tags), + }); + } + + updateTags(tags: Set) { + for (const tagElement of this.#tagsList) { + tagElement.classList.toggle( + PresetTableRow.#tagMissingClassName, + !tags.has(tagElement.dataset.tagName || ''), + ); + } + } + + remove() { + this.container.remove(); + } + + static create(preset: TagEditorPreset) { + return new this(document.createElement('tr'), preset); + } + + static #tagMissingClassName = 'is-missing'; +} diff --git a/src/content/components/MaintenancePopup.ts b/src/content/components/extension/profiles/TaggingProfilePopup.ts similarity index 84% rename from src/content/components/MaintenancePopup.ts rename to src/content/components/extension/profiles/TaggingProfilePopup.ts index 53f3cd4..a63b9b4 100644 --- a/src/content/components/MaintenancePopup.ts +++ b/src/content/components/extension/profiles/TaggingProfilePopup.ts @@ -1,8 +1,8 @@ -import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings"; -import MaintenanceProfile from "$entities/MaintenanceProfile"; +import TaggingProfilesPreferences from "$lib/extension/preferences/TaggingProfilesPreferences"; +import TaggingProfile from "$entities/TaggingProfile"; import { BaseComponent } from "$content/components/base/BaseComponent"; import { getComponent } from "$content/components/base/component-utils"; -import ScrapedAPI from "$lib/booru/scraped/ScrapedAPI"; +import ScrapedAPI from "$lib/philomena/scraping/ScrapedAPI"; import { tagsBlacklist } from "$config/tags"; import { emitterAt } from "$content/components/events/comms"; import { @@ -10,8 +10,8 @@ import { EVENT_MAINTENANCE_STATE_CHANGED, EVENT_TAGS_UPDATED } from "$content/components/events/maintenance-popup-events"; -import type { MediaBoxTools } from "$content/components/MediaBoxTools"; -import { resolveTagCategoryFromTagName } from "$lib/booru/tag-utils"; +import type { MediaBoxTools } from "$content/components/extension/MediaBoxTools"; +import { resolveTagCategoryFromTagName } from "$lib/philomena/tag-utils"; class BlackListedTagsEncounteredError extends Error { constructor(tagName: string) { @@ -21,11 +21,11 @@ class BlackListedTagsEncounteredError extends Error { } } -export class MaintenancePopup extends BaseComponent { +export class TaggingProfilePopup extends BaseComponent { #tagsListElement: HTMLElement = document.createElement('div'); #tagsList: HTMLElement[] = []; #suggestedInvalidTags: Map = new Map(); - #activeProfile: MaintenanceProfile | null = null; + #activeProfile: TaggingProfile | null = null; #mediaBoxTools: MediaBoxTools | null = null; #tagsToRemove: Set = new Set(); #tagsToAdd: Set = new Set(); @@ -66,7 +66,7 @@ export class MaintenancePopup extends BaseComponent { this.#mediaBoxTools = mediaBoxTools; - MaintenancePopup.#watchActiveProfile(this.#onActiveProfileChanged.bind(this)); + TaggingProfilePopup.#watchActiveProfile(this.#onActiveProfileChanged.bind(this)); this.#tagsListElement.addEventListener('click', this.#handleTagClick.bind(this)); const mediaBox = this.#mediaBoxTools.mediaBox; @@ -79,7 +79,7 @@ export class MaintenancePopup extends BaseComponent { mediaBox.on('mouseover', this.#onMouseEnteredArea.bind(this)); } - #onActiveProfileChanged(activeProfile: MaintenanceProfile | null) { + #onActiveProfileChanged(activeProfile: TaggingProfile | null) { this.#activeProfile = activeProfile; this.container.classList.toggle('is-active', activeProfile !== null); this.#refreshTagsList(); @@ -110,7 +110,7 @@ export class MaintenancePopup extends BaseComponent { activeProfileTagsList .sort((a, b) => a.localeCompare(b)) .forEach((tagName, index) => { - const tagElement = MaintenancePopup.#buildTagElement(tagName); + const tagElement = TaggingProfilePopup.#buildTagElement(tagName); this.#tagsList[index] = tagElement; this.#tagsListElement.appendChild(tagElement); @@ -122,10 +122,10 @@ export class MaintenancePopup extends BaseComponent { // Just to prevent duplication, we need to include this tag to the map of suggested invalid tags if (tagsBlacklist.includes(tagName)) { - MaintenancePopup.#markTagElementWithCategory(tagElement, 'error'); + TaggingProfilePopup.#markTagElementWithCategory(tagElement, 'error'); this.#suggestedInvalidTags.set(tagName, tagElement); } else { - MaintenancePopup.#markTagElementWithCategory( + TaggingProfilePopup.#markTagElementWithCategory( tagElement, resolveTagCategoryFromTagName(tagName) ?? '', ); @@ -179,7 +179,7 @@ export class MaintenancePopup extends BaseComponent { if (this.#tagsToAdd.size || this.#tagsToRemove.size) { // Notify only once, when first planning to submit if (!this.#isPlanningToSubmit) { - MaintenancePopup.#notifyAboutPendingSubmission(true); + TaggingProfilePopup.#notifyAboutPendingSubmission(true); } this.#isPlanningToSubmit = true; @@ -197,7 +197,7 @@ export class MaintenancePopup extends BaseComponent { if (this.#isPlanningToSubmit && !this.#isSubmitting) { this.#tagsSubmissionTimer = setTimeout( this.#onSubmissionTimerPassed.bind(this), - MaintenancePopup.#delayBeforeSubmissionMs + TaggingProfilePopup.#delayBeforeSubmissionMs ); } } @@ -214,10 +214,10 @@ export class MaintenancePopup extends BaseComponent { let maybeTagsAndAliasesAfterUpdate; - const shouldAutoRemove = await MaintenancePopup.#maintenanceSettings.resolveStripBlacklistedTags(); + const shouldAutoRemove = await TaggingProfilePopup.#preferences.stripBlacklistedTags.get(); try { - maybeTagsAndAliasesAfterUpdate = await MaintenancePopup.#scrapedAPI.updateImageTags( + maybeTagsAndAliasesAfterUpdate = await TaggingProfilePopup.#scrapedAPI.updateImageTags( this.#mediaBoxTools.mediaBox.imageId, tagsList => { for (let tagName of this.#tagsToRemove) { @@ -250,7 +250,7 @@ export class MaintenancePopup extends BaseComponent { console.warn('Tags submission failed:', e); } - MaintenancePopup.#notifyAboutPendingSubmission(false); + TaggingProfilePopup.#notifyAboutPendingSubmission(false); this.#emitter.emit(EVENT_MAINTENANCE_STATE_CHANGED, 'failed'); this.#isSubmitting = false; @@ -268,7 +268,7 @@ export class MaintenancePopup extends BaseComponent { this.#tagsToRemove.clear(); this.#refreshTagsList(); - MaintenancePopup.#notifyAboutPendingSubmission(false); + TaggingProfilePopup.#notifyAboutPendingSubmission(false); this.#isSubmitting = false; } @@ -292,8 +292,8 @@ export class MaintenancePopup extends BaseComponent { continue; } - const tagElement = MaintenancePopup.#buildTagElement(tagName); - MaintenancePopup.#markTagElementWithCategory(tagElement, 'error'); + const tagElement = TaggingProfilePopup.#buildTagElement(tagName); + TaggingProfilePopup.#markTagElementWithCategory(tagElement, 'error'); tagElement.classList.add('is-present'); this.#suggestedInvalidTags.set(tagName, tagElement); @@ -311,6 +311,14 @@ export class MaintenancePopup extends BaseComponent { return this.container.classList.contains('is-active'); } + static create(): HTMLElement { + const container = document.createElement('div'); + + new this(container); + + return container; + } + static #buildTagElement(tagName: string): HTMLElement { const tagElement = document.createElement('span'); tagElement.classList.add('tag'); @@ -333,7 +341,7 @@ export class MaintenancePopup extends BaseComponent { /** * Controller with maintenance settings. */ - static #maintenanceSettings = new MaintenanceSettings(); + static #preferences = new TaggingProfilesPreferences(); /** * Subscribe to all necessary feeds to watch for every active profile change. Additionally, will execute the callback @@ -341,10 +349,10 @@ export class MaintenancePopup extends BaseComponent { * @param callback Callback to execute whenever selection of active profile or profile itself has been changed. * @return Unsubscribe function. Call it to stop watching for changes. */ - static #watchActiveProfile(callback: (profile: MaintenanceProfile | null) => void): () => void { + static #watchActiveProfile(callback: (profile: TaggingProfile | null) => void): () => void { let lastActiveProfileId: string | null | undefined = null; - const unsubscribeFromProfilesChanges = MaintenanceProfile.subscribe(profiles => { + const unsubscribeFromProfilesChanges = TaggingProfile.subscribe(profiles => { if (lastActiveProfileId) { callback( profiles.find(profile => profile.id === lastActiveProfileId) || null @@ -352,20 +360,18 @@ export class MaintenancePopup extends BaseComponent { } }); - const unsubscribeFromMaintenanceSettings = this.#maintenanceSettings.subscribe(settings => { + const unsubscribeFromMaintenanceSettings = this.#preferences.subscribe(settings => { if (settings.activeProfile === lastActiveProfileId) { return; } lastActiveProfileId = settings.activeProfile; - this.#maintenanceSettings - .resolveActiveProfileAsObject() + this.#preferences.activeProfile.asObject() .then(callback); }); - this.#maintenanceSettings - .resolveActiveProfileAsObject() + this.#preferences.activeProfile.asObject() .then(profileOrNull => { if (profileOrNull) { lastActiveProfileId = profileOrNull.id; @@ -416,11 +422,3 @@ export class MaintenancePopup extends BaseComponent { */ static #pendingSubmissionCount: number|null = null; } - -export function createMaintenancePopup() { - const container = document.createElement('div'); - - new MaintenancePopup(container); - - return container; -} diff --git a/src/content/components/MaintenanceStatusIcon.ts b/src/content/components/extension/profiles/TaggingProfileStatusIcon.ts similarity index 81% rename from src/content/components/MaintenanceStatusIcon.ts rename to src/content/components/extension/profiles/TaggingProfileStatusIcon.ts index 9c67830..6e5ab8b 100644 --- a/src/content/components/MaintenanceStatusIcon.ts +++ b/src/content/components/extension/profiles/TaggingProfileStatusIcon.ts @@ -2,9 +2,9 @@ import { BaseComponent } from "$content/components/base/BaseComponent"; import { getComponent } from "$content/components/base/component-utils"; import { on } from "$content/components/events/comms"; import { EVENT_MAINTENANCE_STATE_CHANGED } from "$content/components/events/maintenance-popup-events"; -import type { MediaBoxTools } from "$content/components/MediaBoxTools"; +import type { MediaBoxTools } from "$content/components/extension/MediaBoxTools"; -export class MaintenanceStatusIcon extends BaseComponent { +export class TaggingProfileStatusIcon extends BaseComponent { #mediaBoxTools: MediaBoxTools | null = null; build() { @@ -52,13 +52,13 @@ export class MaintenanceStatusIcon extends BaseComponent { this.container.innerText = '❓'; } } -} - -export function createMaintenanceStatusIcon() { - const element = document.createElement('div'); - element.classList.add('maintenance-status-icon'); - - new MaintenanceStatusIcon(element); - - return element; + + static create(): HTMLElement { + const element = document.createElement('div'); + element.classList.add('maintenance-status-icon'); + + new TaggingProfileStatusIcon(element); + + return element; + } } diff --git a/src/content/components/BlockCommunication.ts b/src/content/components/philomena/BlockCommunication.ts similarity index 92% rename from src/content/components/BlockCommunication.ts rename to src/content/components/philomena/BlockCommunication.ts index 0c0d151..da192e3 100644 --- a/src/content/components/BlockCommunication.ts +++ b/src/content/components/philomena/BlockCommunication.ts @@ -1,7 +1,7 @@ import { BaseComponent } from "$content/components/base/BaseComponent"; -import TagSettings from "$lib/extension/settings/TagSettings"; +import TagsPreferences from "$lib/extension/preferences/TagsPreferences"; import { getComponent } from "$content/components/base/component-utils"; -import { resolveTagNameFromLink, resolveTagCategoryFromTagName } from "$lib/booru/tag-utils"; +import { resolveTagNameFromLink, resolveTagCategoryFromTagName } from "$lib/philomena/tag-utils"; export class BlockCommunication extends BaseComponent { #contentSection: HTMLElement | null = null; @@ -17,8 +17,8 @@ export class BlockCommunication extends BaseComponent { protected init() { Promise.all([ - BlockCommunication.#tagSettings.resolveReplaceLinks(), - BlockCommunication.#tagSettings.resolveReplaceLinkText(), + BlockCommunication.#preferences.replaceLinks.get(), + BlockCommunication.#preferences.replaceLinkText.get(), ]).then(([replaceLinks, replaceLinkText]) => { this.#onReplaceLinkSettingResolved( replaceLinks, @@ -26,7 +26,7 @@ export class BlockCommunication extends BaseComponent { ); }); - BlockCommunication.#tagSettings.subscribe(settings => { + BlockCommunication.#preferences.subscribe(settings => { this.#onReplaceLinkSettingResolved( settings.replaceLinks ?? false, settings.replaceLinkText ?? true @@ -112,7 +112,7 @@ export class BlockCommunication extends BaseComponent { ); } - static #tagSettings = new TagSettings(); + static #preferences = new TagsPreferences(); /** * Map of links to their original texts. These texts need to be stored here to make them restorable. Keys is a link diff --git a/src/content/components/MediaBoxWrapper.ts b/src/content/components/philomena/MediaBox.ts similarity index 56% rename from src/content/components/MediaBoxWrapper.ts rename to src/content/components/philomena/MediaBox.ts index a694f70..2403b69 100644 --- a/src/content/components/MediaBoxWrapper.ts +++ b/src/content/components/philomena/MediaBox.ts @@ -1,10 +1,10 @@ import { BaseComponent } from "$content/components/base/BaseComponent"; import { getComponent } from "$content/components/base/component-utils"; -import { buildTagsAndAliasesMap } from "$lib/booru/tag-utils"; +import { buildTagsAndAliasesMap } from "$lib/philomena/tag-utils"; import { on } from "$content/components/events/comms"; import { EVENT_TAGS_UPDATED } from "$content/components/events/maintenance-popup-events"; -export class MediaBoxWrapper extends BaseComponent { +export class MediaBox extends BaseComponent { #thumbnailContainer: HTMLElement | null = null; #imageLinkElement: HTMLAnchorElement | null = null; #tagsAndAliases: Map | null = null; @@ -60,40 +60,44 @@ export class MediaBoxWrapper extends BaseComponent { return JSON.parse(jsonUris); } -} -/** - * Wrap the media box element into the special wrapper. - */ -export function initializeMediaBox(mediaBoxContainer: HTMLElement, childComponentElements: HTMLElement[]) { - new MediaBoxWrapper(mediaBoxContainer) - .initialize(); + /** + * Wrap the media box element into the special wrapper. + */ + static initialize(mediaBoxContainer: HTMLElement, childComponentElements: HTMLElement[]) { + new MediaBox(mediaBoxContainer) + .initialize(); - for (let childComponentElement of childComponentElements) { - mediaBoxContainer.appendChild(childComponentElement); - getComponent(childComponentElement)?.initialize(); + for (let childComponentElement of childComponentElements) { + mediaBoxContainer.appendChild(childComponentElement); + getComponent(childComponentElement)?.initialize(); + } + } + + static findElements(): NodeListOf { + return document.querySelectorAll('.media-box'); + } + + static initializePositionCalculation(mediaBoxesList: NodeListOf) { + window.addEventListener('resize', () => { + let lastMediaBox: HTMLElement | null = null; + let lastMediaBoxPosition: number | null = null; + + for (const mediaBoxElement of mediaBoxesList) { + const yPosition = mediaBoxElement.getBoundingClientRect().y; + const isOnTheSameLine = yPosition === lastMediaBoxPosition; + + mediaBoxElement.classList.toggle('media-box--first', !isOnTheSameLine); + lastMediaBox?.classList.toggle('media-box--last', !isOnTheSameLine); + + lastMediaBox = mediaBoxElement; + lastMediaBoxPosition = yPosition; + } + + // Last-ever media box is checked separately + if (lastMediaBox && !lastMediaBox.nextElementSibling) { + lastMediaBox.classList.add('media-box--last'); + } + }) } } - -export function calculateMediaBoxesPositions(mediaBoxesList: NodeListOf) { - window.addEventListener('resize', () => { - let lastMediaBox: HTMLElement | null = null; - let lastMediaBoxPosition: number | null = null; - - for (const mediaBoxElement of mediaBoxesList) { - const yPosition = mediaBoxElement.getBoundingClientRect().y; - const isOnTheSameLine = yPosition === lastMediaBoxPosition; - - mediaBoxElement.classList.toggle('media-box--first', !isOnTheSameLine); - lastMediaBox?.classList.toggle('media-box--last', !isOnTheSameLine); - - lastMediaBox = mediaBoxElement; - lastMediaBoxPosition = yPosition; - } - - // Last-ever media box is checked separately - if (lastMediaBox && !lastMediaBox.nextElementSibling) { - lastMediaBox.classList.add('media-box--last'); - } - }) -} diff --git a/src/content/components/TagDropdownWrapper.ts b/src/content/components/philomena/TagDropdown.ts similarity index 70% rename from src/content/components/TagDropdownWrapper.ts rename to src/content/components/philomena/TagDropdown.ts index f970792..8719281 100644 --- a/src/content/components/TagDropdownWrapper.ts +++ b/src/content/components/philomena/TagDropdown.ts @@ -1,6 +1,6 @@ import { BaseComponent } from "$content/components/base/BaseComponent"; -import MaintenanceProfile from "$entities/MaintenanceProfile"; -import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings"; +import TaggingProfile from "$entities/TaggingProfile"; +import TaggingProfilesPreferences from "$lib/extension/preferences/TaggingProfilesPreferences"; import { getComponent } from "$content/components/base/component-utils"; import CustomCategoriesResolver from "$lib/extension/CustomCategoriesResolver"; import { on } from "$content/components/events/comms"; @@ -8,9 +8,7 @@ import { EVENT_FORM_EDITOR_UPDATED } from "$content/components/events/tags-form- import { EVENT_TAG_GROUP_RESOLVED } from "$content/components/events/tag-dropdown-events"; import type TagGroup from "$entities/TagGroup"; -const categoriesResolver = new CustomCategoriesResolver(); - -export class TagDropdownWrapper extends BaseComponent { +export class TagDropdown extends BaseComponent { /** * Container with dropdown elements to insert options into. */ @@ -29,7 +27,7 @@ export class TagDropdownWrapper extends BaseComponent { /** * Local clone of the currently active profile used for updating the list of tags. */ - #activeProfile: MaintenanceProfile | null = null; + #activeProfile: TaggingProfile | null = null; /** * Is cursor currently entered the dropdown. @@ -46,7 +44,7 @@ export class TagDropdownWrapper extends BaseComponent { this.on('mouseenter', this.#onDropdownEntered.bind(this)); this.on('mouseleave', this.#onDropdownLeft.bind(this)); - TagDropdownWrapper.#watchActiveProfile(activeProfileOrNull => { + TagDropdown.#watchActiveProfile(activeProfileOrNull => { this.#activeProfile = activeProfileOrNull; if (this.#isEntered) { @@ -122,7 +120,7 @@ export class TagDropdownWrapper extends BaseComponent { #updateButtons() { if (!this.#activeProfile) { - this.#addToNewButton ??= TagDropdownWrapper.#createDropdownLink( + this.#addToNewButton ??= TagDropdown.#createDropdownLink( 'Add to new tagging profile', this.#onAddToNewClicked.bind(this) ); @@ -135,7 +133,7 @@ export class TagDropdownWrapper extends BaseComponent { } if (this.#activeProfile) { - this.#toggleOnExistingButton ??= TagDropdownWrapper.#createDropdownLink( + this.#toggleOnExistingButton ??= TagDropdown.#createDropdownLink( 'Add to existing tagging profile', this.#onToggleInExistingClicked.bind(this) ); @@ -172,14 +170,14 @@ export class TagDropdownWrapper extends BaseComponent { throw new Error('Missing tag name to create the profile!'); } - const profile = new MaintenanceProfile(crypto.randomUUID(), { + const profile = new TaggingProfile(crypto.randomUUID(), { name: 'Temporary Profile (' + (new Date().toISOString()) + ')', tags: [this.tagName], temporary: true, }); await profile.save(); - await TagDropdownWrapper.#maintenanceSettings.setActiveProfileId(profile.id); + await TagDropdown.#preferences.activeProfile.set(profile.id); } async #onToggleInExistingClicked() { @@ -205,25 +203,25 @@ export class TagDropdownWrapper extends BaseComponent { await this.#activeProfile.save(); } - static #maintenanceSettings = new MaintenanceSettings(); + static #preferences = new TaggingProfilesPreferences(); /** * Watch for changes to active profile. * @param onActiveProfileChange Callback to call when profile was * changed. */ - static #watchActiveProfile(onActiveProfileChange: (profile: MaintenanceProfile | null) => void) { + static #watchActiveProfile(onActiveProfileChange: (profile: TaggingProfile | null) => void) { let lastActiveProfile: string | null = null; - this.#maintenanceSettings.subscribe((settings) => { + this.#preferences.subscribe((settings) => { lastActiveProfile = settings.activeProfile ?? null; - this.#maintenanceSettings - .resolveActiveProfileAsObject() + this.#preferences + .activeProfile.asObject() .then(onActiveProfileChange); }); - MaintenanceProfile.subscribe(profiles => { + TaggingProfile.subscribe(profiles => { const activeProfile = profiles .find(profile => profile.id === lastActiveProfile); @@ -231,8 +229,8 @@ export class TagDropdownWrapper extends BaseComponent { ); }); - this.#maintenanceSettings - .resolveActiveProfileAsObject() + this.#preferences + .activeProfile.asObject() .then(activeProfile => { lastActiveProfile = activeProfile?.id ?? null; onActiveProfileChange(activeProfile); @@ -263,58 +261,65 @@ export class TagDropdownWrapper extends BaseComponent { return dropdownLink; } -} -export function wrapTagDropdown(element: HTMLElement) { - // Skip initialization when tag component is already wrapped - if (getComponent(element)) { - return; + static #categoriesResolver = new CustomCategoriesResolver(); + static #processedElements: WeakSet = new WeakSet(); + + static #findAll(parentNode: ParentNode = document): NodeListOf { + return parentNode.querySelectorAll('.tag.dropdown'); } - const tagDropdown = new TagDropdownWrapper(element); - tagDropdown.initialize(); + static #initialize(element: HTMLElement) { + // Skip initialization when tag component is already wrapped + if (getComponent(element)) { + return; + } - categoriesResolver.addElement(tagDropdown); -} + const tagDropdown = new TagDropdown(element); + tagDropdown.initialize(); -const processedElementsSet = new WeakSet(); - -export function watchTagDropdownsInTagsEditor() { - // We only need to watch for new editor elements if there is a tag editor present on the page - if (!document.querySelector('#image_tags_and_source')) { - return; + this.#categoriesResolver.addElement(tagDropdown); } - document.body.addEventListener('mouseover', event => { - const targetElement = event.target; + static findAllAndInitialize(parentNode: ParentNode = document) { + for (const element of this.#findAll(parentNode)) { + this.#initialize(element); + } + } - if (!(targetElement instanceof HTMLElement)) { + static watch() { + // We only need to watch for new editor elements if there is a tag editor present on the page + if (!document.querySelector('#image_tags_and_source')) { return; } - if (processedElementsSet.has(targetElement)) { - return; - } + document.body.addEventListener('mouseover', event => { + const targetElement = event.target; - const closestTagEditor = targetElement.closest('#image_tags_and_source'); + if (!(targetElement instanceof HTMLElement)) { + return; + } - if (!closestTagEditor || processedElementsSet.has(closestTagEditor)) { - processedElementsSet.add(targetElement); - return; - } + if (this.#processedElements.has(targetElement)) { + return; + } - processedElementsSet.add(targetElement); - processedElementsSet.add(closestTagEditor); + const closestTagEditor = targetElement.closest('#image_tags_and_source'); - for (const tagDropdownElement of closestTagEditor.querySelectorAll('.tag.dropdown')) { - wrapTagDropdown(tagDropdownElement); - } - }); + if (!closestTagEditor || this.#processedElements.has(closestTagEditor)) { + this.#processedElements.add(targetElement); + return; + } - // When form is submitted, its DOM is completely updated. We need to fetch those tags in this case. - on(document.body, EVENT_FORM_EDITOR_UPDATED, event => { - for (const tagDropdownElement of event.detail.querySelectorAll('.tag.dropdown')) { - wrapTagDropdown(tagDropdownElement); - } - }); + this.#processedElements.add(targetElement); + this.#processedElements.add(closestTagEditor); + + this.findAllAndInitialize(closestTagEditor); + }); + + // When form is submitted, its DOM is completely updated. We need to fetch those tags in this case. + on(document.body, EVENT_FORM_EDITOR_UPDATED, event => { + this.findAllAndInitialize(event.detail); + }); + } } diff --git a/src/content/components/philomena/TagsForm.ts b/src/content/components/philomena/TagsForm.ts new file mode 100644 index 0000000..7a27cdc --- /dev/null +++ b/src/content/components/philomena/TagsForm.ts @@ -0,0 +1,291 @@ +import { BaseComponent } from "$content/components/base/BaseComponent"; +import { getComponent } from "$content/components/base/component-utils"; +import { emit, on, type UnsubscribeFunction } from "$content/components/events/comms"; +import { EVENT_FETCH_COMPLETE, EVENT_RELOAD, type ReloadCustomOptions } from "$content/components/events/booru-events"; +import { EVENT_FORM_EDITOR_UPDATED } from "$content/components/events/tags-form-events"; +import EditorPresetsBlock from "$content/components/extension/presets/EditorPresetsBlock"; +import { EVENT_PRESET_TAG_CHANGE_APPLIED, type PresetTagChange } from "$content/components/events/preset-block-events"; + +export class TagsForm extends BaseComponent { + #togglePresetsButton: HTMLButtonElement = document.createElement('button'); + #presetsList = EditorPresetsBlock.create(); + #plainEditorTextarea: HTMLTextAreaElement|null = null; + #fancyEditorInput: HTMLInputElement|null = null; + #tagsSet: Set = new Set(); + + protected build() { + this.#togglePresetsButton.classList.add( + 'button', + 'button--state-primary', + 'button--bold', + 'button--separate-left', + ); + + this.#togglePresetsButton.textContent = 'Presets'; + + this.container + .querySelector(':is(.fancy-tag-edit, .fancy-tag-upload) ~ button:last-of-type') + ?.after(this.#togglePresetsButton, this.#presetsList.container); + + this.#plainEditorTextarea = this.container.querySelector('textarea.tagsinput'); + this.#fancyEditorInput = this.container.querySelector('.js-taginput-fancy input'); + } + + protected init() { + // Site sending the event when form is submitted vie Fetch API. We use this event to reload our logic here. + const unsubscribe = on( + this.container, + EVENT_FETCH_COMPLETE, + () => this.#waitAndDetectUpdatedForm(unsubscribe), + ); + + this.#togglePresetsButton.addEventListener('click', this.#togglePresetsList.bind(this)); + this.#presetsList.initialize(); + + this.#plainEditorTextarea?.addEventListener('input', this.#refreshTagsListForPresets.bind(this)); + this.#fancyEditorInput?.addEventListener('keydown', this.#refreshTagsListForPresets.bind(this)); + + this.#refreshTagsListForPresets(); + + on(this.#presetsList, EVENT_PRESET_TAG_CHANGE_APPLIED, this.#onTagChangeRequested.bind(this)); + + if (this.#plainEditorTextarea) { + // When reloaded, we should catch and refresh the colors. Extension reuses this event to force site to update + // list of tags in the fancy tag editor. + on(this.#plainEditorTextarea, EVENT_RELOAD, this.#onPlainEditorReloadRequested.bind(this)); + } + } + + #waitAndDetectUpdatedForm(unsubscribe: UnsubscribeFunction): void { + const elementContainingTagEditor = this.container + .closest('#image_tags_and_source') + ?.parentElement; + + if (!elementContainingTagEditor) { + return; + } + + const observer = new MutationObserver(() => { + const tagsFormElement = elementContainingTagEditor.querySelector('#tags-form'); + + if (!tagsFormElement || getComponent(tagsFormElement)) { + return; + } + + const tagFormComponent = new TagsForm(tagsFormElement); + tagFormComponent.initialize(); + + const fullTagEditor = tagFormComponent.parentTagEditorElement; + + if (fullTagEditor) { + emit(document.body, EVENT_FORM_EDITOR_UPDATED, fullTagEditor); + } else { + console.info('Tag form is not in the tag editor. Event is not sent.'); + } + + observer.disconnect(); + unsubscribe(); + }); + + observer.observe(elementContainingTagEditor, { + subtree: true, + childList: true, + }); + + // Make sure to forcibly disconnect everything after a while. + setTimeout(() => { + observer.disconnect(); + unsubscribe(); + }, 5000); + } + + get parentTagEditorElement(): HTMLElement | null { + return this.container.closest('.js-tagsauce') + } + + /** + * Collect all the tag categories available on the page and color the tags in the editor according to them. + */ + refreshTagColors() { + const tagCategories = this.#gatherTagCategories(); + const editableTags = this.container.querySelectorAll('.tag'); + + for (const tagElement of editableTags) { + // Tag name is stored in the "remove" link and not in the tag itself. + const removeLink = tagElement.querySelector('a'); + + if (!removeLink) { + continue; + } + + const tagName = removeLink.dataset.tagName; + + if (!tagName || !tagCategories.has(tagName)) { + continue; + } + + const categoryName = tagCategories.get(tagName)!; + + tagElement.dataset.tagCategory = categoryName; + tagElement.setAttribute('data-tag-category', categoryName); + } + } + + /** + * Collect list of categories from the tags on the page. + * @return + */ + #gatherTagCategories(): Map { + const tagCategories: Map = new Map(); + + for (const tagElement of document.querySelectorAll('.tag[data-tag-name][data-tag-category]')) { + const tagName = tagElement.dataset.tagName; + const tagCategory = tagElement.dataset.tagCategory; + + if (!tagName || !tagCategory) { + console.warn('Missing tag name or category!'); + continue; + } + + tagCategories.set(tagName, tagCategory); + } + + return tagCategories; + } + + #togglePresetsList(event: Event) { + event.stopPropagation(); + event.preventDefault(); + + this.#presetsList.toggleVisibility(); + this.#refreshTagsListForPresets(); + } + + #refreshTagsListForPresets() { + this.#tagsSet = new Set( + this.#plainEditorTextarea?.value + .split(',') + .map(tagName => tagName.trim()) + ); + + this.#presetsList.updateTags(this.#tagsSet); + } + + #onTagChangeRequested(event: CustomEvent) { + const { addedTags = null, removedTags = null } = event.detail; + let tagChangeList: string[] = []; + + if (addedTags) { + tagChangeList.push(...addedTags); + } + + if (removedTags) { + tagChangeList.push( + ...Array.from(removedTags) + .filter(tagName => this.#tagsSet.has(tagName)) + .map(tagName => `-${tagName}`) + ); + } + + const offsetBeforeSubmission = this.#presetsList.container.offsetTop; + + this.#applyTagChangesWithFancyTagEditor( + tagChangeList.join(',') + ); + + const offsetDifference = this.#presetsList.container.offsetTop - offsetBeforeSubmission; + + // Compensating for the layout shift: when user clicks on a tag (or on "add/remove all tags"), tag editor might + // overflow the current line and wrap tags around to the next line, causing presets section to shift. We need to + // avoid that for better UX. + if (offsetDifference !== 0) { + window.scrollTo({ + top: window.scrollY + offsetDifference, + behavior: 'instant', + }); + } + } + + #applyTagChangesWithFancyTagEditor(tagsListWithChanges: string): void { + if (!this.#fancyEditorInput || !this.#plainEditorTextarea) { + return; + } + + const originalValue = this.#fancyEditorInput.value; + + // We have to tell plain text editor to also refresh the list of tags in the fancy editor, just in case user + // made changes to it in plain mode. + emit(this.#plainEditorTextarea, EVENT_RELOAD, { + // Sending that we don't need to refresh the color on this event, since we will do that ourselves later, after + // changes are applied. + skipTagColorRefresh: true, + skipTagRefresh: true, + }); + + this.#fancyEditorInput.value = tagsListWithChanges; + this.#fancyEditorInput.dispatchEvent(new KeyboardEvent('keydown', { + key: 'Comma', + })); + + this.#fancyEditorInput.value = originalValue; + + this.refreshTagColors(); + } + + #onPlainEditorReloadRequested(event: CustomEvent) { + if (!event.detail?.skipTagColorRefresh) { + this.refreshTagColors(); + } + + if (!event.detail?.skipTagRefresh) { + this.#refreshTagsListForPresets(); + } + } + + static watchForEditors() { + document.body.addEventListener('click', event => { + const targetElement = event.target; + + if (!(targetElement instanceof HTMLElement)) { + return; + } + + const tagEditorWrapper = targetElement.closest('#image_tags_and_source'); + + if (!tagEditorWrapper) { + return; + } + + const refreshTrigger = targetElement.closest('.js-taginput-show, #edit-tags') + + if (!refreshTrigger) { + return; + } + + const tagFormElement = tagEditorWrapper.querySelector('#tags-form'); + + if (!tagFormElement) { + return; + } + + let tagEditor = getComponent(tagFormElement); + + if (!tagEditor || !(tagEditor instanceof TagsForm)) { + tagEditor = new TagsForm(tagFormElement); + tagEditor.initialize(); + } + + (tagEditor as TagsForm).refreshTagColors(); + }); + } + + static initializeUploadEditor() { + const uploadEditorContainer = document.querySelector('.field:has(.fancy-tag-upload)'); + + if (!uploadEditorContainer) { + return; + } + + new TagsForm(uploadEditorContainer).initialize(); + } +} diff --git a/src/content/components/TagsListBlock.ts b/src/content/components/philomena/TagsListBlock.ts similarity index 86% rename from src/content/components/TagsListBlock.ts rename to src/content/components/philomena/TagsListBlock.ts index 3a5bf5e..0b2cead 100644 --- a/src/content/components/TagsListBlock.ts +++ b/src/content/components/philomena/TagsListBlock.ts @@ -1,11 +1,11 @@ import { BaseComponent } from "$content/components/base/BaseComponent"; import type TagGroup from "$entities/TagGroup"; -import type { TagDropdownWrapper } from "$content/components/TagDropdownWrapper"; +import type { TagDropdown } from "$content/components/philomena/TagDropdown"; import { on } from "$content/components/events/comms"; import { EVENT_FORM_EDITOR_UPDATED } from "$content/components/events/tags-form-events"; import { getComponent } from "$content/components/base/component-utils"; import { EVENT_TAG_GROUP_RESOLVED } from "$content/components/events/tag-dropdown-events"; -import TagSettings from "$lib/extension/settings/TagSettings"; +import TagsPreferences from "$lib/extension/preferences/TagsPreferences"; export class TagsListBlock extends BaseComponent { #tagsListButtonsContainer: HTMLElement | null = null; @@ -14,14 +14,14 @@ export class TagsListBlock extends BaseComponent { #toggleGroupingButton = document.createElement('a'); #toggleGroupingButtonIcon = document.createElement('i'); - #tagSettings = new TagSettings(); + #preferences = new TagsPreferences(); #shouldDisplaySeparation = false; #separatedGroups = new Map(); #separatedHeaders = new Map(); #groupsCount = new Map(); - #lastTagGroup = new WeakMap; + #lastTagGroup = new WeakMap; #isReorderingPlanned = false; @@ -44,8 +44,8 @@ export class TagsListBlock extends BaseComponent { } init() { - this.#tagSettings.resolveGroupSeparation().then(this.#onTagSeparationChange.bind(this)); - this.#tagSettings.subscribe(settings => { + this.#preferences.groupSeparation.get().then(this.#onTagSeparationChange.bind(this)); + this.#preferences.subscribe(settings => { this.#onTagSeparationChange(Boolean(settings.groupSeparation)) }); @@ -80,7 +80,7 @@ export class TagsListBlock extends BaseComponent { return; } - const tagDropdown = getComponent(maybeDropdownElement); + const tagDropdown = getComponent(maybeDropdownElement); if (!tagDropdown) { return; @@ -103,7 +103,7 @@ export class TagsListBlock extends BaseComponent { #onToggleGroupingClicked(event: Event) { event.preventDefault(); - void this.#tagSettings.setGroupSeparation(!this.#shouldDisplaySeparation); + void this.#preferences.groupSeparation.set(!this.#shouldDisplaySeparation); } #handleTagGroupChanges(tagGroup: TagGroup) { @@ -146,7 +146,7 @@ export class TagsListBlock extends BaseComponent { heading.innerText = group.settings.name; } - #handleResolvedTagGroup(resolvedGroup: TagGroup | null, tagComponent: TagDropdownWrapper) { + #handleResolvedTagGroup(resolvedGroup: TagGroup | null, tagComponent: TagDropdown) { const previousGroupId = this.#lastTagGroup.get(tagComponent)?.id; const currentGroupId = resolvedGroup?.id; const isDifferentId = currentGroupId !== previousGroupId; @@ -217,28 +217,28 @@ export class TagsListBlock extends BaseComponent { static #iconGroupingDisabled = 'fa-folder'; static #iconGroupingEnabled = 'fa-folder-tree'; -} -export function initializeAllTagsLists() { - for (let element of document.querySelectorAll('#image_tags_and_source')) { - if (getComponent(element)) { - return; + static initializeAll() { + for (let element of document.querySelectorAll('#image_tags_and_source')) { + if (getComponent(element)) { + return; + } + + new TagsListBlock(element) + .initialize(); } + } - new TagsListBlock(element) - .initialize(); + static watchUpdatedLists() { + on(document, EVENT_FORM_EDITOR_UPDATED, event => { + const tagsListElement = event.detail.closest('#image_tags_and_source'); + + if (!tagsListElement || getComponent(tagsListElement)) { + return; + } + + new TagsListBlock(tagsListElement) + .initialize(); + }) } } - -export function watchForUpdatedTagLists() { - on(document, EVENT_FORM_EDITOR_UPDATED, event => { - const tagsListElement = event.detail.closest('#image_tags_and_source'); - - if (!tagsListElement || getComponent(tagsListElement)) { - return; - } - - new TagsListBlock(tagsListElement) - .initialize(); - }); -} diff --git a/src/content/components/listing/ImageListContainer.ts b/src/content/components/philomena/listing/ImageListContainer.ts similarity index 58% rename from src/content/components/listing/ImageListContainer.ts rename to src/content/components/philomena/listing/ImageListContainer.ts index 3f07bdc..20e4798 100644 --- a/src/content/components/listing/ImageListContainer.ts +++ b/src/content/components/philomena/listing/ImageListContainer.ts @@ -1,5 +1,5 @@ import { BaseComponent } from "$content/components/base/BaseComponent"; -import { ImageListInfo } from "$content/components/listing/ImageListInfo"; +import { ImageListInfo } from "$content/components/philomena/listing/ImageListInfo"; export class ImageListContainer extends BaseComponent { #info: ImageListInfo | null = null; @@ -12,8 +12,12 @@ export class ImageListContainer extends BaseComponent { this.#info.initialize(); } } -} -export function initializeImageListContainer(element: HTMLElement) { - new ImageListContainer(element).initialize(); + static findAndInitialize() { + const imageListContainer = document.querySelector('#imagelist-container'); + + if (imageListContainer) { + new ImageListContainer(imageListContainer).initialize(); + } + } } diff --git a/src/content/components/listing/ImageListInfo.ts b/src/content/components/philomena/listing/ImageListInfo.ts similarity index 100% rename from src/content/components/listing/ImageListInfo.ts rename to src/content/components/philomena/listing/ImageListInfo.ts diff --git a/src/content/listing.ts b/src/content/listing.ts index 5f20635..21e240e 100644 --- a/src/content/listing.ts +++ b/src/content/listing.ts @@ -1,19 +1,18 @@ -import { createMaintenancePopup } from "$content/components/MaintenancePopup"; -import { createMediaBoxTools } from "$content/components/MediaBoxTools"; -import { calculateMediaBoxesPositions, initializeMediaBox } from "$content/components/MediaBoxWrapper"; -import { createMaintenanceStatusIcon } from "$content/components/MaintenanceStatusIcon"; -import { createImageShowFullscreenButton } from "$content/components/ImageShowFullscreenButton"; -import { initializeImageListContainer } from "$content/components/listing/ImageListContainer"; +import { TaggingProfilePopup } from "$content/components/extension/profiles/TaggingProfilePopup"; +import { MediaBoxTools } from "$content/components/extension/MediaBoxTools"; +import { MediaBox } from "$content/components/philomena/MediaBox"; +import { TaggingProfileStatusIcon } from "$content/components/extension/profiles/TaggingProfileStatusIcon"; +import { ImageShowFullscreenButton } from "$content/components/extension/ImageShowFullscreenButton"; +import { ImageListContainer } from "$content/components/philomena/listing/ImageListContainer"; -const mediaBoxes = document.querySelectorAll('.media-box'); -const imageListContainer = document.querySelector('#imagelist-container'); +const mediaBoxes = MediaBox.findElements(); mediaBoxes.forEach(mediaBoxElement => { - initializeMediaBox(mediaBoxElement, [ - createMediaBoxTools( - createMaintenancePopup(), - createMaintenanceStatusIcon(), - createImageShowFullscreenButton(), + MediaBox.initialize(mediaBoxElement, [ + MediaBoxTools.create( + TaggingProfilePopup.create(), + TaggingProfileStatusIcon.create(), + ImageShowFullscreenButton.create(), ) ]); @@ -23,8 +22,5 @@ mediaBoxes.forEach(mediaBoxElement => { }) }); -calculateMediaBoxesPositions(mediaBoxes); - -if (imageListContainer) { - initializeImageListContainer(imageListContainer); -} +MediaBox.initializePositionCalculation(mediaBoxes); +ImageListContainer.findAndInitialize(); diff --git a/src/content/posts.ts b/src/content/posts.ts index a331a58..1028377 100644 --- a/src/content/posts.ts +++ b/src/content/posts.ts @@ -1,3 +1,3 @@ -import { BlockCommunication } from "$content/components/BlockCommunication"; +import { BlockCommunication } from "$content/components/philomena/BlockCommunication"; BlockCommunication.findAndInitializeAll(); diff --git a/src/content/tags-editor.ts b/src/content/tags-editor.ts index e12559d..afda8a4 100644 --- a/src/content/tags-editor.ts +++ b/src/content/tags-editor.ts @@ -1,6 +1,6 @@ -import { TagsForm } from "$content/components/TagsForm"; -import { initializeAllTagsLists, watchForUpdatedTagLists } from "$content/components/TagsListBlock"; +import { TagsForm } from "$content/components/philomena/TagsForm"; +import { TagsListBlock } from "$content/components/philomena/TagsListBlock"; -initializeAllTagsLists(); -watchForUpdatedTagLists(); +TagsListBlock.initializeAll(); +TagsListBlock.watchUpdatedLists(); TagsForm.watchForEditors(); diff --git a/src/content/tags.ts b/src/content/tags.ts index b012167..3a8b992 100644 --- a/src/content/tags.ts +++ b/src/content/tags.ts @@ -1,7 +1,4 @@ -import { watchTagDropdownsInTagsEditor, wrapTagDropdown } from "$content/components/TagDropdownWrapper"; +import { TagDropdown } from "$content/components/philomena/TagDropdown"; -for (let tagDropdownElement of document.querySelectorAll('.tag.dropdown')) { - wrapTagDropdown(tagDropdownElement); -} - -watchTagDropdownsInTagsEditor(); +TagDropdown.findAllAndInitialize(); +TagDropdown.watch(); diff --git a/src/content/upload.ts b/src/content/upload.ts new file mode 100644 index 0000000..096430c --- /dev/null +++ b/src/content/upload.ts @@ -0,0 +1,3 @@ +import { TagsForm } from "$content/components/philomena/TagsForm"; + +TagsForm.initializeUploadEditor(); diff --git a/src/lib/dom-utils.ts b/src/lib/dom-utils.ts new file mode 100644 index 0000000..bfef56b --- /dev/null +++ b/src/lib/dom-utils.ts @@ -0,0 +1,11 @@ +/** + * Reusable function to create icons from FontAwesome. Usable only for website, since extension doesn't host its own + * copy of FA styles. Extension should use imports of SVGs inside CSS instead. + * @param iconSlug Slug of the icon to be added. + * @return Element with classes for FontAwesome icon added. + */ +export function createFontAwesomeIcon(iconSlug: string): HTMLElement { + const iconElement = document.createElement('i'); + iconElement.classList.add('fa-solid', `fa-${iconSlug}`); + return iconElement; +} diff --git a/src/lib/extension/BulkEntitiesTransporter.ts b/src/lib/extension/BulkEntitiesTransporter.ts index b83fa49..08ed77c 100644 --- a/src/lib/extension/BulkEntitiesTransporter.ts +++ b/src/lib/extension/BulkEntitiesTransporter.ts @@ -2,8 +2,9 @@ import type StorageEntity from "$lib/extension/base/StorageEntity"; import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from "lz-string"; import type { ImportableElementsList, ImportableEntityObject } from "$lib/extension/transporting/importables"; import EntitiesTransporter, { type SameSiteStatus } from "$lib/extension/EntitiesTransporter"; -import MaintenanceProfile from "$entities/MaintenanceProfile"; +import TaggingProfile from "$entities/TaggingProfile"; import TagGroup from "$entities/TagGroup"; +import TagEditorPreset from "$entities/TagEditorPreset"; type TransportersMapping = { [EntityName in keyof App.EntityNamesMap]: EntitiesTransporter; @@ -73,10 +74,12 @@ export default class BulkEntitiesTransporter { elements: entities .map(entity => { switch (true) { - case entity instanceof MaintenanceProfile: + case entity instanceof TaggingProfile: return BulkEntitiesTransporter.#transporters.profiles.exportToObject(entity); case entity instanceof TagGroup: return BulkEntitiesTransporter.#transporters.groups.exportToObject(entity); + case entity instanceof TagEditorPreset: + return BulkEntitiesTransporter.#transporters.presets.exportToObject(entity); } return null; @@ -99,8 +102,9 @@ export default class BulkEntitiesTransporter { } static #transporters: TransportersMapping = { - profiles: new EntitiesTransporter(MaintenanceProfile), + profiles: new EntitiesTransporter(TaggingProfile), groups: new EntitiesTransporter(TagGroup), + presets: new EntitiesTransporter(TagEditorPreset), } /** diff --git a/src/lib/extension/CustomCategoriesResolver.ts b/src/lib/extension/CustomCategoriesResolver.ts index 560cc77..74d67b7 100644 --- a/src/lib/extension/CustomCategoriesResolver.ts +++ b/src/lib/extension/CustomCategoriesResolver.ts @@ -1,4 +1,4 @@ -import type { TagDropdownWrapper } from "$content/components/TagDropdownWrapper"; +import type { TagDropdown } from "$content/components/philomena/TagDropdown"; import TagGroup from "$entities/TagGroup"; import { escapeRegExp } from "$lib/utils"; import { emit } from "$content/components/events/comms"; @@ -7,7 +7,7 @@ import { EVENT_TAG_GROUP_RESOLVED } from "$content/components/events/tag-dropdow export default class CustomCategoriesResolver { #exactGroupMatches = new Map(); #regExpGroupMatches = new Map(); - #tagDropdowns: TagDropdownWrapper[] = []; + #tagDropdowns: TagDropdown[] = []; #nextQueuedUpdate: Timeout | null = null; constructor() { @@ -15,7 +15,7 @@ export default class CustomCategoriesResolver { TagGroup.readAll().then(this.#onTagGroupsReceived.bind(this)); } - public addElement(tagDropdown: TagDropdownWrapper): void { + public addElement(tagDropdown: TagDropdown): void { this.#tagDropdowns.push(tagDropdown); if (!this.#exactGroupMatches.size && !this.#regExpGroupMatches.size) { @@ -49,7 +49,7 @@ export default class CustomCategoriesResolver { * @return {boolean} Will return false when tag is processed and true when it is not found. * @private */ - #applyCustomCategoryForExactMatches(tagDropdown: TagDropdownWrapper): boolean { + #applyCustomCategoryForExactMatches(tagDropdown: TagDropdown): boolean { const tagName = tagDropdown.tagName!; if (!this.#exactGroupMatches.has(tagName)) { @@ -65,7 +65,7 @@ export default class CustomCategoriesResolver { return false; } - #matchCustomCategoryByRegExp(tagDropdown: TagDropdownWrapper) { + #matchCustomCategoryByRegExp(tagDropdown: TagDropdown) { const tagName = tagDropdown.tagName!; for (const targetRegularExpression of this.#regExpGroupMatches.keys()) { @@ -117,7 +117,7 @@ export default class CustomCategoriesResolver { this.#queueUpdatingTags(); } - static #resetToOriginalCategory(tagDropdown: TagDropdownWrapper): void { + static #resetToOriginalCategory(tagDropdown: TagDropdown): void { emit( tagDropdown, EVENT_TAG_GROUP_RESOLVED, diff --git a/src/lib/extension/base/CacheablePreferences.ts b/src/lib/extension/base/CacheablePreferences.ts new file mode 100644 index 0000000..315a6f3 --- /dev/null +++ b/src/lib/extension/base/CacheablePreferences.ts @@ -0,0 +1,179 @@ +import ConfigurationController from "$lib/extension/ConfigurationController"; + +/** + * Initialization options for the preference field helper class. + */ +type PreferenceFieldOptions = { + /** + * Field name which will be read or updated. + */ + field: FieldKey; + /** + * Default value for this field. + */ + defaultValue: ValueType; +} + +/** + * Helper class for a field. Contains all information needed to read or set the values into the preferences while + * retaining proper types for the values. + */ +export class PreferenceField< + /** + * Mapping of keys to fields. Usually this is the same type used for defining the structure of the storage itself. + * Is automatically captured when preferences class instance is passed into the constructor. + */ + Fields extends Record = Record, + /** + * Field key for resolving which value will be resolved from getter or which value type should be passed into the + * setter method. + */ + Key extends keyof Fields = keyof Fields +> { + /** + * Instance of the preferences class to read/update values on. + * @private + */ + readonly #preferences: CacheablePreferences; + /** + * Key of a field we want to read or write with the helper class. + * @private + */ + readonly #fieldKey: Key; + /** + * Stored default value for a field. + * @private + */ + readonly #defaultValue: Fields[Key]; + + /** + * @param preferencesInstance Instance of preferences to work with. + * @param options Initialization options for this field. + */ + constructor(preferencesInstance: CacheablePreferences, options: PreferenceFieldOptions) { + this.#preferences = preferencesInstance; + this.#fieldKey = options.field; + this.#defaultValue = options.defaultValue; + } + + /** + * Read the field value from the preferences. + */ + get() { + return this.#preferences.readRaw(this.#fieldKey, this.#defaultValue); + } + + /** + * Update the preference field with provided value. + * @param value Value to update the field with. + */ + set(value: Fields[Key]) { + return this.#preferences.writeRaw(this.#fieldKey, value); + } +} + +/** + * Helper type for preference classes to enforce having field objects inside the preferences instance. It should be + * applied on child classes of {@link CacheablePreferences}. + */ +export type WithFields> = { + readonly [FieldKey in keyof FieldsType]: PreferenceField; +} + +/** + * Base class for any preferences instances. It contains methods for reading or updating any arbitrary values inside + * extension storage. It also tries to save the value resolved from the storage into special internal cache after the + * first call. + * + * Should be usually paired with implementation of {@link WithFields} helper type as interface for much more usable + * API. + */ +export default abstract class CacheablePreferences { + #controller: ConfigurationController; + #cachedValues: Map = new Map(); + #disposables: Function[] = []; + + /** + * @param settingsNamespace Name of the field inside the extension storage where these preferences stored. + * @protected + */ + protected constructor(settingsNamespace: string) { + this.#controller = new ConfigurationController(settingsNamespace); + + this.#disposables.push( + this.#controller.subscribeToChanges(settings => { + for (const key of Object.keys(settings)) { + this.#cachedValues.set( + key as keyof Fields, + settings[key] + ); + } + }) + ); + } + + /** + * Read the value from the preferences by the field. This function doesn't handle default values, so you generally + * should avoid using this method and accessing the special fields instead. + * @param settingName Name of the field to read. + * @param defaultValue Default value to return if value is not set. + * @return Value of the field or default value if it is not set. + */ + public async readRaw(settingName: Key, defaultValue: Fields[Key]): Promise { + if (this.#cachedValues.has(settingName)) { + return this.#cachedValues.get(settingName); + } + + const settingValue = await this.#controller.readSetting(settingName as string, defaultValue); + + this.#cachedValues.set(settingName, settingValue); + + return settingValue; + } + + /** + * Write the value into specific field of the storage. You should generally avoid calling this function directly and + * instead rely on special field helpers inside your preferences class. + * @param settingName Name of the setting to write. + * @param value Value to pass. + * @param force Ignore the cache and force the update. + * @protected + */ + async writeRaw(settingName: Key, value: Fields[Key], force: boolean = false): Promise { + if ( + !force + && this.#cachedValues.has(settingName) + && this.#cachedValues.get(settingName) === value + ) { + return; + } + + return this.#controller.writeSetting( + settingName as string, + value + ); + } + + /** + * Subscribe to the changes made to the storage. + * @param callback Callback which will receive list of settings on every update. This function will not be called + * on initialization. + * @return Unsubscribe function to call in order to disable the watching. + */ + subscribe(callback: (settings: Partial) => void): () => void { + const unsubscribeCallback = this.#controller.subscribeToChanges(callback as (fields: Record) => void); + + this.#disposables.push(unsubscribeCallback); + + return unsubscribeCallback; + } + + /** + * Completely disable all subscriptions. + */ + dispose() { + for (let disposeCallback of this.#disposables) { + disposeCallback(); + } + } +} diff --git a/src/lib/extension/base/CacheableSettings.ts b/src/lib/extension/base/CacheableSettings.ts deleted file mode 100644 index 23e508d..0000000 --- a/src/lib/extension/base/CacheableSettings.ts +++ /dev/null @@ -1,81 +0,0 @@ -import ConfigurationController from "$lib/extension/ConfigurationController"; - -export default class CacheableSettings { - #controller: ConfigurationController; - #cachedValues: Map = new Map(); - #disposables: Function[] = []; - - constructor(settingsNamespace: string) { - this.#controller = new ConfigurationController(settingsNamespace); - - this.#disposables.push( - this.#controller.subscribeToChanges(settings => { - for (const key of Object.keys(settings)) { - this.#cachedValues.set( - key as keyof Fields, - settings[key] - ); - } - }) - ); - } - - /** - * @template SettingType - * @param {string} settingName - * @param {SettingType} defaultValue - * @return {Promise} - * @protected - */ - protected async _resolveSetting(settingName: Key, defaultValue: Fields[Key]): Promise { - if (this.#cachedValues.has(settingName)) { - return this.#cachedValues.get(settingName); - } - - const settingValue = await this.#controller.readSetting(settingName as string, defaultValue); - - this.#cachedValues.set(settingName, settingValue); - - return settingValue; - } - - /** - * @param settingName Name of the setting to write. - * @param value Value to pass. - * @param force Ignore the cache and force the update. - * @protected - */ - async _writeSetting(settingName: Key, value: Fields[Key], force: boolean = false): Promise { - if ( - !force - && this.#cachedValues.has(settingName) - && this.#cachedValues.get(settingName) === value - ) { - return; - } - - return this.#controller.writeSetting( - settingName as string, - value - ); - } - - /** - * Subscribe to the changes made to the storage. - * @param {function(Object): void} callback Callback which will receive list of settings. - * @return {function(): void} Unsubscribe function. - */ - subscribe(callback: (settings: Partial) => void): () => void { - const unsubscribeCallback = this.#controller.subscribeToChanges(callback as (fields: Record) => void); - - this.#disposables.push(unsubscribeCallback); - - return unsubscribeCallback; - } - - dispose() { - for (let disposeCallback of this.#disposables) { - disposeCallback(); - } - } -} diff --git a/src/lib/extension/entities/TagEditorPreset.ts b/src/lib/extension/entities/TagEditorPreset.ts new file mode 100644 index 0000000..d904d5f --- /dev/null +++ b/src/lib/extension/entities/TagEditorPreset.ts @@ -0,0 +1,17 @@ +import StorageEntity from "$lib/extension/base/StorageEntity"; + +interface TagEditorPresetSettings { + name: string; + tags: string[]; +} + +export default class TagEditorPreset extends StorageEntity { + constructor(id: string, settings: Partial) { + super(id, { + name: settings.name || '', + tags: settings.tags || [], + }); + } + + public static readonly _entityName = 'presets'; +} diff --git a/src/lib/extension/entities/MaintenanceProfile.ts b/src/lib/extension/entities/TaggingProfile.ts similarity index 69% rename from src/lib/extension/entities/MaintenanceProfile.ts rename to src/lib/extension/entities/TaggingProfile.ts index fd2dc82..a202961 100644 --- a/src/lib/extension/entities/MaintenanceProfile.ts +++ b/src/lib/extension/entities/TaggingProfile.ts @@ -1,20 +1,20 @@ import StorageEntity from "$lib/extension/base/StorageEntity"; -export interface MaintenanceProfileSettings { +export interface TaggingProfileSettings { name: string; tags: string[]; temporary: boolean; } /** - * Class representing the maintenance profile entity. + * Class representing the tagging profile entity. */ -export default class MaintenanceProfile extends StorageEntity { +export default class TaggingProfile extends StorageEntity { /** * @param id ID of the entity. * @param settings Maintenance profile settings object. */ - constructor(id: string, settings: Partial) { + constructor(id: string, settings: Partial) { super(id, { name: settings.name || '', tags: settings.tags || [], diff --git a/src/lib/extension/preferences/MiscPreferences.ts b/src/lib/extension/preferences/MiscPreferences.ts new file mode 100644 index 0000000..0c14051 --- /dev/null +++ b/src/lib/extension/preferences/MiscPreferences.ts @@ -0,0 +1,27 @@ +import CacheablePreferences, { + PreferenceField, + type WithFields +} from "$lib/extension/base/CacheablePreferences"; + +export type FullscreenViewerSize = keyof App.ImageURIs; + +interface MiscPreferencesFields { + fullscreenViewer: boolean; + fullscreenViewerSize: FullscreenViewerSize; +} + +export default class MiscPreferences extends CacheablePreferences implements WithFields { + constructor() { + super("misc"); + } + + readonly fullscreenViewer = new PreferenceField(this, { + field: "fullscreenViewer", + defaultValue: true, + }); + + readonly fullscreenViewerSize = new PreferenceField(this, { + field: "fullscreenViewerSize", + defaultValue: "large", + }); +} diff --git a/src/lib/extension/preferences/TaggingProfilesPreferences.ts b/src/lib/extension/preferences/TaggingProfilesPreferences.ts new file mode 100644 index 0000000..c32eae7 --- /dev/null +++ b/src/lib/extension/preferences/TaggingProfilesPreferences.ts @@ -0,0 +1,40 @@ +import TaggingProfile from "$entities/TaggingProfile"; +import CacheablePreferences, { PreferenceField, type WithFields } from "$lib/extension/base/CacheablePreferences"; + +interface TaggingProfilePreferencesFields { + activeProfile: string | null; + stripBlacklistedTags: boolean; +} + +class ActiveProfilePreference extends PreferenceField { + constructor(preferencesInstance: CacheablePreferences) { + super(preferencesInstance, { + field: "activeProfile", + defaultValue: null, + }); + } + + async asObject(): Promise { + const activeProfileId = await this.get(); + + if (!activeProfileId) { + return null; + } + + return (await TaggingProfile.readAll()) + .find(profile => profile.id === activeProfileId) || null; + } +} + +export default class TaggingProfilesPreferences extends CacheablePreferences implements WithFields { + constructor() { + super("maintenance"); + } + + readonly activeProfile = new ActiveProfilePreference(this); + + readonly stripBlacklistedTags = new PreferenceField(this, { + field: "stripBlacklistedTags", + defaultValue: false, + }); +} diff --git a/src/lib/extension/preferences/TagsPreferences.ts b/src/lib/extension/preferences/TagsPreferences.ts new file mode 100644 index 0000000..06596bb --- /dev/null +++ b/src/lib/extension/preferences/TagsPreferences.ts @@ -0,0 +1,28 @@ +import CacheablePreferences, { PreferenceField, type WithFields } from "$lib/extension/base/CacheablePreferences"; + +interface TagsPreferencesFields { + groupSeparation: boolean; + replaceLinks: boolean; + replaceLinkText: boolean; +} + +export default class TagsPreferences extends CacheablePreferences implements WithFields { + constructor() { + super("tag"); + } + + readonly groupSeparation = new PreferenceField(this, { + field: "groupSeparation", + defaultValue: true, + }); + + readonly replaceLinks = new PreferenceField(this, { + field: "replaceLinks", + defaultValue: false, + }); + + readonly replaceLinkText = new PreferenceField(this, { + field: "replaceLinkText", + defaultValue: true, + }); +} diff --git a/src/lib/extension/settings/MaintenanceSettings.ts b/src/lib/extension/settings/MaintenanceSettings.ts deleted file mode 100644 index 065db65..0000000 --- a/src/lib/extension/settings/MaintenanceSettings.ts +++ /dev/null @@ -1,48 +0,0 @@ -import MaintenanceProfile from "$entities/MaintenanceProfile"; -import CacheableSettings from "$lib/extension/base/CacheableSettings"; - -interface MaintenanceSettingsFields { - activeProfile: string | null; - stripBlacklistedTags: boolean; -} - -export default class MaintenanceSettings extends CacheableSettings { - constructor() { - super("maintenance"); - } - - /** - * Set the active maintenance profile. - */ - async resolveActiveProfileId() { - return this._resolveSetting("activeProfile", null); - } - - /** - * Get the active maintenance profile if it is set. - */ - async resolveActiveProfileAsObject(): Promise { - const resolvedProfileId = await this.resolveActiveProfileId(); - - return (await MaintenanceProfile.readAll()) - .find(profile => profile.id === resolvedProfileId) || null; - } - - async resolveStripBlacklistedTags() { - return this._resolveSetting('stripBlacklistedTags', false); - } - - /** - * Set the active maintenance profile. - * - * @param profileId ID of the profile to set as active. If `null`, the active profile will be considered - * unset. - */ - async setActiveProfileId(profileId: string | null): Promise { - await this._writeSetting("activeProfile", profileId); - } - - async setStripBlacklistedTags(isEnabled: boolean) { - await this._writeSetting('stripBlacklistedTags', isEnabled); - } -} diff --git a/src/lib/extension/settings/MiscSettings.ts b/src/lib/extension/settings/MiscSettings.ts deleted file mode 100644 index c8b4508..0000000 --- a/src/lib/extension/settings/MiscSettings.ts +++ /dev/null @@ -1,30 +0,0 @@ -import CacheableSettings from "$lib/extension/base/CacheableSettings"; - -export type FullscreenViewerSize = keyof App.ImageURIs; - -interface MiscSettingsFields { - fullscreenViewer: boolean; - fullscreenViewerSize: FullscreenViewerSize; -} - -export default class MiscSettings extends CacheableSettings { - constructor() { - super("misc"); - } - - async resolveFullscreenViewerEnabled() { - return this._resolveSetting("fullscreenViewer", true); - } - - async resolveFullscreenViewerPreviewSize() { - return this._resolveSetting('fullscreenViewerSize', 'large'); - } - - async setFullscreenViewerEnabled(isEnabled: boolean) { - return this._writeSetting("fullscreenViewer", isEnabled); - } - - async setFullscreenViewerPreviewSize(size: FullscreenViewerSize | string) { - return this._writeSetting('fullscreenViewerSize', size as FullscreenViewerSize); - } -} diff --git a/src/lib/extension/settings/TagSettings.ts b/src/lib/extension/settings/TagSettings.ts deleted file mode 100644 index f657557..0000000 --- a/src/lib/extension/settings/TagSettings.ts +++ /dev/null @@ -1,37 +0,0 @@ -import CacheableSettings from "$lib/extension/base/CacheableSettings"; - -interface TagSettingsFields { - groupSeparation: boolean; - replaceLinks: boolean; - replaceLinkText: boolean; -} - -export default class TagSettings extends CacheableSettings { - constructor() { - super("tag"); - } - - async resolveGroupSeparation() { - return this._resolveSetting("groupSeparation", true); - } - - async resolveReplaceLinks() { - return this._resolveSetting("replaceLinks", false); - } - - async resolveReplaceLinkText() { - return this._resolveSetting("replaceLinkText", true); - } - - async setGroupSeparation(value: boolean) { - return this._writeSetting("groupSeparation", Boolean(value)); - } - - async setReplaceLinks(value: boolean) { - return this._writeSetting("replaceLinks", Boolean(value)); - } - - async setReplaceLinkText(value: boolean) { - return this._writeSetting("replaceLinkText", Boolean(value)); - } -} diff --git a/src/lib/extension/transporting/exporters.ts b/src/lib/extension/transporting/exporters.ts index a0aa6c7..bdd9d75 100644 --- a/src/lib/extension/transporting/exporters.ts +++ b/src/lib/extension/transporting/exporters.ts @@ -33,6 +33,16 @@ const entitiesExporters: ExportersMap = { category: entity.settings.category, separate: entity.settings.separate, } + }, + presets: entity => { + return { + $type: "presets", + $site: __CURRENT_SITE__, + v: 1, + id: entity.id, + name: entity.settings.name, + tags: entity.settings.tags, + } } }; diff --git a/src/lib/extension/transporting/validators.ts b/src/lib/extension/transporting/validators.ts index 2efec8b..6b3d13e 100644 --- a/src/lib/extension/transporting/validators.ts +++ b/src/lib/extension/transporting/validators.ts @@ -64,6 +64,19 @@ const entitiesValidators: EntitiesValidationMap = { throw new Error('Invalid group format detected!'); } }, + presets: importedObject => { + if (!importedObject.v || importedObject.v > 1) { + throw new Error('Unsupported preset version!'); + } + + if ( + !validateRequiredString(importedObject?.id) + || !validateRequiredString(importedObject?.name) + || !validateOptionalArray(importedObject?.tags) + ) { + throw new Error('Invalid preset format detected!'); + } + } }; /** diff --git a/src/lib/booru/scraped/ScrapedAPI.ts b/src/lib/philomena/scraping/ScrapedAPI.ts similarity index 96% rename from src/lib/booru/scraped/ScrapedAPI.ts rename to src/lib/philomena/scraping/ScrapedAPI.ts index 7bcb929..39cdf71 100644 --- a/src/lib/booru/scraped/ScrapedAPI.ts +++ b/src/lib/philomena/scraping/ScrapedAPI.ts @@ -1,4 +1,4 @@ -import PostParser from "$lib/booru/scraped/parsing/PostParser"; +import PostParser from "$lib/philomena/scraping/parsing/PostParser"; type UpdaterFunction = (tags: Set) => Set; diff --git a/src/lib/booru/scraped/parsing/PageParser.ts b/src/lib/philomena/scraping/parsing/PageParser.ts similarity index 100% rename from src/lib/booru/scraped/parsing/PageParser.ts rename to src/lib/philomena/scraping/parsing/PageParser.ts diff --git a/src/lib/booru/scraped/parsing/PostParser.ts b/src/lib/philomena/scraping/parsing/PostParser.ts similarity index 94% rename from src/lib/booru/scraped/parsing/PostParser.ts rename to src/lib/philomena/scraping/parsing/PostParser.ts index 43704b8..e7c6822 100644 --- a/src/lib/booru/scraped/parsing/PostParser.ts +++ b/src/lib/philomena/scraping/parsing/PostParser.ts @@ -1,5 +1,5 @@ -import PageParser from "$lib/booru/scraped/parsing/PageParser"; -import { buildTagsAndAliasesMap } from "$lib/booru/tag-utils"; +import PageParser from "$lib/philomena/scraping/parsing/PageParser"; +import { buildTagsAndAliasesMap } from "$lib/philomena/tag-utils"; export default class PostParser extends PageParser { #tagEditorForm: HTMLFormElement | null = null; diff --git a/src/lib/booru/search/QueryLexer.ts b/src/lib/philomena/search/QueryLexer.ts similarity index 100% rename from src/lib/booru/search/QueryLexer.ts rename to src/lib/philomena/search/QueryLexer.ts diff --git a/src/lib/booru/tag-utils.ts b/src/lib/philomena/tag-utils.ts similarity index 97% rename from src/lib/booru/tag-utils.ts rename to src/lib/philomena/tag-utils.ts index 228c7a3..0890a2c 100644 --- a/src/lib/booru/tag-utils.ts +++ b/src/lib/philomena/tag-utils.ts @@ -1,5 +1,5 @@ import { namespaceCategories } from "$config/tags"; -import { QueryLexer, QuotedTermToken, TermToken } from "$lib/booru/search/QueryLexer"; +import { QueryLexer, QuotedTermToken, TermToken } from "$lib/philomena/search/QueryLexer"; /** * Build the map containing both real tags and their aliases. diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 251fd43..e1c7d50 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,3 +1,6 @@ +import type StorageEntity from "$lib/extension/base/StorageEntity"; +import type TagGroup from "$entities/TagGroup"; + /** * Traverse and find the object using the key path. * @param targetObject Target object to traverse into. @@ -39,3 +42,14 @@ export function escapeRegExp(value: string): string { unsafeRegExpCharacters.lastIndex = 0; return value.replace(unsafeRegExpCharacters, "\\$&"); } + +type OnlyStringFields> = { + [FieldKey in keyof Fields as Fields[FieldKey] extends string ? FieldKey : never]: string; +}; + +export function sortEntitiesByField(entities: StorageEntity[], fieldName: keyof OnlyStringFields) { + return entities.toSorted( + (a, b) => (a.settings[fieldName] as string) + .localeCompare(b.settings[fieldName] as string) + ); +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 537f71e..a806a3a 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,31 +1,32 @@ {#if activeProfile} - + Active Profile: {activeProfile.settings.name}
{/if} - Tagging Profiles + Tagging Profiles Tag Groups + Tag Presets
Import/Export Preferences diff --git a/src/routes/features/+page.svelte b/src/routes/features/+page.svelte index 5ae405f..216b5d2 100644 --- a/src/routes/features/+page.svelte +++ b/src/routes/features/+page.svelte @@ -6,5 +6,5 @@ Back
- Tagging Profiles + Tagging Profiles
diff --git a/src/routes/features/presets/+page.svelte b/src/routes/features/presets/+page.svelte new file mode 100644 index 0000000..537caf1 --- /dev/null +++ b/src/routes/features/presets/+page.svelte @@ -0,0 +1,19 @@ + + + + Back + Create New + {#if presets.length} +
+ {#each presets as preset} + {preset.settings.name} + {/each} + {/if} +
diff --git a/src/routes/features/presets/[id]/+page.svelte b/src/routes/features/presets/[id]/+page.svelte new file mode 100644 index 0000000..cf9aa2d --- /dev/null +++ b/src/routes/features/presets/[id]/+page.svelte @@ -0,0 +1,42 @@ + + + + Back +
+
+{#if preset} + +{/if} + +
+ Edit Preset + Delete Preset +
diff --git a/src/routes/features/presets/[id]/delete/+page.svelte b/src/routes/features/presets/[id]/delete/+page.svelte new file mode 100644 index 0000000..f93430d --- /dev/null +++ b/src/routes/features/presets/[id]/delete/+page.svelte @@ -0,0 +1,49 @@ + + + + Back +
+
+{#if targetPreset} +

+ Do you want to remove preset "{targetPreset.settings.name}"? This action is irreversible. +

+ +
+ Yes + No +
+{:else} +

Loading...

+{/if} diff --git a/src/routes/features/presets/[id]/edit/+page.svelte b/src/routes/features/presets/[id]/edit/+page.svelte new file mode 100644 index 0000000..6a23ea2 --- /dev/null +++ b/src/routes/features/presets/[id]/edit/+page.svelte @@ -0,0 +1,74 @@ + + + + + Back + + + + + + + + + + + +
+ Save Preset +
diff --git a/src/routes/features/maintenance/+page.svelte b/src/routes/features/profiles/+page.svelte similarity index 58% rename from src/routes/features/maintenance/+page.svelte rename to src/routes/features/profiles/+page.svelte index 3c66f72..5841977 100644 --- a/src/routes/features/maintenance/+page.svelte +++ b/src/routes/features/profiles/+page.svelte @@ -2,45 +2,45 @@ import Menu from "$components/ui/menu/Menu.svelte"; import MenuItem from "$components/ui/menu/MenuItem.svelte"; import MenuRadioItem from "$components/ui/menu/MenuRadioItem.svelte"; - import { activeProfileStore, maintenanceProfiles } from "$stores/entities/maintenance-profiles"; - import MaintenanceProfile from "$entities/MaintenanceProfile"; + import { activeTaggingProfile, taggingProfiles } from "$stores/entities/tagging-profiles"; + import TaggingProfile from "$entities/TaggingProfile"; import { popupTitle } from "$stores/popup"; $popupTitle = 'Tagging Profiles'; - let profiles = $derived( - $maintenanceProfiles.sort((a, b) => a.settings.name.localeCompare(b.settings.name)) + let profiles = $derived( + $taggingProfiles.sort((a, b) => a.settings.name.localeCompare(b.settings.name)) ); function resetActiveProfile() { - $activeProfileStore = null; + $activeTaggingProfile = null; } function enableSelectedProfile(event: Event) { const target = event.target; if (target instanceof HTMLInputElement && target.checked) { - activeProfileStore.set(target.value); + activeTaggingProfile.set(target.value); } } Back - Create New + Create New {#if profiles.length}
{/if} {#each profiles as profile} - {profile.settings.name} {/each}
Reset Active Profile - Import Profile + Import Profile
diff --git a/src/routes/features/maintenance/[id]/+page.svelte b/src/routes/features/profiles/[id]/+page.svelte similarity index 53% rename from src/routes/features/maintenance/[id]/+page.svelte rename to src/routes/features/profiles/[id]/+page.svelte index f304bf4..1c2c09c 100644 --- a/src/routes/features/maintenance/[id]/+page.svelte +++ b/src/routes/features/profiles/[id]/+page.svelte @@ -3,26 +3,26 @@ import MenuItem from "$components/ui/menu/MenuItem.svelte"; import { page } from "$app/state"; import { goto } from "$app/navigation"; - import { activeProfileStore, maintenanceProfiles } from "$stores/entities/maintenance-profiles"; + import { activeTaggingProfile, taggingProfiles } from "$stores/entities/tagging-profiles"; import ProfileView from "$components/features/ProfileView.svelte"; import MenuCheckboxItem from "$components/ui/menu/MenuCheckboxItem.svelte"; - import MaintenanceProfile from "$entities/MaintenanceProfile"; + import TaggingProfile from "$entities/TaggingProfile"; import { popupTitle } from "$stores/popup"; let profileId = $derived(page.params.id); - let profile = $derived( - $maintenanceProfiles.find(profile => profile.id === profileId) || null + let profile = $derived( + $taggingProfiles.find(profile => profile.id === profileId) || null ); $effect(() => { if (profileId === 'new') { - goto('/features/maintenance/new/edit'); + goto('/features/profiles/new/edit'); return; } if (!profile) { console.warn(`Profile ${profileId} not found.`); - goto('/features/maintenance'); + goto('/features/profiles'); } else { $popupTitle = `Tagging Profile: ${profile.settings.name}`; } @@ -31,22 +31,22 @@ let isActiveProfile = $state(false); $effect.pre(() => { - isActiveProfile = $activeProfileStore === profileId; + isActiveProfile = $activeTaggingProfile === profileId; }); $effect(() => { - if (isActiveProfile && $activeProfileStore !== profileId) { - $activeProfileStore = profileId; + if (isActiveProfile && $activeTaggingProfile !== profileId) { + $activeTaggingProfile = profileId; } - if (!isActiveProfile && $activeProfileStore === profileId) { - $activeProfileStore = null; + if (!isActiveProfile && $activeTaggingProfile === profileId) { + $activeTaggingProfile = null; } }); - Back + Back
{#if profile} @@ -54,14 +54,14 @@ {/if}
- Edit Profile + Edit Profile Activate Profile - + Export Profile - + Delete Profile
diff --git a/src/routes/features/maintenance/[id]/delete/+page.svelte b/src/routes/features/profiles/[id]/delete/+page.svelte similarity index 64% rename from src/routes/features/maintenance/[id]/delete/+page.svelte rename to src/routes/features/profiles/[id]/delete/+page.svelte index b487d06..67fce00 100644 --- a/src/routes/features/maintenance/[id]/delete/+page.svelte +++ b/src/routes/features/profiles/[id]/delete/+page.svelte @@ -3,18 +3,18 @@ import Menu from "$components/ui/menu/Menu.svelte"; import MenuItem from "$components/ui/menu/MenuItem.svelte"; import { page } from "$app/state"; - import { maintenanceProfiles } from "$stores/entities/maintenance-profiles"; - import MaintenanceProfile from "$entities/MaintenanceProfile"; + import { taggingProfiles } from "$stores/entities/tagging-profiles"; + import TaggingProfile from "$entities/TaggingProfile"; import { popupTitle } from "$stores/popup"; const profileId = $derived(page.params.id); - const targetProfile = $derived( - $maintenanceProfiles.find(profile => profile.id === profileId) || null + const targetProfile = $derived( + $taggingProfiles.find(profile => profile.id === profileId) || null ); $effect(() => { if (!targetProfile) { - goto('/features/maintenance'); + goto('/features/profiles'); } else { $popupTitle = `Deleting Tagging Profile: ${targetProfile.settings.name}` } @@ -27,12 +27,12 @@ } await targetProfile.delete(); - await goto('/features/maintenance'); + await goto('/features/profiles'); } - Back + Back
{#if targetProfile} @@ -42,7 +42,7 @@
Yes - No + No
{:else}

Loading...

diff --git a/src/routes/features/maintenance/[id]/edit/+page.svelte b/src/routes/features/profiles/[id]/edit/+page.svelte similarity index 76% rename from src/routes/features/maintenance/[id]/edit/+page.svelte rename to src/routes/features/profiles/[id]/edit/+page.svelte index 504700b..63bff1c 100644 --- a/src/routes/features/maintenance/[id]/edit/+page.svelte +++ b/src/routes/features/profiles/[id]/edit/+page.svelte @@ -7,19 +7,19 @@ import FormContainer from "$components/ui/forms/FormContainer.svelte"; import { page } from "$app/state"; import { goto } from "$app/navigation"; - import { maintenanceProfiles } from "$stores/entities/maintenance-profiles"; - import MaintenanceProfile from "$entities/MaintenanceProfile"; + import { taggingProfiles } from "$stores/entities/tagging-profiles"; + import TaggingProfile from "$entities/TaggingProfile"; import { popupTitle } from "$stores/popup"; let profileId = $derived(page.params.id); - let targetProfile = $derived.by(() => { + let targetProfile = $derived.by(() => { if (profileId === 'new') { - return new MaintenanceProfile(crypto.randomUUID(), {}); + return new TaggingProfile(crypto.randomUUID(), {}); } - return $maintenanceProfiles.find(profile => profile.id === profileId) || null; + return $taggingProfiles.find(profile => profile.id === profileId) || null; }); let profileName = $state(''); @@ -32,7 +32,7 @@ } if (!targetProfile) { - goto('/features/maintenance'); + goto('/features/profiles'); return; } @@ -53,12 +53,12 @@ targetProfile.settings.temporary = false; await targetProfile.save(); - await goto('/features/maintenance/' + targetProfile.id); + await goto('/features/profiles/' + targetProfile.id); } - + Back
diff --git a/src/routes/features/maintenance/[id]/export/+page.svelte b/src/routes/features/profiles/[id]/export/+page.svelte similarity index 76% rename from src/routes/features/maintenance/[id]/export/+page.svelte rename to src/routes/features/profiles/[id]/export/+page.svelte index c85e236..db039f9 100644 --- a/src/routes/features/maintenance/[id]/export/+page.svelte +++ b/src/routes/features/profiles/[id]/export/+page.svelte @@ -1,31 +1,31 @@ - + Back
diff --git a/src/routes/features/maintenance/import/+page.svelte b/src/routes/features/profiles/import/+page.svelte similarity index 80% rename from src/routes/features/maintenance/import/+page.svelte rename to src/routes/features/profiles/import/+page.svelte index 19202fd..f1b9f12 100644 --- a/src/routes/features/maintenance/import/+page.svelte +++ b/src/routes/features/profiles/import/+page.svelte @@ -2,22 +2,22 @@ import Menu from "$components/ui/menu/Menu.svelte"; import MenuItem from "$components/ui/menu/MenuItem.svelte"; import FormContainer from "$components/ui/forms/FormContainer.svelte"; - import MaintenanceProfile from "$entities/MaintenanceProfile"; + import TaggingProfile from "$entities/TaggingProfile"; import FormControl from "$components/ui/forms/FormControl.svelte"; import ProfileView from "$components/features/ProfileView.svelte"; - import { maintenanceProfiles } from "$stores/entities/maintenance-profiles"; + import { taggingProfiles } from "$stores/entities/tagging-profiles"; import { goto } from "$app/navigation"; import EntitiesTransporter from "$lib/extension/EntitiesTransporter"; import { popupTitle } from "$stores/popup"; import Notice from "$components/ui/Notice.svelte"; - const profilesTransporter = new EntitiesTransporter(MaintenanceProfile); + const profilesTransporter = new EntitiesTransporter(TaggingProfile); let importedString = $state(''); let errorMessage = $state(''); - let candidateProfile = $state(null); - let existingProfile = $state(null); + let candidateProfile = $state(null); + let existingProfile = $state(null); $effect(() => { $popupTitle = candidateProfile @@ -49,7 +49,7 @@ } if (candidateProfile) { - existingProfile = $maintenanceProfiles.find(profile => profile.id === candidateProfile?.id) ?? null; + existingProfile = $taggingProfiles.find(profile => profile.id === candidateProfile?.id) ?? null; } } @@ -59,7 +59,7 @@ } candidateProfile.save().then(() => { - goto(`/features/maintenance`); + goto(`/features/profiles`); }); } @@ -68,16 +68,16 @@ return; } - const clonedProfile = new MaintenanceProfile(crypto.randomUUID(), candidateProfile.settings); + const clonedProfile = new TaggingProfile(crypto.randomUUID(), candidateProfile.settings); clonedProfile.settings.name += ` (Clone ${new Date().toISOString()})`; clonedProfile.save().then(() => { - goto(`/features/maintenance`); + goto(`/features/profiles`); }); } - Back + Back
{#if errorMessage} diff --git a/src/routes/preferences/tags/+page.svelte b/src/routes/preferences/tags/+page.svelte index a9e4df2..0187835 100644 --- a/src/routes/preferences/tags/+page.svelte +++ b/src/routes/preferences/tags/+page.svelte @@ -4,12 +4,12 @@ import FormControl from "$components/ui/forms/FormControl.svelte"; import Menu from "$components/ui/menu/Menu.svelte"; import MenuItem from "$components/ui/menu/MenuItem.svelte"; - import { stripBlacklistedTagsEnabled } from "$stores/preferences/maintenance"; + import { stripBlacklistedTagsEnabled } from "$stores/preferences/profiles"; import { shouldReplaceLinksOnForumPosts, shouldReplaceTextOfTagLinks, shouldSeparateTagGroups - } from "$stores/preferences/tag"; + } from "$stores/preferences/tags"; import { popupTitle } from "$stores/popup"; $popupTitle = 'Tagging Preferences'; diff --git a/src/routes/transporting/export/+page.svelte b/src/routes/transporting/export/+page.svelte index 438f124..3c3c119 100644 --- a/src/routes/transporting/export/+page.svelte +++ b/src/routes/transporting/export/+page.svelte @@ -1,7 +1,7 @@ @@ -202,7 +231,7 @@ previewedEntity = null} icon="arrow-left">Back to Selection
- {#if previewedEntity instanceof MaintenanceProfile} + {#if previewedEntity instanceof TaggingProfile} {:else if previewedEntity instanceof TagGroup} @@ -251,10 +280,7 @@ {/if} {#if importedGroups.length}
- + Import All Groups {#each importedGroups as candidateGroup} @@ -272,6 +298,26 @@ {/each} {/if} + {#if importedPresets.length} +
+ + Import All Presets + + {#each importedPresets as candidatePreset} + + {#if existingPresetsMap.has(candidatePreset.id)} + Update: + {:else} + New: + {/if} + {candidatePreset.settings.name || 'Unnamed Preset'} + + {/each} + {/if}
Imported Selected diff --git a/src/stores/entities/maintenance-profiles.ts b/src/stores/entities/maintenance-profiles.ts deleted file mode 100644 index e3f0271..0000000 --- a/src/stores/entities/maintenance-profiles.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { type Writable, writable } from "svelte/store"; -import MaintenanceProfile from "$entities/MaintenanceProfile"; -import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings"; - -/** - * Store for working with maintenance profiles in the Svelte popup. - */ -export const maintenanceProfiles: Writable = writable([]); - -/** - * Store for the active maintenance profile ID. - */ -export const activeProfileStore: Writable = writable(null); - -const maintenanceSettings = new MaintenanceSettings(); - -/** - * Active profile ID stored locally. Used to reset active profile once the existing profile was removed. - */ -let lastActiveProfileId: string|null = null; - -Promise.allSettled([ - // Read the initial values from the storages first - MaintenanceProfile.readAll().then(profiles => { - maintenanceProfiles.set(profiles); - }), - maintenanceSettings.resolveActiveProfileId().then(activeProfileId => { - activeProfileStore.set(activeProfileId); - }) -]).then(() => { - // And only after initial values are loaded, start watching for changes from storage and from user's interaction - MaintenanceProfile.subscribe(profiles => { - maintenanceProfiles.set(profiles); - }); - - maintenanceSettings.subscribe(settings => { - activeProfileStore.set(settings.activeProfile || null); - }); - - activeProfileStore.subscribe(profileId => { - lastActiveProfileId = profileId; - - void maintenanceSettings.setActiveProfileId(profileId); - }); - - // Watch the existence of the active profile on every change. - MaintenanceProfile.subscribe(profiles => { - if (!profiles.find(profile => profile.id === lastActiveProfileId)) { - activeProfileStore.set(null); - } - }); -}); diff --git a/src/stores/entities/tag-editor-presets.ts b/src/stores/entities/tag-editor-presets.ts new file mode 100644 index 0000000..079248c --- /dev/null +++ b/src/stores/entities/tag-editor-presets.ts @@ -0,0 +1,11 @@ +import { type Writable, writable } from "svelte/store"; +import TagEditorPreset from "$entities/TagEditorPreset"; + +export const tagEditorPresets: Writable = writable([]); + +TagEditorPreset + .readAll() + .then(presets => tagEditorPresets.set(presets)) + .then(() => { + TagEditorPreset.subscribe(presets => tagEditorPresets.set(presets)) + }); diff --git a/src/stores/entities/tagging-profiles.ts b/src/stores/entities/tagging-profiles.ts new file mode 100644 index 0000000..177268e --- /dev/null +++ b/src/stores/entities/tagging-profiles.ts @@ -0,0 +1,52 @@ +import { type Writable, writable } from "svelte/store"; +import TaggingProfile from "$entities/TaggingProfile"; +import TaggingProfilesPreferences from "$lib/extension/preferences/TaggingProfilesPreferences"; + +/** + * Store for working with maintenance profiles in the Svelte popup. + */ +export const taggingProfiles: Writable = writable([]); + +/** + * Store for the active maintenance profile ID. + */ +export const activeTaggingProfile: Writable = writable(null); + +const preferences = new TaggingProfilesPreferences(); + +/** + * Active profile ID stored locally. Used to reset active profile once the existing profile was removed. + */ +let lastActiveProfileId: string|null = null; + +Promise.allSettled([ + // Read the initial values from the storages first + TaggingProfile.readAll().then(profiles => { + taggingProfiles.set(profiles); + }), + preferences.activeProfile.get().then(activeProfileId => { + activeTaggingProfile.set(activeProfileId); + }) +]).then(() => { + // And only after initial values are loaded, start watching for changes from storage and from user's interaction + TaggingProfile.subscribe(profiles => { + taggingProfiles.set(profiles); + }); + + preferences.subscribe(settings => { + activeTaggingProfile.set(settings.activeProfile || null); + }); + + activeTaggingProfile.subscribe(profileId => { + lastActiveProfileId = profileId; + + void preferences.activeProfile.set(profileId); + }); + + // Watch the existence of the active profile on every change. + TaggingProfile.subscribe(profiles => { + if (!profiles.find(profile => profile.id === lastActiveProfileId)) { + activeTaggingProfile.set(null); + } + }); +}); diff --git a/src/stores/preferences/maintenance.ts b/src/stores/preferences/maintenance.ts deleted file mode 100644 index b230285..0000000 --- a/src/stores/preferences/maintenance.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { writable } from "svelte/store"; -import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings"; - -export const stripBlacklistedTagsEnabled = writable(true); - -const maintenanceSettings = new MaintenanceSettings(); - -Promise - .all([ - maintenanceSettings.resolveStripBlacklistedTags().then(v => stripBlacklistedTagsEnabled.set(v ?? true)) - ]) - .then(() => { - maintenanceSettings.subscribe(settings => { - stripBlacklistedTagsEnabled.set(typeof settings.stripBlacklistedTags === 'boolean' ? settings.stripBlacklistedTags : true); - }); - - stripBlacklistedTagsEnabled.subscribe(v => maintenanceSettings.setStripBlacklistedTags(v)); - }); diff --git a/src/stores/preferences/misc.ts b/src/stores/preferences/misc.ts index 0a912f3..121939a 100644 --- a/src/stores/preferences/misc.ts +++ b/src/stores/preferences/misc.ts @@ -1,18 +1,18 @@ import { writable } from "svelte/store"; -import MiscSettings from "$lib/extension/settings/MiscSettings"; +import MiscPreferences from "$lib/extension/preferences/MiscPreferences"; export const fullScreenViewerEnabled = writable(true); -const miscSettings = new MiscSettings(); +const preferences = new MiscPreferences(); Promise.allSettled([ - miscSettings.resolveFullscreenViewerEnabled().then(v => fullScreenViewerEnabled.set(v)) + preferences.fullscreenViewer.get().then(v => fullScreenViewerEnabled.set(v)) ]).then(() => { fullScreenViewerEnabled.subscribe(value => { - void miscSettings.setFullscreenViewerEnabled(value); + void preferences.fullscreenViewer.set(value); }); - miscSettings.subscribe(settings => { + preferences.subscribe(settings => { fullScreenViewerEnabled.set(Boolean(settings.fullscreenViewer)); }); }); diff --git a/src/stores/preferences/profiles.ts b/src/stores/preferences/profiles.ts new file mode 100644 index 0000000..f7a65fc --- /dev/null +++ b/src/stores/preferences/profiles.ts @@ -0,0 +1,18 @@ +import { writable } from "svelte/store"; +import TaggingProfilesPreferences from "$lib/extension/preferences/TaggingProfilesPreferences"; + +export const stripBlacklistedTagsEnabled = writable(true); + +const preferences = new TaggingProfilesPreferences(); + +Promise + .all([ + preferences.stripBlacklistedTags.get().then(v => stripBlacklistedTagsEnabled.set(v ?? true)) + ]) + .then(() => { + preferences.subscribe(settings => { + stripBlacklistedTagsEnabled.set(typeof settings.stripBlacklistedTags === 'boolean' ? settings.stripBlacklistedTags : true); + }); + + stripBlacklistedTagsEnabled.subscribe(v => preferences.stripBlacklistedTags.set(v)); + }); diff --git a/src/stores/preferences/tag.ts b/src/stores/preferences/tags.ts similarity index 54% rename from src/stores/preferences/tag.ts rename to src/stores/preferences/tags.ts index f491c5c..84b1239 100644 --- a/src/stores/preferences/tag.ts +++ b/src/stores/preferences/tags.ts @@ -1,7 +1,7 @@ import { writable } from "svelte/store"; -import TagSettings from "$lib/extension/settings/TagSettings"; +import TagsPreferences from "$lib/extension/preferences/TagsPreferences"; -const tagSettings = new TagSettings(); +const preferences = new TagsPreferences(); export const shouldSeparateTagGroups = writable(false); export const shouldReplaceLinksOnForumPosts = writable(false); @@ -9,24 +9,24 @@ export const shouldReplaceTextOfTagLinks = writable(true); Promise .allSettled([ - tagSettings.resolveGroupSeparation().then(value => shouldSeparateTagGroups.set(value)), - tagSettings.resolveReplaceLinks().then(value => shouldReplaceLinksOnForumPosts.set(value)), - tagSettings.resolveReplaceLinkText().then(value => shouldReplaceTextOfTagLinks.set(value)), + preferences.groupSeparation.get().then(value => shouldSeparateTagGroups.set(value)), + preferences.replaceLinks.get().then(value => shouldReplaceLinksOnForumPosts.set(value)), + preferences.replaceLinkText.get().then(value => shouldReplaceTextOfTagLinks.set(value)), ]) .then(() => { shouldSeparateTagGroups.subscribe(value => { - void tagSettings.setGroupSeparation(value); + void preferences.groupSeparation.set(value); }); shouldReplaceLinksOnForumPosts.subscribe(value => { - void tagSettings.setReplaceLinks(value); + void preferences.replaceLinks.set(value); }); shouldReplaceTextOfTagLinks.subscribe(value => { - void tagSettings.setReplaceLinkText(value); + void preferences.replaceLinkText.set(value); }); - tagSettings.subscribe(settings => { + preferences.subscribe(settings => { shouldSeparateTagGroups.set(Boolean(settings.groupSeparation)); shouldReplaceLinksOnForumPosts.set(Boolean(settings.replaceLinks)); shouldReplaceTextOfTagLinks.set(Boolean(settings.replaceLinkText)); diff --git a/src/styles/content/tag-presets.scss b/src/styles/content/tag-presets.scss new file mode 100644 index 0000000..cf24ef6 --- /dev/null +++ b/src/styles/content/tag-presets.scss @@ -0,0 +1,16 @@ +@use '$styles/booru-vars'; + +.block.tag-presets { + .tag { + cursor: pointer; + + &.is-missing { + opacity: 0.5; + } + + &:hover { + color: booru-vars.$resolved-tag-background; + background: booru-vars.$resolved-tag-color; + } + } +} diff --git a/static/icon.svg b/static/icon.svg deleted file mode 100644 index 1cf7596..0000000 --- a/static/icon.svg +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/static/icon128.png b/static/icon128.png index 050b1d8..a6441ae 100644 Binary files a/static/icon128.png and b/static/icon128.png differ diff --git a/static/icon16.png b/static/icon16.png index 63dd587..d92f9e7 100644 Binary files a/static/icon16.png and b/static/icon16.png differ diff --git a/static/icon256.png b/static/icon256.png index 5954b69..0d108fa 100644 Binary files a/static/icon256.png and b/static/icon256.png differ diff --git a/static/icon48.png b/static/icon48.png index 296725d..05909da 100644 Binary files a/static/icon48.png and b/static/icon48.png differ diff --git a/static/icons/derpibooru/icon128.png b/static/icons/derpibooru/icon128.png new file mode 100644 index 0000000..d8f8e24 Binary files /dev/null and b/static/icons/derpibooru/icon128.png differ diff --git a/static/icons/derpibooru/icon16.png b/static/icons/derpibooru/icon16.png new file mode 100644 index 0000000..f2d30be Binary files /dev/null and b/static/icons/derpibooru/icon16.png differ diff --git a/static/icons/derpibooru/icon256.png b/static/icons/derpibooru/icon256.png new file mode 100644 index 0000000..bcfdfc0 Binary files /dev/null and b/static/icons/derpibooru/icon256.png differ diff --git a/static/icons/derpibooru/icon48.png b/static/icons/derpibooru/icon48.png new file mode 100644 index 0000000..958c8fb Binary files /dev/null and b/static/icons/derpibooru/icon48.png differ diff --git a/static/icons/tantabus/icon128.png b/static/icons/tantabus/icon128.png new file mode 100644 index 0000000..489139d Binary files /dev/null and b/static/icons/tantabus/icon128.png differ diff --git a/static/icons/tantabus/icon16.png b/static/icons/tantabus/icon16.png new file mode 100644 index 0000000..1547eb8 Binary files /dev/null and b/static/icons/tantabus/icon16.png differ diff --git a/static/icons/tantabus/icon256.png b/static/icons/tantabus/icon256.png new file mode 100644 index 0000000..7fc0fb4 Binary files /dev/null and b/static/icons/tantabus/icon256.png differ diff --git a/static/icons/tantabus/icon48.png b/static/icons/tantabus/icon48.png new file mode 100644 index 0000000..295e002 Binary files /dev/null and b/static/icons/tantabus/icon48.png differ