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

40 Commits
0.5.0 ... 0.5.3

Author SHA1 Message Date
ab625d0181 Merge pull request #141 from koloml/release/0.5.3
Release: 0.5.3
2026-01-09 08:51:45 +04:00
c59d8f55f0 Bumped version to 0.5.3 2026-01-09 08:50:05 +04:00
8dfc5f49f9 Merge pull request #143 from koloml/feature/code-reorganization
Slight change in code organization for content script components
2026-01-09 08:48:59 +04:00
2ecd37512f Moving all content_scripts-related components under $content directory
Having $lib/component with just $component was a bit confusing,
especially since $lib is also used in Svelte components all over the
place. This move will hopefully make it less confusing for me.
2026-01-09 07:06:58 +04:00
c8ff80d445 Move list of tag categories into the tags config script 2026-01-09 06:55:51 +04:00
38cbd725d9 Merge pull request #142 from koloml/bugfix/profile-view-tags-list
Profile View: Fixed tags list not being properly reactive in the extension popup
2026-01-09 06:44:23 +04:00
26f09c7c46 Fixed tags list for tagging profiles not updating reactively in popup 2026-01-09 06:41:28 +04:00
64be6a6e15 Bumping dependencies (#140)
* Updated `vite` from 7.1.6 to 7.3.1

* Updated `@sveltejs/vite-plugin-svelte` from 6.2.0 to 6.2.3

* Updated `@sveltejs/kit` from 2.42.2 to 2.49.4

* Updated `@sveltejs/adapter-static` from 3.0.9 to 3.0.10

* Updated `svelte` from 5.39.4 to 5.46.1

* Updated `svelte-check` from 4.3.1 to 4.3.5

* Updated `typescript` from 5.9.2 to 5.9.3

* Updated `sass` from 1.93.0 to 1.97.2

* Updated `jsdom` from 27.0.0 to 27.4.0

* Updated `cross-env` from 10.0.0 to 10.1.0

* Updated `@types/node` from 22.18.6 to 25.0.3

* Updated `@types/chrome` from 0.0.326 to 0.1.32

* Updated `vitest` and `@vitest/coverage-v8` from 3.2.4 to 4.0.16

* Updated `@fortawesome/fontawesome-free` from 6.7.2 to 7.1.0
2026-01-09 06:35:52 +04:00
cb22b2deab Merge pull request #139 from koloml/feature/display-dedicated-popup-titles
Popup: Display different tab titles for different routes
2026-01-09 06:35:25 +04:00
5c5e0812dc Provide names for all popup routes using new store 2026-01-09 05:56:33 +04:00
70129d7a0e Added the store for dynamically changing the popup titles 2026-01-09 05:27:48 +04:00
5fd6dee999 Added constant with the full name of the plugin 2026-01-09 05:27:11 +04:00
ec41ba5030 Furbooru: Added screenshots for tag groups feature 2025-11-04 19:26:47 -05:00
55624285e1 Merge pull request #138 from koloml/feature/release-build-pipeline
Added GitHub action for building the project in CI
2025-10-02 13:41:29 +04:00
b97255ccd6 Release CI action
Builds the extension for both sites and uploads them as artifacts. This
will make it possible to just release the project and then grab the
ready-to-be-deployed archive for publishing.

This change was made using Cursor. Just a small test run to check how
useful it is for my workflows.
2025-10-02 13:40:02 +04:00
ef76560bfb Merge pull request #136 from koloml/release/0.5.2
Release: 0.5.2
2025-09-27 22:24:38 +04:00
faa909a0db Properly rearranging non-dev dependencies 2025-09-27 22:22:40 +04:00
3955e3191e Bumped version to 0.5.2 2025-09-27 22:15:38 +04:00
17dab5854c Merge pull request #137 from koloml/feature/reduce-gap-in-tag-category-titles
Furbooru: Applying styling changes previously used only for Derpibooru
2025-09-24 13:13:22 +04:00
a20632e58e Furbooru: Updated tags appearance in media box popups 2025-09-22 22:50:48 +04:00
5f4a1a6c00 Furbooru: Use margin for tag category titles used for Derpibooru 2025-09-22 22:49:33 +04:00
48fc58f042 Merge pull request #135 from koloml/feature/updated-tag-dropdown
Making dropdown link icon active for both boorus
2025-09-22 05:09:45 +04:00
8356956b2e Making dropdown link icon active for both boorus
Furbooru was updated to the latest version of Philomena a few days ago.
Now it uses icons in tag dropdown just like on Derpibooru.
2025-09-22 05:05:52 +04:00
3833cada1e Bumping dependencies (#134)
* Updated `vite` from 6.3.5 to 7.1.2

* Updated `@sveltejs/kit` and `@sveltejs/vite-plugin-svelte`

These are updated together, since they're interconnected with Vite

* Updated `svelte` from 5.33.14 to 5.38.1

* Updated `@sveltejs/adapter-static` from 3.0.8 to 3.0.9

* Updated `vitest` and `@vitest/coverage-v8` from 3.2.0 to 3.2.4

* Updated `svelte-check` from 4.2.1 to 4.3.1

* Updated `typescript` from 5.8.3 to 5.9.2

* Updated `sass` from 1.89.1 to 1.90.0

* Updated `@types/node` from 22.15.29 to 22.17.2

* Updated `cheerio` from 1.0.0 to 1.1.2

* Updated `@sveltejs/kit` from 2.31.1 to 2.42.2

* Updated `vite` from 7.1.2 to 7.1.6

* Updated `@sveltejs/vite-plugin-svelte` from 6.1.2 to 6.2.0

* Updated `svelte` from 5.38.1 to 5.39.4

* Updated `sass` from 1.90.0 to 1.93.0

* Updated `@types/node` from 22.17.2 to 22.18.6

* Updated `jsdom` from 26.1.0 to 27.0.0
2025-09-21 20:49:49 -04:00
f3d80b58b1 Merge pull request #130 from koloml/release/0.5.1
Release: 0.5.1
2025-08-13 18:23:14 +04:00
d567ab4dec Merge pull request #133 from koloml/feature/derpibooru-smaller-tags
Derpibooru: Making tags slightly smaller in height to fit the styling used by the site
2025-08-13 17:55:39 +04:00
e4322b3021 Derpibooru: Making tags slightly smaller inside popup 2025-08-13 17:54:40 +04:00
4907efdaab Bumped version to 0.5.1 2025-08-13 17:50:31 +04:00
c6b9250d71 Merge pull request #132 from koloml/feature/add-to-profile-icon
Derpibooru: Added icon to the tag dropdown option
2025-08-13 17:27:05 +04:00
c330aa303a Derpibooru: Added icon to the tag dropdown option 2025-08-13 17:24:36 +04:00
9ed3f6939d Merge pull request #129 from koloml/bugfix/derpibooru-tag-editor-styling
Fixed tag categories headlines having inconsistent spacing between Derpibooru and Furbooru
2025-08-13 16:52:39 +04:00
5584733b17 Merge pull request #131 from koloml/bugfix/inconsistent-auto-run
Firefox: Fixed content scripts randomly loading asynchronously and not auto-running
2025-08-13 16:52:23 +04:00
91947b8cc7 Merge remote-tracking branch 'origin/release/0.5.1' into bugfix/inconsistent-auto-run
# Conflicts:
#	src/content/deps/amd.ts
2025-08-13 16:49:18 +04:00
df61c812fe Updated autorun logic to resolve issues with loading modules on Firefox
Sometimes Firefox decides to load different groups of content scripts
asynchronously, causing our trick with `requestAnimationFrame` to miss
everything. To prevent this, I decided to just attempt to autorun
everything on each definition using `setTimeout`.

I've also tried to use `queueMicrotask` to put autorun logic right
between different groups of modules, but this trick was only working on
Firefox and completely breaking on Chromium. I sure love browsers!
2025-08-13 16:48:27 +04:00
65c420c36c Merge pull request #128 from koloml/bugfix/ignore-duplicated-modules
Firefox: Fixed an error message appearing when single chunk is trying to execute multiple times
2025-08-13 16:42:16 +04:00
79cd9bc44d Reduced the space used by the tag category headline
This is mainly affecting the Derpibooru version of the extension. Tags
list on Derpibooru is using flex with gaps instead of flex with margins
appearing like gaps (what currently Furbooru uses). This change would
likely be applied to the Furbooru as well.
2025-08-13 15:56:31 +04:00
cf28d2d131 AMD Loader: Ignore duplicated module definitions
This fixes an error appearing when chunk is mention multiple times for
different entry content scripts.
2025-08-13 15:27:25 +04:00
50238d8ef4 Added links to the Derpibooru extension 2025-08-13 14:56:44 +04:00
98b5311cfc Derpibooru: Added screenshot about tag colors in editor 2025-08-12 13:58:34 +04:00
e60d20fd60 Added showcase screenshots for Derpibooru 2025-08-11 09:11:52 +04:00
73 changed files with 1262 additions and 1406 deletions

BIN
.github/assets/colors-in-editor.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
.github/assets/groups-showcase-0.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

BIN
.github/assets/groups-showcase-1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

63
.github/workflows/build-extensions.yml vendored Normal file
View File

@@ -0,0 +1,63 @@
name: Build Extensions
on:
push:
branches: [ master ]
jobs:
build-extensions:
runs-on: ubuntu-latest
strategy:
matrix:
site: [furbooru, derpibooru]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build extension for ${{ matrix.site }}
run: |
if [ "${{ matrix.site }}" = "derpibooru" ]; then
npm run build:derpibooru
else
npm run build
fi
- name: Create extension zip
run: |
cd build
zip -r "../${{ matrix.site }}-tagging-assistant-extension.zip" .
- name: Upload extension artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.site }}-tagging-assistant-extension
path: ${{ matrix.site }}-tagging-assistant-extension.zip
retention-days: 30
create-release-artifacts:
runs-on: ubuntu-latest
needs: build-extensions
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Create combined artifact
uses: actions/upload-artifact@v4
with:
name: all-extensions
path: artifacts/
retention-days: 90

