1
0
mirror of https://github.com/koloml/furbooru-tagging-assistant.git synced 2026-03-24 23:02:58 +00:00

6 Commits

Author SHA1 Message Date
6c2ef795b3 Merge pull request #161 from koloml/feature/tagging-profiles-code-renaming
Refactoring: Renaming the entity classes, updating the API for accessing preferences values
2026-03-07 17:38:11 +04:00
58b620ef09 Renaming Philomena and scraping-related classes directory 2026-03-07 17:35:38 +04:00
9445b1e862 Restructuring and renaming content components and their initialization 2026-03-07 17:22:13 +04:00
9024883949 Refactoring how preferences classes provide access to fields inside
Instead of constantly implementing these weird methods to read or update
values, there will be fields inside the preferences which contain
methods to read or update them.
2026-03-07 06:41:28 +04:00
dc29c6ca69 Renaming for tagging profiles and preferences classes 2026-02-28 22:49:57 +04:00
441091142c Added screenshot preview of tag link replacement 2026-02-26 15:22:52 +04:00
54 changed files with 720 additions and 640 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

4
src/app.d.ts vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,3 @@
import { BlockCommunication } from "$content/components/BlockCommunication";
import { BlockCommunication } from "$content/components/philomena/BlockCommunication";
BlockCommunication.findAndInitializeAll();

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View 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",
});
}

View 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,
});
}

View 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,
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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);
}
});
});

View File

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

View File

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

View 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));
});

View File

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