mirror of
https://github.com/koloml/philomena-tagging-assistant.git
synced 2026-05-09 07:12:19 +00:00
@@ -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",
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "furbooru-tagging-assistant",
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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)));
|
||||
</script>
|
||||
|
||||
<DetailsBlock title="Preset Name">
|
||||
@@ -18,3 +19,17 @@
|
||||
<DetailsBlock title="Tags">
|
||||
<TagsList tags={sortedTagsList}></TagsList>
|
||||
</DetailsBlock>
|
||||
{#if preset.settings.exclusive}
|
||||
<DetailsBlock title="Exclusivity">
|
||||
Only one tag in this preset should be active at a time. If you will click on other non-active tag, other tags will
|
||||
be automatically removed from the editor.
|
||||
</DetailsBlock>
|
||||
{/if}
|
||||
{#if preset.settings.conditional}
|
||||
<DetailsBlock title="Conditional">
|
||||
This preset will only appear when one of the tags below are present on image.
|
||||
</DetailsBlock>
|
||||
<DetailsBlock title="Conditional Tags">
|
||||
<TagsList tags={requiredTagsList}></TagsList>
|
||||
</DetailsBlock>
|
||||
{/if}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MaintenancePopupEventsMap } from "$content/components/events/maintenance-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
|
||||
|
||||
@@ -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 {
|
||||
export interface TaggingProfilePopupEventsMap {
|
||||
[EVENT_ACTIVE_PROFILE_CHANGED]: TaggingProfile | null;
|
||||
[EVENT_MAINTENANCE_STATE_CHANGED]: MaintenanceState;
|
||||
[EVENT_PROFILE_POPUP_STATE_CHANGED]: ProfilePopupState;
|
||||
[EVENT_TAGS_UPDATED]: Map<string, string> | null;
|
||||
}
|
||||
@@ -2,13 +2,13 @@ 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";
|
||||
|
||||
export class MediaBoxTools extends BaseComponent {
|
||||
#mediaBox: MediaBox | null = null;
|
||||
#maintenancePopup: TaggingProfilePopup | null = null;
|
||||
#popup: TaggingProfilePopup | null = null;
|
||||
|
||||
init() {
|
||||
const mediaBoxElement = this.container.closest<HTMLElement>('.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;
|
||||
}
|
||||
|
||||
@@ -5,10 +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');
|
||||
|
||||
constructor(container: HTMLElement, preset: TagEditorPreset) {
|
||||
super(container);
|
||||
@@ -32,6 +35,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 +56,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,
|
||||
@@ -64,6 +80,8 @@ export default class PresetTableRow extends BaseComponent {
|
||||
tagsCell,
|
||||
actionsCell,
|
||||
);
|
||||
|
||||
this.#alternateColorDummy.style.display = 'none';
|
||||
}
|
||||
|
||||
protected init() {
|
||||
@@ -85,6 +103,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])
|
||||
});
|
||||
@@ -108,13 +150,53 @@ export default class PresetTableRow extends BaseComponent {
|
||||
});
|
||||
}
|
||||
|
||||
#maybeRefreshVisibilityFromTags(sourceTags: Set<string>) {
|
||||
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<string>): boolean {
|
||||
const listOfRequiredTags = this.#preset.settings.requiredTags;
|
||||
|
||||
return Boolean(
|
||||
listOfRequiredTags.length
|
||||
&& listOfRequiredTags.some(tagName => sourceTags.has(tagName))
|
||||
);
|
||||
}
|
||||
|
||||
updateTags(tags: Set<string>) {
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
this.#maybeRefreshVisibilityFromTags(tags);
|
||||
}
|
||||
|
||||
remove() {
|
||||
@@ -126,4 +208,5 @@ export default class PresetTableRow extends BaseComponent {
|
||||
}
|
||||
|
||||
static #tagMissingClassName = 'is-missing';
|
||||
static #presetWarningClassName = 'has-warning';
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ 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";
|
||||
} from "$content/components/events/tagging-profile-popup-events";
|
||||
import type { MediaBoxTools } from "$content/components/extension/MediaBoxTools";
|
||||
import { resolveTagCategoryFromTagName } from "$lib/philomena/tag-utils";
|
||||
|
||||
@@ -183,7 +183,20 @@ 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');
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +223,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 +265,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 +275,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();
|
||||
@@ -360,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;
|
||||
}
|
||||
@@ -382,7 +395,7 @@ export class TaggingProfilePopup extends BaseComponent {
|
||||
|
||||
return () => {
|
||||
unsubscribeFromProfilesChanges();
|
||||
unsubscribeFromMaintenanceSettings();
|
||||
unsubscribeFromPreferences();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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/tagging-profile-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.#onPopupStateChanged.bind(this));
|
||||
}
|
||||
|
||||
#onMaintenanceStateChanged(stateChangeEvent: CustomEvent<string>) {
|
||||
#onPopupStateChanged(stateChangeEvent: CustomEvent<ProfilePopupState>) {
|
||||
// TODO Replace those with FontAwesome icons later. Those icons can probably be sourced from the website itself.
|
||||
switch (stateChangeEvent.detail) {
|
||||
case "ready":
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<string> = new Set();
|
||||
|
||||
protected build() {
|
||||
@@ -172,7 +172,8 @@ export class TagsForm extends BaseComponent {
|
||||
}
|
||||
|
||||
#onTagChangeRequested(event: CustomEvent<PresetTagChange>) {
|
||||
const { addedTags = null, removedTags = null } = event.detail;
|
||||
const targetElement = event.target instanceof HTMLElement ? event.target : null;
|
||||
const {addedTags = null, removedTags = null} = event.detail;
|
||||
let tagChangeList: string[] = [];
|
||||
|
||||
if (addedTags) {
|
||||
@@ -187,23 +188,33 @@ export class TagsForm extends BaseComponent {
|
||||
);
|
||||
}
|
||||
|
||||
const offsetBeforeSubmission = this.#presetsList.container.offsetTop;
|
||||
|
||||
this.#applyTagChangesWithFancyTagEditor(
|
||||
tagChangeList.join(',')
|
||||
this.#executeAndCompensateForLayoutShift(
|
||||
() => this.#applyTagChangesWithFancyTagEditor(tagChangeList.join(',')),
|
||||
[this.#presetsList.container, targetElement],
|
||||
);
|
||||
}
|
||||
|
||||
const offsetDifference = this.#presetsList.container.offsetTop - offsetBeforeSubmission;
|
||||
#executeAndCompensateForLayoutShift(executeOperation: () => void, elements: (HTMLElement | null)[]) {
|
||||
const offsetsListBefore = TagsForm.#gatherOffsetsFromElements(elements);
|
||||
executeOperation();
|
||||
const offsetsListAfter = TagsForm.#gatherOffsetsFromElements(elements);
|
||||
|
||||
// Compensating for the layout shift: when user clicks on a tag (or on "add/remove all tags"), tag editor might
|
||||
// overflow the current line and wrap tags around to the next line, causing presets section to shift. We need to
|
||||
// avoid that for better UX.
|
||||
if (offsetDifference !== 0) {
|
||||
window.scrollTo({
|
||||
top: window.scrollY + offsetDifference,
|
||||
behavior: 'instant',
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
window.scrollTo({
|
||||
top: scrollY + resultDifference,
|
||||
behavior: 'instant',
|
||||
})
|
||||
}
|
||||
|
||||
#applyTagChangesWithFancyTagEditor(tagsListWithChanges: string): void {
|
||||
@@ -232,7 +243,7 @@ export class TagsForm extends BaseComponent {
|
||||
this.refreshTagColors();
|
||||
}
|
||||
|
||||
#onPlainEditorReloadRequested(event: CustomEvent<ReloadCustomOptions|null>) {
|
||||
#onPlainEditorReloadRequested(event: CustomEvent<ReloadCustomOptions | null>) {
|
||||
if (!event.detail?.skipTagColorRefresh) {
|
||||
this.refreshTagColors();
|
||||
}
|
||||
@@ -242,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;
|
||||
|
||||
@@ -3,6 +3,9 @@ import StorageEntity from "$lib/extension/base/StorageEntity";
|
||||
interface TagEditorPresetSettings {
|
||||
name: string;
|
||||
tags: string[];
|
||||
exclusive: boolean;
|
||||
conditional: boolean;
|
||||
requiredTags: string[];
|
||||
}
|
||||
|
||||
export default class TagEditorPreset extends StorageEntity<TagEditorPresetSettings> {
|
||||
@@ -10,6 +13,9 @@ export default class TagEditorPreset extends StorageEntity<TagEditorPresetSettin
|
||||
super(id, {
|
||||
name: settings.name || '',
|
||||
tags: settings.tags || [],
|
||||
exclusive: settings.exclusive ?? false,
|
||||
conditional: settings.conditional || false,
|
||||
requiredTags: settings.requiredTags || [],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,9 @@ const entitiesExporters: ExportersMap = {
|
||||
id: entity.id,
|
||||
name: entity.settings.name,
|
||||
tags: entity.settings.tags,
|
||||
exclusive: entity.settings.exclusive,
|
||||
conditional: entity.settings.conditional,
|
||||
requiredTags: entity.settings.requiredTags,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -29,7 +29,15 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 value === undefined || typeof value === 'boolean';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,6 +81,9 @@ const entitiesValidators: EntitiesValidationMap = {
|
||||
!validateRequiredString(importedObject?.id)
|
||||
|| !validateRequiredString(importedObject?.name)
|
||||
|| !validateOptionalArray(importedObject?.tags)
|
||||
|| !validateOptionalBoolean(importedObject?.exclusive)
|
||||
|| !validateOptionalBoolean(importedObject?.conditional)
|
||||
|| !validateOptionalArray(importedObject?.requiredTags)
|
||||
) {
|
||||
throw new Error('Invalid preset format detected!');
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import FormControl from "$components/ui/forms/FormControl.svelte";
|
||||
import TextField from "$components/ui/forms/TextField.svelte";
|
||||
import TagsEditor from "$components/tags/TagsEditor.svelte";
|
||||
import CheckboxField from "$components/ui/forms/CheckboxField.svelte";
|
||||
|
||||
let presetId = $derived(page.params.id);
|
||||
|
||||
@@ -23,6 +24,9 @@
|
||||
|
||||
let presetName = $state('');
|
||||
let tagsList = $state<string[]>([]);
|
||||
let isExclusive = $state(false);
|
||||
let isConditional = $state<boolean>(false);
|
||||
let requiredTags = $state<string[]>([]);
|
||||
|
||||
$effect(() => {
|
||||
if (presetId === 'new') {
|
||||
@@ -39,6 +43,9 @@
|
||||
|
||||
presetName = targetPreset.settings.name;
|
||||
tagsList = [...targetPreset.settings.tags].sort((a, b) => a.localeCompare(b));
|
||||
isExclusive = targetPreset.settings.exclusive;
|
||||
isConditional = targetPreset.settings.conditional;
|
||||
requiredTags = [...targetPreset.settings.requiredTags].sort((a, b) => a.localeCompare(b));
|
||||
});
|
||||
|
||||
async function savePreset() {
|
||||
@@ -49,6 +56,9 @@
|
||||
|
||||
targetPreset.settings.name = presetName;
|
||||
targetPreset.settings.tags = [...tagsList];
|
||||
targetPreset.settings.exclusive = isExclusive;
|
||||
targetPreset.settings.conditional = isConditional;
|
||||
targetPreset.settings.requiredTags = [...requiredTags];
|
||||
|
||||
await targetPreset.save();
|
||||
await goto(`/features/presets/${targetPreset.id}`);
|
||||
@@ -67,6 +77,21 @@
|
||||
<FormControl label="Tags">
|
||||
<TagsEditor bind:tags={tagsList}></TagsEditor>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<CheckboxField bind:checked={isExclusive}>
|
||||
Keep only one tag from this preset active at a time.
|
||||
</CheckboxField>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<CheckboxField bind:checked={isConditional}>
|
||||
Show this preset only when any of specified tags are provided.
|
||||
</CheckboxField>
|
||||
</FormControl>
|
||||
{#if isConditional}
|
||||
<FormControl label="Required Tags">
|
||||
<TagsEditor bind:tags={requiredTags}></TagsEditor>
|
||||
</FormControl>
|
||||
{/if}
|
||||
</FormContainer>
|
||||
<Menu>
|
||||
<hr>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
.block.tag-presets {
|
||||
.tag {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&.is-missing {
|
||||
opacity: 0.5;
|
||||
@@ -13,4 +14,11 @@
|
||||
background: booru-vars.$resolved-tag-color;
|
||||
}
|
||||
}
|
||||
|
||||
.block.block--fixed.block--warning {
|
||||
margin: {
|
||||
top: booru-vars.$block-spacing;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user