View File

@@ -51,6 +51,7 @@ function wrapScriptIntoIIFE() {
function makeAliases(rootDir) {
return {
"$config": path.resolve(rootDir, 'src/config'),
"$content": path.resolve(rootDir, 'src/content'),
"$lib": path.resolve(rootDir, 'src/lib'),
"$entities": path.resolve(rootDir, 'src/lib/extension/entities'),
"$styles": path.resolve(rootDir, 'src/styles'),

View File

@@ -16,7 +16,8 @@ below.
### Derpibooru Tagging Assistant
I wasn't able to release the extension for Derpibooru yet. Links will be available shortly.
[![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)
## Features

View File

@@ -1,7 +1,7 @@
{
"name": "Furbooru Tagging Assistant",
"description": "Small experimental extension for slightly quicker tagging experience. Furbooru Edition.",
"version": "0.5.0",
"version": "0.5.3",
"browser_specific_settings": {
"gecko": {
"id": "furbooru-tagging-assistant@thecore.city"
@@ -47,6 +47,9 @@
],
"js": [
"src/content/tags-editor.ts"
],
"css": [
"src/styles/content/tags-editor.scss"
]
},
{

2168
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.5.0",
"version": "0.5.3",
"private": true,
"scripts": {
"build": "npm run build:popup && npm run build:extension",
@@ -13,26 +13,26 @@
"test:watch": "vitest watch --coverage"
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.21.1",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@types/chrome": "^0.0.326",
"@types/node": "^22.15.29",
"@vitest/coverage-v8": "^3.2.0",
"cheerio": "^1.0.0",
"cross-env": "^10.0.0",
"jsdom": "^26.1.0",
"sass": "^1.89.1",
"svelte": "^5.33.14",
"svelte-check": "^4.2.1",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vitest": "^3.2.0"
"@sveltejs/vite-plugin-svelte": "^6.2.3",
"@types/chrome": "^0.1.32",
"@types/node": "^25.0.3",
"@vitest/coverage-v8": "^4.0.16",
"cheerio": "^1.1.2",
"cross-env": "^10.1.0",
"jsdom": "^27.4.0",
"svelte-check": "^4.3.5",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vitest": "^4.0.16"
},
"type": "module",
"dependencies": {
"@fortawesome/fontawesome-free": "^6.7.2",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.49.4",
"@fortawesome/fontawesome-free": "^7.1.0",
"amd-lite": "^1.0.1",
"lz-string": "^1.5.0"
"lz-string": "^1.5.0",
"sass": "^1.97.2",
"svelte": "^5.46.1"
}
}

View File

@@ -7,7 +7,7 @@
let { profile }: ProfileViewProps = $props();
const sortedTagsList = profile.settings.tags.sort((a, b) => a.localeCompare(b));
const sortedTagsList = $derived(profile.settings.tags.sort((a, b) => a.localeCompare(b)));
</script>
<div class="block">

View File

@@ -1,5 +1,9 @@
<script lang="ts">
import { PLUGIN_NAME } from "$lib/constants";
</script>
<header>
<a href="/">{__CURRENT_SITE_NAME__} Tagging Assistant</a>
<a href="/">{PLUGIN_NAME}</a>
</header>
<style lang="scss">

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import SelectField from "$components/ui/forms/SelectField.svelte";
import { categories } from "$lib/booru/tag-categories";
import { categories } from "$config/tags";
interface TagCategorySelectFieldProps {
value?: string;

View File

@@ -1,3 +1,23 @@
/**
* List of categories defined by the sites.
*/
export const categories: string[] = [
'rating',
'spoiler',
'origin',
'oc',
'error',
'character',
'content-official',
'content-fanmade',
'species',
'body-type',
];
/**
* List of tags which marked by the site as blacklisted. These tags are blocked from being added by the tag editor and
* should usually just be removed automatically.
*/
export const tagsBlacklist: string[] = (__CURRENT_SITE__ === 'furbooru' ? [
"anthro art",
"anthro artist",

View File

@@ -1,7 +1,7 @@
import { BaseComponent } from "$lib/components/base/BaseComponent";
import { BaseComponent } from "$content/components/base/BaseComponent";
import MiscSettings, { type FullscreenViewerSize } from "$lib/extension/settings/MiscSettings";
import { emit, on } from "$lib/components/events/comms";
import { EVENT_SIZE_LOADED } from "$lib/components/events/fullscreen-viewer-events";
import { emit, on } from "$content/components/events/comms";
import { EVENT_SIZE_LOADED } from "$content/components/events/fullscreen-viewer-events";
export class FullscreenViewer extends BaseComponent {
#videoElement: HTMLVideoElement = document.createElement('video');

View File

@@ -1,8 +1,8 @@
import { BaseComponent } from "$lib/components/base/BaseComponent";
import { getComponent } from "$lib/components/base/component-utils";
import { BaseComponent } from "$content/components/base/BaseComponent";
import { getComponent } from "$content/components/base/component-utils";
import MiscSettings from "$lib/extension/settings/MiscSettings";
import { FullscreenViewer } from "$lib/components/FullscreenViewer";
import type { MediaBoxTools } from "$lib/components/MediaBoxTools";
import { FullscreenViewer } from "$content/components/FullscreenViewer";
import type { MediaBoxTools } from "$content/components/MediaBoxTools";
export class ImageShowFullscreenButton extends BaseComponent {
#mediaBoxTools: MediaBoxTools | null = null;

View File

@@ -1,16 +1,16 @@
import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings";
import MaintenanceProfile from "$entities/MaintenanceProfile";
import { BaseComponent } from "$lib/components/base/BaseComponent";
import { getComponent } from "$lib/components/base/component-utils";
import { BaseComponent } from "$content/components/base/BaseComponent";
import { getComponent } from "$content/components/base/component-utils";
import ScrapedAPI from "$lib/booru/scraped/ScrapedAPI";
import { tagsBlacklist } from "$config/tags";
import { emitterAt } from "$lib/components/events/comms";
import { emitterAt } from "$content/components/events/comms";
import {
EVENT_ACTIVE_PROFILE_CHANGED,
EVENT_MAINTENANCE_STATE_CHANGED,
EVENT_TAGS_UPDATED
} from "$lib/components/events/maintenance-popup-events";
import type { MediaBoxTools } from "$lib/components/MediaBoxTools";
} from "$content/components/events/maintenance-popup-events";
import type { MediaBoxTools } from "$content/components/MediaBoxTools";
class BlackListedTagsEncounteredError extends Error {
constructor(tagName: string) {

View File

@@ -1,8 +1,8 @@
import { BaseComponent } from "$lib/components/base/BaseComponent";
import { getComponent } from "$lib/components/base/component-utils";
import { on } from "$lib/components/events/comms";
import { EVENT_MAINTENANCE_STATE_CHANGED } from "$lib/components/events/maintenance-popup-events";
import type { MediaBoxTools } from "$lib/components/MediaBoxTools";
import { BaseComponent } from "$content/components/base/BaseComponent";
import { getComponent } from "$content/components/base/component-utils";
import { on } from "$content/components/events/comms";
import { EVENT_MAINTENANCE_STATE_CHANGED } from "$content/components/events/maintenance-popup-events";
import type { MediaBoxTools } from "$content/components/MediaBoxTools";
export class MaintenanceStatusIcon extends BaseComponent {
#mediaBoxTools: MediaBoxTools | null = null;

View File

@@ -1,9 +1,9 @@
import { BaseComponent } from "$lib/components/base/BaseComponent";
import { getComponent } from "$lib/components/base/component-utils";
import { MaintenancePopup } from "$lib/components/MaintenancePopup";
import { on } from "$lib/components/events/comms";
import { EVENT_ACTIVE_PROFILE_CHANGED } from "$lib/components/events/maintenance-popup-events";
import type { MediaBoxWrapper } from "$lib/components/MediaBoxWrapper";
import { BaseComponent } from "$content/components/base/BaseComponent";
import { getComponent } from "$content/components/base/component-utils";
import { MaintenancePopup } from "$content/components/MaintenancePopup";
import { on } from "$content/components/events/comms";
import { EVENT_ACTIVE_PROFILE_CHANGED } from "$content/components/events/maintenance-popup-events";
import type { MediaBoxWrapper } from "$content/components/MediaBoxWrapper";
import type MaintenanceProfile from "$entities/MaintenanceProfile";
export class MediaBoxTools extends BaseComponent {

View File

@@ -1,8 +1,8 @@
import { BaseComponent } from "$lib/components/base/BaseComponent";
import { getComponent } from "$lib/components/base/component-utils";
import { BaseComponent } from "$content/components/base/BaseComponent";
import { getComponent } from "$content/components/base/component-utils";
import { buildTagsAndAliasesMap } from "$lib/booru/tag-utils";
import { on } from "$lib/components/events/comms";
import { EVENT_TAGS_UPDATED } from "$lib/components/events/maintenance-popup-events";
import { on } from "$content/components/events/comms";
import { EVENT_TAGS_UPDATED } from "$content/components/events/maintenance-popup-events";
export class MediaBoxWrapper extends BaseComponent {
#thumbnailContainer: HTMLElement | null = null;

View File

@@ -1,11 +1,11 @@
import { BaseComponent } from "$lib/components/base/BaseComponent";
import { BaseComponent } from "$content/components/base/BaseComponent";
import MaintenanceProfile from "$entities/MaintenanceProfile";
import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings";
import { getComponent } from "$lib/components/base/component-utils";
import { getComponent } from "$content/components/base/component-utils";
import CustomCategoriesResolver from "$lib/extension/CustomCategoriesResolver";
import { on } from "$lib/components/events/comms";
import { EVENT_FORM_EDITOR_UPDATED } from "$lib/components/events/tags-form-events";
import { EVENT_TAG_GROUP_RESOLVED } from "$lib/components/events/tag-dropdown-events";
import { on } from "$content/components/events/comms";
import { EVENT_FORM_EDITOR_UPDATED } from "$content/components/events/tags-form-events";
import { EVENT_TAG_GROUP_RESOLVED } from "$content/components/events/tag-dropdown-events";
import type TagGroup from "$entities/TagGroup";
const categoriesResolver = new CustomCategoriesResolver();
@@ -148,7 +148,12 @@ export class TagDropdownWrapper extends BaseComponent {
profileSpecificButtonText = `Remove from profile "${profileName}"`;
}
this.#toggleOnExistingButton.innerText = profileSpecificButtonText;
if (this.#toggleOnExistingButton.lastChild instanceof Text) {
this.#toggleOnExistingButton.lastChild.textContent = ` ${profileSpecificButtonText}`;
} else {
// Just in case last child is missing, then update the text on the full element.
this.#toggleOnExistingButton.textContent = profileSpecificButtonText;
}
if (!this.#toggleOnExistingButton.isConnected) {
this.#dropdownContainer?.append(this.#toggleOnExistingButton);
@@ -243,9 +248,14 @@ export class TagDropdownWrapper extends BaseComponent {
static #createDropdownLink(text: string, onClickHandler: (event: MouseEvent) => void): HTMLAnchorElement {
const dropdownLink = document.createElement('a');
dropdownLink.href = '#';
dropdownLink.innerText = text;
dropdownLink.className = 'tag__dropdown__link';
const dropdownLinkIcon = document.createElement('i');
dropdownLinkIcon.classList.add('fa', 'fa-tags');
dropdownLink.textContent = ` ${text}`;
dropdownLink.insertAdjacentElement('afterbegin', dropdownLinkIcon);
dropdownLink.addEventListener('click', event => {
event.preventDefault();
onClickHandler(event);

View File

@@ -1,8 +1,8 @@
import { BaseComponent } from "$lib/components/base/BaseComponent";
import { getComponent } from "$lib/components/base/component-utils";
import { emit, on, type UnsubscribeFunction } from "$lib/components/events/comms";
import { EVENT_FETCH_COMPLETE } from "$lib/components/events/booru-events";
import { EVENT_FORM_EDITOR_UPDATED } from "$lib/components/events/tags-form-events";
import { BaseComponent } from "$content/components/base/BaseComponent";
import { getComponent } from "$content/components/base/component-utils";
import { emit, on, type UnsubscribeFunction } from "$content/components/events/comms";
import { EVENT_FETCH_COMPLETE } from "$content/components/events/booru-events";
import { EVENT_FORM_EDITOR_UPDATED } from "$content/components/events/tags-form-events";
export class TagsForm extends BaseComponent {
protected init() {

View File

@@ -1,10 +1,10 @@
import { BaseComponent } from "$lib/components/base/BaseComponent";
import { BaseComponent } from "$content/components/base/BaseComponent";
import type TagGroup from "$entities/TagGroup";
import type { TagDropdownWrapper } from "$lib/components/TagDropdownWrapper";
import { on } from "$lib/components/events/comms";
import { EVENT_FORM_EDITOR_UPDATED } from "$lib/components/events/tags-form-events";
import { getComponent } from "$lib/components/base/component-utils";
import { EVENT_TAG_GROUP_RESOLVED } from "$lib/components/events/tag-dropdown-events";
import type { TagDropdownWrapper } from "$content/components/TagDropdownWrapper";
import { on } from "$content/components/events/comms";
import { EVENT_FORM_EDITOR_UPDATED } from "$content/components/events/tags-form-events";
import { getComponent } from "$content/components/base/component-utils";
import { EVENT_TAG_GROUP_RESOLVED } from "$content/components/events/tag-dropdown-events";
import TagSettings from "$lib/extension/settings/TagSettings";
export class TagsListBlock extends BaseComponent {
@@ -134,6 +134,7 @@ export class TagsListBlock extends BaseComponent {
heading.style.display = 'none';
heading.style.order = `var(${TagsListBlock.#orderCssVariableForGroup(group.id)}, 0)`;
heading.style.flexBasis = '100%';
heading.classList.add('tag-category-headline');
// We're inserting heading to the top just to make sure that heading is always in front of the tags related to
// this category.

View File

@@ -1,4 +1,4 @@
import { bindComponent } from "$lib/components/base/component-utils";
import { bindComponent } from "$content/components/base/component-utils";
type ComponentEventListener<EventName extends keyof HTMLElementEventMap> =
(this: HTMLElement, event: HTMLElementEventMap[EventName]) => void;

View File

@@ -1,4 +1,4 @@
import type { BaseComponent } from "$lib/components/base/BaseComponent";
import type { BaseComponent } from "$content/components/base/BaseComponent";
const instanceSymbol = Symbol.for('instance');

View File

@@ -1,9 +1,9 @@
import type { MaintenancePopupEventsMap } from "$lib/components/events/maintenance-popup-events";
import { BaseComponent } from "$lib/components/base/BaseComponent";
import type { FullscreenViewerEventsMap } from "$lib/components/events/fullscreen-viewer-events";
import type { BooruEventsMap } from "$lib/components/events/booru-events";
import type { TagsFormEventsMap } from "$lib/components/events/tags-form-events";
import type { TagDropdownEvents } from "$lib/components/events/tag-dropdown-events";
import type { MaintenancePopupEventsMap } from "$content/components/events/maintenance-popup-events";
import { BaseComponent } from "$content/components/base/BaseComponent";
import type { FullscreenViewerEventsMap } from "$content/components/events/fullscreen-viewer-events";
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";
type EventsMapping =
MaintenancePopupEventsMap

View File

@@ -1,5 +1,5 @@
import { BaseComponent } from "$lib/components/base/BaseComponent";
import { ImageListInfo } from "$lib/components/listing/ImageListInfo";
import { BaseComponent } from "$content/components/base/BaseComponent";
import { ImageListInfo } from "$content/components/listing/ImageListInfo";
export class ImageListContainer extends BaseComponent {
#info: ImageListInfo | null = null;

View File

@@ -1,4 +1,4 @@
import { BaseComponent } from "$lib/components/base/BaseComponent";
import { BaseComponent } from "$content/components/base/BaseComponent";
export class ImageListInfo extends BaseComponent {
#tagElement: HTMLElement | null = null;

View File

@@ -2,21 +2,52 @@ import { amdLite } from "amd-lite";
const originalDefine = amdLite.define;
/**
* Set of already defined modules. Used for deduplication.
*/
const definedModules = new Set<string>();
/**
* Throttle timer to make sure only one attempt at loading modules will run for a batch of loaded scripts.
*/
let throttledAutoRunTimer: NodeJS.Timeout | number | undefined;
/**
* Schedule the automatic resolving of all waiting modules on the next available frame.
*/
function scheduleModulesAutoRun() {
clearTimeout(throttledAutoRunTimer);
throttledAutoRunTimer = setTimeout(() => {
amdLite.resolveDependencies(Object.keys(amdLite.waitingModules));
});
}
amdLite.define = (name, dependencies, originalCallback) => {
return originalDefine(name, dependencies, function () {
// Chrome doesn't run the same content script multiple times, while Firefox does. Since each content script and their
// chunks are intended to be run only once, we should just ignore any attempts of running the same module more than
// once. Names of the modules are assumed to be unique.
if (definedModules.has(name)) {
return;
}
definedModules.add(name);
originalDefine(name, dependencies, function () {
const callbackResult = originalCallback(...arguments);
// Workaround for the entry script not returning anything causing AMD-Lite to send warning about modules not
// being loaded/not existing.
return typeof callbackResult !== 'undefined' ? callbackResult : {};
})
});
// Schedule the auto run on the next available frame. Firefox and Chromium have a lot of differences in how they
// decide to execute content scripts. For example, Firefox might decide to skip a frame before attempting to load
// different groups of them. Chromium on the other hand doesn't have that issue, but it doesn't allow us to, for
// example, schedule a microtask to run the modules.
scheduleModulesAutoRun();
}
amdLite.init({
publicScope: window
});
// We don't have anything asynchronous, so it's safe to execute everything on the next frame.
requestAnimationFrame(() => {
amdLite.resolveDependencies(Object.keys(amdLite.waitingModules))
});

View File

@@ -1,9 +1,9 @@
import { createMaintenancePopup } from "$lib/components/MaintenancePopup";
import { createMediaBoxTools } from "$lib/components/MediaBoxTools";
import { calculateMediaBoxesPositions, initializeMediaBox } from "$lib/components/MediaBoxWrapper";
import { createMaintenanceStatusIcon } from "$lib/components/MaintenanceStatusIcon";
import { createImageShowFullscreenButton } from "$lib/components/ImageShowFullscreenButton";
import { initializeImageListContainer } from "$lib/components/listing/ImageListContainer";
import { createMaintenancePopup } from "$content/components/MaintenancePopup";
import { createMediaBoxTools } from "$content/components/MediaBoxTools";
import { calculateMediaBoxesPositions, initializeMediaBox } from "$content/components/MediaBoxWrapper";
import { createMaintenanceStatusIcon } from "$content/components/MaintenanceStatusIcon";
import { createImageShowFullscreenButton } from "$content/components/ImageShowFullscreenButton";
import { initializeImageListContainer } from "$content/components/listing/ImageListContainer";
const mediaBoxes = document.querySelectorAll<HTMLElement>('.media-box');
const imageListContainer = document.querySelector<HTMLElement>('#imagelist-container');

View File

@@ -1,5 +1,5 @@
import { TagsForm } from "$lib/components/TagsForm";
import { initializeAllTagsLists, watchForUpdatedTagLists } from "$lib/components/TagsListBlock";
import { TagsForm } from "$content/components/TagsForm";
import { initializeAllTagsLists, watchForUpdatedTagLists } from "$content/components/TagsListBlock";
initializeAllTagsLists();
watchForUpdatedTagLists();

View File

@@ -1,4 +1,4 @@
import { watchTagDropdownsInTagsEditor, wrapTagDropdown } from "$lib/components/TagDropdownWrapper";
import { watchTagDropdownsInTagsEditor, wrapTagDropdown } from "$content/components/TagDropdownWrapper";
for (let tagDropdownElement of document.querySelectorAll<HTMLElement>('.tag.dropdown')) {
wrapTagDropdown(tagDropdownElement);

View File

@@ -1,12 +0,0 @@
export const categories = [
'rating',
'spoiler',
'origin',
'oc',
'error',
'character',
'content-official',
'content-fanmade',
'species',
'body-type',
];

4
src/lib/constants.ts Normal file
View File

@@ -0,0 +1,4 @@
/**
* Automatically generated name of the plugin.
*/
export const PLUGIN_NAME = __CURRENT_SITE_NAME__ + ' Tagging Assistant';

View File

@@ -1,8 +1,8 @@
import type { TagDropdownWrapper } from "$lib/components/TagDropdownWrapper";
import type { TagDropdownWrapper } from "$content/components/TagDropdownWrapper";
import TagGroup from "$entities/TagGroup";
import { escapeRegExp } from "$lib/utils";
import { emit } from "$lib/components/events/comms";
import { EVENT_TAG_GROUP_RESOLVED } from "$lib/components/events/tag-dropdown-events";
import { emit } from "$content/components/events/comms";
import { EVENT_TAG_GROUP_RESOLVED } from "$content/components/events/tag-dropdown-events";
export default class CustomCategoriesResolver {
#exactGroupMatches = new Map<string, TagGroup>();

View File

@@ -4,6 +4,7 @@
import Footer from "$components/layout/Footer.svelte";
import { initializeLinksReplacement } from "$lib/popup-links";
import { onDestroy } from "svelte";
import { headTitle } from "$stores/popup";
interface Props {
children?: import('svelte').Snippet;
@@ -22,6 +23,10 @@
})
</script>
<svelte:head>
<title>{$headTitle}</title>
</svelte:head>
<Header/>
<main>
{@render children?.()}

View File

@@ -4,6 +4,9 @@
import { activeProfileStore, maintenanceProfiles } from "$stores/entities/maintenance-profiles";
import MenuCheckboxItem from "$components/ui/menu/MenuCheckboxItem.svelte";
import MaintenanceProfile from "$entities/MaintenanceProfile";
import { popupTitle } from "$stores/popup";
$popupTitle = null;
let activeProfile = $derived<MaintenanceProfile | null>(
$maintenanceProfiles.find(profile => profile.id === $activeProfileStore) || null

View File

@@ -1,6 +1,10 @@
<script>
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { PLUGIN_NAME } from "$lib/constants";
import { popupTitle } from "$stores/popup";
$popupTitle = 'About';
let currentSiteUrl = 'https://furbooru.org';
@@ -14,7 +18,7 @@
<hr>
</Menu>
<h1>
{__CURRENT_SITE_NAME__} Tagging Assistant
{PLUGIN_NAME}
</h1>
<p>
This is a small tool to make tagging on {__CURRENT_SITE_NAME__} just a little bit more convenient. Group tags with

View File

@@ -3,6 +3,9 @@
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { tagGroups } from "$stores/entities/tag-groups";
import TagGroup from "$entities/TagGroup";
import { popupTitle } from "$stores/popup";
$popupTitle = 'Tag Groups';
let groups = $derived<TagGroup[]>($tagGroups.sort((a, b) => a.settings.name.localeCompare(b.settings.name)));
</script>

View File

@@ -6,6 +6,7 @@
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { tagGroups } from "$stores/entities/tag-groups";
import TagGroup from "$entities/TagGroup";
import { popupTitle } from "$stores/popup";
let groupId = $derived<string>(page.params.id);
let group = $derived<TagGroup | null>($tagGroups.find(group => group.id === groupId) || null);
@@ -19,6 +20,8 @@
if (!group) {
console.warn(`Group ${groupId} not found.`);
goto('/features/groups');
} else {
$popupTitle = `Tag Group: ${group.settings.name}`;
}
})
</script>

View File

@@ -5,6 +5,7 @@
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { tagGroups } from "$stores/entities/tag-groups";
import type TagGroup from "$entities/TagGroup";
import { popupTitle } from "$stores/popup";
const groupId = $derived<string>(page.params.id);
const targetGroup = $derived<TagGroup | undefined>($tagGroups.find(group => group.id === groupId));
@@ -12,6 +13,8 @@
$effect(() => {
if (!targetGroup) {
goto('/features/groups');
} else {
$popupTitle = `Deleting Tag Group: ${targetGroup.settings.name}`;
}
})

View File

@@ -12,6 +12,7 @@
import TagGroup from "$entities/TagGroup";
import { tagGroups } from "$stores/entities/tag-groups";
import CheckboxField from "$components/ui/forms/CheckboxField.svelte";
import { popupTitle } from "$stores/popup";
let groupId = $derived(page.params.id);
@@ -32,6 +33,7 @@
$effect(() => {
if (groupId === 'new') {
$popupTitle = 'Create Tag Group';
return;
}
@@ -40,6 +42,8 @@
return;
}
$popupTitle = `Edit Tag Group: ${targetGroup.settings.name}`;
groupName = targetGroup.settings.name;
tagsList = [...targetGroup.settings.tags].sort((a, b) => a.localeCompare(b));
prefixesList = [...targetGroup.settings.prefixes].sort((a, b) => a.localeCompare(b));

View File

@@ -8,6 +8,7 @@
import TagGroup from "$entities/TagGroup";
import EntitiesTransporter from "$lib/extension/EntitiesTransporter";
import { tagGroups } from "$stores/entities/tag-groups";
import { popupTitle } from "$stores/popup";
let isEncodedGroupShown = $state(true);
@@ -17,6 +18,8 @@
$effect(() => {
if (!group) {
goto('/features/groups');
} else {
$popupTitle = `Export Tag Group: ${group.settings.name}`;
}
});

View File

@@ -8,6 +8,7 @@
import TagGroup from "$entities/TagGroup";
import EntitiesTransporter from "$lib/extension/EntitiesTransporter";
import { tagGroups } from "$stores/entities/tag-groups";
import { popupTitle } from "$stores/popup";
const groupTransporter = new EntitiesTransporter(TagGroup);
@@ -17,6 +18,12 @@
let candidateGroup = $state<TagGroup | null>(null);
let existingGroup = $state<TagGroup | null>(null);
$effect(() => {
$popupTitle = candidateGroup
? 'Confirm Imported Tag Group'
: 'Import Tag Group';
});
function tryImportingGroup() {
candidateGroup = null;
existingGroup = null;

View File

@@ -4,6 +4,9 @@
import MenuRadioItem from "$components/ui/menu/MenuRadioItem.svelte";
import { activeProfileStore, maintenanceProfiles } from "$stores/entities/maintenance-profiles";
import MaintenanceProfile from "$entities/MaintenanceProfile";
import { popupTitle } from "$stores/popup";
$popupTitle = 'Tagging Profiles';
let profiles = $derived<MaintenanceProfile[]>(
$maintenanceProfiles.sort((a, b) => a.settings.name.localeCompare(b.settings.name))

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { page } from "$app/state";
@@ -8,7 +7,7 @@
import ProfileView from "$components/features/ProfileView.svelte";
import MenuCheckboxItem from "$components/ui/menu/MenuCheckboxItem.svelte";
import MaintenanceProfile from "$entities/MaintenanceProfile";
import { onMount } from "svelte";
import { popupTitle } from "$stores/popup";
let profileId = $derived(page.params.id);
let profile = $derived<MaintenanceProfile|null>(
@@ -24,6 +23,8 @@
if (!profile) {
console.warn(`Profile ${profileId} not found.`);
goto('/features/maintenance');
} else {
$popupTitle = `Tagging Profile: ${profile.settings.name}`;
}
});

View File

@@ -5,6 +5,7 @@
import { page } from "$app/state";
import { maintenanceProfiles } from "$stores/entities/maintenance-profiles";
import MaintenanceProfile from "$entities/MaintenanceProfile";
import { popupTitle } from "$stores/popup";
const profileId = $derived(page.params.id);
const targetProfile = $derived<MaintenanceProfile | null>(
@@ -14,6 +15,8 @@
$effect(() => {
if (!targetProfile) {
goto('/features/maintenance');
} else {
$popupTitle = `Deleting Tagging Profile: ${targetProfile.settings.name}`
}
});

View File

@@ -9,6 +9,7 @@
import { goto } from "$app/navigation";
import { maintenanceProfiles } from "$stores/entities/maintenance-profiles";
import MaintenanceProfile from "$entities/MaintenanceProfile";
import { popupTitle } from "$stores/popup";
let profileId = $derived(page.params.id);
@@ -26,6 +27,7 @@
$effect(() => {
if (profileId === 'new') {
$popupTitle = 'Create Tagging Profile';
return;
}
@@ -34,6 +36,8 @@
return;
}
$popupTitle = `Edit Tagging Profile: ${targetProfile.settings.name}`;
profileName = targetProfile.settings.name;
tagsList = [...targetProfile.settings.tags].sort((a, b) => a.localeCompare(b));
});

View File

@@ -8,6 +8,7 @@
import FormControl from "$components/ui/forms/FormControl.svelte";
import EntitiesTransporter from "$lib/extension/EntitiesTransporter";
import MaintenanceProfile from "$entities/MaintenanceProfile";
import { popupTitle } from "$stores/popup";
let isCompressedProfileShown = $state(true);
@@ -19,6 +20,8 @@
$effect(() => {
if (!profile) {
goto('/features/maintenance/');
} else {
$popupTitle = `Export Tagging Profile: ${profile.settings.name}`;
}
});

View File

@@ -8,6 +8,7 @@
import { maintenanceProfiles } from "$stores/entities/maintenance-profiles";
import { goto } from "$app/navigation";
import EntitiesTransporter from "$lib/extension/EntitiesTransporter";
import { popupTitle } from "$stores/popup";
const profilesTransporter = new EntitiesTransporter(MaintenanceProfile);
@@ -17,6 +18,11 @@
let candidateProfile = $state<MaintenanceProfile | null>(null);
let existingProfile = $state<MaintenanceProfile | null>(null);
$effect(() => {
$popupTitle = candidateProfile
? 'Confirm Imported Tagging Profile'
: 'Import Tagging Profile';
})
function tryImportingProfile() {
candidateProfile = null;
existingProfile = null;

View File

@@ -1,6 +1,9 @@
<script>
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { popupTitle } from "$stores/popup";
$popupTitle = 'Preferences';
</script>
<Menu>

View File

@@ -1,6 +1,9 @@
<script>
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { popupTitle } from "$stores/popup";
$popupTitle = 'Debugging Tools';
</script>
<Menu>

View File

@@ -2,6 +2,9 @@
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import Menu from "$components/ui/menu/Menu.svelte";
import { storagesCollection } from "$stores/debug";
import { popupTitle } from "$stores/popup";
$popupTitle = 'Storage Inspector';
</script>
<Menu>

View File

@@ -2,6 +2,7 @@
import StorageViewer from "$components/debugging/StorageViewer.svelte";
import { page } from "$app/state";
import { goto } from "$app/navigation";
import { popupTitle } from "$stores/popup";
let pathArray = $derived.by<string[]>(() => {
const pathString = page.params.path;
@@ -30,6 +31,10 @@
if (!storageName) {
goto("/preferences/debug/storage");
}
$popupTitle = storageName
? 'Inspecting: ' + storageName
: 'Storage Inspector';
});
</script>

View File

@@ -5,6 +5,9 @@
import FormControl from "$components/ui/forms/FormControl.svelte";
import CheckboxField from "$components/ui/forms/CheckboxField.svelte";
import { fullScreenViewerEnabled } from "$stores/preferences/misc";
import { popupTitle } from "$stores/popup";
$popupTitle = 'Misc Preferences';
</script>
<Menu>

View File

@@ -6,6 +6,9 @@
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { stripBlacklistedTagsEnabled } from "$stores/preferences/maintenance";
import { shouldSeparateTagGroups } from "$stores/preferences/tag";
import { popupTitle } from "$stores/popup";
$popupTitle = 'Tagging Preferences';
</script>
<Menu>

View File

@@ -1,6 +1,9 @@
<script lang="ts">
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { popupTitle } from "$stores/popup";
$popupTitle = 'Import/Export';
</script>
<Menu>

View File

@@ -8,6 +8,7 @@
import type StorageEntity from "$lib/extension/base/StorageEntity";
import FormControl from "$components/ui/forms/FormControl.svelte";
import FormContainer from "$components/ui/forms/FormContainer.svelte";
import { popupTitle } from "$stores/popup";
const bulkTransporter = new BulkEntitiesTransporter();
@@ -46,6 +47,12 @@
}
});
$effect(() => {
$popupTitle = displayExportedString
? 'Exported String'
: 'Select Entities to Export';
});
function refreshAreAllEntitiesChecked() {
requestAnimationFrame(() => {
exportAllProfiles = $maintenanceProfiles.every(profile => exportedEntities.profiles[profile.id]);

View File

@@ -14,6 +14,7 @@
import GroupView from "$components/features/GroupView.svelte";
import { goto } from "$app/navigation";
import type { SameSiteStatus } from "$lib/extension/EntitiesTransporter";
import { popupTitle } from "$stores/popup";
let importedString = $state('');
let errorMessage = $state('');
@@ -49,6 +50,16 @@
Boolean(importedProfiles.length || importedGroups.length)
);
$effect(() => {
$popupTitle = hasImportedEntities
? (
previewedEntity
? 'Preview of Imported Entity'
: 'Select & Preview Imported Entities'
)
: 'Import';
});
const transporter = new BulkEntitiesTransporter();
let lastImportStatus = $state<SameSiteStatus>(null);

15
src/stores/popup.ts Normal file
View File

@@ -0,0 +1,15 @@
import { derived, writable } from "svelte/store";
import { PLUGIN_NAME } from "$lib/constants";
/**
* Store containing the name of the subpage. This name will be added to the title alongside the name of the plugin.
*/
export const popupTitle = writable<string | null>(null);
/**
* Name of the current route with the name of the plugin appended to it.
*/
export const headTitle = derived(
popupTitle,
$popupTitle => ($popupTitle ? `${$popupTitle} | ` : '') + PLUGIN_NAME
);

View File

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

View File

@@ -68,15 +68,8 @@
.tag {
cursor: pointer;
user-select: none;
// Derpibooru has slight differences in how tags are displayed.
@if environment.$current-site == 'derpibooru' {
padding: 0 5px;
gap: 0;
}
@else {
padding: 5px;
}
padding: 0 5px;
gap: 0;
&:hover {
background: booru-vars.$resolved-tag-color;

View File

@@ -0,0 +1,13 @@
@use '$styles/booru-vars';
@use '$styles/environment';
h2.tag-category-headline {
// Basic margin top and bottom values gathered from Chrome.
$base-margin-top: .83em;
$base-margin-bottom: .62em;
margin: {
top: calc(#{$base-margin-top} - #{booru-vars.$padding-small});
bottom: calc(#{$base-margin-bottom} - #{booru-vars.$padding-small});
}
}

View File

@@ -3,7 +3,6 @@
.tag {
background: colors.$tag-background;
line-height: 28px;
color: colors.$tag-text;
font-weight: 700;
font-size: 14px;
@@ -12,6 +11,10 @@
@if environment.$current-site == 'derpibooru' {
border: 1px solid colors.$tag-border;
line-height: 24px;
}
@else {
line-height: 28px;
}
.remove {

View File

@@ -15,6 +15,7 @@ const config = {
},
alias: {
"$config": "./src/config",
"$content": "./src/content",
"$components": "./src/components",
"$styles": "./src/styles",
"$stores": "./src/stores",

View File

@@ -1,5 +1,5 @@
import { BaseComponent } from "$lib/components/base/BaseComponent";
import { getComponent } from "$lib/components/base/component-utils";
import { BaseComponent } from "$content/components/base/BaseComponent";
import { getComponent } from "$content/components/base/component-utils";
import { randomString } from "$tests/utils";
describe('BaseComponent', () => {