1
0
mirror of https://github.com/koloml/furbooru-tagging-assistant.git synced 2026-03-24 23:02:58 +00:00

20 Commits

Author SHA1 Message Date
83c7608e99 Presets: Added flag for making it "exclusive"
This will make it so only one tag will be active from marked preset.
This can be useful for some tags that cannot be together in the editor,
for example, rating tags.
2026-03-22 04:09:18 +04:00
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
54 changed files with 1837 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,26 @@
<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>
{#if preset.settings.exclusive}
<DetailsBlock title="Exclusivity">
Only one tag in this preset should be active at a time. If you will click on other non-active tag, other tags will
be automatically removed from the editor.
</DetailsBlock>
{/if}

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,186 @@
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');
#exclusiveWarning = document.createElement('div');
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');
tagsCell.style.width = '70%';
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';
if (this.#preset.settings.exclusive) {
this.#applyAllButton.disabled = true;
this.#applyAllButton.title = "You can't add all tags from this preset since it only allows one tag to be active";
this.#exclusiveWarning.classList.add('block', 'block--fixed', 'block--warning');
this.#exclusiveWarning.textContent = ' Multiple tags from this preset present in the editor! If you will click one of the tags here, other tags will be cleared automatically.'
this.#exclusiveWarning.prepend(createFontAwesomeIcon('triangle-exclamation'));
this.#exclusiveWarning.style.display = 'none';
tagsCell.append(this.#exclusiveWarning);
}
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);
if (!tagName) {
return;
}
// If a user clicks on the tag which was missing, then we have to remove all other active tags that are in this
// preset. But only when clicking on a tag which is missing, just so they will be able to remove any cases where
// multiple tags from exclusive present are active.
if (this.#preset.settings.exclusive && isMissing) {
const tagNamesToRemove = this.#tagsList
.filter(
tagElement => tagElement !== targetElement
&& !tagElement.classList.contains(PresetTableRow.#tagMissingClassName)
)
.map(tagElement => tagElement.dataset.tagName)
.filter(tagName => typeof tagName === 'string');
emit(this, EVENT_PRESET_TAG_CHANGE_APPLIED, {
addedTags: new Set([tagName]),
removedTags: new Set(tagNamesToRemove)
});
return;
}
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>) {
let presentTagsAmount = 0;
for (const tagElement of this.#tagsList) {
const isTagMissing = tagElement.classList.toggle(
PresetTableRow.#tagMissingClassName,
!tags.has(tagElement.dataset.tagName || ''),
);
if (!isTagMissing) {
presentTagsAmount++;
}
}
if (this.#preset.settings.exclusive) {
const multipleTagsInExclusivePreset = presentTagsAmount > 1;
this.container.classList.toggle(PresetTableRow.#presetWarningClassName, multipleTagsInExclusivePreset);
if (multipleTagsInExclusivePreset) {
this.#exclusiveWarning.style.removeProperty('display');
} else {
this.#exclusiveWarning.style.display = 'none';
}
}
}
remove() {
this.container.remove();
}
static create(preset: TagEditorPreset) {
return new this(document.createElement('tr'), preset);
}
static #tagMissingClassName = 'is-missing';
static #presetWarningClassName = 'has-warning';
}

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,19 @@
import StorageEntity from "$lib/extension/base/StorageEntity";
interface TagEditorPresetSettings {
name: string;
tags: string[];
exclusive: boolean;
}
export default class TagEditorPreset extends StorageEntity<TagEditorPresetSettings> {
constructor(id: string, settings: Partial<TagEditorPresetSettings>) {
super(id, {
name: settings.name || '',
tags: settings.tags || [],
exclusive: settings.exclusive ?? false
});
}
public static readonly _entityName = 'presets';
}

View File

@@ -33,6 +33,17 @@ 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,
exclusive: entity.settings.exclusive,
}
}
};

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,83 @@
<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";
import CheckboxField from "$components/ui/forms/CheckboxField.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[]>([]);
let isExclusive = $state(false);
$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));
isExclusive = targetPreset.settings.exclusive;
});
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];
targetPreset.settings.exclusive = isExclusive;
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>
<FormControl>
<CheckboxField bind:checked={isExclusive}>
Keep only one tag from this preset active at a time.
</CheckboxField>
</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

@@ -4,6 +4,7 @@ $media-box-color: var(--media-box-color);
$padding-small: var(--padding-small);
$padding-normal: var(--padding-normal);
$padding-large: var(--padding-large);
$block-spacing: var(--block-spacing);
// These variables are defined dynamically based on the category of the tag
$resolved-tag-background: var(--tag-background);

View File

@@ -0,0 +1,23 @@
@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;
}
}
.block.block--fixed.block--warning {
margin: {
top: booru-vars.$block-spacing;
bottom: 0;
}
}
}

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