schoolbox web extension :)
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor(plugins): use class instead of helper

* #314

willow cf647101 dd89b3cc

+412 -510
+55 -39
src/entrypoints/plugins/homepageSwitcher.ts
··· 1 1 import { browser } from "#imports"; 2 - import { definePlugin } from "@/utils/plugin"; 2 + import { Plugin } from "@/utils/plugin"; 3 + import { Toggle } from "@/utils/storage"; 4 + import { StorageState } from "@/utils/storage/state.svelte"; 3 5 4 6 let logos: HTMLAnchorElement[] | null = null; 5 7 let controller: AbortController | null = null; 6 8 7 - export default function init() { 8 - definePlugin( 9 - "homepageSwitcher", 10 - (settings) => { 11 - if (logos !== null) return; 9 + type Settings = { 10 + closeCurrentTab: StorageState<Toggle>; 11 + }; 12 12 13 - logos = Array.from(document.getElementsByClassName("logo")) as HTMLAnchorElement[]; 13 + export default new Plugin<Settings>( 14 + { 15 + id: "homepageSwitcher", 16 + name: "Homepage Switcher", 17 + description: "The logo will switch to existing Schoolbox homepage when available.", 18 + }, 19 + { 20 + toggle: true, 21 + settings: { 22 + closeCurrentTab: { 23 + toggle: false, 24 + }, 25 + }, 26 + }, 27 + async (settings) => { 28 + if (logos !== null) return; 14 29 15 - // add event listeners 16 - const closeCurrentTab = settings?.toggle.closeCurrentTab === true; 17 - controller = new AbortController(); 30 + logos = Array.from(document.getElementsByClassName("logo")) as HTMLAnchorElement[]; 18 31 19 - for (const logo of logos) { 20 - logo.addEventListener( 21 - "click", 22 - (e) => { 23 - if (window.location.pathname === "/") return; 32 + // add event listeners 33 + const closeCurrentTab = await settings.closeCurrentTab.get(); 34 + controller = new AbortController(); 24 35 25 - e.preventDefault(); 36 + for (const logo of logos) { 37 + logo.addEventListener( 38 + "click", 39 + (e) => { 40 + if (window.location.pathname === "/") return; 26 41 27 - if (logos) { 28 - const tab = logos[0].href; 29 - if (closeCurrentTab) window.close(); // TODO: Scripts may only close windows that were opened by a script. 30 - browser.runtime.sendMessage({ toTab: tab }); 31 - } 32 - }, 33 - { 34 - signal: controller.signal, 35 - }, 36 - ); 37 - } 38 - }, 39 - () => { 40 - // remove event listeners 41 - if (controller) { 42 - controller.abort(); 43 - controller = null; 44 - } 42 + e.preventDefault(); 43 + 44 + if (logos) { 45 + const tab = logos[0].href; 46 + if (closeCurrentTab.toggle) window.close(); // TODO: Scripts may only close windows that were opened by a script. 47 + browser.runtime.sendMessage({ toTab: tab }); 48 + } 49 + }, 50 + { 51 + signal: controller.signal, 52 + }, 53 + ); 54 + } 55 + }, 56 + () => { 57 + // remove event listeners 58 + if (controller) { 59 + controller.abort(); 60 + controller = null; 61 + } 45 62 46 - logos = null; 47 - }, 48 - [".logo"], 49 - ); 50 - } 63 + logos = null; 64 + }, 65 + [".logo"], 66 + );
+38 -23
src/entrypoints/plugins/modernIcons/index.ts
··· 6 6 uninjectInlineStyles, 7 7 uninjectStylesheet, 8 8 } from "@/utils"; 9 - import { definePlugin } from "@/utils/plugin"; 9 + import { Plugin } from "@/utils/plugin"; 10 10 import styleText from "./styles.css?inline"; 11 + import { Toggle } from "@/utils/storage"; 12 + import { StorageState } from "@/utils/storage/state.svelte"; 11 13 12 14 const ID = "modernIcons"; 13 15 const PLUGIN_ID = `plugin-${ID}`; 14 16 15 - export default function init() { 16 - definePlugin( 17 - ID, 18 - async (settings) => { 19 - const iconNames = [...new Set(Object.values(icons))].sort(); 20 - const fontUrl = `https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:FILL@0..1&icon_names=${iconNames.join(",")}`; 17 + type Settings = { 18 + filled: StorageState<Toggle>; 19 + }; 20 + 21 + export default new Plugin<Settings>( 22 + { 23 + id: ID, 24 + name: "Modern Icons", 25 + description: "Modernise the icons across Schoolbox.", 26 + }, 27 + { 28 + toggle: true, 29 + settings: { 30 + filled: { toggle: true }, 31 + }, 32 + }, 33 + 34 + async (settings) => { 35 + const iconNames = [...new Set(Object.values(icons))].sort(); 36 + const fontUrl = `https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:FILL@0..1&icon_names=${iconNames.join(",")}`; 21 37 22 - // inject font face 23 - injectStylesheet(fontUrl, PLUGIN_ID); 38 + // inject font face 39 + injectStylesheet(fontUrl, PLUGIN_ID); 24 40 25 - // inject icon styling 26 - injectInlineStyles(styleText, PLUGIN_ID); 41 + // inject icon styling 42 + injectInlineStyles(styleText, PLUGIN_ID); 27 43 28 - // inject icons 29 - const filled = settings?.toggle.filled ?? false; 30 - injectIcons(icons, filled); 31 - }, 32 - () => { 33 - uninjectStylesheet(PLUGIN_ID); 34 - uninjectInlineStyles(PLUGIN_ID); 35 - uninjectIcons(); 36 - }, 37 - ["nav.tab-bar .top-menu", "#overflow-nav"], 38 - ); 39 - } 44 + // inject icons 45 + const filled = await settings.filled.get(); 46 + injectIcons(icons, filled.toggle); 47 + }, 48 + () => { 49 + uninjectStylesheet(PLUGIN_ID); 50 + uninjectInlineStyles(PLUGIN_ID); 51 + uninjectIcons(); 52 + }, 53 + ["nav.tab-bar .top-menu", "#overflow-nav"], 54 + ); 40 55 41 56 // [className, iconName] (material icons) 42 57 const icons = {
+27 -22
src/entrypoints/plugins/progressBar/index.ts
··· 1 1 import { dataAttr, injectInlineStyles, setDataAttr, uninjectInlineStyles } from "@/utils"; 2 2 import type { Period } from "@/utils/periodUtils"; 3 3 import { getListOfPeriods } from "@/utils/periodUtils"; 4 - import { definePlugin } from "@/utils/plugin"; 4 + import { Plugin } from "@/utils/plugin"; 5 5 import styleText from "./styles.css?inline"; 6 6 7 7 const ID = "progressBar"; 8 8 const PLUGIN_ID = `plugin-${ID}`; 9 9 10 - export default function init() { 11 - definePlugin( 12 - ID, 13 - () => { 14 - if (window.location.pathname === "/" && document.querySelector(".timetable")) { 15 - const periodList = getListOfPeriods(); 10 + export default new Plugin( 11 + { 12 + id: ID, 13 + name: "Progress Bar", 14 + description: "Displays a progress bar below the timetable to show the time of the day.", 15 + }, 16 + { 17 + toggle: true, 18 + }, 19 + () => { 20 + if (window.location.pathname === "/" && document.querySelector(".timetable")) { 21 + const periodList = getListOfPeriods(); 16 22 17 - const progressRow = document.createElement("tr"); 18 - progressRow.classList.add("progress-container"); 19 - document.querySelector(".timetable > thead")?.insertAdjacentElement("beforeend", progressRow); 23 + const progressRow = document.createElement("tr"); 24 + progressRow.classList.add("progress-container"); 25 + document.querySelector(".timetable > thead")?.insertAdjacentElement("beforeend", progressRow); 20 26 21 - injectInlineStyles(styleText, PLUGIN_ID); 22 - injectProgressBars(periodList, progressRow); 27 + injectInlineStyles(styleText, PLUGIN_ID); 28 + injectProgressBars(periodList, progressRow); 23 29 24 - setDataAttr(progressRow, `${PLUGIN_ID}-row`); 25 - } 26 - }, 27 - () => { 28 - uninjectInlineStyles(PLUGIN_ID); 29 - uninjectProgressBars(); 30 - }, 31 - [".timetable"], 32 - ); 33 - } 30 + setDataAttr(progressRow, `${PLUGIN_ID}-row`); 31 + } 32 + }, 33 + () => { 34 + uninjectInlineStyles(PLUGIN_ID); 35 + uninjectProgressBars(); 36 + }, 37 + [".timetable"], 38 + ); 34 39 35 40 function injectProgressBars(periodList: Period[], container: HTMLElement) { 36 41 if (document.querySelector(dataAttr(`${PLUGIN_ID}-row`))) return;
+57 -41
src/entrypoints/plugins/scrollPeriod.ts
··· 1 1 import { getCurrentPeriod } from "@/utils/periodUtils"; 2 - import { definePlugin } from "@/utils/plugin"; 2 + import { Plugin } from "@/utils/plugin"; 3 + import { Slider, Toggle } from "@/utils/storage"; 4 + import { StorageState } from "@/utils/storage/state.svelte"; 3 5 4 6 let interval: NodeJS.Timeout | null = null; 5 7 let controller: AbortController | null = null; 6 8 7 - export default function init() { 8 - definePlugin( 9 - "scrollPeriod", 10 - async (settings) => { 11 - const timetable = document.querySelector("[data-timetable-container] div.scrollable"); 9 + type Settings = { 10 + resetCooldownOnMouseMove: StorageState<Toggle>; 11 + cooldownDuration: StorageState<Slider>; 12 + }; 12 13 13 - if (window.location.pathname === "/" && timetable) { 14 - updateScrollbar(timetable); 14 + export default new Plugin<Settings>( 15 + { 16 + id: "scrollPeriod", 17 + name: "Scroll Period", 18 + description: "Scrolls to the current period on the timetable.", 19 + }, 20 + { 21 + toggle: true, 22 + settings: { 23 + resetCooldownOnMouseMove: { toggle: true }, 24 + cooldownDuration: { min: 1, max: 60, value: 10 }, 25 + }, 26 + }, 27 + async (settings) => { 28 + const timetable = document.querySelector("[data-timetable-container] div.scrollable"); 15 29 16 - const cooldownDuration = settings?.slider.cooldownDuration; 17 - const resetCooldownOnMouseMove = settings?.toggle.resetCooldownOnMouseMove; 30 + if (window.location.pathname === "/" && timetable) { 31 + updateScrollbar(timetable); 18 32 19 - const setUpdateInterval = () => { 20 - interval = setInterval(() => updateScrollbar(timetable), (cooldownDuration?.value || 10) * 1000); 21 - }; 33 + const cooldownDuration = await settings.cooldownDuration.get(); 34 + const resetCooldownOnMouseMove = await settings.resetCooldownOnMouseMove.get(); 22 35 23 - setUpdateInterval(); 36 + const setUpdateInterval = () => { 37 + interval = setInterval(() => updateScrollbar(timetable), (cooldownDuration?.value || 10) * 1000); 38 + }; 24 39 25 - if (resetCooldownOnMouseMove === true) { 26 - controller = new AbortController(); 27 - document.addEventListener( 28 - "mousemove", 29 - () => { 30 - if (interval) { 31 - clearInterval(interval); 32 - setUpdateInterval(); 33 - } 34 - }, 35 - { signal: controller.signal }, 36 - ); 37 - } 40 + setUpdateInterval(); 41 + 42 + if (resetCooldownOnMouseMove.toggle === true) { 43 + controller = new AbortController(); 44 + document.addEventListener( 45 + "mousemove", 46 + () => { 47 + if (interval) { 48 + clearInterval(interval); 49 + setUpdateInterval(); 50 + } 51 + }, 52 + { signal: controller.signal }, 53 + ); 38 54 } 39 - }, 40 - () => { 41 - if (controller) { 42 - controller.abort(); 43 - controller = null; 44 - } 45 - if (interval) { 46 - clearInterval(interval); 47 - interval = null; 48 - } 49 - }, 50 - [".timetable"], 51 - ); 52 - } 55 + } 56 + }, 57 + () => { 58 + if (controller) { 59 + controller.abort(); 60 + controller = null; 61 + } 62 + if (interval) { 63 + clearInterval(interval); 64 + interval = null; 65 + } 66 + }, 67 + [".timetable"], 68 + ); 53 69 54 70 function updateScrollbar(timetable: Element) { 55 71 const currentPeriod = getCurrentPeriod();
+38 -33
src/entrypoints/plugins/scrollSegments/index.ts
··· 1 1 import { dataAttr, injectInlineStyles, setDataAttr, uninjectInlineStyles } from "@/utils"; 2 - import { definePlugin } from "@/utils/plugin"; 2 + import { Plugin } from "@/utils/plugin"; 3 3 import styleText from "./styles.css?inline"; 4 4 5 5 const ID = "scrollSegments"; 6 6 const PLUGIN_ID = `plugin-${ID}`; 7 7 8 - export default function init() { 9 - definePlugin( 10 - ID, 11 - () => { 12 - const footerCopy = document.querySelector(dataAttr(PLUGIN_ID)); 13 - if (footerCopy) return; 8 + export default new Plugin( 9 + { 10 + id: ID, 11 + name: "Scroll Segments", 12 + description: "Segments the Schoolbox page into scrollable sections.", 13 + }, 14 + { 15 + toggle: true, 16 + }, 17 + () => { 18 + const footerCopy = document.querySelector(dataAttr(PLUGIN_ID)); 19 + if (footerCopy) return; 14 20 15 - // scroll to top to avoid hot reload bug 16 - window.scrollTo({ 17 - top: 0, 18 - behavior: "instant", 19 - }); 21 + // scroll to top to avoid hot reload bug 22 + window.scrollTo({ 23 + top: 0, 24 + behavior: "instant", 25 + }); 20 26 21 - const content = document.querySelector("#content"); 22 - const footer = document.querySelector<HTMLDivElement>("#footer"); 27 + const content = document.querySelector("#content"); 28 + const footer = document.querySelector<HTMLDivElement>("#footer"); 23 29 24 - // add copy of footer to content 25 - if (content && footer) { 26 - const clone = footer.cloneNode(true) as HTMLDivElement; 27 - setDataAttr(clone, PLUGIN_ID); 28 - content.appendChild(clone); 29 - } 30 + // add copy of footer to content 31 + if (content && footer) { 32 + const clone = footer.cloneNode(true) as HTMLDivElement; 33 + setDataAttr(clone, PLUGIN_ID); 34 + content.appendChild(clone); 35 + } 30 36 31 - injectInlineStyles(styleText, PLUGIN_ID); 32 - }, 33 - () => { 34 - const footerCopy = document.querySelector(dataAttr(PLUGIN_ID)); 35 - if (!footerCopy) return; 37 + injectInlineStyles(styleText, PLUGIN_ID); 38 + }, 39 + () => { 40 + const footerCopy = document.querySelector(dataAttr(PLUGIN_ID)); 41 + if (!footerCopy) return; 36 42 37 - // remove copy of footer from content 38 - const content = document.querySelector("#content"); 39 - content?.removeChild(footerCopy); 43 + // remove copy of footer from content 44 + const content = document.querySelector("#content"); 45 + content?.removeChild(footerCopy); 40 46 41 - uninjectInlineStyles(PLUGIN_ID); 42 - }, 43 - ["#content", "#footer"], 44 - ); 45 - } 47 + uninjectInlineStyles(PLUGIN_ID); 48 + }, 49 + ["#content", "#footer"], 50 + );
+25 -11
src/entrypoints/plugins/subheader/index.ts
··· 1 1 import { getCurrentPeriod } from "@/utils/periodUtils"; 2 - import { definePlugin } from "@/utils/plugin"; 2 + import { Plugin } from "@/utils/plugin"; 3 3 import styleText from "./styles.css?inline"; 4 4 import { dataAttr, injectInlineStyles, setDataAttr, uninjectInlineStyles } from "@/utils"; 5 + import { StorageState } from "@/utils/storage/state.svelte"; 6 + import { Toggle } from "@/utils/storage"; 5 7 6 8 const ID = "subheader"; 7 9 const PLUGIN_ID = `plugin-${ID}`; ··· 10 12 let oldChildren: ChildNode[] = []; 11 13 let subheader: HTMLHeadingElement | null = null; 12 14 13 - export default function init() { 14 - definePlugin( 15 - "subheader", 16 - (settings) => { 17 - const openInNewTab = settings?.toggle.openInNewTab ?? false; 18 - injectSubheader(openInNewTab); 15 + type Settings = { 16 + openInNewTab: StorageState<Toggle>; 17 + }; 18 + 19 + export default new Plugin<Settings>( 20 + { 21 + id: ID, 22 + name: "Subheader Revamp", 23 + description: "Adds a clock and current period info to the subheader.", 24 + }, 25 + { 26 + toggle: true, 27 + settings: { 28 + openInNewTab: { toggle: true }, 19 29 }, 20 - uninjectSubheader, 21 - [".subheader", ".timetable"], 22 - ); 23 - } 30 + }, 31 + async (settings) => { 32 + const openInNewTab = await settings.openInNewTab.get(); 33 + injectSubheader(openInNewTab.toggle); 34 + }, 35 + uninjectSubheader, 36 + [".subheader", ".timetable"], 37 + ); 24 38 25 39 function injectSubheader(openInNewTab: boolean) { 26 40 // abort if plugin is injected
+70 -56
src/entrypoints/plugins/tabTitle.ts
··· 1 - import { definePlugin } from "@/utils/plugin"; 1 + import { Plugin } from "@/utils/plugin"; 2 + import { Toggle } from "@/utils/storage"; 3 + import { StorageState } from "@/utils/storage/state.svelte"; 2 4 3 5 const ID = "tabTitle"; 4 6 let originalTitle: string | null = null; 5 7 6 - export default function init() { 7 - definePlugin( 8 - ID, 9 - async (settings) => { 10 - // if already injected, abort 11 - if (originalTitle) return; 8 + type Settings = { 9 + showSubjectPrefix: StorageState<Toggle>; 10 + }; 12 11 13 - // backup original title (used for uninjection) 14 - originalTitle = document.title; 12 + export default new Plugin<Settings>( 13 + { 14 + id: ID, 15 + name: "Better Tab Titles", 16 + description: "Improves the tab titles for easier navigation.", 17 + }, 18 + { 19 + toggle: true, 20 + settings: { 21 + showSubjectPrefix: { toggle: true }, 22 + }, 23 + }, 24 + async (settings) => { 25 + // if already injected, abort 26 + if (originalTitle) return; 15 27 16 - const path = window.location.pathname; 17 - const titleMap: { [key: string]: string } = { 18 - "/": "Homepage", 19 - "/calendar": "Calendar", 20 - "/news": "News", 21 - "/learning/classes": "Classes", 22 - "/resources": "Resources", 23 - "/groups": "Groups", 24 - "/settings/notifications": "Notifications Settings", 25 - "/mail/create": "Compose Email", 26 - "/feedback": "Support and Feedback", 27 - "/policy": "Guidelines of Use and Privacy Policy", 28 - }; 28 + // backup original title (used for uninjection) 29 + originalTitle = document.title; 29 30 30 - if (titleMap[path]) { 31 - document.title = titleMap[path]; 32 - } else if (path.includes("/timetable")) { 33 - document.title = "Timetable"; 34 - } else if (path.includes("/calendar")) { 35 - document.title = "Calendar"; 36 - } else if (path.includes("/grades/")) { 37 - document.title = "Grades"; 38 - } else if (path.includes("/news/")) { 39 - document.title = `News (${document.getElementsByTagName("h1")[0].innerText})`; 40 - } else if (path.includes("/assessments/")) { 41 - document.title = `Assessments - ${document.getElementsByTagName("h1")[0].innerText})`; 42 - } else if (path.includes("/mail/create")) { 43 - document.title = "Compose Email"; 44 - } else if (path.includes("/search/user")) { 45 - document.title = `Profile - ${document.getElementsByTagName("h1")[0].innerText}`; 46 - } else if (path.includes("/learning/due/")) { 47 - document.title = "Due Work"; 48 - } else if (path.includes("/homepage/")) { 49 - if (settings?.toggle.showSubjectPrefix === false) { 50 - document.title = document.getElementsByTagName("h1")[0].innerText.replace(/^.*- /, ""); 51 - } else { 52 - document.title = document.getElementsByTagName("h1")[0].innerText; 53 - } 31 + const path = window.location.pathname; 32 + const titleMap: { [key: string]: string } = { 33 + "/": "Homepage", 34 + "/calendar": "Calendar", 35 + "/news": "News", 36 + "/learning/classes": "Classes", 37 + "/resources": "Resources", 38 + "/groups": "Groups", 39 + "/settings/notifications": "Notifications Settings", 40 + "/mail/create": "Compose Email", 41 + "/feedback": "Support and Feedback", 42 + "/policy": "Guidelines of Use and Privacy Policy", 43 + }; 44 + 45 + if (titleMap[path]) { 46 + document.title = titleMap[path]; 47 + } else if (path.includes("/timetable")) { 48 + document.title = "Timetable"; 49 + } else if (path.includes("/calendar")) { 50 + document.title = "Calendar"; 51 + } else if (path.includes("/grades/")) { 52 + document.title = "Grades"; 53 + } else if (path.includes("/news/")) { 54 + document.title = `News (${document.getElementsByTagName("h1")[0].innerText})`; 55 + } else if (path.includes("/assessments/")) { 56 + document.title = `Assessments - ${document.getElementsByTagName("h1")[0].innerText})`; 57 + } else if (path.includes("/mail/create")) { 58 + document.title = "Compose Email"; 59 + } else if (path.includes("/search/user")) { 60 + document.title = `Profile - ${document.getElementsByTagName("h1")[0].innerText}`; 61 + } else if (path.includes("/learning/due/")) { 62 + document.title = "Due Work"; 63 + } else if (path.includes("/homepage/")) { 64 + if (!(await settings.showSubjectPrefix.get()).toggle) { 65 + document.title = document.getElementsByTagName("h1")[0].innerText.replace(/^.*- /, ""); 66 + } else { 67 + document.title = document.getElementsByTagName("h1")[0].innerText; 54 68 } 55 - }, 56 - () => { 57 - // if not injected, abort 58 - if (!originalTitle) return; 69 + } 70 + }, 71 + () => { 72 + // if not injected, abort 73 + if (!originalTitle) return; 59 74 60 - document.title = originalTitle; 61 - originalTitle = null; 62 - }, 63 - ["h1"], 64 - ); 65 - } 75 + document.title = originalTitle; 76 + originalTitle = null; 77 + }, 78 + ["h1"], 79 + );
+99 -110
src/utils/plugin.ts
··· 1 + import { storage } from "#imports"; 1 2 import { hasChanged } from "."; 2 3 import { logger } from "./logger"; 3 - import type { PluginId, PluginSetting, Slider } from "./storage"; 4 - import { globalSettings, plugins, schoolboxUrls } from "./storage"; 4 + import type { Toggle } from "./storage"; 5 + import { globalSettings } from "./storage"; 6 + import { StorageState } from "./storage/state.svelte"; 5 7 6 - export async function definePlugin( 7 - pluginId: PluginId, 8 - injectCallback: (settings?: { 9 - toggle: Record<string, boolean>; 10 - slider: Record<string, Slider>; 11 - }) => Promise<void> | void, 12 - uninjectCallback: (settings?: { 13 - toggle: Record<string, boolean>; 14 - slider: Record<string, Slider>; 15 - }) => Promise<void> | void, 16 - elementsToWaitFor: string[] = [], 17 - ) { 18 - const plugin = await plugins[pluginId].toggle.get(); 19 - let injected = false; 8 + export class Plugin<T extends Record<string, StorageState<any>> | undefined = undefined> { 9 + private injected = false; 10 + public toggle: StorageState<Toggle>; 11 + public settings!: T; 20 12 21 - logger.info(`${plugins[pluginId].name}: ${plugin.toggle ? "enabled" : "disabled"}`); 13 + constructor( 14 + public meta: { 15 + id: string; 16 + name: string; 17 + description: string; 18 + }, 19 + defaultConfig: { 20 + toggle: boolean; 21 + settings?: Record<string, object>; 22 + }, 23 + private injectCallback: (settings: T) => Promise<void> | void, 24 + private uninjectCallback: (settings: T) => Promise<void> | void, 25 + private elementsToWaitFor: string[] = [], 26 + ) { 27 + this.meta = meta; 28 + this.elementsToWaitFor = elementsToWaitFor; 29 + this.injectCallback = injectCallback; 30 + this.uninjectCallback = uninjectCallback; 22 31 23 - const settings = await globalSettings.get(); 24 - const urls = (await schoolboxUrls.get()).urls; 32 + // init plugin storage 33 + this.toggle = new StorageState( 34 + storage.defineItem(`local:plugin-${meta.id}`, { 35 + fallback: { toggle: defaultConfig.toggle }, 36 + }), 37 + ); 38 + if (defaultConfig.settings) { 39 + this.settings = Object.fromEntries( 40 + Object.entries(defaultConfig.settings).map(([key, value]) => [ 41 + key, 42 + new StorageState( 43 + storage.defineItem(`local:plugin-${meta.id}-${key}`, { 44 + fallback: value, 45 + }), 46 + ), 47 + ]), 48 + ) as T; 49 + } 50 + } 25 51 26 - if (plugin && typeof window !== "undefined" && urls.includes(window.location.origin)) { 27 - const allElementsPresent = () => elementsToWaitFor.every((selector) => document.querySelector(selector) !== null); 52 + async init() { 53 + if (await this.isEnabled()) this.initObserver(); 28 54 29 - const inject = async () => { 30 - if (injected) return; 31 - if (!allElementsPresent()) return; 32 - logger.info(`injecting plugin: ${plugins[pluginId].name}`); 33 - injectCallback(await getSettingsValues(plugins[pluginId]?.settings)); 34 - injected = true; 35 - }; 55 + // init watchers 56 + globalSettings.watch((newValue, oldValue) => { 57 + if (hasChanged(newValue, oldValue, ["global", "plugins"])) this.reload(); 58 + }); 59 + this.toggle.watch(this.reload); 60 + if (this.settings) { 61 + for (const setting of Object.values(this.settings)) { 62 + setting.watch(this.reload); 63 + } 64 + } 65 + } 36 66 37 - const uninject = async () => { 38 - if (!injected) return; 39 - logger.info(`uninjecting plugin: ${plugins[pluginId].name}`); 40 - uninjectCallback(await getSettingsValues(plugins[pluginId]?.settings)); 41 - injected = false; 42 - }; 67 + async reload() { 68 + if (this.injected) this.uninject(); 69 + if (await this.isEnabled()) this.inject(); 70 + } 43 71 44 - const initWatchers = () => { 45 - // add watchers for injecting plugin 46 - globalSettings.watch(async (newValue, oldValue) => { 47 - if (hasChanged(newValue, oldValue, ["global", "plugins"])) { 48 - const plugin = await plugins[pluginId].toggle.get(); 49 - if (newValue.global && newValue.plugins && plugin.toggle) { 50 - inject(); 51 - } else { 52 - uninject(); 53 - } 72 + private initObserver() { 73 + // wait for elements to be loaded 74 + if (this.elementsToWaitFor.length > 0) { 75 + // create an observer to wait for all elements to be loaded 76 + const observer = new MutationObserver((_mutations, observer) => { 77 + if (this.allElementsPresent()) { 78 + observer.disconnect(); 79 + this.inject(); 54 80 } 55 81 }); 56 - plugins[pluginId].toggle.watch(async (newValue) => { 57 - const settings = await globalSettings.get(); 58 - if (newValue.toggle && settings.global && settings.plugins) { 59 - inject(); 60 - } else { 61 - uninject(); 62 - } 63 - }); 82 + observer.observe(document.body, { childList: true, subtree: true }); 64 83 65 - // reload plugin if settings have been updated 66 - if (plugins[pluginId].settings) { 67 - for (const setting of Object.values(plugins[pluginId].settings)) { 68 - setting.state.watch(async () => { 69 - uninject(); 70 - const settings = await globalSettings.get(); 71 - const toggle = await plugins[pluginId].toggle.get(); 72 - if (toggle && settings.global && settings.plugins) { 73 - inject(); 74 - } 75 - }); 76 - } 84 + // check if elements are already present 85 + if (this.allElementsPresent()) { 86 + observer.disconnect(); 87 + this.inject(); 77 88 } 78 - }; 79 - 80 - initWatchers(); 89 + } else { 90 + // no elements to wait for 91 + this.inject(); 92 + } 93 + } 81 94 82 - if (settings.global && settings.plugins && plugin.toggle) { 83 - const initObserver = () => { 84 - // wait for elements to be loaded 95 + private inject() { 96 + if (this.injected) return; 97 + if (!this.allElementsPresent()) return; 98 + logger.info(`injecting plugin: ${this.meta.name}`); 99 + this.injectCallback(this.settings); 100 + this.injected = true; 101 + } 85 102 86 - if (elementsToWaitFor.length > 0) { 87 - // create an observer to wait for all elements to be loaded 88 - const observer = new MutationObserver((_mutations, observer) => { 89 - if (allElementsPresent()) { 90 - observer.disconnect(); 91 - inject(); 92 - } 93 - }); 94 - observer.observe(document.body, { childList: true, subtree: true }); 103 + private uninject() { 104 + if (!this.injected) return; 105 + logger.info(`uninjecting plugin: ${this.meta.name}`); 106 + this.uninjectCallback(this.settings); 107 + this.injected = false; 108 + } 95 109 96 - // check if elements are already present 97 - if (allElementsPresent()) { 98 - observer.disconnect(); 99 - inject(); 100 - } 101 - } else { 102 - // no elements to wait for 103 - inject(); 104 - } 105 - }; 110 + private async isEnabled(): Promise<boolean> { 111 + const settings = await globalSettings.get(); 112 + const toggle = await this.toggle.get(); 106 113 107 - if (document.body) { 108 - initObserver(); 109 - } else { 110 - document.addEventListener("DOMContentLoaded", initObserver); 111 - } 112 - } 114 + return settings.global && settings.plugins && toggle.toggle; 113 115 } 114 - } 115 - 116 - async function getSettingsValues(settings?: Record<string, PluginSetting>) { 117 - if (!settings) return undefined; 118 116 119 - const result: { 120 - toggle: Record<string, boolean>; 121 - slider: Record<string, Slider>; 122 - } = { toggle: {}, slider: {} }; 123 - for (const [key, setting] of Object.entries(settings)) { 124 - if (setting.type === "toggle") { 125 - result.toggle[key] = (await setting.state.get()).toggle; 126 - } else if (setting.type === "slider") { 127 - result.slider[key] = await setting.state.get(); 128 - } 117 + private allElementsPresent() { 118 + return this.elementsToWaitFor.every((selector) => document.querySelector(selector) !== null); 129 119 } 130 - return result; 131 120 }
-1
src/utils/storage/index.ts
··· 1 1 export * from "./global"; 2 - export * from "./plugins"; 3 2 export * from "./snippets"; 4 3 export * from "./types";
-138
src/utils/storage/plugins.ts
··· 1 - import { storage } from "#imports"; 2 - import { StorageState } from "./state.svelte"; 3 - import type * as Types from "./types"; 4 - 5 - export const pluginConfig: Record<string, Types.PluginConfig> = { 6 - subheader: { 7 - name: "Subheader Revamp", 8 - description: "Adds a clock and current period info to the subheader.", 9 - default: true, 10 - settings: { 11 - openInNewTab: { 12 - type: "toggle", 13 - name: "Open links in new tab", 14 - description: "Whether to open the class link in a new tab.", 15 - default: { toggle: true }, 16 - }, 17 - }, 18 - }, 19 - scrollSegments: { 20 - name: "Scroll Segments", 21 - description: "Segments the Schoolbox page into scrollable sections.", 22 - default: true, 23 - }, 24 - scrollPeriod: { 25 - name: "Scroll Period", 26 - description: "Scrolls to the current period on the timetable.", 27 - default: true, 28 - settings: { 29 - resetCooldownOnMouseMove: { 30 - type: "toggle", 31 - name: "Reset on mouse move", 32 - description: "Whether to reset the scrolling cooldown when you move your mouse.", 33 - default: { toggle: true }, 34 - }, 35 - cooldownDuration: { 36 - type: "slider", 37 - name: "Cooldown duration (s)", 38 - description: "How long to wait before scrolling.", 39 - default: { min: 1, max: 60, value: 10 }, 40 - }, 41 - }, 42 - }, 43 - progressBar: { 44 - name: "Progress Bar", 45 - description: "Displays a progress bar below the timetable to show the time of the day.", 46 - default: true, 47 - }, 48 - modernIcons: { 49 - name: "Modern Icons", 50 - description: "Modernise the icons across Schoolbox.", 51 - default: true, 52 - settings: { 53 - filled: { 54 - type: "toggle", 55 - name: "Filled icons", 56 - description: "Whether the icons should be filled or outlined.", 57 - default: { toggle: true }, 58 - }, 59 - }, 60 - }, 61 - tabTitle: { 62 - name: "Better Tab Titles", 63 - description: "Improves the tab titles for easier navigation.", 64 - default: true, 65 - settings: { 66 - showSubjectPrefix: { 67 - type: "toggle", 68 - name: "Show subject prefix", 69 - description: `e.g. "ENG - VCE English 1 & 2" becomes "VCE English 1 & 2"`, 70 - default: { toggle: true }, 71 - }, 72 - }, 73 - }, 74 - homepageSwitcher: { 75 - name: "Homepage Switcher", 76 - description: "The logo will switch to existing Schoolbox homepage when available.", 77 - default: true, 78 - settings: { 79 - closeCurrentTab: { 80 - type: "toggle", 81 - name: "Close current tab", 82 - description: "When switching to another tab, close the current one.", 83 - default: { toggle: false }, 84 - }, 85 - }, 86 - }, 87 - } as const; 88 - 89 - export const plugins = buildPluginsFromConfig(pluginConfig); 90 - 91 - function buildPluginsFromConfig(config: Record<string, Types.PluginConfig>): Record<Types.PluginId, Types.PluginData> { 92 - const plugins: Partial<Record<Types.PluginId, Types.PluginData>> = {}; 93 - 94 - for (const [pluginId, pluginConfig] of Object.entries(config)) { 95 - const plugin: Types.PluginData = { 96 - name: pluginConfig.name, 97 - description: pluginConfig.description, 98 - toggle: new StorageState( 99 - storage.defineItem<Types.Toggle>(`local:plugin-${pluginId}`, { 100 - fallback: { toggle: pluginConfig.default }, 101 - }), 102 - ), 103 - }; 104 - 105 - // define settings 106 - if (pluginConfig.settings) { 107 - plugin.settings = {}; 108 - 109 - // iterate over each setting and create state 110 - for (const [settingId, settingConfig] of Object.entries(pluginConfig.settings)) { 111 - let state; 112 - if (settingConfig.type === "toggle") { 113 - state = new StorageState( 114 - storage.defineItem<Types.Toggle>(`local:plugin-${pluginId}-${settingId}`, { 115 - fallback: settingConfig.default, 116 - }), 117 - ); 118 - } else { 119 - state = new StorageState( 120 - storage.defineItem<Types.Slider>(`local:plugin-${pluginId}-${settingId}`, { 121 - fallback: settingConfig.default, 122 - }), 123 - ); 124 - } 125 - plugin.settings[settingId] = { 126 - state, 127 - type: settingConfig.type, 128 - name: settingConfig.name, 129 - description: settingConfig.description, 130 - } as Types.PluginSetting; 131 - } 132 - } 133 - 134 - plugins[pluginId as Types.PluginId] = plugin; 135 - } 136 - 137 - return plugins as Record<Types.PluginId, Types.PluginData>; 138 - }
+3 -36
src/utils/storage/types.ts
··· 1 - import type { pluginConfig } from "./plugins"; 2 1 import type { snippetConfig } from "./snippets"; 3 2 import type { StorageState } from "./state.svelte"; 4 3 ··· 47 46 toggle: boolean; 48 47 } 49 48 50 - // Common for plugins and snippets 49 + // common for plugins and snippets 51 50 export interface ItemInfo { 52 51 name: string; 53 52 description: string; 54 53 } 55 54 56 - // Plugins 57 - export type PluginId = keyof typeof pluginConfig; 58 - 55 + // plugins 59 56 export type Toggle = { toggle: boolean }; 60 57 61 58 export type Slider = { ··· 64 61 max: number; 65 62 }; 66 63 67 - export type PluginData = { 68 - toggle: StorageState<Toggle>; 69 - settings?: Record<string, PluginSetting>; 70 - } & ItemInfo; 71 - 72 - export type PluginConfig = { 73 - default: boolean; 74 - settings?: Record<string, PluginSettingConfig>; 75 - } & ItemInfo; 76 - 77 - export type PluginSetting = 78 - | ({ 79 - type: "toggle"; 80 - state: StorageState<Toggle>; 81 - } & ItemInfo) 82 - | ({ 83 - type: "slider"; 84 - state: StorageState<Slider>; 85 - } & ItemInfo); 86 - 87 - export type PluginSettingConfig = 88 - | ({ 89 - type: "toggle"; 90 - default: Toggle; 91 - } & ItemInfo) 92 - | ({ 93 - type: "slider"; 94 - default: Slider; 95 - } & ItemInfo); 96 - 97 - // Snippets 64 + // snippets 98 65 export type SnippetId = keyof typeof snippetConfig; 99 66 100 67 export type SnippetData = {