mirror of
https://github.com/koloml/furbooru-tagging-assistant.git
synced 2026-03-24 23:02:58 +00:00
Compare commits
6 Commits
0.6.1
...
6c2ef795b3
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c2ef795b3 | |||
| 58b620ef09 | |||
| 9445b1e862 | |||
| 9024883949 | |||
| dc29c6ca69 | |||
| 441091142c |
BIN
.github/assets/derpibooru-preview-of-tag-link-replacement.png
vendored
Normal file
BIN
.github/assets/derpibooru-preview-of-tag-link-replacement.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
4
src/app.d.ts
vendored
4
src/app.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import type TagGroup from "$entities/TagGroup";
|
||||
|
||||
declare global {
|
||||
@@ -37,7 +37,7 @@ declare global {
|
||||
);
|
||||
|
||||
interface EntityNamesMap {
|
||||
profiles: MaintenanceProfile;
|
||||
profiles: TaggingProfile;
|
||||
groups: TagGroup;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import type TaggingProfile from "$entities/TaggingProfile";
|
||||
|
||||
interface ProfileViewProps {
|
||||
profile: MaintenanceProfile;
|
||||
profile: TaggingProfile;
|
||||
}
|
||||
|
||||
let { profile }: ProfileViewProps = $props();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FullscreenViewerSize } from "$lib/extension/settings/MiscSettings";
|
||||
import type { FullscreenViewerSize } from "$lib/extension/preferences/MiscPreferences";
|
||||
|
||||
export const EVENT_SIZE_LOADED = 'size-loaded';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import type TaggingProfile from "$entities/TaggingProfile";
|
||||
|
||||
export const EVENT_ACTIVE_PROFILE_CHANGED = 'active-profile-changed';
|
||||
export const EVENT_MAINTENANCE_STATE_CHANGED = 'maintenance-state-change';
|
||||
@@ -7,7 +7,7 @@ export const EVENT_TAGS_UPDATED = 'tags-updated';
|
||||
type MaintenanceState = 'processing' | 'failed' | 'complete' | 'waiting';
|
||||
|
||||
export interface MaintenancePopupEventsMap {
|
||||
[EVENT_ACTIVE_PROFILE_CHANGED]: MaintenanceProfile | null;
|
||||
[EVENT_ACTIVE_PROFILE_CHANGED]: TaggingProfile | null;
|
||||
[EVENT_MAINTENANCE_STATE_CHANGED]: MaintenanceState;
|
||||
[EVENT_TAGS_UPDATED]: Map<string, string> | null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BaseComponent } from "$content/components/base/BaseComponent";
|
||||
import MiscSettings, { type FullscreenViewerSize } from "$lib/extension/settings/MiscSettings";
|
||||
import MiscPreferences, { type FullscreenViewerSize } from "$lib/extension/preferences/MiscPreferences";
|
||||
import { emit, on } from "$content/components/events/comms";
|
||||
import { EVENT_SIZE_LOADED } from "$content/components/events/fullscreen-viewer-events";
|
||||
|
||||
@@ -53,8 +53,8 @@ export class FullscreenViewer extends BaseComponent {
|
||||
this.#imageElement.addEventListener('load', this.#onLoaded.bind(this));
|
||||
this.#sizeSelectorElement.addEventListener('click', event => event.stopPropagation());
|
||||
|
||||
FullscreenViewer.#miscSettings
|
||||
.resolveFullscreenViewerPreviewSize()
|
||||
FullscreenViewer.#preferences
|
||||
.fullscreenViewerSize.get()
|
||||
.then(this.#onSizeResolved.bind(this))
|
||||
.then(this.#watchForSizeSelectionChanges.bind(this));
|
||||
}
|
||||
@@ -179,7 +179,7 @@ export class FullscreenViewer extends BaseComponent {
|
||||
#watchForSizeSelectionChanges() {
|
||||
let lastActiveSize = this.#sizeSelectorElement.value;
|
||||
|
||||
FullscreenViewer.#miscSettings.subscribe(settings => {
|
||||
FullscreenViewer.#preferences.subscribe(settings => {
|
||||
const targetSize = settings.fullscreenViewerSize;
|
||||
|
||||
if (!targetSize || lastActiveSize === targetSize) {
|
||||
@@ -202,7 +202,7 @@ export class FullscreenViewer extends BaseComponent {
|
||||
}
|
||||
|
||||
lastActiveSize = targetSize;
|
||||
void FullscreenViewer.#miscSettings.setFullscreenViewerPreviewSize(targetSize);
|
||||
void FullscreenViewer.#preferences.fullscreenViewerSize.set(targetSize as FullscreenViewerSize);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -289,7 +289,7 @@ export class FullscreenViewer extends BaseComponent {
|
||||
return url.endsWith('.mp4') || url.endsWith('.webm');
|
||||
}
|
||||
|
||||
static #miscSettings = new MiscSettings();
|
||||
static #preferences = new MiscPreferences();
|
||||
|
||||
static #offsetProperty = '--offset';
|
||||
static #opacityProperty = '--opacity';
|
||||
@@ -1,8 +1,8 @@
|
||||
import { BaseComponent } from "$content/components/base/BaseComponent";
|
||||
import { getComponent } from "$content/components/base/component-utils";
|
||||
import MiscSettings from "$lib/extension/settings/MiscSettings";
|
||||
import { FullscreenViewer } from "$content/components/FullscreenViewer";
|
||||
import type { MediaBoxTools } from "$content/components/MediaBoxTools";
|
||||
import MiscPreferences from "$lib/extension/preferences/MiscPreferences";
|
||||
import { FullscreenViewer } from "$content/components/extension/FullscreenViewer";
|
||||
import type { MediaBoxTools } from "$content/components/extension/MediaBoxTools";
|
||||
|
||||
export class ImageShowFullscreenButton extends BaseComponent {
|
||||
#mediaBoxTools: MediaBoxTools | null = null;
|
||||
@@ -10,8 +10,6 @@ export class ImageShowFullscreenButton extends BaseComponent {
|
||||
|
||||
protected build() {
|
||||
this.container.innerText = '🔍';
|
||||
|
||||
ImageShowFullscreenButton.#miscSettings ??= new MiscSettings();
|
||||
}
|
||||
|
||||
protected init() {
|
||||
@@ -27,14 +25,14 @@ export class ImageShowFullscreenButton extends BaseComponent {
|
||||
|
||||
this.on('click', this.#onButtonClicked.bind(this));
|
||||
|
||||
if (ImageShowFullscreenButton.#miscSettings) {
|
||||
ImageShowFullscreenButton.#miscSettings.resolveFullscreenViewerEnabled()
|
||||
if (ImageShowFullscreenButton.#preferences) {
|
||||
ImageShowFullscreenButton.#preferences.fullscreenViewer.get()
|
||||
.then(isEnabled => {
|
||||
this.#isFullscreenButtonEnabled = isEnabled;
|
||||
this.#updateFullscreenButtonVisibility();
|
||||
})
|
||||
.then(() => {
|
||||
ImageShowFullscreenButton.#miscSettings?.subscribe(settings => {
|
||||
ImageShowFullscreenButton.#preferences?.subscribe(settings => {
|
||||
this.#isFullscreenButtonEnabled = settings.fullscreenViewer ?? true;
|
||||
this.#updateFullscreenButtonVisibility();
|
||||
})
|
||||
@@ -58,6 +56,15 @@ export class ImageShowFullscreenButton extends BaseComponent {
|
||||
?.show(imageLinks);
|
||||
}
|
||||
|
||||
static create(): HTMLElement {
|
||||
const element = document.createElement('div');
|
||||
element.classList.add('media-box-show-fullscreen');
|
||||
|
||||
new ImageShowFullscreenButton(element);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
static #viewer: FullscreenViewer | null = null;
|
||||
|
||||
static #resolveViewer(): FullscreenViewer {
|
||||
@@ -76,14 +83,5 @@ export class ImageShowFullscreenButton extends BaseComponent {
|
||||
return viewer;
|
||||
}
|
||||
|
||||
static #miscSettings: MiscSettings | null = null;
|
||||
}
|
||||
|
||||
export function createImageShowFullscreenButton() {
|
||||
const element = document.createElement('div');
|
||||
element.classList.add('media-box-show-fullscreen');
|
||||
|
||||
new ImageShowFullscreenButton(element);
|
||||
|
||||
return element;
|
||||
static #preferences = new MiscPreferences();
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { BaseComponent } from "$content/components/base/BaseComponent";
|
||||
import { getComponent } from "$content/components/base/component-utils";
|
||||
import { MaintenancePopup } from "$content/components/MaintenancePopup";
|
||||
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 type { MediaBoxWrapper } from "$content/components/MediaBoxWrapper";
|
||||
import type MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import type { MediaBox } from "$content/components/philomena/MediaBox";
|
||||
import type TaggingProfile from "$entities/TaggingProfile";
|
||||
|
||||
export class MediaBoxTools extends BaseComponent {
|
||||
#mediaBox: MediaBoxWrapper | null = null;
|
||||
#maintenancePopup: MaintenancePopup | null = null;
|
||||
#mediaBox: MediaBox | null = null;
|
||||
#maintenancePopup: TaggingProfilePopup | null = null;
|
||||
|
||||
init() {
|
||||
const mediaBoxElement = this.container.closest<HTMLElement>('.media-box');
|
||||
@@ -34,7 +34,7 @@ export class MediaBoxTools extends BaseComponent {
|
||||
component.initialize();
|
||||
}
|
||||
|
||||
if (!this.#maintenancePopup && component instanceof MaintenancePopup) {
|
||||
if (!this.#maintenancePopup && component instanceof TaggingProfilePopup) {
|
||||
this.#maintenancePopup = component;
|
||||
}
|
||||
}
|
||||
@@ -42,33 +42,33 @@ export class MediaBoxTools extends BaseComponent {
|
||||
on(this, EVENT_ACTIVE_PROFILE_CHANGED, this.#onActiveProfileChanged.bind(this));
|
||||
}
|
||||
|
||||
#onActiveProfileChanged(profileChangedEvent: CustomEvent<MaintenanceProfile | null>) {
|
||||
#onActiveProfileChanged(profileChangedEvent: CustomEvent<TaggingProfile | null>) {
|
||||
this.container.classList.toggle('has-active-profile', profileChangedEvent.detail !== null);
|
||||
}
|
||||
|
||||
get maintenancePopup(): MaintenancePopup | null {
|
||||
get maintenancePopup(): TaggingProfilePopup | null {
|
||||
return this.#maintenancePopup;
|
||||
}
|
||||
|
||||
get mediaBox(): MediaBoxWrapper | null {
|
||||
get mediaBox(): MediaBox | null {
|
||||
return this.#mediaBox;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a maintenance popup element.
|
||||
* @param childrenElements List of children elements to append to the component.
|
||||
* @return The maintenance popup element.
|
||||
*/
|
||||
export function createMediaBoxTools(...childrenElements: HTMLElement[]): HTMLElement {
|
||||
const mediaBoxToolsContainer = document.createElement('div');
|
||||
mediaBoxToolsContainer.classList.add('media-box-tools');
|
||||
/**
|
||||
* Create a maintenance popup element.
|
||||
* @param childrenElements List of children elements to append to the component.
|
||||
* @return The maintenance popup element.
|
||||
*/
|
||||
static create(...childrenElements: HTMLElement[]): HTMLElement {
|
||||
const mediaBoxToolsContainer = document.createElement('div');
|
||||
mediaBoxToolsContainer.classList.add('media-box-tools');
|
||||
|
||||
if (childrenElements.length) {
|
||||
mediaBoxToolsContainer.append(...childrenElements);
|
||||
if (childrenElements.length) {
|
||||
mediaBoxToolsContainer.append(...childrenElements);
|
||||
}
|
||||
|
||||
new MediaBoxTools(mediaBoxToolsContainer);
|
||||
|
||||
return mediaBoxToolsContainer;
|
||||
}
|
||||
|
||||
new MediaBoxTools(mediaBoxToolsContainer);
|
||||
|
||||
return mediaBoxToolsContainer;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings";
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import TaggingProfilesPreferences from "$lib/extension/preferences/TaggingProfilesPreferences";
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import { BaseComponent } from "$content/components/base/BaseComponent";
|
||||
import { getComponent } from "$content/components/base/component-utils";
|
||||
import ScrapedAPI from "$lib/booru/scraped/ScrapedAPI";
|
||||
import ScrapedAPI from "$lib/philomena/scraping/ScrapedAPI";
|
||||
import { tagsBlacklist } from "$config/tags";
|
||||
import { emitterAt } from "$content/components/events/comms";
|
||||
import {
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
EVENT_MAINTENANCE_STATE_CHANGED,
|
||||
EVENT_TAGS_UPDATED
|
||||
} from "$content/components/events/maintenance-popup-events";
|
||||
import type { MediaBoxTools } from "$content/components/MediaBoxTools";
|
||||
import { resolveTagCategoryFromTagName } from "$lib/booru/tag-utils";
|
||||
import type { MediaBoxTools } from "$content/components/extension/MediaBoxTools";
|
||||
import { resolveTagCategoryFromTagName } from "$lib/philomena/tag-utils";
|
||||
|
||||
class BlackListedTagsEncounteredError extends Error {
|
||||
constructor(tagName: string) {
|
||||
@@ -21,11 +21,11 @@ class BlackListedTagsEncounteredError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export class MaintenancePopup extends BaseComponent {
|
||||
export class TaggingProfilePopup extends BaseComponent {
|
||||
#tagsListElement: HTMLElement = document.createElement('div');
|
||||
#tagsList: HTMLElement[] = [];
|
||||
#suggestedInvalidTags: Map<string, HTMLElement> = new Map();
|
||||
#activeProfile: MaintenanceProfile | null = null;
|
||||
#activeProfile: TaggingProfile | null = null;
|
||||
#mediaBoxTools: MediaBoxTools | null = null;
|
||||
#tagsToRemove: Set<string> = new Set();
|
||||
#tagsToAdd: Set<string> = new Set();
|
||||
@@ -66,7 +66,7 @@ export class MaintenancePopup extends BaseComponent {
|
||||
|
||||
this.#mediaBoxTools = mediaBoxTools;
|
||||
|
||||
MaintenancePopup.#watchActiveProfile(this.#onActiveProfileChanged.bind(this));
|
||||
TaggingProfilePopup.#watchActiveProfile(this.#onActiveProfileChanged.bind(this));
|
||||
this.#tagsListElement.addEventListener('click', this.#handleTagClick.bind(this));
|
||||
|
||||
const mediaBox = this.#mediaBoxTools.mediaBox;
|
||||
@@ -79,7 +79,7 @@ export class MaintenancePopup extends BaseComponent {
|
||||
mediaBox.on('mouseover', this.#onMouseEnteredArea.bind(this));
|
||||
}
|
||||
|
||||
#onActiveProfileChanged(activeProfile: MaintenanceProfile | null) {
|
||||
#onActiveProfileChanged(activeProfile: TaggingProfile | null) {
|
||||
this.#activeProfile = activeProfile;
|
||||
this.container.classList.toggle('is-active', activeProfile !== null);
|
||||
this.#refreshTagsList();
|
||||
@@ -110,7 +110,7 @@ export class MaintenancePopup extends BaseComponent {
|
||||
activeProfileTagsList
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.forEach((tagName, index) => {
|
||||
const tagElement = MaintenancePopup.#buildTagElement(tagName);
|
||||
const tagElement = TaggingProfilePopup.#buildTagElement(tagName);
|
||||
this.#tagsList[index] = tagElement;
|
||||
this.#tagsListElement.appendChild(tagElement);
|
||||
|
||||
@@ -122,10 +122,10 @@ export class MaintenancePopup extends BaseComponent {
|
||||
|
||||
// Just to prevent duplication, we need to include this tag to the map of suggested invalid tags
|
||||
if (tagsBlacklist.includes(tagName)) {
|
||||
MaintenancePopup.#markTagElementWithCategory(tagElement, 'error');
|
||||
TaggingProfilePopup.#markTagElementWithCategory(tagElement, 'error');
|
||||
this.#suggestedInvalidTags.set(tagName, tagElement);
|
||||
} else {
|
||||
MaintenancePopup.#markTagElementWithCategory(
|
||||
TaggingProfilePopup.#markTagElementWithCategory(
|
||||
tagElement,
|
||||
resolveTagCategoryFromTagName(tagName) ?? '',
|
||||
);
|
||||
@@ -179,7 +179,7 @@ export class MaintenancePopup extends BaseComponent {
|
||||
if (this.#tagsToAdd.size || this.#tagsToRemove.size) {
|
||||
// Notify only once, when first planning to submit
|
||||
if (!this.#isPlanningToSubmit) {
|
||||
MaintenancePopup.#notifyAboutPendingSubmission(true);
|
||||
TaggingProfilePopup.#notifyAboutPendingSubmission(true);
|
||||
}
|
||||
|
||||
this.#isPlanningToSubmit = true;
|
||||
@@ -197,7 +197,7 @@ export class MaintenancePopup extends BaseComponent {
|
||||
if (this.#isPlanningToSubmit && !this.#isSubmitting) {
|
||||
this.#tagsSubmissionTimer = setTimeout(
|
||||
this.#onSubmissionTimerPassed.bind(this),
|
||||
MaintenancePopup.#delayBeforeSubmissionMs
|
||||
TaggingProfilePopup.#delayBeforeSubmissionMs
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -214,10 +214,10 @@ export class MaintenancePopup extends BaseComponent {
|
||||
|
||||
let maybeTagsAndAliasesAfterUpdate;
|
||||
|
||||
const shouldAutoRemove = await MaintenancePopup.#maintenanceSettings.resolveStripBlacklistedTags();
|
||||
const shouldAutoRemove = await TaggingProfilePopup.#preferences.stripBlacklistedTags.get();
|
||||
|
||||
try {
|
||||
maybeTagsAndAliasesAfterUpdate = await MaintenancePopup.#scrapedAPI.updateImageTags(
|
||||
maybeTagsAndAliasesAfterUpdate = await TaggingProfilePopup.#scrapedAPI.updateImageTags(
|
||||
this.#mediaBoxTools.mediaBox.imageId,
|
||||
tagsList => {
|
||||
for (let tagName of this.#tagsToRemove) {
|
||||
@@ -250,7 +250,7 @@ export class MaintenancePopup extends BaseComponent {
|
||||
console.warn('Tags submission failed:', e);
|
||||
}
|
||||
|
||||
MaintenancePopup.#notifyAboutPendingSubmission(false);
|
||||
TaggingProfilePopup.#notifyAboutPendingSubmission(false);
|
||||
|
||||
this.#emitter.emit(EVENT_MAINTENANCE_STATE_CHANGED, 'failed');
|
||||
this.#isSubmitting = false;
|
||||
@@ -268,7 +268,7 @@ export class MaintenancePopup extends BaseComponent {
|
||||
this.#tagsToRemove.clear();
|
||||
|
||||
this.#refreshTagsList();
|
||||
MaintenancePopup.#notifyAboutPendingSubmission(false);
|
||||
TaggingProfilePopup.#notifyAboutPendingSubmission(false);
|
||||
|
||||
this.#isSubmitting = false;
|
||||
}
|
||||
@@ -292,8 +292,8 @@ export class MaintenancePopup extends BaseComponent {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tagElement = MaintenancePopup.#buildTagElement(tagName);
|
||||
MaintenancePopup.#markTagElementWithCategory(tagElement, 'error');
|
||||
const tagElement = TaggingProfilePopup.#buildTagElement(tagName);
|
||||
TaggingProfilePopup.#markTagElementWithCategory(tagElement, 'error');
|
||||
tagElement.classList.add('is-present');
|
||||
|
||||
this.#suggestedInvalidTags.set(tagName, tagElement);
|
||||
@@ -311,6 +311,14 @@ export class MaintenancePopup extends BaseComponent {
|
||||
return this.container.classList.contains('is-active');
|
||||
}
|
||||
|
||||
static create(): HTMLElement {
|
||||
const container = document.createElement('div');
|
||||
|
||||
new this(container);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
static #buildTagElement(tagName: string): HTMLElement {
|
||||
const tagElement = document.createElement('span');
|
||||
tagElement.classList.add('tag');
|
||||
@@ -333,7 +341,7 @@ export class MaintenancePopup extends BaseComponent {
|
||||
/**
|
||||
* Controller with maintenance settings.
|
||||
*/
|
||||
static #maintenanceSettings = new MaintenanceSettings();
|
||||
static #preferences = new TaggingProfilesPreferences();
|
||||
|
||||
/**
|
||||
* Subscribe to all necessary feeds to watch for every active profile change. Additionally, will execute the callback
|
||||
@@ -341,10 +349,10 @@ export class MaintenancePopup extends BaseComponent {
|
||||
* @param callback Callback to execute whenever selection of active profile or profile itself has been changed.
|
||||
* @return Unsubscribe function. Call it to stop watching for changes.
|
||||
*/
|
||||
static #watchActiveProfile(callback: (profile: MaintenanceProfile | null) => void): () => void {
|
||||
static #watchActiveProfile(callback: (profile: TaggingProfile | null) => void): () => void {
|
||||
let lastActiveProfileId: string | null | undefined = null;
|
||||
|
||||
const unsubscribeFromProfilesChanges = MaintenanceProfile.subscribe(profiles => {
|
||||
const unsubscribeFromProfilesChanges = TaggingProfile.subscribe(profiles => {
|
||||
if (lastActiveProfileId) {
|
||||
callback(
|
||||
profiles.find(profile => profile.id === lastActiveProfileId) || null
|
||||
@@ -352,20 +360,18 @@ export class MaintenancePopup extends BaseComponent {
|
||||
}
|
||||
});
|
||||
|
||||
const unsubscribeFromMaintenanceSettings = this.#maintenanceSettings.subscribe(settings => {
|
||||
const unsubscribeFromMaintenanceSettings = this.#preferences.subscribe(settings => {
|
||||
if (settings.activeProfile === lastActiveProfileId) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastActiveProfileId = settings.activeProfile;
|
||||
|
||||
this.#maintenanceSettings
|
||||
.resolveActiveProfileAsObject()
|
||||
this.#preferences.activeProfile.asObject()
|
||||
.then(callback);
|
||||
});
|
||||
|
||||
this.#maintenanceSettings
|
||||
.resolveActiveProfileAsObject()
|
||||
this.#preferences.activeProfile.asObject()
|
||||
.then(profileOrNull => {
|
||||
if (profileOrNull) {
|
||||
lastActiveProfileId = profileOrNull.id;
|
||||
@@ -416,11 +422,3 @@ export class MaintenancePopup extends BaseComponent {
|
||||
*/
|
||||
static #pendingSubmissionCount: number|null = null;
|
||||
}
|
||||
|
||||
export function createMaintenancePopup() {
|
||||
const container = document.createElement('div');
|
||||
|
||||
new MaintenancePopup(container);
|
||||
|
||||
return container;
|
||||
}
|
||||
@@ -2,9 +2,9 @@ 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 type { MediaBoxTools } from "$content/components/MediaBoxTools";
|
||||
import type { MediaBoxTools } from "$content/components/extension/MediaBoxTools";
|
||||
|
||||
export class MaintenanceStatusIcon extends BaseComponent {
|
||||
export class TaggingProfileStatusIcon extends BaseComponent {
|
||||
#mediaBoxTools: MediaBoxTools | null = null;
|
||||
|
||||
build() {
|
||||
@@ -52,13 +52,13 @@ export class MaintenanceStatusIcon extends BaseComponent {
|
||||
this.container.innerText = '❓';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createMaintenanceStatusIcon() {
|
||||
const element = document.createElement('div');
|
||||
element.classList.add('maintenance-status-icon');
|
||||
|
||||
new MaintenanceStatusIcon(element);
|
||||
|
||||
return element;
|
||||
|
||||
static create(): HTMLElement {
|
||||
const element = document.createElement('div');
|
||||
element.classList.add('maintenance-status-icon');
|
||||
|
||||
new TaggingProfileStatusIcon(element);
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BaseComponent } from "$content/components/base/BaseComponent";
|
||||
import TagSettings from "$lib/extension/settings/TagSettings";
|
||||
import TagsPreferences from "$lib/extension/preferences/TagsPreferences";
|
||||
import { getComponent } from "$content/components/base/component-utils";
|
||||
import { resolveTagNameFromLink, resolveTagCategoryFromTagName } from "$lib/booru/tag-utils";
|
||||
import { resolveTagNameFromLink, resolveTagCategoryFromTagName } from "$lib/philomena/tag-utils";
|
||||
|
||||
export class BlockCommunication extends BaseComponent {
|
||||
#contentSection: HTMLElement | null = null;
|
||||
@@ -17,8 +17,8 @@ export class BlockCommunication extends BaseComponent {
|
||||
|
||||
protected init() {
|
||||
Promise.all([
|
||||
BlockCommunication.#tagSettings.resolveReplaceLinks(),
|
||||
BlockCommunication.#tagSettings.resolveReplaceLinkText(),
|
||||
BlockCommunication.#preferences.replaceLinks.get(),
|
||||
BlockCommunication.#preferences.replaceLinkText.get(),
|
||||
]).then(([replaceLinks, replaceLinkText]) => {
|
||||
this.#onReplaceLinkSettingResolved(
|
||||
replaceLinks,
|
||||
@@ -26,7 +26,7 @@ export class BlockCommunication extends BaseComponent {
|
||||
);
|
||||
});
|
||||
|
||||
BlockCommunication.#tagSettings.subscribe(settings => {
|
||||
BlockCommunication.#preferences.subscribe(settings => {
|
||||
this.#onReplaceLinkSettingResolved(
|
||||
settings.replaceLinks ?? false,
|
||||
settings.replaceLinkText ?? true
|
||||
@@ -112,7 +112,7 @@ export class BlockCommunication extends BaseComponent {
|
||||
);
|
||||
}
|
||||
|
||||
static #tagSettings = new TagSettings();
|
||||
static #preferences = new TagsPreferences();
|
||||
|
||||
/**
|
||||
* Map of links to their original texts. These texts need to be stored here to make them restorable. Keys is a link
|
||||
@@ -1,10 +1,10 @@
|
||||
import { BaseComponent } from "$content/components/base/BaseComponent";
|
||||
import { getComponent } from "$content/components/base/component-utils";
|
||||
import { buildTagsAndAliasesMap } from "$lib/booru/tag-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";
|
||||
|
||||
export class MediaBoxWrapper extends BaseComponent {
|
||||
export class MediaBox extends BaseComponent {
|
||||
#thumbnailContainer: HTMLElement | null = null;
|
||||
#imageLinkElement: HTMLAnchorElement | null = null;
|
||||
#tagsAndAliases: Map<string, string> | null = null;
|
||||
@@ -60,40 +60,44 @@ export class MediaBoxWrapper extends BaseComponent {
|
||||
|
||||
return JSON.parse(jsonUris);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the media box element into the special wrapper.
|
||||
*/
|
||||
export function initializeMediaBox(mediaBoxContainer: HTMLElement, childComponentElements: HTMLElement[]) {
|
||||
new MediaBoxWrapper(mediaBoxContainer)
|
||||
.initialize();
|
||||
/**
|
||||
* Wrap the media box element into the special wrapper.
|
||||
*/
|
||||
static initialize(mediaBoxContainer: HTMLElement, childComponentElements: HTMLElement[]) {
|
||||
new MediaBox(mediaBoxContainer)
|
||||
.initialize();
|
||||
|
||||
for (let childComponentElement of childComponentElements) {
|
||||
mediaBoxContainer.appendChild(childComponentElement);
|
||||
getComponent(childComponentElement)?.initialize();
|
||||
for (let childComponentElement of childComponentElements) {
|
||||
mediaBoxContainer.appendChild(childComponentElement);
|
||||
getComponent(childComponentElement)?.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
static findElements(): NodeListOf<HTMLElement> {
|
||||
return document.querySelectorAll('.media-box');
|
||||
}
|
||||
|
||||
static initializePositionCalculation(mediaBoxesList: NodeListOf<HTMLElement>) {
|
||||
window.addEventListener('resize', () => {
|
||||
let lastMediaBox: HTMLElement | null = null;
|
||||
let lastMediaBoxPosition: number | null = null;
|
||||
|
||||
for (const mediaBoxElement of mediaBoxesList) {
|
||||
const yPosition = mediaBoxElement.getBoundingClientRect().y;
|
||||
const isOnTheSameLine = yPosition === lastMediaBoxPosition;
|
||||
|
||||
mediaBoxElement.classList.toggle('media-box--first', !isOnTheSameLine);
|
||||
lastMediaBox?.classList.toggle('media-box--last', !isOnTheSameLine);
|
||||
|
||||
lastMediaBox = mediaBoxElement;
|
||||
lastMediaBoxPosition = yPosition;
|
||||
}
|
||||
|
||||
// Last-ever media box is checked separately
|
||||
if (lastMediaBox && !lastMediaBox.nextElementSibling) {
|
||||
lastMediaBox.classList.add('media-box--last');
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function calculateMediaBoxesPositions(mediaBoxesList: NodeListOf<HTMLElement>) {
|
||||
window.addEventListener('resize', () => {
|
||||
let lastMediaBox: HTMLElement | null = null;
|
||||
let lastMediaBoxPosition: number | null = null;
|
||||
|
||||
for (const mediaBoxElement of mediaBoxesList) {
|
||||
const yPosition = mediaBoxElement.getBoundingClientRect().y;
|
||||
const isOnTheSameLine = yPosition === lastMediaBoxPosition;
|
||||
|
||||
mediaBoxElement.classList.toggle('media-box--first', !isOnTheSameLine);
|
||||
lastMediaBox?.classList.toggle('media-box--last', !isOnTheSameLine);
|
||||
|
||||
lastMediaBox = mediaBoxElement;
|
||||
lastMediaBoxPosition = yPosition;
|
||||
}
|
||||
|
||||
// Last-ever media box is checked separately
|
||||
if (lastMediaBox && !lastMediaBox.nextElementSibling) {
|
||||
lastMediaBox.classList.add('media-box--last');
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BaseComponent } from "$content/components/base/BaseComponent";
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings";
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import TaggingProfilesPreferences from "$lib/extension/preferences/TaggingProfilesPreferences";
|
||||
import { getComponent } from "$content/components/base/component-utils";
|
||||
import CustomCategoriesResolver from "$lib/extension/CustomCategoriesResolver";
|
||||
import { on } from "$content/components/events/comms";
|
||||
@@ -8,9 +8,7 @@ import { EVENT_FORM_EDITOR_UPDATED } from "$content/components/events/tags-form-
|
||||
import { EVENT_TAG_GROUP_RESOLVED } from "$content/components/events/tag-dropdown-events";
|
||||
import type TagGroup from "$entities/TagGroup";
|
||||
|
||||
const categoriesResolver = new CustomCategoriesResolver();
|
||||
|
||||
export class TagDropdownWrapper extends BaseComponent {
|
||||
export class TagDropdown extends BaseComponent {
|
||||
/**
|
||||
* Container with dropdown elements to insert options into.
|
||||
*/
|
||||
@@ -29,7 +27,7 @@ export class TagDropdownWrapper extends BaseComponent {
|
||||
/**
|
||||
* Local clone of the currently active profile used for updating the list of tags.
|
||||
*/
|
||||
#activeProfile: MaintenanceProfile | null = null;
|
||||
#activeProfile: TaggingProfile | null = null;
|
||||
|
||||
/**
|
||||
* Is cursor currently entered the dropdown.
|
||||
@@ -46,7 +44,7 @@ export class TagDropdownWrapper extends BaseComponent {
|
||||
this.on('mouseenter', this.#onDropdownEntered.bind(this));
|
||||
this.on('mouseleave', this.#onDropdownLeft.bind(this));
|
||||
|
||||
TagDropdownWrapper.#watchActiveProfile(activeProfileOrNull => {
|
||||
TagDropdown.#watchActiveProfile(activeProfileOrNull => {
|
||||
this.#activeProfile = activeProfileOrNull;
|
||||
|
||||
if (this.#isEntered) {
|
||||
@@ -122,7 +120,7 @@ export class TagDropdownWrapper extends BaseComponent {
|
||||
|
||||
#updateButtons() {
|
||||
if (!this.#activeProfile) {
|
||||
this.#addToNewButton ??= TagDropdownWrapper.#createDropdownLink(
|
||||
this.#addToNewButton ??= TagDropdown.#createDropdownLink(
|
||||
'Add to new tagging profile',
|
||||
this.#onAddToNewClicked.bind(this)
|
||||
);
|
||||
@@ -135,7 +133,7 @@ export class TagDropdownWrapper extends BaseComponent {
|
||||
}
|
||||
|
||||
if (this.#activeProfile) {
|
||||
this.#toggleOnExistingButton ??= TagDropdownWrapper.#createDropdownLink(
|
||||
this.#toggleOnExistingButton ??= TagDropdown.#createDropdownLink(
|
||||
'Add to existing tagging profile',
|
||||
this.#onToggleInExistingClicked.bind(this)
|
||||
);
|
||||
@@ -172,14 +170,14 @@ export class TagDropdownWrapper extends BaseComponent {
|
||||
throw new Error('Missing tag name to create the profile!');
|
||||
}
|
||||
|
||||
const profile = new MaintenanceProfile(crypto.randomUUID(), {
|
||||
const profile = new TaggingProfile(crypto.randomUUID(), {
|
||||
name: 'Temporary Profile (' + (new Date().toISOString()) + ')',
|
||||
tags: [this.tagName],
|
||||
temporary: true,
|
||||
});
|
||||
|
||||
await profile.save();
|
||||
await TagDropdownWrapper.#maintenanceSettings.setActiveProfileId(profile.id);
|
||||
await TagDropdown.#preferences.activeProfile.set(profile.id);
|
||||
}
|
||||
|
||||
async #onToggleInExistingClicked() {
|
||||
@@ -205,25 +203,25 @@ export class TagDropdownWrapper extends BaseComponent {
|
||||
await this.#activeProfile.save();
|
||||
}
|
||||
|
||||
static #maintenanceSettings = new MaintenanceSettings();
|
||||
static #preferences = new TaggingProfilesPreferences();
|
||||
|
||||
/**
|
||||
* Watch for changes to active profile.
|
||||
* @param onActiveProfileChange Callback to call when profile was
|
||||
* changed.
|
||||
*/
|
||||
static #watchActiveProfile(onActiveProfileChange: (profile: MaintenanceProfile | null) => void) {
|
||||
static #watchActiveProfile(onActiveProfileChange: (profile: TaggingProfile | null) => void) {
|
||||
let lastActiveProfile: string | null = null;
|
||||
|
||||
this.#maintenanceSettings.subscribe((settings) => {
|
||||
this.#preferences.subscribe((settings) => {
|
||||
lastActiveProfile = settings.activeProfile ?? null;
|
||||
|
||||
this.#maintenanceSettings
|
||||
.resolveActiveProfileAsObject()
|
||||
this.#preferences
|
||||
.activeProfile.asObject()
|
||||
.then(onActiveProfileChange);
|
||||
});
|
||||
|
||||
MaintenanceProfile.subscribe(profiles => {
|
||||
TaggingProfile.subscribe(profiles => {
|
||||
const activeProfile = profiles
|
||||
.find(profile => profile.id === lastActiveProfile);
|
||||
|
||||
@@ -231,8 +229,8 @@ export class TagDropdownWrapper extends BaseComponent {
|
||||
);
|
||||
});
|
||||
|
||||
this.#maintenanceSettings
|
||||
.resolveActiveProfileAsObject()
|
||||
this.#preferences
|
||||
.activeProfile.asObject()
|
||||
.then(activeProfile => {
|
||||
lastActiveProfile = activeProfile?.id ?? null;
|
||||
onActiveProfileChange(activeProfile);
|
||||
@@ -263,58 +261,65 @@ export class TagDropdownWrapper extends BaseComponent {
|
||||
|
||||
return dropdownLink;
|
||||
}
|
||||
}
|
||||
|
||||
export function wrapTagDropdown(element: HTMLElement) {
|
||||
// Skip initialization when tag component is already wrapped
|
||||
if (getComponent(element)) {
|
||||
return;
|
||||
static #categoriesResolver = new CustomCategoriesResolver();
|
||||
static #processedElements: WeakSet<HTMLElement> = new WeakSet();
|
||||
|
||||
static #findAll(parentNode: ParentNode = document): NodeListOf<HTMLElement> {
|
||||
return parentNode.querySelectorAll('.tag.dropdown');
|
||||
}
|
||||
|
||||
const tagDropdown = new TagDropdownWrapper(element);
|
||||
tagDropdown.initialize();
|
||||
static #initialize(element: HTMLElement) {
|
||||
// Skip initialization when tag component is already wrapped
|
||||
if (getComponent(element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
categoriesResolver.addElement(tagDropdown);
|
||||
}
|
||||
const tagDropdown = new TagDropdown(element);
|
||||
tagDropdown.initialize();
|
||||
|
||||
const processedElementsSet = new WeakSet<HTMLElement>();
|
||||
|
||||
export function watchTagDropdownsInTagsEditor() {
|
||||
// We only need to watch for new editor elements if there is a tag editor present on the page
|
||||
if (!document.querySelector('#image_tags_and_source')) {
|
||||
return;
|
||||
this.#categoriesResolver.addElement(tagDropdown);
|
||||
}
|
||||
|
||||
document.body.addEventListener('mouseover', event => {
|
||||
const targetElement = event.target;
|
||||
static findAllAndInitialize(parentNode: ParentNode = document) {
|
||||
for (const element of this.#findAll(parentNode)) {
|
||||
this.#initialize(element);
|
||||
}
|
||||
}
|
||||
|
||||
if (!(targetElement instanceof HTMLElement)) {
|
||||
static watch() {
|
||||
// We only need to watch for new editor elements if there is a tag editor present on the page
|
||||
if (!document.querySelector('#image_tags_and_source')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (processedElementsSet.has(targetElement)) {
|
||||
return;
|
||||
}
|
||||
document.body.addEventListener('mouseover', event => {
|
||||
const targetElement = event.target;
|
||||
|
||||
const closestTagEditor = targetElement.closest<HTMLElement>('#image_tags_and_source');
|
||||
if (!(targetElement instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!closestTagEditor || processedElementsSet.has(closestTagEditor)) {
|
||||
processedElementsSet.add(targetElement);
|
||||
return;
|
||||
}
|
||||
if (this.#processedElements.has(targetElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
processedElementsSet.add(targetElement);
|
||||
processedElementsSet.add(closestTagEditor);
|
||||
const closestTagEditor = targetElement.closest<HTMLElement>('#image_tags_and_source');
|
||||
|
||||
for (const tagDropdownElement of closestTagEditor.querySelectorAll<HTMLElement>('.tag.dropdown')) {
|
||||
wrapTagDropdown(tagDropdownElement);
|
||||
}
|
||||
});
|
||||
if (!closestTagEditor || this.#processedElements.has(closestTagEditor)) {
|
||||
this.#processedElements.add(targetElement);
|
||||
return;
|
||||
}
|
||||
|
||||
// When form is submitted, its DOM is completely updated. We need to fetch those tags in this case.
|
||||
on(document.body, EVENT_FORM_EDITOR_UPDATED, event => {
|
||||
for (const tagDropdownElement of event.detail.querySelectorAll<HTMLElement>('.tag.dropdown')) {
|
||||
wrapTagDropdown(tagDropdownElement);
|
||||
}
|
||||
});
|
||||
this.#processedElements.add(targetElement);
|
||||
this.#processedElements.add(closestTagEditor);
|
||||
|
||||
this.findAllAndInitialize(closestTagEditor);
|
||||
});
|
||||
|
||||
// When form is submitted, its DOM is completely updated. We need to fetch those tags in this case.
|
||||
on(document.body, EVENT_FORM_EDITOR_UPDATED, event => {
|
||||
this.findAllAndInitialize(event.detail);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { BaseComponent } from "$content/components/base/BaseComponent";
|
||||
import type TagGroup from "$entities/TagGroup";
|
||||
import type { TagDropdownWrapper } from "$content/components/TagDropdownWrapper";
|
||||
import type { TagDropdown } from "$content/components/philomena/TagDropdown";
|
||||
import { on } from "$content/components/events/comms";
|
||||
import { EVENT_FORM_EDITOR_UPDATED } from "$content/components/events/tags-form-events";
|
||||
import { getComponent } from "$content/components/base/component-utils";
|
||||
import { EVENT_TAG_GROUP_RESOLVED } from "$content/components/events/tag-dropdown-events";
|
||||
import TagSettings from "$lib/extension/settings/TagSettings";
|
||||
import TagsPreferences from "$lib/extension/preferences/TagsPreferences";
|
||||
|
||||
export class TagsListBlock extends BaseComponent {
|
||||
#tagsListButtonsContainer: HTMLElement | null = null;
|
||||
@@ -14,14 +14,14 @@ export class TagsListBlock extends BaseComponent {
|
||||
#toggleGroupingButton = document.createElement('a');
|
||||
#toggleGroupingButtonIcon = document.createElement('i');
|
||||
|
||||
#tagSettings = new TagSettings();
|
||||
#preferences = new TagsPreferences();
|
||||
|
||||
#shouldDisplaySeparation = false;
|
||||
|
||||
#separatedGroups = new Map<string, TagGroup>();
|
||||
#separatedHeaders = new Map<string, HTMLElement>();
|
||||
#groupsCount = new Map<string, number>();
|
||||
#lastTagGroup = new WeakMap<TagDropdownWrapper, TagGroup | null>;
|
||||
#lastTagGroup = new WeakMap<TagDropdown, TagGroup | null>;
|
||||
|
||||
#isReorderingPlanned = false;
|
||||
|
||||
@@ -44,8 +44,8 @@ export class TagsListBlock extends BaseComponent {
|
||||
}
|
||||
|
||||
init() {
|
||||
this.#tagSettings.resolveGroupSeparation().then(this.#onTagSeparationChange.bind(this));
|
||||
this.#tagSettings.subscribe(settings => {
|
||||
this.#preferences.groupSeparation.get().then(this.#onTagSeparationChange.bind(this));
|
||||
this.#preferences.subscribe(settings => {
|
||||
this.#onTagSeparationChange(Boolean(settings.groupSeparation))
|
||||
});
|
||||
|
||||
@@ -80,7 +80,7 @@ export class TagsListBlock extends BaseComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
const tagDropdown = getComponent<TagDropdownWrapper>(maybeDropdownElement);
|
||||
const tagDropdown = getComponent<TagDropdown>(maybeDropdownElement);
|
||||
|
||||
if (!tagDropdown) {
|
||||
return;
|
||||
@@ -103,7 +103,7 @@ export class TagsListBlock extends BaseComponent {
|
||||
|
||||
#onToggleGroupingClicked(event: Event) {
|
||||
event.preventDefault();
|
||||
void this.#tagSettings.setGroupSeparation(!this.#shouldDisplaySeparation);
|
||||
void this.#preferences.groupSeparation.set(!this.#shouldDisplaySeparation);
|
||||
}
|
||||
|
||||
#handleTagGroupChanges(tagGroup: TagGroup) {
|
||||
@@ -146,7 +146,7 @@ export class TagsListBlock extends BaseComponent {
|
||||
heading.innerText = group.settings.name;
|
||||
}
|
||||
|
||||
#handleResolvedTagGroup(resolvedGroup: TagGroup | null, tagComponent: TagDropdownWrapper) {
|
||||
#handleResolvedTagGroup(resolvedGroup: TagGroup | null, tagComponent: TagDropdown) {
|
||||
const previousGroupId = this.#lastTagGroup.get(tagComponent)?.id;
|
||||
const currentGroupId = resolvedGroup?.id;
|
||||
const isDifferentId = currentGroupId !== previousGroupId;
|
||||
@@ -217,28 +217,28 @@ export class TagsListBlock extends BaseComponent {
|
||||
|
||||
static #iconGroupingDisabled = 'fa-folder';
|
||||
static #iconGroupingEnabled = 'fa-folder-tree';
|
||||
}
|
||||
|
||||
export function initializeAllTagsLists() {
|
||||
for (let element of document.querySelectorAll<HTMLElement>('#image_tags_and_source')) {
|
||||
if (getComponent(element)) {
|
||||
return;
|
||||
static initializeAll() {
|
||||
for (let element of document.querySelectorAll<HTMLElement>('#image_tags_and_source')) {
|
||||
if (getComponent(element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
new TagsListBlock(element)
|
||||
.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
new TagsListBlock(element)
|
||||
.initialize();
|
||||
static watchUpdatedLists() {
|
||||
on(document, EVENT_FORM_EDITOR_UPDATED, event => {
|
||||
const tagsListElement = event.detail.closest<HTMLElement>('#image_tags_and_source');
|
||||
|
||||
if (!tagsListElement || getComponent(tagsListElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
new TagsListBlock(tagsListElement)
|
||||
.initialize();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function watchForUpdatedTagLists() {
|
||||
on(document, EVENT_FORM_EDITOR_UPDATED, event => {
|
||||
const tagsListElement = event.detail.closest<HTMLElement>('#image_tags_and_source');
|
||||
|
||||
if (!tagsListElement || getComponent(tagsListElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
new TagsListBlock(tagsListElement)
|
||||
.initialize();
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BaseComponent } from "$content/components/base/BaseComponent";
|
||||
import { ImageListInfo } from "$content/components/listing/ImageListInfo";
|
||||
import { ImageListInfo } from "$content/components/philomena/listing/ImageListInfo";
|
||||
|
||||
export class ImageListContainer extends BaseComponent {
|
||||
#info: ImageListInfo | null = null;
|
||||
@@ -12,8 +12,12 @@ export class ImageListContainer extends BaseComponent {
|
||||
this.#info.initialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function initializeImageListContainer(element: HTMLElement) {
|
||||
new ImageListContainer(element).initialize();
|
||||
static findAndInitialize() {
|
||||
const imageListContainer = document.querySelector<HTMLElement>('#imagelist-container');
|
||||
|
||||
if (imageListContainer) {
|
||||
new ImageListContainer(imageListContainer).initialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
import { createMaintenancePopup } from "$content/components/MaintenancePopup";
|
||||
import { createMediaBoxTools } from "$content/components/MediaBoxTools";
|
||||
import { calculateMediaBoxesPositions, initializeMediaBox } from "$content/components/MediaBoxWrapper";
|
||||
import { createMaintenanceStatusIcon } from "$content/components/MaintenanceStatusIcon";
|
||||
import { createImageShowFullscreenButton } from "$content/components/ImageShowFullscreenButton";
|
||||
import { initializeImageListContainer } from "$content/components/listing/ImageListContainer";
|
||||
import { TaggingProfilePopup } from "$content/components/extension/profiles/TaggingProfilePopup";
|
||||
import { MediaBoxTools } from "$content/components/extension/MediaBoxTools";
|
||||
import { MediaBox } from "$content/components/philomena/MediaBox";
|
||||
import { TaggingProfileStatusIcon } from "$content/components/extension/profiles/TaggingProfileStatusIcon";
|
||||
import { ImageShowFullscreenButton } from "$content/components/extension/ImageShowFullscreenButton";
|
||||
import { ImageListContainer } from "$content/components/philomena/listing/ImageListContainer";
|
||||
|
||||
const mediaBoxes = document.querySelectorAll<HTMLElement>('.media-box');
|
||||
const imageListContainer = document.querySelector<HTMLElement>('#imagelist-container');
|
||||
const mediaBoxes = MediaBox.findElements();
|
||||
|
||||
mediaBoxes.forEach(mediaBoxElement => {
|
||||
initializeMediaBox(mediaBoxElement, [
|
||||
createMediaBoxTools(
|
||||
createMaintenancePopup(),
|
||||
createMaintenanceStatusIcon(),
|
||||
createImageShowFullscreenButton(),
|
||||
MediaBox.initialize(mediaBoxElement, [
|
||||
MediaBoxTools.create(
|
||||
TaggingProfilePopup.create(),
|
||||
TaggingProfileStatusIcon.create(),
|
||||
ImageShowFullscreenButton.create(),
|
||||
)
|
||||
]);
|
||||
|
||||
@@ -23,8 +22,5 @@ mediaBoxes.forEach(mediaBoxElement => {
|
||||
})
|
||||
});
|
||||
|
||||
calculateMediaBoxesPositions(mediaBoxes);
|
||||
|
||||
if (imageListContainer) {
|
||||
initializeImageListContainer(imageListContainer);
|
||||
}
|
||||
MediaBox.initializePositionCalculation(mediaBoxes);
|
||||
ImageListContainer.findAndInitialize();
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { BlockCommunication } from "$content/components/BlockCommunication";
|
||||
import { BlockCommunication } from "$content/components/philomena/BlockCommunication";
|
||||
|
||||
BlockCommunication.findAndInitializeAll();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TagsForm } from "$content/components/TagsForm";
|
||||
import { initializeAllTagsLists, watchForUpdatedTagLists } from "$content/components/TagsListBlock";
|
||||
import { TagsForm } from "$content/components/philomena/TagsForm";
|
||||
import { TagsListBlock } from "$content/components/philomena/TagsListBlock";
|
||||
|
||||
initializeAllTagsLists();
|
||||
watchForUpdatedTagLists();
|
||||
TagsListBlock.initializeAll();
|
||||
TagsListBlock.watchUpdatedLists();
|
||||
TagsForm.watchForEditors();
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { watchTagDropdownsInTagsEditor, wrapTagDropdown } from "$content/components/TagDropdownWrapper";
|
||||
import { TagDropdown } from "$content/components/philomena/TagDropdown";
|
||||
|
||||
for (let tagDropdownElement of document.querySelectorAll<HTMLElement>('.tag.dropdown')) {
|
||||
wrapTagDropdown(tagDropdownElement);
|
||||
}
|
||||
|
||||
watchTagDropdownsInTagsEditor();
|
||||
TagDropdown.findAllAndInitialize();
|
||||
TagDropdown.watch();
|
||||
|
||||
@@ -2,7 +2,7 @@ import type StorageEntity from "$lib/extension/base/StorageEntity";
|
||||
import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from "lz-string";
|
||||
import type { ImportableElementsList, ImportableEntityObject } from "$lib/extension/transporting/importables";
|
||||
import EntitiesTransporter, { type SameSiteStatus } from "$lib/extension/EntitiesTransporter";
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import TagGroup from "$entities/TagGroup";
|
||||
|
||||
type TransportersMapping = {
|
||||
@@ -73,7 +73,7 @@ export default class BulkEntitiesTransporter {
|
||||
elements: entities
|
||||
.map(entity => {
|
||||
switch (true) {
|
||||
case entity instanceof MaintenanceProfile:
|
||||
case entity instanceof TaggingProfile:
|
||||
return BulkEntitiesTransporter.#transporters.profiles.exportToObject(entity);
|
||||
case entity instanceof TagGroup:
|
||||
return BulkEntitiesTransporter.#transporters.groups.exportToObject(entity);
|
||||
@@ -99,7 +99,7 @@ export default class BulkEntitiesTransporter {
|
||||
}
|
||||
|
||||
static #transporters: TransportersMapping = {
|
||||
profiles: new EntitiesTransporter(MaintenanceProfile),
|
||||
profiles: new EntitiesTransporter(TaggingProfile),
|
||||
groups: new EntitiesTransporter(TagGroup),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TagDropdownWrapper } from "$content/components/TagDropdownWrapper";
|
||||
import type { TagDropdown } from "$content/components/philomena/TagDropdown";
|
||||
import TagGroup from "$entities/TagGroup";
|
||||
import { escapeRegExp } from "$lib/utils";
|
||||
import { emit } from "$content/components/events/comms";
|
||||
@@ -7,7 +7,7 @@ import { EVENT_TAG_GROUP_RESOLVED } from "$content/components/events/tag-dropdow
|
||||
export default class CustomCategoriesResolver {
|
||||
#exactGroupMatches = new Map<string, TagGroup>();
|
||||
#regExpGroupMatches = new Map<RegExp, TagGroup>();
|
||||
#tagDropdowns: TagDropdownWrapper[] = [];
|
||||
#tagDropdowns: TagDropdown[] = [];
|
||||
#nextQueuedUpdate: Timeout | null = null;
|
||||
|
||||
constructor() {
|
||||
@@ -15,7 +15,7 @@ export default class CustomCategoriesResolver {
|
||||
TagGroup.readAll().then(this.#onTagGroupsReceived.bind(this));
|
||||
}
|
||||
|
||||
public addElement(tagDropdown: TagDropdownWrapper): void {
|
||||
public addElement(tagDropdown: TagDropdown): void {
|
||||
this.#tagDropdowns.push(tagDropdown);
|
||||
|
||||
if (!this.#exactGroupMatches.size && !this.#regExpGroupMatches.size) {
|
||||
@@ -49,7 +49,7 @@ export default class CustomCategoriesResolver {
|
||||
* @return {boolean} Will return false when tag is processed and true when it is not found.
|
||||
* @private
|
||||
*/
|
||||
#applyCustomCategoryForExactMatches(tagDropdown: TagDropdownWrapper): boolean {
|
||||
#applyCustomCategoryForExactMatches(tagDropdown: TagDropdown): boolean {
|
||||
const tagName = tagDropdown.tagName!;
|
||||
|
||||
if (!this.#exactGroupMatches.has(tagName)) {
|
||||
@@ -65,7 +65,7 @@ export default class CustomCategoriesResolver {
|
||||
return false;
|
||||
}
|
||||
|
||||
#matchCustomCategoryByRegExp(tagDropdown: TagDropdownWrapper) {
|
||||
#matchCustomCategoryByRegExp(tagDropdown: TagDropdown) {
|
||||
const tagName = tagDropdown.tagName!;
|
||||
|
||||
for (const targetRegularExpression of this.#regExpGroupMatches.keys()) {
|
||||
@@ -117,7 +117,7 @@ export default class CustomCategoriesResolver {
|
||||
this.#queueUpdatingTags();
|
||||
}
|
||||
|
||||
static #resetToOriginalCategory(tagDropdown: TagDropdownWrapper): void {
|
||||
static #resetToOriginalCategory(tagDropdown: TagDropdown): void {
|
||||
emit(
|
||||
tagDropdown,
|
||||
EVENT_TAG_GROUP_RESOLVED,
|
||||
|
||||
179
src/lib/extension/base/CacheablePreferences.ts
Normal file
179
src/lib/extension/base/CacheablePreferences.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import ConfigurationController from "$lib/extension/ConfigurationController";
|
||||
|
||||
/**
|
||||
* Initialization options for the preference field helper class.
|
||||
*/
|
||||
type PreferenceFieldOptions<FieldKey, ValueType> = {
|
||||
/**
|
||||
* Field name which will be read or updated.
|
||||
*/
|
||||
field: FieldKey;
|
||||
/**
|
||||
* Default value for this field.
|
||||
*/
|
||||
defaultValue: ValueType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for a field. Contains all information needed to read or set the values into the preferences while
|
||||
* retaining proper types for the values.
|
||||
*/
|
||||
export class PreferenceField<
|
||||
/**
|
||||
* Mapping of keys to fields. Usually this is the same type used for defining the structure of the storage itself.
|
||||
* Is automatically captured when preferences class instance is passed into the constructor.
|
||||
*/
|
||||
Fields extends Record<string, any> = Record<string, any>,
|
||||
/**
|
||||
* Field key for resolving which value will be resolved from getter or which value type should be passed into the
|
||||
* setter method.
|
||||
*/
|
||||
Key extends keyof Fields = keyof Fields
|
||||
> {
|
||||
/**
|
||||
* Instance of the preferences class to read/update values on.
|
||||
* @private
|
||||
*/
|
||||
readonly #preferences: CacheablePreferences<Fields>;
|
||||
/**
|
||||
* Key of a field we want to read or write with the helper class.
|
||||
* @private
|
||||
*/
|
||||
readonly #fieldKey: Key;
|
||||
/**
|
||||
* Stored default value for a field.
|
||||
* @private
|
||||
*/
|
||||
readonly #defaultValue: Fields[Key];
|
||||
|
||||
/**
|
||||
* @param preferencesInstance Instance of preferences to work with.
|
||||
* @param options Initialization options for this field.
|
||||
*/
|
||||
constructor(preferencesInstance: CacheablePreferences<Fields>, options: PreferenceFieldOptions<Key, Fields[Key]>) {
|
||||
this.#preferences = preferencesInstance;
|
||||
this.#fieldKey = options.field;
|
||||
this.#defaultValue = options.defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the field value from the preferences.
|
||||
*/
|
||||
get() {
|
||||
return this.#preferences.readRaw(this.#fieldKey, this.#defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the preference field with provided value.
|
||||
* @param value Value to update the field with.
|
||||
*/
|
||||
set(value: Fields[Key]) {
|
||||
return this.#preferences.writeRaw(this.#fieldKey, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper type for preference classes to enforce having field objects inside the preferences instance. It should be
|
||||
* applied on child classes of {@link CacheablePreferences}.
|
||||
*/
|
||||
export type WithFields<FieldsType extends Record<string, any>> = {
|
||||
readonly [FieldKey in keyof FieldsType]: PreferenceField<FieldsType, FieldKey>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for any preferences instances. It contains methods for reading or updating any arbitrary values inside
|
||||
* extension storage. It also tries to save the value resolved from the storage into special internal cache after the
|
||||
* first call.
|
||||
*
|
||||
* Should be usually paired with implementation of {@link WithFields} helper type as interface for much more usable
|
||||
* API.
|
||||
*/
|
||||
export default abstract class CacheablePreferences<Fields> {
|
||||
#controller: ConfigurationController;
|
||||
#cachedValues: Map<keyof Fields, any> = new Map();
|
||||
#disposables: Function[] = [];
|
||||
|
||||
/**
|
||||
* @param settingsNamespace Name of the field inside the extension storage where these preferences stored.
|
||||
* @protected
|
||||
*/
|
||||
protected constructor(settingsNamespace: string) {
|
||||
this.#controller = new ConfigurationController(settingsNamespace);
|
||||
|
||||
this.#disposables.push(
|
||||
this.#controller.subscribeToChanges(settings => {
|
||||
for (const key of Object.keys(settings)) {
|
||||
this.#cachedValues.set(
|
||||
key as keyof Fields,
|
||||
settings[key]
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the value from the preferences by the field. This function doesn't handle default values, so you generally
|
||||
* should avoid using this method and accessing the special fields instead.
|
||||
* @param settingName Name of the field to read.
|
||||
* @param defaultValue Default value to return if value is not set.
|
||||
* @return Value of the field or default value if it is not set.
|
||||
*/
|
||||
public async readRaw<Key extends keyof Fields>(settingName: Key, defaultValue: Fields[Key]): Promise<Fields[Key]> {
|
||||
if (this.#cachedValues.has(settingName)) {
|
||||
return this.#cachedValues.get(settingName);
|
||||
}
|
||||
|
||||
const settingValue = await this.#controller.readSetting(settingName as string, defaultValue);
|
||||
|
||||
this.#cachedValues.set(settingName, settingValue);
|
||||
|
||||
return settingValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the value into specific field of the storage. You should generally avoid calling this function directly and
|
||||
* instead rely on special field helpers inside your preferences class.
|
||||
* @param settingName Name of the setting to write.
|
||||
* @param value Value to pass.
|
||||
* @param force Ignore the cache and force the update.
|
||||
* @protected
|
||||
*/
|
||||
async writeRaw<Key extends keyof Fields>(settingName: Key, value: Fields[Key], force: boolean = false): Promise<void> {
|
||||
if (
|
||||
!force
|
||||
&& this.#cachedValues.has(settingName)
|
||||
&& this.#cachedValues.get(settingName) === value
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.#controller.writeSetting(
|
||||
settingName as string,
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to the changes made to the storage.
|
||||
* @param callback Callback which will receive list of settings on every update. This function will not be called
|
||||
* on initialization.
|
||||
* @return Unsubscribe function to call in order to disable the watching.
|
||||
*/
|
||||
subscribe(callback: (settings: Partial<Fields>) => void): () => void {
|
||||
const unsubscribeCallback = this.#controller.subscribeToChanges(callback as (fields: Record<string, any>) => void);
|
||||
|
||||
this.#disposables.push(unsubscribeCallback);
|
||||
|
||||
return unsubscribeCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Completely disable all subscriptions.
|
||||
*/
|
||||
dispose() {
|
||||
for (let disposeCallback of this.#disposables) {
|
||||
disposeCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import ConfigurationController from "$lib/extension/ConfigurationController";
|
||||
|
||||
export default class CacheableSettings<Fields> {
|
||||
#controller: ConfigurationController;
|
||||
#cachedValues: Map<keyof Fields, any> = new Map();
|
||||
#disposables: Function[] = [];
|
||||
|
||||
constructor(settingsNamespace: string) {
|
||||
this.#controller = new ConfigurationController(settingsNamespace);
|
||||
|
||||
this.#disposables.push(
|
||||
this.#controller.subscribeToChanges(settings => {
|
||||
for (const key of Object.keys(settings)) {
|
||||
this.#cachedValues.set(
|
||||
key as keyof Fields,
|
||||
settings[key]
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template SettingType
|
||||
* @param {string} settingName
|
||||
* @param {SettingType} defaultValue
|
||||
* @return {Promise<SettingType>}
|
||||
* @protected
|
||||
*/
|
||||
protected async _resolveSetting<Key extends keyof Fields>(settingName: Key, defaultValue: Fields[Key]): Promise<Fields[Key]> {
|
||||
if (this.#cachedValues.has(settingName)) {
|
||||
return this.#cachedValues.get(settingName);
|
||||
}
|
||||
|
||||
const settingValue = await this.#controller.readSetting(settingName as string, defaultValue);
|
||||
|
||||
this.#cachedValues.set(settingName, settingValue);
|
||||
|
||||
return settingValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param settingName Name of the setting to write.
|
||||
* @param value Value to pass.
|
||||
* @param force Ignore the cache and force the update.
|
||||
* @protected
|
||||
*/
|
||||
async _writeSetting<Key extends keyof Fields>(settingName: Key, value: Fields[Key], force: boolean = false): Promise<void> {
|
||||
if (
|
||||
!force
|
||||
&& this.#cachedValues.has(settingName)
|
||||
&& this.#cachedValues.get(settingName) === value
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.#controller.writeSetting(
|
||||
settingName as string,
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to the changes made to the storage.
|
||||
* @param {function(Object): void} callback Callback which will receive list of settings.
|
||||
* @return {function(): void} Unsubscribe function.
|
||||
*/
|
||||
subscribe(callback: (settings: Partial<Fields>) => void): () => void {
|
||||
const unsubscribeCallback = this.#controller.subscribeToChanges(callback as (fields: Record<string, any>) => void);
|
||||
|
||||
this.#disposables.push(unsubscribeCallback);
|
||||
|
||||
return unsubscribeCallback;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
for (let disposeCallback of this.#disposables) {
|
||||
disposeCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
import StorageEntity from "$lib/extension/base/StorageEntity";
|
||||
|
||||
export interface MaintenanceProfileSettings {
|
||||
export interface TaggingProfileSettings {
|
||||
name: string;
|
||||
tags: string[];
|
||||
temporary: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class representing the maintenance profile entity.
|
||||
* Class representing the tagging profile entity.
|
||||
*/
|
||||
export default class MaintenanceProfile extends StorageEntity<MaintenanceProfileSettings> {
|
||||
export default class TaggingProfile extends StorageEntity<TaggingProfileSettings> {
|
||||
/**
|
||||
* @param id ID of the entity.
|
||||
* @param settings Maintenance profile settings object.
|
||||
*/
|
||||
constructor(id: string, settings: Partial<MaintenanceProfileSettings>) {
|
||||
constructor(id: string, settings: Partial<TaggingProfileSettings>) {
|
||||
super(id, {
|
||||
name: settings.name || '',
|
||||
tags: settings.tags || [],
|
||||
27
src/lib/extension/preferences/MiscPreferences.ts
Normal file
27
src/lib/extension/preferences/MiscPreferences.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import CacheablePreferences, {
|
||||
PreferenceField,
|
||||
type WithFields
|
||||
} from "$lib/extension/base/CacheablePreferences";
|
||||
|
||||
export type FullscreenViewerSize = keyof App.ImageURIs;
|
||||
|
||||
interface MiscPreferencesFields {
|
||||
fullscreenViewer: boolean;
|
||||
fullscreenViewerSize: FullscreenViewerSize;
|
||||
}
|
||||
|
||||
export default class MiscPreferences extends CacheablePreferences<MiscPreferencesFields> implements WithFields<MiscPreferencesFields> {
|
||||
constructor() {
|
||||
super("misc");
|
||||
}
|
||||
|
||||
readonly fullscreenViewer = new PreferenceField(this, {
|
||||
field: "fullscreenViewer",
|
||||
defaultValue: true,
|
||||
});
|
||||
|
||||
readonly fullscreenViewerSize = new PreferenceField(this, {
|
||||
field: "fullscreenViewerSize",
|
||||
defaultValue: "large",
|
||||
});
|
||||
}
|
||||
40
src/lib/extension/preferences/TaggingProfilesPreferences.ts
Normal file
40
src/lib/extension/preferences/TaggingProfilesPreferences.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import CacheablePreferences, { PreferenceField, type WithFields } from "$lib/extension/base/CacheablePreferences";
|
||||
|
||||
interface TaggingProfilePreferencesFields {
|
||||
activeProfile: string | null;
|
||||
stripBlacklistedTags: boolean;
|
||||
}
|
||||
|
||||
class ActiveProfilePreference extends PreferenceField<TaggingProfilePreferencesFields, "activeProfile"> {
|
||||
constructor(preferencesInstance: CacheablePreferences<TaggingProfilePreferencesFields>) {
|
||||
super(preferencesInstance, {
|
||||
field: "activeProfile",
|
||||
defaultValue: null,
|
||||
});
|
||||
}
|
||||
|
||||
async asObject(): Promise<TaggingProfile | null> {
|
||||
const activeProfileId = await this.get();
|
||||
|
||||
if (!activeProfileId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (await TaggingProfile.readAll())
|
||||
.find(profile => profile.id === activeProfileId) || null;
|
||||
}
|
||||
}
|
||||
|
||||
export default class TaggingProfilesPreferences extends CacheablePreferences<TaggingProfilePreferencesFields> implements WithFields<TaggingProfilePreferencesFields> {
|
||||
constructor() {
|
||||
super("maintenance");
|
||||
}
|
||||
|
||||
readonly activeProfile = new ActiveProfilePreference(this);
|
||||
|
||||
readonly stripBlacklistedTags = new PreferenceField(this, {
|
||||
field: "stripBlacklistedTags",
|
||||
defaultValue: false,
|
||||
});
|
||||
}
|
||||
28
src/lib/extension/preferences/TagsPreferences.ts
Normal file
28
src/lib/extension/preferences/TagsPreferences.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import CacheablePreferences, { PreferenceField, type WithFields } from "$lib/extension/base/CacheablePreferences";
|
||||
|
||||
interface TagsPreferencesFields {
|
||||
groupSeparation: boolean;
|
||||
replaceLinks: boolean;
|
||||
replaceLinkText: boolean;
|
||||
}
|
||||
|
||||
export default class TagsPreferences extends CacheablePreferences<TagsPreferencesFields> implements WithFields<TagsPreferencesFields> {
|
||||
constructor() {
|
||||
super("tag");
|
||||
}
|
||||
|
||||
readonly groupSeparation = new PreferenceField(this, {
|
||||
field: "groupSeparation",
|
||||
defaultValue: true,
|
||||
});
|
||||
|
||||
readonly replaceLinks = new PreferenceField(this, {
|
||||
field: "replaceLinks",
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
readonly replaceLinkText = new PreferenceField(this, {
|
||||
field: "replaceLinkText",
|
||||
defaultValue: true,
|
||||
});
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import CacheableSettings from "$lib/extension/base/CacheableSettings";
|
||||
|
||||
interface MaintenanceSettingsFields {
|
||||
activeProfile: string | null;
|
||||
stripBlacklistedTags: boolean;
|
||||
}
|
||||
|
||||
export default class MaintenanceSettings extends CacheableSettings<MaintenanceSettingsFields> {
|
||||
constructor() {
|
||||
super("maintenance");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active maintenance profile.
|
||||
*/
|
||||
async resolveActiveProfileId() {
|
||||
return this._resolveSetting("activeProfile", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active maintenance profile if it is set.
|
||||
*/
|
||||
async resolveActiveProfileAsObject(): Promise<MaintenanceProfile | null> {
|
||||
const resolvedProfileId = await this.resolveActiveProfileId();
|
||||
|
||||
return (await MaintenanceProfile.readAll())
|
||||
.find(profile => profile.id === resolvedProfileId) || null;
|
||||
}
|
||||
|
||||
async resolveStripBlacklistedTags() {
|
||||
return this._resolveSetting('stripBlacklistedTags', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active maintenance profile.
|
||||
*
|
||||
* @param profileId ID of the profile to set as active. If `null`, the active profile will be considered
|
||||
* unset.
|
||||
*/
|
||||
async setActiveProfileId(profileId: string | null): Promise<void> {
|
||||
await this._writeSetting("activeProfile", profileId);
|
||||
}
|
||||
|
||||
async setStripBlacklistedTags(isEnabled: boolean) {
|
||||
await this._writeSetting('stripBlacklistedTags', isEnabled);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import CacheableSettings from "$lib/extension/base/CacheableSettings";
|
||||
|
||||
export type FullscreenViewerSize = keyof App.ImageURIs;
|
||||
|
||||
interface MiscSettingsFields {
|
||||
fullscreenViewer: boolean;
|
||||
fullscreenViewerSize: FullscreenViewerSize;
|
||||
}
|
||||
|
||||
export default class MiscSettings extends CacheableSettings<MiscSettingsFields> {
|
||||
constructor() {
|
||||
super("misc");
|
||||
}
|
||||
|
||||
async resolveFullscreenViewerEnabled() {
|
||||
return this._resolveSetting("fullscreenViewer", true);
|
||||
}
|
||||
|
||||
async resolveFullscreenViewerPreviewSize() {
|
||||
return this._resolveSetting('fullscreenViewerSize', 'large');
|
||||
}
|
||||
|
||||
async setFullscreenViewerEnabled(isEnabled: boolean) {
|
||||
return this._writeSetting("fullscreenViewer", isEnabled);
|
||||
}
|
||||
|
||||
async setFullscreenViewerPreviewSize(size: FullscreenViewerSize | string) {
|
||||
return this._writeSetting('fullscreenViewerSize', size as FullscreenViewerSize);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import CacheableSettings from "$lib/extension/base/CacheableSettings";
|
||||
|
||||
interface TagSettingsFields {
|
||||
groupSeparation: boolean;
|
||||
replaceLinks: boolean;
|
||||
replaceLinkText: boolean;
|
||||
}
|
||||
|
||||
export default class TagSettings extends CacheableSettings<TagSettingsFields> {
|
||||
constructor() {
|
||||
super("tag");
|
||||
}
|
||||
|
||||
async resolveGroupSeparation() {
|
||||
return this._resolveSetting("groupSeparation", true);
|
||||
}
|
||||
|
||||
async resolveReplaceLinks() {
|
||||
return this._resolveSetting("replaceLinks", false);
|
||||
}
|
||||
|
||||
async resolveReplaceLinkText() {
|
||||
return this._resolveSetting("replaceLinkText", true);
|
||||
}
|
||||
|
||||
async setGroupSeparation(value: boolean) {
|
||||
return this._writeSetting("groupSeparation", Boolean(value));
|
||||
}
|
||||
|
||||
async setReplaceLinks(value: boolean) {
|
||||
return this._writeSetting("replaceLinks", Boolean(value));
|
||||
}
|
||||
|
||||
async setReplaceLinkText(value: boolean) {
|
||||
return this._writeSetting("replaceLinkText", Boolean(value));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import PostParser from "$lib/booru/scraped/parsing/PostParser";
|
||||
import PostParser from "$lib/philomena/scraping/parsing/PostParser";
|
||||
|
||||
type UpdaterFunction = (tags: Set<string>) => Set<string>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import PageParser from "$lib/booru/scraped/parsing/PageParser";
|
||||
import { buildTagsAndAliasesMap } from "$lib/booru/tag-utils";
|
||||
import PageParser from "$lib/philomena/scraping/parsing/PageParser";
|
||||
import { buildTagsAndAliasesMap } from "$lib/philomena/tag-utils";
|
||||
|
||||
export default class PostParser extends PageParser {
|
||||
#tagEditorForm: HTMLFormElement | null = null;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { namespaceCategories } from "$config/tags";
|
||||
import { QueryLexer, QuotedTermToken, TermToken } from "$lib/booru/search/QueryLexer";
|
||||
import { QueryLexer, QuotedTermToken, TermToken } from "$lib/philomena/search/QueryLexer";
|
||||
|
||||
/**
|
||||
* Build the map containing both real tags and their aliases.
|
||||
@@ -1,30 +1,30 @@
|
||||
<script lang="ts">
|
||||
import Menu from "$components/ui/menu/Menu.svelte";
|
||||
import MenuItem from "$components/ui/menu/MenuItem.svelte";
|
||||
import { activeProfileStore, maintenanceProfiles } from "$stores/entities/maintenance-profiles";
|
||||
import { activeTaggingProfile, taggingProfiles } from "$stores/entities/tagging-profiles";
|
||||
import MenuCheckboxItem from "$components/ui/menu/MenuCheckboxItem.svelte";
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import { popupTitle } from "$stores/popup";
|
||||
|
||||
$popupTitle = null;
|
||||
|
||||
let activeProfile = $derived<MaintenanceProfile | null>(
|
||||
$maintenanceProfiles.find(profile => profile.id === $activeProfileStore) || null
|
||||
let activeProfile = $derived<TaggingProfile | null>(
|
||||
$taggingProfiles.find(profile => profile.id === $activeTaggingProfile) || null
|
||||
);
|
||||
|
||||
function turnOffActiveProfile() {
|
||||
$activeProfileStore = null;
|
||||
$activeTaggingProfile = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
{#if activeProfile}
|
||||
<MenuCheckboxItem checked oninput={turnOffActiveProfile} href="/features/maintenance/{activeProfile.id}">
|
||||
<MenuCheckboxItem checked oninput={turnOffActiveProfile} href="/features/profiles/{activeProfile.id}">
|
||||
Active Profile: {activeProfile.settings.name}
|
||||
</MenuCheckboxItem>
|
||||
<hr>
|
||||
{/if}
|
||||
<MenuItem href="/features/maintenance">Tagging Profiles</MenuItem>
|
||||
<MenuItem href="/features/profiles">Tagging Profiles</MenuItem>
|
||||
<MenuItem href="/features/groups">Tag Groups</MenuItem>
|
||||
<hr>
|
||||
<MenuItem href="/transporting">Import/Export</MenuItem>
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
<Menu>
|
||||
<MenuItem href="/">Back</MenuItem>
|
||||
<hr>
|
||||
<MenuItem href="/features/maintenance">Tagging Profiles</MenuItem>
|
||||
<MenuItem href="/features/profiles">Tagging Profiles</MenuItem>
|
||||
</Menu>
|
||||
|
||||
@@ -2,45 +2,45 @@
|
||||
import Menu from "$components/ui/menu/Menu.svelte";
|
||||
import MenuItem from "$components/ui/menu/MenuItem.svelte";
|
||||
import MenuRadioItem from "$components/ui/menu/MenuRadioItem.svelte";
|
||||
import { activeProfileStore, maintenanceProfiles } from "$stores/entities/maintenance-profiles";
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import { activeTaggingProfile, taggingProfiles } from "$stores/entities/tagging-profiles";
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import { popupTitle } from "$stores/popup";
|
||||
|
||||
$popupTitle = 'Tagging Profiles';
|
||||
|
||||
let profiles = $derived<MaintenanceProfile[]>(
|
||||
$maintenanceProfiles.sort((a, b) => a.settings.name.localeCompare(b.settings.name))
|
||||
let profiles = $derived<TaggingProfile[]>(
|
||||
$taggingProfiles.sort((a, b) => a.settings.name.localeCompare(b.settings.name))
|
||||
);
|
||||
|
||||
function resetActiveProfile() {
|
||||
$activeProfileStore = null;
|
||||
$activeTaggingProfile = null;
|
||||
}
|
||||
|
||||
function enableSelectedProfile(event: Event) {
|
||||
const target = event.target;
|
||||
|
||||
if (target instanceof HTMLInputElement && target.checked) {
|
||||
activeProfileStore.set(target.value);
|
||||
activeTaggingProfile.set(target.value);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuItem href="/" icon="arrow-left">Back</MenuItem>
|
||||
<MenuItem href="/features/maintenance/new/edit" icon="plus">Create New</MenuItem>
|
||||
<MenuItem href="/features/profiles/new/edit" icon="plus">Create New</MenuItem>
|
||||
{#if profiles.length}
|
||||
<hr>
|
||||
{/if}
|
||||
{#each profiles as profile}
|
||||
<MenuRadioItem href="/features/maintenance/{profile.id}"
|
||||
<MenuRadioItem href="/features/profiles/{profile.id}"
|
||||
name="active-profile"
|
||||
value={profile.id}
|
||||
checked={$activeProfileStore === profile.id}
|
||||
checked={$activeTaggingProfile === profile.id}
|
||||
oninput={enableSelectedProfile}>
|
||||
{profile.settings.name}
|
||||
</MenuRadioItem>
|
||||
{/each}
|
||||
<hr>
|
||||
<MenuItem href="#" onclick={resetActiveProfile}>Reset Active Profile</MenuItem>
|
||||
<MenuItem href="/features/maintenance/import">Import Profile</MenuItem>
|
||||
<MenuItem href="/features/profiles/import">Import Profile</MenuItem>
|
||||
</Menu>
|
||||
@@ -3,26 +3,26 @@
|
||||
import MenuItem from "$components/ui/menu/MenuItem.svelte";
|
||||
import { page } from "$app/state";
|
||||
import { goto } from "$app/navigation";
|
||||
import { activeProfileStore, maintenanceProfiles } from "$stores/entities/maintenance-profiles";
|
||||
import { activeTaggingProfile, taggingProfiles } from "$stores/entities/tagging-profiles";
|
||||
import ProfileView from "$components/features/ProfileView.svelte";
|
||||
import MenuCheckboxItem from "$components/ui/menu/MenuCheckboxItem.svelte";
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import { popupTitle } from "$stores/popup";
|
||||
|
||||
let profileId = $derived(page.params.id);
|
||||
let profile = $derived<MaintenanceProfile|null>(
|
||||
$maintenanceProfiles.find(profile => profile.id === profileId) || null
|
||||
let profile = $derived<TaggingProfile|null>(
|
||||
$taggingProfiles.find(profile => profile.id === profileId) || null
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (profileId === 'new') {
|
||||
goto('/features/maintenance/new/edit');
|
||||
goto('/features/profiles/new/edit');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!profile) {
|
||||
console.warn(`Profile ${profileId} not found.`);
|
||||
goto('/features/maintenance');
|
||||
goto('/features/profiles');
|
||||
} else {
|
||||
$popupTitle = `Tagging Profile: ${profile.settings.name}`;
|
||||
}
|
||||
@@ -31,22 +31,22 @@
|
||||
let isActiveProfile = $state(false);
|
||||
|
||||
$effect.pre(() => {
|
||||
isActiveProfile = $activeProfileStore === profileId;
|
||||
isActiveProfile = $activeTaggingProfile === profileId;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (isActiveProfile && $activeProfileStore !== profileId) {
|
||||
$activeProfileStore = profileId;
|
||||
if (isActiveProfile && $activeTaggingProfile !== profileId) {
|
||||
$activeTaggingProfile = profileId;
|
||||
}
|
||||
|
||||
if (!isActiveProfile && $activeProfileStore === profileId) {
|
||||
$activeProfileStore = null;
|
||||
if (!isActiveProfile && $activeTaggingProfile === profileId) {
|
||||
$activeTaggingProfile = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuItem href="/features/maintenance" icon="arrow-left">Back</MenuItem>
|
||||
<MenuItem href="/features/profiles" icon="arrow-left">Back</MenuItem>
|
||||
<hr>
|
||||
</Menu>
|
||||
{#if profile}
|
||||
@@ -54,14 +54,14 @@
|
||||
{/if}
|
||||
<Menu>
|
||||
<hr>
|
||||
<MenuItem href="/features/maintenance/{profileId}/edit" icon="wrench">Edit Profile</MenuItem>
|
||||
<MenuItem href="/features/profiles/{profileId}/edit" icon="wrench">Edit Profile</MenuItem>
|
||||
<MenuCheckboxItem bind:checked={isActiveProfile}>
|
||||
Activate Profile
|
||||
</MenuCheckboxItem>
|
||||
<MenuItem href="/features/maintenance/{profileId}/export" icon="file-export">
|
||||
<MenuItem href="/features/profiles/{profileId}/export" icon="file-export">
|
||||
Export Profile
|
||||
</MenuItem>
|
||||
<MenuItem href="/features/maintenance/{profileId}/delete" icon="trash">
|
||||
<MenuItem href="/features/profiles/{profileId}/delete" icon="trash">
|
||||
Delete Profile
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
@@ -3,18 +3,18 @@
|
||||
import Menu from "$components/ui/menu/Menu.svelte";
|
||||
import MenuItem from "$components/ui/menu/MenuItem.svelte";
|
||||
import { page } from "$app/state";
|
||||
import { maintenanceProfiles } from "$stores/entities/maintenance-profiles";
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import { taggingProfiles } from "$stores/entities/tagging-profiles";
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import { popupTitle } from "$stores/popup";
|
||||
|
||||
const profileId = $derived(page.params.id);
|
||||
const targetProfile = $derived<MaintenanceProfile | null>(
|
||||
$maintenanceProfiles.find(profile => profile.id === profileId) || null
|
||||
const targetProfile = $derived<TaggingProfile | null>(
|
||||
$taggingProfiles.find(profile => profile.id === profileId) || null
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (!targetProfile) {
|
||||
goto('/features/maintenance');
|
||||
goto('/features/profiles');
|
||||
} else {
|
||||
$popupTitle = `Deleting Tagging Profile: ${targetProfile.settings.name}`
|
||||
}
|
||||
@@ -27,12 +27,12 @@
|
||||
}
|
||||
|
||||
await targetProfile.delete();
|
||||
await goto('/features/maintenance');
|
||||
await goto('/features/profiles');
|
||||
}
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuItem href="/features/maintenance/{profileId}" icon="arrow-left">Back</MenuItem>
|
||||
<MenuItem href="/features/profiles/{profileId}" icon="arrow-left">Back</MenuItem>
|
||||
<hr>
|
||||
</Menu>
|
||||
{#if targetProfile}
|
||||
@@ -42,7 +42,7 @@
|
||||
<Menu>
|
||||
<hr>
|
||||
<MenuItem onclick={deleteProfile}>Yes</MenuItem>
|
||||
<MenuItem href="/features/maintenance/{profileId}">No</MenuItem>
|
||||
<MenuItem href="/features/profiles/{profileId}">No</MenuItem>
|
||||
</Menu>
|
||||
{:else}
|
||||
<p>Loading...</p>
|
||||
@@ -7,19 +7,19 @@
|
||||
import FormContainer from "$components/ui/forms/FormContainer.svelte";
|
||||
import { page } from "$app/state";
|
||||
import { goto } from "$app/navigation";
|
||||
import { maintenanceProfiles } from "$stores/entities/maintenance-profiles";
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import { taggingProfiles } from "$stores/entities/tagging-profiles";
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import { popupTitle } from "$stores/popup";
|
||||
|
||||
|
||||
let profileId = $derived(page.params.id);
|
||||
|
||||
let targetProfile = $derived.by<MaintenanceProfile | null>(() => {
|
||||
let targetProfile = $derived.by<TaggingProfile | null>(() => {
|
||||
if (profileId === 'new') {
|
||||
return new MaintenanceProfile(crypto.randomUUID(), {});
|
||||
return new TaggingProfile(crypto.randomUUID(), {});
|
||||
}
|
||||
|
||||
return $maintenanceProfiles.find(profile => profile.id === profileId) || null;
|
||||
return $taggingProfiles.find(profile => profile.id === profileId) || null;
|
||||
});
|
||||
|
||||
let profileName = $state('');
|
||||
@@ -32,7 +32,7 @@
|
||||
}
|
||||
|
||||
if (!targetProfile) {
|
||||
goto('/features/maintenance');
|
||||
goto('/features/profiles');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -53,12 +53,12 @@
|
||||
targetProfile.settings.temporary = false;
|
||||
|
||||
await targetProfile.save();
|
||||
await goto('/features/maintenance/' + targetProfile.id);
|
||||
await goto('/features/profiles/' + targetProfile.id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuItem href="/features/maintenance{profileId === 'new' ? '' : '/' + profileId}" icon="arrow-left">
|
||||
<MenuItem href="/features/profiles{profileId === 'new' ? '' : '/' + profileId}" icon="arrow-left">
|
||||
Back
|
||||
</MenuItem>
|
||||
<hr>
|
||||
@@ -1,31 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { page } from "$app/state";
|
||||
import { goto } from "$app/navigation";
|
||||
import { maintenanceProfiles } from "$stores/entities/maintenance-profiles";
|
||||
import { taggingProfiles } from "$stores/entities/tagging-profiles";
|
||||
import Menu from "$components/ui/menu/Menu.svelte";
|
||||
import MenuItem from "$components/ui/menu/MenuItem.svelte";
|
||||
import FormContainer from "$components/ui/forms/FormContainer.svelte";
|
||||
import FormControl from "$components/ui/forms/FormControl.svelte";
|
||||
import EntitiesTransporter from "$lib/extension/EntitiesTransporter";
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import { popupTitle } from "$stores/popup";
|
||||
|
||||
let isCompressedProfileShown = $state(true);
|
||||
|
||||
const profileId = $derived(page.params.id);
|
||||
const profile = $derived<MaintenanceProfile | null>(
|
||||
$maintenanceProfiles.find(profile => profile.id === profileId) || null
|
||||
const profile = $derived<TaggingProfile | null>(
|
||||
$taggingProfiles.find(profile => profile.id === profileId) || null
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (!profile) {
|
||||
goto('/features/maintenance/');
|
||||
goto('/features/profiles/');
|
||||
} else {
|
||||
$popupTitle = `Export Tagging Profile: ${profile.settings.name}`;
|
||||
}
|
||||
});
|
||||
|
||||
const profilesTransporter = new EntitiesTransporter(MaintenanceProfile);
|
||||
const profilesTransporter = new EntitiesTransporter(TaggingProfile);
|
||||
|
||||
let rawExportedProfile = $derived(profile ? profilesTransporter.exportToJSON(profile) : '');
|
||||
let compressedExportedProfile = $derived(profile ? profilesTransporter.exportToCompressedJSON(profile) : '');
|
||||
@@ -33,7 +33,7 @@
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuItem href="/features/maintenance/{profileId}" icon="arrow-left">
|
||||
<MenuItem href="/features/profiles/{profileId}" icon="arrow-left">
|
||||
Back
|
||||
</MenuItem>
|
||||
<hr>
|
||||
@@ -2,22 +2,22 @@
|
||||
import Menu from "$components/ui/menu/Menu.svelte";
|
||||
import MenuItem from "$components/ui/menu/MenuItem.svelte";
|
||||
import FormContainer from "$components/ui/forms/FormContainer.svelte";
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import FormControl from "$components/ui/forms/FormControl.svelte";
|
||||
import ProfileView from "$components/features/ProfileView.svelte";
|
||||
import { maintenanceProfiles } from "$stores/entities/maintenance-profiles";
|
||||
import { taggingProfiles } from "$stores/entities/tagging-profiles";
|
||||
import { goto } from "$app/navigation";
|
||||
import EntitiesTransporter from "$lib/extension/EntitiesTransporter";
|
||||
import { popupTitle } from "$stores/popup";
|
||||
import Notice from "$components/ui/Notice.svelte";
|
||||
|
||||
const profilesTransporter = new EntitiesTransporter(MaintenanceProfile);
|
||||
const profilesTransporter = new EntitiesTransporter(TaggingProfile);
|
||||
|
||||
let importedString = $state('');
|
||||
let errorMessage = $state('');
|
||||
|
||||
let candidateProfile = $state<MaintenanceProfile | null>(null);
|
||||
let existingProfile = $state<MaintenanceProfile | null>(null);
|
||||
let candidateProfile = $state<TaggingProfile | null>(null);
|
||||
let existingProfile = $state<TaggingProfile | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
$popupTitle = candidateProfile
|
||||
@@ -49,7 +49,7 @@
|
||||
}
|
||||
|
||||
if (candidateProfile) {
|
||||
existingProfile = $maintenanceProfiles.find(profile => profile.id === candidateProfile?.id) ?? null;
|
||||
existingProfile = $taggingProfiles.find(profile => profile.id === candidateProfile?.id) ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
}
|
||||
|
||||
candidateProfile.save().then(() => {
|
||||
goto(`/features/maintenance`);
|
||||
goto(`/features/profiles`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,16 +68,16 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const clonedProfile = new MaintenanceProfile(crypto.randomUUID(), candidateProfile.settings);
|
||||
const clonedProfile = new TaggingProfile(crypto.randomUUID(), candidateProfile.settings);
|
||||
clonedProfile.settings.name += ` (Clone ${new Date().toISOString()})`;
|
||||
clonedProfile.save().then(() => {
|
||||
goto(`/features/maintenance`);
|
||||
goto(`/features/profiles`);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuItem href="/features/maintenance" icon="arrow-left">Back</MenuItem>
|
||||
<MenuItem href="/features/profiles" icon="arrow-left">Back</MenuItem>
|
||||
<hr>
|
||||
</Menu>
|
||||
{#if errorMessage}
|
||||
@@ -4,12 +4,12 @@
|
||||
import FormControl from "$components/ui/forms/FormControl.svelte";
|
||||
import Menu from "$components/ui/menu/Menu.svelte";
|
||||
import MenuItem from "$components/ui/menu/MenuItem.svelte";
|
||||
import { stripBlacklistedTagsEnabled } from "$stores/preferences/maintenance";
|
||||
import { stripBlacklistedTagsEnabled } from "$stores/preferences/profiles";
|
||||
import {
|
||||
shouldReplaceLinksOnForumPosts,
|
||||
shouldReplaceTextOfTagLinks,
|
||||
shouldSeparateTagGroups
|
||||
} from "$stores/preferences/tag";
|
||||
} from "$stores/preferences/tags";
|
||||
import { popupTitle } from "$stores/popup";
|
||||
|
||||
$popupTitle = 'Tagging Preferences';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Menu from "$components/ui/menu/Menu.svelte";
|
||||
import MenuItem from "$components/ui/menu/MenuItem.svelte";
|
||||
import { maintenanceProfiles } from "$stores/entities/maintenance-profiles";
|
||||
import { taggingProfiles } from "$stores/entities/tagging-profiles";
|
||||
import MenuCheckboxItem from "$components/ui/menu/MenuCheckboxItem.svelte";
|
||||
import { tagGroups } from "$stores/entities/tag-groups";
|
||||
import BulkEntitiesTransporter from "$lib/extension/BulkEntitiesTransporter";
|
||||
@@ -30,7 +30,7 @@
|
||||
if (displayExportedString) {
|
||||
const elementsToExport: StorageEntity[] = [];
|
||||
|
||||
$maintenanceProfiles.forEach(profile => {
|
||||
$taggingProfiles.forEach(profile => {
|
||||
if (exportedEntities.profiles[profile.id]) {
|
||||
elementsToExport.push(profile);
|
||||
}
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
function refreshAreAllEntitiesChecked() {
|
||||
requestAnimationFrame(() => {
|
||||
exportAllProfiles = $maintenanceProfiles.every(profile => exportedEntities.profiles[profile.id]);
|
||||
exportAllProfiles = $taggingProfiles.every(profile => exportedEntities.profiles[profile.id]);
|
||||
exportAllGroups = $tagGroups.every(group => exportedEntities.groups[group.id]);
|
||||
});
|
||||
}
|
||||
@@ -69,7 +69,7 @@
|
||||
requestAnimationFrame(() => {
|
||||
switch (targetEntity) {
|
||||
case "profiles":
|
||||
$maintenanceProfiles.forEach(profile => exportedEntities.profiles[profile.id] = exportAllProfiles);
|
||||
$taggingProfiles.forEach(profile => exportedEntities.profiles[profile.id] = exportAllProfiles);
|
||||
break;
|
||||
case "groups":
|
||||
$tagGroups.forEach(group => exportedEntities.groups[group.id] = exportAllGroups);
|
||||
@@ -94,11 +94,11 @@
|
||||
<Menu>
|
||||
<MenuItem href="/transporting" icon="arrow-left">Back</MenuItem>
|
||||
<hr>
|
||||
{#if $maintenanceProfiles.length}
|
||||
{#if $taggingProfiles.length}
|
||||
<MenuCheckboxItem bind:checked={exportAllProfiles} oninput={createToggleAllOnUserInput('profiles')}>
|
||||
Export All Profiles
|
||||
</MenuCheckboxItem>
|
||||
{#each $maintenanceProfiles as profile}
|
||||
{#each $taggingProfiles as profile}
|
||||
<MenuCheckboxItem bind:checked={exportedEntities.profiles[profile.id]} oninput={refreshAreAllEntitiesChecked}>
|
||||
Profile: {profile.settings.name}
|
||||
</MenuCheckboxItem>
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import MenuItem from "$components/ui/menu/MenuItem.svelte";
|
||||
import FormContainer from "$components/ui/forms/FormContainer.svelte";
|
||||
import FormControl from "$components/ui/forms/FormControl.svelte";
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import TagGroup from "$entities/TagGroup";
|
||||
import BulkEntitiesTransporter from "$lib/extension/BulkEntitiesTransporter";
|
||||
import type StorageEntity from "$lib/extension/base/StorageEntity";
|
||||
import { maintenanceProfiles } from "$stores/entities/maintenance-profiles";
|
||||
import { taggingProfiles } from "$stores/entities/tagging-profiles";
|
||||
import { tagGroups } from "$stores/entities/tag-groups";
|
||||
import MenuCheckboxItem from "$components/ui/menu/MenuCheckboxItem.svelte";
|
||||
import ProfileView from "$components/features/ProfileView.svelte";
|
||||
@@ -20,7 +20,7 @@
|
||||
let importedString = $state('');
|
||||
let errorMessage = $state('');
|
||||
|
||||
let importedProfiles = $state<MaintenanceProfile[]>([]);
|
||||
let importedProfiles = $state<TaggingProfile[]>([]);
|
||||
let importedGroups = $state<TagGroup[]>([]);
|
||||
|
||||
let saveAllProfiles = $state(false);
|
||||
@@ -36,10 +36,10 @@
|
||||
let previewedEntity = $state<StorageEntity | null>(null);
|
||||
|
||||
const existingProfilesMap = $derived(
|
||||
$maintenanceProfiles.reduce((map, profile) => {
|
||||
$taggingProfiles.reduce((map, profile) => {
|
||||
map.set(profile.id, profile);
|
||||
return map;
|
||||
}, new Map<string, MaintenanceProfile>())
|
||||
}, new Map<string, TaggingProfile>())
|
||||
);
|
||||
|
||||
const existingGroupsMap = $derived(
|
||||
@@ -98,7 +98,7 @@
|
||||
for (const targetImportedEntity of importedEntities) {
|
||||
switch (targetImportedEntity.type) {
|
||||
case "profiles":
|
||||
importedProfiles.push(targetImportedEntity as MaintenanceProfile);
|
||||
importedProfiles.push(targetImportedEntity as TaggingProfile);
|
||||
break;
|
||||
case "groups":
|
||||
importedGroups.push(targetImportedEntity as TagGroup);
|
||||
@@ -202,7 +202,7 @@
|
||||
<MenuItem onclick={() => previewedEntity = null} icon="arrow-left">Back to Selection</MenuItem>
|
||||
<hr>
|
||||
</Menu>
|
||||
{#if previewedEntity instanceof MaintenanceProfile}
|
||||
{#if previewedEntity instanceof TaggingProfile}
|
||||
<ProfileView profile={previewedEntity}></ProfileView>
|
||||
{:else if previewedEntity instanceof TagGroup}
|
||||
<GroupView group={previewedEntity}></GroupView>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { type Writable, writable } from "svelte/store";
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings";
|
||||
|
||||
/**
|
||||
* Store for working with maintenance profiles in the Svelte popup.
|
||||
*/
|
||||
export const maintenanceProfiles: Writable<MaintenanceProfile[]> = writable([]);
|
||||
|
||||
/**
|
||||
* Store for the active maintenance profile ID.
|
||||
*/
|
||||
export const activeProfileStore: Writable<string|null> = writable(null);
|
||||
|
||||
const maintenanceSettings = new MaintenanceSettings();
|
||||
|
||||
/**
|
||||
* Active profile ID stored locally. Used to reset active profile once the existing profile was removed.
|
||||
*/
|
||||
let lastActiveProfileId: string|null = null;
|
||||
|
||||
Promise.allSettled([
|
||||
// Read the initial values from the storages first
|
||||
MaintenanceProfile.readAll().then(profiles => {
|
||||
maintenanceProfiles.set(profiles);
|
||||
}),
|
||||
maintenanceSettings.resolveActiveProfileId().then(activeProfileId => {
|
||||
activeProfileStore.set(activeProfileId);
|
||||
})
|
||||
]).then(() => {
|
||||
// And only after initial values are loaded, start watching for changes from storage and from user's interaction
|
||||
MaintenanceProfile.subscribe(profiles => {
|
||||
maintenanceProfiles.set(profiles);
|
||||
});
|
||||
|
||||
maintenanceSettings.subscribe(settings => {
|
||||
activeProfileStore.set(settings.activeProfile || null);
|
||||
});
|
||||
|
||||
activeProfileStore.subscribe(profileId => {
|
||||
lastActiveProfileId = profileId;
|
||||
|
||||
void maintenanceSettings.setActiveProfileId(profileId);
|
||||
});
|
||||
|
||||
// Watch the existence of the active profile on every change.
|
||||
MaintenanceProfile.subscribe(profiles => {
|
||||
if (!profiles.find(profile => profile.id === lastActiveProfileId)) {
|
||||
activeProfileStore.set(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
52
src/stores/entities/tagging-profiles.ts
Normal file
52
src/stores/entities/tagging-profiles.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { type Writable, writable } from "svelte/store";
|
||||
import TaggingProfile from "$entities/TaggingProfile";
|
||||
import TaggingProfilesPreferences from "$lib/extension/preferences/TaggingProfilesPreferences";
|
||||
|
||||
/**
|
||||
* Store for working with maintenance profiles in the Svelte popup.
|
||||
*/
|
||||
export const taggingProfiles: Writable<TaggingProfile[]> = writable([]);
|
||||
|
||||
/**
|
||||
* Store for the active maintenance profile ID.
|
||||
*/
|
||||
export const activeTaggingProfile: Writable<string|null> = writable(null);
|
||||
|
||||
const preferences = new TaggingProfilesPreferences();
|
||||
|
||||
/**
|
||||
* Active profile ID stored locally. Used to reset active profile once the existing profile was removed.
|
||||
*/
|
||||
let lastActiveProfileId: string|null = null;
|
||||
|
||||
Promise.allSettled([
|
||||
// Read the initial values from the storages first
|
||||
TaggingProfile.readAll().then(profiles => {
|
||||
taggingProfiles.set(profiles);
|
||||
}),
|
||||
preferences.activeProfile.get().then(activeProfileId => {
|
||||
activeTaggingProfile.set(activeProfileId);
|
||||
})
|
||||
]).then(() => {
|
||||
// And only after initial values are loaded, start watching for changes from storage and from user's interaction
|
||||
TaggingProfile.subscribe(profiles => {
|
||||
taggingProfiles.set(profiles);
|
||||
});
|
||||
|
||||
preferences.subscribe(settings => {
|
||||
activeTaggingProfile.set(settings.activeProfile || null);
|
||||
});
|
||||
|
||||
activeTaggingProfile.subscribe(profileId => {
|
||||
lastActiveProfileId = profileId;
|
||||
|
||||
void preferences.activeProfile.set(profileId);
|
||||
});
|
||||
|
||||
// Watch the existence of the active profile on every change.
|
||||
TaggingProfile.subscribe(profiles => {
|
||||
if (!profiles.find(profile => profile.id === lastActiveProfileId)) {
|
||||
activeTaggingProfile.set(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
import { writable } from "svelte/store";
|
||||
import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings";
|
||||
|
||||
export const stripBlacklistedTagsEnabled = writable(true);
|
||||
|
||||
const maintenanceSettings = new MaintenanceSettings();
|
||||
|
||||
Promise
|
||||
.all([
|
||||
maintenanceSettings.resolveStripBlacklistedTags().then(v => stripBlacklistedTagsEnabled.set(v ?? true))
|
||||
])
|
||||
.then(() => {
|
||||
maintenanceSettings.subscribe(settings => {
|
||||
stripBlacklistedTagsEnabled.set(typeof settings.stripBlacklistedTags === 'boolean' ? settings.stripBlacklistedTags : true);
|
||||
});
|
||||
|
||||
stripBlacklistedTagsEnabled.subscribe(v => maintenanceSettings.setStripBlacklistedTags(v));
|
||||
});
|
||||
@@ -1,18 +1,18 @@
|
||||
import { writable } from "svelte/store";
|
||||
import MiscSettings from "$lib/extension/settings/MiscSettings";
|
||||
import MiscPreferences from "$lib/extension/preferences/MiscPreferences";
|
||||
|
||||
export const fullScreenViewerEnabled = writable(true);
|
||||
|
||||
const miscSettings = new MiscSettings();
|
||||
const preferences = new MiscPreferences();
|
||||
|
||||
Promise.allSettled([
|
||||
miscSettings.resolveFullscreenViewerEnabled().then(v => fullScreenViewerEnabled.set(v))
|
||||
preferences.fullscreenViewer.get().then(v => fullScreenViewerEnabled.set(v))
|
||||
]).then(() => {
|
||||
fullScreenViewerEnabled.subscribe(value => {
|
||||
void miscSettings.setFullscreenViewerEnabled(value);
|
||||
void preferences.fullscreenViewer.set(value);
|
||||
});
|
||||
|
||||
miscSettings.subscribe(settings => {
|
||||
preferences.subscribe(settings => {
|
||||
fullScreenViewerEnabled.set(Boolean(settings.fullscreenViewer));
|
||||
});
|
||||
});
|
||||
|
||||
18
src/stores/preferences/profiles.ts
Normal file
18
src/stores/preferences/profiles.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { writable } from "svelte/store";
|
||||
import TaggingProfilesPreferences from "$lib/extension/preferences/TaggingProfilesPreferences";
|
||||
|
||||
export const stripBlacklistedTagsEnabled = writable(true);
|
||||
|
||||
const preferences = new TaggingProfilesPreferences();
|
||||
|
||||
Promise
|
||||
.all([
|
||||
preferences.stripBlacklistedTags.get().then(v => stripBlacklistedTagsEnabled.set(v ?? true))
|
||||
])
|
||||
.then(() => {
|
||||
preferences.subscribe(settings => {
|
||||
stripBlacklistedTagsEnabled.set(typeof settings.stripBlacklistedTags === 'boolean' ? settings.stripBlacklistedTags : true);
|
||||
});
|
||||
|
||||
stripBlacklistedTagsEnabled.subscribe(v => preferences.stripBlacklistedTags.set(v));
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { writable } from "svelte/store";
|
||||
import TagSettings from "$lib/extension/settings/TagSettings";
|
||||
import TagsPreferences from "$lib/extension/preferences/TagsPreferences";
|
||||
|
||||
const tagSettings = new TagSettings();
|
||||
const preferences = new TagsPreferences();
|
||||
|
||||
export const shouldSeparateTagGroups = writable(false);
|
||||
export const shouldReplaceLinksOnForumPosts = writable(false);
|
||||
@@ -9,24 +9,24 @@ export const shouldReplaceTextOfTagLinks = writable(true);
|
||||
|
||||
Promise
|
||||
.allSettled([
|
||||
tagSettings.resolveGroupSeparation().then(value => shouldSeparateTagGroups.set(value)),
|
||||
tagSettings.resolveReplaceLinks().then(value => shouldReplaceLinksOnForumPosts.set(value)),
|
||||
tagSettings.resolveReplaceLinkText().then(value => shouldReplaceTextOfTagLinks.set(value)),
|
||||
preferences.groupSeparation.get().then(value => shouldSeparateTagGroups.set(value)),
|
||||
preferences.replaceLinks.get().then(value => shouldReplaceLinksOnForumPosts.set(value)),
|
||||
preferences.replaceLinkText.get().then(value => shouldReplaceTextOfTagLinks.set(value)),
|
||||
])
|
||||
.then(() => {
|
||||
shouldSeparateTagGroups.subscribe(value => {
|
||||
void tagSettings.setGroupSeparation(value);
|
||||
void preferences.groupSeparation.set(value);
|
||||
});
|
||||
|
||||
shouldReplaceLinksOnForumPosts.subscribe(value => {
|
||||
void tagSettings.setReplaceLinks(value);
|
||||
void preferences.replaceLinks.set(value);
|
||||
});
|
||||
|
||||
shouldReplaceTextOfTagLinks.subscribe(value => {
|
||||
void tagSettings.setReplaceLinkText(value);
|
||||
void preferences.replaceLinkText.set(value);
|
||||
});
|
||||
|
||||
tagSettings.subscribe(settings => {
|
||||
preferences.subscribe(settings => {
|
||||
shouldSeparateTagGroups.set(Boolean(settings.groupSeparation));
|
||||
shouldReplaceLinksOnForumPosts.set(Boolean(settings.replaceLinks));
|
||||
shouldReplaceTextOfTagLinks.set(Boolean(settings.replaceLinkText));
|
||||
Reference in New Issue
Block a user