schoolbox web extension :)
1import { storage } from "#imports"; 2import { hasChanged } from "."; 3import { logger } from "./logger"; 4import type { Toggle } from "./storage"; 5import { globalSettings } from "./storage"; 6import { StorageState } from "./storage/state.svelte"; 7 8export class Plugin<T extends Record<string, StorageState<any>> | undefined = undefined> { 9 private injected = false; 10 public toggle: StorageState<Toggle>; 11 public settings!: T; 12 public menu: string | undefined; 13 14 constructor( 15 public meta: { 16 id: string; 17 name: string; 18 description: string; 19 }, 20 defaultToggle: boolean, 21 settings: { 22 config: Record<string, object>; 23 menu: string; 24 } | null, 25 private injectCallback: (settings: T) => Promise<void> | void, 26 private uninjectCallback: (settings: T) => Promise<void> | void, 27 private elementsToWaitFor: string[] = [], 28 ) { 29 this.meta = meta; 30 this.elementsToWaitFor = elementsToWaitFor; 31 this.injectCallback = injectCallback; 32 this.uninjectCallback = uninjectCallback; 33 if (settings && settings.menu) this.menu = settings.menu; 34 35 // init plugin storage 36 this.toggle = new StorageState( 37 storage.defineItem(`local:plugin-${meta.id}`, { 38 fallback: { toggle: defaultToggle }, 39 }), 40 ); 41 if (settings && settings.config) { 42 this.settings = Object.fromEntries( 43 Object.entries(settings.config).map(([key, value]) => [ 44 key, 45 new StorageState( 46 storage.defineItem(`local:plugin-${meta.id}-${key}`, { 47 fallback: value, 48 }), 49 ), 50 ]), 51 ) as T; 52 } 53 } 54 55 async init() { 56 logger.info(`init plugin: ${this.meta.name}`); 57 58 if (await this.isEnabled()) { 59 // wait for elements to be loaded 60 if (this.elementsToWaitFor.length > 0) { 61 // create an observer to wait for all elements to be loaded 62 const observer = new MutationObserver((_mutations, observer) => { 63 if (this.allElementsPresent()) { 64 observer.disconnect(); 65 this.inject(); 66 } 67 }); 68 observer.observe(document.body, { childList: true, subtree: true }); 69 70 // check if elements are already present 71 if (this.allElementsPresent()) { 72 observer.disconnect(); 73 this.inject(); 74 } 75 } else { 76 // no elements to wait for 77 this.inject(); 78 } 79 } 80 81 // init watchers 82 globalSettings.watch((newValue, oldValue) => { 83 if (hasChanged(newValue, oldValue, ["global", "plugins"])) this.reload(); 84 }); 85 this.toggle.watch(this.reload.bind(this)); 86 if (this.settings) { 87 for (const setting of Object.values(this.settings)) { 88 setting.watch(this.reload.bind(this)); 89 } 90 } 91 } 92 93 private inject() { 94 if (this.injected) return; 95 if (!this.allElementsPresent()) return; 96 logger.info(`injecting plugin: ${this.meta.name}`); 97 this.injectCallback(this.settings); 98 this.injected = true; 99 } 100 101 private uninject() { 102 if (!this.injected) return; 103 logger.info(`uninjecting plugin: ${this.meta.name}`); 104 this.uninjectCallback(this.settings); 105 this.injected = false; 106 } 107 108 private async reload() { 109 if (this.injected) this.uninject(); 110 if (await this.isEnabled()) this.inject(); 111 } 112 113 private async isEnabled(): Promise<boolean> { 114 const settings = await globalSettings.get(); 115 const toggle = await this.toggle.get(); 116 117 return settings.global && settings.plugins && toggle.toggle; 118 } 119 120 private allElementsPresent() { 121 return this.elementsToWaitFor.every((selector) => document.querySelector(selector) !== null); 122 } 123}