1
0
mirror of https://github.com/koloml/furbooru-tagging-assistant.git synced 2026-03-25 07:12:58 +00:00

19 Commits

Author SHA1 Message Date
f0083169f3 Merge pull request #164 from koloml/release/0.7.0
Release: 0.7.0
2026-03-14 21:58:02 +04:00
a416befcff Bumped version to 0.7.0 2026-03-14 21:57:10 +04:00
d840070a3e Updated dependencies (#166)
* Updated `vitest` and `@vitest/coverage-v8` from 4.0.18 to 4.1.0

* Updated `svelte` from 5.53.3 to 5.53.12

* Updated `@sveltejs/kit` from 2.53.0 to 2.55.0

* Updated `svelte-check` from 4.4.3 to 4.4.5

* Updated `sass` from 1.97.3 to 1.98.0

* Updated `@types/node` from 25.3.0 to 25.5.0
2026-03-14 21:54:23 +04:00
0bf2a35e9b Added links to Tantabus version of extension 2026-03-14 20:20:35 +04:00
8889c300a0 Merge pull request #163 from koloml/feature/tag-presets
Added Tag Presets for the image upload & edit pages
2026-03-14 20:10:52 +04:00
1a542e0fb7 Merge pull request #162 from koloml/feature/different-icons
Updated icons for different extensions versions
2026-03-14 20:07:54 +04:00
b973947070 Fixed presets not refreshing after tag loading/clearing 2026-03-14 20:06:12 +04:00
7821cebb1b Added missed deletion interface for the presets 2026-03-14 19:37:10 +04:00
71fb565247 Sort list of presets and their tags more consistently 2026-03-14 19:15:59 +04:00
0ba81b1509 Separate icons for extensions, support replacing icons on build 2026-03-14 18:59:03 +04:00
f9fb2d66b8 Don't warn about manifest replacements without ENV variable 2026-03-14 18:31:26 +04:00
2d7db61a76 Reusing details block and tags list components on Profile & Group views 2026-03-12 01:43:14 +04:00
4a3d7a1bb0 Fixing build warning on new details block 2026-03-12 01:42:59 +04:00
939b5fec20 Properly refresh the tag category colors after preset changes applied 2026-03-12 01:33:14 +04:00
d7b7aa5b98 Compensating for possible layout shift after toggling tags from preset 2026-03-12 01:22:03 +04:00
7f41a7e6f0 Fixed tag removal stopping on the first encountered missing tag
Syntax of `-tagname` we rely upon here will stop execution of suggested
tags when first tag which is not present in the editor is encountered.
Just a Philomena's specific quirk, nothing more.
2026-03-12 01:07:37 +04:00
2a2a488592 Added presets and tag editor handling to the image upload page 2026-03-12 00:57:23 +04:00
bda707b5ac Builder: Support replacing domains inside exclude_matches 2026-03-12 00:53:45 +04:00
74866949bb Added Tag Presets, popup editor for them, implemented presets image edit 2026-03-12 00:17:45 +04:00
53 changed files with 1754 additions and 395 deletions

View File

@@ -96,21 +96,16 @@ class ManifestProcessor {
singleOrMultipleHostnames = [singleOrMultipleHostnames];
}
const matchPatterReplacer = ManifestProcessor.#createHostnameReplacementReduce(singleOrMultipleHostnames);
this.#manifestObject.host_permissions = singleOrMultipleHostnames.map(hostname => `*://*.${hostname}/`);
this.#manifestObject.content_scripts?.forEach(entry => {
entry.matches = entry.matches.reduce((resultMatches, originalMatchPattern) => {
for (const updatedHostname of singleOrMultipleHostnames) {
resultMatches.push(
originalMatchPattern.replace(
/\*:\/\/\*\.[a-z]+\.[a-z]+\//,
`*://*.${updatedHostname}/`
),
);
}
entry.matches = entry.matches.reduce(matchPatterReplacer, []);
return resultMatches;
}, []);
if (entry.exclude_matches) {
entry.exclude_matches = entry.exclude_matches.reduce(matchPatterReplacer, []);
}
})
}
@@ -148,6 +143,32 @@ class ManifestProcessor {
}
);
}
/**
* @param {string|(string[])} singleOrMultipleHostnames
* @return {function(string[], string): string[]}
*/
static #createHostnameReplacementReduce(singleOrMultipleHostnames) {
return (
/**
* @param {string[]} resultMatches
* @param {string} originalMatchPattern
* @return {string[]}
*/
(resultMatches, originalMatchPattern) => {
for (const updatedHostname of singleOrMultipleHostnames) {
resultMatches.push(
originalMatchPattern.replace(
/\*:\/\/\*\.[a-z]+\.[a-z]+\//,
`*://*.${updatedHostname}/`
),
);
}
return resultMatches;
}
);
}
}
/**
@@ -186,6 +207,7 @@ export function loadManifest(filePath) {
/**
* @typedef {Object} ContentScriptsEntry
* @property {string[]} matches
* @property {string[]} exclude_matches
* @property {string[]|undefined} js
* @property {string[]|undefined} css
*/

View File

