From 83c7608e999c6eee23514dbb4a4fe6f363879b47 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 22 Mar 2026 04:09:18 +0400 Subject: [PATCH 01/18] 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. --- src/components/features/PresetView.svelte | 6 ++ .../extension/presets/PresetTableRow.ts | 59 ++++++++++++++++++- src/lib/extension/entities/TagEditorPreset.ts | 2 + src/lib/extension/transporting/exporters.ts | 1 + .../features/presets/[id]/edit/+page.svelte | 9 +++ src/styles/booru-vars.scss | 1 + src/styles/content/tag-presets.scss | 7 +++ 7 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/components/features/PresetView.svelte b/src/components/features/PresetView.svelte index 18c24ca..4dcc1b4 100644 --- a/src/components/features/PresetView.svelte +++ b/src/components/features/PresetView.svelte @@ -18,3 +18,9 @@ +{#if preset.settings.exclusive} + + 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. + +{/if} diff --git a/src/content/components/extension/presets/PresetTableRow.ts b/src/content/components/extension/presets/PresetTableRow.ts index 61bcafd..025e3da 100644 --- a/src/content/components/extension/presets/PresetTableRow.ts +++ b/src/content/components/extension/presets/PresetTableRow.ts @@ -9,6 +9,7 @@ export default class PresetTableRow extends BaseComponent { #tagsList: HTMLElement[] = []; #applyAllButton = document.createElement('button'); #removeAllButton = document.createElement('button'); + #exclusiveWarning = document.createElement('div'); constructor(container: HTMLElement, preset: TagEditorPreset) { super(container); @@ -32,6 +33,7 @@ export default class PresetTableRow extends BaseComponent { 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'); @@ -52,6 +54,18 @@ export default class PresetTableRow extends BaseComponent { 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, @@ -85,6 +99,30 @@ export default class PresetTableRow extends BaseComponent { 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]) }); @@ -109,11 +147,29 @@ export default class PresetTableRow extends BaseComponent { } updateTags(tags: Set) { + let presentTagsAmount = 0; + for (const tagElement of this.#tagsList) { - tagElement.classList.toggle( + 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'; + } } } @@ -126,4 +182,5 @@ export default class PresetTableRow extends BaseComponent { } static #tagMissingClassName = 'is-missing'; + static #presetWarningClassName = 'has-warning'; } diff --git a/src/lib/extension/entities/TagEditorPreset.ts b/src/lib/extension/entities/TagEditorPreset.ts index d904d5f..3932862 100644 --- a/src/lib/extension/entities/TagEditorPreset.ts +++ b/src/lib/extension/entities/TagEditorPreset.ts @@ -3,6 +3,7 @@ import StorageEntity from "$lib/extension/base/StorageEntity"; interface TagEditorPresetSettings { name: string; tags: string[]; + exclusive: boolean; } export default class TagEditorPreset extends StorageEntity { @@ -10,6 +11,7 @@ export default class TagEditorPreset extends StorageEntity([]); + let isExclusive = $state(false); $effect(() => { if (presetId === 'new') { @@ -39,6 +41,7 @@ presetName = targetPreset.settings.name; tagsList = [...targetPreset.settings.tags].sort((a, b) => a.localeCompare(b)); + isExclusive = targetPreset.settings.exclusive; }); async function savePreset() { @@ -49,6 +52,7 @@ targetPreset.settings.name = presetName; targetPreset.settings.tags = [...tagsList]; + targetPreset.settings.exclusive = isExclusive; await targetPreset.save(); await goto(`/features/presets/${targetPreset.id}`); @@ -67,6 +71,11 @@ + + + Keep only one tag from this preset active at a time. + +
diff --git a/src/styles/booru-vars.scss b/src/styles/booru-vars.scss index 462d9c5..1f9d5f6 100644 --- a/src/styles/booru-vars.scss +++ b/src/styles/booru-vars.scss @@ -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); diff --git a/src/styles/content/tag-presets.scss b/src/styles/content/tag-presets.scss index cf24ef6..30450c3 100644 --- a/src/styles/content/tag-presets.scss +++ b/src/styles/content/tag-presets.scss @@ -13,4 +13,11 @@ background: booru-vars.$resolved-tag-color; } } + + .block.block--fixed.block--warning { + margin: { + top: booru-vars.$block-spacing; + bottom: 0; + } + } } From 9b262393fa3c769d053ce6e51f1a1e9cb19b237a Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 29 Mar 2026 03:48:12 +0400 Subject: [PATCH 02/18] Fixed event & underlying type not updated --- .../components/events/maintenance-popup-events.ts | 6 +++--- .../extension/profiles/TaggingProfilePopup.ts | 10 +++++----- .../extension/profiles/TaggingProfileStatusIcon.ts | 9 ++++++--- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/content/components/events/maintenance-popup-events.ts b/src/content/components/events/maintenance-popup-events.ts index 95c780f..89bc9c4 100644 --- a/src/content/components/events/maintenance-popup-events.ts +++ b/src/content/components/events/maintenance-popup-events.ts @@ -1,13 +1,13 @@ import type TaggingProfile from "$entities/TaggingProfile"; export const EVENT_ACTIVE_PROFILE_CHANGED = 'active-profile-changed'; -export const EVENT_MAINTENANCE_STATE_CHANGED = 'maintenance-state-change'; +export const EVENT_PROFILE_POPUP_STATE_CHANGED = 'maintenance-state-change'; export const EVENT_TAGS_UPDATED = 'tags-updated'; -type MaintenanceState = 'processing' | 'failed' | 'complete' | 'waiting'; +export type ProfilePopupState = 'ready' | 'processing' | 'failed' | 'complete' | 'waiting'; export interface MaintenancePopupEventsMap { [EVENT_ACTIVE_PROFILE_CHANGED]: TaggingProfile | null; - [EVENT_MAINTENANCE_STATE_CHANGED]: MaintenanceState; + [EVENT_PROFILE_POPUP_STATE_CHANGED]: ProfilePopupState; [EVENT_TAGS_UPDATED]: Map | null; } diff --git a/src/content/components/extension/profiles/TaggingProfilePopup.ts b/src/content/components/extension/profiles/TaggingProfilePopup.ts index a63b9b4..14074fc 100644 --- a/src/content/components/extension/profiles/TaggingProfilePopup.ts +++ b/src/content/components/extension/profiles/TaggingProfilePopup.ts @@ -7,7 +7,7 @@ import { tagsBlacklist } from "$config/tags"; import { emitterAt } from "$content/components/events/comms"; import { EVENT_ACTIVE_PROFILE_CHANGED, - EVENT_MAINTENANCE_STATE_CHANGED, + EVENT_PROFILE_POPUP_STATE_CHANGED, EVENT_TAGS_UPDATED } from "$content/components/events/maintenance-popup-events"; import type { MediaBoxTools } from "$content/components/extension/MediaBoxTools"; @@ -183,7 +183,7 @@ export class TaggingProfilePopup extends BaseComponent { } this.#isPlanningToSubmit = true; - this.#emitter.emit(EVENT_MAINTENANCE_STATE_CHANGED, 'waiting'); + this.#emitter.emit(EVENT_PROFILE_POPUP_STATE_CHANGED, 'waiting'); } } @@ -210,7 +210,7 @@ export class TaggingProfilePopup extends BaseComponent { this.#isPlanningToSubmit = false; this.#isSubmitting = true; - this.#emitter.emit(EVENT_MAINTENANCE_STATE_CHANGED, 'processing'); + this.#emitter.emit(EVENT_PROFILE_POPUP_STATE_CHANGED, 'processing'); let maybeTagsAndAliasesAfterUpdate; @@ -252,7 +252,7 @@ export class TaggingProfilePopup extends BaseComponent { TaggingProfilePopup.#notifyAboutPendingSubmission(false); - this.#emitter.emit(EVENT_MAINTENANCE_STATE_CHANGED, 'failed'); + this.#emitter.emit(EVENT_PROFILE_POPUP_STATE_CHANGED, 'failed'); this.#isSubmitting = false; return; @@ -262,7 +262,7 @@ export class TaggingProfilePopup extends BaseComponent { this.#emitter.emit(EVENT_TAGS_UPDATED, maybeTagsAndAliasesAfterUpdate); } - this.#emitter.emit(EVENT_MAINTENANCE_STATE_CHANGED, 'complete'); + this.#emitter.emit(EVENT_PROFILE_POPUP_STATE_CHANGED, 'complete'); this.#tagsToAdd.clear(); this.#tagsToRemove.clear(); diff --git a/src/content/components/extension/profiles/TaggingProfileStatusIcon.ts b/src/content/components/extension/profiles/TaggingProfileStatusIcon.ts index 6e5ab8b..b1b1246 100644 --- a/src/content/components/extension/profiles/TaggingProfileStatusIcon.ts +++ b/src/content/components/extension/profiles/TaggingProfileStatusIcon.ts @@ -1,7 +1,10 @@ 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 { + EVENT_PROFILE_POPUP_STATE_CHANGED, + type ProfilePopupState +} from "$content/components/events/maintenance-popup-events"; import type { MediaBoxTools } from "$content/components/extension/MediaBoxTools"; export class TaggingProfileStatusIcon extends BaseComponent { @@ -22,10 +25,10 @@ export class TaggingProfileStatusIcon extends BaseComponent { throw new Error('Status icon element initialized outside of the media box!'); } - on(this.#mediaBoxTools, EVENT_MAINTENANCE_STATE_CHANGED, this.#onMaintenanceStateChanged.bind(this)); + on(this.#mediaBoxTools, EVENT_PROFILE_POPUP_STATE_CHANGED, this.#onMaintenanceStateChanged.bind(this)); } - #onMaintenanceStateChanged(stateChangeEvent: CustomEvent) { + #onMaintenanceStateChanged(stateChangeEvent: CustomEvent) { // TODO Replace those with FontAwesome icons later. Those icons can probably be sourced from the website itself. switch (stateChangeEvent.detail) { case "ready": From c36929b8240cc562954bc4ea1a542fc658c2c9e8 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 29 Mar 2026 03:49:22 +0400 Subject: [PATCH 03/18] Renaming the file with events as well --- src/content/components/events/comms.ts | 2 +- ...ntenance-popup-events.ts => tagging-profile-popup-events.ts} | 0 src/content/components/extension/MediaBoxTools.ts | 2 +- .../components/extension/profiles/TaggingProfilePopup.ts | 2 +- .../components/extension/profiles/TaggingProfileStatusIcon.ts | 2 +- src/content/components/philomena/MediaBox.ts | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename src/content/components/events/{maintenance-popup-events.ts => tagging-profile-popup-events.ts} (100%) diff --git a/src/content/components/events/comms.ts b/src/content/components/events/comms.ts index 7127a25..965087a 100644 --- a/src/content/components/events/comms.ts +++ b/src/content/components/events/comms.ts @@ -1,4 +1,4 @@ -import type { MaintenancePopupEventsMap } from "$content/components/events/maintenance-popup-events"; +import type { MaintenancePopupEventsMap } from "$content/components/events/tagging-profile-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"; diff --git a/src/content/components/events/maintenance-popup-events.ts b/src/content/components/events/tagging-profile-popup-events.ts similarity index 100% rename from src/content/components/events/maintenance-popup-events.ts rename to src/content/components/events/tagging-profile-popup-events.ts diff --git a/src/content/components/extension/MediaBoxTools.ts b/src/content/components/extension/MediaBoxTools.ts index 0c05ee5..832512b 100644 --- a/src/content/components/extension/MediaBoxTools.ts +++ b/src/content/components/extension/MediaBoxTools.ts @@ -2,7 +2,7 @@ import { BaseComponent } from "$content/components/base/BaseComponent"; import { getComponent } from "$content/components/base/component-utils"; import { TaggingProfilePopup } from "$content/components/extension/profiles/TaggingProfilePopup"; import { on } from "$content/components/events/comms"; -import { EVENT_ACTIVE_PROFILE_CHANGED } from "$content/components/events/maintenance-popup-events"; +import { EVENT_ACTIVE_PROFILE_CHANGED } from "$content/components/events/tagging-profile-popup-events"; import type { MediaBox } from "$content/components/philomena/MediaBox"; import type TaggingProfile from "$entities/TaggingProfile"; diff --git a/src/content/components/extension/profiles/TaggingProfilePopup.ts b/src/content/components/extension/profiles/TaggingProfilePopup.ts index 14074fc..6aa2cc3 100644 --- a/src/content/components/extension/profiles/TaggingProfilePopup.ts +++ b/src/content/components/extension/profiles/TaggingProfilePopup.ts @@ -9,7 +9,7 @@ import { EVENT_ACTIVE_PROFILE_CHANGED, EVENT_PROFILE_POPUP_STATE_CHANGED, EVENT_TAGS_UPDATED -} from "$content/components/events/maintenance-popup-events"; +} from "$content/components/events/tagging-profile-popup-events"; import type { MediaBoxTools } from "$content/components/extension/MediaBoxTools"; import { resolveTagCategoryFromTagName } from "$lib/philomena/tag-utils"; diff --git a/src/content/components/extension/profiles/TaggingProfileStatusIcon.ts b/src/content/components/extension/profiles/TaggingProfileStatusIcon.ts index b1b1246..2958104 100644 --- a/src/content/components/extension/profiles/TaggingProfileStatusIcon.ts +++ b/src/content/components/extension/profiles/TaggingProfileStatusIcon.ts @@ -4,7 +4,7 @@ import { on } from "$content/components/events/comms"; import { EVENT_PROFILE_POPUP_STATE_CHANGED, type ProfilePopupState -} from "$content/components/events/maintenance-popup-events"; +} from "$content/components/events/tagging-profile-popup-events"; import type { MediaBoxTools } from "$content/components/extension/MediaBoxTools"; export class TaggingProfileStatusIcon extends BaseComponent { diff --git a/src/content/components/philomena/MediaBox.ts b/src/content/components/philomena/MediaBox.ts index 2403b69..c7b1cc6 100644 --- a/src/content/components/philomena/MediaBox.ts +++ b/src/content/components/philomena/MediaBox.ts @@ -2,7 +2,7 @@ import { BaseComponent } from "$content/components/base/BaseComponent"; import { getComponent } from "$content/components/base/component-utils"; import { buildTagsAndAliasesMap } from "$lib/philomena/tag-utils"; import { on } from "$content/components/events/comms"; -import { EVENT_TAGS_UPDATED } from "$content/components/events/maintenance-popup-events"; +import { EVENT_TAGS_UPDATED } from "$content/components/events/tagging-profile-popup-events"; export class MediaBox extends BaseComponent { #thumbnailContainer: HTMLElement | null = null; From daceb9ad59461ff7d4cc905da8190d003e6ac281 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 29 Mar 2026 03:57:06 +0400 Subject: [PATCH 04/18] Cancel the planned submission when pending changes canceled --- .../extension/profiles/TaggingProfilePopup.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/content/components/extension/profiles/TaggingProfilePopup.ts b/src/content/components/extension/profiles/TaggingProfilePopup.ts index 6aa2cc3..d551336 100644 --- a/src/content/components/extension/profiles/TaggingProfilePopup.ts +++ b/src/content/components/extension/profiles/TaggingProfilePopup.ts @@ -185,6 +185,19 @@ export class TaggingProfilePopup extends BaseComponent { this.#isPlanningToSubmit = true; this.#emitter.emit(EVENT_PROFILE_POPUP_STATE_CHANGED, 'waiting'); } + + // Whenever user undoes the change they wanted to do in the popup, it's better to not send the submission and just + // do nothing. + if (!this.#tagsToAdd.size && !this.#tagsToRemove.size && this.#isPlanningToSubmit) { + this.#isPlanningToSubmit = false; + this.#emitter.emit(EVENT_PROFILE_POPUP_STATE_CHANGED, 'ready'); + TaggingProfilePopup.#notifyAboutPendingSubmission(false); + + // Probably shouldn't ever happen, but make sure we cancel any delayed submission. + if (this.#tagsSubmissionTimer) { + clearTimeout(this.#tagsSubmissionTimer); + } + } } #onMouseEnteredArea() { From e8b0afc81f62d61036f15a8368bce00181481000 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 29 Mar 2026 04:17:12 +0400 Subject: [PATCH 05/18] And even more renaming of popup across multiple files --- src/content/components/events/comms.ts | 4 ++-- .../components/events/tagging-profile-popup-events.ts | 2 +- src/content/components/extension/MediaBoxTools.ts | 10 +++------- .../extension/profiles/TaggingProfilePopup.ts | 4 ++-- .../extension/profiles/TaggingProfileStatusIcon.ts | 4 ++-- 5 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/content/components/events/comms.ts b/src/content/components/events/comms.ts index 965087a..e5ba62a 100644 --- a/src/content/components/events/comms.ts +++ b/src/content/components/events/comms.ts @@ -1,4 +1,4 @@ -import type { MaintenancePopupEventsMap } from "$content/components/events/tagging-profile-popup-events"; +import type { TaggingProfilePopupEventsMap } from "$content/components/events/tagging-profile-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"; @@ -7,7 +7,7 @@ import type { TagDropdownEvents } from "$content/components/events/tag-dropdown- import type { PresetBlockEventsMap } from "$content/components/events/preset-block-events"; type EventsMapping = - MaintenancePopupEventsMap + TaggingProfilePopupEventsMap & FullscreenViewerEventsMap & BooruEventsMap & TagsFormEventsMap diff --git a/src/content/components/events/tagging-profile-popup-events.ts b/src/content/components/events/tagging-profile-popup-events.ts index 89bc9c4..668c850 100644 --- a/src/content/components/events/tagging-profile-popup-events.ts +++ b/src/content/components/events/tagging-profile-popup-events.ts @@ -6,7 +6,7 @@ export const EVENT_TAGS_UPDATED = 'tags-updated'; export type ProfilePopupState = 'ready' | 'processing' | 'failed' | 'complete' | 'waiting'; -export interface MaintenancePopupEventsMap { +export interface TaggingProfilePopupEventsMap { [EVENT_ACTIVE_PROFILE_CHANGED]: TaggingProfile | null; [EVENT_PROFILE_POPUP_STATE_CHANGED]: ProfilePopupState; [EVENT_TAGS_UPDATED]: Map | null; diff --git a/src/content/components/extension/MediaBoxTools.ts b/src/content/components/extension/MediaBoxTools.ts index 832512b..1de65b7 100644 --- a/src/content/components/extension/MediaBoxTools.ts +++ b/src/content/components/extension/MediaBoxTools.ts @@ -8,7 +8,7 @@ import type TaggingProfile from "$entities/TaggingProfile"; export class MediaBoxTools extends BaseComponent { #mediaBox: MediaBox | null = null; - #maintenancePopup: TaggingProfilePopup | null = null; + #popup: TaggingProfilePopup | null = null; init() { const mediaBoxElement = this.container.closest('.media-box'); @@ -34,8 +34,8 @@ export class MediaBoxTools extends BaseComponent { component.initialize(); } - if (!this.#maintenancePopup && component instanceof TaggingProfilePopup) { - this.#maintenancePopup = component; + if (!this.#popup && component instanceof TaggingProfilePopup) { + this.#popup = component; } } @@ -46,10 +46,6 @@ export class MediaBoxTools extends BaseComponent { this.container.classList.toggle('has-active-profile', profileChangedEvent.detail !== null); } - get maintenancePopup(): TaggingProfilePopup | null { - return this.#maintenancePopup; - } - get mediaBox(): MediaBox | null { return this.#mediaBox; } diff --git a/src/content/components/extension/profiles/TaggingProfilePopup.ts b/src/content/components/extension/profiles/TaggingProfilePopup.ts index d551336..0b912cd 100644 --- a/src/content/components/extension/profiles/TaggingProfilePopup.ts +++ b/src/content/components/extension/profiles/TaggingProfilePopup.ts @@ -373,7 +373,7 @@ export class TaggingProfilePopup extends BaseComponent { } }); - const unsubscribeFromMaintenanceSettings = this.#preferences.subscribe(settings => { + const unsubscribeFromPreferences = this.#preferences.subscribe(settings => { if (settings.activeProfile === lastActiveProfileId) { return; } @@ -395,7 +395,7 @@ export class TaggingProfilePopup extends BaseComponent { return () => { unsubscribeFromProfilesChanges(); - unsubscribeFromMaintenanceSettings(); + unsubscribeFromPreferences(); } } diff --git a/src/content/components/extension/profiles/TaggingProfileStatusIcon.ts b/src/content/components/extension/profiles/TaggingProfileStatusIcon.ts index 2958104..dc77501 100644 --- a/src/content/components/extension/profiles/TaggingProfileStatusIcon.ts +++ b/src/content/components/extension/profiles/TaggingProfileStatusIcon.ts @@ -25,10 +25,10 @@ export class TaggingProfileStatusIcon extends BaseComponent { throw new Error('Status icon element initialized outside of the media box!'); } - on(this.#mediaBoxTools, EVENT_PROFILE_POPUP_STATE_CHANGED, this.#onMaintenanceStateChanged.bind(this)); + on(this.#mediaBoxTools, EVENT_PROFILE_POPUP_STATE_CHANGED, this.#onPopupStateChanged.bind(this)); } - #onMaintenanceStateChanged(stateChangeEvent: CustomEvent) { + #onPopupStateChanged(stateChangeEvent: CustomEvent) { // TODO Replace those with FontAwesome icons later. Those icons can probably be sourced from the website itself. switch (stateChangeEvent.detail) { case "ready": From f581f840653be4893abcae80f6afe8c0716ba16a Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 5 Apr 2026 17:52:15 +0400 Subject: [PATCH 06/18] Presets: Added conditional presets option Now presets can be configured to show up only when specific tag is provided. --- src/lib/extension/entities/TagEditorPreset.ts | 4 ++++ src/lib/extension/transporting/exporters.ts | 2 ++ src/lib/extension/transporting/validators.ts | 2 ++ .../features/presets/[id]/edit/+page.svelte | 17 +++++++++++++++++ 4 files changed, 25 insertions(+) diff --git a/src/lib/extension/entities/TagEditorPreset.ts b/src/lib/extension/entities/TagEditorPreset.ts index d904d5f..681fc61 100644 --- a/src/lib/extension/entities/TagEditorPreset.ts +++ b/src/lib/extension/entities/TagEditorPreset.ts @@ -3,6 +3,8 @@ import StorageEntity from "$lib/extension/base/StorageEntity"; interface TagEditorPresetSettings { name: string; tags: string[]; + conditional: boolean; + requiredTags: string[]; } export default class TagEditorPreset extends StorageEntity { @@ -10,6 +12,8 @@ export default class TagEditorPreset extends StorageEntity([]); + let isConditional = $state(false); + let requiredTags = $state([]); $effect(() => { if (presetId === 'new') { @@ -39,6 +42,8 @@ presetName = targetPreset.settings.name; tagsList = [...targetPreset.settings.tags].sort((a, b) => a.localeCompare(b)); + isConditional = targetPreset.settings.conditional; + requiredTags = [...targetPreset.settings.requiredTags].sort((a, b) => a.localeCompare(b)); }); async function savePreset() { @@ -49,6 +54,8 @@ targetPreset.settings.name = presetName; targetPreset.settings.tags = [...tagsList]; + targetPreset.settings.conditional = isConditional; + targetPreset.settings.requiredTags = [...requiredTags]; await targetPreset.save(); await goto(`/features/presets/${targetPreset.id}`); @@ -67,6 +74,16 @@ + + + Show this preset only when specified tags are provided. + + + {#if isConditional} + + + + {/if}
From 399e75809b4b3c29cf9f697be5ededa6daabcb5f Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 5 Apr 2026 17:52:35 +0400 Subject: [PATCH 07/18] Display presets when tag is found or hide it otherwise --- .../extension/presets/PresetTableRow.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/content/components/extension/presets/PresetTableRow.ts b/src/content/components/extension/presets/PresetTableRow.ts index 61bcafd..4e6a453 100644 --- a/src/content/components/extension/presets/PresetTableRow.ts +++ b/src/content/components/extension/presets/PresetTableRow.ts @@ -9,6 +9,7 @@ export default class PresetTableRow extends BaseComponent { #tagsList: HTMLElement[] = []; #applyAllButton = document.createElement('button'); #removeAllButton = document.createElement('button'); + #alternateColorDummy = document.createElement('span'); constructor(container: HTMLElement, preset: TagEditorPreset) { super(container); @@ -64,6 +65,8 @@ export default class PresetTableRow extends BaseComponent { tagsCell, actionsCell, ); + + this.#alternateColorDummy.style.display = 'none'; } protected init() { @@ -108,6 +111,26 @@ export default class PresetTableRow extends BaseComponent { }); } + #maybeRefreshVisibilityFromTags(sourceTags: Set) { + if (!this.#preset.settings.conditional || this.#isMatchesConditional(sourceTags)) { + this.container.style.display = ''; + this.#alternateColorDummy.remove(); + return; + } + + this.container.style.display = 'none'; + this.container.after(this.#alternateColorDummy); + } + + #isMatchesConditional(sourceTags: Set): boolean { + const listOfRequiredTags = this.#preset.settings.requiredTags; + + return Boolean( + listOfRequiredTags.length + && listOfRequiredTags.some(tagName => sourceTags.has(tagName)) + ); + } + updateTags(tags: Set) { for (const tagElement of this.#tagsList) { tagElement.classList.toggle( @@ -115,6 +138,8 @@ export default class PresetTableRow extends BaseComponent { !tags.has(tagElement.dataset.tagName || ''), ); } + + this.#maybeRefreshVisibilityFromTags(tags); } remove() { From b031b88512e2358dd22603d942048a105954cf61 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 5 Apr 2026 17:52:46 +0400 Subject: [PATCH 08/18] Fixed tags being user-selectable --- src/styles/content/tag-presets.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/styles/content/tag-presets.scss b/src/styles/content/tag-presets.scss index cf24ef6..9b7bdcc 100644 --- a/src/styles/content/tag-presets.scss +++ b/src/styles/content/tag-presets.scss @@ -3,6 +3,7 @@ .block.tag-presets { .tag { cursor: pointer; + user-select: none; &.is-missing { opacity: 0.5; From a45248cebffc4bb47bfdd115b5ec338323d9749c Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 5 Apr 2026 18:02:54 +0400 Subject: [PATCH 09/18] Fixed crashing build due to missing trailing comma --- src/lib/extension/entities/TagEditorPreset.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/extension/entities/TagEditorPreset.ts b/src/lib/extension/entities/TagEditorPreset.ts index 360bf9a..ba708a6 100644 --- a/src/lib/extension/entities/TagEditorPreset.ts +++ b/src/lib/extension/entities/TagEditorPreset.ts @@ -13,7 +13,7 @@ export default class TagEditorPreset extends StorageEntity Date: Sun, 5 Apr 2026 18:18:52 +0400 Subject: [PATCH 10/18] Show when preset is conditional in the viewer block --- src/components/features/PresetView.svelte | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/features/PresetView.svelte b/src/components/features/PresetView.svelte index 4dcc1b4..bdd1a32 100644 --- a/src/components/features/PresetView.svelte +++ b/src/components/features/PresetView.svelte @@ -10,6 +10,7 @@ let { preset }: PresetViewProps = $props(); const sortedTagsList = $derived(preset.settings.tags.toSorted((a, b) => a.localeCompare(b))); + const requiredTagsList = $derived(preset.settings.requiredTags.toSorted((a, b) => a.localeCompare(b))); @@ -24,3 +25,11 @@ be automatically removed from the editor. {/if} +{#if preset.settings.conditional} + + This preset will only appear when one of the tags below are present on image. + + + + +{/if} From 3f9412b02de545fe13be4090fc4467e11c8bb3b7 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 5 Apr 2026 18:27:02 +0400 Subject: [PATCH 11/18] A bit more concrete wording for checkbox --- src/routes/features/presets/[id]/edit/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/features/presets/[id]/edit/+page.svelte b/src/routes/features/presets/[id]/edit/+page.svelte index 1af79a5..41dfd77 100644 --- a/src/routes/features/presets/[id]/edit/+page.svelte +++ b/src/routes/features/presets/[id]/edit/+page.svelte @@ -84,7 +84,7 @@ - Show this preset only when specified tags are provided. + Show this preset only when any of specified tags are provided. {#if isConditional} From 66fd093e5ae32dbc09702c1c2993596ab49d3166 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 5 Apr 2026 18:29:47 +0400 Subject: [PATCH 12/18] Marking unchanged properties as readonly --- .../components/extension/presets/PresetTableRow.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/content/components/extension/presets/PresetTableRow.ts b/src/content/components/extension/presets/PresetTableRow.ts index 0de7860..a38fa9e 100644 --- a/src/content/components/extension/presets/PresetTableRow.ts +++ b/src/content/components/extension/presets/PresetTableRow.ts @@ -5,12 +5,13 @@ import { EVENT_PRESET_TAG_CHANGE_APPLIED } from "$content/components/events/pres import { createFontAwesomeIcon } from "$lib/dom-utils"; export default class PresetTableRow extends BaseComponent { - #preset: TagEditorPreset; + readonly #preset: TagEditorPreset; + readonly #applyAllButton = document.createElement('button'); + readonly #removeAllButton = document.createElement('button'); + readonly #exclusiveWarning = document.createElement('div'); + readonly #alternateColorDummy = document.createElement('span'); + #tagsList: HTMLElement[] = []; - #applyAllButton = document.createElement('button'); - #removeAllButton = document.createElement('button'); - #exclusiveWarning = document.createElement('div'); - #alternateColorDummy = document.createElement('span'); constructor(container: HTMLElement, preset: TagEditorPreset) { super(container); From b4419b5de320a55f5505d4ad3cf0316daa152df6 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 5 Apr 2026 18:33:34 +0400 Subject: [PATCH 13/18] Validate `exclusive` and `conditional` as optional booleans --- src/lib/extension/transporting/validators.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/lib/extension/transporting/validators.ts b/src/lib/extension/transporting/validators.ts index 510a591..53ceceb 100644 --- a/src/lib/extension/transporting/validators.ts +++ b/src/lib/extension/transporting/validators.ts @@ -32,6 +32,14 @@ function validateOptionalArray(value: unknown): boolean { return typeof value === 'undefined' || value === null || Array.isArray(value); } +/** + * Check if the following value is not set or is a valid boolean. + * @param value Value to be checked. + */ +function validateOptionalBoolean(value: unknown): boolean { + return typeof value === 'undefined' || typeof value === 'boolean'; +} + /** * Map of validators for each entity. Function should throw the error if validation failed. */ @@ -73,7 +81,8 @@ const entitiesValidators: EntitiesValidationMap = { !validateRequiredString(importedObject?.id) || !validateRequiredString(importedObject?.name) || !validateOptionalArray(importedObject?.tags) - || typeof importedObject.conditional !== 'boolean' + || !validateOptionalBoolean(importedObject?.exclusive) + || !validateOptionalBoolean(importedObject?.conditional) || !validateOptionalArray(importedObject?.requiredTags) ) { throw new Error('Invalid preset format detected!'); From c12b00817b0fa771eb57cde86c315b46f819ca65 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 5 Apr 2026 18:35:26 +0400 Subject: [PATCH 14/18] Compare value to undefined instead of calling `typeof` --- src/lib/extension/transporting/validators.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/extension/transporting/validators.ts b/src/lib/extension/transporting/validators.ts index 53ceceb..1ff6150 100644 --- a/src/lib/extension/transporting/validators.ts +++ b/src/lib/extension/transporting/validators.ts @@ -29,7 +29,7 @@ function validateRequiredString(value: unknown): boolean { * @param value Value to be checked. */ function validateOptionalArray(value: unknown): boolean { - return typeof value === 'undefined' || value === null || Array.isArray(value); + return value === undefined || value === null || Array.isArray(value); } /** @@ -37,7 +37,7 @@ function validateOptionalArray(value: unknown): boolean { * @param value Value to be checked. */ function validateOptionalBoolean(value: unknown): boolean { - return typeof value === 'undefined' || typeof value === 'boolean'; + return value === undefined || typeof value === 'boolean'; } /** From 81b3d61a20e566bb257a9686034bf72c893aefd2 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 5 Apr 2026 19:12:11 +0400 Subject: [PATCH 15/18] Fixed content layout shift caused by exclusive tags warning --- src/content/components/philomena/TagsForm.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/content/components/philomena/TagsForm.ts b/src/content/components/philomena/TagsForm.ts index 7a27cdc..606f149 100644 --- a/src/content/components/philomena/TagsForm.ts +++ b/src/content/components/philomena/TagsForm.ts @@ -172,6 +172,7 @@ export class TagsForm extends BaseComponent { } #onTagChangeRequested(event: CustomEvent) { + const targetElement = event.target instanceof HTMLElement ? event.target : null; const { addedTags = null, removedTags = null } = event.detail; let tagChangeList: string[] = []; @@ -187,20 +188,22 @@ export class TagsForm extends BaseComponent { ); } - const offsetBeforeSubmission = this.#presetsList.container.offsetTop; + const containerOffset = this.#presetsList.container.offsetTop; + const presetOffset = targetElement?.offsetTop || 0; this.#applyTagChangesWithFancyTagEditor( tagChangeList.join(',') ); - const offsetDifference = this.#presetsList.container.offsetTop - offsetBeforeSubmission; + const containerOffsetDifference = this.#presetsList.container.offsetTop - containerOffset; + const presetOffsetDifference = (targetElement?.offsetTop || 0) - presetOffset; // 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) { + if (containerOffsetDifference !== 0 || presetOffsetDifference !== 0) { window.scrollTo({ - top: window.scrollY + offsetDifference, + top: window.scrollY + containerOffsetDifference + presetOffsetDifference, behavior: 'instant', }); } From 7d41524b4afaa59b34a09a859b7782f2e5c237e6 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 5 Apr 2026 19:21:09 +0400 Subject: [PATCH 16/18] Fixed scroll jump when preset becomes hidden --- src/content/components/philomena/TagsForm.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/content/components/philomena/TagsForm.ts b/src/content/components/philomena/TagsForm.ts index 606f149..f33d33e 100644 --- a/src/content/components/philomena/TagsForm.ts +++ b/src/content/components/philomena/TagsForm.ts @@ -196,7 +196,13 @@ export class TagsForm extends BaseComponent { ); const containerOffsetDifference = this.#presetsList.container.offsetTop - containerOffset; - const presetOffsetDifference = (targetElement?.offsetTop || 0) - presetOffset; + let presetOffsetDifference = (targetElement?.offsetTop || 0) - presetOffset; + + // If target element is no longer visible, then there is no need to apply scrolling fix, otherwise it will shift + // user up. Invisible elements are always report 0 offset. + if (targetElement?.checkVisibility() === false) { + presetOffsetDifference = 0; + } // 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 From 4f52906123789e0f8024d205ef9a5d1b4f5d06c7 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 5 Apr 2026 19:46:34 +0400 Subject: [PATCH 17/18] Extracted CLS compensation logic into separate method --- src/content/components/philomena/TagsForm.ts | 58 ++++++++++++-------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/src/content/components/philomena/TagsForm.ts b/src/content/components/philomena/TagsForm.ts index f33d33e..8307ed8 100644 --- a/src/content/components/philomena/TagsForm.ts +++ b/src/content/components/philomena/TagsForm.ts @@ -9,8 +9,8 @@ import { EVENT_PRESET_TAG_CHANGE_APPLIED, type PresetTagChange } from "$content/ export class TagsForm extends BaseComponent { #togglePresetsButton: HTMLButtonElement = document.createElement('button'); #presetsList = EditorPresetsBlock.create(); - #plainEditorTextarea: HTMLTextAreaElement|null = null; - #fancyEditorInput: HTMLInputElement|null = null; + #plainEditorTextarea: HTMLTextAreaElement | null = null; + #fancyEditorInput: HTMLInputElement | null = null; #tagsSet: Set = new Set(); protected build() { @@ -173,7 +173,7 @@ export class TagsForm extends BaseComponent { #onTagChangeRequested(event: CustomEvent) { const targetElement = event.target instanceof HTMLElement ? event.target : null; - const { addedTags = null, removedTags = null } = event.detail; + const {addedTags = null, removedTags = null} = event.detail; let tagChangeList: string[] = []; if (addedTags) { @@ -188,31 +188,33 @@ export class TagsForm extends BaseComponent { ); } - const containerOffset = this.#presetsList.container.offsetTop; - const presetOffset = targetElement?.offsetTop || 0; - - this.#applyTagChangesWithFancyTagEditor( - tagChangeList.join(',') + this.#executeAndCompensateForLayoutShift( + () => this.#applyTagChangesWithFancyTagEditor(tagChangeList.join(',')), + [this.#presetsList.container, targetElement], ); + } - const containerOffsetDifference = this.#presetsList.container.offsetTop - containerOffset; - let presetOffsetDifference = (targetElement?.offsetTop || 0) - presetOffset; + #executeAndCompensateForLayoutShift(executeOperation: () => void, elements: (HTMLElement | null)[]) { + const offsetsListBefore = TagsForm.#gatherOffsetsFromElements(elements); + executeOperation(); + const offsetsListAfter = TagsForm.#gatherOffsetsFromElements(elements); - // If target element is no longer visible, then there is no need to apply scrolling fix, otherwise it will shift - // user up. Invisible elements are always report 0 offset. - if (targetElement?.checkVisibility() === false) { - presetOffsetDifference = 0; + const resultDifference = offsetsListAfter + .map((offsetAfter, index) => + offsetAfter !== null && offsetsListBefore[index] !== null + ? offsetAfter - offsetsListBefore[index] + : null) + .filter(difference => difference !== null) + .reduce((summary, difference) => summary + difference, 0); + + if (resultDifference === 0) { + return; } - // 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 (containerOffsetDifference !== 0 || presetOffsetDifference !== 0) { - window.scrollTo({ - top: window.scrollY + containerOffsetDifference + presetOffsetDifference, - behavior: 'instant', - }); - } + window.scrollTo({ + top: scrollY + resultDifference, + behavior: 'instant', + }) } #applyTagChangesWithFancyTagEditor(tagsListWithChanges: string): void { @@ -241,7 +243,7 @@ export class TagsForm extends BaseComponent { this.refreshTagColors(); } - #onPlainEditorReloadRequested(event: CustomEvent) { + #onPlainEditorReloadRequested(event: CustomEvent) { if (!event.detail?.skipTagColorRefresh) { this.refreshTagColors(); } @@ -251,6 +253,14 @@ export class TagsForm extends BaseComponent { } } + static #gatherOffsetsFromElements(elements: (HTMLElement | null)[]): (number | null)[] { + return elements.map( + maybeElement => maybeElement?.checkVisibility() + ? maybeElement?.offsetTop + : null + ); + } + static watchForEditors() { document.body.addEventListener('click', event => { const targetElement = event.target; From 5c26888292b168a4250137be131d8b886231e137 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 5 Apr 2026 18:56:28 +0400 Subject: [PATCH 18/18] Bumped version to 0.7.1 --- manifest.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/manifest.json b/manifest.json index e8a851e..9184427 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name": "Furbooru Tagging Assistant", "description": "Small experimental extension for slightly quicker tagging experience. Furbooru Edition.", - "version": "0.7.0", + "version": "0.7.1", "browser_specific_settings": { "gecko": { "id": "furbooru-tagging-assistant@thecore.city", diff --git a/package-lock.json b/package-lock.json index 8f4a5ed..92eae16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "furbooru-tagging-assistant", - "version": "0.7.0", + "version": "0.7.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "furbooru-tagging-assistant", - "version": "0.7.0", + "version": "0.7.1", "dependencies": { "@fortawesome/fontawesome-free": "^7.2.0", "@sveltejs/adapter-static": "^3.0.10", diff --git a/package.json b/package.json index 3e0b22a..6961400 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "furbooru-tagging-assistant", - "version": "0.7.0", + "version": "0.7.1", "private": true, "type": "module", "scripts": {