mirror of
https://github.com/koloml/philomena-tagging-assistant.git
synced 2026-06-24 02:32:21 +00:00
Added Tag Presets, popup editor for them, implemented presets image edit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<EventDetails> = (event: CustomEvent<EventDetails>) => void;
|
||||
export type UnsubscribeFunction = () => void;
|
||||
|
||||
10
src/content/components/events/preset-block-events.ts
Normal file
10
src/content/components/events/preset-block-events.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export const EVENT_PRESET_TAG_CHANGE_APPLIED = 'preset-tag-changed';
|
||||
|
||||
export interface PresetTagChange {
|
||||
addedTags?: Set<string>;
|
||||
removedTags?: Set<string>;
|
||||
}
|
||||
|
||||
export interface PresetBlockEventsMap {
|
||||
[EVENT_PRESET_TAG_CHANGE_APPLIED]: PresetTagChange;
|
||||
}
|
||||
101
src/content/components/extension/presets/EditorPresetsBlock.ts
Normal file
101
src/content/components/extension/presets/EditorPresetsBlock.ts
Normal file
@@ -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<string> = 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<string>) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
127
src/content/components/extension/presets/PresetTableRow.ts
Normal file
127
src/content/components/extension/presets/PresetTableRow.ts
Normal file
@@ -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<string>) {
|
||||
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';
|
||||
}
|
||||
@@ -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<string> = 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<PresetTagChange>) {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user