schoolbox web extension :)
0
fork

Configure Feed

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

feat: partial user snippet and theme hot reload

willow c7ed1d74 da7713e6

+141 -27
+53 -5
src/entrypoints/start.content.ts
··· 1 1 import { browser, defineContentScript } from "#imports"; 2 - import { injectCatppuccin, injectLogo, injectStylesheet, injectUserSnippets } from "@/utils"; 2 + import { 3 + injectCatppuccin, 4 + injectLogo, 5 + injectStylesheet, 6 + uninjectStylesheet, 7 + injectUserSnippet, 8 + uninjectUserSnippet, 9 + hasChanged, 10 + } from "@/utils"; 3 11 import { EXCLUDE_MATCHES, LOGO_INFO } from "@/utils/constants"; 4 - import type { LogoId } from "@/utils/storage"; 12 + import type { LogoId, Settings } from "@/utils/storage"; 5 13 import { globalSettings, schoolboxUrls } from "@/utils/storage"; 6 14 import cssUrl from "./catppuccin.css?url"; 15 + import { WatchCallback } from "wxt/utils/storage"; 7 16 8 17 export default defineContentScript({ 9 18 matches: ["<all_urls>"], ··· 14 23 const settings = await globalSettings.storage.getValue(); 15 24 const urls = (await schoolboxUrls.storage.getValue()).urls; 16 25 26 + const updateThemes: WatchCallback<Settings> = (newValue, oldValue) => { 27 + // if global or themes was changed 28 + if (hasChanged(newValue, oldValue, ["global", "themes"])) { 29 + if (newValue.global && newValue.themes) { 30 + injectThemes(); 31 + } else { 32 + uninjectThemes(); 33 + } 34 + } 35 + }; 36 + 37 + const updateUserSnippets: WatchCallback<Settings> = (newValue, oldValue) => { 38 + // if global or userSnippets were changed 39 + if (hasChanged(newValue, oldValue, ["global", "userSnippets"])) { 40 + for (const [id, userSnippet] of Object.entries(newValue.userSnippets)) { 41 + if (newValue.global && newValue.snippets && userSnippet.toggle) { 42 + injectUserSnippet(id); 43 + } else { 44 + uninjectUserSnippet(id); 45 + } 46 + } 47 + } 48 + }; 49 + 50 + const injectThemes = () => injectStylesheet(cssUrl, "themes"); 51 + const uninjectThemes = () => uninjectStylesheet("themes"); 52 + 17 53 if (settings.global && urls.includes(window.location.origin)) { 18 54 // inject themes 19 55 if (settings.themes) { 20 - injectStylesheet(cssUrl); 56 + injectThemes(); 57 + 58 + // inject CSS variables 21 59 injectCatppuccin(settings.themeFlavour, settings.themeAccent); 22 60 } 23 61 24 62 // inject logo 25 63 injectLogo(LOGO_INFO[settings.themeLogo as LogoId], settings.themeLogoAsFavicon); 26 64 27 - // inject snippets 65 + // inject user snippets 28 66 if (settings.snippets) { 29 - injectUserSnippets(settings.userSnippets); 67 + const userSnippets = globalSettings.get().userSnippets; 68 + for (const id of Object.keys(userSnippets)) { 69 + injectUserSnippet(id); 70 + } 30 71 } 31 72 32 73 // update icon 33 74 browser.runtime.sendMessage({ updateIcon: true }); 75 + 76 + // storage listeners for hot reload 77 + // if global is toggled, inject or uninject theme and snippets 78 + globalSettings.storage.watch((newValue, oldValue) => { 79 + updateThemes(newValue, oldValue); 80 + updateUserSnippets(newValue, oldValue); 81 + }); 34 82 } 35 83 }, 36 84 });
+88 -22
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 type { LogoInfo, UserSnippet } from "./storage"; 4 + import { globalSettings } from "./storage"; 5 + import type { LogoInfo } from "./storage"; 5 6 7 + // TODO: uninjectStyles 6 8 export function injectStyles(styleText: string) { 7 - logger.info(`[content-utils] Injecting styles`); 9 + logger.info(`injecting styles`); 8 10 const style = document.createElement("style"); 9 11 style.textContent = styleText; 10 12 style.classList.add("schooltape"); ··· 12 14 } 13 15 14 16 export function injectCatppuccin(flavour: string, accent: string) { 15 - logger.info(`[content-utils] Injecting Catppuccin: ${flavour} ${accent}`); 17 + logger.info(`injecting catppuccin: ${flavour} ${accent}`); 16 18 let styleText = ":root {"; 17 19 const flavourArray = flavorEntries.find((entry) => entry[0] === flavour); 18 20 if (flavourArray) { ··· 33 35 // eslint-disable-next-line @typescript-eslint/no-explicit-any 34 36 url = browser.runtime.getURL(url as any); 35 37 } 36 - logger.info(`[content-utils] Injecting Logo: ${logo.name}`); 38 + logger.info(`injecting logo: ${logo.name}`); 37 39 if (logo.disable) { 38 40 return; 39 41 } ··· 72 74 } 73 75 74 76 // eslint-disable-next-line @typescript-eslint/no-explicit-any 75 - export function injectStylesheet(url: any) { 76 - logger.info(`[content-utils] Injecting stylesheet: ${url}`); 77 + export function injectStylesheet(url: any, id: string) { 78 + // check if stylesheet has already been injected 79 + const existingLink = document.querySelector(`link[data-schooltape="${id}"]`); 80 + if (existingLink) { 81 + logger.info(`stylesheet with id ${id} already injected, aborting`); 82 + return; 83 + } 84 + 85 + // inject stylesheet 86 + logger.info(`injecting stylesheet with id ${id}: ${url}`); 77 87 const link = document.createElement("link"); 78 88 link.rel = "stylesheet"; 79 89 link.href = browser.runtime.getURL(url); 80 - link.classList.add("schooltape"); 90 + link.dataset.schooltape = id; 81 91 document.head.appendChild(link); 82 92 } 83 93 84 - export async function injectUserSnippets(userSnippets: Record<string, UserSnippet>) { 85 - logger.info("[content-utils] Injecting snippets"); 86 - // user snippets 87 - Object.keys(userSnippets).forEach((snippetId) => { 88 - const userSnippet = userSnippets[snippetId]; 89 - if (userSnippet.toggle) { 90 - fetch(`https://gist.githubusercontent.com/${userSnippet.author}/${snippetId}/raw`) 91 - .then((response) => response.text()) 92 - .then((css) => { 93 - const style = document.createElement("style"); 94 - style.textContent = css; 95 - style.classList.add("schooltape"); 96 - document.head.appendChild(style); 97 - }); 94 + export function uninjectStylesheet(id: string) { 95 + logger.info(`uninjecting stylesheet with id ${id}`); 96 + const link = document.querySelector(`link[data-schooltape="${id}"]`); 97 + if (link) { 98 + document.head.removeChild(link); 99 + } else { 100 + // logger.warn(`stylesheet with id ${id} not found, aborting`); 101 + } 102 + } 103 + 104 + export function injectUserSnippet(id: string) { 105 + logger.info(`injecting user snippet with id ${id}`); 106 + 107 + const userSnippets = globalSettings.get().userSnippets; 108 + const snippet = userSnippets[id]; 109 + 110 + if (!snippet) { 111 + logger.error(`user snippet with id ${id} not found, aborting`); 112 + return; 113 + } 114 + 115 + if (snippet.toggle === true) { 116 + // check not already injected 117 + const style = document.querySelector(`style[data-schooltape="userSnippet-${id}"]`); 118 + if (style) { 119 + logger.info(`user snippet with id ${id} already injected, aborting`); 120 + return; 98 121 } 99 - }); 122 + 123 + // inject user snippet 124 + fetch(`https://gist.githubusercontent.com/${snippet.author}/${id}/raw`) 125 + .then((response) => response.text()) 126 + .then((css) => { 127 + const style = document.createElement("style"); 128 + style.textContent = css; 129 + style.dataset.schooltape = `userSnippet-${id}`; 130 + document.head.appendChild(style); 131 + logger.info(`injected user snippet with id ${id}`); 132 + }); 133 + } 134 + } 135 + 136 + export function uninjectUserSnippet(id: string) { 137 + logger.info(`uninjecting user snippet with id ${id}`); 138 + const style = document.querySelector(`style[data-schooltape="userSnippet-${id}"]`); 139 + if (style) { 140 + document.head.removeChild(style); 141 + logger.info(`uninjected user snippet with id ${id}`); 142 + } else { 143 + // logger.warn(`user snippet with id ${id} not found, aborting`) 144 + } 145 + } 146 + 147 + function getChangedValues<T extends Record<string, any>>(newValue: T, oldValue: T) { 148 + const changedKeys = []; 149 + 150 + for (const key in newValue) { 151 + if (newValue.hasOwnProperty(key) && oldValue[key] !== newValue[key]) { 152 + changedKeys.push(key); 153 + } 154 + } 155 + 156 + return changedKeys; 157 + } 158 + 159 + function includesSome(array: string[], items: string[]) { 160 + return items.some((item) => array.includes(item)); 161 + } 162 + 163 + export function hasChanged<T extends Record<string, any>>(newValue: T, oldValue: T, keys: string[]) { 164 + const changed = getChangedValues(newValue, oldValue); 165 + return includesSome(changed, keys); 100 166 }