1
0
mirror of https://github.com/koloml/philomena-tagging-assistant.git synced 2026-05-09 15:12:21 +00:00

24 Commits

Author SHA1 Message Date
b905ed668c Merge pull request #170 from koloml/release/0.7.1
Release: 0.7.1
2026-04-05 11:54:19 -04:00
5c26888292 Bumped version to 0.7.1 2026-04-05 19:49:28 +04:00
b6329bc4ef Merge pull request #171 from koloml/bugfix/preset-cls
Presets: Preserve correct scroll position when exclusive/conditional
2026-04-05 11:48:23 -04:00
4f52906123 Extracted CLS compensation logic into separate method 2026-04-05 19:46:34 +04:00
7d41524b4a Fixed scroll jump when preset becomes hidden 2026-04-05 19:21:09 +04:00
81b3d61a20 Fixed content layout shift caused by exclusive tags warning 2026-04-05 19:15:39 +04:00
dc78b2fe84 Merge pull request #169 from koloml/feature/conditional-presets
Presets: Added option to make presets conditional
2026-04-05 10:45:49 -04:00
c12b00817b Compare value to undefined instead of calling typeof 2026-04-05 18:35:26 +04:00
b4419b5de3 Validate exclusive and conditional as optional booleans 2026-04-05 18:33:34 +04:00
66fd093e5a Marking unchanged properties as readonly 2026-04-05 18:29:47 +04:00
bc7f85eaf9 Merge pull request #168 from koloml/feature/tag-profile-popup-cancel
Tagging Profiles: Cancel pending submission in popup if user decides to cancel their changes
2026-04-05 10:27:17 -04:00
3f9412b02d A bit more concrete wording for checkbox 2026-04-05 18:27:02 +04:00
a75dd098dc Show when preset is conditional in the viewer block 2026-04-05 18:18:52 +04:00
a45248cebf Fixed crashing build due to missing trailing comma 2026-04-05 18:02:54 +04:00
c777b57efb Merge remote-tracking branch 'origin/release/0.7.1' into feature/conditional-presets
# Conflicts:
#	src/content/components/extension/presets/PresetTableRow.ts
#	src/lib/extension/entities/TagEditorPreset.ts
#	src/lib/extension/transporting/exporters.ts
#	src/routes/features/presets/[id]/edit/+page.svelte
2026-04-05 18:01:33 +04:00
025cbaebb7 Merge pull request #167 from koloml/feature/exclusive-tags-in-preset
Presets: Added "exclusive" mode where only one tag is meant to be active at a time
2026-04-05 09:57:25 -04:00
b031b88512 Fixed tags being user-selectable 2026-04-05 17:52:46 +04:00
399e75809b Display presets when tag is found or hide it otherwise 2026-04-05 17:52:35 +04:00
f581f84065 Presets: Added conditional presets option
Now presets can be configured to show up only when specific tag is
provided.
2026-04-05 17:52:15 +04:00
e8b0afc81f And even more renaming of popup across multiple files 2026-03-29 04:17:12 +04:00
daceb9ad59 Cancel the planned submission when pending changes canceled 2026-03-29 03:57:06 +04:00
c36929b824 Renaming the file with events as well 2026-03-29 03:49:22 +04:00
9b262393fa Fixed event & underlying type not updated 2026-03-29 03:48:12 +04:00
83c7608e99 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.
2026-03-22 04:09:18 +04:00
18 changed files with 235 additions and 52 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "furbooru-tagging-assistant",
"version": "0.7.0",
"version": "0.7.1",
"private": true,
"type": "module",
"scripts": {

View File

@@ -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}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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';
}

View File

@@ -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();
}
}

View File

@@ -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":

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 || [],
});
}

View File

@@ -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,
}
}
};

View File

@@ -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!');
}

View File

@@ -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>

View File

@@ -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);

View File

@@ -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;
}
}
}