@@ -3,6 +3,7 @@ import path from "path";
import { buildScriptsAndStyles } from "./lib/content-scripts.js";
import { extractInlineScriptsFromIndex } from "./lib/index-file.js";
import { normalizePath } from "vite";
import fs from "fs";
/**
* Build addition assets required for the extension and pack it into the directory.
@@ -84,12 +85,55 @@ export async function packExtension(settings) {
break;
default:
console.warn('No replacement set up for site: ' + process.env.SITE);
if (process.env.SITE) {
console.warn('No replacement set up for site: ' + process.env.SITE);
}
}
manifest.passVersionFromPackage(path.resolve(settings.rootDir, 'package.json'));
manifest.saveTo(path.resolve(settings.exportDir, 'manifest.json'));
const iconsDirectory = path.resolve(settings.exportDir, 'icons');
switch (process.env.SITE) {
case "derpibooru":
case "tantabus":
const siteIconsDirectory = path.resolve(iconsDirectory, process.env.SITE);
if (!fs.existsSync(siteIconsDirectory)) {
console.warn(`Can't find replacement icons for site ${process.env.SITE}`);
break;
}
console.log(`Found replacement icons for ${process.env.SITE}, swapping them...`);
fs.readdirSync(siteIconsDirectory).forEach(fileName => {
const originalIconPath = path.resolve(settings.exportDir, fileName);
const replacementIconPath = path.resolve(siteIconsDirectory, fileName);
if (!fs.existsSync(originalIconPath)) {
console.warn(`Original icon not found: ${originalIconPath}`)
return;
}
fs.rmSync(originalIconPath);
fs.cpSync(replacementIconPath, originalIconPath);
console.log(`Replaced: ${path.relative(settings.rootDir, replacementIconPath)}${path.relative(settings.rootDir, originalIconPath)}`);
});
break;
}
if (fs.existsSync(iconsDirectory)) {
console.log('Cleaning up icon replacements directory');
fs.rmSync(iconsDirectory, {
recursive: true,
force: true,
});
}
extractInlineScriptsFromIndex(path.resolve(settings.exportDir, 'index.html'));
}

View File

@@ -9,16 +9,21 @@ tags to them without opening each individual image.
This extension is available for both Chromium- and Firefox-based browsers. You can find the links to the extension pages
below.
### Furbooru Tagging Assistant
### Furbooru
[![Get the Add-on on Firefox](.github/assets/firefox.png)](https://addons.mozilla.org/en-US/firefox/addon/furbooru-tagging-assistant/)
[![Get the extension on Chrome](.github/assets/chrome.png)](https://chromewebstore.google.com/detail/kpgaphaooaaodgodmnkamhmoedjcnfkj)
### Derpibooru Tagging Assistant
### Derpibooru
[![Get the Add-on on Firefox](.github/assets/firefox.png)](https://addons.mozilla.org/en-US/firefox/addon/derpibooru-tagging-assistant/)
[![Get the extension on Chrome](.github/assets/chrome.png)](https://chromewebstore.google.com/detail/pnmbomcdbfcghgmegklfofncfigdielb)
### Tantabus
[![Get the Add-on on Firefox](.github/assets/firefox.png)](https://addons.mozilla.org/en-US/firefox/addon/tantabus-tagging-assistant/)
[![Get the extension on Chrome](.github/assets/chrome.png)](https://chromewebstore.google.com/detail/jpfkohpgdnpabpjafgagonghknaiecih)
## Features
### Tagging Profiles

View File

@@ -1,7 +1,7 @@
{
"name": "Furbooru Tagging Assistant",
"description": "Small experimental extension for slightly quicker tagging experience. Furbooru Edition.",
"version": "0.6.1",
"version": "0.7.0",
"browser_specific_settings": {
"gecko": {
"id": "furbooru-tagging-assistant@thecore.city",
@@ -50,11 +50,16 @@
"matches": [
"*://*.furbooru.org/images/*"
],
"exclude_matches": [
"*://*.furbooru.org/images/new",
"*://*.furbooru.org/images/new?*"
],
"js": [
"src/content/tags-editor.ts"
],
"css": [
"src/styles/content/tags-editor.scss"
"src/styles/content/tags-editor.scss",
"src/styles/content/tag-presets.scss"
]
},
{
@@ -87,6 +92,17 @@
"css": [
"src/styles/content/posts.scss"
]
},
{
"matches": [
"*://*.furbooru.org/images/new"
],
"js": [
"src/content/upload.ts"
],
"css": [
"src/styles/content/tag-presets.scss"
]
}
],
"action": {

786
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "furbooru-tagging-assistant",
"version": "0.6.1",
"version": "0.7.0",
"private": true,
"type": "module",
"scripts": {
@@ -16,24 +16,24 @@
},
"dependencies": {
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.53.0",
"@sveltejs/kit": "^2.55.0",
"@fortawesome/fontawesome-free": "^7.2.0",
"amd-lite": "^1.0.1",
"lz-string": "^1.5.0",
"sass": "^1.97.3",
"svelte": "^5.53.3"
"sass": "^1.98.0",
"svelte": "^5.53.12"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@types/chrome": "^0.1.37",
"@types/node": "^25.3.0",
"@vitest/coverage-v8": "^4.0.18",
"@types/node": "^25.5.0",
"@vitest/coverage-v8": "^4.1.0",
"cheerio": "^1.2.0",
"cross-env": "^10.1.0",
"jsdom": "^28.1.0",
"svelte-check": "^4.4.3",
"svelte-check": "^4.4.5",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vitest": "^4.0.18"
"vitest": "^4.1.0"
}
}

4
src/app.d.ts vendored
View File

@@ -1,7 +1,8 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
import TaggingProfile from "$entities/TaggingProfile";
import type TaggingProfile from "$entities/TaggingProfile";
import type TagGroup from "$entities/TagGroup";
import type TagEditorPreset from "$entities/TagEditorPreset";
declare global {
/**
@@ -39,6 +40,7 @@ declare global {
interface EntityNamesMap {
profiles: TaggingProfile;
groups: TagGroup;
presets: TagEditorPreset;
}
interface ImageURIs {

57
src/assets/icon/README.md Normal file
View File

@@ -0,0 +1,57 @@
# Extension Icon
This folder contains original resources used to make an icon for the extension. Since I'm not really an icon designer, I
ended up just composing the icon from sites logos + the shorthand name of the extension with fancy font. Nothing
special.
## Sources
All resources used for composing an icon are stored here as copies just to not lose anything. Original assets are
sourced from the following places:
- [Derpibooru Logo](https://github.com/derpibooru/philomena/blob/40ffb1b75bd0d96db24fa7c84bce36fcb7f2935f/assets/static/favicon.svg)
- [Furbooru Logo](https://github.com/furbooru/philomena/blob/cbfde406de34734403c06952bcaca51db6df1390/assets/static/favicon.svg)
- [Tantabus Logo](https://github.com/tantabus-ai/philomena/blob/285a7666ae4be46ac4da36bbc9ac8fda9e5c0fc3/assets/static/favicon.svg)
- [RoundFeather Font](https://drive.google.com/file/d/18ggNplAZNYtO4eNtMUpv3XpkeOAxSkxm/view?usp=sharing)
- Made by [allorus162](https://bsky.app/profile/allorus162.bsky.social)
- [Original Bluesky post](https://bsky.app/profile/allorus162.bsky.social/post/3mfqntff4j22i)
## Rendering
**Note:** You don't need to do anything to pack current version of icon to the extension. All icons are already pre-rendered and
placed into the `static` directory.
For now, any change to the icons will require manual re-rendering of PNG versions of the logos used when packing
extension for the release. All you need is to open `/src/assets/icon/icon.svg` in software like Inskape, hide the
currently opened logo and toggle the required one and save it into `icon256.png`, `icon128.png`, `icon48.png` and
`icon16.png`.
For the font on the bottom-right to work, you will need to install it from the file
`src/assets/icon/fonts/roundfeather-regular-1.001.ttf` (or you can download and install it from the source link).
You should render them into `/static` directory in the following structure:
- Place Furbooru icons into `/static` directory
- Then add same icons for Derpibooru and Tantabus into `/static/icons/depribooru` and `/static/icons/tantabus`
respectively.
Resulting structure will look like this:
```
static/
icons/
derpibooru/
icon16.png
icon48.png
icon128.png
icon256.png
tantabus/
icon16.png
icon48.png
icon128.png
icon256.png
icon16.png
icon48.png
icon128.png
icon256.png
```

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="132.29mm" height="132.29mm" version="1.1" viewBox="0 0 132.29 132.29" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(1.0427 0 0 1.0427 -.017147 -1.1711)"><path d="m103.79 1.5c2.226 55.884-2.592 66.082-26.774 81.11-10.46 6.495-24.44-7.2571-36.56-5.1501-14.529 2.53-33.119 12.655-39.169 20.603l0.00607-0.0012c56.82 1.164 44.414 29.924 80.7 29.64 21.39-0.17 35.89-18.483 38.62-27.615 10.456-34.942 3.1379-65.073-16.823-98.587zm11.375 57.714c-1.6412 10.692-0.54281 11.478 6.66 16.938-8.649-2.0114-10.977 0.7243-14.702 8.2179 1.5999-8.243 0.4741-11.62-5.8526-15.099 8.3149 0.88078 9.4155 0.05081 13.895-10.057zm-34.884 33.692c-3.7887 11.898-1.8578 13.462 6.4355 18.092-12.032-2.4927-11.44 1.5364-16.965 7.6122 2.9076-9.5873 1.2336-12.084-5.6426-16.122 5.6959 0.20558 11.418 1.8392 16.172-9.5819z" style="fill:#73d6ed"/><path d="m69.863 33.123-42.793 78.941 5.4648 2.9629 42.791-78.943z" style="-inkscape-stroke:none;color:#000000;fill:#73d6ed"/><g style="fill:#73d6ed"><path d="m64.894 48.218 7.18-13.796" style="-inkscape-stroke:none;color:#000000;fill:#73d6ed;stroke-width:6.557"/></g><path d="m89.844 3.2499-14.504 13.697-16.245-10.04 7.31 17.535-16.03 14.152 21.282-2.9352 8.782 19.4 2.8299-22.61 19.26-1.614-17.67-8.8749zm-7.235 13.028-1.5585 8.3965 7.2681 3.9461-8.1864 1.1558-1.6952 9.6819-3.8619-8.8428-9.5827 1.4895 7.3746-6.5192-3.4781-7.6725 7.4099 4.1481z" style="fill:#73d6ed"/></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="132.29mm" height="132.29mm" version="1.1" viewBox="0 0 132.29 132.29" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.13264 0 0 .13264 -.54013 .21654)"><path d="m396.42 189.81c-51.874-0.0769-99.544 21.731-131.29 68.784 3.6141-17.709 12.731-35.245 20.313-51.85-64.951 23.01-110.98 58.779-153.95 130.96 31.861 78.452 47.007 130.73 32.991 180.63-12.789 45.531-57.874 81.129-120.66 54.031 11.733 79.157 138.9 169.44 265.13 95.148-15.402 34.788-33.651 44.104-58.67 59.609 109.87 18.28 240.46-16.733 327.04-89.364 80.473-67.509 145.76-176.62 150.49-271.1-16.925 5.2436-48.396 10.423-61.314 8.6977-4.9416-13.958-6.5445-47.664-6.6378-65.502-78.741 1.4559-134.21-82.398-225.29-56.079 24.483-19.73 56.853-19.667 84.992-25.658-39.916-24.957-82.805-38.256-123.15-38.315zm46.019 201.23c33.445 0.29408 70.846 13.949 108.14 44.486-71.827-12.193-167.68 9.8684-228.78 100.49-3.4843-86.066 49.678-145.6 120.65-144.98z" style="fill:#9a91d9"/><path d="m70.559 99.836c-11.606 103.12 31.76 196.05 73.743 312.81 47.028 130.79-54.846 190.58-100.97 125.58-46.123-65.005-59.672-280.22 27.226-438.39z" style="fill:#9a91d9"/><path d="m126.15 320.07s-84.457-177.47-13.898-310.03c12.18 95.304 37.741 170.41 85.526 228.25-34.102 31.125-51.147 53.357-71.628 81.784z" style="fill:#9a91d9"/><path d="m54.523 683.55c36.454 189.97 245.77 300.87 372.04 295.07-11.047-9.7005-22.094-18.617-33.141-31.003 95.291 43.85 187.43 17.122 251.23-12.829-16.164-4.6041-32.272-9.3803-47.039-18.174 21.351-4.409 43.671-15.588 59.868-33.141-52.566-1.4772-102.82-10.573-151.86-54.928 57.575-28.86 90.002-66.925 102.7-97.767-13.158 6.0202-27.475 9.3163-40.636 10.507 12.007-23.538 20.064-48.835 23.52-78.043-232.6 178.14-441.6 75.628-536.68 20.312z" style="fill:#9a91d9"/><path d="m611.51 653.89c-3.4653 21.491-9.3328 46.627-17.472 65.294 25.751-12.728 37.33-30.294 47.406-48.456 0 0-10.691 113.32-106.91 158.22 0 0 57.784 50.56 163.62 32.385-12.482 20.32-26.396 37.375-64.404 55.584 57.955 10.976 153.12-24.053 185.6-80.951-28.742 9.1492-48.987 9.8028-69.933 11.156 160.72-63.788 92.562-249.91 248.03-342.1-61.514-30.693-156.71-52.064-253.37 42.763 9.4346-13.824 22.374-34.745 43.832-56.661-72.65 21.206-114.21 112.97-176.4 162.77z" style="fill:#9a91d9"/><path d="m676.96 308.22c-2.2133 13.699-3.9692 34.942 1.8899 51.493 15.962 2.1315 36.687-1.0078 49.243-8.4218-0.26672-23.09-4.5591-41.009-10.829-57.596-12.902 6.3961-22.273 10.895-40.304 14.525z" style="fill:#9a91d9"/></g></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="250mm"
height="250mm"
version="1.1"
viewBox="0 0 250 250"
xml:space="preserve"
id="svg10"
sodipodi:docname="favicon.svg"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs14" /><sodipodi:namedview
id="namedview12"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.46847003"
inkscape:cx="662.79587"
inkscape:cy="691.61308"
inkscape:window-width="2048"
inkscape:window-height="1403"
inkscape:window-x="1966"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg10" /><path
id="circle4"
style="fill:#b189d7;stroke-width:1.0041"
d="M 43.466348 113.64503 A 112.33 112.33 0 0 1 40.650026 88.648915 A 112.33 112.33 0 0 1 40.918168 81.481475 A 81.915001 81.915001 0 0 0 121.44996 148.71379 A 81.915001 81.915001 0 0 0 203.36481 66.799192 A 81.915001 81.915001 0 0 0 121.45008 -15.116154 A 81.915001 81.915001 0 0 0 107.06707 -13.789539 A 112.33 112.33 0 0 1 152.97993 -23.681174 A 112.33 112.33 0 0 1 265.31002 88.648731 A 112.33 112.33 0 0 1 152.98012 200.97882 A 112.33 112.33 0 0 1 43.466348 113.64503 z "
transform="matrix(.25882 .96593 .96593 -.25882 0 0)" /><path
d="m120.78 137.49c-13.186 22.457-39.753 18.697-46.615-10.102-3.3594-14.101 17.903-33.046 13.75-51.609-2.9261-13.079 16.12-43.432 15.115-40.727-5.9218 15.937-6.5312 30.238 1.25 42.264 16.946 26.186 24.595 46.387 16.499 60.175z"
style="fill:#b189d7;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.0644"
id="path8" /></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

115
src/assets/icon/icon.svg Normal file
View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="500"
height="500"
version="1.1"
viewBox="0 0 132.29167 132.29167"
xml:space="preserve"
id="svg6"
sodipodi:docname="icon.svg"
inkscape:version="1.4.3 (0d15f75, 2025-12-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs6" /><sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1"
inkscape:cx="179"
inkscape:cy="388.49999"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg6"
showguides="false" /><g
id="g19"
transform="matrix(0.58884997,0,0,0.58884997,-7.6052124,-7.356684)"
inkscape:label="tantabus"
style="display:none"><path
id="circle4"
style="fill:#b189d7;stroke-width:1.0041"
d="m 43.466348,113.64503 a 112.33,112.33 0 0 1 -2.816322,-24.996115 112.33,112.33 0 0 1 0.268142,-7.16744 81.915001,81.915001 0 0 0 80.531792,67.232315 81.915001,81.915001 0 0 0 81.91485,-81.914598 81.915001,81.915001 0 0 0 -81.91473,-81.915346 81.915001,81.915001 0 0 0 -14.38301,1.326615 112.33,112.33 0 0 1 45.91286,-9.891635 A 112.33,112.33 0 0 1 265.31002,88.648731 112.33,112.33 0 0 1 152.98012,200.97882 112.33,112.33 0 0 1 43.466348,113.64503 Z"
transform="matrix(0.25882,0.96593,0.96593,-0.25882,0,0)" /><path
d="M 120.78,137.49 C 107.594,159.947 81.027,156.187 74.165,127.388 70.8056,113.287 92.068,94.342 87.915,75.779 84.9889,62.7 104.035,32.347 103.03,35.052 c -5.9218,15.937 -6.5312,30.238 1.25,42.264 16.946,26.186 24.595,46.387 16.499,60.175 z"
style="fill:#b189d7;stroke-width:2.0644;stroke-linecap:round;stroke-linejoin:round"
id="path8" /></g><g
transform="matrix(1.048236,0,0,1.048236,-0.23527111,-1.5720411)"
id="g4"
inkscape:label="derpibooru"
style="display:none"><path
d="m 103.79,1.5 c 2.226,55.884 -2.592,66.082 -26.774,81.11 -10.46,6.495 -24.44,-7.2571 -36.56,-5.1501 -14.529,2.53 -33.119,12.655 -39.169,20.603 l 0.00607,-0.0012 c 56.82,1.164 44.414,29.924 80.7,29.64 21.39,-0.17 35.89,-18.483 38.62,-27.615 10.456,-34.942 3.1379,-65.073 -16.823,-98.587 z m 11.375,57.714 c -1.6412,10.692 -0.54281,11.478 6.66,16.938 -8.649,-2.0114 -10.977,0.7243 -14.702,8.2179 1.5999,-8.243 0.4741,-11.62 -5.8526,-15.099 8.3149,0.88078 9.4155,0.05081 13.895,-10.057 z M 80.281,92.906 c -3.7887,11.898 -1.8578,13.462 6.4355,18.092 -12.032,-2.4927 -11.44,1.5364 -16.965,7.6122 2.9076,-9.5873 1.2336,-12.084 -5.6426,-16.122 5.6959,0.20558 11.418,1.8392 16.172,-9.5819 z"
style="fill:#73d6ed"
id="path1-0" /><path
d="m 69.863,33.123 -42.793,78.941 5.4648,2.9629 42.791,-78.943 z"
style="color:#000000;fill:#73d6ed;-inkscape-stroke:none"
id="path2-9" /><g
style="fill:#73d6ed"
id="g3"><path
d="m 64.894,48.218 7.18,-13.796"
style="color:#000000;fill:#73d6ed;stroke-width:6.557;-inkscape-stroke:none"
id="path3-4" /></g><path
d="m 89.844,3.2499 -14.504,13.697 -16.245,-10.04 7.31,17.535 -16.03,14.152 21.282,-2.9352 8.782,19.4 2.8299,-22.61 19.26,-1.614 -17.67,-8.8749 z m -7.235,13.028 -1.5585,8.3965 7.2681,3.9461 -8.1864,1.1558 -1.6952,9.6819 -3.8619,-8.8428 -9.5827,1.4895 7.3746,-6.5192 -3.4781,-7.6725 7.4099,4.1481 z"
style="fill:#73d6ed"
id="path4-8" /></g><g
transform="matrix(0.13355808,0,0,0.13355808,-0.92543932,0.1095915)"
id="g6"
inkscape:label="furbooru"
style="display:inline"><path
d="m 396.42,189.81 c -51.874,-0.0769 -99.544,21.731 -131.29,68.784 3.6141,-17.709 12.731,-35.245 20.313,-51.85 -64.951,23.01 -110.98,58.779 -153.95,130.96 31.861,78.452 47.007,130.73 32.991,180.63 -12.789,45.531 -57.874,81.129 -120.66,54.031 11.733,79.157 138.9,169.44 265.13,95.148 -15.402,34.788 -33.651,44.104 -58.67,59.609 109.87,18.28 240.46,-16.733 327.04,-89.364 80.473,-67.509 145.76,-176.62 150.49,-271.1 -16.925,5.2436 -48.396,10.423 -61.314,8.6977 -4.9416,-13.958 -6.5445,-47.664 -6.6378,-65.502 -78.741,1.4559 -134.21,-82.398 -225.29,-56.079 24.483,-19.73 56.853,-19.667 84.992,-25.658 -39.916,-24.957 -82.805,-38.256 -123.15,-38.315 z m 46.019,201.23 c 33.445,0.29408 70.846,13.949 108.14,44.486 -71.827,-12.193 -167.68,9.8684 -228.78,100.49 -3.4843,-86.066 49.678,-145.6 120.65,-144.98 z"
style="fill:#9a91d9"
id="path1"
inkscape:label="head" /><path
d="m 70.559,99.836 c -11.606,103.12 31.76,196.05 73.743,312.81 47.028,130.79 -54.846,190.58 -100.97,125.58 C -2.791,473.221 -16.34,258.006 70.558,99.836 Z"
style="fill:#9a91d9"
id="path2"
inkscape:label="rightear" /><path
d="m 126.15,320.07 c 0,0 -84.457,-177.47 -13.898,-310.03 12.18,95.304 37.741,170.41 85.526,228.25 -34.102,31.125 -51.147,53.357 -71.628,81.784 z"
style="fill:#9a91d9"
id="path3"
inkscape:label="leftear" /><path
d="m 54.523,683.55 c 36.454,189.97 245.77,300.87 372.04,295.07 -11.047,-9.7005 -22.094,-18.617 -33.141,-31.003 95.291,43.85 187.43,17.122 251.23,-12.829 -16.164,-4.6041 -32.272,-9.3803 -47.039,-18.174 21.351,-4.409 43.671,-15.588 59.868,-33.141 -52.566,-1.4772 -102.82,-10.573 -151.86,-54.928 57.575,-28.86 90.002,-66.925 102.7,-97.767 -13.158,6.0202 -27.475,9.3163 -40.636,10.507 12.007,-23.538 20.064,-48.835 23.52,-78.043 -232.6,178.14 -441.6,75.628 -536.68,20.312 z"
style="fill:#9a91d9"
id="path4"
inkscape:label="tail" /><path
d="m 611.51,653.89 c -3.4653,21.491 -9.3328,46.627 -17.472,65.294 25.751,-12.728 37.33,-30.294 47.406,-48.456 0,0 -10.691,113.32 -106.91,158.22 0,0 57.784,50.56 163.62,32.385 -12.482,20.32 -26.396,37.375 -64.404,55.584 57.955,10.976 153.12,-24.053 185.6,-80.951 -28.742,9.1492 -48.987,9.8028 -69.933,11.156 160.72,-63.788 92.562,-249.91 248.03,-342.1 -61.514,-30.693 -156.71,-52.064 -253.37,42.763 9.4346,-13.824 22.374,-34.745 43.832,-56.661 -72.65,21.206 -114.21,112.97 -176.4,162.77 z"
style="fill:#9a91d9"
id="path5"
inkscape:label="tailend" /><path
d="m 676.96,308.22 c -2.2133,13.699 -3.9692,34.942 1.8899,51.493 15.962,2.1315 36.687,-1.0078 49.243,-8.4218 -0.26672,-23.09 -4.5591,-41.009 -10.829,-57.596 -12.902,6.3961 -22.273,10.895 -40.304,14.525 z"
style="fill:#9a91d9"
id="path6"
inkscape:label="nose" /></g><g
id="g18"
inkscape:label="badge"
style="display:inline"
transform="matrix(0.9615385,0,0,0.9615385,66.145836,5.0881347)"><rect
style="opacity:1;fill:#1b3c21;fill-opacity:1;stroke:none;stroke-width:1.42664;stroke-linecap:round;stroke-linejoin:round"
id="rect6"
width="68.791664"
height="44.450001"
x="-2.5431316e-06"
y="87.841667"
ry="5.2916665"
inkscape:label="bg" /><text
xml:space="preserve"
style="font-size:34.0036px;line-height:0.85;font-family:sans-serif;text-align:center;text-anchor:middle;fill:#4aa158;fill-opacity:1;stroke-width:0.261214"
x="34.024727"
y="128.18593"
id="text11"
transform="scale(1.0109068,0.98921086)"
inkscape:label="text"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:34.0036px;line-height:0.85;font-family:Roundfeather;-inkscape-font-specification:Roundfeather;fill:#4aa158;fill-opacity:1;stroke-width:0.261214"
x="34.024727"
y="128.18593"
id="tspan12"
sodipodi:role="line">PTA</tspan></text></g></svg>

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -1,6 +1,8 @@
<script lang="ts">
import TagsColorContainer from "$components/tags/TagsColorContainer.svelte";
import type TagGroup from "$entities/TagGroup";
import DetailsBlock from "$components/ui/DetailsBlock.svelte";
import TagsList from "$components/tags/TagsList.svelte";
interface GroupViewProps {
group: TagGroup;
@@ -14,60 +16,27 @@
</script>
<div class="block">
<strong>Group Name:</strong>
<div>{group.settings.name}</div>
</div>
<DetailsBlock title="Group Name">
{group.settings.name}
</DetailsBlock>
{#if sortedTagsList.length}
<div class="block">
<strong>Tags:</strong>
<DetailsBlock title="Tags">
<TagsColorContainer targetCategory={group.settings.category}>
<div class="tags-list">
{#each sortedTagsList as tagName}
<span class="tag">{tagName}</span>
{/each}
</div>
<TagsList tags={sortedTagsList} />
</TagsColorContainer>
</div>
</DetailsBlock>
{/if}
{#if sortedPrefixes.length}
<div class="block">
<strong>Prefixes:</strong>
<DetailsBlock title="Prefixes">
<TagsColorContainer targetCategory={group.settings.category}>
<div class="tags-list">
{#each sortedPrefixes as prefixName}
<span class="tag">{prefixName}*</span>
{/each}
</div>
<TagsList tags={sortedPrefixes} append="*" />
</TagsColorContainer>
</div>
</DetailsBlock>
{/if}
{#if sortedSuffixes.length}
<div class="block">
<strong>Suffixes:</strong>
<DetailsBlock title="Suffixes">
<TagsColorContainer targetCategory={group.settings.category}>
<div class="tags-list">
{#each sortedSuffixes as suffixName}
<span class="tag">*{suffixName}</span>
{/each}
</div>
<TagsList tags={sortedSuffixes} prepend="*" />
</TagsColorContainer>
</div>
</DetailsBlock>
{/if}
<style lang="scss">
.tags-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.block + .block {
margin-top: .5em;
strong {
display: block;
margin-bottom: .25em;
}
}
</style>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type TagEditorPreset from "$entities/TagEditorPreset";
import DetailsBlock from "$components/ui/DetailsBlock.svelte";
import TagsList from "$components/tags/TagsList.svelte";
interface PresetViewProps {
preset: TagEditorPreset;
}
let { preset }: PresetViewProps = $props();
const sortedTagsList = $derived(preset.settings.tags.toSorted((a, b) => a.localeCompare(b)));
</script>
<DetailsBlock title="Preset Name">
{preset.settings.name}
</DetailsBlock>
<DetailsBlock title="Tags">
<TagsList tags={sortedTagsList}></TagsList>
</DetailsBlock>

View File

@@ -1,5 +1,7 @@
<script lang="ts">
import type TaggingProfile from "$entities/TaggingProfile";
import DetailsBlock from "$components/ui/DetailsBlock.svelte";
import TagsList from "$components/tags/TagsList.svelte";
interface ProfileViewProps {
profile: TaggingProfile;
@@ -10,32 +12,9 @@
const sortedTagsList = $derived(profile.settings.tags.sort((a, b) => a.localeCompare(b)));
</script>
<div class="block">
<strong>Profile:</strong>
<div>{profile.settings.name}</div>
</div>
<div class="block">
<strong>Tags:</strong>
<div class="tags-list">
{#each sortedTagsList as tagName}
<span class="tag">{tagName}</span>
{/each}
</div>
</div>
<style lang="scss">
.tags-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.block + .block {
margin-top: .5em;
strong {
display: block;
margin-bottom: .25em;
}
}
</style>
<DetailsBlock title="Profile">
{profile.settings.name}
</DetailsBlock>
<DetailsBlock title="Tags">
<TagsList tags={sortedTagsList} />
</DetailsBlock>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
interface TagsListProps {
tags: string[];
prepend?: string;
append?: string;
}
let { tags, prepend, append }: TagsListProps = $props();
</script>
<div class="tags-list">
{#each tags as tagName}
<div class="tag">{prepend || ''}{tagName}{append || ''}</div>
{/each}
</div>
<style lang="scss">
.tags-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
</style>

View File

@@ -0,0 +1,30 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface DetailsBlockProps {
title?: string;
children?: Snippet;
}
let { title, children }: DetailsBlockProps = $props();
</script>
<div class="block">
{#if title?.length}
<strong>{title}:</strong>
{/if}
<div>
{@render children?.()}
</div>
</div>
<style lang="scss">
.block strong {
display: block;
margin-bottom: .25em;
}
.block + :global(.block) {
margin-top: .5em;
}
</style>

View File

@@ -1,5 +1,15 @@
export const EVENT_FETCH_COMPLETE = 'fetchcomplete';
export const EVENT_RELOAD = 'reload';
/**
* Custom data for the reload event on plain editor textarea. Philomena doesn't send anything on this event.
*/
export interface ReloadCustomOptions {
skipTagColorRefresh?: boolean;
skipTagRefresh?: boolean;
}
export interface BooruEventsMap {
[EVENT_FETCH_COMPLETE]: null; // Site sends the response, but extension will not get it due to isolation.
[EVENT_RELOAD]: ReloadCustomOptions|null;
}

