From 9024883949f58df2fc0619512504a92450d2acd2 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sat, 7 Mar 2026 06:41:28 +0400 Subject: [PATCH] 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. --- src/content/components/BlockCommunication.ts | 4 +- src/content/components/FullscreenViewer.ts | 4 +- .../components/ImageShowFullscreenButton.ts | 2 +- src/content/components/MaintenancePopup.ts | 8 +- src/content/components/TagDropdownWrapper.ts | 6 +- src/content/components/TagsListBlock.ts | 4 +- .../extension/base/CacheablePreferences.ts | 120 ++++++++++++++++-- .../extension/preferences/MiscPreferences.ts | 29 ++--- .../preferences/TaggingProfilesPreferences.ts | 62 ++++----- .../extension/preferences/TagsPreferences.ts | 37 ++---- src/stores/entities/tagging-profiles.ts | 4 +- src/stores/preferences/misc.ts | 4 +- src/stores/preferences/profiles.ts | 4 +- src/stores/preferences/tags.ts | 12 +- 14 files changed, 188 insertions(+), 112 deletions(-) diff --git a/src/content/components/BlockCommunication.ts b/src/content/components/BlockCommunication.ts index 675c83d..bcc444f 100644 --- a/src/content/components/BlockCommunication.ts +++ b/src/content/components/BlockCommunication.ts @@ -17,8 +17,8 @@ export class BlockCommunication extends BaseComponent { protected init() { Promise.all([ - BlockCommunication.#preferences.resolveReplaceLinks(), - BlockCommunication.#preferences.resolveReplaceLinkText(), + BlockCommunication.#preferences.replaceLinks.get(), + BlockCommunication.#preferences.replaceLinkText.get(), ]).then(([replaceLinks, replaceLinkText]) => { this.#onReplaceLinkSettingResolved( replaceLinks, diff --git a/src/content/components/FullscreenViewer.ts b/src/content/components/FullscreenViewer.ts index b5183d1..616acdf 100644 --- a/src/content/components/FullscreenViewer.ts +++ b/src/content/components/FullscreenViewer.ts @@ -54,7 +54,7 @@ export class FullscreenViewer extends BaseComponent { this.#sizeSelectorElement.addEventListener('click', event => event.stopPropagation()); FullscreenViewer.#preferences - .resolveFullscreenViewerPreviewSize() + .fullscreenViewerSize.get() .then(this.#onSizeResolved.bind(this)) .then(this.#watchForSizeSelectionChanges.bind(this)); } @@ -202,7 +202,7 @@ export class FullscreenViewer extends BaseComponent { } lastActiveSize = targetSize; - void FullscreenViewer.#preferences.setFullscreenViewerPreviewSize(targetSize); + void FullscreenViewer.#preferences.fullscreenViewerSize.set(targetSize as FullscreenViewerSize); }); } diff --git a/src/content/components/ImageShowFullscreenButton.ts b/src/content/components/ImageShowFullscreenButton.ts index 7e58985..e50ce09 100644 --- a/src/content/components/ImageShowFullscreenButton.ts +++ b/src/content/components/ImageShowFullscreenButton.ts @@ -26,7 +26,7 @@ export class ImageShowFullscreenButton extends BaseComponent { this.on('click', this.#onButtonClicked.bind(this)); if (ImageShowFullscreenButton.#preferences) { - ImageShowFullscreenButton.#preferences.resolveFullscreenViewerEnabled() + ImageShowFullscreenButton.#preferences.fullscreenViewer.get() .then(isEnabled => { this.#isFullscreenButtonEnabled = isEnabled; this.#updateFullscreenButtonVisibility(); diff --git a/src/content/components/MaintenancePopup.ts b/src/content/components/MaintenancePopup.ts index dbcc01c..1f3c886 100644 --- a/src/content/components/MaintenancePopup.ts +++ b/src/content/components/MaintenancePopup.ts @@ -214,7 +214,7 @@ export class MaintenancePopup extends BaseComponent { let maybeTagsAndAliasesAfterUpdate; - const shouldAutoRemove = await MaintenancePopup.#preferences.resolveStripBlacklistedTags(); + const shouldAutoRemove = await MaintenancePopup.#preferences.stripBlacklistedTags.get(); try { maybeTagsAndAliasesAfterUpdate = await MaintenancePopup.#scrapedAPI.updateImageTags( @@ -359,13 +359,11 @@ export class MaintenancePopup extends BaseComponent { lastActiveProfileId = settings.activeProfile; - this.#preferences - .resolveActiveProfileAsObject() + this.#preferences.activeProfile.asObject() .then(callback); }); - this.#preferences - .resolveActiveProfileAsObject() + this.#preferences.activeProfile.asObject() .then(profileOrNull => { if (profileOrNull) { lastActiveProfileId = profileOrNull.id; diff --git a/src/content/components/TagDropdownWrapper.ts b/src/content/components/TagDropdownWrapper.ts index f50b6b2..0a64ec3 100644 --- a/src/content/components/TagDropdownWrapper.ts +++ b/src/content/components/TagDropdownWrapper.ts @@ -179,7 +179,7 @@ export class TagDropdownWrapper extends BaseComponent { }); await profile.save(); - await TagDropdownWrapper.#preferences.setActiveProfileId(profile.id); + await TagDropdownWrapper.#preferences.activeProfile.set(profile.id); } async #onToggleInExistingClicked() { @@ -219,7 +219,7 @@ export class TagDropdownWrapper extends BaseComponent { lastActiveProfile = settings.activeProfile ?? null; this.#preferences - .resolveActiveProfileAsObject() + .activeProfile.asObject() .then(onActiveProfileChange); }); @@ -232,7 +232,7 @@ export class TagDropdownWrapper extends BaseComponent { }); this.#preferences - .resolveActiveProfileAsObject() + .activeProfile.asObject() .then(activeProfile => { lastActiveProfile = activeProfile?.id ?? null; onActiveProfileChange(activeProfile); diff --git a/src/content/components/TagsListBlock.ts b/src/content/components/TagsListBlock.ts index 082b21c..1f2557e 100644 --- a/src/content/components/TagsListBlock.ts +++ b/src/content/components/TagsListBlock.ts @@ -44,7 +44,7 @@ export class TagsListBlock extends BaseComponent { } init() { - this.#preferences.resolveGroupSeparation().then(this.#onTagSeparationChange.bind(this)); + this.#preferences.groupSeparation.get().then(this.#onTagSeparationChange.bind(this)); this.#preferences.subscribe(settings => { this.#onTagSeparationChange(Boolean(settings.groupSeparation)) }); @@ -103,7 +103,7 @@ export class TagsListBlock extends BaseComponent { #onToggleGroupingClicked(event: Event) { event.preventDefault(); - void this.#preferences.setGroupSeparation(!this.#shouldDisplaySeparation); + void this.#preferences.groupSeparation.set(!this.#shouldDisplaySeparation); } #handleTagGroupChanges(tagGroup: TagGroup) { diff --git a/src/lib/extension/base/CacheablePreferences.ts b/src/lib/extension/base/CacheablePreferences.ts index d5f6f35..315a6f3 100644 --- a/src/lib/extension/base/CacheablePreferences.ts +++ b/src/lib/extension/base/CacheablePreferences.ts @@ -1,11 +1,103 @@ import ConfigurationController from "$lib/extension/ConfigurationController"; -export default class CacheablePreferences { +/** + * Initialization options for the preference field helper class. + */ +type PreferenceFieldOptions = { + /** + * 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 = Record, + /** + * 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; + /** + * 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, options: PreferenceFieldOptions) { + 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> = { + readonly [FieldKey in keyof FieldsType]: PreferenceField; +} + +/** + * 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 { #controller: ConfigurationController; #cachedValues: Map = new Map(); #disposables: Function[] = []; - constructor(settingsNamespace: string) { + /** + * @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( @@ -21,13 +113,13 @@ export default class CacheablePreferences { } /** - * @template SettingType - * @param {string} settingName - * @param {SettingType} defaultValue - * @return {Promise} - * @protected + * 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. */ - protected async _resolveSetting(settingName: Key, defaultValue: Fields[Key]): Promise { + public async readRaw(settingName: Key, defaultValue: Fields[Key]): Promise { if (this.#cachedValues.has(settingName)) { return this.#cachedValues.get(settingName); } @@ -40,12 +132,14 @@ export default class CacheablePreferences { } /** + * 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 _writeSetting(settingName: Key, value: Fields[Key], force: boolean = false): Promise { + async writeRaw(settingName: Key, value: Fields[Key], force: boolean = false): Promise { if ( !force && this.#cachedValues.has(settingName) @@ -62,8 +156,9 @@ export default class CacheablePreferences { /** * 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. + * @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) => void): () => void { const unsubscribeCallback = this.#controller.subscribeToChanges(callback as (fields: Record) => void); @@ -73,6 +168,9 @@ export default class CacheablePreferences { return unsubscribeCallback; } + /** + * Completely disable all subscriptions. + */ dispose() { for (let disposeCallback of this.#disposables) { disposeCallback(); diff --git a/src/lib/extension/preferences/MiscPreferences.ts b/src/lib/extension/preferences/MiscPreferences.ts index 68dffbf..0c14051 100644 --- a/src/lib/extension/preferences/MiscPreferences.ts +++ b/src/lib/extension/preferences/MiscPreferences.ts @@ -1,4 +1,7 @@ -import CacheablePreferences from "$lib/extension/base/CacheablePreferences"; +import CacheablePreferences, { + PreferenceField, + type WithFields +} from "$lib/extension/base/CacheablePreferences"; export type FullscreenViewerSize = keyof App.ImageURIs; @@ -7,24 +10,18 @@ interface MiscPreferencesFields { fullscreenViewerSize: FullscreenViewerSize; } -export default class MiscPreferences extends CacheablePreferences { +export default class MiscPreferences extends CacheablePreferences implements WithFields { constructor() { super("misc"); } - async resolveFullscreenViewerEnabled() { - return this._resolveSetting("fullscreenViewer", true); - } + readonly fullscreenViewer = new PreferenceField(this, { + field: "fullscreenViewer", + defaultValue: 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); - } + readonly fullscreenViewerSize = new PreferenceField(this, { + field: "fullscreenViewerSize", + defaultValue: "large", + }); } diff --git a/src/lib/extension/preferences/TaggingProfilesPreferences.ts b/src/lib/extension/preferences/TaggingProfilesPreferences.ts index f255ba1..c32eae7 100644 --- a/src/lib/extension/preferences/TaggingProfilesPreferences.ts +++ b/src/lib/extension/preferences/TaggingProfilesPreferences.ts @@ -1,48 +1,40 @@ import TaggingProfile from "$entities/TaggingProfile"; -import CacheablePreferences from "$lib/extension/base/CacheablePreferences"; +import CacheablePreferences, { PreferenceField, type WithFields } from "$lib/extension/base/CacheablePreferences"; interface TaggingProfilePreferencesFields { activeProfile: string | null; stripBlacklistedTags: boolean; } -export default class TaggingProfilesPreferences extends CacheablePreferences { +class ActiveProfilePreference extends PreferenceField { + constructor(preferencesInstance: CacheablePreferences) { + super(preferencesInstance, { + field: "activeProfile", + defaultValue: null, + }); + } + + async asObject(): Promise { + 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 implements WithFields { constructor() { super("maintenance"); } - /** - * Set the active maintenance profile. - */ - async resolveActiveProfileId() { - return this._resolveSetting("activeProfile", null); - } + readonly activeProfile = new ActiveProfilePreference(this); - /** - * Get the active maintenance profile if it is set. - */ - async resolveActiveProfileAsObject(): Promise { - const resolvedProfileId = await this.resolveActiveProfileId(); - - return (await TaggingProfile.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 { - await this._writeSetting("activeProfile", profileId); - } - - async setStripBlacklistedTags(isEnabled: boolean) { - await this._writeSetting('stripBlacklistedTags', isEnabled); - } + readonly stripBlacklistedTags = new PreferenceField(this, { + field: "stripBlacklistedTags", + defaultValue: false, + }); } diff --git a/src/lib/extension/preferences/TagsPreferences.ts b/src/lib/extension/preferences/TagsPreferences.ts index 4922991..06596bb 100644 --- a/src/lib/extension/preferences/TagsPreferences.ts +++ b/src/lib/extension/preferences/TagsPreferences.ts @@ -1,4 +1,4 @@ -import CacheablePreferences from "$lib/extension/base/CacheablePreferences"; +import CacheablePreferences, { PreferenceField, type WithFields } from "$lib/extension/base/CacheablePreferences"; interface TagsPreferencesFields { groupSeparation: boolean; @@ -6,32 +6,23 @@ interface TagsPreferencesFields { replaceLinkText: boolean; } -export default class TagsPreferences extends CacheablePreferences { +export default class TagsPreferences extends CacheablePreferences implements WithFields { constructor() { super("tag"); } - async resolveGroupSeparation() { - return this._resolveSetting("groupSeparation", true); - } + readonly groupSeparation = new PreferenceField(this, { + field: "groupSeparation", + defaultValue: true, + }); - async resolveReplaceLinks() { - return this._resolveSetting("replaceLinks", false); - } + readonly replaceLinks = new PreferenceField(this, { + field: "replaceLinks", + defaultValue: 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)); - } + readonly replaceLinkText = new PreferenceField(this, { + field: "replaceLinkText", + defaultValue: true, + }); } diff --git a/src/stores/entities/tagging-profiles.ts b/src/stores/entities/tagging-profiles.ts index 7c38751..177268e 100644 --- a/src/stores/entities/tagging-profiles.ts +++ b/src/stores/entities/tagging-profiles.ts @@ -24,7 +24,7 @@ Promise.allSettled([ TaggingProfile.readAll().then(profiles => { taggingProfiles.set(profiles); }), - preferences.resolveActiveProfileId().then(activeProfileId => { + preferences.activeProfile.get().then(activeProfileId => { activeTaggingProfile.set(activeProfileId); }) ]).then(() => { @@ -40,7 +40,7 @@ Promise.allSettled([ activeTaggingProfile.subscribe(profileId => { lastActiveProfileId = profileId; - void preferences.setActiveProfileId(profileId); + void preferences.activeProfile.set(profileId); }); // Watch the existence of the active profile on every change. diff --git a/src/stores/preferences/misc.ts b/src/stores/preferences/misc.ts index 00aeb8c..121939a 100644 --- a/src/stores/preferences/misc.ts +++ b/src/stores/preferences/misc.ts @@ -6,10 +6,10 @@ export const fullScreenViewerEnabled = writable(true); const preferences = new MiscPreferences(); Promise.allSettled([ - preferences.resolveFullscreenViewerEnabled().then(v => fullScreenViewerEnabled.set(v)) + preferences.fullscreenViewer.get().then(v => fullScreenViewerEnabled.set(v)) ]).then(() => { fullScreenViewerEnabled.subscribe(value => { - void preferences.setFullscreenViewerEnabled(value); + void preferences.fullscreenViewer.set(value); }); preferences.subscribe(settings => { diff --git a/src/stores/preferences/profiles.ts b/src/stores/preferences/profiles.ts index 2e915dc..f7a65fc 100644 --- a/src/stores/preferences/profiles.ts +++ b/src/stores/preferences/profiles.ts @@ -7,12 +7,12 @@ const preferences = new TaggingProfilesPreferences(); Promise .all([ - preferences.resolveStripBlacklistedTags().then(v => stripBlacklistedTagsEnabled.set(v ?? true)) + 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.setStripBlacklistedTags(v)); + stripBlacklistedTagsEnabled.subscribe(v => preferences.stripBlacklistedTags.set(v)); }); diff --git a/src/stores/preferences/tags.ts b/src/stores/preferences/tags.ts index 190758d..84b1239 100644 --- a/src/stores/preferences/tags.ts +++ b/src/stores/preferences/tags.ts @@ -9,21 +9,21 @@ export const shouldReplaceTextOfTagLinks = writable(true); Promise .allSettled([ - preferences.resolveGroupSeparation().then(value => shouldSeparateTagGroups.set(value)), - preferences.resolveReplaceLinks().then(value => shouldReplaceLinksOnForumPosts.set(value)), - preferences.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 preferences.setGroupSeparation(value); + void preferences.groupSeparation.set(value); }); shouldReplaceLinksOnForumPosts.subscribe(value => { - void preferences.setReplaceLinks(value); + void preferences.replaceLinks.set(value); }); shouldReplaceTextOfTagLinks.subscribe(value => { - void preferences.setReplaceLinkText(value); + void preferences.replaceLinkText.set(value); }); preferences.subscribe(settings => {