From 38cb925fa4c7e9f534aea80505c04d777e69068a Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sat, 12 Oct 2024 03:38:01 +0400 Subject: [PATCH] Implemented option to add the tag into active profile from dropdown --- manifest.json | 17 ++ src/content/tags.js | 5 + src/lib/components/TagDropdownWrapper.js | 191 +++++++++++++++++++++++ 3 files changed, 213 insertions(+) create mode 100644 src/content/tags.js create mode 100644 src/lib/components/TagDropdownWrapper.js diff --git a/manifest.json b/manifest.json index 0da50e3..a6be1ee 100644 --- a/manifest.json +++ b/manifest.json @@ -39,6 +39,23 @@ "js": [ "src/content/header.js" ] + }, + { + "matches": [ + "*://*.furbooru.org/images?*", + "*://*.furbooru.org/images/*", + "*://*.furbooru.org/images/*/tag_changes", + "*://*.furbooru.org/images/*/tag_changes?*", + "*://*.furbooru.org/search?*", + "*://*.furbooru.org/tags?*", + "*://*.furbooru.org/tags/*", + "*://*.furbooru.org/profiles/*/tag_changes", + "*://*.furbooru.org/profiles/*/tag_changes?*", + "*://*.furbooru.org/filters/*" + ], + "js": [ + "src/content/tags.js" + ] } ], "action": { diff --git a/src/content/tags.js b/src/content/tags.js new file mode 100644 index 0000000..232bb7c --- /dev/null +++ b/src/content/tags.js @@ -0,0 +1,5 @@ +import {wrapTagDropdown} from "$lib/components/TagDropdownWrapper.js"; + +for (let tagDropdownElement of document.querySelectorAll('.tag.dropdown')) { + wrapTagDropdown(tagDropdownElement); +} diff --git a/src/lib/components/TagDropdownWrapper.js b/src/lib/components/TagDropdownWrapper.js new file mode 100644 index 0000000..728e9b2 --- /dev/null +++ b/src/lib/components/TagDropdownWrapper.js @@ -0,0 +1,191 @@ +import {BaseComponent} from "$lib/components/base/BaseComponent.js"; +import MaintenanceProfile from "$entities/MaintenanceProfile.js"; +import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings.js"; + +class TagDropdownWrapper extends BaseComponent { + /** + * Container with dropdown elements to insert options into. + * @type {HTMLElement} + */ + #dropdownContainer; + + /** + * Button to add or remove the current tag into/from the active profile. + * @type {HTMLAnchorElement|null} + */ + #toggleOnExistingButton = null; + + /** + * Button to create a new profile, make it active and add the current tag into the active profile. + * @type {HTMLAnchorElement|null} + */ + #addToNewButton = null; + + /** + * Local clone of the currently active profile used for updating the list of tags. + * @type {MaintenanceProfile|null} + */ + #activeProfile = null; + + /** + * Is cursor currently entered the dropdown. + * @type {boolean} + */ + #isEntered = false; + + build() { + this.#dropdownContainer = this.container.querySelector('.dropdown__content'); + } + + init() { + this.on('mouseenter', this.#onDropdownEntered.bind(this)); + this.on('mouseleave', this.#onDropdownLeft.bind(this)); + + TagDropdownWrapper.#watchActiveProfile(activeProfileOrNull => { + this.#activeProfile = activeProfileOrNull; + + if (this.#isEntered) { + this.#updateButtons(); + } + }); + } + + get #tagName() { + return this.container.dataset.tagName; + } + + #onDropdownEntered() { + this.#isEntered = true; + this.#updateButtons(); + } + + #onDropdownLeft() { + this.#isEntered = false; + } + + #updateButtons() { + if (!this.#activeProfile) { + this.#addToNewButton ??= TagDropdownWrapper.#createDropdownLink( + 'Add to new tagging profile', + this.#onAddToNewClicked.bind(this) + ); + + if (!this.#addToNewButton.isConnected) { + this.#dropdownContainer.append(this.#addToNewButton); + } + } else { + this.#addToNewButton?.remove(); + } + + if (this.#activeProfile) { + this.#toggleOnExistingButton ??= TagDropdownWrapper.#createDropdownLink( + 'Add to existing tagging profile', + this.#onToggleInExistingClicked.bind(this) + ); + + const profileName = this.#activeProfile.settings.name; + let profileSpecificButtonText = `Add to profile "${profileName}"`; + + if (this.#activeProfile.settings.tags.includes(this.#tagName)) { + profileSpecificButtonText = `Remove from profile "${profileName}"`; + } + + this.#toggleOnExistingButton.innerText = profileSpecificButtonText; + + if (!this.#toggleOnExistingButton.isConnected) { + this.#dropdownContainer.append(this.#toggleOnExistingButton); + } + + return; + } + + this.#toggleOnExistingButton?.remove(); + } + + async #onAddToNewClicked() { + const profile = new MaintenanceProfile(crypto.randomUUID(), { + name: 'Temporary Profile (' + (new Date().toISOString()) + ')', + tags: [this.#tagName] + }); + + await profile.save(); + await TagDropdownWrapper.#maintenanceSettings.setActiveProfileId(profile.id); + } + + async #onToggleInExistingClicked() { + if (!this.#activeProfile) { + return; + } + + const tagsList = new Set(this.#activeProfile.settings.tags); + const targetTagName = this.#tagName; + + if (tagsList.has(targetTagName)) { + tagsList.delete(targetTagName); + } else { + tagsList.add(targetTagName); + } + + this.#activeProfile.settings.tags = Array.from(tagsList.values()); + + await this.#activeProfile.save(); + } + + static #maintenanceSettings = new MaintenanceSettings(); + + /** + * Watch for changes to active profile. + * @param {(profile: MaintenanceProfile|null) => void} onActiveProfileChange Callback to call when profile was + * changed. + */ + static #watchActiveProfile(onActiveProfileChange) { + let lastActiveProfile; + + this.#maintenanceSettings.subscribe((settings) => { + lastActiveProfile = settings.activeProfile; + + this.#maintenanceSettings + .resolveActiveProfileAsObject() + .then(onActiveProfileChange); + }); + + MaintenanceProfile.subscribe(profiles => { + const activeProfile = profiles + .find(profile => profile.id === lastActiveProfile); + + onActiveProfileChange(activeProfile); + }); + + this.#maintenanceSettings + .resolveActiveProfileAsObject() + .then(activeProfile => { + lastActiveProfile = activeProfile?.id ?? null; + onActiveProfileChange(activeProfile); + }); + } + + /** + * Create element for dropdown. + * @param {string} text Base text for the option. + * @param {(event: MouseEvent) => void} onClickHandler Click handler. Event will be prevented by default. + * @return {HTMLAnchorElement} + */ + static #createDropdownLink(text, onClickHandler) { + /** @type {HTMLAnchorElement} */ + const dropdownLink = document.createElement('a'); + dropdownLink.href = '#'; + dropdownLink.innerText = text; + dropdownLink.className = 'tag__dropdown__link'; + + dropdownLink.addEventListener('click', event => { + event.preventDefault(); + onClickHandler(event); + }); + + return dropdownLink; + } +} + +export function wrapTagDropdown(element) { + new TagDropdownWrapper(element).initialize(); +}