schoolbox web extension :)
1import { browser } from "#imports";
2import { flavorEntries } from "@catppuccin/palette";
3import { logger } from "./logger";
4import type { LogoInfo } from "./storage";
5import { globalSettings } from "./storage";
6
7export const dataAttr = (id: string) => `[data-schooltape="${id}"]`;
8export function setDataAttr(el: HTMLElement, id: string) {
9 el.dataset.schooltape = id;
10}
11
12export function injectInlineStyles(styleText: string, id: string) {
13 logger.info(`injecting styles with id ${id}`);
14 const style = document.createElement("style");
15 style.textContent = styleText;
16 setDataAttr(style, `inline-${id}`);
17 document.head.append(style);
18 // logger.info(`injected styles with id ${id}`);
19}
20
21export function uninjectInlineStyles(id: string) {
22 logger.info(`uninjecting styles with id ${id}`);
23 const style = document.querySelector(dataAttr(`inline-${id}`));
24 if (style) document.head.removeChild(style);
25}
26
27export async function injectCatppuccin() {
28 const settings = await globalSettings.get();
29 const flavour = settings.themeFlavour;
30 const accent = settings.themeAccent;
31
32 logger.info(`injecting catppuccin: ${flavour} ${accent}`);
33 let styleText = ":root {";
34 const flavourArray = flavorEntries.find((entry) => entry[0] === flavour);
35 if (flavourArray) {
36 flavourArray[1].colorEntries.map(([colorName, { hsl }]) => {
37 styleText += `--ctp-${colorName}: ${hsl.h}, ${hsl.s * 100}%, ${hsl.l * 100}%;\n`;
38 if (colorName === accent) {
39 styleText += `--ctp-accent: ${hsl.h}, ${hsl.s * 100}%, ${hsl.l * 100}%;\n`;
40 }
41 });
42 }
43 styleText += "}";
44 injectInlineStyles(styleText, "catppuccin");
45}
46
47export function uninjectCatppuccin() {
48 uninjectInlineStyles("catppuccin");
49}
50
51export function injectLogo(logo: LogoInfo, setAsFavicon: boolean) {
52 let url = logo.url;
53 if (!url.startsWith("http")) {
54 // eslint-disable-next-line @typescript-eslint/no-explicit-any
55 url = browser.runtime.getURL(url as any);
56 }
57 logger.info(`injecting logo: ${logo.name}`);
58 if (logo.disable) {
59 return;
60 }
61 const style = document.createElement("style");
62 style.classList.add("schooltape");
63 if (logo.adaptive) {
64 style.textContent = `a.logo > img { display: none !important; } a.logo { display: flex; align-items: center; justify-content: center; }`;
65 const span = document.createElement("span");
66 span.style.mask = `url("${url}") no-repeat center`;
67 span.style.maskSize = "100% 100%";
68 span.style.backgroundColor = "hsl(var(--ctp-accent))";
69 span.style.width = "100%";
70 span.style.height = "60px";
71 span.style.display = "block";
72 window.addEventListener("load", () => {
73 document.querySelectorAll("a.logo").forEach((logo) => {
74 const clonedSpan = span.cloneNode(true);
75 logo.append(clonedSpan);
76 });
77 });
78 } else {
79 style.textContent = `a.logo > img { content: url("${url}"); max-width: 30%; width: 100px; }`;
80 }
81 document.head.appendChild(style);
82
83 // inject favicon
84 if (setAsFavicon) {
85 let favicon = document.querySelector("link[rel~='icon']") as HTMLLinkElement | null;
86 if (!favicon) {
87 favicon = document.createElement("link") as HTMLLinkElement;
88 favicon.rel = "icon";
89 document.head.appendChild(favicon);
90 }
91 favicon.href = url;
92 }
93}
94
95export function injectStylesheet(url: string, id: string) {
96 // check if stylesheet has already been injected
97 const existingLink = document.querySelector(dataAttr(`stylesheet-${id}`));
98 if (existingLink) return;
99
100 // inject stylesheet
101 logger.info(`injecting stylesheet with id ${id}: ${url}`);
102 const link = document.createElement("link");
103 link.rel = "stylesheet";
104 link.href = url;
105 setDataAttr(link, `stylesheet-${id}`);
106 document.head.appendChild(link);
107}
108
109export function uninjectStylesheet(id: string) {
110 logger.info(`uninjecting stylesheet with id ${id}`);
111
112 const link = document.querySelector(dataAttr(`stylesheet-${id}`));
113 if (link) document.head.removeChild(link);
114}
115
116export async function injectUserSnippet(id: string) {
117 logger.info(`injecting user snippet with id ${id}`);
118
119 const userSnippets = (await globalSettings.get()).userSnippets;
120 const snippet = userSnippets[id];
121
122 if (!snippet) {
123 logger.error(`user snippet with id ${id} not found, aborting`);
124 return;
125 }
126
127 if (!snippet.toggle) {
128 logger.error(`trying to inject user snippet with id ${id} which is disabled, aborting`);
129 return;
130 }
131
132 // check not already injected
133 if (document.querySelector(dataAttr(`userSnippet-${id}`))) {
134 logger.info(`user snippet with id ${id} already injected, aborting`);
135 return;
136 }
137
138 // inject user snippet
139 const response = await fetch(`https://gist.githubusercontent.com/${snippet.author}/${id}/raw`);
140 const css = await response.text();
141 const style = document.createElement("style");
142
143 style.textContent = css;
144 setDataAttr(style, `userSnippet-${id}`);
145 document.head.appendChild(style);
146
147 logger.info(`injected user snippet with id ${id}`);
148}
149
150export function uninjectUserSnippet(id: string) {
151 logger.info(`uninjecting user snippet with id ${id}`);
152
153 const style = document.querySelector(dataAttr(`userSnippet-${id}`));
154 if (!style) return;
155
156 document.head.removeChild(style);
157 logger.info(`uninjected user snippet with id ${id}`);
158}
159
160export function hasChanged<T>(newValue: T, oldValue: T, keys: (keyof T)[]) {
161 const changed: (keyof T)[] = [];
162
163 for (const key in newValue) {
164 if (Object.prototype.hasOwnProperty.call(newValue, key) && oldValue[key] !== newValue[key]) {
165 changed.push(key);
166 }
167 }
168
169 return keys.some((item) => changed.includes(item));
170}