View File

@@ -4,13 +4,15 @@ import type { FullscreenViewerEventsMap } from "$content/components/events/fulls
import type { BooruEventsMap } from "$content/components/events/booru-events";
import type { TagsFormEventsMap } from "$content/components/events/tags-form-events";
import type { TagDropdownEvents } from "$content/components/events/tag-dropdown-events";
import type { PresetBlockEventsMap } from "$content/components/events/preset-block-events";
type EventsMapping =
MaintenancePopupEventsMap
& FullscreenViewerEventsMap
& BooruEventsMap
& TagsFormEventsMap
& TagDropdownEvents;
& TagDropdownEvents
& PresetBlockEventsMap;
type EventCallback<EventDetails> = (event: CustomEvent<EventDetails>) => void;
export type UnsubscribeFunction = () => void;

View File

@@ -0,0 +1,10 @@
export const EVENT_PRESET_TAG_CHANGE_APPLIED = 'preset-tag-changed';
export interface PresetTagChange {
addedTags?: Set<string>;
removedTags?: Set<string>;
}
export interface PresetBlockEventsMap {
[EVENT_PRESET_TAG_CHANGE_APPLIED]: PresetTagChange;
}

View File

@@ -0,0 +1,102 @@
import { BaseComponent } from "$content/components/base/BaseComponent";
import TagEditorPreset from "$entities/TagEditorPreset";
import PresetTableRow from "$content/components/extension/presets/PresetTableRow";
import { createFontAwesomeIcon } from "$lib/dom-utils";
import { sortEntitiesByField } from "$lib/utils";
export default class EditorPresetsBlock extends BaseComponent {
#presetsTable = document.createElement('table');
#presetBlocks: PresetTableRow[] = [];
#tags: Set<string> = new Set();
protected build() {
this.container.classList.add('block', 'hidden', 'tag-presets');
this.container.style.marginTop = 'var(--block-spacing)';
const header = document.createElement('div');
header.classList.add('block__header');
const headerTitle = document.createElement('div');
headerTitle.classList.add('block__header__title')
headerTitle.textContent = ' Presets';
const content = document.createElement('div');
content.classList.add('block__content');
this.#presetsTable.append(
document.createElement('thead'),
document.createElement('tbody'),
);
this.#presetsTable.tHead?.append(
EditorPresetsBlock.#createRowWithTableHeads([
'Name',
'Tags',
'Actions'
]),
);
headerTitle.prepend(createFontAwesomeIcon('layer-group'));
header.append(headerTitle);
content.append(this.#presetsTable);
this.container.append(
header,
content,
);
}
protected init() {
TagEditorPreset.readAll()
.then(this.#refreshPresets.bind(this))
.then(() => TagEditorPreset.subscribe(this.#refreshPresets.bind(this)));
}
toggleVisibility(shouldBeVisible: boolean | undefined = undefined) {
this.container.classList.toggle('hidden', shouldBeVisible);
}
updateTags(tags: Set<string>) {
this.#tags = tags;
for (const presetBlock of this.#presetBlocks) {
presetBlock.updateTags(tags);
}
}
#refreshPresets(presetsList: TagEditorPreset[]) {
if (this.#presetBlocks.length) {
for (const block of this.#presetBlocks) {
block.remove();
}
}
for (const preset of sortEntitiesByField(presetsList, "name")) {
const block = PresetTableRow.create(preset);
this.#presetsTable.tBodies[0]?.append(block.container);
block.initialize();
block.updateTags(this.#tags);
this.#presetBlocks.push(block);
}
}
static create(): EditorPresetsBlock {
return new EditorPresetsBlock(
document.createElement('div')
);
}
static #createRowWithTableHeads(columnNames: string[]): HTMLTableRowElement {
const rowElement = document.createElement('tr');
for (const columnName of columnNames) {
const columnHeadElement = document.createElement('th');
columnHeadElement.textContent = columnName;
rowElement.append(columnHeadElement);
}
return rowElement;
}
}

View File

@@ -0,0 +1,129 @@
import { BaseComponent } from "$content/components/base/BaseComponent";
import type TagEditorPreset from "$entities/TagEditorPreset";
import { emit } from "$content/components/events/comms";
import { EVENT_PRESET_TAG_CHANGE_APPLIED } from "$content/components/events/preset-block-events";
import { createFontAwesomeIcon } from "$lib/dom-utils";
export default class PresetTableRow extends BaseComponent {
#preset: TagEditorPreset;
#tagsList: HTMLElement[] = [];
#applyAllButton = document.createElement('button');
#removeAllButton = document.createElement('button');
constructor(container: HTMLElement, preset: TagEditorPreset) {
super(container);
this.#preset = preset;
}
protected build() {
this.#tagsList = this.#preset.settings.tags
.toSorted((a, b) => a.localeCompare(b))
.map(tagName => {
const tagElement = document.createElement('span');
tagElement.classList.add('tag');
tagElement.textContent = tagName;
tagElement.dataset.tagName = tagName;
return tagElement;
});
const nameCell = document.createElement('td');
nameCell.textContent = this.#preset.settings.name;
const tagsCell = document.createElement('td');
const tagsListContainer = document.createElement('div');
tagsListContainer.classList.add('tag-list');
tagsListContainer.append(...this.#tagsList);
tagsCell.append(tagsListContainer);
const actionsCell = document.createElement('td');
const actionsContainer = document.createElement('div');
actionsContainer.classList.add('flex', 'flex--gap-small');
this.#applyAllButton.classList.add('button', 'button--state-success', 'button--bold');
this.#applyAllButton.append(createFontAwesomeIcon('circle-plus'));
this.#applyAllButton.title = 'Add all tags from this preset into the editor';
this.#removeAllButton.classList.add('button', 'button--state-danger', 'button--bold');
this.#removeAllButton.append(createFontAwesomeIcon('circle-minus'));
this.#removeAllButton.title = 'Remove all tags from this preset from the editor';
actionsContainer.append(
this.#applyAllButton,
this.#removeAllButton,
);
actionsCell.append(actionsContainer);
this.container.append(
nameCell,
tagsCell,
actionsCell,
);
}
protected init() {
for (const tagElement of this.#tagsList) {
tagElement.addEventListener('click', this.#onTagClicked.bind(this));
}
this.#applyAllButton.addEventListener('click', this.#onApplyAllClicked.bind(this));
this.#removeAllButton.addEventListener('click', this.#onRemoveAllClicked.bind(this));
}
#onTagClicked(event: Event) {
const targetElement = event.currentTarget;
if (!(targetElement instanceof HTMLElement)) {
return;
}
const tagName = targetElement.dataset.tagName;
const isMissing = targetElement.classList.contains(PresetTableRow.#tagMissingClassName);
emit(this, EVENT_PRESET_TAG_CHANGE_APPLIED, {
[isMissing ? 'addedTags' : 'removedTags']: new Set([tagName])
});
}
#onApplyAllClicked(event: Event) {
event.preventDefault();
event.stopPropagation();
emit(this, EVENT_PRESET_TAG_CHANGE_APPLIED, {
addedTags: new Set(this.#preset.settings.tags),
});
}
#onRemoveAllClicked(event: Event) {
event.preventDefault();
event.stopPropagation();
emit(this, EVENT_PRESET_TAG_CHANGE_APPLIED, {
removedTags: new Set(this.#preset.settings.tags),
});
}
updateTags(tags: Set<string>) {
for (const tagElement of this.#tagsList) {
tagElement.classList.toggle(
PresetTableRow.#tagMissingClassName,
!tags.has(tagElement.dataset.tagName || ''),
);
}
}
remove() {
this.container.remove();
}
static create(preset: TagEditorPreset) {
return new this(document.createElement('tr'), preset);
}
static #tagMissingClassName = 'is-missing';
}

View File

@@ -1,10 +1,36 @@
import { BaseComponent } from "$content/components/base/BaseComponent";
import { getComponent } from "$content/components/base/component-utils";
import { emit, on, type UnsubscribeFunction } from "$content/components/events/comms";
import { EVENT_FETCH_COMPLETE } from "$content/components/events/booru-events";
import { EVENT_FETCH_COMPLETE, EVENT_RELOAD, type ReloadCustomOptions } from "$content/components/events/booru-events";
import { EVENT_FORM_EDITOR_UPDATED } from "$content/components/events/tags-form-events";
import EditorPresetsBlock from "$content/components/extension/presets/EditorPresetsBlock";
import { EVENT_PRESET_TAG_CHANGE_APPLIED, type PresetTagChange } from "$content/components/events/preset-block-events";
export class TagsForm extends BaseComponent {
#togglePresetsButton: HTMLButtonElement = document.createElement('button');
#presetsList = EditorPresetsBlock.create();
#plainEditorTextarea: HTMLTextAreaElement|null = null;
#fancyEditorInput: HTMLInputElement|null = null;
#tagsSet: Set<string> = new Set();
protected build() {
this.#togglePresetsButton.classList.add(
'button',
'button--state-primary',
'button--bold',
'button--separate-left',
);
this.#togglePresetsButton.textContent = 'Presets';
this.container
.querySelector(':is(.fancy-tag-edit, .fancy-tag-upload) ~ button:last-of-type')
?.after(this.#togglePresetsButton, this.#presetsList.container);
this.#plainEditorTextarea = this.container.querySelector('textarea.tagsinput');
this.#fancyEditorInput = this.container.querySelector('.js-taginput-fancy input');
}
protected init() {
// Site sending the event when form is submitted vie Fetch API. We use this event to reload our logic here.
const unsubscribe = on(
@@ -12,6 +38,22 @@ export class TagsForm extends BaseComponent {
EVENT_FETCH_COMPLETE,
() => this.#waitAndDetectUpdatedForm(unsubscribe),
);
this.#togglePresetsButton.addEventListener('click', this.#togglePresetsList.bind(this));
this.#presetsList.initialize();
this.#plainEditorTextarea?.addEventListener('input', this.#refreshTagsListForPresets.bind(this));
this.#fancyEditorInput?.addEventListener('keydown', this.#refreshTagsListForPresets.bind(this));
this.#refreshTagsListForPresets();
on(this.#presetsList, EVENT_PRESET_TAG_CHANGE_APPLIED, this.#onTagChangeRequested.bind(this));
if (this.#plainEditorTextarea) {
// When reloaded, we should catch and refresh the colors. Extension reuses this event to force site to update
// list of tags in the fancy tag editor.
on(this.#plainEditorTextarea, EVENT_RELOAD, this.#onPlainEditorReloadRequested.bind(this));
}
}
#waitAndDetectUpdatedForm(unsubscribe: UnsubscribeFunction): void {
@@ -111,6 +153,95 @@ export class TagsForm extends BaseComponent {
return tagCategories;
}
#togglePresetsList(event: Event) {
event.stopPropagation();
event.preventDefault();
this.#presetsList.toggleVisibility();
this.#refreshTagsListForPresets();
}
#refreshTagsListForPresets() {
this.#tagsSet = new Set(
this.#plainEditorTextarea?.value
.split(',')
.map(tagName => tagName.trim())
);
this.#presetsList.updateTags(this.#tagsSet);
}
#onTagChangeRequested(event: CustomEvent<PresetTagChange>) {
const { addedTags = null, removedTags = null } = event.detail;
let tagChangeList: string[] = [];
if (addedTags) {
tagChangeList.push(...addedTags);
}
if (removedTags) {
tagChangeList.push(
...Array.from(removedTags)
.filter(tagName => this.#tagsSet.has(tagName))
.map(tagName => `-${tagName}`)
);
}
const offsetBeforeSubmission = this.#presetsList.container.offsetTop;
this.#applyTagChangesWithFancyTagEditor(
tagChangeList.join(',')
);
const offsetDifference = this.#presetsList.container.offsetTop - offsetBeforeSubmission;
// Compensating for the layout shift: when user clicks on a tag (or on "add/remove all tags"), tag editor might
// overflow the current line and wrap tags around to the next line, causing presets section to shift. We need to
// avoid that for better UX.
if (offsetDifference !== 0) {
window.scrollTo({
top: window.scrollY + offsetDifference,
behavior: 'instant',
});
}
}
#applyTagChangesWithFancyTagEditor(tagsListWithChanges: string): void {
if (!this.#fancyEditorInput || !this.#plainEditorTextarea) {
return;
}
const originalValue = this.#fancyEditorInput.value;
// We have to tell plain text editor to also refresh the list of tags in the fancy editor, just in case user
// made changes to it in plain mode.
emit(this.#plainEditorTextarea, EVENT_RELOAD, {
// Sending that we don't need to refresh the color on this event, since we will do that ourselves later, after
// changes are applied.
skipTagColorRefresh: true,
skipTagRefresh: true,
});
this.#fancyEditorInput.value = tagsListWithChanges;
this.#fancyEditorInput.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Comma',
}));
this.#fancyEditorInput.value = originalValue;
this.refreshTagColors();
}
#onPlainEditorReloadRequested(event: CustomEvent<ReloadCustomOptions|null>) {
if (!event.detail?.skipTagColorRefresh) {
this.refreshTagColors();
}
if (!event.detail?.skipTagRefresh) {
this.#refreshTagsListForPresets();
}
}
static watchForEditors() {
document.body.addEventListener('click', event => {
const targetElement = event.target;
@@ -147,4 +278,14 @@ export class TagsForm extends BaseComponent {
(tagEditor as TagsForm).refreshTagColors();
});
}
static initializeUploadEditor() {
const uploadEditorContainer = document.querySelector<HTMLElement>('.field:has(.fancy-tag-upload)');
if (!uploadEditorContainer) {
return;
}
new TagsForm(uploadEditorContainer).initialize();
}
}

