schoolbox web extension :)
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}