mirror of
https://github.com/koloml/furbooru-tagging-assistant.git
synced 2026-03-24 23:02:58 +00:00
Compare commits
51 Commits
0.5.3
...
6c2ef795b3
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c2ef795b3 | |||
| 58b620ef09 | |||
| 9445b1e862 | |||
| 9024883949 | |||
| dc29c6ca69 | |||
| 441091142c | |||
| 94733c9ff3 | |||
| d11cc2a9c5 | |||
| f4e30c60ad | |||
| 9031055ec9 | |||
| 8194a84ef7 | |||
| 2829ac022f | |||
| 5aac85dcaa | |||
| 9a14a5568d | |||
| a2ab0d4e7c | |||
| 5123b57320 | |||
| 2bdb789777 | |||
| 486ab9cafa | |||
| ba7b96d888 | |||
| b08937e47b | |||
| 1d332ea7d1 | |||
| 2920946015 | |||
| f879c45517 | |||
| 00083fdadb | |||
| 7ffee170c3 | |||
| db34b361b3 | |||
| bf81b7111f | |||
| dc79959b8f | |||
| dfdab180ee | |||
| b768f9072c | |||
| 72a731aaff | |||
| d181509d6f | |||
| 03b0763db4 | |||
| a7e0aefe6b | |||
| 687c12a8f4 | |||
| 9b7ba4a6e2 | |||
| 8d7b151911 | |||
| fccd79292d | |||
| 8041f2d2a1 | |||
| 3fac472ae0 | |||
| 44aca3120c | |||
| 3aee3defba | |||
| b7a9dc2a2b | |||
| 242dfc5972 | |||
| b6840996b6 | |||
| 4c5b796f1d | |||
| 7f2e06a1b1 | |||
| 31a33131cd | |||
| 7063459622 | |||
| 5a82b8751d | |||
| 9318bd51fa |
BIN
.github/assets/derpibooru-preview-of-tag-link-replacement.png
vendored
Normal file
BIN
.github/assets/derpibooru-preview-of-tag-link-replacement.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
8
.github/workflows/build-extensions.yml
vendored
8
.github/workflows/build-extensions.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
site: [furbooru, derpibooru]
|
||||
site: [furbooru, derpibooru, tantabus]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -27,10 +27,10 @@ jobs:
|
||||
|
||||
- name: Build extension for ${{ matrix.site }}
|
||||
run: |
|
||||
if [ "${{ matrix.site }}" = "derpibooru" ]; then
|
||||
npm run build:derpibooru
|
||||
else
|
||||
if [ "${{ matrix.site }}" = "furbooru" ]; then
|
||||
npm run build
|
||||
else
|
||||
npm run build:${{ matrix.site }}
|
||||
fi
|
||||
|
||||
- name: Create extension zip
|
||||
|
||||
@@ -174,6 +174,15 @@ export async function buildScriptsAndStyles(buildOptions) {
|
||||
}
|
||||
});
|
||||
|
||||
const tantabusSwapPlugin = SwapDefinedVariablesPlugin({
|
||||
envVariable: 'SITE',
|
||||
expectedValue: 'tantabus',
|
||||
define: {
|
||||
__CURRENT_SITE__: JSON.stringify('tantabus'),
|
||||
__CURRENT_SITE_NAME__: JSON.stringify('Tantabus'),
|
||||
}
|
||||
});
|
||||
|
||||
// Building all scripts together with AMD loader in mind
|
||||
await build({
|
||||
configFile: false,
|
||||
@@ -209,6 +218,7 @@ export async function buildScriptsAndStyles(buildOptions) {
|
||||
?.push(...dependencies);
|
||||
}),
|
||||
derpibooruSwapPlugin,
|
||||
tantabusSwapPlugin,
|
||||
],
|
||||
define: defineConstants,
|
||||
});
|
||||
@@ -235,6 +245,7 @@ export async function buildScriptsAndStyles(buildOptions) {
|
||||
wrapScriptIntoIIFE(),
|
||||
ScssViteReadEnvVariableFunctionPlugin(),
|
||||
derpibooruSwapPlugin,
|
||||
tantabusSwapPlugin,
|
||||
],
|
||||
define: defineConstants,
|
||||
});
|
||||
|
||||
@@ -67,13 +67,24 @@ export async function packExtension(settings) {
|
||||
return entry;
|
||||
})
|
||||
|
||||
if (process.env.SITE === 'derpibooru') {
|
||||
manifest.replaceHostTo([
|
||||
'derpibooru.org',
|
||||
'trixiebooru.org'
|
||||
]);
|
||||
manifest.replaceBooruNameWith('Derpibooru');
|
||||
manifest.setGeckoIdentifier('derpibooru-tagging-assistant@thecore.city');
|
||||
switch (process.env.SITE) {
|
||||
case 'derpibooru':
|
||||
manifest.replaceHostTo([
|
||||
'derpibooru.org',
|
||||
'trixiebooru.org'
|
||||
]);
|
||||
manifest.replaceBooruNameWith('Derpibooru');
|
||||
manifest.setGeckoIdentifier('derpibooru-tagging-assistant@thecore.city');
|
||||
break;
|
||||
|
||||
case 'tantabus':
|
||||
manifest.replaceHostTo('tantabus.ai');
|
||||
manifest.replaceBooruNameWith('Tantabus');
|
||||
manifest.setGeckoIdentifier('tantabus-tagging-assistant@thecore.city');
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn('No replacement set up for site: ' + process.env.SITE);
|
||||
}
|
||||
|
||||
manifest.passVersionFromPackage(path.resolve(settings.rootDir, 'package.json'));
|
||||
|
||||
15
README.md
15
README.md
@@ -1,8 +1,8 @@
|
||||
# Philomena Tagging Assistant
|
||||
|
||||
This is a browser extension written for the [Furbooru](https://furbooru.org) and [Derpibooru](https://derpibooru.org)
|
||||
image-boards. It gives you the ability to manually go over the list of images and apply tags to them without opening
|
||||
each individual image.
|
||||
This is a browser extension written for the [Furbooru](https://furbooru.org), [Derpibooru](https://derpibooru.org) and
|
||||
[Tantabus](https://tantabus.ai) image-boards. It gives you the ability to manually go over the list of images and apply
|
||||
tags to them without opening each individual image.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -59,14 +59,17 @@ npm install --save-dev
|
||||
Second, you need to run the `build` command. It will first build the popup using SvelteKit and then build all the
|
||||
content scripts/stylesheets and copy the manifest afterward.
|
||||
|
||||
Extension can currently be built for 2 different imageboards using one of the following commands:
|
||||
Extension can currently be built for multiple different imageboards using one of the following commands:
|
||||
|
||||
```shell
|
||||
# To build the extension for Furbooru, use:
|
||||
# Furbooru:
|
||||
npm run build
|
||||
|
||||
# To build the extension for Derpbooru, use:
|
||||
# Derpibooru:
|
||||
npm run build:derpibooru
|
||||
|
||||
# Tantabus:
|
||||
npm run build:tantabus
|
||||
```
|
||||
|
||||
When build is complete, extension files can be found in the `/build` directory. These files can be either used
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
{
|
||||
"name": "Furbooru Tagging Assistant",
|
||||
"description": "Small experimental extension for slightly quicker tagging experience. Furbooru Edition.",
|
||||
"version": "0.5.3",
|
||||
"version": "0.6.1",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "furbooru-tagging-assistant@thecore.city"
|
||||
"id": "furbooru-tagging-assistant@thecore.city",
|
||||
"data_collection_permissions": {
|
||||
"required": [
|
||||
"none"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"icons": {
|
||||
@@ -69,6 +74,19 @@
|
||||
"js": [
|
||||
"src/content/tags.ts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matches": [
|
||||
"*://*.furbooru.org/posts",
|
||||
"*://*.furbooru.org/posts?*",
|
||||
"*://*.furbooru.org/forums/*/topics/*"
|
||||
],
|
||||
"js": [
|
||||
"src/content/posts.ts"
|
||||
],
|
||||
"css": [
|
||||
"src/styles/content/posts.scss"
|
||||
]
|
||||
}
|
||||
],
|
||||
"action": {
|
||||
|
||||
540
package-lock.json
generated
540
package-lock.json
generated
@@ -1,70 +1,63 @@
|
||||
{
|
||||
"name": "furbooru-tagging-assistant",
|
||||
"version": "0.5.3",
|
||||
"version": "0.6.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "furbooru-tagging-assistant",
|
||||
"version": "0.5.3",
|
||||
"version": "0.6.1",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^7.1.0",
|
||||
"@fortawesome/fontawesome-free": "^7.2.0",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.49.4",
|
||||
"@sveltejs/kit": "^2.53.0",
|
||||
"amd-lite": "^1.0.1",
|
||||
"lz-string": "^1.5.0",
|
||||
"sass": "^1.97.2",
|
||||
"svelte": "^5.46.1"
|
||||
"sass": "^1.97.3",
|
||||
"svelte": "^5.53.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.3",
|
||||
"@types/chrome": "^0.1.32",
|
||||
"@types/node": "^25.0.3",
|
||||
"@vitest/coverage-v8": "^4.0.16",
|
||||
"cheerio": "^1.1.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@types/chrome": "^0.1.37",
|
||||
"@types/node": "^25.3.0",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"cheerio": "^1.2.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"jsdom": "^27.4.0",
|
||||
"svelte-check": "^4.3.5",
|
||||
"jsdom": "^28.1.0",
|
||||
"svelte-check": "^4.4.3",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vitest": "^4.0.16"
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@acemir/cssom": {
|
||||
"version": "0.9.30",
|
||||
"resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.30.tgz",
|
||||
"integrity": "sha512-9CnlMCI0LmCIq0olalQqdWrJHPzm0/tw3gzOA9zJSgvFX7Xau3D24mAGa4BtwxwY69nsuJW6kQqqCzf/mEcQgg==",
|
||||
"version": "0.9.31",
|
||||
"resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz",
|
||||
"integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz",
|
||||
"integrity": "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz",
|
||||
"integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/css-calc": "^2.1.4",
|
||||
"@csstools/css-color-parser": "^3.1.0",
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-tokenizer": "^3.0.4",
|
||||
"lru-cache": "^11.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
|
||||
"version": "11.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
|
||||
"integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"@csstools/css-calc": "^3.1.1",
|
||||
"@csstools/css-color-parser": "^4.0.2",
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.0",
|
||||
"lru-cache": "^11.2.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector": {
|
||||
"version": "6.7.6",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.6.tgz",
|
||||
"integrity": "sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==",
|
||||
"version": "6.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz",
|
||||
"integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -72,17 +65,7 @@
|
||||
"bidi-js": "^1.0.3",
|
||||
"css-tree": "^3.1.0",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"lru-cache": "^11.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": {
|
||||
"version": "11.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
|
||||
"integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
"lru-cache": "^11.2.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/nwsapi": {
|
||||
@@ -152,10 +135,23 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@bramus/specificity": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
|
||||
"integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-tree": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"specificity": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
||||
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
|
||||
"integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -169,13 +165,13 @@
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-calc": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
|
||||
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz",
|
||||
"integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -189,17 +185,17 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-color-parser": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
|
||||
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz",
|
||||
"integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -213,21 +209,21 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/color-helpers": "^5.1.0",
|
||||
"@csstools/css-calc": "^2.1.4"
|
||||
"@csstools/color-helpers": "^6.0.2",
|
||||
"@csstools/css-calc": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-parser-algorithms": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
|
||||
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
|
||||
"integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -241,16 +237,16 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-syntax-patches-for-csstree": {
|
||||
"version": "1.0.23",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.23.tgz",
|
||||
"integrity": "sha512-YEmgyklR6l/oKUltidNVYdjSmLSW88vMsKx0pmiS3r71s8ZZRpd8A0Yf0U+6p/RzElmMnPBv27hNWjDQMSZRtQ==",
|
||||
"version": "1.0.28",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.28.tgz",
|
||||
"integrity": "sha512-1NRf1CUBjnr3K7hu8BLxjQrKCxEe8FP/xmPTenAxCRZWVLbmGotkFvG9mfNpjA6k7Bw1bw4BilZq9cu19RA5pg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -262,15 +258,12 @@
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
"license": "MIT-0"
|
||||
},
|
||||
"node_modules/@csstools/css-tokenizer": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
|
||||
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
|
||||
"integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -284,7 +277,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@epic-web/invariant": {
|
||||
@@ -711,27 +704,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@exodus/bytes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.8.0.tgz",
|
||||
"integrity": "sha512-8JPn18Bcp8Uo1T82gR8lh2guEOa5KKU/IEKvvdp0sgmi7coPBWf1Doi1EXsGZb2ehc8ym/StJCjffYV+ne7sXQ==",
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.12.0.tgz",
|
||||
"integrity": "sha512-BuCOHA/EJdPN0qQ5MdgAiJSt9fYDHbghlgrj33gRdy/Yp1/FMCDhU6vJfcKrLC0TPWGSrfH3vYXBQWmFHxlddw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@exodus/crypto": "^1.0.0-rc.4"
|
||||
"@noble/hashes": "^1.8.0 || ^2.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@exodus/crypto": {
|
||||
"@noble/hashes": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-free": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.1.0.tgz",
|
||||
"integrity": "sha512-+WxNld5ZCJHvPQCr/GnzCTVREyStrAJjisUPtUxG5ngDA8TMlPnKp6dddlTpai4+1GNmltAeuk1hJEkBohwZYA==",
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.2.0.tgz",
|
||||
"integrity": "sha512-3DguDv/oUE+7vjMeTSOjCSG+KeawgVQOHrKRnvUuqYh1mfArrh7s+s8hXW3e4RerBA1+Wh+hBqf8sJNpqNrBWg==",
|
||||
"license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -1150,9 +1143,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/kit": {
|
||||
"version": "2.49.4",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.49.4.tgz",
|
||||
"integrity": "sha512-JFtOqDoU0DI/+QSG8qnq5bKcehVb3tCHhOG4amsSYth5/KgO4EkJvi42xSAiyKmXAAULW1/Zdb6lkgGEgSxdZg==",
|
||||
"version": "2.53.0",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.53.0.tgz",
|
||||
"integrity": "sha512-Brh/9h8QEg7rWIj+Nnz/2sC49NUeS8g3Qd9H5dTO3EbWG8vCEUl06jE+r5jQVDMHdr1swmCkwZkONFsWelGTpQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
@@ -1160,13 +1153,12 @@
|
||||
"@types/cookie": "^0.6.0",
|
||||
"acorn": "^8.14.1",
|
||||
"cookie": "^0.6.0",
|
||||
"devalue": "^5.3.2",
|
||||
"devalue": "^5.6.3",
|
||||
"esm-env": "^1.2.2",
|
||||
"kleur": "^4.1.5",
|
||||
"magic-string": "^0.30.5",
|
||||
"mrmime": "^2.0.0",
|
||||
"sade": "^1.8.1",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"set-cookie-parser": "^3.0.0",
|
||||
"sirv": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -1177,10 +1169,10 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0",
|
||||
"svelte": "^4.0.0 || ^5.0.0-next.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0"
|
||||
"vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
@@ -1192,9 +1184,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/vite-plugin-svelte": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.3.tgz",
|
||||
"integrity": "sha512-a+uxqQ9j6Lxmq4plbGaNdM9hgDCZyxAv/yvuyF5iWoA2H5icZkqD3rdK155ZQgFLX2lc3NvahHG4OgKpYqYPiQ==",
|
||||
"version": "6.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz",
|
||||
"integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
|
||||
@@ -1240,9 +1232,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chrome": {
|
||||
"version": "0.1.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.1.32.tgz",
|
||||
"integrity": "sha512-n5Cqlh7zyAqRLQWLXkeV5K/1BgDZdVcO/dJSTa8x+7w+sx7m73UrDmduAptg4KorMtyTW2TNnPu8RGeaDMKNGg==",
|
||||
"version": "0.1.37",
|
||||
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.1.37.tgz",
|
||||
"integrity": "sha512-IJE4ceuDO7lrEuua7Pow47zwNcI8E6qqkowRP7aFPaZ0lrjxh6y836OPqqkIZeTX64FTogbw+4RNH0+QrweCTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1286,28 +1278,33 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
|
||||
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
|
||||
"version": "25.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz",
|
||||
"integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.16.tgz",
|
||||
"integrity": "sha512-2rNdjEIsPRzsdu6/9Eq0AYAzYdpP6Bx9cje9tL3FE5XzXRQF1fNU9pe/1yE8fCrS0HD+fBtt6gLPh6LI57tX7A==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@bcoe/v8-coverage": "^1.0.2",
|
||||
"@vitest/utils": "4.0.16",
|
||||
"ast-v8-to-istanbul": "^0.3.8",
|
||||
"@vitest/utils": "4.0.18",
|
||||
"ast-v8-to-istanbul": "^0.3.10",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-lib-source-maps": "^5.0.6",
|
||||
"istanbul-reports": "^3.2.0",
|
||||
"magicast": "^0.5.1",
|
||||
"obug": "^2.1.1",
|
||||
@@ -1318,8 +1315,8 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vitest/browser": "4.0.16",
|
||||
"vitest": "4.0.16"
|
||||
"@vitest/browser": "4.0.18",
|
||||
"vitest": "4.0.18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vitest/browser": {
|
||||
@@ -1328,16 +1325,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz",
|
||||
"integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==",
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
|
||||
"integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/spy": "4.0.16",
|
||||
"@vitest/utils": "4.0.16",
|
||||
"@vitest/spy": "4.0.18",
|
||||
"@vitest/utils": "4.0.18",
|
||||
"chai": "^6.2.1",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
@@ -1346,13 +1343,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz",
|
||||
"integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==",
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz",
|
||||
"integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "4.0.16",
|
||||
"@vitest/spy": "4.0.18",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.21"
|
||||
},
|
||||
@@ -1373,9 +1370,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.16.tgz",
|
||||
"integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==",
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz",
|
||||
"integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1386,13 +1383,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.16.tgz",
|
||||
"integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==",
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz",
|
||||
"integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.0.16",
|
||||
"@vitest/utils": "4.0.18",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
@@ -1400,13 +1397,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.16.tgz",
|
||||
"integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==",
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz",
|
||||
"integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.0.16",
|
||||
"@vitest/pretty-format": "4.0.18",
|
||||
"magic-string": "^0.30.21",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
@@ -1415,9 +1412,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.16.tgz",
|
||||
"integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==",
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz",
|
||||
"integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -1425,13 +1422,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz",
|
||||
"integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==",
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz",
|
||||
"integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.0.16",
|
||||
"@vitest/pretty-format": "4.0.18",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
@@ -1466,9 +1463,10 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aria-query": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
||||
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
|
||||
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
@@ -1540,9 +1538,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz",
|
||||
"integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz",
|
||||
"integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1551,11 +1549,11 @@
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.2.2",
|
||||
"encoding-sniffer": "^0.2.1",
|
||||
"htmlparser2": "^10.0.0",
|
||||
"htmlparser2": "^10.1.0",
|
||||
"parse5": "^7.3.0",
|
||||
"parse5-htmlparser2-tree-adapter": "^7.1.0",
|
||||
"parse5-parser-stream": "^7.1.2",
|
||||
"undici": "^7.12.0",
|
||||
"undici": "^7.19.0",
|
||||
"whatwg-mimetype": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1684,41 +1682,41 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cssstyle": {
|
||||
"version": "5.3.7",
|
||||
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz",
|
||||
"integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==",
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.1.0.tgz",
|
||||
"integrity": "sha512-Ml4fP2UT2K3CUBQnVlbdV/8aFDdlY69E+YnwJM+3VUWl08S3J8c8aRuJqCkD9Py8DHZ7zNNvsfKl8psocHZEFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^4.1.1",
|
||||
"@csstools/css-syntax-patches-for-csstree": "^1.0.21",
|
||||
"@asamuzakjp/css-color": "^5.0.0",
|
||||
"@csstools/css-syntax-patches-for-csstree": "^1.0.28",
|
||||
"css-tree": "^3.1.0",
|
||||
"lru-cache": "^11.2.4"
|
||||
"lru-cache": "^11.2.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/cssstyle/node_modules/lru-cache": {
|
||||
"version": "11.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
|
||||
"integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz",
|
||||
"integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
|
||||
"integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^15.0.0"
|
||||
"whatwg-mimetype": "^5.0.0",
|
||||
"whatwg-url": "^16.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls/node_modules/whatwg-mimetype": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
|
||||
"integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
@@ -1766,9 +1764,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/devalue": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.1.tgz",
|
||||
"integrity": "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==",
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz",
|
||||
"integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
@@ -1903,9 +1901,9 @@
|
||||
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="
|
||||
},
|
||||
"node_modules/esrap": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.1.tgz",
|
||||
"integrity": "sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==",
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz",
|
||||
"integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||
@@ -1987,9 +1985,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz",
|
||||
"integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==",
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
|
||||
"integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||
@@ -2002,14 +2000,14 @@
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.2.1",
|
||||
"entities": "^6.0.0"
|
||||
"domutils": "^3.2.2",
|
||||
"entities": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/htmlparser2/node_modules/entities": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
@@ -2136,21 +2134,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-source-maps": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
|
||||
"integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.23",
|
||||
"debug": "^4.1.1",
|
||||
"istanbul-lib-coverage": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-reports": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
|
||||
@@ -2173,17 +2156,18 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "27.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz",
|
||||
"integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==",
|
||||
"version": "28.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz",
|
||||
"integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@acemir/cssom": "^0.9.28",
|
||||
"@asamuzakjp/dom-selector": "^6.7.6",
|
||||
"@exodus/bytes": "^1.6.0",
|
||||
"cssstyle": "^5.3.4",
|
||||
"data-urls": "^6.0.0",
|
||||
"@acemir/cssom": "^0.9.31",
|
||||
"@asamuzakjp/dom-selector": "^6.8.1",
|
||||
"@bramus/specificity": "^2.4.2",
|
||||
"@exodus/bytes": "^1.11.0",
|
||||
"cssstyle": "^6.0.1",
|
||||
"data-urls": "^7.0.0",
|
||||
"decimal.js": "^10.6.0",
|
||||
"html-encoding-sniffer": "^6.0.0",
|
||||
"http-proxy-agent": "^7.0.2",
|
||||
@@ -2193,11 +2177,11 @@
|
||||
"saxes": "^6.0.0",
|
||||
"symbol-tree": "^3.2.4",
|
||||
"tough-cookie": "^6.0.0",
|
||||
"undici": "^7.21.0",
|
||||
"w3c-xmlserializer": "^5.0.0",
|
||||
"webidl-conversions": "^8.0.0",
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^15.1.0",
|
||||
"ws": "^8.18.3",
|
||||
"webidl-conversions": "^8.0.1",
|
||||
"whatwg-mimetype": "^5.0.0",
|
||||
"whatwg-url": "^16.0.0",
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2238,6 +2222,16 @@
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/whatwg-mimetype": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
|
||||
"integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/kleur": {
|
||||
"version": "4.1.5",
|
||||
"license": "MIT",
|
||||
@@ -2249,6 +2243,16 @@
|
||||
"version": "3.0.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.2.6",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
|
||||
"integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/lz-string": {
|
||||
"version": "1.5.0",
|
||||
"license": "MIT",
|
||||
@@ -2314,6 +2318,7 @@
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
@@ -2559,6 +2564,7 @@
|
||||
},
|
||||
"node_modules/sade": {
|
||||
"version": "1.8.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mri": "^1.1.0"
|
||||
@@ -2573,9 +2579,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.97.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.97.2.tgz",
|
||||
"integrity": "sha512-y5LWb0IlbO4e97Zr7c3mlpabcbBtS+ieiZ9iwDooShpFKWXf62zz5pEPdwrLYm+Bxn1fnbwFGzHuCLSA9tBmrw==",
|
||||
"version": "1.97.3",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz",
|
||||
"integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
@@ -2619,7 +2625,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.6.0",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.0.1.tgz",
|
||||
"integrity": "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
@@ -2699,22 +2707,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "5.46.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.46.1.tgz",
|
||||
"integrity": "sha512-ynjfCHD3nP2el70kN5Pmg37sSi0EjOm9FgHYQdC4giWG/hzO3AatzXXJJgP305uIhGQxSufJLuYWtkY8uK/8RA==",
|
||||
"version": "5.53.3",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.3.tgz",
|
||||
"integrity": "sha512-pRUBr6j6uQDgBi208gHnGRMykw0Rf2Yr1HmLyRucsvcaYgIUxswJkT93WZJflsmezu5s8Lq+q78EoyLv2yaFCg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@sveltejs/acorn-typescript": "^1.0.5",
|
||||
"@types/estree": "^1.0.5",
|
||||
"@types/trusted-types": "^2.0.7",
|
||||
"acorn": "^8.12.1",
|
||||
"aria-query": "^5.3.1",
|
||||
"aria-query": "5.3.1",
|
||||
"axobject-query": "^4.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"devalue": "^5.5.0",
|
||||
"devalue": "^5.6.3",
|
||||
"esm-env": "^1.2.1",
|
||||
"esrap": "^2.2.1",
|
||||
"esrap": "^2.2.2",
|
||||
"is-reference": "^3.0.3",
|
||||
"locate-character": "^3.0.0",
|
||||
"magic-string": "^0.30.11",
|
||||
@@ -2725,9 +2734,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-check": {
|
||||
"version": "4.3.5",
|
||||
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.5.tgz",
|
||||
"integrity": "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q==",
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.3.tgz",
|
||||
"integrity": "sha512-4HtdEv2hOoLCEsSXI+RDELk9okP/4sImWa7X02OjMFFOWeSdFF3NFy3vqpw0z+eH9C88J9vxZfUXz/Uv2A1ANw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2934,9 +2943,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.13.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.13.0.tgz",
|
||||
"integrity": "sha512-l+zSMssRqrzDcb3fjMkjjLGmuiiK2pMIcV++mJaAc9vhjSGpvM7h43QgP+OAMb1GImHmbPyG2tBXeuyG5iY4gA==",
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.21.0.tgz",
|
||||
"integrity": "sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2944,9 +2953,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -3073,19 +3082,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz",
|
||||
"integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==",
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
|
||||
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.16",
|
||||
"@vitest/mocker": "4.0.16",
|
||||
"@vitest/pretty-format": "4.0.16",
|
||||
"@vitest/runner": "4.0.16",
|
||||
"@vitest/snapshot": "4.0.16",
|
||||
"@vitest/spy": "4.0.16",
|
||||
"@vitest/utils": "4.0.16",
|
||||
"@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",
|
||||
"magic-string": "^0.30.21",
|
||||
@@ -3113,10 +3122,10 @@
|
||||
"@edge-runtime/vm": "*",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||
"@vitest/browser-playwright": "4.0.16",
|
||||
"@vitest/browser-preview": "4.0.16",
|
||||
"@vitest/browser-webdriverio": "4.0.16",
|
||||
"@vitest/ui": "4.0.16",
|
||||
"@vitest/browser-playwright": "4.0.18",
|
||||
"@vitest/browser-preview": "4.0.18",
|
||||
"@vitest/browser-webdriverio": "4.0.18",
|
||||
"@vitest/ui": "4.0.18",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
@@ -3177,9 +3186,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz",
|
||||
"integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==",
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
|
||||
"integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
@@ -3206,17 +3215,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "15.1.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz",
|
||||
"integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==",
|
||||
"version": "16.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.0.tgz",
|
||||
"integrity": "sha512-9CcxtEKsf53UFwkSUZjG+9vydAsFO4lFHBpJUtjBcoJOCJpKnSJNwCw813zrYJHpCJ7sgfbtOe0V5Ku7Pa1XMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@exodus/bytes": "^1.11.0",
|
||||
"tr46": "^6.0.0",
|
||||
"webidl-conversions": "^8.0.0"
|
||||
"webidl-conversions": "^8.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
@@ -3252,28 +3262,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml-name-validator": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||
|
||||
39
package.json
39
package.json
@@ -1,10 +1,12 @@
|
||||
{
|
||||
"name": "furbooru-tagging-assistant",
|
||||
"version": "0.5.3",
|
||||
"version": "0.6.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "npm run build:popup && npm run build:extension",
|
||||
"build:derpibooru": "cross-env SITE=derpibooru npm run build",
|
||||
"build:tantabus": "cross-env SITE=tantabus npm run build",
|
||||
"build:popup": "vite build",
|
||||
"build:extension": "node build-extension.js",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
@@ -12,27 +14,26 @@
|
||||
"test": "vitest run --coverage",
|
||||
"test:watch": "vitest watch --coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.3",
|
||||
"@types/chrome": "^0.1.32",
|
||||
"@types/node": "^25.0.3",
|
||||
"@vitest/coverage-v8": "^4.0.16",
|
||||
"cheerio": "^1.1.2",
|
||||
"cross-env": "^10.1.0",
|
||||
"jsdom": "^27.4.0",
|
||||
"svelte-check": "^4.3.5",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vitest": "^4.0.16"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.49.4",
|
||||
"@fortawesome/fontawesome-free": "^7.1.0",
|
||||
"@sveltejs/kit": "^2.53.0",
|
||||
"@fortawesome/fontawesome-free": "^7.2.0",
|
||||
"amd-lite": "^1.0.1",
|
||||
"lz-string": "^1.5.0",
|
||||
"sass": "^1.97.2",
|
||||
"svelte": "^5.46.1"
|
||||
"sass": "^1.97.3",
|
||||
"svelte": "^5.53.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@types/chrome": "^0.1.37",
|
||||
"@types/node": "^25.3.0",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"cheerio": "^1.2.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"jsdom": "^28.1.0",
|
||||
"svelte-check": "^4.4.3",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
}
|
||||
|
||||
4
src/app.d.ts
vendored
4
src/app.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import type TagGroup from "$entities/TagGroup";
|
||||
|
||||
declare global {
|
||||
@@ -37,7 +37,7 @@ declare global {
|
||||
);
|
||||
|
||||
interface EntityNamesMap {
|
||||
profiles: MaintenanceProfile;
|
||||
profiles: TaggingProfile;
|
||||
groups: TagGroup;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import type TaggingProfile from "$entities/TaggingProfile";
|
||||
|
||||
interface ProfileViewProps {
|
||||
profile: MaintenanceProfile;
|
||||
profile: TaggingProfile;
|
||||
}
|
||||
|
||||
let { profile }: ProfileViewProps = $props();
|
||||
|
||||
32
src/components/ui/Notice.svelte
Normal file
32
src/components/ui/Notice.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
interface MessageProps {
|
||||
children?: import('svelte').Snippet;
|
||||
level: 'warning' | 'error';
|
||||
}
|
||||
|
||||
let { children, level }: MessageProps = $props();
|
||||
</script>
|
||||
|
||||
<p class="{level}">
|
||||
{@render children?.()}
|
||||
</p>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$styles/colors';
|
||||
|
||||
p {
|
||||
padding: 5px 24px;
|
||||
margin: {
|
||||
left: -24px;
|
||||
right: -24px;
|
||||
}
|
||||
}
|
||||
|
||||
.warning {
|
||||
background: colors.$warning-background;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: colors.$error-background;
|
||||
}
|
||||
</style>
|
||||
@@ -14,6 +14,33 @@ export const categories: string[] = [
|
||||
'body-type',
|
||||
];
|
||||
|
||||
/**
|
||||
* Mapping of namespaces to their respective categories. These namespaces are automatically assigned to them, so we can
|
||||
* automatically assume categories of tags which start with them. Mapping is extracted from Philomena directly.
|
||||
*
|
||||
* This mapping may differ between boorus.
|
||||
*
|
||||
* @see https://github.com/philomena-dev/philomena/blob/6086757b654da8792ae52adb2a2f501ea6c30d12/lib/philomena/tags/tag.ex#L33-L45
|
||||
*/
|
||||
export const namespaceCategories: Map<string, string> = new Map([
|
||||
['artist', 'origin'],
|
||||
['art pack', 'content-fanmade'],
|
||||
['colorist', 'origin'],
|
||||
['comic', 'content-fanmade'],
|
||||
['editor', 'origin'],
|
||||
['fanfic', 'content-fanmade'],
|
||||
['oc', 'oc'],
|
||||
['photographer', 'origin'],
|
||||
['series', 'content-fanmade'],
|
||||
['spoiler', 'spoiler'],
|
||||
['video', 'content-fanmade'],
|
||||
...(__CURRENT_SITE__ === 'tantabus' ? <const> [
|
||||
["prompter", "origin"],
|
||||
["creator", "origin"],
|
||||
["generator", "origin"]
|
||||
] : [])
|
||||
]);
|
||||
|
||||
/**
|
||||
* List of tags which marked by the site as blacklisted. These tags are blocked from being added by the tag editor and
|
||||
* should usually just be removed automatically.
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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<string, string> | null;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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<HTMLElement>('.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<MaintenanceProfile | null>) {
|
||||
#onActiveProfileChanged(profileChangedEvent: CustomEvent<TaggingProfile | null>) {
|
||||
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;
|
||||
}
|
||||
@@ -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,7 +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 type { MediaBoxTools } from "$content/components/extension/MediaBoxTools";
|
||||
import { resolveTagCategoryFromTagName } from "$lib/philomena/tag-utils";
|
||||
|
||||
class BlackListedTagsEncounteredError extends Error {
|
||||
constructor(tagName: string) {
|
||||
@@ -20,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<string, HTMLElement> = new Map();
|
||||
#activeProfile: MaintenanceProfile | null = null;
|
||||
#activeProfile: TaggingProfile | null = null;
|
||||
#mediaBoxTools: MediaBoxTools | null = null;
|
||||
#tagsToRemove: Set<string> = new Set();
|
||||
#tagsToAdd: Set<string> = new Set();
|
||||
@@ -65,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;
|
||||
@@ -78,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();
|
||||
@@ -109,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);
|
||||
|
||||
@@ -121,8 +122,13 @@ 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.#markTagAsInvalid(tagElement);
|
||||
TaggingProfilePopup.#markTagElementWithCategory(tagElement, 'error');
|
||||
this.#suggestedInvalidTags.set(tagName, tagElement);
|
||||
} else {
|
||||
TaggingProfilePopup.#markTagElementWithCategory(
|
||||
tagElement,
|
||||
resolveTagCategoryFromTagName(tagName) ?? '',
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -173,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;
|
||||
@@ -191,7 +197,7 @@ export class MaintenancePopup extends BaseComponent {
|
||||
if (this.#isPlanningToSubmit && !this.#isSubmitting) {
|
||||
this.#tagsSubmissionTimer = setTimeout(
|
||||
this.#onSubmissionTimerPassed.bind(this),
|
||||
MaintenancePopup.#delayBeforeSubmissionMs
|
||||
TaggingProfilePopup.#delayBeforeSubmissionMs
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -208,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) {
|
||||
@@ -244,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;
|
||||
@@ -262,7 +268,7 @@ export class MaintenancePopup extends BaseComponent {
|
||||
this.#tagsToRemove.clear();
|
||||
|
||||
this.#refreshTagsList();
|
||||
MaintenancePopup.#notifyAboutPendingSubmission(false);
|
||||
TaggingProfilePopup.#notifyAboutPendingSubmission(false);
|
||||
|
||||
this.#isSubmitting = false;
|
||||
}
|
||||
@@ -286,8 +292,8 @@ export class MaintenancePopup extends BaseComponent {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tagElement = MaintenancePopup.#buildTagElement(tagName);
|
||||
MaintenancePopup.#markTagAsInvalid(tagElement);
|
||||
const tagElement = TaggingProfilePopup.#buildTagElement(tagName);
|
||||
TaggingProfilePopup.#markTagElementWithCategory(tagElement, 'error');
|
||||
tagElement.classList.add('is-present');
|
||||
|
||||
this.#suggestedInvalidTags.set(tagName, tagElement);
|
||||
@@ -305,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');
|
||||
@@ -315,18 +329,19 @@ export class MaintenancePopup extends BaseComponent {
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the tag with red color.
|
||||
* Mark the tag element with specified category.
|
||||
* @param tagElement Element to mark.
|
||||
* @param category Code name of category to mark.
|
||||
*/
|
||||
static #markTagAsInvalid(tagElement: HTMLElement) {
|
||||
tagElement.dataset.tagCategory = 'error';
|
||||
tagElement.setAttribute('data-tag-category', 'error');
|
||||
static #markTagElementWithCategory(tagElement: HTMLElement, category: string) {
|
||||
tagElement.dataset.tagCategory = category;
|
||||
tagElement.setAttribute('data-tag-category', category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -334,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
|
||||
@@ -345,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;
|
||||
@@ -409,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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
133
src/content/components/philomena/BlockCommunication.ts
Normal file
133
src/content/components/philomena/BlockCommunication.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { BaseComponent } from "$content/components/base/BaseComponent";
|
||||
import TagsPreferences from "$lib/extension/preferences/TagsPreferences";
|
||||
import { getComponent } from "$content/components/base/component-utils";
|
||||
import { resolveTagNameFromLink, resolveTagCategoryFromTagName } from "$lib/philomena/tag-utils";
|
||||
|
||||
export class BlockCommunication extends BaseComponent {
|
||||
#contentSection: HTMLElement | null = null;
|
||||
#tagLinks: HTMLAnchorElement[] = [];
|
||||
|
||||
#tagLinksReplaced: boolean | null = null;
|
||||
#linkTextReplaced: boolean | null = null;
|
||||
|
||||
protected build() {
|
||||
this.#contentSection = this.container.querySelector('.communication__content');
|
||||
this.#tagLinks = this.#findAllTagLinks();
|
||||
}
|
||||
|
||||
protected init() {
|
||||
Promise.all([
|
||||
BlockCommunication.#preferences.replaceLinks.get(),
|
||||
BlockCommunication.#preferences.replaceLinkText.get(),
|
||||
]).then(([replaceLinks, replaceLinkText]) => {
|
||||
this.#onReplaceLinkSettingResolved(
|
||||
replaceLinks,
|
||||
replaceLinkText
|
||||
);
|
||||
});
|
||||
|
||||
BlockCommunication.#preferences.subscribe(settings => {
|
||||
this.#onReplaceLinkSettingResolved(
|
||||
settings.replaceLinks ?? false,
|
||||
settings.replaceLinkText ?? true
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#onReplaceLinkSettingResolved(haveToReplaceLinks: boolean, shouldReplaceLinkText: boolean) {
|
||||
if (
|
||||
!this.#tagLinks.length
|
||||
|| this.#tagLinksReplaced === haveToReplaceLinks
|
||||
&& this.#linkTextReplaced === shouldReplaceLinkText
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const linkElement of this.#tagLinks) {
|
||||
linkElement.classList.toggle('tag', haveToReplaceLinks);
|
||||
|
||||
// Sometimes tags are being decorated with the code block inside. It should be fine to replace it right away.
|
||||
if (linkElement.childElementCount === 1 && linkElement.children[0].tagName === 'CODE') {
|
||||
linkElement.textContent = linkElement.children[0].textContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolved tag name. It should be stored for the text replacement.
|
||||
*/
|
||||
let tagName: string | undefined;
|
||||
|
||||
if (haveToReplaceLinks) {
|
||||
tagName = resolveTagNameFromLink(new URL(linkElement.href)) ?? '';
|
||||
linkElement.dataset.tagCategory = resolveTagCategoryFromTagName(tagName) ?? '';
|
||||
} else {
|
||||
linkElement.dataset.tagCategory = '';
|
||||
}
|
||||
|
||||
this.#toggleTagLinkText(
|
||||
linkElement,
|
||||
haveToReplaceLinks && shouldReplaceLinkText,
|
||||
tagName,
|
||||
);
|
||||
}
|
||||
|
||||
this.#tagLinksReplaced = haveToReplaceLinks;
|
||||
this.#linkTextReplaced = shouldReplaceLinkText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap the link text with the tag name or restore it back to original content. This function will only perform
|
||||
* replacement on links without any additional tags inside. This will ensure link won't break original content.
|
||||
* @param linkElement Element to swap the text on.
|
||||
* @param shouldSwapToTagName Should we swap the text to tag name or retore it back from memory.
|
||||
* @param tagName Tag name to swap the text to. If not provided, text will be swapped back.
|
||||
* @private
|
||||
*/
|
||||
#toggleTagLinkText(linkElement: HTMLElement, shouldSwapToTagName: boolean, tagName?: string) {
|
||||
if (linkElement.childElementCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we save the original text to memory.
|
||||
if (!BlockCommunication.#originalTagLinkTexts.has(linkElement)) {
|
||||
BlockCommunication.#originalTagLinkTexts.set(linkElement, linkElement.textContent);
|
||||
}
|
||||
|
||||
if (shouldSwapToTagName && tagName) {
|
||||
linkElement.textContent = tagName;
|
||||
} else {
|
||||
linkElement.textContent = BlockCommunication.#originalTagLinkTexts.get(linkElement) ?? linkElement.textContent;
|
||||
}
|
||||
}
|
||||
|
||||
#findAllTagLinks(): HTMLAnchorElement[] {
|
||||
return Array
|
||||
.from(this.#contentSection?.querySelectorAll('a') || [])
|
||||
.filter(
|
||||
link =>
|
||||
// Support links pointing to the tag page.
|
||||
link.pathname.startsWith('/tags/')
|
||||
// Also capture link which point to the search results with single tag.
|
||||
|| link.pathname.startsWith('/search')
|
||||
&& link.search.includes('q=')
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
* element and value is a text.
|
||||
* @private
|
||||
*/
|
||||
static #originalTagLinkTexts: WeakMap<HTMLElement, string> = new WeakMap();
|
||||
|
||||
static findAndInitializeAll() {
|
||||
for (const container of document.querySelectorAll<HTMLElement>('.block.communication')) {
|
||||
if (getComponent(container)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
new BlockCommunication(container).initialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<string, string> | 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<HTMLElement> {
|
||||
return document.querySelectorAll('.media-box');
|
||||
}
|
||||
|
||||
static initializePositionCalculation(mediaBoxesList: NodeListOf<HTMLElement>) {
|
||||
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<HTMLElement>) {
|
||||
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');
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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<HTMLElement> = new WeakSet();
|
||||
|
||||
static #findAll(parentNode: ParentNode = document): NodeListOf<HTMLElement> {
|
||||
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<HTMLElement>();
|
||||
|
||||
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<HTMLElement>('#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<HTMLElement>('#image_tags_and_source');
|
||||
|
||||
for (const tagDropdownElement of closestTagEditor.querySelectorAll<HTMLElement>('.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<HTMLElement>('.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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<string, TagGroup>();
|
||||
#separatedHeaders = new Map<string, HTMLElement>();
|
||||
#groupsCount = new Map<string, number>();
|
||||
#lastTagGroup = new WeakMap<TagDropdownWrapper, TagGroup | null>;
|
||||
#lastTagGroup = new WeakMap<TagDropdown, TagGroup | null>;
|
||||
|
||||
#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<TagDropdownWrapper>(maybeDropdownElement);
|
||||
const tagDropdown = getComponent<TagDropdown>(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<HTMLElement>('#image_tags_and_source')) {
|
||||
if (getComponent(element)) {
|
||||
return;
|
||||
static initializeAll() {
|
||||
for (let element of document.querySelectorAll<HTMLElement>('#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<HTMLElement>('#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<HTMLElement>('#image_tags_and_source');
|
||||
|
||||
if (!tagsListElement || getComponent(tagsListElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
new TagsListBlock(tagsListElement)
|
||||
.initialize();
|
||||
});
|
||||
}
|
||||
@@ -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<HTMLElement>('#imagelist-container');
|
||||
|
||||
if (imageListContainer) {
|
||||
new ImageListContainer(imageListContainer).initialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<HTMLElement>('.media-box');
|
||||
const imageListContainer = document.querySelector<HTMLElement>('#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();
|
||||
|
||||
3
src/content/posts.ts
Normal file
3
src/content/posts.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { BlockCommunication } from "$content/components/philomena/BlockCommunication";
|
||||
|
||||
BlockCommunication.findAndInitializeAll();
|
||||
@@ -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();
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { watchTagDropdownsInTagsEditor, wrapTagDropdown } from "$content/components/TagDropdownWrapper";
|
||||
import { TagDropdown } from "$content/components/philomena/TagDropdown";
|
||||
|
||||
for (let tagDropdownElement of document.querySelectorAll<HTMLElement>('.tag.dropdown')) {
|
||||
wrapTagDropdown(tagDropdownElement);
|
||||
}
|
||||
|
||||
watchTagDropdownsInTagsEditor();
|
||||
TagDropdown.findAllAndInitialize();
|
||||
TagDropdown.watch();
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* Build the map containing both real tags and their aliases.
|
||||
*
|
||||
* @param realAndAliasedTags List combining aliases and tag names.
|
||||
* @param realTags List of actual tag names, excluding aliases.
|
||||
*
|
||||
* @return Map where key is a tag or alias and value is an actual tag name.
|
||||
*/
|
||||
export function buildTagsAndAliasesMap(realAndAliasedTags: string[], realTags: string[]): Map<string, string> {
|
||||
const tagsAndAliasesMap: Map<string, string> = new Map();
|
||||
|
||||
for (const tagName of realTags) {
|
||||
tagsAndAliasesMap.set(tagName, tagName);
|
||||
}
|
||||
|
||||
let realTagName: string | null = null;
|
||||
|
||||
for (const tagNameOrAlias of realAndAliasedTags) {
|
||||
if (tagsAndAliasesMap.has(tagNameOrAlias)) {
|
||||
realTagName = tagNameOrAlias;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!realTagName) {
|
||||
console.warn('No real tag found for the alias:', tagNameOrAlias);
|
||||
continue;
|
||||
}
|
||||
|
||||
tagsAndAliasesMap.set(tagNameOrAlias, realTagName);
|
||||
}
|
||||
|
||||
return tagsAndAliasesMap;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ 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";
|
||||
|
||||
type TransportersMapping = {
|
||||
@@ -73,7 +73,7 @@ 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);
|
||||
@@ -99,7 +99,7 @@ export default class BulkEntitiesTransporter {
|
||||
}
|
||||
|
||||
static #transporters: TransportersMapping = {
|
||||
profiles: new EntitiesTransporter(MaintenanceProfile),
|
||||
profiles: new EntitiesTransporter(TaggingProfile),
|
||||
groups: new EntitiesTransporter(TagGroup),
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, TagGroup>();
|
||||
#regExpGroupMatches = new Map<RegExp, TagGroup>();
|
||||
#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()) {
|
||||
@@ -90,6 +90,7 @@ export default class CustomCategoriesResolver {
|
||||
this.#regExpGroupMatches.clear();
|
||||
|
||||
if (!tagGroups.length) {
|
||||
this.#queueUpdatingTags();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -116,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,
|
||||
|
||||
179
src/lib/extension/base/CacheablePreferences.ts
Normal file
179
src/lib/extension/base/CacheablePreferences.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import ConfigurationController from "$lib/extension/ConfigurationController";
|
||||
|
||||
/**
|
||||
* Initialization options for the preference field helper class.
|
||||
*/
|
||||
type PreferenceFieldOptions<FieldKey, ValueType> = {
|
||||
/**
|
||||
* 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<string, any> = Record<string, any>,
|
||||
/**
|
||||
* 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<Fields>;
|
||||
/**
|
||||
* 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<Fields>, options: PreferenceFieldOptions<Key, Fields[Key]>) {
|
||||
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<FieldsType extends Record<string, any>> = {
|
||||
readonly [FieldKey in keyof FieldsType]: PreferenceField<FieldsType, FieldKey>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<Fields> {
|
||||
#controller: ConfigurationController;
|
||||
#cachedValues: Map<keyof Fields, any> = 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<Key extends keyof Fields>(settingName: Key, defaultValue: Fields[Key]): Promise<Fields[Key]> {
|
||||
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<Key extends keyof Fields>(settingName: Key, value: Fields[Key], force: boolean = false): Promise<void> {
|
||||
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<Fields>) => void): () => void {
|
||||
const unsubscribeCallback = this.#controller.subscribeToChanges(callback as (fields: Record<string, any>) => void);
|
||||
|
||||
this.#disposables.push(unsubscribeCallback);
|
||||
|
||||
return unsubscribeCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Completely disable all subscriptions.
|
||||
*/
|
||||
dispose() {
|
||||
for (let disposeCallback of this.#disposables) {
|
||||
disposeCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import ConfigurationController from "$lib/extension/ConfigurationController";
|
||||
|
||||
export default class CacheableSettings<Fields> {
|
||||
#controller: ConfigurationController;
|
||||
#cachedValues: Map<keyof Fields, any> = 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<SettingType>}
|
||||
* @protected
|
||||
*/
|
||||
protected async _resolveSetting<Key extends keyof Fields>(settingName: Key, defaultValue: Fields[Key]): Promise<Fields[Key]> {
|
||||
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<Key extends keyof Fields>(settingName: Key, value: Fields[Key], force: boolean = false): Promise<void> {
|
||||
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<Fields>) => void): () => void {
|
||||
const unsubscribeCallback = this.#controller.subscribeToChanges(callback as (fields: Record<string, any>) => void);
|
||||
|
||||
this.#disposables.push(unsubscribeCallback);
|
||||
|
||||
return unsubscribeCallback;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
for (let disposeCallback of this.#disposables) {
|
||||
disposeCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<MaintenanceProfileSettings> {
|
||||
export default class TaggingProfile extends StorageEntity<TaggingProfileSettings> {
|
||||
/**
|
||||
* @param id ID of the entity.
|
||||
* @param settings Maintenance profile settings object.
|
||||
*/
|
||||
constructor(id: string, settings: Partial<MaintenanceProfileSettings>) {
|
||||
constructor(id: string, settings: Partial<TaggingProfileSettings>) {
|
||||
super(id, {
|
||||
name: settings.name || '',
|
||||
tags: settings.tags || [],
|
||||
27
src/lib/extension/preferences/MiscPreferences.ts
Normal file
27
src/lib/extension/preferences/MiscPreferences.ts
Normal file
@@ -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<MiscPreferencesFields> implements WithFields<MiscPreferencesFields> {
|
||||
constructor() {
|
||||
super("misc");
|
||||
}
|
||||
|
||||
readonly fullscreenViewer = new PreferenceField(this, {
|
||||
field: "fullscreenViewer",
|
||||
defaultValue: true,
|
||||
});
|
||||
|
||||
readonly fullscreenViewerSize = new PreferenceField(this, {
|
||||
field: "fullscreenViewerSize",
|
||||
defaultValue: "large",
|
||||
});
|
||||
}
|
||||
40
src/lib/extension/preferences/TaggingProfilesPreferences.ts
Normal file
40
src/lib/extension/preferences/TaggingProfilesPreferences.ts
Normal file
@@ -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<TaggingProfilePreferencesFields, "activeProfile"> {
|
||||
constructor(preferencesInstance: CacheablePreferences<TaggingProfilePreferencesFields>) {
|
||||
super(preferencesInstance, {
|
||||
field: "activeProfile",
|
||||
defaultValue: null,
|
||||
});
|
||||
}
|
||||
|
||||
async asObject(): Promise<TaggingProfile | null> {
|
||||
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<TaggingProfilePreferencesFields> implements WithFields<TaggingProfilePreferencesFields> {
|
||||
constructor() {
|
||||
super("maintenance");
|
||||
}
|
||||
|
||||
readonly activeProfile = new ActiveProfilePreference(this);
|
||||
|
||||
readonly stripBlacklistedTags = new PreferenceField(this, {
|
||||
field: "stripBlacklistedTags",
|
||||
defaultValue: false,
|
||||
});
|
||||
}
|
||||
28
src/lib/extension/preferences/TagsPreferences.ts
Normal file
28
src/lib/extension/preferences/TagsPreferences.ts
Normal file
@@ -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<TagsPreferencesFields> implements WithFields<TagsPreferencesFields> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
@@ -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<MaintenanceSettingsFields> {
|
||||
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<MaintenanceProfile | null> {
|
||||
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<void> {
|
||||
await this._writeSetting("activeProfile", profileId);
|
||||
}
|
||||
|
||||
async setStripBlacklistedTags(isEnabled: boolean) {
|
||||
await this._writeSetting('stripBlacklistedTags', isEnabled);
|
||||
}
|
||||
}
|
||||
@@ -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<MiscSettingsFields> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import CacheableSettings from "$lib/extension/base/CacheableSettings";
|
||||
|
||||
interface TagSettingsFields {
|
||||
groupSeparation: boolean;
|
||||
}
|
||||
|
||||
export default class TagSettings extends CacheableSettings<TagSettingsFields> {
|
||||
constructor() {
|
||||
super("tag");
|
||||
}
|
||||
|
||||
async resolveGroupSeparation() {
|
||||
return this._resolveSetting("groupSeparation", true);
|
||||
}
|
||||
|
||||
async setGroupSeparation(value: boolean) {
|
||||
return this._writeSetting("groupSeparation", Boolean(value));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import PostParser from "$lib/booru/scraped/parsing/PostParser";
|
||||
import PostParser from "$lib/philomena/scraping/parsing/PostParser";
|
||||
|
||||
type UpdaterFunction = (tags: Set<string>) => Set<string>;
|
||||
|
||||
@@ -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;
|
||||
118
src/lib/philomena/tag-utils.ts
Normal file
118
src/lib/philomena/tag-utils.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { namespaceCategories } from "$config/tags";
|
||||
import { QueryLexer, QuotedTermToken, TermToken } from "$lib/philomena/search/QueryLexer";
|
||||
|
||||
/**
|
||||
* Build the map containing both real tags and their aliases.
|
||||
*
|
||||
* @param realAndAliasedTags List combining aliases and tag names.
|
||||
* @param realTags List of actual tag names, excluding aliases.
|
||||
*
|
||||
* @return Map where key is a tag or alias and value is an actual tag name.
|
||||
*/
|
||||
export function buildTagsAndAliasesMap(realAndAliasedTags: string[], realTags: string[]): Map<string, string> {
|
||||
const tagsAndAliasesMap: Map<string, string> = new Map();
|
||||
|
||||
for (const tagName of realTags) {
|
||||
tagsAndAliasesMap.set(tagName, tagName);
|
||||
}
|
||||
|
||||
let realTagName: string | null = null;
|
||||
|
||||
for (const tagNameOrAlias of realAndAliasedTags) {
|
||||
if (tagsAndAliasesMap.has(tagNameOrAlias)) {
|
||||
realTagName = tagNameOrAlias;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!realTagName) {
|
||||
console.warn('No real tag found for the alias:', tagNameOrAlias);
|
||||
continue;
|
||||
}
|
||||
|
||||
tagsAndAliasesMap.set(tagNameOrAlias, realTagName);
|
||||
}
|
||||
|
||||
return tagsAndAliasesMap;
|
||||
}
|
||||
|
||||
const tagLinkRegExp = /\/tags\/(?<encodedTagName>[^/?#]+)/;
|
||||
|
||||
/**
|
||||
* List of encoded characters from Philomena.
|
||||
*
|
||||
* @see https://github.com/philomena-dev/philomena/blob/6086757b654da8792ae52adb2a2f501ea6c30d12/lib/philomena/slug.ex#L52-L57
|
||||
*/
|
||||
const slugEncodedCharacters: Map<string, string> = new Map([
|
||||
['-dash-', '-'],
|
||||
['-fwslash-', '/'],
|
||||
['-bwslash-', '\\'],
|
||||
['-colon-', ':'],
|
||||
['-dot-', '.'],
|
||||
['-plus-', '+'],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Try to parse the tag name from the search query URL. It uses the same tokenizer as the booru. It only returns the
|
||||
* tag name if query contains only one single tag without any additional conditions.
|
||||
*
|
||||
* @param searchLink Link with search query.
|
||||
*
|
||||
* @return Tag name or NULL if query contains more than 1 tag or doesn't have any tags at all.
|
||||
*/
|
||||
function parseTagNameFromSearchQuery(searchLink: URL): string | null {
|
||||
const lexer = new QueryLexer(searchLink.searchParams.get('q') || '');
|
||||
const parsedQuery = lexer.parse();
|
||||
|
||||
if (parsedQuery.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [token] = parsedQuery;
|
||||
|
||||
switch (true) {
|
||||
case token instanceof TermToken:
|
||||
return token.value;
|
||||
case token instanceof QuotedTermToken:
|
||||
return token.decodedValue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the tag name from the following link.
|
||||
*
|
||||
* @param tagLink Search link or link to the tag to parse the tag name from.
|
||||
*
|
||||
* @return Tag name or NULL if function is failed to parse the name of the tag.
|
||||
*/
|
||||
export function resolveTagNameFromLink(tagLink: URL): string | null {
|
||||
if (tagLink.pathname.startsWith('/search') && tagLink.searchParams.has('q')) {
|
||||
return parseTagNameFromSearchQuery(tagLink);
|
||||
}
|
||||
|
||||
tagLinkRegExp.lastIndex = 0;
|
||||
|
||||
const result = tagLinkRegExp.exec(tagLink.pathname);
|
||||
const encodedTagName = result?.groups?.encodedTagName;
|
||||
|
||||
if (!encodedTagName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return decodeURIComponent(encodedTagName)
|
||||
.replaceAll(/-[a-z]+-/gi, match => slugEncodedCharacters.get(match) ?? match)
|
||||
.replaceAll('-', ' ')
|
||||
.replaceAll('+', ' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to resolve the category from the tag name.
|
||||
*
|
||||
* @param tagName Name of the tag.
|
||||
*/
|
||||
export function resolveTagCategoryFromTagName(tagName: string): string | null {
|
||||
const namespace = tagName.split(':')[0];
|
||||
|
||||
return namespaceCategories.get(namespace) ?? null;
|
||||
}
|
||||
@@ -1,30 +1,30 @@
|
||||
<script lang="ts">
|
||||
import Menu from "$components/ui/menu/Menu.svelte";
|
||||
import MenuItem from "$components/ui/menu/MenuItem.svelte";
|
||||
import { activeProfileStore, maintenanceProfiles } from "$stores/entities/maintenance-profiles";
|
||||
import { activeTaggingProfile, taggingProfiles } from "$stores/entities/tagging-profiles";
|
||||
import MenuCheckboxItem from "$components/ui/menu/MenuCheckboxItem.svelte";
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import { popupTitle } from "$stores/popup";
|
||||
|
||||
$popupTitle = null;
|
||||
|
||||
let activeProfile = $derived<MaintenanceProfile | null>(
|
||||
$maintenanceProfiles.find(profile => profile.id === $activeProfileStore) || null
|
||||
let activeProfile = $derived<TaggingProfile | null>(
|
||||
$taggingProfiles.find(profile => profile.id === $activeTaggingProfile) || null
|
||||
);
|
||||
|
||||
function turnOffActiveProfile() {
|
||||
$activeProfileStore = null;
|
||||
$activeTaggingProfile = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
{#if activeProfile}
|
||||
<MenuCheckboxItem checked oninput={turnOffActiveProfile} href="/features/maintenance/{activeProfile.id}">
|
||||
<MenuCheckboxItem checked oninput={turnOffActiveProfile} href="/features/profiles/{activeProfile.id}">
|
||||
Active Profile: {activeProfile.settings.name}
|
||||
</MenuCheckboxItem>
|
||||
<hr>
|
||||
{/if}
|
||||
<MenuItem href="/features/maintenance">Tagging Profiles</MenuItem>
|
||||
<MenuItem href="/features/profiles">Tagging Profiles</MenuItem>
|
||||
<MenuItem href="/features/groups">Tag Groups</MenuItem>
|
||||
<hr>
|
||||
<MenuItem href="/transporting">Import/Export</MenuItem>
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
if (__CURRENT_SITE__ === 'derpibooru') {
|
||||
currentSiteUrl = 'https://derpibooru.org';
|
||||
}
|
||||
|
||||
if (__CURRENT_SITE__ === 'tantabus') {
|
||||
currentSiteUrl = 'https://tantabus.ai';
|
||||
}
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
<Menu>
|
||||
<MenuItem href="/">Back</MenuItem>
|
||||
<hr>
|
||||
<MenuItem href="/features/maintenance">Tagging Profiles</MenuItem>
|
||||
<MenuItem href="/features/profiles">Tagging Profiles</MenuItem>
|
||||
</Menu>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import EntitiesTransporter from "$lib/extension/EntitiesTransporter";
|
||||
import { tagGroups } from "$stores/entities/tag-groups";
|
||||
import { popupTitle } from "$stores/popup";
|
||||
import Notice from "$components/ui/Notice.svelte";
|
||||
|
||||
const groupTransporter = new EntitiesTransporter(TagGroup);
|
||||
|
||||
@@ -81,7 +82,7 @@
|
||||
<hr>
|
||||
</Menu>
|
||||
{#if errorMessage}
|
||||
<p class="error">Failed to import: {errorMessage}</p>
|
||||
<Notice level="error">Failed to import: {errorMessage}</Notice>
|
||||
<Menu>
|
||||
<hr>
|
||||
</Menu>
|
||||
@@ -98,9 +99,10 @@
|
||||
</Menu>
|
||||
{:else}
|
||||
{#if existingGroup}
|
||||
<p class="warning">
|
||||
<Notice level="warning">
|
||||
This group will replace the existing "{existingGroup.settings.name}" group, since it have the same ID.
|
||||
</p>
|
||||
</Notice>
|
||||
<br>
|
||||
{/if}
|
||||
<GroupView group={candidateGroup}></GroupView>
|
||||
<Menu>
|
||||
@@ -114,24 +116,3 @@
|
||||
<MenuItem onclick={() => candidateGroup = null}>Cancel</MenuItem>
|
||||
</Menu>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
@use '$styles/colors';
|
||||
|
||||
.error, .warning {
|
||||
padding: 5px 24px;
|
||||
margin: {
|
||||
left: -24px;
|
||||
right: -24px;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
background: colors.$error-background;
|
||||
}
|
||||
|
||||
.warning {
|
||||
background: colors.$warning-background;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,45 +2,45 @@
|
||||
import Menu from "$components/ui/menu/Menu.svelte";
|
||||
import MenuItem from "$components/ui/menu/MenuItem.svelte";
|
||||
import MenuRadioItem from "$components/ui/menu/MenuRadioItem.svelte";
|
||||
import { activeProfileStore, maintenanceProfiles } from "$stores/entities/maintenance-profiles";
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import { activeTaggingProfile, taggingProfiles } from "$stores/entities/tagging-profiles";
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import { popupTitle } from "$stores/popup";
|
||||
|
||||
$popupTitle = 'Tagging Profiles';
|
||||
|
||||
let profiles = $derived<MaintenanceProfile[]>(
|
||||
$maintenanceProfiles.sort((a, b) => a.settings.name.localeCompare(b.settings.name))
|
||||
let profiles = $derived<TaggingProfile[]>(
|
||||
$taggingProfiles.sort((a, b) => a.settings.name.localeCompare(b.settings.name))
|
||||
);
|
||||
|
||||
function resetActiveProfile() {
|
||||
$activeProfileStore = null;
|
||||
$activeTaggingProfile = null;
|
||||
}
|
||||
|
||||
function enableSelectedProfile(event: Event) {
|
||||
const target = event.target;
|
||||
|
||||
if (target instanceof HTMLInputElement && target.checked) {
|
||||
activeProfileStore.set(target.value);
|
||||
activeTaggingProfile.set(target.value);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuItem href="/" icon="arrow-left">Back</MenuItem>
|
||||
<MenuItem href="/features/maintenance/new/edit" icon="plus">Create New</MenuItem>
|
||||
<MenuItem href="/features/profiles/new/edit" icon="plus">Create New</MenuItem>
|
||||
{#if profiles.length}
|
||||
<hr>
|
||||
{/if}
|
||||
{#each profiles as profile}
|
||||
<MenuRadioItem href="/features/maintenance/{profile.id}"
|
||||
<MenuRadioItem href="/features/profiles/{profile.id}"
|
||||
name="active-profile"
|
||||
value={profile.id}
|
||||
checked={$activeProfileStore === profile.id}
|
||||
checked={$activeTaggingProfile === profile.id}
|
||||
oninput={enableSelectedProfile}>
|
||||
{profile.settings.name}
|
||||
</MenuRadioItem>
|
||||
{/each}
|
||||
<hr>
|
||||
<MenuItem href="#" onclick={resetActiveProfile}>Reset Active Profile</MenuItem>
|
||||
<MenuItem href="/features/maintenance/import">Import Profile</MenuItem>
|
||||
<MenuItem href="/features/profiles/import">Import Profile</MenuItem>
|
||||
</Menu>
|
||||
@@ -3,26 +3,26 @@
|
||||
import MenuItem from "$components/ui/menu/MenuItem.svelte";
|
||||
import { page } from "$app/state";
|
||||
import { goto } from "$app/navigation";
|
||||
import { activeProfileStore, maintenanceProfiles } from "$stores/entities/maintenance-profiles";
|
||||
import { activeTaggingProfile, taggingProfiles } from "$stores/entities/tagging-profiles";
|
||||
import ProfileView from "$components/features/ProfileView.svelte";
|
||||
import MenuCheckboxItem from "$components/ui/menu/MenuCheckboxItem.svelte";
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import { popupTitle } from "$stores/popup";
|
||||
|
||||
let profileId = $derived(page.params.id);
|
||||
let profile = $derived<MaintenanceProfile|null>(
|
||||
$maintenanceProfiles.find(profile => profile.id === profileId) || null
|
||||
let profile = $derived<TaggingProfile|null>(
|
||||
$taggingProfiles.find(profile => profile.id === profileId) || null
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (profileId === 'new') {
|
||||
goto('/features/maintenance/new/edit');
|
||||
goto('/features/profiles/new/edit');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!profile) {
|
||||
console.warn(`Profile ${profileId} not found.`);
|
||||
goto('/features/maintenance');
|
||||
goto('/features/profiles');
|
||||
} else {
|
||||
$popupTitle = `Tagging Profile: ${profile.settings.name}`;
|
||||
}
|
||||
@@ -31,22 +31,22 @@
|
||||
let isActiveProfile = $state(false);
|
||||
|
||||
$effect.pre(() => {
|
||||
isActiveProfile = $activeProfileStore === profileId;
|
||||
isActiveProfile = $activeTaggingProfile === profileId;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (isActiveProfile && $activeProfileStore !== profileId) {
|
||||
$activeProfileStore = profileId;
|
||||
if (isActiveProfile && $activeTaggingProfile !== profileId) {
|
||||
$activeTaggingProfile = profileId;
|
||||
}
|
||||
|
||||
if (!isActiveProfile && $activeProfileStore === profileId) {
|
||||
$activeProfileStore = null;
|
||||
if (!isActiveProfile && $activeTaggingProfile === profileId) {
|
||||
$activeTaggingProfile = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuItem href="/features/maintenance" icon="arrow-left">Back</MenuItem>
|
||||
<MenuItem href="/features/profiles" icon="arrow-left">Back</MenuItem>
|
||||
<hr>
|
||||
</Menu>
|
||||
{#if profile}
|
||||
@@ -54,14 +54,14 @@
|
||||
{/if}
|
||||
<Menu>
|
||||
<hr>
|
||||
<MenuItem href="/features/maintenance/{profileId}/edit" icon="wrench">Edit Profile</MenuItem>
|
||||
<MenuItem href="/features/profiles/{profileId}/edit" icon="wrench">Edit Profile</MenuItem>
|
||||
<MenuCheckboxItem bind:checked={isActiveProfile}>
|
||||
Activate Profile
|
||||
</MenuCheckboxItem>
|
||||
<MenuItem href="/features/maintenance/{profileId}/export" icon="file-export">
|
||||
<MenuItem href="/features/profiles/{profileId}/export" icon="file-export">
|
||||
Export Profile
|
||||
</MenuItem>
|
||||
<MenuItem href="/features/maintenance/{profileId}/delete" icon="trash">
|
||||
<MenuItem href="/features/profiles/{profileId}/delete" icon="trash">
|
||||
Delete Profile
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
@@ -3,18 +3,18 @@
|
||||
import Menu from "$components/ui/menu/Menu.svelte";
|
||||
import MenuItem from "$components/ui/menu/MenuItem.svelte";
|
||||
import { page } from "$app/state";
|
||||
import { maintenanceProfiles } from "$stores/entities/maintenance-profiles";
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import { taggingProfiles } from "$stores/entities/tagging-profiles";
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import { popupTitle } from "$stores/popup";
|
||||
|
||||
const profileId = $derived(page.params.id);
|
||||
const targetProfile = $derived<MaintenanceProfile | null>(
|
||||
$maintenanceProfiles.find(profile => profile.id === profileId) || null
|
||||
const targetProfile = $derived<TaggingProfile | null>(
|
||||
$taggingProfiles.find(profile => profile.id === profileId) || null
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (!targetProfile) {
|
||||
goto('/features/maintenance');
|
||||
goto('/features/profiles');
|
||||
} else {
|
||||
$popupTitle = `Deleting Tagging Profile: ${targetProfile.settings.name}`
|
||||
}
|
||||
@@ -27,12 +27,12 @@
|
||||
}
|
||||
|
||||
await targetProfile.delete();
|
||||
await goto('/features/maintenance');
|
||||
await goto('/features/profiles');
|
||||
}
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuItem href="/features/maintenance/{profileId}" icon="arrow-left">Back</MenuItem>
|
||||
<MenuItem href="/features/profiles/{profileId}" icon="arrow-left">Back</MenuItem>
|
||||
<hr>
|
||||
</Menu>
|
||||
{#if targetProfile}
|
||||
@@ -42,7 +42,7 @@
|
||||
<Menu>
|
||||
<hr>
|
||||
<MenuItem onclick={deleteProfile}>Yes</MenuItem>
|
||||
<MenuItem href="/features/maintenance/{profileId}">No</MenuItem>
|
||||
<MenuItem href="/features/profiles/{profileId}">No</MenuItem>
|
||||
</Menu>
|
||||
{:else}
|
||||
<p>Loading...</p>
|
||||
@@ -7,19 +7,19 @@
|
||||
import FormContainer from "$components/ui/forms/FormContainer.svelte";
|
||||
import { page } from "$app/state";
|
||||
import { goto } from "$app/navigation";
|
||||
import { maintenanceProfiles } from "$stores/entities/maintenance-profiles";
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import { taggingProfiles } from "$stores/entities/tagging-profiles";
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import { popupTitle } from "$stores/popup";
|
||||
|
||||
|
||||
let profileId = $derived(page.params.id);
|
||||
|
||||
let targetProfile = $derived.by<MaintenanceProfile | null>(() => {
|
||||
let targetProfile = $derived.by<TaggingProfile | null>(() => {
|
||||
if (profileId === 'new') {
|
||||
return new MaintenanceProfile(crypto.randomUUID(), {});
|
||||
return new TaggingProfile(crypto.randomUUID(), {});
|
||||
}
|
||||
|
||||
return $maintenanceProfiles.find(profile => profile.id === profileId) || null;
|
||||
return $taggingProfiles.find(profile => profile.id === profileId) || null;
|
||||
});
|
||||
|
||||
let profileName = $state('');
|
||||
@@ -32,7 +32,7 @@
|
||||
}
|
||||
|
||||
if (!targetProfile) {
|
||||
goto('/features/maintenance');
|
||||
goto('/features/profiles');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -53,12 +53,12 @@
|
||||
targetProfile.settings.temporary = false;
|
||||
|
||||
await targetProfile.save();
|
||||
await goto('/features/maintenance/' + targetProfile.id);
|
||||
await goto('/features/profiles/' + targetProfile.id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuItem href="/features/maintenance{profileId === 'new' ? '' : '/' + profileId}" icon="arrow-left">
|
||||
<MenuItem href="/features/profiles{profileId === 'new' ? '' : '/' + profileId}" icon="arrow-left">
|
||||
Back
|
||||
</MenuItem>
|
||||
<hr>
|
||||
@@ -1,31 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { page } from "$app/state";
|
||||
import { goto } from "$app/navigation";
|
||||
import { maintenanceProfiles } from "$stores/entities/maintenance-profiles";
|
||||
import { taggingProfiles } from "$stores/entities/tagging-profiles";
|
||||
import Menu from "$components/ui/menu/Menu.svelte";
|
||||
import MenuItem from "$components/ui/menu/MenuItem.svelte";
|
||||
import FormContainer from "$components/ui/forms/FormContainer.svelte";
|
||||
import FormControl from "$components/ui/forms/FormControl.svelte";
|
||||
import EntitiesTransporter from "$lib/extension/EntitiesTransporter";
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import { popupTitle } from "$stores/popup";
|
||||
|
||||
let isCompressedProfileShown = $state(true);
|
||||
|
||||
const profileId = $derived(page.params.id);
|
||||
const profile = $derived<MaintenanceProfile | null>(
|
||||
$maintenanceProfiles.find(profile => profile.id === profileId) || null
|
||||
const profile = $derived<TaggingProfile | null>(
|
||||
$taggingProfiles.find(profile => profile.id === profileId) || null
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (!profile) {
|
||||
goto('/features/maintenance/');
|
||||
goto('/features/profiles/');
|
||||
} else {
|
||||
$popupTitle = `Export Tagging Profile: ${profile.settings.name}`;
|
||||
}
|
||||
});
|
||||
|
||||
const profilesTransporter = new EntitiesTransporter(MaintenanceProfile);
|
||||
const profilesTransporter = new EntitiesTransporter(TaggingProfile);
|
||||
|
||||
let rawExportedProfile = $derived(profile ? profilesTransporter.exportToJSON(profile) : '');
|
||||
let compressedExportedProfile = $derived(profile ? profilesTransporter.exportToCompressedJSON(profile) : '');
|
||||
@@ -33,7 +33,7 @@
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuItem href="/features/maintenance/{profileId}" icon="arrow-left">
|
||||
<MenuItem href="/features/profiles/{profileId}" icon="arrow-left">
|
||||
Back
|
||||
</MenuItem>
|
||||
<hr>
|
||||
@@ -2,21 +2,22 @@
|
||||
import Menu from "$components/ui/menu/Menu.svelte";
|
||||
import MenuItem from "$components/ui/menu/MenuItem.svelte";
|
||||
import FormContainer from "$components/ui/forms/FormContainer.svelte";
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import FormControl from "$components/ui/forms/FormControl.svelte";
|
||||
import ProfileView from "$components/features/ProfileView.svelte";
|
||||
import { maintenanceProfiles } from "$stores/entities/maintenance-profiles";
|
||||
import { taggingProfiles } from "$stores/entities/tagging-profiles";
|
||||
import { goto } from "$app/navigation";
|
||||
import EntitiesTransporter from "$lib/extension/EntitiesTransporter";
|
||||
import { popupTitle } from "$stores/popup";
|
||||
import Notice from "$components/ui/Notice.svelte";
|
||||
|
||||
const profilesTransporter = new EntitiesTransporter(MaintenanceProfile);
|
||||
const profilesTransporter = new EntitiesTransporter(TaggingProfile);
|
||||
|
||||
let importedString = $state('');
|
||||
let errorMessage = $state('');
|
||||
|
||||
let candidateProfile = $state<MaintenanceProfile | null>(null);
|
||||
let existingProfile = $state<MaintenanceProfile | null>(null);
|
||||
let candidateProfile = $state<TaggingProfile | null>(null);
|
||||
let existingProfile = $state<TaggingProfile | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
$popupTitle = candidateProfile
|
||||
@@ -48,7 +49,7 @@
|
||||
}
|
||||
|
||||
if (candidateProfile) {
|
||||
existingProfile = $maintenanceProfiles.find(profile => profile.id === candidateProfile?.id) ?? null;
|
||||
existingProfile = $taggingProfiles.find(profile => profile.id === candidateProfile?.id) ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +59,7 @@
|
||||
}
|
||||
|
||||
candidateProfile.save().then(() => {
|
||||
goto(`/features/maintenance`);
|
||||
goto(`/features/profiles`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -67,20 +68,20 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const clonedProfile = new MaintenanceProfile(crypto.randomUUID(), candidateProfile.settings);
|
||||
const clonedProfile = new TaggingProfile(crypto.randomUUID(), candidateProfile.settings);
|
||||
clonedProfile.settings.name += ` (Clone ${new Date().toISOString()})`;
|
||||
clonedProfile.save().then(() => {
|
||||
goto(`/features/maintenance`);
|
||||
goto(`/features/profiles`);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuItem href="/features/maintenance" icon="arrow-left">Back</MenuItem>
|
||||
<MenuItem href="/features/profiles" icon="arrow-left">Back</MenuItem>
|
||||
<hr>
|
||||
</Menu>
|
||||
{#if errorMessage}
|
||||
<p class="error">Failed to import: {errorMessage}</p>
|
||||
<Notice level="error">Failed to import: {errorMessage}</Notice>
|
||||
<Menu>
|
||||
<hr>
|
||||
</Menu>
|
||||
@@ -97,9 +98,10 @@
|
||||
</Menu>
|
||||
{:else}
|
||||
{#if existingProfile}
|
||||
<p class="warning">
|
||||
<Notice level="warning">
|
||||
This profile will replace the existing "{existingProfile.settings.name}" profile, since it have the same ID.
|
||||
</p>
|
||||
</Notice>
|
||||
<br>
|
||||
{/if}
|
||||
<ProfileView profile={candidateProfile}></ProfileView>
|
||||
<Menu>
|
||||
@@ -113,24 +115,3 @@
|
||||
<MenuItem onclick={() => candidateProfile = null}>Cancel</MenuItem>
|
||||
</Menu>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
@use '$styles/colors';
|
||||
|
||||
.error, .warning {
|
||||
padding: 5px 24px;
|
||||
margin: {
|
||||
left: -24px;
|
||||
right: -24px;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
background: colors.$error-background;
|
||||
}
|
||||
|
||||
.warning {
|
||||
background: colors.$warning-background;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
</style>
|
||||
@@ -4,8 +4,12 @@
|
||||
import FormControl from "$components/ui/forms/FormControl.svelte";
|
||||
import Menu from "$components/ui/menu/Menu.svelte";
|
||||
import MenuItem from "$components/ui/menu/MenuItem.svelte";
|
||||
import { stripBlacklistedTagsEnabled } from "$stores/preferences/maintenance";
|
||||
import { shouldSeparateTagGroups } from "$stores/preferences/tag";
|
||||
import { stripBlacklistedTagsEnabled } from "$stores/preferences/profiles";
|
||||
import {
|
||||
shouldReplaceLinksOnForumPosts,
|
||||
shouldReplaceTextOfTagLinks,
|
||||
shouldSeparateTagGroups
|
||||
} from "$stores/preferences/tags";
|
||||
import { popupTitle } from "$stores/popup";
|
||||
|
||||
$popupTitle = 'Tagging Preferences';
|
||||
@@ -26,4 +30,16 @@
|
||||
Enable separation of custom tag groups on the image pages
|
||||
</CheckboxField>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<CheckboxField bind:checked={$shouldReplaceLinksOnForumPosts}>
|
||||
Find and replace links to the tags in the forum posts
|
||||
</CheckboxField>
|
||||
</FormControl>
|
||||
{#if $shouldReplaceLinksOnForumPosts}
|
||||
<FormControl>
|
||||
<CheckboxField bind:checked={$shouldReplaceTextOfTagLinks}>
|
||||
Try to replace text on links pointing to tags in forum posts
|
||||
</CheckboxField>
|
||||
</FormControl>
|
||||
{/if}
|
||||
</FormContainer>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Menu from "$components/ui/menu/Menu.svelte";
|
||||
import MenuItem from "$components/ui/menu/MenuItem.svelte";
|
||||
import { maintenanceProfiles } from "$stores/entities/maintenance-profiles";
|
||||
import { taggingProfiles } from "$stores/entities/tagging-profiles";
|
||||
import MenuCheckboxItem from "$components/ui/menu/MenuCheckboxItem.svelte";
|
||||
import { tagGroups } from "$stores/entities/tag-groups";
|
||||
import BulkEntitiesTransporter from "$lib/extension/BulkEntitiesTransporter";
|
||||
@@ -30,7 +30,7 @@
|
||||
if (displayExportedString) {
|
||||
const elementsToExport: StorageEntity[] = [];
|
||||
|
||||
$maintenanceProfiles.forEach(profile => {
|
||||
$taggingProfiles.forEach(profile => {
|
||||
if (exportedEntities.profiles[profile.id]) {
|
||||
elementsToExport.push(profile);
|
||||
}
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
function refreshAreAllEntitiesChecked() {
|
||||
requestAnimationFrame(() => {
|
||||
exportAllProfiles = $maintenanceProfiles.every(profile => exportedEntities.profiles[profile.id]);
|
||||
exportAllProfiles = $taggingProfiles.every(profile => exportedEntities.profiles[profile.id]);
|
||||
exportAllGroups = $tagGroups.every(group => exportedEntities.groups[group.id]);
|
||||
});
|
||||
}
|
||||
@@ -69,7 +69,7 @@
|
||||
requestAnimationFrame(() => {
|
||||
switch (targetEntity) {
|
||||
case "profiles":
|
||||
$maintenanceProfiles.forEach(profile => exportedEntities.profiles[profile.id] = exportAllProfiles);
|
||||
$taggingProfiles.forEach(profile => exportedEntities.profiles[profile.id] = exportAllProfiles);
|
||||
break;
|
||||
case "groups":
|
||||
$tagGroups.forEach(group => exportedEntities.groups[group.id] = exportAllGroups);
|
||||
@@ -94,11 +94,11 @@
|
||||
<Menu>
|
||||
<MenuItem href="/transporting" icon="arrow-left">Back</MenuItem>
|
||||
<hr>
|
||||
{#if $maintenanceProfiles.length}
|
||||
{#if $taggingProfiles.length}
|
||||
<MenuCheckboxItem bind:checked={exportAllProfiles} oninput={createToggleAllOnUserInput('profiles')}>
|
||||
Export All Profiles
|
||||
</MenuCheckboxItem>
|
||||
{#each $maintenanceProfiles as profile}
|
||||
{#each $taggingProfiles as profile}
|
||||
<MenuCheckboxItem bind:checked={exportedEntities.profiles[profile.id]} oninput={refreshAreAllEntitiesChecked}>
|
||||
Profile: {profile.settings.name}
|
||||
</MenuCheckboxItem>
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import MenuItem from "$components/ui/menu/MenuItem.svelte";
|
||||
import FormContainer from "$components/ui/forms/FormContainer.svelte";
|
||||
import FormControl from "$components/ui/forms/FormControl.svelte";
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import TagGroup from "$entities/TagGroup";
|
||||
import BulkEntitiesTransporter from "$lib/extension/BulkEntitiesTransporter";
|
||||
import type StorageEntity from "$lib/extension/base/StorageEntity";
|
||||
import { maintenanceProfiles } from "$stores/entities/maintenance-profiles";
|
||||
import { taggingProfiles } from "$stores/entities/tagging-profiles";
|
||||
import { tagGroups } from "$stores/entities/tag-groups";
|
||||
import MenuCheckboxItem from "$components/ui/menu/MenuCheckboxItem.svelte";
|
||||
import ProfileView from "$components/features/ProfileView.svelte";
|
||||
@@ -15,16 +15,19 @@
|
||||
import { goto } from "$app/navigation";
|
||||
import type { SameSiteStatus } from "$lib/extension/EntitiesTransporter";
|
||||
import { popupTitle } from "$stores/popup";
|
||||
import Notice from "$components/ui/Notice.svelte";
|
||||
|
||||
let importedString = $state('');
|
||||
let errorMessage = $state('');
|
||||
|
||||
let importedProfiles = $state<MaintenanceProfile[]>([]);
|
||||
let importedProfiles = $state<TaggingProfile[]>([]);
|
||||
let importedGroups = $state<TagGroup[]>([]);
|
||||
|
||||
let saveAllProfiles = $state(false);
|
||||
let saveAllGroups = $state(false);
|
||||
|
||||
let isSaving = $state(false);
|
||||
|
||||
let selectedEntities: Record<keyof App.EntityNamesMap, Record<string, boolean>> = $state({
|
||||
profiles: {},
|
||||
groups: {},
|
||||
@@ -33,10 +36,10 @@
|
||||
let previewedEntity = $state<StorageEntity | null>(null);
|
||||
|
||||
const existingProfilesMap = $derived(
|
||||
$maintenanceProfiles.reduce((map, profile) => {
|
||||
$taggingProfiles.reduce((map, profile) => {
|
||||
map.set(profile.id, profile);
|
||||
return map;
|
||||
}, new Map<string, MaintenanceProfile>())
|
||||
}, new Map<string, TaggingProfile>())
|
||||
);
|
||||
|
||||
const existingGroupsMap = $derived(
|
||||
@@ -95,7 +98,7 @@
|
||||
for (const targetImportedEntity of importedEntities) {
|
||||
switch (targetImportedEntity.type) {
|
||||
case "profiles":
|
||||
importedProfiles.push(targetImportedEntity as MaintenanceProfile);
|
||||
importedProfiles.push(targetImportedEntity as TaggingProfile);
|
||||
break;
|
||||
case "groups":
|
||||
importedGroups.push(targetImportedEntity as TagGroup);
|
||||
@@ -145,31 +148,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
function saveSelectedEntities() {
|
||||
Promise.allSettled([
|
||||
Promise.allSettled(
|
||||
importedProfiles
|
||||
.filter(profile => selectedEntities.profiles[profile.id])
|
||||
.map(profile => profile.save())
|
||||
),
|
||||
Promise.allSettled(
|
||||
importedGroups
|
||||
.filter(group => selectedEntities.groups[group.id])
|
||||
.map(group => group.save())
|
||||
),
|
||||
]).then(() => {
|
||||
goto("/transporting");
|
||||
});
|
||||
async function saveSelectedEntities() {
|
||||
if (isSaving) {
|
||||
return;
|
||||
}
|
||||
|
||||
isSaving = true;
|
||||
|
||||
for (const profile of importedProfiles) {
|
||||
if (!selectedEntities.profiles[profile.id]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await profile.save();
|
||||
}
|
||||
|
||||
for (const group of importedGroups) {
|
||||
if (!selectedEntities.groups[group.id]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await group.save();
|
||||
}
|
||||
|
||||
await goto("/transporting");
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !hasImportedEntities}
|
||||
{#if isSaving}
|
||||
<p>Saving imported entities...</p>
|
||||
{:else if !hasImportedEntities}
|
||||
<Menu>
|
||||
<MenuItem href="/transporting" icon="arrow-left">Back</MenuItem>
|
||||
<hr>
|
||||
</Menu>
|
||||
{#if errorMessage}
|
||||
<p class="error">{errorMessage}</p>
|
||||
<Notice level="error">{errorMessage}</Notice>
|
||||
<Menu>
|
||||
<hr>
|
||||
</Menu>
|
||||
@@ -188,7 +202,7 @@
|
||||
<MenuItem onclick={() => previewedEntity = null} icon="arrow-left">Back to Selection</MenuItem>
|
||||
<hr>
|
||||
</Menu>
|
||||
{#if previewedEntity instanceof MaintenanceProfile}
|
||||
{#if previewedEntity instanceof TaggingProfile}
|
||||
<ProfileView profile={previewedEntity}></ProfileView>
|
||||
{:else if previewedEntity instanceof TagGroup}
|
||||
<GroupView group={previewedEntity}></GroupView>
|
||||
@@ -201,18 +215,18 @@
|
||||
{/if}
|
||||
</Menu>
|
||||
{#if lastImportStatus === "different"}
|
||||
<p class="warning">
|
||||
<Notice level="warning">
|
||||
<b>Warning!</b>
|
||||
Looks like these entities were exported for the different extension! There are many differences between tagging
|
||||
systems of Furobooru and Derpibooru, so make sure to check if these settings are correct before using them!
|
||||
</p>
|
||||
</Notice>
|
||||
{/if}
|
||||
{#if lastImportStatus === 'unknown'}
|
||||
<p class="warning">
|
||||
<Notice level="warning">
|
||||
<b>Warning!</b>
|
||||
We couldn't verify if these settings are meant for this site or not. There are many differences between tagging
|
||||
systems of Furbooru and Derpibooru, so make sure to check if these settings are correct before using them.
|
||||
</p>
|
||||
</Notice>
|
||||
{/if}
|
||||
<Menu>
|
||||
{#if importedProfiles.length}
|
||||
@@ -264,23 +278,3 @@
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
@use '$styles/colors';
|
||||
|
||||
.error, .warning {
|
||||
padding: 5px 24px;
|
||||
margin: {
|
||||
left: -24px;
|
||||
right: -24px;
|
||||
}
|
||||
}
|
||||
|
||||
.warning {
|
||||
background: colors.$warning-background;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: colors.$error-background;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { type Writable, writable } from "svelte/store";
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings";
|
||||
|
||||
/**
|
||||
* Store for working with maintenance profiles in the Svelte popup.
|
||||
*/
|
||||
export const maintenanceProfiles: Writable<MaintenanceProfile[]> = writable([]);
|
||||
|
||||
/**
|
||||
* Store for the active maintenance profile ID.
|
||||
*/
|
||||
export const activeProfileStore: Writable<string|null> = writable(null);
|
||||
|
||||
const maintenanceSettings = new MaintenanceSettings();
|
||||
|
||||
/**
|
||||
* Active profile ID stored locally. Used to reset active profile once the existing profile was removed.
|
||||
*/
|
||||
let lastActiveProfileId: string|null = null;
|
||||
|
||||
Promise.allSettled([
|
||||
// Read the initial values from the storages first
|
||||
MaintenanceProfile.readAll().then(profiles => {
|
||||
maintenanceProfiles.set(profiles);
|
||||
}),
|
||||
maintenanceSettings.resolveActiveProfileId().then(activeProfileId => {
|
||||
activeProfileStore.set(activeProfileId);
|
||||
})
|
||||
]).then(() => {
|
||||
// And only after initial values are loaded, start watching for changes from storage and from user's interaction
|
||||
MaintenanceProfile.subscribe(profiles => {
|
||||
maintenanceProfiles.set(profiles);
|
||||
});
|
||||
|
||||
maintenanceSettings.subscribe(settings => {
|
||||
activeProfileStore.set(settings.activeProfile || null);
|
||||
});
|
||||
|
||||
activeProfileStore.subscribe(profileId => {
|
||||
lastActiveProfileId = profileId;
|
||||
|
||||
void maintenanceSettings.setActiveProfileId(profileId);
|
||||
});
|
||||
|
||||
// Watch the existence of the active profile on every change.
|
||||
MaintenanceProfile.subscribe(profiles => {
|
||||
if (!profiles.find(profile => profile.id === lastActiveProfileId)) {
|
||||
activeProfileStore.set(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
52
src/stores/entities/tagging-profiles.ts
Normal file
52
src/stores/entities/tagging-profiles.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { type Writable, writable } from "svelte/store";
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import TaggingProfilesPreferences from "$lib/extension/preferences/TaggingProfilesPreferences";
|
||||
|
||||
/**
|
||||
* Store for working with maintenance profiles in the Svelte popup.
|
||||
*/
|
||||
export const taggingProfiles: Writable<TaggingProfile[]> = writable([]);
|
||||
|
||||
/**
|
||||
* Store for the active maintenance profile ID.
|
||||
*/
|
||||
export const activeTaggingProfile: Writable<string|null> = writable(null);
|
||||
|
||||
const preferences = new TaggingProfilesPreferences();
|
||||
|
||||
/**
|
||||
* Active profile ID stored locally. Used to reset active profile once the existing profile was removed.
|
||||
*/
|
||||
let lastActiveProfileId: string|null = null;
|
||||
|
||||
Promise.allSettled([
|
||||
// Read the initial values from the storages first
|
||||
TaggingProfile.readAll().then(profiles => {
|
||||
taggingProfiles.set(profiles);
|
||||
}),
|
||||
preferences.activeProfile.get().then(activeProfileId => {
|
||||
activeTaggingProfile.set(activeProfileId);
|
||||
})
|
||||
]).then(() => {
|
||||
// And only after initial values are loaded, start watching for changes from storage and from user's interaction
|
||||
TaggingProfile.subscribe(profiles => {
|
||||
taggingProfiles.set(profiles);
|
||||
});
|
||||
|
||||
preferences.subscribe(settings => {
|
||||
activeTaggingProfile.set(settings.activeProfile || null);
|
||||
});
|
||||
|
||||
activeTaggingProfile.subscribe(profileId => {
|
||||
lastActiveProfileId = profileId;
|
||||
|
||||
void preferences.activeProfile.set(profileId);
|
||||
});
|
||||
|
||||
// Watch the existence of the active profile on every change.
|
||||
TaggingProfile.subscribe(profiles => {
|
||||
if (!profiles.find(profile => profile.id === lastActiveProfileId)) {
|
||||
activeTaggingProfile.set(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
import { writable } from "svelte/store";
|
||||
import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings";
|
||||
|
||||
export const stripBlacklistedTagsEnabled = writable(true);
|
||||
|
||||
const maintenanceSettings = new MaintenanceSettings();
|
||||
|
||||
Promise
|
||||
.all([
|
||||
maintenanceSettings.resolveStripBlacklistedTags().then(v => stripBlacklistedTagsEnabled.set(v ?? true))
|
||||
])
|
||||
.then(() => {
|
||||
maintenanceSettings.subscribe(settings => {
|
||||
stripBlacklistedTagsEnabled.set(typeof settings.stripBlacklistedTags === 'boolean' ? settings.stripBlacklistedTags : true);
|
||||
});
|
||||
|
||||
stripBlacklistedTagsEnabled.subscribe(v => maintenanceSettings.setStripBlacklistedTags(v));
|
||||
});
|
||||
@@ -1,18 +1,18 @@
|
||||
import { writable } from "svelte/store";
|
||||
import MiscSettings from "$lib/extension/settings/MiscSettings";
|
||||
import MiscPreferences from "$lib/extension/preferences/MiscPreferences";
|
||||
|
||||
export const fullScreenViewerEnabled = writable(true);
|
||||
|
||||
const miscSettings = new MiscSettings();
|
||||
const preferences = new MiscPreferences();
|
||||
|
||||
Promise.allSettled([
|
||||
miscSettings.resolveFullscreenViewerEnabled().then(v => fullScreenViewerEnabled.set(v))
|
||||
preferences.fullscreenViewer.get().then(v => fullScreenViewerEnabled.set(v))
|
||||
]).then(() => {
|
||||
fullScreenViewerEnabled.subscribe(value => {
|
||||
void miscSettings.setFullscreenViewerEnabled(value);
|
||||
void preferences.fullscreenViewer.set(value);
|
||||
});
|
||||
|
||||
miscSettings.subscribe(settings => {
|
||||
preferences.subscribe(settings => {
|
||||
fullScreenViewerEnabled.set(Boolean(settings.fullscreenViewer));
|
||||
});
|
||||
});
|
||||
|
||||
18
src/stores/preferences/profiles.ts
Normal file
18
src/stores/preferences/profiles.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { writable } from "svelte/store";
|
||||
import TaggingProfilesPreferences from "$lib/extension/preferences/TaggingProfilesPreferences";
|
||||
|
||||
export const stripBlacklistedTagsEnabled = writable(true);
|
||||
|
||||
const preferences = new TaggingProfilesPreferences();
|
||||
|
||||
Promise
|
||||
.all([
|
||||
preferences.stripBlacklistedTags.get().then(v => stripBlacklistedTagsEnabled.set(v ?? true))
|
||||
])
|
||||
.then(() => {
|
||||
preferences.subscribe(settings => {
|
||||
stripBlacklistedTagsEnabled.set(typeof settings.stripBlacklistedTags === 'boolean' ? settings.stripBlacklistedTags : true);
|
||||
});
|
||||
|
||||
stripBlacklistedTagsEnabled.subscribe(v => preferences.stripBlacklistedTags.set(v));
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
import { writable } from "svelte/store";
|
||||
import TagSettings from "$lib/extension/settings/TagSettings";
|
||||
|
||||
const tagSettings = new TagSettings();
|
||||
|
||||
export const shouldSeparateTagGroups = writable(false);
|
||||
|
||||
tagSettings.resolveGroupSeparation()
|
||||
.then(value => shouldSeparateTagGroups.set(value))
|
||||
.then(() => {
|
||||
shouldSeparateTagGroups.subscribe(value => {
|
||||
void tagSettings.setGroupSeparation(value);
|
||||
});
|
||||
|
||||
tagSettings.subscribe(settings => {
|
||||
shouldSeparateTagGroups.set(Boolean(settings.groupSeparation));
|
||||
});
|
||||
})
|
||||
34
src/stores/preferences/tags.ts
Normal file
34
src/stores/preferences/tags.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { writable } from "svelte/store";
|
||||
import TagsPreferences from "$lib/extension/preferences/TagsPreferences";
|
||||
|
||||
const preferences = new TagsPreferences();
|
||||
|
||||
export const shouldSeparateTagGroups = writable(false);
|
||||
export const shouldReplaceLinksOnForumPosts = writable(false);
|
||||
export const shouldReplaceTextOfTagLinks = writable(true);
|
||||
|
||||
Promise
|
||||
.allSettled([
|
||||
preferences.groupSeparation.get().then(value => shouldSeparateTagGroups.set(value)),
|
||||
preferences.replaceLinks.get().then(value => shouldReplaceLinksOnForumPosts.set(value)),
|
||||
preferences.replaceLinkText.get().then(value => shouldReplaceTextOfTagLinks.set(value)),
|
||||
])
|
||||
.then(() => {
|
||||
shouldSeparateTagGroups.subscribe(value => {
|
||||
void preferences.groupSeparation.set(value);
|
||||
});
|
||||
|
||||
shouldReplaceLinksOnForumPosts.subscribe(value => {
|
||||
void preferences.replaceLinks.set(value);
|
||||
});
|
||||
|
||||
shouldReplaceTextOfTagLinks.subscribe(value => {
|
||||
void preferences.replaceLinkText.set(value);
|
||||
});
|
||||
|
||||
preferences.subscribe(settings => {
|
||||
shouldSeparateTagGroups.set(Boolean(settings.groupSeparation));
|
||||
shouldReplaceLinksOnForumPosts.set(Boolean(settings.replaceLinks));
|
||||
shouldReplaceTextOfTagLinks.set(Boolean(settings.replaceLinkText));
|
||||
});
|
||||
});
|
||||
@@ -95,3 +95,31 @@ $warning-border: #95562c;
|
||||
$input-background: #282e39;
|
||||
$input-border: #575e6b;
|
||||
}
|
||||
|
||||
@if environment.$current-site == 'tantabus' {
|
||||
$background: #221117;
|
||||
|
||||
$text: #e0e0e0;
|
||||
$text-gray: #bb90a6;
|
||||
|
||||
$link: #ee157a;
|
||||
$link-hover: #b099dd;
|
||||
|
||||
$header: #811242;
|
||||
$header-toolbar: #501e36;
|
||||
$header-hover-background: #5d0d30;
|
||||
$header-mobile-link-hover: #995470;
|
||||
|
||||
$footer: #2f1d26;
|
||||
$footer-text: $text-gray;
|
||||
|
||||
$block-header: $header-toolbar;
|
||||
$block-border: $header-toolbar;
|
||||
$block-background: #2f1d26;
|
||||
$block-background-alternate: #26171e;
|
||||
|
||||
$media-box-border: #573142;
|
||||
|
||||
$input-background: #392833;
|
||||
$input-border: #6b5764;
|
||||
}
|
||||
|
||||
9
src/styles/content/posts.scss
Normal file
9
src/styles/content/posts.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
@use '$styles/booru-vars';
|
||||
|
||||
.block.communication {
|
||||
.tag {
|
||||
&:hover {
|
||||
color: booru-vars.$resolved-tag-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
padding: 0 4px;
|
||||
display: flex;
|
||||
|
||||
@if environment.$current-site == 'derpibooru' {
|
||||
@if environment.$current-site == 'derpibooru' or environment.$current-site == 'tantabus' {
|
||||
border: 1px solid colors.$tag-border;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,14 @@ export default defineConfig(() => {
|
||||
__CURRENT_SITE_NAME__: JSON.stringify('Derpibooru'),
|
||||
}
|
||||
}),
|
||||
SwapDefinedVariablesPlugin({
|
||||
envVariable: 'SITE',
|
||||
expectedValue: 'tantabus',
|
||||
define: {
|
||||
__CURRENT_SITE__: JSON.stringify('tantabus'),
|
||||
__CURRENT_SITE_NAME__: JSON.stringify('Tantabus'),
|
||||
}
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
globals: true,
|
||||
|
||||
Reference in New Issue
Block a user