From a2452a5fea181899020841f8f6c4966999496c45 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Tue, 13 Oct 2026 22:09:45 +0200 Subject: [PATCH 1/4] Extracted tags list toggler from profile popup to reuse it later --- src/config/tags.ts | 60 +++--- src/content/components/events/comms.ts | 4 +- .../events/tags-list-toggler-events.ts | 5 + .../extension/profiles/TaggingProfilePopup.ts | 195 ++---------------- .../extension/profiles/TagsListToggler.ts | 195 ++++++++++++++++++ src/lib/philomena/scraping/ScrapedAPI.ts | 4 +- 6 files changed, 260 insertions(+), 203 deletions(-) create mode 100644 src/content/components/events/tags-list-toggler-events.ts create mode 100644 src/content/components/extension/profiles/TagsListToggler.ts diff --git a/src/config/tags.ts b/src/config/tags.ts index 2065e88..70bd295 100644 --- a/src/config/tags.ts +++ b/src/config/tags.ts @@ -1,18 +1,24 @@ +/** + * Enum-like object with categories used by the Philomena, available for calling in the code. Values are technical + * names of these categories. + */ +export const CategoriesEnum = { + Rating: 'rating', + Spoiler: 'spoiler', + Origin: 'origin', + OC: 'oc', + Error: 'error', + Character: 'character', + ContentOfficial: 'content-official', + ContentFanmade: 'content-fanmade', + Species: 'species', + BodyType: 'body-type', +} + /** * List of categories defined by the sites. */ -export const categories: string[] = [ - 'rating', - 'spoiler', - 'origin', - 'oc', - 'error', - 'character', - 'content-official', - 'content-fanmade', - 'species', - 'body-type', -]; +export const categories: string[] = Object.values(CategoriesEnum); /** * Mapping of namespaces to their respective categories. These namespaces are automatically assigned to them, so we can @@ -23,21 +29,21 @@ export const categories: string[] = [ * @see https://github.com/philomena-dev/philomena/blob/6086757b654da8792ae52adb2a2f501ea6c30d12/lib/philomena/tags/tag.ex#L33-L45 */ export const namespaceCategories: Map = new Map([ - ['artist', 'origin'], - ['art pack', 'content-fanmade'], - ['colorist', 'origin'], - ['comic', 'content-fanmade'], - ['editor', 'origin'], - ['fanfic', 'content-fanmade'], - ['oc', 'oc'], - ['photographer', 'origin'], - ['series', 'content-fanmade'], - ['spoiler', 'spoiler'], - ['video', 'content-fanmade'], - ...(__CURRENT_SITE__ === 'tantabus' ? [ - ["prompter", "origin"], - ["creator", "origin"], - ["generator", "origin"] + ['artist', CategoriesEnum.Origin], + ['art pack', CategoriesEnum.ContentFanmade], + ['colorist', CategoriesEnum.Origin], + ['comic', CategoriesEnum.ContentFanmade], + ['editor', CategoriesEnum.Origin], + ['fanfic', CategoriesEnum.ContentFanmade], + ['oc', CategoriesEnum.OC], + ['photographer', CategoriesEnum.Origin], + ['series', CategoriesEnum.ContentFanmade], + ['spoiler', CategoriesEnum.Spoiler], + ['video', CategoriesEnum.ContentFanmade], + ...(__CURRENT_SITE__ === 'tantabus' ? [ + ["prompter", CategoriesEnum.Origin], + ["creator", CategoriesEnum.Origin], + ["generator", CategoriesEnum.Origin] ] : []) ]); diff --git a/src/content/components/events/comms.ts b/src/content/components/events/comms.ts index e5ba62a..643cae2 100644 --- a/src/content/components/events/comms.ts +++ b/src/content/components/events/comms.ts @@ -5,6 +5,7 @@ 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"; +import type { TagsListTogglerEventsMap } from "$content/components/events/tags-list-toggler-events"; type EventsMapping = TaggingProfilePopupEventsMap @@ -12,7 +13,8 @@ type EventsMapping = & BooruEventsMap & TagsFormEventsMap & TagDropdownEvents - & PresetBlockEventsMap; + & PresetBlockEventsMap + & TagsListTogglerEventsMap; type EventCallback = (event: CustomEvent) => void; export type UnsubscribeFunction = () => void; diff --git a/src/content/components/events/tags-list-toggler-events.ts b/src/content/components/events/tags-list-toggler-events.ts new file mode 100644 index 0000000..824fca1 --- /dev/null +++ b/src/content/components/events/tags-list-toggler-events.ts @@ -0,0 +1,5 @@ +export const EVENT_TAG_TOGGLED = 'tag-toggled'; + +export interface TagsListTogglerEventsMap { + [EVENT_TAG_TOGGLED]: [string, boolean]; +} diff --git a/src/content/components/extension/profiles/TaggingProfilePopup.ts b/src/content/components/extension/profiles/TaggingProfilePopup.ts index 0b912cd..ac32c78 100644 --- a/src/content/components/extension/profiles/TaggingProfilePopup.ts +++ b/src/content/components/extension/profiles/TaggingProfilePopup.ts @@ -4,31 +4,21 @@ import { BaseComponent } from "$content/components/base/BaseComponent"; import { getComponent } from "$content/components/base/component-utils"; import ScrapedAPI from "$lib/philomena/scraping/ScrapedAPI"; import { tagsBlacklist } from "$config/tags"; -import { emitterAt } from "$content/components/events/comms"; +import { emitterAt, on } from "$content/components/events/comms"; import { EVENT_ACTIVE_PROFILE_CHANGED, EVENT_PROFILE_POPUP_STATE_CHANGED, EVENT_TAGS_UPDATED } from "$content/components/events/tagging-profile-popup-events"; import type { MediaBoxTools } from "$content/components/extension/MediaBoxTools"; -import { resolveTagCategoryFromTagName } from "$lib/philomena/tag-utils"; +import TagsListToggler, { BlackListedTagsEncounteredError } from "$content/components/extension/profiles/TagsListToggler"; +import { EVENT_TAG_TOGGLED } from "$content/components/events/tags-list-toggler-events"; -class BlackListedTagsEncounteredError extends Error { - constructor(tagName: string) { - super(`This tag is blacklisted and prevents submission: ${tagName}`, { - cause: tagName - }); - } -} export class TaggingProfilePopup extends BaseComponent { - #tagsListElement: HTMLElement = document.createElement('div'); - #tagsList: HTMLElement[] = []; - #suggestedInvalidTags: Map = new Map(); + #tagsListToggler = TagsListToggler.create(); #activeProfile: TaggingProfile | null = null; #mediaBoxTools: MediaBoxTools | null = null; - #tagsToRemove: Set = new Set(); - #tagsToAdd: Set = new Set(); #isPlanningToSubmit: boolean = false; #isSubmitting: boolean = false; #tagsSubmissionTimer: Timeout | null = null; @@ -41,11 +31,8 @@ export class TaggingProfilePopup extends BaseComponent { this.container.innerHTML = ''; this.container.classList.add('maintenance-popup'); - this.#tagsListElement.classList.add('tags-list'); - - this.container.append( - this.#tagsListElement, - ); + this.#tagsListToggler.initialize(); + this.container.append(this.#tagsListToggler.container); } /** @@ -67,7 +54,7 @@ export class TaggingProfilePopup extends BaseComponent { this.#mediaBoxTools = mediaBoxTools; TaggingProfilePopup.#watchActiveProfile(this.#onActiveProfileChanged.bind(this)); - this.#tagsListElement.addEventListener('click', this.#handleTagClick.bind(this)); + on(this.#tagsListToggler, EVENT_TAG_TOGGLED, this.#onTagToggled.bind(this)); const mediaBox = this.#mediaBoxTools.mediaBox; @@ -82,101 +69,30 @@ export class TaggingProfilePopup extends BaseComponent { #onActiveProfileChanged(activeProfile: TaggingProfile | null) { this.#activeProfile = activeProfile; this.container.classList.toggle('is-active', activeProfile !== null); - this.#refreshTagsList(); + + this.#refreshTagsToggler(); this.#emitter.emit(EVENT_ACTIVE_PROFILE_CHANGED, activeProfile); } - #refreshTagsList() { + #refreshTagsToggler() { if (!this.#mediaBoxTools?.mediaBox) { return; } const activeProfileTagsList: string[] = this.#activeProfile?.settings.tags || []; + const currentPostTags = new Set(this.#mediaBoxTools.mediaBox.tagsAndAliases?.keys() || []); - for (const tagElement of this.#tagsList) { - tagElement.remove(); - } - - for (const tagElement of this.#suggestedInvalidTags.values()) { - tagElement.remove(); - } - - this.#tagsList = new Array(activeProfileTagsList.length); - this.#suggestedInvalidTags.clear(); - - const currentPostTags = this.#mediaBoxTools.mediaBox.tagsAndAliases; - - activeProfileTagsList - .sort((a, b) => a.localeCompare(b)) - .forEach((tagName, index) => { - const tagElement = TaggingProfilePopup.#buildTagElement(tagName); - this.#tagsList[index] = tagElement; - this.#tagsListElement.appendChild(tagElement); - - const isPresent = currentPostTags?.has(tagName); - - tagElement.classList.toggle('is-present', isPresent); - tagElement.classList.toggle('is-missing', !isPresent); - tagElement.classList.toggle('is-aliased', isPresent && currentPostTags?.get(tagName) !== tagName); - - // Just to prevent duplication, we need to include this tag to the map of suggested invalid tags - if (tagsBlacklist.includes(tagName)) { - TaggingProfilePopup.#markTagElementWithCategory(tagElement, 'error'); - this.#suggestedInvalidTags.set(tagName, tagElement); - } else { - TaggingProfilePopup.#markTagElementWithCategory( - tagElement, - resolveTagCategoryFromTagName(tagName) ?? '', - ); - } - }); + this.#tagsListToggler.replaceTags(activeProfileTagsList, currentPostTags); } /** * Detect and process clicks made directly to the tags. */ - #handleTagClick(event: MouseEvent) { - const targetObject = event.target; + #onTagToggled() { + const hasPendingChanges = this.#tagsListToggler.hasChanges; - - if (!targetObject || !(targetObject instanceof HTMLElement)) { - return; - } - - let tagElement: HTMLElement | null = targetObject; - - if (!tagElement.classList.contains('tag')) { - tagElement = tagElement.closest('.tag'); - } - - if (!tagElement?.dataset.name) { - return; - } - - const tagName = tagElement.dataset.name; - - if (tagElement.classList.contains('is-present')) { - const isToBeRemoved = tagElement.classList.toggle('is-removed'); - - if (isToBeRemoved) { - this.#tagsToRemove.add(tagName); - } else { - this.#tagsToRemove.delete(tagName); - } - } - - if (tagElement.classList.contains('is-missing')) { - const isToBeAdded = tagElement.classList.toggle('is-added'); - - if (isToBeAdded) { - this.#tagsToAdd.add(tagName); - } else { - this.#tagsToAdd.delete(tagName); - } - } - - if (this.#tagsToAdd.size || this.#tagsToRemove.size) { + if (hasPendingChanges) { // Notify only once, when first planning to submit if (!this.#isPlanningToSubmit) { TaggingProfilePopup.#notifyAboutPendingSubmission(true); @@ -188,7 +104,7 @@ export class TaggingProfilePopup extends BaseComponent { // 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) { + if (!hasPendingChanges && this.#isPlanningToSubmit) { this.#isPlanningToSubmit = false; this.#emitter.emit(EVENT_PROFILE_POPUP_STATE_CHANGED, 'ready'); TaggingProfilePopup.#notifyAboutPendingSubmission(false); @@ -232,29 +148,7 @@ export class TaggingProfilePopup extends BaseComponent { try { maybeTagsAndAliasesAfterUpdate = await TaggingProfilePopup.#scrapedAPI.updateImageTags( this.#mediaBoxTools.mediaBox.imageId, - tagsList => { - for (let tagName of this.#tagsToRemove) { - tagsList.delete(tagName); - } - - for (let tagName of this.#tagsToAdd) { - tagsList.add(tagName); - } - - if (shouldAutoRemove) { - for (let tagName of tagsBlacklist) { - tagsList.delete(tagName); - } - } else { - for (let tagName of tagsList) { - if (tagsBlacklist.includes(tagName)) { - throw new BlackListedTagsEncounteredError(tagName); - } - } - } - - return tagsList; - } + this.#tagsListToggler.createTagsUpdater(shouldAutoRemove), ); } catch (e) { if (e instanceof BlackListedTagsEncounteredError) { @@ -277,10 +171,7 @@ export class TaggingProfilePopup extends BaseComponent { this.#emitter.emit(EVENT_PROFILE_POPUP_STATE_CHANGED, 'complete'); - this.#tagsToAdd.clear(); - this.#tagsToRemove.clear(); - - this.#refreshTagsList(); + this.#refreshTagsToggler(); TaggingProfilePopup.#notifyAboutPendingSubmission(false); this.#isSubmitting = false; @@ -291,37 +182,13 @@ export class TaggingProfilePopup extends BaseComponent { return; } - const tagsAndAliases = this.#mediaBoxTools.mediaBox.tagsAndAliases; + const activeTags = new Set(this.#mediaBoxTools.mediaBox.tagsAndAliases?.keys() || []); - if (!tagsAndAliases) { + if (!activeTags.size) { return; } - const firstTagInList = this.#tagsList[0]; - - for (let tagName of tagsBlacklist) { - if (tagsAndAliases.has(tagName)) { - if (this.#suggestedInvalidTags.has(tagName)) { - continue; - } - - const tagElement = TaggingProfilePopup.#buildTagElement(tagName); - TaggingProfilePopup.#markTagElementWithCategory(tagElement, 'error'); - tagElement.classList.add('is-present'); - - this.#suggestedInvalidTags.set(tagName, tagElement); - - if (firstTagInList && firstTagInList.isConnected) { - this.#tagsListElement.insertBefore(tagElement, firstTagInList); - } else { - this.#tagsListElement.appendChild(tagElement); - } - } - } - } - - get isActive() { - return this.container.classList.contains('is-active'); + this.#tagsListToggler.prependTags(tagsBlacklist, activeTags); } static create(): HTMLElement { @@ -332,24 +199,6 @@ export class TaggingProfilePopup extends BaseComponent { return container; } - static #buildTagElement(tagName: string): HTMLElement { - const tagElement = document.createElement('span'); - tagElement.classList.add('tag'); - tagElement.innerText = tagName; - tagElement.dataset.name = tagName; - - return tagElement; - } - - /** - * Mark the tag element with specified category. - * @param tagElement Element to mark. - * @param category Code name of category to mark. - */ - static #markTagElementWithCategory(tagElement: HTMLElement, category: string) { - tagElement.dataset.tagCategory = category; - tagElement.setAttribute('data-tag-category', category); - } /** * Controller with maintenance settings. @@ -433,5 +282,5 @@ export class TaggingProfilePopup extends BaseComponent { /** * Amount of pending submissions or NULL if logic was not yet initialized. */ - static #pendingSubmissionCount: number|null = null; + static #pendingSubmissionCount: number | null = null; } diff --git a/src/content/components/extension/profiles/TagsListToggler.ts b/src/content/components/extension/profiles/TagsListToggler.ts new file mode 100644 index 0000000..e9b8c37 --- /dev/null +++ b/src/content/components/extension/profiles/TagsListToggler.ts @@ -0,0 +1,195 @@ +import { BaseComponent } from "$content/components/base/BaseComponent"; +import { CategoriesEnum, tagsBlacklist } from "$config/tags"; +import { resolveTagCategoryFromTagName } from "$lib/philomena/tag-utils"; +import { EVENT_TAG_TOGGLED } from "$content/components/events/tags-list-toggler-events"; +import { emit } from "$content/components/events/comms"; +import type { TagsUpdaterFunction } from "$lib/philomena/scraping/ScrapedAPI"; + +export default class TagsListToggler extends BaseComponent { + #tagsList: HTMLElement[] = []; + #invalidTagsList: Map = new Map(); + #tagsToAdd: Set = new Set(); + #tagsToRemove: Set = new Set(); + + protected build() { + this.container.classList.add('tags-list'); + } + + protected init() { + this.container.addEventListener('click', this.#handleTagClick.bind(this)); + } + + get hasChanges(): boolean { + return (this.#tagsToAdd.size + this.#tagsToRemove.size) > 0; + } + + replaceTags(tags: string[], activeTags: Set) { + for (const tagElement of this.#tagsList) { + tagElement.remove(); + } + + for (const tagElement of this.#invalidTagsList.values()) { + tagElement.remove(); + } + + this.#tagsList = []; + this.#invalidTagsList = new Map(); + + this.#renderAndInsertTags(tags, activeTags); + } + + prependTags(tags: string[], activeTags: Set) { + this.#renderAndInsertTags(tags, activeTags, true); + } + + #renderAndInsertTags(toggleableTags: string[], activeTags: Set, shouldPrepend: boolean = false) { + toggleableTags.sort((a, b) => a.localeCompare(b)); + + let lastInserted: HTMLElement|null = null; + + for (let tagIndex = 0; tagIndex < toggleableTags.length; tagIndex++) { + const tagName = toggleableTags[tagIndex]; + const tagElement = TagsListToggler.#buildTagElement(tagName); + + this.#tagsList.push(tagElement); + + const isPresent = activeTags.has(tagName); + + tagElement.classList.toggle(TagsListToggler.#missingClass, !isPresent); + tagElement.classList.toggle(TagsListToggler.#containsClass, isPresent); + + const tagCategory = tagsBlacklist.includes(tagName) && CategoriesEnum.Error + || resolveTagCategoryFromTagName(tagName) + || ''; + + TagsListToggler.#markTagElementWithCategory(tagElement, tagCategory); + + if (tagCategory === CategoriesEnum.Error) { + this.#invalidTagsList.set(tagName, tagElement); + } + + if (shouldPrepend) { + if (lastInserted) { + lastInserted.after(tagElement); + } else { + this.container.prepend(tagElement); + } + + lastInserted = tagElement; + } else { + this.container.append(tagElement); + } + } + } + + createTagsUpdater(shouldAutoRemoveInvalid: boolean): TagsUpdaterFunction { + return tagsList => { + for (let tagName of this.#tagsToRemove) { + tagsList.delete(tagName); + } + + for (let tagName of this.#tagsToAdd) { + tagsList.add(tagName); + } + + if (shouldAutoRemoveInvalid) { + for (let tagName of tagsBlacklist) { + tagsList.delete(tagName); + } + } else { + for (let tagName of tagsList) { + if (tagsBlacklist.includes(tagName)) { + throw new BlackListedTagsEncounteredError(tagName); + } + } + } + + return tagsList; + } + } + + #handleTagClick(event: MouseEvent) { + const targetObject = event.target; + + + if (!targetObject || !(targetObject instanceof HTMLElement)) { + return; + } + + let tagElement: HTMLElement | null = targetObject; + + if (!tagElement.classList.contains('tag')) { + tagElement = tagElement.closest('.tag'); + } + + if (!tagElement?.dataset.name) { + return; + } + + const tagName = tagElement.dataset.name; + + let willBeActive = false; + + if (tagElement.classList.contains(TagsListToggler.#containsClass)) { + const isToBeRemoved = tagElement.classList.toggle(TagsListToggler.#removedClass); + willBeActive = !isToBeRemoved; + + if (isToBeRemoved) { + this.#tagsToRemove.add(tagName); + } else { + this.#tagsToRemove.delete(tagName); + } + } + + if (tagElement.classList.contains(TagsListToggler.#missingClass)) { + const isToBeAdded = tagElement.classList.toggle(TagsListToggler.#addedClass); + willBeActive = isToBeAdded; + + if (isToBeAdded) { + this.#tagsToAdd.add(tagName); + } else { + this.#tagsToAdd.delete(tagName); + } + } + + emit(this.container, EVENT_TAG_TOGGLED, [tagName, willBeActive]); + } + + static #buildTagElement(tagName: string): HTMLElement { + const tagElement = document.createElement('span'); + tagElement.classList.add('tag'); + tagElement.innerText = tagName; + tagElement.dataset.name = tagName; + + return tagElement; + } + + /** + * Mark the tag element with specified category. + * @param tagElement Element to mark. + * @param category Code name of category to mark. + */ + static #markTagElementWithCategory(tagElement: HTMLElement, category: string) { + tagElement.dataset.tagCategory = category; + tagElement.setAttribute('data-tag-category', category); + } + + static create(): TagsListToggler { + const tagsListContainer = document.createElement('div'); + return new TagsListToggler(tagsListContainer); + } + + static #containsClass = 'is-present'; + static #missingClass = 'is-missing'; + + static #addedClass = 'is-added'; + static #removedClass = 'is-removed'; +} + +export class BlackListedTagsEncounteredError extends Error { + constructor(tagName: string) { + super(`This tag is blacklisted and prevents submission: ${tagName}`, { + cause: tagName + }); + } +} diff --git a/src/lib/philomena/scraping/ScrapedAPI.ts b/src/lib/philomena/scraping/ScrapedAPI.ts index 39cdf71..292b7ff 100644 --- a/src/lib/philomena/scraping/ScrapedAPI.ts +++ b/src/lib/philomena/scraping/ScrapedAPI.ts @@ -1,6 +1,6 @@ import PostParser from "$lib/philomena/scraping/parsing/PostParser"; -type UpdaterFunction = (tags: Set) => Set; +export type TagsUpdaterFunction = (tags: Set) => Set; export default class ScrapedAPI { /** @@ -9,7 +9,7 @@ export default class ScrapedAPI { * @param callback Callback to call to change the content. * @return Updated tags and aliases list for updating internal cached state. */ - async updateImageTags(imageId: number, callback: UpdaterFunction): Promise | null> { + async updateImageTags(imageId: number, callback: TagsUpdaterFunction): Promise | null> { const postParser = new PostParser(imageId); const formData = await postParser.resolveTagEditorFormData(); const tagsFieldValue = formData.get(PostParser.tagsInputName); From 690faa854fa2ce7f157cc67663aeef329bd185d7 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Wed, 14 Oct 2026 00:42:51 +0200 Subject: [PATCH 2/4] Added tags list and active profile tags into fullscreen viewer Images now can be toggled with tagging profiles using fullscreen viewer and not just with the popup over the small media box. Submission of tag changes is still processed by the popup inside original media box. Fullscreen viewer is just sending these changes there and then, once it's closed, tag changes will be sent. --- .../components/extension/FullscreenViewer.ts | 125 ++++++++++++++++-- .../extension/ImageShowFullscreenButton.ts | 8 +- .../components/extension/MediaBoxTools.ts | 4 + .../extension/profiles/TaggingProfilePopup.ts | 35 ++++- .../extension/profiles/TagsListToggler.ts | 35 +++++ src/content/components/philomena/MediaBox.ts | 31 ++++- src/content/listing.ts | 12 +- src/styles/booru-vars.scss | 2 + src/styles/content/listing.scss | 94 +++++++++---- 9 files changed, 290 insertions(+), 56 deletions(-) diff --git a/src/content/components/extension/FullscreenViewer.ts b/src/content/components/extension/FullscreenViewer.ts index 616acdf..784ebc5 100644 --- a/src/content/components/extension/FullscreenViewer.ts +++ b/src/content/components/extension/FullscreenViewer.ts @@ -1,28 +1,50 @@ import { BaseComponent } from "$content/components/base/BaseComponent"; import MiscPreferences, { type FullscreenViewerSize } from "$lib/extension/preferences/MiscPreferences"; -import { emit, on } from "$content/components/events/comms"; +import { emit, on, type UnsubscribeFunction } from "$content/components/events/comms"; import { EVENT_SIZE_LOADED } from "$content/components/events/fullscreen-viewer-events"; +import type { MediaBox } from "$content/components/philomena/MediaBox"; +import TagsListToggler from "$content/components/extension/profiles/TagsListToggler"; +import { EVENT_ACTIVE_PROFILE_CHANGED } from "$content/components/events/tagging-profile-popup-events"; +import type TaggingProfile from "$entities/TaggingProfile"; +import { EVENT_TAG_TOGGLED } from "$content/components/events/tags-list-toggler-events"; +import type { TaggingProfilePopup } from "$content/components/extension/profiles/TaggingProfilePopup"; export class FullscreenViewer extends BaseComponent { + #currentMediaBox: MediaBox | null = null; #videoElement: HTMLVideoElement = document.createElement('video'); #imageElement: HTMLImageElement = document.createElement('img'); #spinnerElement: HTMLElement = document.createElement('i'); #sizeSelectorElement: HTMLSelectElement = document.createElement('select'); #closeButtonElement: HTMLElement = document.createElement('i'); + #tagsPopupElement: HTMLElement = document.createElement('div'); + #imageTagsToggler: TagsListToggler = TagsListToggler.create(); + #profileTagsToggler: TagsListToggler = TagsListToggler.create(); + #activeProfileListenerUnsubscriber: UnsubscribeFunction | null = null; #touchId: number | null = null; #startX: number | null = null; #startY: number | null = null; #isClosingSwipeStarted: boolean | null = null; #isSizeFetched: boolean = false; - #currentURIs: App.ImageURIs | null = null; protected build() { this.container.classList.add('fullscreen-viewer'); + this.#imageTagsToggler.initialize(); + this.#profileTagsToggler.initialize(); + + this.#tagsPopupElement.append( + this.#profileTagsToggler.container, + document.createElement('hr'), + this.#imageTagsToggler.container, + ); + + this.#tagsPopupElement.classList.add('tags-panel'); + this.container.append( this.#spinnerElement, this.#sizeSelectorElement, this.#closeButtonElement, + this.#tagsPopupElement, ); this.#spinnerElement.classList.add('spinner', 'fa', 'fa-circle-notch', 'fa-spin'); @@ -52,11 +74,15 @@ export class FullscreenViewer extends BaseComponent { this.#videoElement.addEventListener('loadeddata', this.#onLoaded.bind(this)); this.#imageElement.addEventListener('load', this.#onLoaded.bind(this)); this.#sizeSelectorElement.addEventListener('click', event => event.stopPropagation()); + this.#tagsPopupElement.addEventListener('click', event => event.stopPropagation()); FullscreenViewer.#preferences .fullscreenViewerSize.get() .then(this.#onSizeResolved.bind(this)) .then(this.#watchForSizeSelectionChanges.bind(this)); + + on(this.#profileTagsToggler, EVENT_TAG_TOGGLED, this.#onTagToggled.bind(this)); + on(this.#imageTagsToggler, EVENT_TAG_TOGGLED, this.#onTagToggled.bind(this)); } #onLoaded() { @@ -193,8 +219,8 @@ export class FullscreenViewer extends BaseComponent { this.#sizeSelectorElement.addEventListener('input', () => { const targetSize = this.#sizeSelectorElement.value; - if (this.#currentURIs) { - void this.show(this.#currentURIs); + if (this.#currentMediaBox) { + void this.show(this.#currentMediaBox); } if (!targetSize || targetSize === lastActiveSize || !(targetSize in FullscreenViewer.#previewSizes)) { @@ -206,8 +232,55 @@ export class FullscreenViewer extends BaseComponent { }); } + #onTagToggled(event: CustomEvent<[string, boolean]>) { + const sourceTogglerContainer = event.currentTarget; + + if (!(sourceTogglerContainer instanceof HTMLElement)) { + return; + } + + if (sourceTogglerContainer !== this.#imageTagsToggler.container) { + this.#imageTagsToggler.refreshStateFromChanges(); + } + + if (sourceTogglerContainer !== this.#profileTagsToggler.container) { + this.#imageTagsToggler.refreshStateFromChanges(); + } + + this.#currentMediaBox?.tools?.popup?.tagsToggler.refreshStateFromChanges(); + } + + #onActiveProfileChanged(eventOrTaggingProfile: CustomEvent | TaggingProfile | null) { + let resolvedActiveTaggingProfile: TaggingProfile | null = null; + + if (eventOrTaggingProfile instanceof CustomEvent) { + resolvedActiveTaggingProfile = eventOrTaggingProfile.detail; + } else { + resolvedActiveTaggingProfile = eventOrTaggingProfile; + } + + this.#profileTagsToggler.container.classList.toggle('is-hidden', !resolvedActiveTaggingProfile); + + if (!resolvedActiveTaggingProfile) { + return; + } + + const uniqueTagsSet = this.#currentMediaBox?.uniqueTagsSet; + + this.#profileTagsToggler.replaceTags(resolvedActiveTaggingProfile.settings.tags, uniqueTagsSet ?? new Set()); + } + #close() { - this.#currentURIs = null; + const taggingPopup = this.#currentMediaBox?.tools?.popup; + + if (taggingPopup) { + this.#activeProfileListenerUnsubscriber?.(); + + taggingPopup.tagsToggler.disconnectChangeSet(); + taggingPopup.toggleSubmissionLock(false); + } + + this.#currentMediaBox = null; this.container.classList.remove(FullscreenViewer.#shownState); document.body.style.removeProperty('overflow'); @@ -247,16 +320,23 @@ export class FullscreenViewer extends BaseComponent { return imageUris[targetSize as FullscreenViewerSize]; } - async show(imageUris: App.ImageURIs): Promise { - this.#currentURIs = imageUris; + async show(mediaBox: MediaBox): Promise { + const isSameMediaBox = this.#currentMediaBox === mediaBox; + this.#currentMediaBox = mediaBox; - const url = await this.#resolveCurrentSelectedSizeUrl(imageUris); + const url = await this.#resolveCurrentSelectedSizeUrl(mediaBox.imageLinks); if (!url) { console.warn('Failed to resolve media for the viewer!'); return; } + const taggingPopup = mediaBox?.tools?.popup; + + if (taggingPopup && !isSameMediaBox) { + this.#connectAndRefreshTagTogglers(taggingPopup, mediaBox); + } + this.container.classList.add('loading'); requestAnimationFrame(() => { @@ -285,6 +365,35 @@ export class FullscreenViewer extends BaseComponent { this.container.append(this.#imageElement); } + /** + * When connecting to the new popup, we need to connect the tag editor inside the fullscreen viewer to the original + * popup, since submission is still being sent from the popup inside media box. + * @param taggingPopup + * @param mediaBox + * @private + */ + #connectAndRefreshTagTogglers(taggingPopup: TaggingProfilePopup, mediaBox: MediaBox) { + taggingPopup.toggleSubmissionLock(true); + + const sourceToggler = taggingPopup.tagsToggler; + sourceToggler.connectChangeSet(this.#profileTagsToggler); + sourceToggler.connectChangeSet(this.#imageTagsToggler); + + const uniqueTagsSet = mediaBox.uniqueTagsSet ?? new Set(); + const uniqueTags = Array.from(uniqueTagsSet); + + this.#imageTagsToggler.replaceTags(uniqueTags, uniqueTagsSet); + + this.#activeProfileListenerUnsubscriber?.(); + this.#activeProfileListenerUnsubscriber = on( + taggingPopup, + EVENT_ACTIVE_PROFILE_CHANGED, + this.#onActiveProfileChanged.bind(this), + ); + + this.#onActiveProfileChanged(taggingPopup.activeProfile); + } + static #isVideoUrl(url: string): boolean { return url.endsWith('.mp4') || url.endsWith('.webm'); } diff --git a/src/content/components/extension/ImageShowFullscreenButton.ts b/src/content/components/extension/ImageShowFullscreenButton.ts index 25422a8..6d21c91 100644 --- a/src/content/components/extension/ImageShowFullscreenButton.ts +++ b/src/content/components/extension/ImageShowFullscreenButton.ts @@ -45,15 +45,13 @@ export class ImageShowFullscreenButton extends BaseComponent { } #onButtonClicked() { - const imageLinks = this.#mediaBoxTools?.mediaBox?.imageLinks; + const mediaBox = this.#mediaBoxTools?.mediaBox; - if (!imageLinks) { + if (!mediaBox) { throw new Error('Failed to resolve image links from media box tools!'); } - ImageShowFullscreenButton - .#resolveViewer() - ?.show(imageLinks); + ImageShowFullscreenButton.#resolveViewer()?.show(mediaBox); } static create(): HTMLElement { diff --git a/src/content/components/extension/MediaBoxTools.ts b/src/content/components/extension/MediaBoxTools.ts index 1de65b7..7ea50a6 100644 --- a/src/content/components/extension/MediaBoxTools.ts +++ b/src/content/components/extension/MediaBoxTools.ts @@ -50,6 +50,10 @@ export class MediaBoxTools extends BaseComponent { return this.#mediaBox; } + get popup(): TaggingProfilePopup | null { + return this.#popup; + } + /** * Create a maintenance popup element. * @param childrenElements List of children elements to append to the component. diff --git a/src/content/components/extension/profiles/TaggingProfilePopup.ts b/src/content/components/extension/profiles/TaggingProfilePopup.ts index ac32c78..b332279 100644 --- a/src/content/components/extension/profiles/TaggingProfilePopup.ts +++ b/src/content/components/extension/profiles/TaggingProfilePopup.ts @@ -19,6 +19,7 @@ export class TaggingProfilePopup extends BaseComponent { #tagsListToggler = TagsListToggler.create(); #activeProfile: TaggingProfile | null = null; #mediaBoxTools: MediaBoxTools | null = null; + #isSubmissionLocked: boolean = false; #isPlanningToSubmit: boolean = false; #isSubmitting: boolean = false; #tagsSubmissionTimer: Timeout | null = null; @@ -62,8 +63,29 @@ export class TaggingProfilePopup extends BaseComponent { throw new Error('Media box component not found!'); } - mediaBox.on('mouseout', this.#onMouseLeftArea.bind(this)); - mediaBox.on('mouseover', this.#onMouseEnteredArea.bind(this)); + mediaBox.on('mouseout', this.#maybeEnqueueSubmission.bind(this)); + mediaBox.on('mouseover', this.#cancelQueuedSubmission.bind(this)); + } + + get tagsToggler() { + return this.#tagsListToggler; + } + + get activeProfile() { + return this.#activeProfile; + } + + toggleSubmissionLock(shouldLockSubmission: boolean) { + this.#isSubmissionLocked = shouldLockSubmission; + + if (shouldLockSubmission) { + this.#cancelQueuedSubmission(); + return; + } + + // First notify that tags might have changed after submission lock was released and then enqueue it. + this.#onTagToggled(); + this.#maybeEnqueueSubmission(); } #onActiveProfileChanged(activeProfile: TaggingProfile | null) { @@ -116,14 +138,14 @@ export class TaggingProfilePopup extends BaseComponent { } } - #onMouseEnteredArea() { + #cancelQueuedSubmission() { if (this.#tagsSubmissionTimer) { clearTimeout(this.#tagsSubmissionTimer); } } - #onMouseLeftArea() { - if (this.#isPlanningToSubmit && !this.#isSubmitting) { + #maybeEnqueueSubmission() { + if (this.#isPlanningToSubmit && !this.#isSubmitting && !this.#isSubmissionLocked) { this.#tagsSubmissionTimer = setTimeout( this.#onSubmissionTimerPassed.bind(this), TaggingProfilePopup.#delayBeforeSubmissionMs @@ -132,7 +154,7 @@ export class TaggingProfilePopup extends BaseComponent { } async #onSubmissionTimerPassed() { - if (!this.#isPlanningToSubmit || this.#isSubmitting || !this.#mediaBoxTools?.mediaBox) { + if (!this.#isPlanningToSubmit || this.#isSubmitting || !this.#mediaBoxTools?.mediaBox || this.#isSubmissionLocked) { return; } @@ -171,6 +193,7 @@ export class TaggingProfilePopup extends BaseComponent { this.#emitter.emit(EVENT_PROFILE_POPUP_STATE_CHANGED, 'complete'); + this.#tagsListToggler.clearChangeSet(); this.#refreshTagsToggler(); TaggingProfilePopup.#notifyAboutPendingSubmission(false); diff --git a/src/content/components/extension/profiles/TagsListToggler.ts b/src/content/components/extension/profiles/TagsListToggler.ts index e9b8c37..f70d727 100644 --- a/src/content/components/extension/profiles/TagsListToggler.ts +++ b/src/content/components/extension/profiles/TagsListToggler.ts @@ -42,11 +42,26 @@ export default class TagsListToggler extends BaseComponent { this.#renderAndInsertTags(tags, activeTags, true); } + refreshStateFromChanges() { + for (const tagElement of this.#tagsList) { + const tagName = tagElement.dataset.name; + + if (!tagName) { + throw new Error('Missing tag name!'); + } + + tagElement.classList.toggle(TagsListToggler.#addedClass, this.#tagsToAdd.has(tagName)); + tagElement.classList.toggle(TagsListToggler.#removedClass, this.#tagsToRemove.has(tagName)); + } + } + #renderAndInsertTags(toggleableTags: string[], activeTags: Set, shouldPrepend: boolean = false) { toggleableTags.sort((a, b) => a.localeCompare(b)); let lastInserted: HTMLElement|null = null; + const hasStoredChanges = this.hasChanges; + for (let tagIndex = 0; tagIndex < toggleableTags.length; tagIndex++) { const tagName = toggleableTags[tagIndex]; const tagElement = TagsListToggler.#buildTagElement(tagName); @@ -58,6 +73,11 @@ export default class TagsListToggler extends BaseComponent { tagElement.classList.toggle(TagsListToggler.#missingClass, !isPresent); tagElement.classList.toggle(TagsListToggler.#containsClass, isPresent); + if (hasStoredChanges) { + tagElement.classList.toggle(TagsListToggler.#addedClass, !isPresent && this.#tagsToAdd.has(tagName)); + tagElement.classList.toggle(TagsListToggler.#removedClass, isPresent && this.#tagsToRemove.has(tagName)); + } + const tagCategory = tagsBlacklist.includes(tagName) && CategoriesEnum.Error || resolveTagCategoryFromTagName(tagName) || ''; @@ -108,6 +128,21 @@ export default class TagsListToggler extends BaseComponent { } } + connectChangeSet(secondaryToggler: TagsListToggler) { + secondaryToggler.#tagsToAdd = this.#tagsToAdd; + secondaryToggler.#tagsToRemove = this.#tagsToRemove; + } + + disconnectChangeSet() { + this.#tagsToAdd = new Set(this.#tagsToAdd.values()); + this.#tagsToRemove = new Set(this.#tagsToRemove.values()); + } + + clearChangeSet() { + this.#tagsToAdd.clear(); + this.#tagsToRemove.clear(); + } + #handleTagClick(event: MouseEvent) { const targetObject = event.target; diff --git a/src/content/components/philomena/MediaBox.ts b/src/content/components/philomena/MediaBox.ts index c7b1cc6..6f1502b 100644 --- a/src/content/components/philomena/MediaBox.ts +++ b/src/content/components/philomena/MediaBox.ts @@ -3,11 +3,13 @@ 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/tagging-profile-popup-events"; +import type { MediaBoxTools } from "$content/components/extension/MediaBoxTools"; export class MediaBox extends BaseComponent { #thumbnailContainer: HTMLElement | null = null; #imageLinkElement: HTMLAnchorElement | null = null; #tagsAndAliases: Map | null = null; + #mediaBoxTools: MediaBoxTools | null = null; init() { this.#thumbnailContainer = this.container.querySelector('.image-container'); @@ -33,6 +35,16 @@ export class MediaBox extends BaseComponent { return buildTagsAndAliasesMap(tagAliases, actualTags); } + get uniqueTagsSet(): Set|null { + const tagsAndAliases = this.tagsAndAliases; + + if (tagsAndAliases === null) { + return null; + } + + return new Set(tagsAndAliases.values()); + } + get tagsAndAliases(): Map | null { if (!this.#tagsAndAliases) { this.#tagsAndAliases = this.#calculateMediaBoxTags(); @@ -61,17 +73,22 @@ export class MediaBox extends BaseComponent { return JSON.parse(jsonUris); } + get tools(): MediaBoxTools | null { + return this.#mediaBoxTools; + } + /** * Wrap the media box element into the special wrapper. */ - static initialize(mediaBoxContainer: HTMLElement, childComponentElements: HTMLElement[]) { - new MediaBox(mediaBoxContainer) - .initialize(); + static initialize(mediaBoxContainer: HTMLElement, mediaBoxToolsElement: HTMLElement) { + const mediaBox = new MediaBox(mediaBoxContainer); + mediaBox.initialize(); - for (let childComponentElement of childComponentElements) { - mediaBoxContainer.appendChild(childComponentElement); - getComponent(childComponentElement)?.initialize(); - } + mediaBoxContainer.appendChild(mediaBoxToolsElement); + const mediaBoxToolsComponent = getComponent(mediaBoxToolsElement); + mediaBoxToolsComponent?.initialize(); + + mediaBox.#mediaBoxTools = mediaBoxToolsComponent; } static findElements(): NodeListOf { diff --git a/src/content/listing.ts b/src/content/listing.ts index 21e240e..d91785e 100644 --- a/src/content/listing.ts +++ b/src/content/listing.ts @@ -8,13 +8,11 @@ import { ImageListContainer } from "$content/components/philomena/listing/ImageL const mediaBoxes = MediaBox.findElements(); mediaBoxes.forEach(mediaBoxElement => { - MediaBox.initialize(mediaBoxElement, [ - MediaBoxTools.create( - TaggingProfilePopup.create(), - TaggingProfileStatusIcon.create(), - ImageShowFullscreenButton.create(), - ) - ]); + MediaBox.initialize(mediaBoxElement, MediaBoxTools.create( + TaggingProfilePopup.create(), + TaggingProfileStatusIcon.create(), + ImageShowFullscreenButton.create(), + )); // Attempt to fix misplacement of media boxes requestAnimationFrame(() => { diff --git a/src/styles/booru-vars.scss b/src/styles/booru-vars.scss index 1f9d5f6..0b3c4b3 100644 --- a/src/styles/booru-vars.scss +++ b/src/styles/booru-vars.scss @@ -10,3 +10,5 @@ $block-spacing: var(--block-spacing); $resolved-tag-background: var(--tag-background); $resolved-tag-border: var(--tag-border); $resolved-tag-color: var(--tag-color); + +$limited-layout-width: var(--limited-layout-width); diff --git a/src/styles/content/listing.scss b/src/styles/content/listing.scss index 2306743..9064550 100644 --- a/src/styles/content/listing.scss +++ b/src/styles/content/listing.scss @@ -2,6 +2,38 @@ @use '$styles/booru-vars'; @use '$styles/environment'; +%box { + background: booru-vars.$background-color; + padding: 5px; +} + +%tags-list { + display: flex; + flex-wrap: wrap; + gap: 6px; + + .tag { + margin: 0; + } +} + +%toggleable-tag { + cursor: pointer; + user-select: none; + padding: 0 5px; + gap: 0; + + &:hover { + background: booru-vars.$resolved-tag-color; + color: booru-vars.$resolved-tag-background; + } + + &.is-missing:not(.is-added), + &.is-present.is-removed { + opacity: 0.5; + } +} + // This will fix wierd misplacing of the modified media boxes in the listing. .js-resizable-media-container { display: flex; @@ -40,14 +72,14 @@ } .maintenance-popup { + @extend %box; + display: none; position: absolute; top: 100%; - padding: 5px; left: -50%; right: -50%; z-index: 8; - background: booru-vars.$background-color; border: { left: booru-vars.$media-border; right: booru-vars.$media-border; @@ -55,31 +87,13 @@ }; .tags-list { - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: 6px; + @extend %tags-list; - .tag { - margin: 0; - } + justify-content: center; } .tag { - cursor: pointer; - user-select: none; - padding: 0 5px; - gap: 0; - - &:hover { - background: booru-vars.$resolved-tag-color; - color: booru-vars.$resolved-tag-background; - } - - &.is-missing:not(.is-added), - &.is-present.is-removed { - opacity: 0.5; - } + @extend %toggleable-tag; } } @@ -244,4 +258,38 @@ opacity: 1; } } + + .tags-panel { + @extend %box; + + width: 96vw; + max-width: booru-vars.$limited-layout-width; + position: fixed; + bottom: 0; + left: 0; + right: 0; + margin-left: auto; + margin-right: auto; + border: { + top: booru-vars.$media-border; + left: booru-vars.$media-border; + right: booru-vars.$media-border; + } + + .tags-list { + @extend %tags-list; + + &.is-hidden { + display: none; + + & + hr { + display: none; + } + } + } + + .tag { + @extend %toggleable-tag; + } + } } From d150d87d8988885394bac75d95da048e306146ed Mon Sep 17 00:00:00 2001 From: KoloMl Date: Wed, 14 Oct 2026 00:44:19 +0200 Subject: [PATCH 3/4] Fixed tags not synced to the profile toggler --- src/content/components/extension/FullscreenViewer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/components/extension/FullscreenViewer.ts b/src/content/components/extension/FullscreenViewer.ts index 784ebc5..0f541cc 100644 --- a/src/content/components/extension/FullscreenViewer.ts +++ b/src/content/components/extension/FullscreenViewer.ts @@ -244,7 +244,7 @@ export class FullscreenViewer extends BaseComponent { } if (sourceTogglerContainer !== this.#profileTagsToggler.container) { - this.#imageTagsToggler.refreshStateFromChanges(); + this.#profileTagsToggler.refreshStateFromChanges(); } this.#currentMediaBox?.tools?.popup?.tagsToggler.refreshStateFromChanges(); From 5578e64306894fa0fca956d47b7aa4fe4dc30816 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Wed, 14 Oct 2026 02:43:13 +0200 Subject: [PATCH 4/4] Allow toggling tags in fullscreen on/off, fixing long tags lists --- .../components/extension/FullscreenViewer.ts | 30 ++++++++++++++++++- .../extension/preferences/MiscPreferences.ts | 6 ++++ src/styles/booru-vars.scss | 5 ++++ src/styles/content/listing.scss | 26 ++++++++++++++++ 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/content/components/extension/FullscreenViewer.ts b/src/content/components/extension/FullscreenViewer.ts index 0f541cc..dad4ab5 100644 --- a/src/content/components/extension/FullscreenViewer.ts +++ b/src/content/components/extension/FullscreenViewer.ts @@ -19,6 +19,7 @@ export class FullscreenViewer extends BaseComponent { #tagsPopupElement: HTMLElement = document.createElement('div'); #imageTagsToggler: TagsListToggler = TagsListToggler.create(); #profileTagsToggler: TagsListToggler = TagsListToggler.create(); + #tagsPopupHider: HTMLElement = document.createElement('div'); #activeProfileListenerUnsubscriber: UnsubscribeFunction | null = null; #touchId: number | null = null; #startX: number | null = null; @@ -32,12 +33,23 @@ export class FullscreenViewer extends BaseComponent { this.#imageTagsToggler.initialize(); this.#profileTagsToggler.initialize(); - this.#tagsPopupElement.append( + const overflowContainer = document.createElement('div'); + overflowContainer.classList.add('tags-panel__scroller'); + + overflowContainer.append( this.#profileTagsToggler.container, document.createElement('hr'), this.#imageTagsToggler.container, ); + this.#tagsPopupElement.append( + this.#tagsPopupHider, + overflowContainer, + ); + + this.#tagsPopupHider.classList.add('tags-panel__toggle'); + this.#tagsPopupHider.textContent = 'Toggle Tags'; + this.#tagsPopupElement.classList.add('tags-panel'); this.container.append( @@ -75,12 +87,17 @@ export class FullscreenViewer extends BaseComponent { this.#imageElement.addEventListener('load', this.#onLoaded.bind(this)); this.#sizeSelectorElement.addEventListener('click', event => event.stopPropagation()); this.#tagsPopupElement.addEventListener('click', event => event.stopPropagation()); + this.#tagsPopupHider.addEventListener('click', () => this.#togglePopupVisibility()); FullscreenViewer.#preferences .fullscreenViewerSize.get() .then(this.#onSizeResolved.bind(this)) .then(this.#watchForSizeSelectionChanges.bind(this)); + FullscreenViewer.#preferences + .fullscreenViewerPopupShown.get() + .then(this.#togglePopupVisibility.bind(this)); + on(this.#profileTagsToggler, EVENT_TAG_TOGGLED, this.#onTagToggled.bind(this)); on(this.#imageTagsToggler, EVENT_TAG_TOGGLED, this.#onTagToggled.bind(this)); } @@ -394,6 +411,16 @@ export class FullscreenViewer extends BaseComponent { this.#onActiveProfileChanged(taggingPopup.activeProfile); } + #togglePopupVisibility(maybeState: boolean | undefined = undefined) { + if (typeof maybeState === 'undefined') { + const isHidden = this.#tagsPopupElement.classList.toggle(FullscreenViewer.#collapsedPopupState); + void FullscreenViewer.#preferences.fullscreenViewerPopupShown.set(!isHidden); + return; + } + + this.#tagsPopupElement.classList.toggle(FullscreenViewer.#collapsedPopupState, !maybeState); + } + static #isVideoUrl(url: string): boolean { return url.endsWith('.mp4') || url.endsWith('.webm'); } @@ -405,6 +432,7 @@ export class FullscreenViewer extends BaseComponent { static #shownState = 'shown'; static #swipeState = 'swiped'; static #minRequiredDistance = 50; + static #collapsedPopupState = 'is-collapsed'; static #previewSizes: Record = { full: 'Full', diff --git a/src/lib/extension/preferences/MiscPreferences.ts b/src/lib/extension/preferences/MiscPreferences.ts index 0c14051..2482d9a 100644 --- a/src/lib/extension/preferences/MiscPreferences.ts +++ b/src/lib/extension/preferences/MiscPreferences.ts @@ -8,6 +8,7 @@ export type FullscreenViewerSize = keyof App.ImageURIs; interface MiscPreferencesFields { fullscreenViewer: boolean; fullscreenViewerSize: FullscreenViewerSize; + fullscreenViewerPopupShown: boolean; } export default class MiscPreferences extends CacheablePreferences implements WithFields { @@ -24,4 +25,9 @@ export default class MiscPreferences extends CacheablePreferences