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 @@
+
+
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 @@
+
+
+
+
+
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 @@