3
src/content/upload.ts Normal file
View File

@@ -0,0 +1,3 @@
import { TagsForm } from "$content/components/philomena/TagsForm";
TagsForm.initializeUploadEditor();

11
src/lib/dom-utils.ts Normal file
View File

@@ -0,0 +1,11 @@
/**
* Reusable function to create icons from FontAwesome. Usable only for website, since extension doesn't host its own
* copy of FA styles. Extension should use imports of SVGs inside CSS instead.
* @param iconSlug Slug of the icon to be added.
* @return Element with classes for FontAwesome icon added.
*/
export function createFontAwesomeIcon(iconSlug: string): HTMLElement {
const iconElement = document.createElement('i');
iconElement.classList.add('fa-solid', `fa-${iconSlug}`);
return iconElement;
}

View File

@@ -4,6 +4,7 @@ import type { ImportableElementsList, ImportableEntityObject } from "$lib/extens
import EntitiesTransporter, { type SameSiteStatus } from "$lib/extension/EntitiesTransporter";
import TaggingProfile from "$entities/TaggingProfile";
import TagGroup from "$entities/TagGroup";
import TagEditorPreset from "$entities/TagEditorPreset";
type TransportersMapping = {
[EntityName in keyof App.EntityNamesMap]: EntitiesTransporter<App.EntityNamesMap[EntityName]>;
@@ -77,6 +78,8 @@ export default class BulkEntitiesTransporter {
return BulkEntitiesTransporter.#transporters.profiles.exportToObject(entity);
case entity instanceof TagGroup:
return BulkEntitiesTransporter.#transporters.groups.exportToObject(entity);
case entity instanceof TagEditorPreset:
return BulkEntitiesTransporter.#transporters.presets.exportToObject(entity);
}
return null;
@@ -101,6 +104,7 @@ export default class BulkEntitiesTransporter {
static #transporters: TransportersMapping = {
profiles: new EntitiesTransporter(TaggingProfile),
groups: new EntitiesTransporter(TagGroup),
presets: new EntitiesTransporter(TagEditorPreset),
}
/**

View File

@@ -0,0 +1,17 @@
import StorageEntity from "$lib/extension/base/StorageEntity";
interface TagEditorPresetSettings {
name: string;
tags: string[];
}
export default class TagEditorPreset extends StorageEntity<TagEditorPresetSettings> {
constructor(id: string, settings: Partial<TagEditorPresetSettings>) {
super(id, {
name: settings.name || '',
tags: settings.tags || [],
});
}
public static readonly _entityName = 'presets';
}

View File

@@ -33,6 +33,16 @@ const entitiesExporters: ExportersMap = {
category: entity.settings.category,
separate: entity.settings.separate,
}
},
presets: entity => {
return {
$type: "presets",
$site: __CURRENT_SITE__,
v: 1,
id: entity.id,
name: entity.settings.name,
tags: entity.settings.tags,
}
}
};

View File

@@ -64,6 +64,19 @@ const entitiesValidators: EntitiesValidationMap = {
throw new Error('Invalid group format detected!');
}
},
presets: importedObject => {
if (!importedObject.v || importedObject.v > 1) {
throw new Error('Unsupported preset version!');
}
if (
!validateRequiredString(importedObject?.id)
|| !validateRequiredString(importedObject?.name)
|| !validateOptionalArray(importedObject?.tags)
) {
throw new Error('Invalid preset format detected!');
}
}
};
/**

View File

@@ -1,3 +1,6 @@
import type StorageEntity from "$lib/extension/base/StorageEntity";
import type TagGroup from "$entities/TagGroup";
/**
* Traverse and find the object using the key path.
* @param targetObject Target object to traverse into.
@@ -39,3 +42,14 @@ export function escapeRegExp(value: string): string {
unsafeRegExpCharacters.lastIndex = 0;
return value.replace(unsafeRegExpCharacters, "\\$&");
}
type OnlyStringFields<Fields extends Record<string, any>> = {
[FieldKey in keyof Fields as Fields[FieldKey] extends string ? FieldKey : never]: string;
};
export function sortEntitiesByField<Fields extends Object>(entities: StorageEntity<Fields>[], fieldName: keyof OnlyStringFields<Fields>) {
return entities.toSorted(
(a, b) => (a.settings[fieldName] as string)
.localeCompare(b.settings[fieldName] as string)
);
}

View File

@@ -26,6 +26,7 @@
{/if}
<MenuItem href="/features/profiles">Tagging Profiles</MenuItem>
<MenuItem href="/features/groups">Tag Groups</MenuItem>
<MenuItem href="/features/presets">Tag Presets</MenuItem>
<hr>
<MenuItem href="/transporting">Import/Export</MenuItem>
<MenuItem href="/preferences">Preferences</MenuItem>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { tagEditorPresets } from "$stores/entities/tag-editor-presets";
import { sortEntitiesByField } from "$lib/utils";
let presets = $derived(sortEntitiesByField($tagEditorPresets, "name"))
</script>
<Menu>
<MenuItem href="/" icon="arrow-left">Back</MenuItem>
<MenuItem href="/features/presets/new/edit" icon="plus">Create New</MenuItem>
{#if presets.length}
<hr>
{#each presets as preset}
<MenuItem href="/features/presets/{preset.id}">{preset.settings.name}</MenuItem>
{/each}
{/if}
</Menu>

View File

@@ -0,0 +1,42 @@
<script lang="ts">
import { page } from "$app/state";
import TagEditorPreset from "$entities/TagEditorPreset";
import { tagEditorPresets } from "$stores/entities/tag-editor-presets";
import { goto } from "$app/navigation";
import { popupTitle } from "$stores/popup";
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import PresetView from "$components/features/PresetView.svelte";
let presetId = $derived(page.params.id);
let preset = $derived<TagEditorPreset|null>(
$tagEditorPresets.find(preset => preset.id === presetId) || null
);
$effect(() => {
if (presetId === 'new') {
goto(`/features/presets/new/edit`);
return;
}
if (!preset) {
console.warn(`Preset ${presetId} not found.`);
goto('/features/presets');
} else {
$popupTitle = `Preset: ${preset.settings.name}`;
}
});
</script>
<Menu>
<MenuItem href="/features/presets" icon="arrow-left">Back</MenuItem>
<hr>
</Menu>
{#if preset}
<PresetView {preset}></PresetView>
{/if}
<Menu>
<hr>
<MenuItem href="/features/presets/{presetId}/edit" icon="wrench">Edit Preset</MenuItem>
<MenuItem href="/features/presets/{presetId}/delete" icon="trash">Delete Preset</MenuItem>
</Menu>

View File

@@ -0,0 +1,49 @@
<script lang="ts">
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { page } from "$app/state";
import TagEditorPreset from "$entities/TagEditorPreset";
import { tagEditorPresets } from "$stores/entities/tag-editor-presets";
import { goto } from "$app/navigation";
import { popupTitle } from "$stores/popup";
const presetId = $derived(page.params.id);
const targetPreset = $derived<TagEditorPreset | null>(
$tagEditorPresets.find(preset => preset.id === presetId) || null
);
$effect(() => {
if (!targetPreset) {
goto('/features/presets');
} else {
$popupTitle = `Deleting Preset: ${targetPreset.settings.name}`
}
});
async function deletePreset() {
if (!targetPreset) {
console.warn('Attempting to delete the preset, but the preset is not loaded yet.');
return;
}
await targetPreset.delete();
await goto('/features/presets');
}
</script>
<Menu>
<MenuItem href="/features/presets/{presetId}" icon="arrow-left">Back</MenuItem>
<hr>
</Menu>
{#if targetPreset}
<p>
Do you want to remove preset "{targetPreset.settings.name}"? This action is irreversible.
</p>
<Menu>
<hr>
<MenuItem onclick={deletePreset}>Yes</MenuItem>
<MenuItem href="/features/presets/{presetId}">No</MenuItem>
</Menu>
{:else}
<p>Loading...</p>
{/if}

View File

@@ -0,0 +1,74 @@
<script lang="ts">
import { page } from "$app/state";
import TagEditorPreset from "$entities/TagEditorPreset";
import { tagEditorPresets } from "$stores/entities/tag-editor-presets";
import { popupTitle } from "$stores/popup";
import { goto } from "$app/navigation";
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 TextField from "$components/ui/forms/TextField.svelte";
import TagsEditor from "$components/tags/TagsEditor.svelte";
let presetId = $derived(page.params.id);
let targetPreset = $derived.by<TagEditorPreset | null>(() => {
if (presetId === 'new') {
return new TagEditorPreset(crypto.randomUUID(), {});
}
return $tagEditorPresets.find(preset => preset.id === presetId) || null;
});
let presetName = $state('');
let tagsList = $state<string[]>([]);
$effect(() => {
if (presetId === 'new') {
$popupTitle = 'Create New Preset';
return;
}
if (!targetPreset) {
goto('/features/presets');
return;
}
$popupTitle = `Edit Tagging Preset: ${targetPreset.settings.name}}`;
presetName = targetPreset.settings.name;
tagsList = [...targetPreset.settings.tags].sort((a, b) => a.localeCompare(b));
});
async function savePreset() {
if (!targetPreset) {
console.warn('Attempting to save the preset, but the preset is not loaded yet.');
return;
}
targetPreset.settings.name = presetName;
targetPreset.settings.tags = [...tagsList];
await targetPreset.save();
await goto(`/features/presets/${targetPreset.id}`);
}
</script>
<Menu>
<MenuItem href="/features/presets{presetId === 'new' ? '' : '/' + presetId}" icon="arrow-left">
Back
</MenuItem>
</Menu>
<FormContainer>
<FormControl label="Preset Name">
<TextField bind:value={presetName} placeholder="Preset Name"></TextField>
</FormControl>
<FormControl label="Tags">
<TagsEditor bind:tags={tagsList}></TagsEditor>
</FormControl>
</FormContainer>
<Menu>
<hr>
<MenuItem href="#" onclick={savePreset}>Save Preset</MenuItem>
</Menu>

View File

@@ -9,11 +9,13 @@
import FormControl from "$components/ui/forms/FormControl.svelte";
import FormContainer from "$components/ui/forms/FormContainer.svelte";
import { popupTitle } from "$stores/popup";
import { tagEditorPresets } from "$stores/entities/tag-editor-presets";
const bulkTransporter = new BulkEntitiesTransporter();
let exportAllProfiles = $state(false);
let exportAllGroups = $state(false);
let exportAllPresets = $state(false);
let displayExportedString = $state(false);
let shouldUseCompressed = $state(true);
@@ -24,6 +26,7 @@
const exportedEntities: Record<keyof App.EntityNamesMap, Record<string, boolean>> = $state({
profiles: {},
groups: {},
presets: {},
});
$effect(() => {
@@ -42,6 +45,12 @@
}
});
$tagEditorPresets.forEach(preset => {
if (exportedEntities.presets[preset.id]) {
elementsToExport.push(preset);
}
});
plainExport = bulkTransporter.exportToJSON(elementsToExport);
compressedExport = bulkTransporter.exportToCompressedJSON(elementsToExport);
}
@@ -57,6 +66,7 @@
requestAnimationFrame(() => {
exportAllProfiles = $taggingProfiles.every(profile => exportedEntities.profiles[profile.id]);
exportAllGroups = $tagGroups.every(group => exportedEntities.groups[group.id]);
exportAllPresets = $tagEditorPresets.every(preset => exportedEntities.presets[preset.id]);
});
}
@@ -74,6 +84,9 @@
case "groups":
$tagGroups.forEach(group => exportedEntities.groups[group.id] = exportAllGroups);
break;
case "presets":
$tagEditorPresets.forEach(preset => exportedEntities.presets[preset.id] = exportAllPresets);
break;
default:
console.warn(`Trying to toggle unsupported entity type: ${targetEntity}`);
}
@@ -116,6 +129,17 @@
{/each}
<hr>
{/if}
{#if $tagEditorPresets.length}
<MenuCheckboxItem bind:checked={exportAllPresets} oninput={createToggleAllOnUserInput('presets')}>
Export All Presets
</MenuCheckboxItem>
{#each $tagEditorPresets as preset}
<MenuCheckboxItem bind:checked={exportedEntities.presets[preset.id]} oninput={refreshAreAllEntitiesChecked}>
Preset: {preset.settings.name}
</MenuCheckboxItem>
{/each}
<hr>
{/if}
<MenuItem icon="file-export" onclick={toggleExportedStringDisplay}>Export Selected</MenuItem>
</Menu>
{:else}

View File

@@ -16,21 +16,26 @@
import type { SameSiteStatus } from "$lib/extension/EntitiesTransporter";
import { popupTitle } from "$stores/popup";
import Notice from "$components/ui/Notice.svelte";
import TagEditorPreset from "$entities/TagEditorPreset";
import { tagEditorPresets } from "$stores/entities/tag-editor-presets";
let importedString = $state('');
let errorMessage = $state('');
let importedProfiles = $state<TaggingProfile[]>([]);
let importedGroups = $state<TagGroup[]>([]);
let importedPresets = $state<TagEditorPreset[]>([]);
let saveAllProfiles = $state(false);
let saveAllGroups = $state(false);
let saveAllPresets = $state(false);
let isSaving = $state(false);
let selectedEntities: Record<keyof App.EntityNamesMap, Record<string, boolean>> = $state({
profiles: {},
groups: {},
presets: {},
});
let previewedEntity = $state<StorageEntity | null>(null);
@@ -49,8 +54,15 @@
}, new Map<string, TagGroup>())
);
const existingPresetsMap = $derived(
$tagEditorPresets.reduce((map, preset) => {
map.set(preset.id, preset);
return map;
}, new Map<string, TagEditorPreset>())
);
const hasImportedEntities = $derived(
Boolean(importedProfiles.length || importedGroups.length)
Boolean(importedProfiles.length || importedGroups.length || importedPresets.length)
);
$effect(() => {
@@ -70,6 +82,7 @@
function tryBulkImport() {
importedProfiles = [];
importedGroups = [];
importedPresets = [];
errorMessage = '';
importedString = importedString.trim();
@@ -103,6 +116,9 @@
case "groups":
importedGroups.push(targetImportedEntity as TagGroup);
break;
case "presets":
importedPresets.push(targetImportedEntity as TagEditorPreset);
break;
default:
console.warn(`Unprocessed entity type detected: ${targetImportedEntity.type}`, targetImportedEntity);
}
@@ -115,12 +131,14 @@
function cancelImport() {
importedProfiles = [];
importedGroups = [];
importedPresets = [];
}
function refreshAreAllEntitiesChecked() {
requestAnimationFrame(() => {
saveAllProfiles = importedProfiles.every(profile => selectedEntities.profiles[profile.id]);
saveAllGroups = importedGroups.every(group => selectedEntities.groups[group.id]);
saveAllPresets = importedPresets.every(preset => selectedEntities.presets[preset.id]);
});
}
@@ -134,6 +152,9 @@
case "groups":
importedGroups.forEach(group => selectedEntities.groups[group.id] = saveAllGroups);
break;
case "presets":
importedPresets.forEach(preset => selectedEntities.presets[preset.id] = saveAllPresets);
break;
default:
console.warn(`Trying to toggle unsupported entity type: ${entityType}`);
}
@@ -171,6 +192,14 @@
await group.save();
}
for (const preset of importedPresets) {
if (!selectedEntities.presets[preset.id]) {
continue;
}
await preset.save();
}
await goto("/transporting");
}
</script>
@@ -251,10 +280,7 @@
{/if}
{#if importedGroups.length}
<hr>
<MenuCheckboxItem
bind:checked={saveAllGroups}
oninput={createToggleAllOnUserInput('groups')}
>
<MenuCheckboxItem bind:checked={saveAllGroups} oninput={createToggleAllOnUserInput('groups')}>
Import All Groups
</MenuCheckboxItem>
{#each importedGroups as candidateGroup}
@@ -272,6 +298,26 @@
</MenuCheckboxItem>
{/each}
{/if}
{#if importedPresets.length}
<hr>
<MenuCheckboxItem bind:checked={saveAllPresets} oninput={createToggleAllOnUserInput('presets')}>
Import All Presets
</MenuCheckboxItem>
{#each importedPresets as candidatePreset}
<MenuCheckboxItem
bind:checked={selectedEntities.presets[candidatePreset.id]}
oninput={refreshAreAllEntitiesChecked}
onitemclick={createShowPreviewForEntity(candidatePreset)}
>
{#if existingPresetsMap.has(candidatePreset.id)}
Update:
{:else}
New:
{/if}
{candidatePreset.settings.name || 'Unnamed Preset'}
</MenuCheckboxItem>
{/each}
{/if}
<hr>
<MenuItem onclick={saveSelectedEntities}>
Imported Selected

View File

@@ -0,0 +1,11 @@
import { type Writable, writable } from "svelte/store";
import TagEditorPreset from "$entities/TagEditorPreset";
export const tagEditorPresets: Writable<TagEditorPreset[]> = writable([]);
TagEditorPreset
.readAll()
.then(presets => tagEditorPresets.set(presets))
.then(() => {
TagEditorPreset.subscribe(presets => tagEditorPresets.set(presets))
});

View File

@@ -0,0 +1,16 @@
@use '$styles/booru-vars';
.block.tag-presets {
.tag {
cursor: pointer;
&.is-missing {
opacity: 0.5;
}
&:hover {
color: booru-vars.$resolved-tag-background;
background: booru-vars.$resolved-tag-color;
}
}
}

View File

@@ -1,76 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="132.29mm" height="132.29mm" version="1.1" viewBox="0 0 132.29 132.29" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(471.71 80.22)">
<g transform="translate(.53454 1.7373)">
<path d="m-360.22-74.459c-28.032 5.3761-64.196 11.821-92.316 21.773-22.49 7.9602-19.605 30.849 5.0539 26.858 30-4.8548 67.281-10.699 95.353-14.616-0.64545-11.834-2.5854-23.356-8.0905-34.016zm-96.954 29.841c3.9562-0.74441 7.7233 1.6284 8.4142 5.2999 0.69043 3.6716-1.9562 7.2513-5.9124 7.9957-3.9562 0.7444-7.7232-1.6285-8.4141-5.2999-0.69042-3.6715 1.9562-7.2512 5.9123-7.9957z" fill="#113456" style="paint-order:normal"/>
<path d="m-354.11-76.234c-6.1145 1.1727-18.326 4.0969-24.43 5.2807l7.3928 33.203c6.3875-0.9175 18.616-2.3159 24.909-3.1941-0.64547-11.834-2.3666-24.63-7.8717-35.29z" fill="#275a8d" style="paint-order:normal"/>
<g transform="matrix(1.1047 -.26755 .26755 1.1047 -295.73 5.0268)" fill="#418dd9" stroke-width="1px" aria-label="+ i really">
<path d="m-109.59-68.714c-0.089 1.3875-2.4458-0.1706-2.4103 1.0204 0.36948 0.92893-0.61602 2.6504-1.1951 1.1347 0.0168-0.93201 0.24139-2.0611-1.1621-1.5039-1.6749 0.12542-0.90266-1.8258 0.45486-1.2068 1.4742 0.43966-0.0647-2.8816 1.747-1.9823 0.14653 1.24-0.13245 2.5429 1.722 1.9166 0.36032 0.02 0.86116 0.16781 0.84361 0.62124z"/>
<path d="m-102.78-72.896c-2.2543-0.55533 1.0522-2.7623 0.72678-0.48004-0.12625 0.27727-0.413 0.49201-0.72678 0.48004zm0.30213 5.4517c0.0582 0.94586 0.41317 4.0299-1.2236 2.6788-0.17956-2.0272-0.0573-4.0762 0.0872-6.1012 2.0744-1.5718 0.95718 2.4924 1.1364 3.4224z"/>
<path d="m-90.867-69.358c-0.35082 1.7588-1.6949 0.24631-1.2891-0.85938-1.4916 0.05506-2.992 1.3472-2.6886 2.9103-0.07892 1.057 0.19116 2.1931-0.15638 3.1909-1.3457 0.70152-1.0836-1.1527-1.0893-1.9485 0.01934-1.633-0.05224-3.2705 0.0705-4.8993 1.0358-1.4884 1.0728 1.7574 1.8807 0.18967 1.4368-1.159 3.8305-0.95952 3.2722 1.4162z"/>
<path d="m-86.348-64.088c-2.353 0.27226-4.1408-2.1361-3.3655-4.3384 0.25129-2.1773 2.5915-3.6168 4.6256-2.7749 2.4364 0.84593 0.66104 3.3783-1.138 3.6566-0.78817 0.37345-1.5763 0.7469-2.3645 1.1204 1.2036 1.8958 3.7418 0.96604 5.0623 0.05203 1.3351 1.4318-1.7546 2.3186-2.8198 2.2844zm-0.18127-6.1163c-1.2603-0.54741-3.1258 3.3447-1.3751 2.164 1.0179-0.5018 2.0935-0.92053 2.9931-1.6269-0.4321-0.4072-1.0427-0.53936-1.618-0.53711z"/>
<path d="m-76.694-63.953c-1.2507-1.5544-3.7187 1.1675-5.1562-0.9668-1.5477-2.2077-0.43496-5.8956 2.3599-6.4134 1.7565-0.47081 3.6364 0.76684 2.7141 2.6104-0.77494 1.6064 1.2116 3.4857 0.31555 4.7261zm-1.3965-4.6661c0.95522-2.544-2.6591-1.5586-2.9272 0.16617-0.9372 1.7855 0.85633 4.353 2.7544 2.8181 0.82545-0.56077-0.03186-2.0799 0.17288-2.9843z"/>
<path d="m-73.196-69.674c-0.08328 1.7284-0.03569 3.4717-0.22072 5.1881-1.9466 1.2665-1.0589-2.4666-1.1435-3.5088 0.08144-2.2743 0.03377-4.5571 0.1498-6.8272 1.924-1.2991 1.1388 2.3198 1.2333 3.3867-0.01142 0.58697-0.01883 1.174-0.01888 1.7611z"/>
<path d="m-69.436-69.674c-0.08328 1.7284-0.03569 3.4717-0.22072 5.1881-1.9466 1.2665-1.0589-2.4666-1.1435-3.5088 0.08144-2.2743 0.03377-4.5571 0.1498-6.8272 1.924-1.2991 1.1388 2.3198 1.2333 3.3867-0.01142 0.58697-0.01883 1.174-0.01888 1.7611z"/>
<path d="m-61.527-70.385c-1.4366 3.1829-3.0461 6.2999-4.1853 9.6042-2.1757 0.84518-0.54642-2.5672-0.08224-3.4957 0.74289-0.83986-0.24231-1.6266-0.52828-2.4351-0.6457-1.3327-1.442-2.6142-2.0188-3.9621 1.2038-1.9336 2.3288 1.7382 2.9679 2.6755 0.42367 1.4505 0.79477 1.47 1.1681 0.15822 0.5285-1.1723 0.95491-2.4281 1.752-3.4447 0.54145-0.35656 1.2854 0.33916 0.92651 0.89966z"/>
</g>
<g transform="matrix(.96209 -.2075 .2075 .96209 25.912 -52.71)" fill="#dadada" stroke-width="1px" aria-label="69">
<path d="m-388.94-81.96c-3.1615 0.07399-3.9724-4.0595-2.8389-6.3982 0.29035-1.6345 2.7892-5.2548 4.0606-3.6089-0.44097 1.0134-4.056 3.4132-1.0784 3.0781 2.8498-0.20087 4.3801 3.4509 2.6287 5.5412-0.6173 0.88091-1.6763 1.4927-2.772 1.3877zm0-5.5121c-2.8259-0.34004-2.5344 4.6205 0.17604 4.3457 2.5863-0.22618 2.7512-4.6838-0.17604-4.3457z"/>
<path d="m-379.28-83.256c-0.7292 0.75861-4.068 2.2452-3.2966 0.2874 1.247-0.55674 4.5546-2.2796 3.3763-3.1846-2.8507 1.1008-5.9514-2.2308-4.2021-4.9031 1.74-3.059 6.8816-1.6565 6.6098 1.9514 0.0631 2.1314-0.83485 4.4599-2.4875 5.8489zm-0.95336-8.3521c-3.2899-0.34723-2.7447 5.4317 0.54912 4.3465 2.2445-0.5994 1.8649-4.0795-0.38969-4.3095l-0.13256-0.03075z"/>
</g>
</g>
<g transform="matrix(.9297 .16991 -.16991 .9297 1852.1 945.27)" fill="#4aa158" stroke-width="1px" aria-label="i love tags">
<path d="m-2555.4-574.69c-0.3676 1.6669 0.9835 4.556-0.6889 5.4079-1.3252-1.1002-0.2302-3.0805-0.6116-4.5626-0.059-1.9432-0.1407-3.8859-0.1648-5.83 1.8661-1.2106 1.4497 2.2146 1.3957 3.2387 0.018 0.58223 0.038 1.1644 0.07 1.7461z"/>
<path d="m-2550.7-569.12c-1.695 0.0205-2.9714-1.6262-3.0509-3.2248-0.1501-1.8024 1.0236-3.7632 2.8687-4.1196 1.1017-0.20636 2.2681 0.43217 2.6999 1.465 0.9646 1.8328 0.4888 4.4339-1.3293 5.5548-0.3586 0.20843-0.7713 0.33513-1.1884 0.3246zm0.2522-6.0037c-1.2907 0.0944-2.056 1.5013-1.9549 2.6922 0.029 0.91158 0.6876 1.9392 1.6952 1.8903 1.2615-0.0788 1.9363-1.493 1.8203-2.6284-0.071-0.83944-0.6072-1.921-1.5606-1.9541z"/>
<path d="m-2542.9-570.06c-1.3061 1.5667-1.9961-1.4672-2.3256-2.4011-0.1987-1.32-2.0746-3.0697-0.9388-4.1448 1.525 0.20085 1.3152 2.1228 1.8816 3.1912 0.1403 0.79789 0.9706 2.1182 1.1744 0.58798 0.7835-1.1228 0.5292-3.8843 2.1726-3.7984 0.8949 1.4035-0.7473 2.7947-0.9974 4.1803-0.3221 0.79501-0.6494 1.588-0.9668 2.3849z"/>
<path d="m-2536.2-569.57c-1.8379 0.25369-3.7629-1.3102-3.5737-3.2271-0.1012-1.9097 1.1916-4.0598 3.2616-4.1279 1.6801-0.45649 3.7706 1.815 1.9193 3.0052-1.0691 1.0064-3.0605 1.1153-3.7768 2.3094 1.2557 1.3521 3.3826 0.86213 4.6361-0.23707 1.7687 0.85334-1.0741 2.4516-2.1601 2.2488zm-0.3347-6.1099c-1.0721-0.21792-2.5866 1.9217-1.5497 2.2758 0.8278-0.54282 3.244-1.0441 2.6665-2.1507-0.3611-0.12265-0.7486-0.0491-1.1168-0.12512z"/>
<path d="m-2524.9-575.13c-1.5405-0.67558-1.6985 1.2986-1.5948 2.4528 0.099 1.1011 0.4855 2.9463-0.6623 3.5102-1.5514-0.27135-0.3798-2.0448-0.6241-3.0366-0.2283-0.92722 0.4148-2.4056-0.3475-2.9818-0.9704 0.62006-2.45-1.4007-0.8898-1.386 1.1211 0.29871 1.5228-0.80797 1.1782-1.6566 0.5385-1.1269 1.8956-0.16987 1.4068 0.84134-0.1404 1.6457 1.8808 0.0764 2.1409 1.4955 0.1437 0.39517-0.2034 0.79615-0.6074 0.7612z"/>
<path d="m-2517.9-568.69c-0.5023-0.0829-0.6877-1.036-1.2791-0.6416-0.7152 0.37561-1.5473 0.56594-2.3442 0.36228-0.7872-0.14923-1.5089-0.66497-1.7954-1.4298-0.3806-1.0402-0.3821-2.2244-0.036-3.2745 0.5829-1.706 2.5214-2.9201 4.3075-2.445 0.5707 0.18706 1.265 0.40504 1.5386 0.98673 0 0.5278-0.316 1.0105-0.2776 1.5567-0.063 1.055-0.2308 2.1533 0.1349 3.1741 0.074 0.48738 0.5674 0.98265 0.2806 1.4585-0.1224 0.16152-0.3244 0.26651-0.5291 0.25261zm-1.2138-4.717c-0.1232-0.48283 0.4777-1.2468-0.099-1.5045-1.015-0.3911-2.1226 0.31928-2.6364 1.1837-0.5199 0.86317-0.573 1.9914-0.187 2.9144 0.3983 0.86477 1.5766 0.91664 2.3401 0.55213 0.4068-0.16553 0.8256-0.49949 0.6616-0.99063-0.082-0.71322-0.089-1.4375-0.08-2.1551z"/>
<path d="m-2510.5-571.74c-0.3089 1.7209-0.065 3.5936-0.951 5.1716-0.2379 0.48456-0.6503 0.8476-1.1523 1.0399-1.2351 0.52484-2.7013 0.49722-3.915-0.0756-0.2566-0.46006-0.08-1.2164 0.582-1.0383 0.7552 0.0602 1.5125 0.42319 2.2691 0.18519 1.0202-0.20188 1.6183-1.2671 1.6977-2.2374 0.022-0.27324-0.01-0.94901-0.2136-0.93026-0.6468 0.90767-1.9707 1.0068-2.9064 0.52172-0.7507-0.34336-1.2755-1.0692-1.414-1.8769-0.3954-1.8524 0.632-3.9893 2.4517-4.642 1.0211-0.32659 2.1925-0.25087 3.1384 0.26587 0.5576 0.29287 0.9341 0.92545 0.8125 1.5612-0.068 0.69708-0.3593 1.3534-0.3991 2.0548zm-2.0403-2.9888c-1.1195-0.15393-2.1374 0.68405-2.5193 1.6896-0.3037 0.75721-0.3717 1.6748 0.03 2.4073 0.3353 0.54377 1.0446 0.82935 1.6492 0.58276 0.9398-0.35299 1.5684-1.2979 1.6896-2.2747 0.1379-0.6666 0.3574-1.4067 0.05-2.0577-0.1732-0.31946-0.5679-0.39385-0.8993-0.3473z"/>
<path d="m-2504.1-573.77c-0.3876-2.4091-4.4757 0.62756-1.3652 0.92784 2.6224 0.90121 1.7346 4.9148-0.9851 4.7134-1.2495 0.50199-3.7735-1.8039-1.5043-1.8019 1.1893 0.86998 3.8843-0.32005 1.8423-1.4348-2.0484-0.34111-3.7316-2.7402-1.4066-4.0613 1.3001-1.1931 5.2979-0.75714 3.679 1.5424-0.087 0.0381-0.1734 0.0762-0.2601 0.11431z"/>
</g>
<path d="m-344.74-25.124c-6.1284-1.0979-18.576-2.7472-24.699-3.8311l-5.0054 33.646c6.2917 1.434 18.208 4.5136 24.398 5.9506 3.6411-11.279 6.6229-23.84 5.3066-35.766z" fill="#2d6236" style="paint-order:normal"/>
<g transform="matrix(.9297 .16991 -.16991 .9297 1916.8 958.68)" fill="#dadada" stroke-width="1px" aria-label="34">
<path d="m-2560.2-569.43q-1.0138 0-1.8194-0.42297-0.9064-0.49011-1.2219-1.3763-0.04-0.12085-0.04-0.23499 0-0.26855 0.2014-0.44311 0.2081-0.18128 0.4767-0.18128 0.2685 0 0.4632 0.24841l0.3089 0.46326q0.2417 0.32898 0.6512 0.49683 0.4096 0.16113 0.9802 0.16113 0.7318 0 1.2824-0.45654 0.5841-0.4834 0.5841-1.1816 0-1.0138-0.7453-1.618-0.6579-0.52368-1.7456-0.63781-0.7452-0.0739-0.7452-0.60425 0-0.39612 0.5841-0.60425l1.6046-0.39612q0.4633-0.16784 0.6983-0.44983 0.235-0.28869 0.2417-0.69824 0.013-0.59082-0.4566-0.95337-0.4901-0.37597-1.3763-0.37597-0.4566 0-0.8862 0.22155l-0.7587 0.47669q-0.2216 0.14099-0.3559 0.14099-0.2685 0-0.4632-0.20142-0.188-0.20813-0.188-0.46997 0-0.53039 1.0272-1.0205 0.893-0.43641 1.4704-0.43641 1.4434 0 2.2894 0.63782 0.9064 0.68482 0.9064 1.9537 0 1.5778-1.2757 2.0813-0.1141 0.047-0.2618 0.094 0.8929 0.32898 1.336 0.98694 0.4432 0.65124 0.4432 1.6315 0 1.3226-0.9333 2.2491-0.9332 0.9198-2.276 0.9198z"/>
<path d="m-2549.4-572.67v2.4841q0 0.62439-0.611 0.62439-0.7117 0-0.7117-0.97352 0-0.12084 0.014-0.3424 0.013-0.22156 0.013-0.30213l-0.01-1.524-2.9608-0.0739q-0.9198 0-1.2757-0.0604-0.6109-0.10071-0.6109-0.47669 0-0.30212 0.3692-0.76538l0.5774-0.68481 3.3032-4.8206q0.4499-0.62439 1.1414-0.62439 0.7587 0 0.7587 0.65796v5.5792q0.1141-7e-3 0.2752-7e-3 1.2354 0 1.2354 0.66467 0 0.4834-0.5103 0.61097-0.2282 0.0604-1.0003 0.0336zm-1.3025-5.6799q-1.7926 2.8601-2.7997 4.2834l2.7997 0.0671z"/>
</g>
<g transform="matrix(.1976 .036114 -.036114 .1976 -325.27 -67.285)" fill="#4aa158" aria-label="+">
<path d="m-493.08 314.34c-0.0489 3.7279-4.7678 3.8996-7.4987 3.3367-3.6053-1.4027-7.9235 0.96777-6.2932 5.2004 1.0798 3.1434-0.60545 8.8662-4.8234 6.7976-3.6452-2.0855-1.24-6.7037-1.8488-9.936-2.0642-4.0613-7.6116-0.089-10.84-2.4827-4.1787-2.5752 1.0937-8.1378 4.6872-6.1325 3.6391 1.3653 7.8794-1.1274 5.9251-5.2444-0.89217-3.2023 1.9875-8.7744 5.6881-6.1442 3.634 2.6914-0.77449 7.4233 1.9769 10.557 3.3424 2.2417 7.9403-1.1136 11.393 1.362 0.90882 0.59128 1.7649 1.5165 1.6334 2.6864z" fill="#4aa158" stroke-width="5.679px"/>
</g>
<path d="m-351.07-25.656c-28.095-5.0334-64.166-11.986-93.984-12.779-23.849-0.63414-29.364 21.767-4.9134 26.884 29.746 6.2259 66.643 14.139 94.252 20.549 3.6411-11.279 5.9617-22.729 4.6453-34.655zm-101.21-6.9112c3.96 0.72376 6.6257 4.2896 5.9541 7.9647-0.67209 3.675-4.4264 6.0676-8.3864 5.3438-3.96-0.72375-6.6256-4.2897-5.954-7.9647 0.67208-3.6749 4.4263-6.0675 8.3863-5.3438z" fill="#1b3c21" style="paint-order:normal"/>
<g transform="matrix(.9297 .16991 -.16991 .9297 1852.1 945.27)" fill="#4aa158" stroke-width="1px" aria-label="i love tags">
<path d="m-2555.4-574.69c-0.3676 1.6669 0.9835 4.556-0.6889 5.4079-1.3252-1.1002-0.2302-3.0805-0.6116-4.5626-0.059-1.9432-0.1407-3.8859-0.1648-5.83 1.8661-1.2106 1.4497 2.2146 1.3957 3.2387 0.018 0.58223 0.038 1.1644 0.07 1.7461z"/>
<path d="m-2550.7-569.12c-1.695 0.0205-2.9714-1.6262-3.0509-3.2248-0.1501-1.8024 1.0236-3.7632 2.8687-4.1196 1.1017-0.20636 2.2681 0.43217 2.6999 1.465 0.9646 1.8328 0.4888 4.4339-1.3293 5.5548-0.3586 0.20843-0.7713 0.33513-1.1884 0.3246zm0.2522-6.0037c-1.2907 0.0944-2.056 1.5013-1.9549 2.6922 0.029 0.91158 0.6876 1.9392 1.6952 1.8903 1.2615-0.0788 1.9363-1.493 1.8203-2.6284-0.071-0.83944-0.6072-1.921-1.5606-1.9541z"/>
<path d="m-2542.9-570.06c-1.3061 1.5667-1.9961-1.4672-2.3256-2.4011-0.1987-1.32-2.0746-3.0697-0.9388-4.1448 1.525 0.20085 1.3152 2.1228 1.8816 3.1912 0.1403 0.79789 0.9706 2.1182 1.1744 0.58798 0.7835-1.1228 0.5292-3.8843 2.1726-3.7984 0.8949 1.4035-0.7473 2.7947-0.9974 4.1803-0.3221 0.79501-0.6494 1.588-0.9668 2.3849z"/>
<path d="m-2536.2-569.57c-1.8379 0.25369-3.7629-1.3102-3.5737-3.2271-0.1012-1.9097 1.1916-4.0598 3.2616-4.1279 1.6801-0.45649 3.7706 1.815 1.9193 3.0052-1.0691 1.0064-3.0605 1.1153-3.7768 2.3094 1.2557 1.3521 3.3826 0.86213 4.6361-0.23707 1.7687 0.85334-1.0741 2.4516-2.1601 2.2488zm-0.3347-6.1099c-1.0721-0.21792-2.5866 1.9217-1.5497 2.2758 0.8278-0.54282 3.244-1.0441 2.6665-2.1507-0.3611-0.12265-0.7486-0.0491-1.1168-0.12512z"/>
<path d="m-2524.9-575.13c-1.5405-0.67558-1.6985 1.2986-1.5948 2.4528 0.099 1.1011 0.4855 2.9463-0.6623 3.5102-1.5514-0.27135-0.3798-2.0448-0.6241-3.0366-0.2283-0.92722 0.4148-2.4056-0.3475-2.9818-0.9704 0.62006-2.45-1.4007-0.8898-1.386 1.1211 0.29871 1.5228-0.80797 1.1782-1.6566 0.5385-1.1269 1.8956-0.16987 1.4068 0.84134-0.1404 1.6457 1.8808 0.0764 2.1409 1.4955 0.1437 0.39517-0.2034 0.79615-0.6074 0.7612z"/>
<path d="m-2517.9-568.69c-0.5023-0.0829-0.6877-1.036-1.2791-0.6416-0.7152 0.37561-1.5473 0.56594-2.3442 0.36228-0.7872-0.14923-1.5089-0.66497-1.7954-1.4298-0.3806-1.0402-0.3821-2.2244-0.036-3.2745 0.5829-1.706 2.5214-2.9201 4.3075-2.445 0.5707 0.18706 1.265 0.40504 1.5386 0.98673 0 0.5278-0.316 1.0105-0.2776 1.5567-0.063 1.055-0.2308 2.1533 0.1349 3.1741 0.074 0.48738 0.5674 0.98265 0.2806 1.4585-0.1224 0.16152-0.3244 0.26651-0.5291 0.25261zm-1.2138-4.717c-0.1232-0.48283 0.4777-1.2468-0.099-1.5045-1.015-0.3911-2.1226 0.31928-2.6364 1.1837-0.5199 0.86317-0.573 1.9914-0.187 2.9144 0.3983 0.86477 1.5766 0.91664 2.3401 0.55213 0.4068-0.16553 0.8256-0.49949 0.6616-0.99063-0.082-0.71322-0.089-1.4375-0.08-2.1551z"/>
<path d="m-2510.5-571.74c-0.3089 1.7209-0.065 3.5936-0.951 5.1716-0.2379 0.48456-0.6503 0.8476-1.1523 1.0399-1.2351 0.52484-2.7013 0.49722-3.915-0.0756-0.2566-0.46006-0.08-1.2164 0.582-1.0383 0.7552 0.0602 1.5125 0.42319 2.2691 0.18519 1.0202-0.20188 1.6183-1.2671 1.6977-2.2374 0.022-0.27324-0.01-0.94901-0.2136-0.93026-0.6468 0.90767-1.9707 1.0068-2.9064 0.52172-0.7507-0.34336-1.2755-1.0692-1.414-1.8769-0.3954-1.8524 0.632-3.9893 2.4517-4.642 1.0211-0.32659 2.1925-0.25087 3.1384 0.26587 0.5576 0.29287 0.9341 0.92545 0.8125 1.5612-0.068 0.69708-0.3593 1.3534-0.3991 2.0548zm-2.0403-2.9888c-1.1195-0.15393-2.1374 0.68405-2.5193 1.6896-0.3037 0.75721-0.3717 1.6748 0.03 2.4073 0.3353 0.54377 1.0446 0.82935 1.6492 0.58276 0.9398-0.35299 1.5684-1.2979 1.6896-2.2747 0.1379-0.6666 0.3574-1.4067 0.05-2.0577-0.1732-0.31946-0.5679-0.39385-0.8993-0.3473z"/>
<path d="m-2504.1-573.77c-0.3876-2.4091-4.4757 0.62756-1.3652 0.92784 2.6224 0.90121 1.7346 4.9148-0.9851 4.7134-1.2495 0.50199-3.7735-1.8039-1.5043-1.8019 1.1893 0.86998 3.8843-0.32005 1.8423-1.4348-2.0484-0.34111-3.7316-2.7402-1.4066-4.0613 1.3001-1.1931 5.2979-0.75714 3.679 1.5424-0.087 0.0381-0.1734 0.0762-0.2601 0.11431z"/>
</g>
<path d="m-344.74-25.124c-6.1284-1.0979-18.576-2.7472-24.699-3.8311l-5.0054 33.646c6.2917 1.434 18.208 4.5136 24.398 5.9506 3.6411-11.279 6.6229-23.84 5.3066-35.766z" fill="#2d6236" style="paint-order:normal"/>
<g transform="matrix(.9297 .16991 -.16991 .9297 1916.8 958.68)" fill="#dadada" stroke-width="1px" aria-label="34">
<path d="m-2560.2-569.43c-1.2856 0.0748-3.2487-0.8227-2.9857-2.3245 0.8676-0.90346 1.4175 0.67501 2.1254 0.91811 1.1413 0.45829 2.7852-0.26374 2.7344-1.6257-0.01-1.4734-1.5962-2.1621-2.8331-2.1868-1.0993-1.0876 0.8582-1.4069 1.6354-1.5299 1.404-0.17239 1.3825-2.2613 0-2.4456-1.1389-0.51399-1.9641 0.71013-2.9866 0.70744-1.1306-0.91929 0.7004-1.8515 1.5546-1.9774 1.4588-0.34957 3.3826 0.3203 3.6813 1.9475 0.3366 1.1155-0.4205 2.2702-1.2292 2.7692 0.9537 0.52993 1.6163 1.529 1.5264 2.6612-0.042 1.69-1.5045 3.159-3.2189 3.0864z"/>
<path d="m-2549.4-572.67c-0.01 0.87671 0.012 1.7544-0.01 2.6305-0.043 0.61406-1.0298 0.63582-1.2131 0.0978-0.1968-0.54036-0.036-1.1185-0.076-1.6787 0-0.36104 0-0.72207-0.01-1.0831-1.3196-0.0367-2.6399-0.059-3.959-0.10245-0.4686 0.0444-1.1513-0.30426-0.7961-0.85094 0.2984-0.55059 0.7995-0.9545 1.1182-1.4929 1.0334-1.5028 2.0571-3.0125 3.0965-4.511 0.3567-0.4952 1.1247-0.74148 1.6532-0.38429 0.2915 0.28649 0.1595 0.72501 0.1897 1.09v4.9827c0.4855-4e-3 1.0967-0.0553 1.4334 0.36688 0.2552 0.41477-0.1168 0.94035-0.5812 0.92651-0.2807 0.0266-0.5782 0.0165-0.849 9e-3zm-1.3025-5.6799c-0.909 1.4433-1.8196 2.8869-2.7997 4.2834 0.9332 0.0224 1.8665 0.0447 2.7997 0.0671v-4.3506z"/>
</g>
<g transform="matrix(.1976 .036114 -.036114 .1976 -325.27 -67.285)" fill="#4aa158" aria-label="+">
<path d="m-493.08 314.34c-0.0489 3.7279-4.7678 3.8996-7.4987 3.3367-3.6053-1.4027-7.9235 0.96777-6.2932 5.2004 1.0798 3.1434-0.60545 8.8662-4.8234 6.7976-3.6452-2.0855-1.24-6.7037-1.8488-9.936-2.0642-4.0613-7.6116-0.089-10.84-2.4827-4.1787-2.5752 1.0937-8.1378 4.6872-6.1325 3.6391 1.3653 7.8794-1.1274 5.9251-5.2444-0.89217-3.2023 1.9875-8.7744 5.6881-6.1442 3.634 2.6914-0.77449 7.4233 1.9769 10.557 3.3424 2.2417 7.9403-1.1136 11.393 1.362 0.90882 0.59128 1.7649 1.5165 1.6334 2.6864z" fill="#4aa158" stroke-width="5.679px"/>
</g>
<g transform="rotate(19.399 -638.5 398.75)">
<path d="m-501.72-53.894c-28.095-5.0334-64.166-11.986-93.984-12.779-23.849-0.63414-29.364 21.767-4.9134 26.884 29.746 6.2259 66.643 14.139 94.252 20.549 3.6411-11.279 5.9617-22.729 4.6453-34.655zm-101.21-6.9112c3.96 0.72376 6.6257 4.2896 5.9541 7.9647-0.67209 3.675-4.4264 6.0676-8.3864 5.3438-3.96-0.72375-6.6256-4.2897-5.954-7.9647 0.67208-3.6749 4.4263-6.0675 8.3863-5.3438z" fill="#193f47" style="paint-order:normal"/>
<path d="m-495.38-53.362c-6.1284-1.0979-18.576-2.7472-24.699-3.8311l-5.0054 33.646c6.2917 1.434 18.208 4.5136 24.398 5.9506 3.6411-11.279 6.6229-23.84 5.3066-35.766z" fill="#2b6a78" style="paint-order:normal"/>
<g transform="matrix(.9297 .16991 -.16991 .9297 1766.1 930.44)" fill="#dadada" aria-label="34">
<path d="m-2563.1-573.66c-0.01-7.3938 10.433-3.4839 10.55-0.48341 0.1174 3.0005 10.91 8.4875 10.781 0.41697-0.1286-8.0705-10.744-3.18-10.781-0.41697-0.037 2.763-10.543 7.8772-10.55 0.48341z" fill="none" stroke="#dadada" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.7519" style="paint-order:normal"/>
</g>
<g transform="matrix(.1976 .036114 -.036114 .1976 -475.92 -95.524)" fill="#4aa158" aria-label="+">
<path d="m-493.08 314.34c-0.0489 3.7279-4.7678 3.8996-7.4987 3.3367-3.6053-1.4027-7.9235 0.96777-6.2932 5.2004 1.0798 3.1434-0.60545 8.8662-4.8234 6.7976-3.6452-2.0855-1.24-6.7037-1.8488-9.936-2.0642-4.0613-7.6116-0.089-10.84-2.4827-4.1787-2.5752 1.0937-8.1378 4.6872-6.1325 3.6391 1.3653 7.8794-1.1274 5.9251-5.2444-0.89217-3.2023 1.9875-8.7744 5.6881-6.1442 3.634 2.6914-0.77449 7.4233 1.9769 10.557 3.3424 2.2417 7.9403-1.1136 11.393 1.362 0.90882 0.59128 1.7649 1.5165 1.6334 2.6864z" fill="#49aabf" stroke-width="5.679px"/>
</g>
<g transform="matrix(1.317 .30927 -.30927 1.317 -47.46 164.76)" fill="#49aabf" stroke-width="1px" aria-label="a lot">
<path d="m-410.25-61.811c-0.65817-0.47252-1.0234-1.0261-1.793-0.36003-1.3776 0.68242-3.4363 0.10802-3.8268-1.5048-0.51946-1.6866-0.15989-3.743 1.3355-4.8252 1.2108-0.99587 3.1869-1.0464 4.3304 0.07008 0.12212 0.99307-0.30691 2.1479-0.15335 3.2374-0.15933 1.0779 0.64504 2.0133 0.68458 3.0099-0.0987 0.22178-0.3329 0.37805-0.57739 0.37262zm-1.3965-4.6661c0.0861-0.77055 0.34961-1.8109-0.8197-1.5846-1.6279 0.1933-2.5964 2.0219-2.2408 3.5415 0.0244 1.2019 1.3521 1.7652 2.3601 1.3216 1.4197-0.319 0.6606-1.7655 0.71377-2.7795-8e-3 -0.16619-0.0133-0.33256-0.0135-0.49893z"/>
<path d="m-402.64-67.531c-0.0644 1.6837-0.0716 3.3716-0.17936 5.0518-0.47397 1.2143-1.738 0.02501-1.2806-0.89431 0.14762-3.0543 0.1549-6.1126 0.2148-9.1689 0.66106-1.3376 1.7124 0.20846 1.3059 1.1447-0.0163 1.289-0.0639 2.5776-0.0608 3.8668z"/>
<path d="m-398.07-61.858c-1.7276 0.04356-2.9503-1.7082-2.8937-3.3234-0.11892-1.8565 1.1557-3.9955 3.1769-4.0297 1.3201-0.17846 2.3745 0.95782 2.6137 2.1699 0.44919 1.7744 0.0145 4.0808-1.764 4.9545-0.35479 0.15908-0.7452 0.2295-1.1329 0.22874zm0.40283-5.9955c-1.5353 0.0069-2.1416 1.8524-1.9407 3.132 0.0593 1.1077 1.3552 1.9156 2.3346 1.3079 1.2388-0.72072 1.16-2.4186 0.78696-3.6124-0.17409-0.49295-0.64956-0.85296-1.1809-0.82754z"/>
<path d="m-388.87-67.974c-0.57242 0.04133-1.8431-0.29375-1.5597 0.63682 0.0222 1.5617 0.16373 3.1261 0.0895 4.686 0.0229 1.2973-1.7893 0.83962-1.3244-0.3101 0.0639-1.642-0.0722-3.2837-0.10538-4.9255-0.85075 0.10945-2.7999-0.18163-1.8098-1.3541 0.55725-0.22568 1.1868 0.01233 1.7762 0.0046 0.098-0.77503-0.5398-2.4403 0.75408-2.3814 0.75002 0.33896 0.55684 1.3355 0.56751 2.021-0.0144 0.82426 1.4999-0.16274 2.0397 0.4032 0.46304 0.37105 0.20127 1.2582-0.42781 1.2194z"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 800 B

After

Width:  |  Height:  |  Size: 700 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB