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