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}