mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
fix: settings props not being correctly set
This commit is contained in:
+51
-103
@@ -1,4 +1,4 @@
|
||||
import type { EventsAPI, Plugin, PluginAPI, PluginSettings, SEQTAAPI, SettingsAPI, StorageAPI } from './types';
|
||||
import type { EventsAPI, Plugin, PluginAPI, PluginSettings, SEQTAAPI, SettingsAPI, SettingValue, StorageAPI } from './types';
|
||||
import { eventManager } from '@/seqta/utils/listeners/EventManager';
|
||||
import ReactFiber from '@/seqta/utils/ReactFiber';
|
||||
import browser from 'webextension-polyfill';
|
||||
@@ -41,28 +41,17 @@ function createSEQTAAPI(): SEQTAAPI {
|
||||
|
||||
function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): SettingsAPI<T> & { loaded: Promise<void> } {
|
||||
const storageKey = `plugin.${plugin.id}.settings`;
|
||||
|
||||
// Use SettingValue to properly type the listeners
|
||||
// This ensures callbacks get correctly typed parameters
|
||||
const listeners = new Map<keyof T, Set<(value: SettingValue<T[keyof T]>) => void>>();
|
||||
|
||||
const listeners = new Map<keyof T, Set<(value: any) => void>>();
|
||||
let settings: { [K in keyof T]: SettingValue<T[K]> };
|
||||
const storageListeners = new Set<(changes: { [key: string]: any }, area: string) => void>();
|
||||
|
||||
// Initialize settings with defaults and proper typing
|
||||
settings = Object.entries(plugin.settings).reduce((acc, [key, setting]) => {
|
||||
// Extract the value from the default based on the setting type
|
||||
if (setting.type === 'boolean') {
|
||||
acc[key as keyof T] = setting.default as SettingValue<T[keyof T]>;
|
||||
} else if (setting.type === 'number') {
|
||||
acc[key as keyof T] = setting.default as SettingValue<T[keyof T]>;
|
||||
} else if (setting.type === 'string') {
|
||||
acc[key as keyof T] = setting.default as SettingValue<T[keyof T]>;
|
||||
} else if (setting.type === 'select') {
|
||||
acc[key as keyof T] = setting.default as SettingValue<T[keyof T]>;
|
||||
}
|
||||
return acc;
|
||||
}, {} as { [K in keyof T]: SettingValue<T[K]> });
|
||||
// Initialize settings with defaults
|
||||
const defaultSettings = {} as { [K in keyof T]: SettingValue<T[K]> };
|
||||
for (const key in plugin.settings) {
|
||||
defaultSettings[key] = plugin.settings[key].default as SettingValue<T[typeof key]>;
|
||||
}
|
||||
settings = defaultSettings;
|
||||
|
||||
|
||||
// Create a promise that resolves when settings are loaded
|
||||
const loaded = (async () => {
|
||||
@@ -71,22 +60,9 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
|
||||
if (stored[storageKey]) {
|
||||
Object.entries(stored[storageKey]).forEach(([key, value]) => {
|
||||
if (key in settings) {
|
||||
// Use proper type assertion based on the setting type
|
||||
const settingType = plugin.settings[key as keyof T].type;
|
||||
if (settingType === 'boolean' && typeof value === 'boolean') {
|
||||
settings[key as keyof T] = value as SettingValue<T[keyof T]>;
|
||||
} else if (settingType === 'number' && typeof value === 'number') {
|
||||
settings[key as keyof T] = value as SettingValue<T[keyof T]>;
|
||||
} else if (settingType === 'string' && typeof value === 'string') {
|
||||
settings[key as keyof T] = value as SettingValue<T[keyof T]>;
|
||||
} else if (settingType === 'select' && typeof value === 'string') {
|
||||
settings[key as keyof T] = value as SettingValue<T[keyof T]>;
|
||||
}
|
||||
|
||||
settings[key as keyof T] = value as any;
|
||||
// Notify any listeners that might have been registered already
|
||||
listeners.get(key as keyof T)?.forEach(callback =>
|
||||
callback(settings[key as keyof T] as SettingValue<T[keyof T]>)
|
||||
);
|
||||
listeners.get(key as keyof T)?.forEach(callback => callback(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -102,24 +78,8 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
|
||||
if (newValue) {
|
||||
// Update settings and notify listeners
|
||||
Object.entries(newValue).forEach(([key, value]) => {
|
||||
if (key in settings) {
|
||||
// Use proper type assertion based on the setting type
|
||||
const settingType = plugin.settings[key as keyof T].type;
|
||||
if (settingType === 'boolean' && typeof value === 'boolean') {
|
||||
settings[key as keyof T] = value as SettingValue<T[keyof T]>;
|
||||
} else if (settingType === 'number' && typeof value === 'number') {
|
||||
settings[key as keyof T] = value as SettingValue<T[keyof T]>;
|
||||
} else if (settingType === 'string' && typeof value === 'string') {
|
||||
settings[key as keyof T] = value as SettingValue<T[keyof T]>;
|
||||
} else if (settingType === 'select' && typeof value === 'string') {
|
||||
settings[key as keyof T] = value as SettingValue<T[keyof T]>;
|
||||
}
|
||||
|
||||
// Notify listeners with the correctly typed value
|
||||
listeners.get(key as keyof T)?.forEach(callback =>
|
||||
callback(settings[key as keyof T] as SettingValue<T[keyof T]>)
|
||||
);
|
||||
}
|
||||
settings[key as keyof T] = value as any;
|
||||
listeners.get(key as keyof T)?.forEach(callback => callback(value));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -127,57 +87,45 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
|
||||
browser.storage.onChanged.addListener(handleStorageChange);
|
||||
storageListeners.add(handleStorageChange);
|
||||
|
||||
// Create a proxy to handle direct property access
|
||||
const proxy = new Proxy(settings, {
|
||||
get(target, prop: string) {
|
||||
if (prop === 'onChange') {
|
||||
return <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => {
|
||||
if (!listeners.has(key)) {
|
||||
listeners.set(key, new Set());
|
||||
}
|
||||
listeners.get(key)!.add(callback as (value: SettingValue<T[keyof T]>) => void);
|
||||
return {
|
||||
unregister: () => {
|
||||
listeners.get(key)?.delete(callback as (value: SettingValue<T[keyof T]>) => void);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
if (prop === 'loaded') {
|
||||
return loaded;
|
||||
}
|
||||
return target[prop as keyof T];
|
||||
},
|
||||
set(target, prop: string, value: any) {
|
||||
if (prop === 'onChange' || prop === 'offChange' || prop === 'loaded') return false;
|
||||
|
||||
// Try to apply the right type based on the setting definition
|
||||
if (prop in plugin.settings) {
|
||||
const settingType = plugin.settings[prop as keyof T].type;
|
||||
if (settingType === 'boolean' && typeof value === 'boolean') {
|
||||
target[prop as keyof T] = value as SettingValue<T[keyof T]>;
|
||||
} else if (settingType === 'number' && typeof value === 'number') {
|
||||
target[prop as keyof T] = value as SettingValue<T[keyof T]>;
|
||||
} else if (settingType === 'string' && typeof value === 'string') {
|
||||
target[prop as keyof T] = value as SettingValue<T[keyof T]>;
|
||||
} else if (settingType === 'select' && typeof value === 'string') {
|
||||
target[prop as keyof T] = value as SettingValue<T[keyof T]>;
|
||||
}
|
||||
}
|
||||
|
||||
// Store all settings under the plugin's settings key
|
||||
browser.storage.local.set({
|
||||
[storageKey]: target
|
||||
});
|
||||
|
||||
// Notify listeners
|
||||
listeners.get(prop as keyof T)?.forEach(callback =>
|
||||
callback(target[prop as keyof T] as SettingValue<T[keyof T]>)
|
||||
);
|
||||
return true;
|
||||
},
|
||||
}) as SettingsAPI<T> & { loaded: Promise<void> };
|
||||
const baseSettings = {} as { [K in keyof T]: SettingValue<T[K]> };
|
||||
for (const key in plugin.settings) {
|
||||
baseSettings[key] = plugin.settings[key].default as SettingValue<T[typeof key]>;
|
||||
}
|
||||
|
||||
const settingsWithMeta = {
|
||||
...baseSettings,
|
||||
onChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => {
|
||||
if (!listeners.has(key)) {
|
||||
listeners.set(key, new Set());
|
||||
}
|
||||
listeners.get(key)!.add(callback);
|
||||
return {
|
||||
unregister: () => {
|
||||
listeners.get(key)!.delete(callback);
|
||||
}
|
||||
};
|
||||
},
|
||||
offChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => {
|
||||
listeners.get(key)?.delete(callback);
|
||||
},
|
||||
loaded
|
||||
};
|
||||
|
||||
const proxy = new Proxy(settingsWithMeta, {
|
||||
get(target, prop) {
|
||||
return target[prop as keyof typeof target];
|
||||
},
|
||||
set(target, prop, value) {
|
||||
if (prop === 'onChange' || prop === 'offChange' || prop === 'loaded') return false;
|
||||
|
||||
target[prop as keyof T] = value;
|
||||
browser.storage.local.set({ [storageKey]: baseSettings }); // Only store base settings
|
||||
listeners.get(prop as keyof T)?.forEach(callback => callback(value));
|
||||
return true;
|
||||
}
|
||||
}) as SettingsAPI<T>;
|
||||
|
||||
|
||||
return proxy;
|
||||
}
|
||||
|
||||
|
||||
+19
-15
@@ -1,4 +1,4 @@
|
||||
import type { Plugin, PluginSettings } from './types';
|
||||
import type { Plugin, PluginSettings, BooleanSetting, StringSetting, NumberSetting, SelectSetting } from './types';
|
||||
import { createPluginAPI } from './createAPI';
|
||||
import browser from 'webextension-polyfill';
|
||||
|
||||
@@ -164,25 +164,29 @@ export class PluginManager {
|
||||
public getAllPluginSettings(): Array<{
|
||||
pluginId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
settings: {
|
||||
[key: string]: {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
type: string;
|
||||
default: any;
|
||||
}
|
||||
[key: string]: (Omit<BooleanSetting, 'type'> & { type: 'boolean', id: string }) |
|
||||
(Omit<StringSetting, 'type'> & { type: 'string', id: string }) |
|
||||
(Omit<NumberSetting, 'type'> & { type: 'number', id: string }) |
|
||||
(Omit<SelectSetting<string>, 'type'> & { type: 'select', id: string, options: Array<{ value: string, label: string }> });
|
||||
}
|
||||
}> {
|
||||
return Array.from(this.plugins.entries()).map(([id, plugin]) => {
|
||||
const settingsEntries = Object.entries(plugin.settings).map(([key, setting]) => {
|
||||
return [key, {
|
||||
id: key,
|
||||
title: (setting as any).title || key,
|
||||
description: (setting as any).description || '',
|
||||
type: (setting as any).type,
|
||||
default: (setting as any).default
|
||||
}];
|
||||
const settingObj = setting as any;
|
||||
// Create a copy of the setting object without any functions
|
||||
const result: any = Object.fromEntries(
|
||||
Object.entries(settingObj)
|
||||
.filter(([_, value]) => typeof value !== 'function')
|
||||
);
|
||||
|
||||
// Ensure required properties are present
|
||||
result.id = key;
|
||||
result.title = result.title || key;
|
||||
result.description = result.description || '';
|
||||
|
||||
return [key, result];
|
||||
});
|
||||
|
||||
if (plugin.disableToggle) {
|
||||
|
||||
@@ -1,108 +1,39 @@
|
||||
import type { PluginSettings } from './types';
|
||||
|
||||
// Base interfaces for our settings
|
||||
interface BaseSettingOptions {
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface BooleanSettingOptions extends BaseSettingOptions {
|
||||
default: boolean;
|
||||
}
|
||||
|
||||
interface StringSettingOptions extends BaseSettingOptions {
|
||||
default: string;
|
||||
maxLength?: number;
|
||||
pattern?: string;
|
||||
}
|
||||
|
||||
interface NumberSettingOptions extends BaseSettingOptions {
|
||||
default: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
interface SelectSettingOptions<T extends string> extends BaseSettingOptions {
|
||||
default: T;
|
||||
options: readonly T[];
|
||||
}
|
||||
|
||||
// The actual decorators
|
||||
export function BooleanSetting(options: BooleanSettingOptions): PropertyDecorator {
|
||||
return (target: Object, propertyKey: string | symbol) => {
|
||||
// Ensure the settings property exists on the constructor's prototype
|
||||
export function Setting(settingDef: any): PropertyDecorator {
|
||||
return (target, propertyKey) => {
|
||||
const proto = target.constructor.prototype;
|
||||
if (!proto.hasOwnProperty('settings')) {
|
||||
proto.settings = {};
|
||||
Object.defineProperty(proto, 'settings', {
|
||||
value: {},
|
||||
writable: true,
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
});
|
||||
}
|
||||
|
||||
// Add the setting to the prototype's settings object with const assertion
|
||||
proto.settings[propertyKey] = {
|
||||
type: 'boolean' as const,
|
||||
...options
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function StringSetting(options: StringSettingOptions): PropertyDecorator {
|
||||
return (target: Object, propertyKey: string | symbol) => {
|
||||
// Ensure the settings property exists on the constructor's prototype
|
||||
const proto = target.constructor.prototype;
|
||||
if (!proto.hasOwnProperty('settings')) {
|
||||
proto.settings = {};
|
||||
}
|
||||
|
||||
// Add the setting to the prototype's settings object with const assertion
|
||||
proto.settings[propertyKey] = {
|
||||
type: 'string' as const,
|
||||
...options
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function NumberSetting(options: NumberSettingOptions): PropertyDecorator {
|
||||
return (target: Object, propertyKey: string | symbol) => {
|
||||
// Ensure the settings property exists on the constructor's prototype
|
||||
const proto = target.constructor.prototype;
|
||||
if (!proto.hasOwnProperty('settings')) {
|
||||
proto.settings = {};
|
||||
}
|
||||
|
||||
// Add the setting to the prototype's settings object with const assertion
|
||||
proto.settings[propertyKey] = {
|
||||
type: 'number' as const,
|
||||
...options
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function SelectSetting<T extends string>(options: SelectSettingOptions<T>): PropertyDecorator {
|
||||
return (target: Object, propertyKey: string | symbol) => {
|
||||
// Ensure the settings property exists on the constructor's prototype
|
||||
const proto = target.constructor.prototype;
|
||||
if (!proto.hasOwnProperty('settings')) {
|
||||
proto.settings = {};
|
||||
}
|
||||
|
||||
// Add the setting to the prototype's settings object with const assertion
|
||||
proto.settings[propertyKey] = {
|
||||
type: 'select' as const,
|
||||
...options
|
||||
};
|
||||
proto.settings[propertyKey] = settingDef;
|
||||
};
|
||||
}
|
||||
|
||||
// Base plugin class that handles settings
|
||||
export abstract class BasePlugin<T extends PluginSettings = PluginSettings> {
|
||||
// The settings property will be populated by decorators
|
||||
settings!: T;
|
||||
|
||||
// Keep the instance property and constructor logic as is,
|
||||
// as changing it would require changing animated-background/index.ts
|
||||
settings!: T; // Use definite assignment assertion
|
||||
|
||||
constructor() {
|
||||
// Copy settings from the prototype to the instance
|
||||
// This ensures that each instance has its own settings object
|
||||
if (this.constructor.prototype.settings) {
|
||||
// IMPORTANT: Ensure the prototype actually HAS settings before copying
|
||||
if (this.constructor.prototype.hasOwnProperty('settings')) {
|
||||
// Deep clone might be safer if settings objects become complex,
|
||||
// but a shallow clone is usually fine for this structure.
|
||||
this.settings = { ...this.constructor.prototype.settings } as T;
|
||||
} else {
|
||||
// Fallback if decorators somehow didn't run or add the property
|
||||
this.settings = {} as T;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { NumberSetting, BooleanSetting, StringSetting, SelectSetting } from './types';
|
||||
|
||||
export function numberSetting(options: Omit<NumberSetting, 'type'>): NumberSetting {
|
||||
return {
|
||||
type: 'number',
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
export function booleanSetting(options: Omit<BooleanSetting, 'type'>): BooleanSetting {
|
||||
return {
|
||||
type: 'boolean',
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
export function stringSetting(options: Omit<StringSetting, 'type'>): StringSetting {
|
||||
return {
|
||||
type: 'string',
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
export function selectSetting<T extends string>(options: Omit<SelectSetting<T>, 'type'>): SelectSetting<T> {
|
||||
return {
|
||||
type: 'select',
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
export function defineSettings<T extends Record<string, any>>(settings: T): T {
|
||||
return settings;
|
||||
}
|
||||
|
||||
export function Setting(settingDef: any): PropertyDecorator {
|
||||
return (target, propertyKey) => {
|
||||
const proto = target.constructor.prototype;
|
||||
if (!proto.hasOwnProperty('settings')) {
|
||||
Object.defineProperty(proto, 'settings', {
|
||||
value: {},
|
||||
writable: true,
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
});
|
||||
}
|
||||
|
||||
proto.settings[propertyKey] = settingDef;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,8 +52,8 @@ export type SettingsAPI<T extends PluginSettings> = {
|
||||
} & {
|
||||
onChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => { unregister: () => void };
|
||||
offChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => void;
|
||||
loaded: Promise<void>; // Promise that resolves when settings are loaded
|
||||
}
|
||||
loaded: Promise<void>;
|
||||
}
|
||||
|
||||
export interface SEQTAAPI {
|
||||
onMount: (selector: string, callback: (element: Element) => void) => { unregister: () => void };
|
||||
|
||||
Reference in New Issue
Block a user