diff --git a/src/app.d.ts b/src/app.d.ts
index d56ae6a..8281fc6 100644
--- a/src/app.d.ts
+++ b/src/app.d.ts
@@ -1,7 +1,8 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
-import TaggingProfile from "$entities/TaggingProfile";
+import type TaggingProfile from "$entities/TaggingProfile";
import type TagGroup from "$entities/TagGroup";
+import type TagEditorPreset from "$entities/TagEditorPreset";
declare global {
/**
@@ -39,6 +40,7 @@ declare global {
interface EntityNamesMap {
profiles: TaggingProfile;
groups: TagGroup;
+ presets: TagEditorPreset;
}
interface ImageURIs {
diff --git a/src/components/features/PresetView.svelte b/src/components/features/PresetView.svelte
new file mode 100644
index 0000000..18c24ca
--- /dev/null
+++ b/src/components/features/PresetView.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {preset.settings.name}
+
+
+
+
diff --git a/src/components/tags/TagsList.svelte b/src/components/tags/TagsList.svelte
new file mode 100644
index 0000000..8fb00b4
--- /dev/null
+++ b/src/components/tags/TagsList.svelte
@@ -0,0 +1,21 @@
+
+
+
+
+
diff --git a/src/components/ui/DetailsBlock.svelte b/src/components/ui/DetailsBlock.svelte
new file mode 100644
index 0000000..a02fc29
--- /dev/null
+++ b/src/components/ui/DetailsBlock.svelte
@@ -0,0 +1,30 @@
+
+
+
+ {#if title?.length}
+
{title}:
+ {/if}
+
+ {@render children?.()}
+
+
+
+
diff --git a/src/content/components/events/booru-events.ts b/src/content/components/events/booru-events.ts
index 05d9e57..65a429d 100644
--- a/src/content/components/events/booru-events.ts
+++ b/src/content/components/events/booru-events.ts
@@ -1,5 +1,7 @@
export const EVENT_FETCH_COMPLETE = 'fetchcomplete';
+export const EVENT_RELOAD = 'reload';
export interface BooruEventsMap {
[EVENT_FETCH_COMPLETE]: null; // Site sends the response, but extension will not get it due to isolation.
+ [EVENT_RELOAD]: null;
}
diff --git a/src/content/components/events/comms.ts b/src/content/components/events/comms.ts
index c31bae0..7127a25 100644
--- a/src/content/components/events/comms.ts
+++ b/src/content/components/events/comms.ts
@@ -4,13 +4,15 @@ import type { FullscreenViewerEventsMap } from "$content/components/events/fulls
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";
type EventsMapping =
MaintenancePopupEventsMap
& FullscreenViewerEventsMap
& BooruEventsMap
& TagsFormEventsMap
- & TagDropdownEvents;
+ & TagDropdownEvents
+ & PresetBlockEventsMap;
type EventCallback = (event: CustomEvent) => void;
export type UnsubscribeFunction = () => void;
diff --git a/src/content/components/events/preset-block-events.ts b/src/content/components/events/preset-block-events.ts
new file mode 100644
index 0000000..4723d61
--- /dev/null
+++ b/src/content/components/events/preset-block-events.ts
@@ -0,0 +1,10 @@
+export const EVENT_PRESET_TAG_CHANGE_APPLIED = 'preset-tag-changed';
+
+export interface PresetTagChange {
+ addedTags?: Set;
+ removedTags?: Set;
+}
+
+export interface PresetBlockEventsMap {
+ [EVENT_PRESET_TAG_CHANGE_APPLIED]: PresetTagChange;
+}
diff --git a/src/content/components/extension/presets/EditorPresetsBlock.ts b/src/content/components/extension/presets/EditorPresetsBlock.ts
new file mode 100644
index 0000000..86bbe1e
--- /dev/null
+++ b/src/content/components/extension/presets/EditorPresetsBlock.ts
@@ -0,0 +1,101 @@
+import { BaseComponent } from "$content/components/base/BaseComponent";
+import TagEditorPreset from "$entities/TagEditorPreset";
+import PresetTableRow from "$content/components/extension/presets/PresetTableRow";
+import { createFontAwesomeIcon } from "$lib/dom-utils";
+
+export default class EditorPresetsBlock extends BaseComponent {
+ #presetsTable = document.createElement('table');
+ #presetBlocks: PresetTableRow[] = [];
+ #tags: Set = new Set();
+
+ protected build() {
+ this.container.classList.add('block', 'hidden', 'tag-presets');
+ this.container.style.marginTop = 'var(--block-spacing)';
+
+ const header = document.createElement('div');
+ header.classList.add('block__header');
+
+ const headerTitle = document.createElement('div');
+ headerTitle.classList.add('block__header__title')
+ headerTitle.textContent = ' Presets';
+
+ const content = document.createElement('div');
+ content.classList.add('block__content');
+
+ this.#presetsTable.append(
+ document.createElement('thead'),
+ document.createElement('tbody'),
+ );
+
+ this.#presetsTable.tHead?.append(
+ EditorPresetsBlock.#createRowWithTableHeads([
+ 'Name',
+ 'Tags',
+ 'Actions'
+ ]),
+ );
+
+ headerTitle.prepend(createFontAwesomeIcon('layer-group'));
+ header.append(headerTitle);
+ content.append(this.#presetsTable);
+
+ this.container.append(
+ header,
+ content,
+ );
+ }
+
+ protected init() {
+ TagEditorPreset.readAll()
+ .then(this.#refreshPresets.bind(this))
+ .then(() => TagEditorPreset.subscribe(this.#refreshPresets.bind(this)));
+ }
+
+ toggleVisibility(shouldBeVisible: boolean | undefined = undefined) {
+ this.container.classList.toggle('hidden', shouldBeVisible);
+ }
+
+ updateTags(tags: Set) {
+ this.#tags = tags;
+
+ for (const presetBlock of this.#presetBlocks) {
+ presetBlock.updateTags(tags);
+ }
+ }
+
+ #refreshPresets(presetsList: TagEditorPreset[]) {
+ if (this.#presetBlocks.length) {
+ for (const block of this.#presetBlocks) {
+ block.remove();
+ }
+ }
+
+ for (const preset of presetsList) {
+ const block = PresetTableRow.create(preset);
+ this.#presetsTable.tBodies[0]?.append(block.container);
+ block.initialize();
+ block.updateTags(this.#tags);
+
+ this.#presetBlocks.push(block);
+ }
+ }
+
+ static create(): EditorPresetsBlock {
+ return new EditorPresetsBlock(
+ document.createElement('div')
+ );
+ }
+
+ static #createRowWithTableHeads(columnNames: string[]): HTMLTableRowElement {
+ const rowElement = document.createElement('tr');
+
+ for (const columnName of columnNames) {
+ const columnHeadElement = document.createElement('th');
+ columnHeadElement.textContent = columnName;
+
+ rowElement.append(columnHeadElement);
+ }
+
+ return rowElement;
+ }
+}
diff --git a/src/content/components/extension/presets/PresetTableRow.ts b/src/content/components/extension/presets/PresetTableRow.ts
new file mode 100644
index 0000000..0141a46
--- /dev/null
+++ b/src/content/components/extension/presets/PresetTableRow.ts
@@ -0,0 +1,127 @@
+import { BaseComponent } from "$content/components/base/BaseComponent";
+import type TagEditorPreset from "$entities/TagEditorPreset";
+import { emit } from "$content/components/events/comms";
+import { EVENT_PRESET_TAG_CHANGE_APPLIED } from "$content/components/events/preset-block-events";
+import { createFontAwesomeIcon } from "$lib/dom-utils";
+
+export default class PresetTableRow extends BaseComponent {
+ #preset: TagEditorPreset;
+ #tagsList: HTMLElement[] = [];
+ #applyAllButton = document.createElement('button');
+ #removeAllButton = document.createElement('button');
+
+ constructor(container: HTMLElement, preset: TagEditorPreset) {
+ super(container);
+
+ this.#preset = preset;
+ }
+
+ protected build() {
+ this.#tagsList = this.#preset.settings.tags.map(tagName => {
+ const tagElement = document.createElement('span');
+ tagElement.classList.add('tag');
+ tagElement.textContent = tagName;
+ tagElement.dataset.tagName = tagName;
+
+ return tagElement;
+ });
+
+ const nameCell = document.createElement('td');
+ nameCell.textContent = this.#preset.settings.name;
+
+ const tagsCell = document.createElement('td');
+
+ const tagsListContainer = document.createElement('div');
+ tagsListContainer.classList.add('tag-list');
+ tagsListContainer.append(...this.#tagsList);
+
+ tagsCell.append(tagsListContainer);
+
+ const actionsCell = document.createElement('td');
+
+ const actionsContainer = document.createElement('div');
+ actionsContainer.classList.add('flex', 'flex--gap-small');
+
+ this.#applyAllButton.classList.add('button', 'button--state-success', 'button--bold');
+ this.#applyAllButton.append(createFontAwesomeIcon('circle-plus'));
+ this.#applyAllButton.title = 'Add all tags from this preset into the editor';
+
+ this.#removeAllButton.classList.add('button', 'button--state-danger', 'button--bold');
+ this.#removeAllButton.append(createFontAwesomeIcon('circle-minus'));
+ this.#removeAllButton.title = 'Remove all tags from this preset from the editor';
+
+ actionsContainer.append(
+ this.#applyAllButton,
+ this.#removeAllButton,
+ );
+
+ actionsCell.append(actionsContainer);
+
+ this.container.append(
+ nameCell,
+ tagsCell,
+ actionsCell,
+ );
+ }
+
+ protected init() {
+ for (const tagElement of this.#tagsList) {
+ tagElement.addEventListener('click', this.#onTagClicked.bind(this));
+ }
+
+ this.#applyAllButton.addEventListener('click', this.#onApplyAllClicked.bind(this));
+ this.#removeAllButton.addEventListener('click', this.#onRemoveAllClicked.bind(this));
+ }
+
+ #onTagClicked(event: Event) {
+ const targetElement = event.currentTarget;
+
+ if (!(targetElement instanceof HTMLElement)) {
+ return;
+ }
+
+ const tagName = targetElement.dataset.tagName;
+ const isMissing = targetElement.classList.contains(PresetTableRow.#tagMissingClassName);
+
+ emit(this, EVENT_PRESET_TAG_CHANGE_APPLIED, {
+ [isMissing ? 'addedTags' : 'removedTags']: new Set([tagName])
+ });
+ }
+
+ #onApplyAllClicked(event: Event) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ emit(this, EVENT_PRESET_TAG_CHANGE_APPLIED, {
+ addedTags: new Set(this.#preset.settings.tags),
+ });
+ }
+
+ #onRemoveAllClicked(event: Event) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ emit(this, EVENT_PRESET_TAG_CHANGE_APPLIED, {
+ removedTags: new Set(this.#preset.settings.tags),
+ });
+ }
+
+ updateTags(tags: Set) {
+ for (const tagElement of this.#tagsList) {
+ tagElement.classList.toggle(
+ PresetTableRow.#tagMissingClassName,
+ !tags.has(tagElement.dataset.tagName || ''),
+ );
+ }
+ }
+
+ remove() {
+ this.container.remove();
+ }
+
+ static create(preset: TagEditorPreset) {
+ return new this(document.createElement('tr'), preset);
+ }
+
+ static #tagMissingClassName = 'is-missing';
+}
diff --git a/src/content/components/philomena/TagsForm.ts b/src/content/components/philomena/TagsForm.ts
index ab79c8c..03095d5 100644
--- a/src/content/components/philomena/TagsForm.ts
+++ b/src/content/components/philomena/TagsForm.ts
@@ -1,10 +1,36 @@
import { BaseComponent } from "$content/components/base/BaseComponent";
import { getComponent } from "$content/components/base/component-utils";
import { emit, on, type UnsubscribeFunction } from "$content/components/events/comms";
-import { EVENT_FETCH_COMPLETE } from "$content/components/events/booru-events";
+import { EVENT_FETCH_COMPLETE, EVENT_RELOAD } from "$content/components/events/booru-events";
import { EVENT_FORM_EDITOR_UPDATED } from "$content/components/events/tags-form-events";
+import EditorPresetsBlock from "$content/components/extension/presets/EditorPresetsBlock";
+import { EVENT_PRESET_TAG_CHANGE_APPLIED, type PresetTagChange } from "$content/components/events/preset-block-events";
export class TagsForm extends BaseComponent {
+ #togglePresetsButton: HTMLButtonElement = document.createElement('button');
+ #presetsList = EditorPresetsBlock.create();
+ #plainEditorTextarea: HTMLTextAreaElement|null = null;
+ #fancyEditorInput: HTMLInputElement|null = null;
+ #tagsSet: Set = new Set();
+
+ protected build() {
+ this.#togglePresetsButton.classList.add(
+ 'button',
+ 'button--state-primary',
+ 'button--bold',
+ 'button--separate-left',
+ );
+
+ this.#togglePresetsButton.textContent = 'Presets';
+
+ this.container
+ .querySelector('.fancy-tag-edit ~ button:last-of-type')
+ ?.after(this.#togglePresetsButton, this.#presetsList.container);
+
+ this.#plainEditorTextarea = this.container.querySelector('textarea.tagsinput');
+ this.#fancyEditorInput = this.container.querySelector('.js-taginput-fancy input');
+ }
+
protected init() {
// Site sending the event when form is submitted vie Fetch API. We use this event to reload our logic here.
const unsubscribe = on(
@@ -12,6 +38,22 @@ export class TagsForm extends BaseComponent {
EVENT_FETCH_COMPLETE,
() => this.#waitAndDetectUpdatedForm(unsubscribe),
);
+
+ this.#togglePresetsButton.addEventListener('click', this.#togglePresetsList.bind(this));
+ this.#presetsList.initialize();
+
+ this.#plainEditorTextarea?.addEventListener('input', this.#refreshTagsList.bind(this));
+ this.#fancyEditorInput?.addEventListener('keydown', this.#refreshTagsList.bind(this));
+
+ this.#refreshTagsList();
+
+ on(this.#presetsList, EVENT_PRESET_TAG_CHANGE_APPLIED, this.#onTagChangeRequested.bind(this));
+
+ if (this.#plainEditorTextarea) {
+ // When reloaded, we should catch and refresh the colors. Extension reuses this event to force site to update
+ // list of tags in the fancy tag editor.
+ on(this.#plainEditorTextarea, EVENT_RELOAD, this.refreshTagColors.bind(this));
+ }
}
#waitAndDetectUpdatedForm(unsubscribe: UnsubscribeFunction): void {
@@ -111,6 +153,60 @@ export class TagsForm extends BaseComponent {
return tagCategories;
}
+ #togglePresetsList(event: Event) {
+ event.stopPropagation();
+ event.preventDefault();
+
+ this.#presetsList.toggleVisibility();
+ this.#refreshTagsList();
+ }
+
+ #refreshTagsList() {
+ this.#tagsSet = new Set(
+ this.#plainEditorTextarea?.value
+ .split(',')
+ .map(tagName => tagName.trim())
+ );
+
+ this.#presetsList.updateTags(this.#tagsSet);
+ }
+
+ #onTagChangeRequested(event: CustomEvent) {
+ const { addedTags = null, removedTags = null } = event.detail;
+ let tagChangeList: string[] = [];
+
+ if (addedTags) {
+ tagChangeList.push(...addedTags);
+ }
+
+ if (removedTags) {
+ tagChangeList.push(...Array.from(removedTags).map(tagName => `-${tagName}`));
+ }
+
+ this.#applyTagChangesWithFancyTagEditor(
+ tagChangeList.join(',')
+ );
+ }
+
+ #applyTagChangesWithFancyTagEditor(tagsListWithChanges: string): void {
+ if (!this.#fancyEditorInput || !this.#plainEditorTextarea) {
+ return;
+ }
+
+ const originalValue = this.#fancyEditorInput.value;
+
+ // We have to tell plain text editor to also refresh the list of tags in the fancy editor, just in case user
+ // made changes to it in plain mode.
+ emit(this.#plainEditorTextarea, EVENT_RELOAD, null);
+
+ this.#fancyEditorInput.value = tagsListWithChanges;
+ this.#fancyEditorInput.dispatchEvent(new KeyboardEvent('keydown', {
+ key: 'Comma',
+ }));
+
+ this.#fancyEditorInput.value = originalValue;
+ }
+
static watchForEditors() {
document.body.addEventListener('click', event => {
const targetElement = event.target;
diff --git a/src/lib/dom-utils.ts b/src/lib/dom-utils.ts
new file mode 100644
index 0000000..bfef56b
--- /dev/null
+++ b/src/lib/dom-utils.ts
@@ -0,0 +1,11 @@
+/**
+ * Reusable function to create icons from FontAwesome. Usable only for website, since extension doesn't host its own
+ * copy of FA styles. Extension should use imports of SVGs inside CSS instead.
+ * @param iconSlug Slug of the icon to be added.
+ * @return Element with classes for FontAwesome icon added.
+ */
+export function createFontAwesomeIcon(iconSlug: string): HTMLElement {
+ const iconElement = document.createElement('i');
+ iconElement.classList.add('fa-solid', `fa-${iconSlug}`);
+ return iconElement;
+}
diff --git a/src/lib/extension/BulkEntitiesTransporter.ts b/src/lib/extension/BulkEntitiesTransporter.ts
index eba3452..08ed77c 100644
--- a/src/lib/extension/BulkEntitiesTransporter.ts
+++ b/src/lib/extension/BulkEntitiesTransporter.ts
@@ -4,6 +4,7 @@ import type { ImportableElementsList, ImportableEntityObject } from "$lib/extens
import EntitiesTransporter, { type SameSiteStatus } from "$lib/extension/EntitiesTransporter";
import TaggingProfile from "$entities/TaggingProfile";
import TagGroup from "$entities/TagGroup";
+import TagEditorPreset from "$entities/TagEditorPreset";
type TransportersMapping = {
[EntityName in keyof App.EntityNamesMap]: EntitiesTransporter;
@@ -77,6 +78,8 @@ export default class BulkEntitiesTransporter {
return BulkEntitiesTransporter.#transporters.profiles.exportToObject(entity);
case entity instanceof TagGroup:
return BulkEntitiesTransporter.#transporters.groups.exportToObject(entity);
+ case entity instanceof TagEditorPreset:
+ return BulkEntitiesTransporter.#transporters.presets.exportToObject(entity);
}
return null;
@@ -101,6 +104,7 @@ export default class BulkEntitiesTransporter {
static #transporters: TransportersMapping = {
profiles: new EntitiesTransporter(TaggingProfile),
groups: new EntitiesTransporter(TagGroup),
+ presets: new EntitiesTransporter(TagEditorPreset),
}
/**
diff --git a/src/lib/extension/entities/TagEditorPreset.ts b/src/lib/extension/entities/TagEditorPreset.ts
new file mode 100644
index 0000000..d904d5f
--- /dev/null
+++ b/src/lib/extension/entities/TagEditorPreset.ts
@@ -0,0 +1,17 @@
+import StorageEntity from "$lib/extension/base/StorageEntity";
+
+interface TagEditorPresetSettings {
+ name: string;
+ tags: string[];
+}
+
+export default class TagEditorPreset extends StorageEntity {
+ constructor(id: string, settings: Partial) {
+ super(id, {
+ name: settings.name || '',
+ tags: settings.tags || [],
+ });
+ }
+
+ public static readonly _entityName = 'presets';
+}
diff --git a/src/lib/extension/transporting/exporters.ts b/src/lib/extension/transporting/exporters.ts
index a0aa6c7..bdd9d75 100644
--- a/src/lib/extension/transporting/exporters.ts
+++ b/src/lib/extension/transporting/exporters.ts
@@ -33,6 +33,16 @@ const entitiesExporters: ExportersMap = {
category: entity.settings.category,
separate: entity.settings.separate,
}
+ },
+ presets: entity => {
+ return {
+ $type: "presets",
+ $site: __CURRENT_SITE__,
+ v: 1,
+ id: entity.id,
+ name: entity.settings.name,
+ tags: entity.settings.tags,
+ }
}
};
diff --git a/src/lib/extension/transporting/validators.ts b/src/lib/extension/transporting/validators.ts
index 2efec8b..6b3d13e 100644
--- a/src/lib/extension/transporting/validators.ts
+++ b/src/lib/extension/transporting/validators.ts
@@ -64,6 +64,19 @@ const entitiesValidators: EntitiesValidationMap = {
throw new Error('Invalid group format detected!');
}
},
+ presets: importedObject => {
+ if (!importedObject.v || importedObject.v > 1) {
+ throw new Error('Unsupported preset version!');
+ }
+
+ if (
+ !validateRequiredString(importedObject?.id)
+ || !validateRequiredString(importedObject?.name)
+ || !validateOptionalArray(importedObject?.tags)
+ ) {
+ throw new Error('Invalid preset format detected!');
+ }
+ }
};
/**
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 251fd43..e1c7d50 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,3 +1,6 @@
+import type StorageEntity from "$lib/extension/base/StorageEntity";
+import type TagGroup from "$entities/TagGroup";
+
/**
* Traverse and find the object using the key path.
* @param targetObject Target object to traverse into.
@@ -39,3 +42,14 @@ export function escapeRegExp(value: string): string {
unsafeRegExpCharacters.lastIndex = 0;
return value.replace(unsafeRegExpCharacters, "\\$&");
}
+
+type OnlyStringFields> = {
+ [FieldKey in keyof Fields as Fields[FieldKey] extends string ? FieldKey : never]: string;
+};
+
+export function sortEntitiesByField(entities: StorageEntity[], fieldName: keyof OnlyStringFields) {
+ return entities.toSorted(
+ (a, b) => (a.settings[fieldName] as string)
+ .localeCompare(b.settings[fieldName] as string)
+ );
+}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 7886217..a806a3a 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -26,6 +26,7 @@
{/if}
+
diff --git a/src/routes/features/presets/+page.svelte b/src/routes/features/presets/+page.svelte
new file mode 100644
index 0000000..537caf1
--- /dev/null
+++ b/src/routes/features/presets/+page.svelte
@@ -0,0 +1,19 @@
+
+
+
diff --git a/src/routes/features/presets/[id]/+page.svelte b/src/routes/features/presets/[id]/+page.svelte
new file mode 100644
index 0000000..36129c3
--- /dev/null
+++ b/src/routes/features/presets/[id]/+page.svelte
@@ -0,0 +1,42 @@
+
+
+
+{#if preset}
+
+{/if}
+
diff --git a/src/routes/features/presets/[id]/edit/+page.svelte b/src/routes/features/presets/[id]/edit/+page.svelte
new file mode 100644
index 0000000..6a23ea2
--- /dev/null
+++ b/src/routes/features/presets/[id]/edit/+page.svelte
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/transporting/export/+page.svelte b/src/routes/transporting/export/+page.svelte
index ed63fc7..3c3c119 100644
--- a/src/routes/transporting/export/+page.svelte
+++ b/src/routes/transporting/export/+page.svelte
@@ -9,11 +9,13 @@
import FormControl from "$components/ui/forms/FormControl.svelte";
import FormContainer from "$components/ui/forms/FormContainer.svelte";
import { popupTitle } from "$stores/popup";
+ import { tagEditorPresets } from "$stores/entities/tag-editor-presets";
const bulkTransporter = new BulkEntitiesTransporter();
let exportAllProfiles = $state(false);
let exportAllGroups = $state(false);
+ let exportAllPresets = $state(false);
let displayExportedString = $state(false);
let shouldUseCompressed = $state(true);
@@ -24,6 +26,7 @@
const exportedEntities: Record> = $state({
profiles: {},
groups: {},
+ presets: {},
});
$effect(() => {
@@ -42,6 +45,12 @@
}
});
+ $tagEditorPresets.forEach(preset => {
+ if (exportedEntities.presets[preset.id]) {
+ elementsToExport.push(preset);
+ }
+ });
+
plainExport = bulkTransporter.exportToJSON(elementsToExport);
compressedExport = bulkTransporter.exportToCompressedJSON(elementsToExport);
}
@@ -57,6 +66,7 @@
requestAnimationFrame(() => {
exportAllProfiles = $taggingProfiles.every(profile => exportedEntities.profiles[profile.id]);
exportAllGroups = $tagGroups.every(group => exportedEntities.groups[group.id]);
+ exportAllPresets = $tagEditorPresets.every(preset => exportedEntities.presets[preset.id]);
});
}
@@ -74,6 +84,9 @@
case "groups":
$tagGroups.forEach(group => exportedEntities.groups[group.id] = exportAllGroups);
break;
+ case "presets":
+ $tagEditorPresets.forEach(preset => exportedEntities.presets[preset.id] = exportAllPresets);
+ break;
default:
console.warn(`Trying to toggle unsupported entity type: ${targetEntity}`);
}
@@ -116,6 +129,17 @@
{/each}
{/if}
+ {#if $tagEditorPresets.length}
+
+ Export All Presets
+
+ {#each $tagEditorPresets as preset}
+
+ Preset: {preset.settings.name}
+
+ {/each}
+
+ {/if}
{:else}
diff --git a/src/routes/transporting/import/+page.svelte b/src/routes/transporting/import/+page.svelte
index 0677607..82f2eee 100644
--- a/src/routes/transporting/import/+page.svelte
+++ b/src/routes/transporting/import/+page.svelte
@@ -16,21 +16,26 @@
import type { SameSiteStatus } from "$lib/extension/EntitiesTransporter";
import { popupTitle } from "$stores/popup";
import Notice from "$components/ui/Notice.svelte";
+ import TagEditorPreset from "$entities/TagEditorPreset";
+ import { tagEditorPresets } from "$stores/entities/tag-editor-presets";
let importedString = $state('');
let errorMessage = $state('');
let importedProfiles = $state([]);
let importedGroups = $state([]);
+ let importedPresets = $state([]);
let saveAllProfiles = $state(false);
let saveAllGroups = $state(false);
+ let saveAllPresets = $state(false);
let isSaving = $state(false);
let selectedEntities: Record> = $state({
profiles: {},
groups: {},
+ presets: {},
});
let previewedEntity = $state(null);
@@ -49,8 +54,15 @@
}, new Map())
);
+ const existingPresetsMap = $derived(
+ $tagEditorPresets.reduce((map, preset) => {
+ map.set(preset.id, preset);
+ return map;
+ }, new Map())
+ );
+
const hasImportedEntities = $derived(
- Boolean(importedProfiles.length || importedGroups.length)
+ Boolean(importedProfiles.length || importedGroups.length || importedPresets.length)
);
$effect(() => {
@@ -70,6 +82,7 @@
function tryBulkImport() {
importedProfiles = [];
importedGroups = [];
+ importedPresets = [];
errorMessage = '';
importedString = importedString.trim();
@@ -103,6 +116,9 @@
case "groups":
importedGroups.push(targetImportedEntity as TagGroup);
break;
+ case "presets":
+ importedPresets.push(targetImportedEntity as TagEditorPreset);
+ break;
default:
console.warn(`Unprocessed entity type detected: ${targetImportedEntity.type}`, targetImportedEntity);
}
@@ -115,12 +131,14 @@
function cancelImport() {
importedProfiles = [];
importedGroups = [];
+ importedPresets = [];
}
function refreshAreAllEntitiesChecked() {
requestAnimationFrame(() => {
saveAllProfiles = importedProfiles.every(profile => selectedEntities.profiles[profile.id]);
saveAllGroups = importedGroups.every(group => selectedEntities.groups[group.id]);
+ saveAllPresets = importedPresets.every(preset => selectedEntities.presets[preset.id]);
});
}
@@ -134,6 +152,9 @@
case "groups":
importedGroups.forEach(group => selectedEntities.groups[group.id] = saveAllGroups);
break;
+ case "presets":
+ importedPresets.forEach(preset => selectedEntities.presets[preset.id] = saveAllPresets);
+ break;
default:
console.warn(`Trying to toggle unsupported entity type: ${entityType}`);
}
@@ -171,6 +192,14 @@
await group.save();
}
+ for (const preset of importedPresets) {
+ if (!selectedEntities.presets[preset.id]) {
+ continue;
+ }
+
+ await preset.save();
+ }
+
await goto("/transporting");
}
@@ -251,10 +280,7 @@
{/if}
{#if importedGroups.length}
-
+
Import All Groups
{#each importedGroups as candidateGroup}
@@ -272,6 +298,26 @@
{/each}
{/if}
+ {#if importedPresets.length}
+
+
+ Import All Presets
+
+ {#each importedPresets as candidatePreset}
+
+ {#if existingPresetsMap.has(candidatePreset.id)}
+ Update:
+ {:else}
+ New:
+ {/if}
+ {candidatePreset.settings.name || 'Unnamed Preset'}
+
+ {/each}
+ {/if}