schoolbox web extension :)
0
fork

Configure Feed

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

feat(plugins): partial hot reload

willow 28530125 73c7c859

+137 -44
+10 -7
src/entrypoints/plugins/modernIcons/index.ts
··· 1 - import { injectStyles } from "@/utils"; 1 + import { injectInlineStyles, injectStylesheet, uninjectInlineStyles, uninjectStylesheet } from "@/utils"; 2 2 import { definePlugin } from "@/utils/plugin"; 3 3 import styleText from "./styles.css?inline"; 4 + import { logger } from "@/utils/logger"; 4 5 5 6 export default function init() { 6 7 definePlugin( ··· 63 64 // logger.info(fontUrl); 64 65 65 66 // inject font face 66 - const style = document.createElement("link"); 67 - style.rel = "stylesheet"; 68 - style.href = fontUrl; 69 - style.type = "text/css"; 70 - document.head.appendChild(style); 67 + injectStylesheet(fontUrl, "plugin-modernIcons"); 71 68 72 - injectStyles(styleText); 69 + // inject icon styling 70 + injectInlineStyles(styleText, "plugin-modernIcons"); 73 71 74 72 for (const [className, iconName] of Object.entries(icons)) { 75 73 insertIcon(className, iconName, settings?.toggle.filled ?? false); 76 74 } 75 + }, 76 + () => { 77 + uninjectStylesheet("plugin-modernIcons"); 78 + uninjectInlineStyles("plugin-modernIcons"); 79 + logger.warn("not implemented!"); 77 80 }, 78 81 ["nav.tab-bar .top-menu", "#overflow-nav"], 79 82 );
+23 -5
src/entrypoints/plugins/progressBar/index.ts
··· 1 - import { injectStyles } from "@/utils"; 1 + import { injectInlineStyles, schooltapeQuerySelectorAll, uninjectInlineStyles } from "@/utils"; 2 2 import type { Period } from "@/utils/periodUtils"; 3 3 import { getListOfPeriods } from "@/utils/periodUtils"; 4 4 import { definePlugin } from "@/utils/plugin"; 5 5 import styleText from "./styles.css?inline"; 6 6 7 + const ID = "progressBar"; 8 + const PLUGIN_ID = `plugin-${ID}`; 9 + 7 10 export default function init() { 8 11 definePlugin( 9 - "progressBar", 12 + ID, 10 13 () => { 11 14 if (window.location.pathname === "/" && document.querySelector(".timetable")) { 12 15 const periodList = getListOfPeriods(); ··· 15 18 progressRow.classList.add("progress-container"); 16 19 document.querySelector(".timetable > thead")?.insertAdjacentElement("beforeend", progressRow); 17 20 18 - injectStyles(styleText); 19 - insertProgressBars(periodList, progressRow); 21 + injectInlineStyles(styleText, PLUGIN_ID); 22 + injectProgressBars(periodList, progressRow); 20 23 } 21 24 }, 25 + () => { 26 + uninjectInlineStyles(PLUGIN_ID); 27 + uninjectProgressBars(); 28 + }, 22 29 [".timetable"], 23 30 ); 24 31 } 25 32 26 - function insertProgressBars(periodList: Period[], container: HTMLElement) { 33 + function getProgressBars() { 34 + return schooltapeQuerySelectorAll(PLUGIN_ID); 35 + } 36 + 37 + function injectProgressBars(periodList: Period[], container: HTMLElement) { 38 + if (getProgressBars().length > 0) return; 39 + 27 40 periodList.forEach((period) => { 28 41 const td = document.createElement("td"); 29 42 const progressBar = document.createElement("progress"); ··· 32 45 progressBar.max = 100; 33 46 progressBar.style.width = "100%"; 34 47 progressBar.value = progress; 48 + progressBar.dataset.schooltape = PLUGIN_ID; 35 49 36 50 if (progress < 100) { 37 51 const intervalId = setInterval(() => { ··· 46 60 container.appendChild(td); 47 61 }); 48 62 } 63 + 64 + function uninjectProgressBars() { 65 + getProgressBars().forEach((progressBar) => document.removeChild(progressBar)); 66 + }
+3 -2
src/entrypoints/plugins/scrollSegments/index.ts
··· 1 - import { injectStyles } from "@/utils"; 1 + import { injectInlineStyles } from "@/utils"; 2 2 import { definePlugin } from "@/utils/plugin"; 3 3 import styleText from "./styles.css?inline"; 4 4 ··· 11 11 if (content && footer) { 12 12 content.appendChild(footer); 13 13 } 14 - injectStyles(styleText); 14 + injectInlineStyles(styleText, "plugin-scrollSegments"); 15 15 }, 16 + () => {}, 16 17 ["#content", "#footer"], 17 18 ); 18 19 }
+5 -5
src/entrypoints/start.content.ts
··· 1 1 import { browser, defineContentScript } from "#imports"; 2 2 import { 3 + hasChanged, 3 4 injectCatppuccin, 4 5 injectLogo, 5 6 injectStylesheet, 6 - uninjectStylesheet, 7 7 injectUserSnippet, 8 - uninjectUserSnippet, 9 - hasChanged, 10 8 uninjectCatppuccin, 9 + uninjectStylesheet, 10 + uninjectUserSnippet, 11 11 } from "@/utils"; 12 12 import { EXCLUDE_MATCHES, LOGO_INFO } from "@/utils/constants"; 13 13 import type { LogoId, Settings } from "@/utils/storage"; 14 14 import { globalSettings, schoolboxUrls } from "@/utils/storage"; 15 + import type { WatchCallback } from "wxt/utils/storage"; 15 16 import cssUrl from "./catppuccin.css?url"; 16 - import { WatchCallback } from "wxt/utils/storage"; 17 17 18 18 export default defineContentScript({ 19 19 matches: ["<all_urls>"], ··· 50 50 } 51 51 }; 52 52 53 - const injectThemes = () => injectStylesheet(cssUrl, "themes"); 53 + const injectThemes = () => injectStylesheet(browser.runtime.getURL(cssUrl), "themes"); 54 54 const uninjectThemes = () => uninjectStylesheet("themes"); 55 55 56 56 if (settings.global && urls.includes(window.location.origin)) {
+10 -7
src/utils/index.ts
··· 1 1 import { browser } from "#imports"; 2 2 import { flavorEntries } from "@catppuccin/palette"; 3 3 import { logger } from "./logger"; 4 - import { globalSettings } from "./storage"; 5 4 import type { LogoInfo } from "./storage"; 5 + import { globalSettings } from "./storage"; 6 6 7 - function schooltapeQuerySelector(id: string) { 7 + export function schooltapeQuerySelector(id: string) { 8 8 return document.querySelector(`[data-schooltape="${id}"]`); 9 9 } 10 + export function schooltapeQuerySelectorAll(id: string) { 11 + return document.querySelectorAll(`[data-schooltape="${id}"]`); 12 + } 10 13 11 - export function injectStyles(styleText: string, id: string) { 14 + export function injectInlineStyles(styleText: string, id: string) { 12 15 logger.info(`injecting styles with id ${id}`); 13 16 const style = document.createElement("style"); 14 17 style.textContent = styleText; ··· 17 20 // logger.info(`injected styles with id ${id}`); 18 21 } 19 22 20 - export function uninjectStyles(id: string) { 23 + export function uninjectInlineStyles(id: string) { 21 24 logger.info(`uninjecting styles with id ${id}`); 22 25 const style = schooltapeQuerySelector(`inline-${id}`); 23 26 if (style) { ··· 45 48 }); 46 49 } 47 50 styleText += "}"; 48 - injectStyles(styleText, "catppuccin"); 51 + injectInlineStyles(styleText, "catppuccin"); 49 52 } 50 53 51 54 export function uninjectCatppuccin() { 52 - uninjectStyles("catppuccin"); 55 + uninjectInlineStyles("catppuccin"); 53 56 } 54 57 55 58 export function injectLogo(logo: LogoInfo, setAsFavicon: boolean) { ··· 109 112 logger.info(`injecting stylesheet with id ${id}: ${url}`); 110 113 const link = document.createElement("link"); 111 114 link.rel = "stylesheet"; 112 - link.href = browser.runtime.getURL(url); 115 + link.href = url; 113 116 link.dataset.schooltape = `stylesheet-${id}`; 114 117 document.head.appendChild(link); 115 118 }
+56 -15
src/utils/plugin.ts
··· 1 + import { hasChanged } from "."; 1 2 import { logger } from "./logger"; 2 3 import type { PluginId, PluginSetting, Slider } from "./storage"; 3 4 import { globalSettings, plugins, schoolboxUrls } from "./storage"; 4 5 5 6 export async function definePlugin( 6 7 pluginId: PluginId, 7 - callback: (settings?: { toggle: Record<string, boolean>; slider: Record<string, Slider> }) => Promise<void> | void, 8 + injectCallback: (settings?: { 9 + toggle: Record<string, boolean>; 10 + slider: Record<string, Slider>; 11 + }) => Promise<void> | void, 12 + uninjectCallback: () => void, 8 13 elementsToWaitFor: string[] = [], 9 14 ) { 10 15 const plugin = await plugins[pluginId].toggle.storage.getValue(); ··· 15 20 const urls = (await schoolboxUrls.storage.getValue()).urls; 16 21 17 22 if (plugin && typeof window !== "undefined" && urls.includes(window.location.origin)) { 23 + const inject = () => { 24 + logger.info(`injecting plugin: ${plugins[pluginId].name}`); 25 + injectCallback(getSettingsValues(plugins[pluginId]?.settings)); 26 + }; 27 + 28 + const uninject = () => { 29 + logger.info(`uninjecting plugin: ${plugins[pluginId].name}`); 30 + uninjectCallback(); 31 + }; 32 + 33 + const initWatchers = () => { 34 + // add watchers for injecting plugin 35 + globalSettings.storage.watch((newValue, oldValue) => { 36 + if (hasChanged(newValue, oldValue, ["global", "plugins"])) { 37 + if (newValue.global && newValue.plugins && plugin.toggle) { 38 + inject(); 39 + } else { 40 + uninject(); 41 + } 42 + } 43 + }); 44 + plugins[pluginId].toggle.storage.watch((newValue) => { 45 + if (newValue.toggle) { 46 + inject(); 47 + } else { 48 + uninject(); 49 + } 50 + }); 51 + 52 + // reload plugin if settings have been updated 53 + if (plugins[pluginId].settings) { 54 + for (const setting of Object.values(plugins[pluginId].settings)) { 55 + setting.state.storage.watch(() => { 56 + uninject(); 57 + if (plugins[pluginId].toggle.get().toggle) inject(); 58 + }); 59 + } 60 + } 61 + }; 62 + 63 + initWatchers(); 64 + 18 65 if (settings.global && settings.plugins && plugin.toggle) { 19 - const injectPlugin = () => { 20 - callback(getSettingsValues(plugins[pluginId]?.settings)); 21 - }; 66 + const initObserver = () => { 67 + // wait for elements to be loaded 22 68 23 - const loadPlugin = () => { 24 - // wait for elements to be loaded 25 69 if (elementsToWaitFor.length > 0) { 70 + // create an observer to wait for all elements to be loaded 26 71 const observer = new MutationObserver((_mutations, observer) => { 27 72 const allElementsPresent = elementsToWaitFor.every((selector) => document.querySelector(selector) !== null); 28 73 if (allElementsPresent) { 29 74 observer.disconnect(); 30 - logger.info(`all elements present, injecting plugin: ${plugins[pluginId].name}`); 31 - injectPlugin(); 75 + inject(); 32 76 } 33 77 }); 34 - 35 78 observer.observe(document.body, { childList: true, subtree: true }); 36 79 37 80 // check if elements are already present 38 81 const allElementsPresent = elementsToWaitFor.every((selector) => document.querySelector(selector) !== null); 39 82 if (allElementsPresent) { 40 83 observer.disconnect(); 41 - logger.info(`all elements already present, injecting plugin: ${plugins[pluginId].name}`); 42 - injectPlugin(); 84 + inject(); 43 85 } 44 86 } else { 45 87 // no elements to wait for 46 - logger.info(`injecting plugin: ${plugins[pluginId].name}`); 47 - injectPlugin(); 88 + inject(); 48 89 } 49 90 }; 50 91 51 92 if (document.body) { 52 - loadPlugin(); 93 + initObserver(); 53 94 } else { 54 - document.addEventListener("DOMContentLoaded", loadPlugin); 95 + document.addEventListener("DOMContentLoaded", initObserver); 55 96 } 56 97 } 57 98 }
+30 -3
src/utils/snippet.ts
··· 1 - import { injectStyles } from "."; 1 + import { hasChanged, injectInlineStyles, uninjectInlineStyles } from "."; 2 2 import { logger } from "./logger"; 3 3 import type { SnippetId } from "./storage"; 4 4 import { globalSettings, schoolboxUrls, snippets } from "./storage"; 5 5 6 6 export async function defineSnippet(snippetId: SnippetId, styleText: string) { 7 7 const snippet = await snippets[snippetId].toggle.storage.getValue(); 8 + const inject = () => { 9 + logger.info(`injecting snippet: ${snippets[snippetId].name}`); 10 + injectInlineStyles(styleText, `snippet-${snippetId}`); 11 + }; 12 + const uninject = () => { 13 + logger.info(`uninjecting snippet: ${snippets[snippetId].name}`); 14 + uninjectInlineStyles(`snippet-${snippetId}`); 15 + }; 8 16 9 17 logger.info(`${snippets[snippetId].name}: ${snippet.toggle ? "enabled" : "disabled"}`); 10 18 ··· 14 22 if (snippet && typeof window !== "undefined" && urls.includes(window.location.origin)) { 15 23 if (settings.global && settings.snippets && snippet.toggle) { 16 24 // inject 17 - logger.info(`Injecting snippet: ${snippets[snippetId].name}`); 18 - injectStyles(styleText); 25 + inject(); 19 26 } 20 27 } 28 + 29 + // settings watcher for uninjection/injection 30 + globalSettings.storage.watch((newValue, oldValue) => { 31 + if (hasChanged(newValue, oldValue, ["global", "snippets"])) { 32 + if (newValue.global && newValue.snippets && snippet.toggle) { 33 + inject(); 34 + } else { 35 + uninject(); 36 + } 37 + } 38 + }); 39 + snippets[snippetId].toggle.storage.watch((newValue, oldValue) => { 40 + if (hasChanged(newValue, oldValue, ["toggle"])) { 41 + if (newValue.toggle && settings.global && settings.snippets) { 42 + inject(); 43 + } else { 44 + uninject(); 45 + } 46 + } 47 + }); 21 48 }