schoolbox web extension :)
1import { getCurrentPeriod } from "@/utils/periodUtils";
2import { Plugin } from "@/utils/plugin";
3import styleText from "./styles.css?inline";
4import { dataAttr, injectInlineStyles, setDataAttr, uninjectInlineStyles } from "@/utils";
5import type { StorageState } from "@/utils/storage/state.svelte";
6import type { Toggle } from "@/utils/storage";
7import menu from "./Menu.svelte?url";
8
9const ID = "subheader";
10const PLUGIN_ID = `plugin-${ID}`;
11
12let intervals: NodeJS.Timeout[] = [];
13let oldChildren: ChildNode[] = [];
14let subheader: HTMLHeadingElement | null = null;
15
16export type Settings = {
17 openInNewTab: StorageState<Toggle>;
18};
19
20export default new Plugin<Settings>(
21 {
22 id: ID,
23 name: "Subheader Revamp",
24 description: "Adds a clock and current period info to the subheader.",
25 },
26 true,
27 {
28 config: {
29 openInNewTab: { toggle: true },
30 },
31 menu,
32 },
33 async (settings) => {
34 const openInNewTab = await settings.openInNewTab.get();
35 injectSubheader(openInNewTab.toggle);
36 },
37 uninjectSubheader,
38 [".subheader", ".timetable"],
39);
40
41function injectSubheader(openInNewTab: boolean) {
42 // abort if plugin is injected
43 if (subheader !== null) return;
44
45 // abort if not on homepage
46 if (window.location.pathname !== "/") return;
47
48 subheader = document.querySelector("h2.subheader");
49 if (!subheader) return;
50
51 // inject subheader styling
52 injectInlineStyles(styleText, PLUGIN_ID);
53
54 // delete all children of the subheader
55 while (subheader.firstChild) {
56 oldChildren.push(subheader.removeChild(subheader.firstChild));
57 }
58
59 updatePeriodSpan(openInNewTab);
60 updateClockSpan();
61 updateDateSpan();
62
63 intervals = [
64 setInterval(() => updatePeriodSpan(openInNewTab), 5000),
65 setInterval(updateClockSpan, 1000),
66 setInterval(updateDateSpan, 60000),
67 ];
68}
69
70function uninjectSubheader() {
71 // abort if plugin is not injected
72 if (subheader === null) return;
73
74 // abort if not on homepage
75 if (window.location.pathname !== "/") return;
76
77 // stop updating the subheader
78 intervals.forEach((interval) => clearInterval(interval));
79
80 // remove new children
81 while (subheader.firstChild) {
82 subheader.removeChild(subheader.firstChild);
83 }
84
85 // restore old children
86 for (const child of oldChildren) {
87 subheader.appendChild(child);
88 }
89
90 // uninject subheader styling
91 uninjectInlineStyles(PLUGIN_ID);
92
93 // reset variables
94 intervals = [];
95 oldChildren = [];
96 subheader = null;
97}
98
99async function updatePeriodSpan(openInNewTab: boolean) {
100 if (!subheader) return;
101
102 const periodId = `${PLUGIN_ID}-period`;
103 let periodSpan = document.querySelector<HTMLSpanElement>(`.subheader ${dataAttr(periodId)}`);
104
105 if (!periodSpan) {
106 periodSpan = document.createElement("span");
107 setDataAttr(periodSpan, periodId);
108 subheader.appendChild(periodSpan);
109 }
110
111 periodSpan.textContent = "";
112
113 // set period span content
114 const period = getCurrentPeriod();
115 if (period) {
116 const name = period.data.name || period.header.name;
117 const room = period.data.room ? ` (${period.data.room})` : "";
118 let periodLink = periodSpan.querySelector("a");
119 if (period.data.name && period.data.link) {
120 // if there's period data
121 if (!periodLink) {
122 periodLink = document.createElement("a");
123
124 periodLink.target = openInNewTab ? "_blank" : "_self";
125 periodSpan.appendChild(periodLink);
126 }
127 periodLink.href = period.data.link;
128 periodLink.textContent = `${name}${room}`;
129 } else {
130 // if there's only the header
131 periodSpan.textContent = `${name}${room}`;
132 if (periodLink) {
133 periodSpan.removeChild(periodLink);
134 }
135 }
136 }
137}
138
139function updateClockSpan() {
140 if (!subheader) return;
141
142 const clockId = `${PLUGIN_ID}-clock`;
143 let clockSpan = document.querySelector<HTMLSpanElement>(`.subheader ${dataAttr(clockId)}`);
144
145 if (!clockSpan) {
146 clockSpan = document.createElement("span");
147 setDataAttr(clockSpan, clockId);
148 subheader.appendChild(clockSpan);
149 }
150
151 // set clock span content
152 const date = new Date();
153 clockSpan.textContent = date.toLocaleTimeString([], {
154 hour: "2-digit",
155 minute: "2-digit",
156 });
157}
158
159function updateDateSpan() {
160 if (!subheader) return;
161
162 const dateId = `${PLUGIN_ID}-date`;
163 let dateSpan = document.querySelector<HTMLSpanElement>(`.subheader ${dataAttr(dateId)}`);
164
165 if (!dateSpan) {
166 dateSpan = document.createElement("span");
167 setDataAttr(dateSpan, dateId);
168 subheader.appendChild(dateSpan);
169 }
170
171 // set date span content
172 const date = new Date();
173 dateSpan.textContent = date.toDateString();
174}