mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
format: run prettify
This commit is contained in:
+109
-62
@@ -1,7 +1,16 @@
|
||||
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';
|
||||
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";
|
||||
|
||||
function createSEQTAAPI(): SEQTAAPI {
|
||||
return {
|
||||
@@ -11,41 +20,46 @@ function createSEQTAAPI(): SEQTAAPI {
|
||||
{
|
||||
customCheck: (element) => element.matches(selector),
|
||||
},
|
||||
callback
|
||||
callback,
|
||||
);
|
||||
},
|
||||
getFiber: (selector) => {
|
||||
return ReactFiber.find(selector);
|
||||
},
|
||||
getCurrentPage: () => {
|
||||
const path = window.location.hash.split('?page=/')[1] || '';
|
||||
return path.split('/')[0];
|
||||
const path = window.location.hash.split("?page=/")[1] || "";
|
||||
return path.split("/")[0];
|
||||
},
|
||||
onPageChange: (callback) => {
|
||||
const handler = () => {
|
||||
const page = window.location.hash.split('?page=/')[1] || '';
|
||||
callback(page.split('/')[0]);
|
||||
const page = window.location.hash.split("?page=/")[1] || "";
|
||||
callback(page.split("/")[0]);
|
||||
};
|
||||
|
||||
window.addEventListener('hashchange', handler);
|
||||
|
||||
|
||||
window.addEventListener("hashchange", handler);
|
||||
|
||||
// Return an unregister function
|
||||
return {
|
||||
unregister: () => {
|
||||
window.removeEventListener('hashchange', handler);
|
||||
}
|
||||
window.removeEventListener("hashchange", handler);
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): SettingsAPI<T> & { loaded: Promise<void> } {
|
||||
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>>();
|
||||
|
||||
// Initialize with default values
|
||||
const settingsWithMeta: any = {
|
||||
onChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => {
|
||||
onChange: <K extends keyof T>(
|
||||
key: K,
|
||||
callback: (value: SettingValue<T[K]>) => void,
|
||||
) => {
|
||||
if (!listeners.has(key)) {
|
||||
listeners.set(key, new Set());
|
||||
}
|
||||
@@ -53,13 +67,16 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
|
||||
return {
|
||||
unregister: () => {
|
||||
listeners.get(key)!.delete(callback);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
offChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => {
|
||||
offChange: <K extends keyof T>(
|
||||
key: K,
|
||||
callback: (value: SettingValue<T[K]>) => void,
|
||||
) => {
|
||||
listeners.get(key)?.delete(callback);
|
||||
},
|
||||
loaded: Promise.resolve() // will be replaced below
|
||||
loaded: Promise.resolve(), // will be replaced below
|
||||
};
|
||||
|
||||
// Fill with defaults first
|
||||
@@ -71,33 +88,45 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
|
||||
const loaded = (async () => {
|
||||
try {
|
||||
const stored = await browser.storage.local.get(storageKey);
|
||||
const storedSettings = stored[storageKey] as Partial<Record<keyof T, any>>;
|
||||
const storedSettings = stored[storageKey] as Partial<
|
||||
Record<keyof T, any>
|
||||
>;
|
||||
if (storedSettings) {
|
||||
for (const key in storedSettings) {
|
||||
if (key in settingsWithMeta) {
|
||||
settingsWithMeta[key] = storedSettings[key];
|
||||
listeners.get(key as keyof T)?.forEach(cb => cb(storedSettings[key]));
|
||||
listeners
|
||||
.get(key as keyof T)
|
||||
?.forEach((cb) => cb(storedSettings[key]));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[BetterSEQTA+] Error loading settings for plugin ${plugin.id}:`, error);
|
||||
console.error(
|
||||
`[BetterSEQTA+] Error loading settings for plugin ${plugin.id}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
settingsWithMeta.loaded = loaded;
|
||||
|
||||
// Listen for storage changes and update settingsWithMeta
|
||||
const handleStorageChange = (changes: { [key: string]: browser.Storage.StorageChange }, area: string) => {
|
||||
if (area !== 'local' || !(storageKey in changes)) return;
|
||||
const handleStorageChange = (
|
||||
changes: { [key: string]: browser.Storage.StorageChange },
|
||||
area: string,
|
||||
) => {
|
||||
if (area !== "local" || !(storageKey in changes)) return;
|
||||
|
||||
const newValue = changes[storageKey].newValue as Partial<Record<keyof T, any>> | undefined;
|
||||
const newValue = changes[storageKey].newValue as
|
||||
| Partial<Record<keyof T, any>>
|
||||
| undefined;
|
||||
if (!newValue) return;
|
||||
|
||||
for (const key in newValue) {
|
||||
const typedKey = key as keyof T;
|
||||
settingsWithMeta[typedKey] = newValue[typedKey];
|
||||
listeners.get(typedKey)?.forEach(cb => cb(newValue[typedKey]));
|
||||
listeners.get(typedKey)?.forEach((cb) => cb(newValue[typedKey]));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -108,7 +137,8 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
|
||||
return target[prop];
|
||||
},
|
||||
set(target, prop, value) {
|
||||
if (['onChange', 'offChange', 'loaded'].includes(prop as string)) return false;
|
||||
if (["onChange", "offChange", "loaded"].includes(prop as string))
|
||||
return false;
|
||||
|
||||
target[prop] = value;
|
||||
|
||||
@@ -120,25 +150,29 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
|
||||
|
||||
browser.storage.local.set({ [storageKey]: dataToStore });
|
||||
|
||||
listeners.get(prop as keyof T)?.forEach(cb => cb(value));
|
||||
listeners.get(prop as keyof T)?.forEach((cb) => cb(value));
|
||||
return true;
|
||||
}
|
||||
},
|
||||
}) as SettingsAPI<T> & { loaded: Promise<void> };
|
||||
|
||||
return proxy;
|
||||
}
|
||||
|
||||
function createStorageAPI<T = any>(pluginId: string): StorageAPI<T> & { [K in keyof T]: T[K] } {
|
||||
function createStorageAPI<T = any>(
|
||||
pluginId: string,
|
||||
): StorageAPI<T> & { [K in keyof T]: T[K] } {
|
||||
const prefix = `plugin.${pluginId}.storage.`;
|
||||
const cache: Record<string, any> = {};
|
||||
const listeners = new Map<string, Set<(value: any) => void>>();
|
||||
const storageListeners = new Set<(changes: { [key: string]: any }, area: string) => void>();
|
||||
|
||||
const storageListeners = new Set<
|
||||
(changes: { [key: string]: any }, area: string) => void
|
||||
>();
|
||||
|
||||
// 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)) {
|
||||
@@ -147,31 +181,39 @@ function createStorageAPI<T = any>(pluginId: string): StorageAPI<T> & { [K in ke
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[BetterSEQTA+] Error loading storage for plugin ${pluginId}:`, error);
|
||||
console.error(
|
||||
`[BetterSEQTA+] Error loading storage for plugin ${pluginId}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
// Listen for storage changes
|
||||
const handleStorageChange = (changes: { [key: string]: any }, area: string) => {
|
||||
if (area === 'local') {
|
||||
const handleStorageChange = (
|
||||
changes: { [key: string]: any },
|
||||
area: string,
|
||||
) => {
|
||||
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));
|
||||
listeners
|
||||
.get(shortKey)
|
||||
?.forEach((callback) => callback(change.newValue));
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
browser.storage.onChanged.addListener(handleStorageChange);
|
||||
storageListeners.add(handleStorageChange);
|
||||
|
||||
|
||||
// Create the proxy for direct property access
|
||||
return new Proxy(cache, {
|
||||
get(target, prop: string) {
|
||||
if (prop === 'onChange') {
|
||||
if (prop === "onChange") {
|
||||
return (key: keyof T, callback: (value: T[keyof T]) => void) => {
|
||||
if (!listeners.has(key as string)) {
|
||||
listeners.set(key as string, new Set());
|
||||
@@ -180,79 +222,84 @@ function createStorageAPI<T = any>(pluginId: string): StorageAPI<T> & { [K in ke
|
||||
return {
|
||||
unregister: () => {
|
||||
listeners.get(key as string)?.delete(callback);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
if (prop === 'offChange') {
|
||||
if (prop === "offChange") {
|
||||
return (key: keyof T, callback: (value: T[keyof T]) => void) => {
|
||||
listeners.get(key as string)?.delete(callback);
|
||||
};
|
||||
}
|
||||
if (prop === 'loaded') {
|
||||
if (prop === "loaded") {
|
||||
return loadStoragePromise;
|
||||
}
|
||||
|
||||
|
||||
// Direct property access
|
||||
return target[prop];
|
||||
},
|
||||
set(target, prop: string, value: any) {
|
||||
if (['onChange', 'offChange', 'loaded'].includes(prop)) {
|
||||
if (["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));
|
||||
|
||||
listeners.get(prop)?.forEach((callback) => callback(value));
|
||||
|
||||
return true;
|
||||
}
|
||||
},
|
||||
}) as StorageAPI<T> & { [K in keyof T]: T[K] };
|
||||
}
|
||||
|
||||
function createEventsAPI(pluginId: string): EventsAPI {
|
||||
const prefix = `plugin.${pluginId}.`;
|
||||
const eventListeners = new Map<string, Set<{ callback: (...args: any[]) => void, listener: EventListener }>>();
|
||||
|
||||
const eventListeners = new Map<
|
||||
string,
|
||||
Set<{ callback: (...args: any[]) => void; listener: EventListener }>
|
||||
>();
|
||||
|
||||
return {
|
||||
on: (event, callback) => {
|
||||
const fullEventName = prefix + event;
|
||||
const listener = ((e: CustomEvent) => {
|
||||
callback(...(e.detail || []));
|
||||
}) as EventListener;
|
||||
|
||||
|
||||
document.addEventListener(fullEventName, listener);
|
||||
|
||||
|
||||
if (!eventListeners.has(event)) {
|
||||
eventListeners.set(event, new Set());
|
||||
}
|
||||
eventListeners.get(event)!.add({ callback, listener });
|
||||
|
||||
|
||||
return {
|
||||
unregister: () => {
|
||||
document.removeEventListener(fullEventName, listener);
|
||||
eventListeners.get(event)?.delete({ callback, listener });
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
emit: (event, ...args) => {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(prefix + event, {
|
||||
detail: args.length > 0 ? args : null
|
||||
})
|
||||
detail: args.length > 0 ? args : null,
|
||||
}),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createPluginAPI<T extends PluginSettings, S = any>(plugin: Plugin<T, S>): PluginAPI<T, S> {
|
||||
export function createPluginAPI<T extends PluginSettings, S = any>(
|
||||
plugin: Plugin<T, S>,
|
||||
): PluginAPI<T, S> {
|
||||
return {
|
||||
seqta: createSEQTAAPI(),
|
||||
settings: createSettingsAPI(plugin),
|
||||
storage: createStorageAPI<S>(plugin.id),
|
||||
events: createEventsAPI(plugin.id),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+108
-74
@@ -1,6 +1,13 @@
|
||||
import type { BooleanSetting, NumberSetting, Plugin, PluginSettings, SelectSetting, StringSetting } from './types';
|
||||
import { createPluginAPI } from './createAPI';
|
||||
import browser from 'webextension-polyfill';
|
||||
import type {
|
||||
BooleanSetting,
|
||||
NumberSetting,
|
||||
Plugin,
|
||||
PluginSettings,
|
||||
SelectSetting,
|
||||
StringSetting,
|
||||
} from "./types";
|
||||
import { createPluginAPI } from "./createAPI";
|
||||
import browser from "webextension-polyfill";
|
||||
|
||||
interface PluginSettingsStorage {
|
||||
enabled?: boolean;
|
||||
@@ -34,7 +41,7 @@ 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)) {
|
||||
document.dispatchEvent(new CustomEvent(fullEventName, { detail: args }));
|
||||
@@ -49,7 +56,7 @@ export class PluginManager {
|
||||
|
||||
private async processBackloggedEvents(pluginId: string) {
|
||||
for (const [key, argsList] of this.eventBacklog.entries()) {
|
||||
const [eventPluginId, event] = key.split(':');
|
||||
const [eventPluginId, event] = key.split(":");
|
||||
if (eventPluginId === pluginId) {
|
||||
for (const args of argsList) {
|
||||
this.dispatchPluginEvent(pluginId, event, args);
|
||||
@@ -59,7 +66,9 @@ export class PluginManager {
|
||||
}
|
||||
}
|
||||
|
||||
public registerPlugin<T extends PluginSettings, S>(plugin: Plugin<T, S>): void {
|
||||
public registerPlugin<T extends PluginSettings, S>(
|
||||
plugin: Plugin<T, S>,
|
||||
): void {
|
||||
if (this.plugins.has(plugin.id)) {
|
||||
throw new Error(`Plugin with id "${plugin.id}" is already registered`);
|
||||
}
|
||||
@@ -79,53 +88,60 @@ export class PluginManager {
|
||||
|
||||
try {
|
||||
const api = createPluginAPI(plugin);
|
||||
|
||||
|
||||
// Check if plugin is enabled before starting
|
||||
if (plugin.disableToggle) {
|
||||
const settings = await browser.storage.local.get(`plugin.${pluginId}.settings`);
|
||||
const pluginSettings = settings[`plugin.${pluginId}.settings`] as PluginSettingsStorage | undefined;
|
||||
const enabled = pluginSettings?.enabled ?? plugin.defaultEnabled ?? true;
|
||||
const settings = await browser.storage.local.get(
|
||||
`plugin.${pluginId}.settings`,
|
||||
);
|
||||
const pluginSettings = settings[`plugin.${pluginId}.settings`] as
|
||||
| PluginSettingsStorage
|
||||
| undefined;
|
||||
const enabled =
|
||||
pluginSettings?.enabled ?? plugin.defaultEnabled ?? true;
|
||||
if (!enabled) {
|
||||
console.info(`Plugin "${pluginId}" is disabled, skipping initialization`);
|
||||
console.info(
|
||||
`Plugin "${pluginId}" is disabled, skipping initialization`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Inject plugin styles if provided
|
||||
if (plugin.styles) {
|
||||
const styleElement = document.createElement('style');
|
||||
const styleElement = document.createElement("style");
|
||||
styleElement.textContent = plugin.styles;
|
||||
document.head.appendChild(styleElement);
|
||||
this.styleElements.set(pluginId, styleElement);
|
||||
}
|
||||
|
||||
|
||||
// Wait for both settings and storage to be loaded before starting the plugin
|
||||
await Promise.all([
|
||||
(api.settings as any).loaded,
|
||||
api.storage.loaded
|
||||
]);
|
||||
|
||||
await Promise.all([(api.settings as any).loaded, api.storage.loaded]);
|
||||
|
||||
const result = await plugin.run(api);
|
||||
if (typeof result === 'function') {
|
||||
if (typeof result === "function") {
|
||||
this.cleanupFunctions.set(plugin.id, result);
|
||||
}
|
||||
this.runningPlugins.set(pluginId, true);
|
||||
console.info(`Plugin "${pluginId}" started successfully`);
|
||||
|
||||
|
||||
// Process any backlogged events
|
||||
await this.processBackloggedEvents(pluginId);
|
||||
} catch (error) {
|
||||
console.error(`[BetterSEQTA+] Failed to start plugin ${pluginId}:`, error);
|
||||
console.error(
|
||||
`[BetterSEQTA+] Failed to start plugin ${pluginId}:`,
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async startAllPlugins(): Promise<void> {
|
||||
const startPromises = Array.from(this.plugins.keys()).map(id =>
|
||||
this.startPlugin(id).catch(error => {
|
||||
const startPromises = Array.from(this.plugins.keys()).map((id) =>
|
||||
this.startPlugin(id).catch((error) => {
|
||||
console.error(`Failed to start plugin "${id}":`, error);
|
||||
return Promise.reject(error);
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.allSettled(startPromises);
|
||||
@@ -146,11 +162,11 @@ export class PluginManager {
|
||||
}
|
||||
this.runningPlugins.set(pluginId, false);
|
||||
console.info(`Plugin "${pluginId}" stopped`);
|
||||
this.emit('plugin.stopped', pluginId);
|
||||
this.emit("plugin.stopped", pluginId);
|
||||
}
|
||||
|
||||
public stopAllPlugins(): void {
|
||||
Array.from(this.plugins.keys()).forEach(id => this.stopPlugin(id));
|
||||
Array.from(this.plugins.keys()).forEach((id) => this.stopPlugin(id));
|
||||
}
|
||||
|
||||
public getPlugin(pluginId: string): Plugin | undefined {
|
||||
@@ -166,40 +182,49 @@ export class PluginManager {
|
||||
name: string;
|
||||
description: string;
|
||||
settings: {
|
||||
[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 }> });
|
||||
}
|
||||
[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]) => {
|
||||
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 || '';
|
||||
result.defaultEnabled = plugin.defaultEnabled ?? true;
|
||||
|
||||
return [key, result];
|
||||
});
|
||||
const settingsEntries = Object.entries(plugin.settings).map(
|
||||
([key, setting]) => {
|
||||
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 || "";
|
||||
result.defaultEnabled = plugin.defaultEnabled ?? true;
|
||||
|
||||
return [key, result];
|
||||
},
|
||||
);
|
||||
|
||||
if (plugin.disableToggle) {
|
||||
settingsEntries.push([
|
||||
'enabled', {
|
||||
id: 'enabled',
|
||||
"enabled",
|
||||
{
|
||||
id: "enabled",
|
||||
title: plugin.name,
|
||||
description: plugin.description,
|
||||
type: 'boolean',
|
||||
default: plugin.defaultEnabled ?? true
|
||||
}
|
||||
])
|
||||
type: "boolean",
|
||||
default: plugin.defaultEnabled ?? true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
return {
|
||||
pluginId: id,
|
||||
@@ -218,7 +243,7 @@ export class PluginManager {
|
||||
private emit(event: string, ...args: any[]): void {
|
||||
const listeners = this.listeners.get(event);
|
||||
if (listeners) {
|
||||
listeners.forEach(listener => listener(...args));
|
||||
listeners.forEach((listener) => listener(...args));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,7 +262,10 @@ export class PluginManager {
|
||||
}
|
||||
|
||||
// Add handler for plugin enable/disable state changes
|
||||
private async handlePluginStateChange(pluginId: string, enabled: boolean): Promise<void> {
|
||||
private async handlePluginStateChange(
|
||||
pluginId: string,
|
||||
enabled: boolean,
|
||||
): Promise<void> {
|
||||
if (enabled) {
|
||||
await this.startPlugin(pluginId);
|
||||
} else {
|
||||
@@ -247,24 +275,30 @@ export class PluginManager {
|
||||
|
||||
// Add listener for plugin settings changes
|
||||
private setupPluginStateListener(): void {
|
||||
browser.storage.onChanged.addListener((changes: { [key: string]: StorageChange }, area: string) => {
|
||||
if (area !== 'local') return;
|
||||
|
||||
for (const [key, change] of Object.entries(changes)) {
|
||||
const match = key.match(/^plugin\.(.+)\.settings$/);
|
||||
if (!match) continue;
|
||||
|
||||
const pluginId = match[1];
|
||||
const plugin = this.plugins.get(pluginId);
|
||||
if (!plugin?.disableToggle) continue;
|
||||
|
||||
const enabled = (change.newValue as PluginSettingsStorage)?.enabled ?? true;
|
||||
const wasEnabled = (change.oldValue as PluginSettingsStorage)?.enabled ?? plugin.defaultEnabled ?? true;
|
||||
|
||||
if (enabled !== wasEnabled) {
|
||||
this.handlePluginStateChange(pluginId, enabled);
|
||||
browser.storage.onChanged.addListener(
|
||||
(changes: { [key: string]: StorageChange }, area: string) => {
|
||||
if (area !== "local") return;
|
||||
|
||||
for (const [key, change] of Object.entries(changes)) {
|
||||
const match = key.match(/^plugin\.(.+)\.settings$/);
|
||||
if (!match) continue;
|
||||
|
||||
const pluginId = match[1];
|
||||
const plugin = this.plugins.get(pluginId);
|
||||
if (!plugin?.disableToggle) continue;
|
||||
|
||||
const enabled =
|
||||
(change.newValue as PluginSettingsStorage)?.enabled ?? true;
|
||||
const wasEnabled =
|
||||
(change.oldValue as PluginSettingsStorage)?.enabled ??
|
||||
plugin.defaultEnabled ??
|
||||
true;
|
||||
|
||||
if (enabled !== wasEnabled) {
|
||||
this.handlePluginStateChange(pluginId, enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { PluginSettings } from './types';
|
||||
import type { PluginSettings } from "./types";
|
||||
|
||||
export function Setting(settingDef: any): PropertyDecorator {
|
||||
return (target, propertyKey) => {
|
||||
const proto = target.constructor.prototype;
|
||||
if (!proto.hasOwnProperty('settings')) {
|
||||
Object.defineProperty(proto, 'settings', {
|
||||
if (!proto.hasOwnProperty("settings")) {
|
||||
Object.defineProperty(proto, "settings", {
|
||||
value: {},
|
||||
writable: true,
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
enumerable: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export abstract class BasePlugin<T extends PluginSettings = PluginSettings> {
|
||||
// Copy settings from the prototype to the instance
|
||||
// This ensures that each instance has its own settings object
|
||||
// IMPORTANT: Ensure the prototype actually HAS settings before copying
|
||||
if (this.constructor.prototype.hasOwnProperty('settings')) {
|
||||
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;
|
||||
@@ -36,4 +36,4 @@ export abstract class BasePlugin<T extends PluginSettings = PluginSettings> {
|
||||
this.settings = {} as T;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,43 @@
|
||||
import type { BooleanSetting, NumberSetting, SelectSetting, StringSetting } from './types';
|
||||
import type {
|
||||
BooleanSetting,
|
||||
NumberSetting,
|
||||
SelectSetting,
|
||||
StringSetting,
|
||||
} from "./types";
|
||||
|
||||
export function numberSetting(options: Omit<NumberSetting, 'type'>): NumberSetting {
|
||||
export function numberSetting(
|
||||
options: Omit<NumberSetting, "type">,
|
||||
): NumberSetting {
|
||||
return {
|
||||
type: 'number',
|
||||
...options
|
||||
type: "number",
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
export function booleanSetting(options: Omit<BooleanSetting, 'type'>): BooleanSetting {
|
||||
export function booleanSetting(
|
||||
options: Omit<BooleanSetting, "type">,
|
||||
): BooleanSetting {
|
||||
return {
|
||||
type: 'boolean',
|
||||
...options
|
||||
type: "boolean",
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
export function stringSetting(options: Omit<StringSetting, 'type'>): StringSetting {
|
||||
export function stringSetting(
|
||||
options: Omit<StringSetting, "type">,
|
||||
): StringSetting {
|
||||
return {
|
||||
type: 'string',
|
||||
...options
|
||||
type: "string",
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
export function selectSetting<T extends string>(options: Omit<SelectSetting<T>, 'type'>): SelectSetting<T> {
|
||||
export function selectSetting<T extends string>(
|
||||
options: Omit<SelectSetting<T>, "type">,
|
||||
): SelectSetting<T> {
|
||||
return {
|
||||
type: 'select',
|
||||
...options
|
||||
type: "select",
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,16 +48,15 @@ export function defineSettings<T extends Record<string, any>>(settings: T): T {
|
||||
export function Setting(settingDef: any): PropertyDecorator {
|
||||
return (target, propertyKey) => {
|
||||
const proto = target.constructor.prototype;
|
||||
if (!proto.hasOwnProperty('settings')) {
|
||||
Object.defineProperty(proto, 'settings', {
|
||||
if (!proto.hasOwnProperty("settings")) {
|
||||
Object.defineProperty(proto, "settings", {
|
||||
value: {},
|
||||
writable: true,
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
enumerable: true,
|
||||
});
|
||||
}
|
||||
|
||||
proto.settings[propertyKey] = settingDef;
|
||||
};
|
||||
}
|
||||
|
||||
+53
-26
@@ -1,14 +1,14 @@
|
||||
import ReactFiber from '@/seqta/utils/ReactFiber';
|
||||
import ReactFiber from "@/seqta/utils/ReactFiber";
|
||||
|
||||
export interface BooleanSetting {
|
||||
type: 'boolean';
|
||||
type: "boolean";
|
||||
default: boolean;
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface StringSetting {
|
||||
type: 'string';
|
||||
type: "string";
|
||||
default: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
@@ -17,7 +17,7 @@ export interface StringSetting {
|
||||
}
|
||||
|
||||
export interface NumberSetting {
|
||||
type: 'number';
|
||||
type: "number";
|
||||
default: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
@@ -27,47 +27,69 @@ export interface NumberSetting {
|
||||
}
|
||||
|
||||
export interface SelectSetting<T extends string> {
|
||||
type: 'select';
|
||||
type: "select";
|
||||
options: readonly T[];
|
||||
default: T;
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export type PluginSetting = BooleanSetting | StringSetting | NumberSetting | SelectSetting<string>;
|
||||
export type PluginSetting =
|
||||
| BooleanSetting
|
||||
| StringSetting
|
||||
| NumberSetting
|
||||
| SelectSetting<string>;
|
||||
|
||||
export type PluginSettings = {
|
||||
[key: string]: PluginSetting;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper type to extract the actual value type from a setting
|
||||
export type SettingValue<T extends PluginSetting> = T extends BooleanSetting ? boolean :
|
||||
T extends StringSetting ? string :
|
||||
T extends NumberSetting ? number :
|
||||
T extends SelectSetting<infer O> ? O :
|
||||
never;
|
||||
export type SettingValue<T extends PluginSetting> = T extends BooleanSetting
|
||||
? boolean
|
||||
: T extends StringSetting
|
||||
? string
|
||||
: T extends NumberSetting
|
||||
? number
|
||||
: T extends SelectSetting<infer O>
|
||||
? O
|
||||
: never;
|
||||
|
||||
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) => { unregister: () => void };
|
||||
offChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => void;
|
||||
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>;
|
||||
}
|
||||
};
|
||||
|
||||
export interface SEQTAAPI {
|
||||
onMount: (selector: string, callback: (element: Element) => void) => { unregister: () => void };
|
||||
onMount: (
|
||||
selector: string,
|
||||
callback: (element: Element) => void,
|
||||
) => { unregister: () => void };
|
||||
getFiber: (selector: string) => ReactFiber;
|
||||
getCurrentPage: () => string;
|
||||
onPageChange: (callback: (page: string) => void) => { unregister: () => void };
|
||||
onPageChange: (callback: (page: string) => void) => {
|
||||
unregister: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export interface StorageAPI<T = any> {
|
||||
/**
|
||||
* Register a callback to be called when a storage value changes
|
||||
*/
|
||||
onChange: <K extends keyof T>(key: K, callback: (value: T[K]) => void) => { unregister: () => void };
|
||||
|
||||
onChange: <K extends keyof T>(
|
||||
key: K,
|
||||
callback: (value: T[K]) => void,
|
||||
) => { unregister: () => void };
|
||||
|
||||
/**
|
||||
* Promise that resolves when storage values are loaded
|
||||
*/
|
||||
@@ -76,10 +98,13 @@ export interface StorageAPI<T = any> {
|
||||
|
||||
export type TypedStorageAPI<T> = StorageAPI<T> & {
|
||||
[K in keyof T]: T[K];
|
||||
}
|
||||
};
|
||||
|
||||
export interface EventsAPI {
|
||||
on: (event: string, callback: (...args: any[]) => void) => { unregister: () => void };
|
||||
on: (
|
||||
event: string,
|
||||
callback: (...args: any[]) => void,
|
||||
) => { unregister: () => void };
|
||||
emit: (event: string, ...args: any[]) => void;
|
||||
}
|
||||
|
||||
@@ -96,8 +121,10 @@ export interface Plugin<T extends PluginSettings = PluginSettings, S = any> {
|
||||
description: string;
|
||||
version: string;
|
||||
settings: T;
|
||||
styles?: string; // Optional CSS styles for the plugin
|
||||
disableToggle?: boolean; // Optional flag to show/hide the plugin's enable/disable toggle in settings
|
||||
defaultEnabled?: boolean; // Optional flag to set the plugin's default enabled state
|
||||
run: (api: PluginAPI<T, S>) => void | Promise<void> | (() => void) | Promise<(() => void)>;
|
||||
}
|
||||
styles?: string; // Optional CSS styles for the plugin
|
||||
disableToggle?: boolean; // Optional flag to show/hide the plugin's enable/disable toggle in settings
|
||||
defaultEnabled?: boolean; // Optional flag to set the plugin's default enabled state
|
||||
run: (
|
||||
api: PluginAPI<T, S>,
|
||||
) => void | Promise<void> | (() => void) | Promise<() => void>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user