mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
feat: display plugin settings in interface
This commit is contained in:
+129
-16
@@ -30,7 +30,7 @@ function createSEQTAAPI(): SEQTAAPI {
|
||||
};
|
||||
}
|
||||
|
||||
function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): SettingsAPI<T> {
|
||||
function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): SettingsAPI<T> & { loaded: Promise<void> } {
|
||||
const storageKey = `plugin.${plugin.id}.settings`;
|
||||
const listeners = new Map<keyof T, Set<(value: any) => void>>();
|
||||
let settings: { [K in keyof T]: T[K]['default'] };
|
||||
@@ -41,10 +41,35 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
|
||||
return acc;
|
||||
}, {} as { [K in keyof T]: T[K]['default'] });
|
||||
|
||||
// Load saved settings
|
||||
browser.storage.local.get(storageKey).then((stored) => {
|
||||
if (stored[storageKey]) {
|
||||
Object.assign(settings, stored[storageKey]);
|
||||
// Create a promise that resolves when settings are loaded
|
||||
const loaded = (async () => {
|
||||
try {
|
||||
const stored = await browser.storage.local.get(storageKey);
|
||||
if (stored[storageKey]) {
|
||||
Object.entries(stored[storageKey]).forEach(([key, value]) => {
|
||||
if (key in settings) {
|
||||
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(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[BetterSEQTA+] Error loading settings for plugin ${plugin.id}:`, error);
|
||||
}
|
||||
})();
|
||||
|
||||
// Listen for storage changes
|
||||
browser.storage.onChanged.addListener((changes, area) => {
|
||||
if (area === 'local' && changes[storageKey]) {
|
||||
const newValue = changes[storageKey].newValue;
|
||||
if (newValue) {
|
||||
// Update settings and notify listeners
|
||||
Object.entries(newValue).forEach(([key, value]) => {
|
||||
settings[key as keyof T] = value as any;
|
||||
listeners.get(key as keyof T)?.forEach(callback => callback(value));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -59,32 +84,120 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
|
||||
listeners.get(key)!.add(callback);
|
||||
};
|
||||
}
|
||||
if (prop === 'offChange') {
|
||||
return (key: keyof T, callback: (value: any) => void) => {
|
||||
listeners.get(key)?.delete(callback);
|
||||
};
|
||||
}
|
||||
if (prop === 'loaded') {
|
||||
return loaded;
|
||||
}
|
||||
return target[prop as keyof T];
|
||||
},
|
||||
set(target, prop: string, value: any) {
|
||||
if (prop === 'onChange') return false;
|
||||
if (prop === 'onChange' || prop === 'offChange' || prop === 'loaded') return false;
|
||||
target[prop as keyof T] = value;
|
||||
browser.storage.local.set({ [storageKey]: target });
|
||||
|
||||
// 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(value));
|
||||
return true;
|
||||
},
|
||||
}) as SettingsAPI<T>;
|
||||
}) as SettingsAPI<T> & { loaded: Promise<void> };
|
||||
|
||||
return proxy;
|
||||
}
|
||||
|
||||
function createStorageAPI(pluginId: string): StorageAPI {
|
||||
const prefix = `plugin.${pluginId}.storage.`;
|
||||
const cache: Record<string, any> = {};
|
||||
const listeners = new Map<string, Set<(value: any) => void>>();
|
||||
|
||||
return {
|
||||
get: async <T>(key: string) => {
|
||||
const result = await browser.storage.local.get(prefix + key);
|
||||
return result[prefix + key] as T || null;
|
||||
// Load all existing storage values for this plugin
|
||||
const loadStoragePromise = (async () => {
|
||||
try {
|
||||
const allStorage = await browser.storage.local.get(null);
|
||||
|
||||
// Filter for this plugin's storage keys and populate cache
|
||||
Object.entries(allStorage).forEach(([key, value]) => {
|
||||
if (key.startsWith(prefix)) {
|
||||
const shortKey = key.slice(prefix.length);
|
||||
cache[shortKey] = value;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[BetterSEQTA+] Error loading storage for plugin ${pluginId}:`, error);
|
||||
}
|
||||
})();
|
||||
|
||||
// Listen for storage changes
|
||||
browser.storage.onChanged.addListener((changes, area) => {
|
||||
if (area === 'local') {
|
||||
Object.entries(changes).forEach(([key, change]) => {
|
||||
if (key.startsWith(prefix)) {
|
||||
const shortKey = key.slice(prefix.length);
|
||||
cache[shortKey] = change.newValue;
|
||||
|
||||
// Notify listeners
|
||||
listeners.get(shortKey)?.forEach(callback => callback(change.newValue));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Create the proxy for direct property access
|
||||
return new Proxy(cache, {
|
||||
get(target, prop: string) {
|
||||
if (prop === 'get') {
|
||||
return async <T>(key: string) => {
|
||||
return target[key] as T || null;
|
||||
};
|
||||
}
|
||||
if (prop === 'set') {
|
||||
return async <T>(key: string, value: T) => {
|
||||
target[key] = value;
|
||||
await browser.storage.local.set({ [prefix + key]: value });
|
||||
};
|
||||
}
|
||||
if (prop === 'onChange') {
|
||||
return (key: string, callback: (value: any) => void) => {
|
||||
if (!listeners.has(key)) {
|
||||
listeners.set(key, new Set());
|
||||
}
|
||||
listeners.get(key)!.add(callback);
|
||||
};
|
||||
}
|
||||
if (prop === 'offChange') {
|
||||
return (key: string, callback: (value: any) => void) => {
|
||||
listeners.get(key)?.delete(callback);
|
||||
};
|
||||
}
|
||||
if (prop === 'loaded') {
|
||||
return loadStoragePromise;
|
||||
}
|
||||
|
||||
// Direct property access
|
||||
return target[prop];
|
||||
},
|
||||
set: async <T>(key: string, value: T) => {
|
||||
await browser.storage.local.set({ [prefix + key]: value });
|
||||
},
|
||||
};
|
||||
set(target, prop: string, value: any) {
|
||||
if (['get', 'set', 'onChange', 'offChange', 'loaded'].includes(prop)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update cache and store in browser storage
|
||||
target[prop] = value;
|
||||
browser.storage.local.set({ [prefix + prop]: value });
|
||||
|
||||
// Notify listeners
|
||||
listeners.get(prop)?.forEach(callback => callback(value));
|
||||
|
||||
return true;
|
||||
}
|
||||
}) as StorageAPI;
|
||||
}
|
||||
|
||||
function createEventsAPI(pluginId: string): EventsAPI {
|
||||
|
||||
@@ -21,11 +21,10 @@ export class PluginManager {
|
||||
public dispatchPluginEvent(pluginId: string, event: string, args?: any) {
|
||||
const fullEventName = `plugin.${pluginId}.${event}`;
|
||||
|
||||
// Dispatch plugin event if it's running otherwise queue it
|
||||
if (this.runningPlugins.get(pluginId)) {
|
||||
// If plugin is running, dispatch immediately
|
||||
document.dispatchEvent(new CustomEvent(fullEventName, { detail: args }));
|
||||
} else {
|
||||
// Otherwise queue it
|
||||
const key = `${pluginId}:${event}`;
|
||||
if (!this.eventBacklog.has(key)) {
|
||||
this.eventBacklog.set(key, []);
|
||||
@@ -66,6 +65,13 @@ export class PluginManager {
|
||||
|
||||
try {
|
||||
const api = createPluginAPI(plugin);
|
||||
|
||||
// Wait for both settings and storage to be loaded before starting the plugin
|
||||
await Promise.all([
|
||||
(api.settings as any).loaded,
|
||||
api.storage.loaded
|
||||
]);
|
||||
|
||||
const result = await plugin.run(api);
|
||||
if (typeof result === 'function') {
|
||||
this.cleanupFunctions.set(plugin.id, result);
|
||||
@@ -115,6 +121,38 @@ export class PluginManager {
|
||||
return Array.from(this.plugins.values());
|
||||
}
|
||||
|
||||
public getAllPluginSettings(): Array<{
|
||||
pluginId: string;
|
||||
name: string;
|
||||
settings: {
|
||||
[key: string]: {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
type: string;
|
||||
default: any;
|
||||
}
|
||||
}
|
||||
}> {
|
||||
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
|
||||
}];
|
||||
});
|
||||
|
||||
return {
|
||||
pluginId: id,
|
||||
name: plugin.name,
|
||||
settings: Object.fromEntries(settingsEntries)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public isPluginRunning(pluginId: string): boolean {
|
||||
return this.runningPlugins.get(pluginId) || false;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ interface SelectSetting<T extends string> {
|
||||
|
||||
type PluginSetting = BooleanSetting | StringSetting | NumberSetting | SelectSetting<string>;
|
||||
|
||||
// Plugin settings configuration
|
||||
export type PluginSettings = {
|
||||
[key: string]: PluginSetting;
|
||||
}
|
||||
@@ -43,15 +42,14 @@ type SettingValue<T extends PluginSetting> = T extends BooleanSetting ? boolean
|
||||
T extends SelectSetting<infer O> ? O :
|
||||
never;
|
||||
|
||||
// Settings API interface
|
||||
export type SettingsAPI<T extends PluginSettings> = {
|
||||
[K in keyof T]: SettingValue<T[K]>;
|
||||
} & {
|
||||
onChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => 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
|
||||
}
|
||||
|
||||
// SEQTA API interface
|
||||
export interface SEQTAAPI {
|
||||
onMount: (selector: string, callback: (element: Element) => void) => void;
|
||||
getFiber: (selector: string) => ReactFiber;
|
||||
@@ -59,19 +57,20 @@ export interface SEQTAAPI {
|
||||
onPageChange: (callback: (page: string) => void) => void;
|
||||
}
|
||||
|
||||
// Storage API interface
|
||||
export interface StorageAPI {
|
||||
get: <T>(key: string) => Promise<T | null>;
|
||||
set: <T>(key: string, value: T) => Promise<void>;
|
||||
onChange: (key: string, callback: (value: any) => void) => void;
|
||||
offChange: (key: string, callback: (value: any) => void) => void;
|
||||
loaded: Promise<void>;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// Events API interface
|
||||
export interface EventsAPI {
|
||||
on: (event: string, callback: (...args: any[]) => void) => void;
|
||||
emit: (event: string, ...args: any[]) => void;
|
||||
}
|
||||
|
||||
// Complete Plugin API interface
|
||||
export interface PluginAPI<T extends PluginSettings> {
|
||||
seqta: SEQTAAPI;
|
||||
settings: SettingsAPI<T>;
|
||||
@@ -79,7 +78,6 @@ export interface PluginAPI<T extends PluginSettings> {
|
||||
events: EventsAPI;
|
||||
}
|
||||
|
||||
// Plugin interface
|
||||
export interface Plugin<T extends PluginSettings = PluginSettings> {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
Reference in New Issue
Block a user