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