mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 11:44:40 +00:00
dev
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import type { Plugin } from '../../core/types';
|
||||
import styles from './styles.css?inline';
|
||||
import { BasePlugin, NumberSetting } from '../../core/settings';
|
||||
|
||||
class AnimatedBackgroundPluginClass extends BasePlugin {
|
||||
@NumberSetting({
|
||||
default: 1,
|
||||
title: "Animation Speed",
|
||||
description: "Controls the speed of the animated background",
|
||||
min: 0.1,
|
||||
max: 2
|
||||
})
|
||||
speed!: number;
|
||||
}
|
||||
|
||||
const settingsInstance = new AnimatedBackgroundPluginClass();
|
||||
|
||||
const animatedBackgroundPlugin: Plugin<typeof settingsInstance.settings> = {
|
||||
id: 'animated-background',
|
||||
name: 'Animated Background',
|
||||
description: 'Adds an animated background to BetterSEQTA+',
|
||||
version: '1.0.0',
|
||||
disableToggle: true,
|
||||
styles,
|
||||
settings: settingsInstance.settings,
|
||||
|
||||
run: async (api) => {
|
||||
// Create the background elements
|
||||
const container = document.getElementById("container");
|
||||
const menu = document.getElementById("menu");
|
||||
|
||||
if (!container || !menu) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const backgrounds = [
|
||||
{ classes: ["bg"] },
|
||||
{ classes: ["bg", "bg2"] },
|
||||
{ classes: ["bg", "bg3"] }
|
||||
];
|
||||
|
||||
backgrounds.forEach(({ classes }) => {
|
||||
const bk = document.createElement("div");
|
||||
classes.forEach(cls => bk.classList.add(cls));
|
||||
container.insertBefore(bk, menu);
|
||||
});
|
||||
|
||||
// Set initial speed
|
||||
updateAnimationSpeed(api.settings.speed);
|
||||
|
||||
// Listen for speed changes
|
||||
const speedUnregister = api.settings.onChange('speed', updateAnimationSpeed);
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
speedUnregister.unregister();
|
||||
// Remove background elements
|
||||
const backgrounds = document.getElementsByClassName('bg');
|
||||
Array.from(backgrounds).forEach(element => element.remove());
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
function updateAnimationSpeed(speed: number) {
|
||||
const bgElements = document.getElementsByClassName('bg');
|
||||
Array.from(bgElements).forEach((element, index) => {
|
||||
const baseSpeed = index === 0 ? 3 : index === 1 ? 4 : 5;
|
||||
(element as HTMLElement).style.animationDuration = `${baseSpeed / speed}s`;
|
||||
});
|
||||
}
|
||||
|
||||
export default animatedBackgroundPlugin;
|
||||
@@ -0,0 +1,31 @@
|
||||
.bg {
|
||||
animation: slide 3s ease-in-out infinite alternate;
|
||||
background: var(--better-main);
|
||||
bottom: 0;
|
||||
left: -50%;
|
||||
opacity: 0.5;
|
||||
position: fixed;
|
||||
right: -50%;
|
||||
top: 0;
|
||||
z-index: 0 !important;
|
||||
overflow: hidden;
|
||||
scale: 1.5;
|
||||
}
|
||||
|
||||
.bg2 {
|
||||
animation-direction: alternate-reverse;
|
||||
animation-duration: 4s;
|
||||
}
|
||||
|
||||
.bg3 {
|
||||
animation-duration: 5s;
|
||||
}
|
||||
|
||||
@keyframes slide {
|
||||
0% {
|
||||
transform: translate(50%) rotate(-60deg);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(5%) rotate(-60deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export function CreateBackground() {
|
||||
const bkCheck = document.getElementsByClassName("bg");
|
||||
if (bkCheck.length !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Creating and inserting 3 divs containing the background applied to the pages
|
||||
const container = document.getElementById("container");
|
||||
const menu = document.getElementById("menu");
|
||||
|
||||
if (!container || !menu) return;
|
||||
|
||||
const backgrounds = [
|
||||
{ classes: ["bg"] },
|
||||
{ classes: ["bg", "bg2"] },
|
||||
{ classes: ["bg", "bg3"] }
|
||||
];
|
||||
|
||||
backgrounds.forEach(({ classes }) => {
|
||||
const bk = document.createElement("div");
|
||||
classes.forEach(cls => bk.classList.add(cls));
|
||||
container.insertBefore(bk, menu);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export function RemoveBackground() {
|
||||
const backgrounds = document.getElementsByClassName("bg");
|
||||
|
||||
// Convert HTMLCollection to Array and remove each element
|
||||
Array.from(backgrounds).forEach(element => element.remove());
|
||||
}
|
||||
@@ -41,15 +41,28 @@ function createSEQTAAPI(): SEQTAAPI {
|
||||
|
||||
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'] };
|
||||
|
||||
// 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>>();
|
||||
|
||||
let settings: { [K in keyof T]: SettingValue<T[K]> };
|
||||
const storageListeners = new Set<(changes: { [key: string]: any }, area: string) => void>();
|
||||
|
||||
// Initialize settings with defaults
|
||||
// Initialize settings with defaults and proper typing
|
||||
settings = Object.entries(plugin.settings).reduce((acc, [key, setting]) => {
|
||||
acc[key as keyof T] = setting.default;
|
||||
// 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]: T[K]['default'] });
|
||||
}, {} as { [K in keyof T]: SettingValue<T[K]> });
|
||||
|
||||
// Create a promise that resolves when settings are loaded
|
||||
const loaded = (async () => {
|
||||
@@ -58,9 +71,22 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
|
||||
if (stored[storageKey]) {
|
||||
Object.entries(stored[storageKey]).forEach(([key, value]) => {
|
||||
if (key in settings) {
|
||||
settings[key as keyof T] = value as any;
|
||||
// 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 any listeners that might have been registered already
|
||||
listeners.get(key as keyof T)?.forEach(callback => callback(value));
|
||||
listeners.get(key as keyof T)?.forEach(callback =>
|
||||
callback(settings[key as keyof T] as SettingValue<T[keyof T]>)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -76,8 +102,24 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
|
||||
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));
|
||||
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]>)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -89,14 +131,14 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
|
||||
const proxy = new Proxy(settings, {
|
||||
get(target, prop: string) {
|
||||
if (prop === 'onChange') {
|
||||
return (key: keyof T, callback: (value: any) => void) => {
|
||||
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);
|
||||
listeners.get(key)!.add(callback as (value: SettingValue<T[keyof T]>) => void);
|
||||
return {
|
||||
unregister: () => {
|
||||
listeners.get(key)?.delete(callback);
|
||||
listeners.get(key)?.delete(callback as (value: SettingValue<T[keyof T]>) => void);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -108,7 +150,20 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
|
||||
},
|
||||
set(target, prop: string, value: any) {
|
||||
if (prop === 'onChange' || prop === 'offChange' || prop === 'loaded') return false;
|
||||
target[prop as keyof T] = value;
|
||||
|
||||
// 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({
|
||||
@@ -116,7 +171,9 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
|
||||
});
|
||||
|
||||
// Notify listeners
|
||||
listeners.get(prop as keyof T)?.forEach(callback => callback(value));
|
||||
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> };
|
||||
|
||||
@@ -19,6 +19,7 @@ export class PluginManager {
|
||||
private eventBacklog: Map<string, any[]> = new Map();
|
||||
private cleanupFunctions: Map<string, () => void> = new Map();
|
||||
private listeners: Map<string, Set<(...args: any[]) => void>> = new Map();
|
||||
private styleElements: Map<string, HTMLStyleElement> = new Map();
|
||||
|
||||
private constructor() {
|
||||
this.setupPluginStateListener();
|
||||
@@ -89,6 +90,14 @@ export class PluginManager {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Inject plugin styles if provided
|
||||
if (plugin.styles) {
|
||||
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([
|
||||
@@ -123,6 +132,13 @@ export class PluginManager {
|
||||
}
|
||||
|
||||
public async stopPlugin(pluginId: string): Promise<void> {
|
||||
// Remove plugin styles
|
||||
const styleElement = this.styleElements.get(pluginId);
|
||||
if (styleElement) {
|
||||
styleElement.remove();
|
||||
this.styleElements.delete(pluginId);
|
||||
}
|
||||
|
||||
const cleanup = this.cleanupFunctions.get(pluginId);
|
||||
if (cleanup) {
|
||||
cleanup();
|
||||
|
||||
@@ -37,9 +37,9 @@ export function BooleanSetting(options: BooleanSettingOptions): PropertyDecorato
|
||||
proto.settings = {};
|
||||
}
|
||||
|
||||
// Add the setting to the prototype's settings object
|
||||
// Add the setting to the prototype's settings object with const assertion
|
||||
proto.settings[propertyKey] = {
|
||||
type: 'boolean',
|
||||
type: 'boolean' as const,
|
||||
...options
|
||||
};
|
||||
};
|
||||
@@ -53,9 +53,9 @@ export function StringSetting(options: StringSettingOptions): PropertyDecorator
|
||||
proto.settings = {};
|
||||
}
|
||||
|
||||
// Add the setting to the prototype's settings object
|
||||
// Add the setting to the prototype's settings object with const assertion
|
||||
proto.settings[propertyKey] = {
|
||||
type: 'string',
|
||||
type: 'string' as const,
|
||||
...options
|
||||
};
|
||||
};
|
||||
@@ -69,9 +69,9 @@ export function NumberSetting(options: NumberSettingOptions): PropertyDecorator
|
||||
proto.settings = {};
|
||||
}
|
||||
|
||||
// Add the setting to the prototype's settings object
|
||||
// Add the setting to the prototype's settings object with const assertion
|
||||
proto.settings[propertyKey] = {
|
||||
type: 'number',
|
||||
type: 'number' as const,
|
||||
...options
|
||||
};
|
||||
};
|
||||
@@ -85,9 +85,9 @@ export function SelectSetting<T extends string>(options: SelectSettingOptions<T>
|
||||
proto.settings = {};
|
||||
}
|
||||
|
||||
// Add the setting to the prototype's settings object
|
||||
// Add the setting to the prototype's settings object with const assertion
|
||||
proto.settings[propertyKey] = {
|
||||
type: 'select',
|
||||
type: 'select' as const,
|
||||
...options
|
||||
};
|
||||
};
|
||||
@@ -96,13 +96,13 @@ export function SelectSetting<T extends string>(options: SelectSettingOptions<T>
|
||||
// Base plugin class that handles settings
|
||||
export abstract class BasePlugin<T extends PluginSettings = PluginSettings> {
|
||||
// The settings property will be populated by decorators
|
||||
settings: T = {} as T;
|
||||
settings!: T;
|
||||
|
||||
constructor() {
|
||||
// Copy settings from the prototype to the instance
|
||||
// This ensures that each instance has its own settings object
|
||||
if (this.constructor.prototype.settings) {
|
||||
this.settings = { ...this.constructor.prototype.settings };
|
||||
this.settings = { ...this.constructor.prototype.settings } as T;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export type PluginSettings = {
|
||||
}
|
||||
|
||||
// Helper type to extract the actual value type from a setting
|
||||
type SettingValue<T extends PluginSetting> = T extends BooleanSetting ? boolean :
|
||||
export type SettingValue<T extends PluginSetting> = T extends BooleanSetting ? boolean :
|
||||
T extends StringSetting ? string :
|
||||
T extends NumberSetting ? number :
|
||||
T extends SelectSetting<infer O> ? O :
|
||||
@@ -50,7 +50,7 @@ type SettingValue<T extends PluginSetting> = T extends BooleanSetting ? boolean
|
||||
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;
|
||||
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
|
||||
}
|
||||
@@ -96,6 +96,7 @@ 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
|
||||
run: (api: PluginAPI<T, S>) => void | Promise<void> | (() => void) | Promise<(() => void)>;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { PluginManager } from './core/manager';
|
||||
import timetablePlugin from './built-in/timetable';
|
||||
import notificationCollectorPlugin from './built-in/notificationCollector';
|
||||
import themesPlugin from './built-in/themes';
|
||||
import animatedBackgroundPlugin from './built-in/animated-background';
|
||||
|
||||
// Initialize plugin manager
|
||||
const pluginManager = PluginManager.getInstance();
|
||||
@@ -12,6 +13,7 @@ const pluginManager = PluginManager.getInstance();
|
||||
pluginManager.registerPlugin(timetablePlugin);
|
||||
pluginManager.registerPlugin(notificationCollectorPlugin);
|
||||
pluginManager.registerPlugin(themesPlugin);
|
||||
pluginManager.registerPlugin(animatedBackgroundPlugin);
|
||||
//pluginManager.registerPlugin(testPlugin);
|
||||
|
||||
export { init as Monofile } from './monofile';
|
||||
|
||||
Reference in New Issue
Block a user