Merge branch 'BetterSEQTA:main' into main

This commit is contained in:
Jones Jankovic
2026-02-01 09:51:38 +10:30
committed by GitHub
7 changed files with 1358 additions and 208 deletions
+34 -22
View File
@@ -25,24 +25,8 @@ if (document.childNodes[1]) {
init();
}
/**
* Initializes BetterSEQTA+ on a SEQTA page.
*
* This function performs the following steps:
* 1. Verifies that the current page is a SEQTA page.
* 2. Injects CSS styles for document loading.
* 3. Changes the page's favicon.
* 4. Initializes the extension's settings state.
* 5. Sets default storage if settings are not already defined.
* 6. Calls the main function to apply core BetterSEQTA+ modifications.
* 7. Initializes legacy and new plugins if the extension is enabled.
* 8. Logs success or error messages during initialization.
*/
async function init() {
const hasSEQTATitle = document.title.includes("SEQTA Learn");
if (hasSEQTAText && hasSEQTATitle && !IsSEQTAPage) {
// Verify we are on a SEQTA page
if (hasSEQTAText && document.title.includes("SEQTA Learn") && !IsSEQTAPage) {
IsSEQTAPage = true;
console.info("[BetterSEQTA+] Verified SEQTA Page");
@@ -50,13 +34,31 @@ async function init() {
documentLoadStyle.textContent = documentLoadCSS;
document.head.appendChild(documentLoadStyle);
const icons =
document.querySelectorAll<HTMLLinkElement>('link[rel*="icon"]');
replaceIcons();
icons.forEach((link) => {
link.href = icon48;
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (
mutation.type === "attributes" &&
mutation.target instanceof HTMLLinkElement &&
mutation.target.rel.includes("icon") &&
mutation.attributeName === "href"
) {
replaceIcons();
return;
}
}
});
observer.observe(document.head, {
subtree: true,
attributes: true,
attributeFilter: ["href"],
});
try {
await initializeSettingsState();
@@ -80,8 +82,18 @@ async function init() {
console.info(
"[BetterSEQTA+] Successfully initialised BetterSEQTA+, starting to load assets.",
);
} catch (error: any) {
} catch (error) {
console.error(error);
}
}
}
function replaceIcons() {
document
.querySelectorAll<HTMLLinkElement>('link[rel*="icon"]')
.forEach((link) => {
if (link.href !== icon48) {
link.href = icon48;
}
});
}
@@ -7,6 +7,20 @@ import {
import { type Plugin } from "@/plugins/core/types";
import stringToHTML from "@/seqta/utils/stringToHTML";
import { waitForElm } from "@/seqta/utils/waitForElm";
import ReactFiber from "@/seqta/utils/ReactFiber.ts";
import {
clearStuck,
getClassByPattern,
initStorage,
letterToNumber,
parseAssessments,
processAssessments,
} from "./utils.ts";
interface weightingsStorage {
weightings: Record<string, string>;
assessments: Record<string, string>;
}
const settings = defineSettings({
lettergrade: booleanSetting({
@@ -23,7 +37,7 @@ class AssessmentsAveragePluginClass extends BasePlugin<typeof settings> {
const instance = new AssessmentsAveragePluginClass();
const assessmentsAveragePlugin: Plugin<typeof settings> = {
const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
id: "assessments-average",
name: "Assessment Averages",
description: "Adds an average grade to the Assessments page",
@@ -32,8 +46,10 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
settings: instance.settings,
run: async (api) => {
await initStorage(api);
clearStuck(api);
api.seqta.onMount(".assessmentsWrapper", async () => {
// Wait for any assessment item to load first
await waitForElm(
"#main > .assessmentsWrapper .assessments [class*='AssessmentItem__AssessmentItem___']",
true,
@@ -41,26 +57,13 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
1000,
);
// Helper function to find actual class names by their base pattern
const getClassByPattern = (
element: Element | Document,
basePattern: string,
): string => {
// Find all classes on the element
const classes = Array.from(element.querySelectorAll("*"))
.flatMap((el) => Array.from(el.classList))
.filter((className) => className.startsWith(basePattern));
await parseAssessments(api);
return classes.length ? classes[0] : "";
};
// Find actual class names from the DOM
const sampleAssessmentItem = document.querySelector(
"[class*='AssessmentItem__AssessmentItem___']",
);
if (!sampleAssessmentItem) return;
// Extract all necessary class patterns from a sample assessment item
const assessmentItemClass =
Array.from(sampleAssessmentItem.classList).find((c) =>
c.startsWith("AssessmentItem__AssessmentItem___"),
@@ -83,7 +86,6 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
"AssessmentItem__title___",
);
// Get Thermoscore classes
const thermoscoreElement = document.querySelector(
"[class*='Thermoscore__Thermoscore___']",
);
@@ -102,62 +104,34 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
"Thermoscore__text___",
);
// Find assessment list
const assessmentsList = document.querySelector(
"#main > .assessmentsWrapper .assessments [class*='AssessmentList__items___']",
);
if (!assessmentsList) return;
const gradeElements = document.querySelectorAll(
"[class*='Thermoscore__text___']",
const state = await ReactFiber.find(
"[class*='AssessmentList__items___']",
).getState();
const marks = state["marks"];
if (!marks || !marks.length) return;
const assessmentItems = Array.from(
assessmentsList.querySelectorAll(
`[class*='AssessmentItem__AssessmentItem___']`,
),
).filter(
(item) =>
!item
.querySelector(`[class*='AssessmentItem__title___']`)
?.textContent?.includes("Subject Average"),
);
if (!gradeElements.length) return;
// Parse and average grades
const letterToNumber: Record<string, number> = {
"A+": 100,
A: 95,
"A-": 90,
"B+": 85,
B: 80,
"B-": 75,
"C+": 70,
C: 65,
"C-": 60,
"D+": 55,
D: 50,
"D-": 45,
"E+": 40,
E: 35,
"E-": 30,
F: 0,
};
const { weightedTotal, totalWeight, hasInaccurateWeighting, count } =
await processAssessments(api, assessmentItems);
function parseGrade(text: string): number {
const str = text.trim().toUpperCase();
if (str.includes("/")) {
const [raw, max] = str.split("/").map((n) => parseFloat(n));
return (raw / max) * 100;
}
if (str.includes("%")) {
return parseFloat(str.replace("%", "")) || 0;
}
return letterToNumber[str] ?? 0;
}
if (!count || totalWeight === 0) return;
let total = 0;
let count = 0;
gradeElements.forEach((el) => {
const grade = parseGrade(el.textContent || "");
if (grade > 0) {
total += grade;
count++;
}
});
if (!count) return;
const avg = total / count;
const avg = weightedTotal / totalWeight;
const rounded = Math.ceil(avg / 5) * 5;
const numberToLetter = Object.entries(letterToNumber).reduce(
(acc, [k, v]) => {
@@ -172,31 +146,40 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
? letterAvg
: `${avg.toFixed(2)}%`;
// Prevent duplicate
const existing = assessmentsList.querySelector(
`[class*='AssessmentItem__title___']`,
);
if (existing?.textContent === "Subject Average") return;
// Use the dynamic class names in the HTML template
const averageElement = stringToHTML(/* html */ `
let warningHTML = "";
if (hasInaccurateWeighting) {
warningHTML = /* html */ `
<div style="margin-top: 4px; font-size: 11px; color: rgba(255, 255, 255, 0.6); opacity: 0.8; line-height: 1.3;">
⚠ Some weightings unavailable
</div>
`;
}
assessmentsList.insertBefore(
stringToHTML(/* html */ `
<div class="${assessmentItemClass}">
<div class="${metaContainerClass}">
<div class="${metaClass}">
<div class="${simpleResultClass}">
<div class="${titleClass}">Subject Average</div>
${warningHTML}
</div>
</div>
</div>
<div class="${thermoscoreClass}">
<div class="${fillClass}" style="width: ${avg.toFixed(2)}%">
<div class="${textClass}" title="${display}">${display}</div>
<div class="${textClass}" title="${hasInaccurateWeighting ? display + " (some weightings unavailable)" : display}">${display}</div>
</div>
</div>
</div>
`).firstChild;
assessmentsList.insertBefore(averageElement!, assessmentsList.firstChild);
`).firstChild!,
assessmentsList.firstChild,
);
});
},
};
@@ -0,0 +1,572 @@
import { getUserInfo } from "@/seqta/ui/AddBetterSEQTAElements.ts";
import ReactFiber from "@/seqta/utils/ReactFiber.ts";
import * as pdfjs from "pdfjs-dist";
pdfjs.GlobalWorkerOptions.workerSrc =
"https://cdn.jsdelivr.net/npm/pdfjs-dist/build/pdf.worker.min.mjs";
export async function initStorage(api: any) {
await api.storage.loaded;
if (!api.storage.weightings) {
api.storage.weightings = {};
}
if (!api.storage.assessments) {
api.storage.assessments = {};
}
}
export function clearStuck(api: any) {
let hasStuckProcessing = false;
for (const key in api.storage.weightings) {
if (api.storage.weightings[key] === "processing") {
delete api.storage.weightings[key];
hasStuckProcessing = true;
}
}
if (hasStuckProcessing) {
api.storage.weightings = { ...api.storage.weightings };
}
}
// Helper function to find actual class names by their base pattern
export const getClassByPattern = (
element: Element | Document,
basePattern: string,
): string => {
const classes = Array.from(element.querySelectorAll("*"))
.flatMap((el) => Array.from(el.classList))
.filter((className) => className.startsWith(basePattern));
return classes.length ? classes[0] : "";
};
export const letterToNumber: Record<string, number> = {
"A+": 100,
A: 95,
"A-": 90,
"B+": 85,
B: 80,
"B-": 75,
"C+": 70,
C: 65,
"C-": 60,
"D+": 55,
D: 50,
"D-": 45,
"E+": 40,
E: 35,
"E-": 30,
F: 0,
};
function parseGrade(text: string): number {
const str = text.trim().toUpperCase();
if (str.includes("/")) {
const [raw, max] = str.split("/").map((n) => parseFloat(n));
return (raw / max) * 100;
}
if (str.includes("%")) {
return parseFloat(str.replace("%", "")) || 0;
}
return letterToNumber[str] ?? 0;
}
function createWeightLabel(
assessmentItem: Element,
weighting: string | undefined,
) {
const statsContainer = assessmentItem.querySelector(
`[class*='AssessmentItem__stats___']`,
) as HTMLElement;
if (
!statsContainer ||
statsContainer.querySelector(".betterseqta-weight-label")
)
return;
const label = statsContainer.querySelector(
`[class*='Label__Label___']`,
) as HTMLElement;
if (!label) return;
const weightLabel = label.cloneNode(true) as HTMLElement;
weightLabel.classList.add("betterseqta-weight-label");
const innerTextDiv = weightLabel.querySelector(
`[class*='Label__innerText___']`,
);
if (innerTextDiv) innerTextDiv.textContent = "Weight";
const textNodes = Array.from(weightLabel.childNodes).filter(
(node) => node.nodeType === Node.TEXT_NODE,
);
if (textNodes.length) {
textNodes[0].textContent =
weighting && weighting !== "processing"
? `${Number(weighting) % 1 === 0 ? Number(weighting) : weighting}%`
: "N/A";
}
statsContainer.style.position = "relative";
weightLabel.style.position = "absolute";
weightLabel.style.right = "0";
weightLabel.style.top = "50%";
weightLabel.style.transform = "translateY(-50%)";
statsContainer.appendChild(weightLabel);
}
export const isFirefox =
navigator.userAgent.toLowerCase().indexOf("firefox") > -1 &&
!navigator.userAgent.toLowerCase().includes("seamonkey") &&
!navigator.userAgent.toLowerCase().includes("waterfox");
async function fetchPDFAsArrayBuffer(url: string): Promise<ArrayBuffer> {
const isBlobUrl = url.startsWith("blob:");
if (isBlobUrl || isFirefox) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
const requestId = `pdf-fetch-${Date.now()}-${Math.random()}`;
const escapedUrl = url.replace(/'/g, "\\'");
script.textContent = `
(function() {
fetch('${escapedUrl}')
.then(response => {
if (!response.ok) {
throw new Error('HTTP ' + response.status + ': ' + response.statusText);
}
return response.arrayBuffer();
})
.then(arrayBuffer => {
window.postMessage({
type: '${requestId}',
success: true,
data: Array.from(new Uint8Array(arrayBuffer))
}, '*');
})
.catch(error => {
window.postMessage({
type: '${requestId}',
success: false,
error: error.message || String(error)
}, '*');
});
})();
`;
const messageHandler = (event: MessageEvent) => {
if (event.data?.type === requestId) {
window.removeEventListener("message", messageHandler);
if (script.parentNode) {
script.parentNode.removeChild(script);
}
if (event.data.success) {
resolve(new Uint8Array(event.data.data).buffer);
} else {
reject(new Error(event.data.error || "Failed to fetch PDF"));
}
}
};
window.addEventListener("message", messageHandler);
(document.head || document.documentElement).appendChild(script);
setTimeout(() => {
window.removeEventListener("message", messageHandler);
if (script.parentNode) {
script.parentNode.removeChild(script);
}
reject(new Error("Timeout fetching PDF"));
}, 30000);
});
}
try {
const response = await fetch(url, {
credentials: "include",
redirect: "follow",
});
if (response.url && response.url.startsWith("blob:")) {
return await fetchPDFAsArrayBuffer(response.url);
}
if (!response.ok) {
throw new Error(
`Failed to fetch PDF: ${response.status} ${response.statusText}`,
);
}
return await response.arrayBuffer();
} catch (error: any) {
if (
error?.message?.includes("blob") ||
error?.message?.includes("Security") ||
error?.message?.includes("CSP")
) {
return await fetchPDFAsArrayBuffer(url);
}
throw error;
}
}
export async function extractPDFText(url: string): Promise<string> {
try {
if (isFirefox) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
const requestId = `pdf-extract-${Date.now()}-${Math.random()}`;
const escapedUrl = url
.replace(/\\/g, "\\\\")
.replace(/'/g, "\\'")
.replace(/"/g, '\\"');
script.textContent = `
(function() {
const requestId = '${requestId}';
const url = '${escapedUrl}';
if (window.pdfjsLib) {
extractPDF();
} else {
const pdfjsScript = document.createElement('script');
pdfjsScript.src = 'https://cdn.jsdelivr.net/npm/pdfjs-dist/build/pdf.min.js';
pdfjsScript.type = 'text/javascript';
pdfjsScript.onload = function() {
extractPDF();
};
pdfjsScript.onerror = function() {
window.postMessage({
type: requestId,
success: false,
error: 'Failed to load pdfjs library'
}, '*');
};
document.head.appendChild(pdfjsScript);
}
function extractPDF() {
try {
window.pdfjsLib.GlobalWorkerOptions.workerSrc = '';
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'arraybuffer';
xhr.withCredentials = true;
xhr.onload = function() {
if (xhr.status !== 200) {
window.postMessage({
type: requestId,
success: false,
error: 'HTTP ' + xhr.status + ': ' + xhr.statusText
}, '*');
return;
}
try {
const arrayBuffer = xhr.response;
if (!arrayBuffer || arrayBuffer.byteLength === 0) {
throw new Error('PDF response is empty');
}
window.pdfjsLib.getDocument({
data: arrayBuffer,
useSystemFonts: true,
verbosity: 0,
useWorkerFetch: false,
isEvalSupported: false
}).promise
.then(pdf => {
const pagePromises = [];
for (let i = 1; i <= pdf.numPages; i++) {
pagePromises.push(
pdf.getPage(i).then(page => {
return page.getTextContent().then(content => {
return content.items.map(item => item.str).join(' ');
});
})
);
}
return Promise.all(pagePromises);
})
.then(pages => {
const text = pages.join('\\n');
window.postMessage({
type: requestId,
success: true,
text: text
}, '*');
})
.catch(error => {
window.postMessage({
type: requestId,
success: false,
error: 'PDF parsing error: ' + (error.message || String(error))
}, '*');
});
} catch (error) {
window.postMessage({
type: requestId,
success: false,
error: 'ArrayBuffer error: ' + (error.message || String(error))
}, '*');
}
};
xhr.onerror = function() {
window.postMessage({
type: requestId,
success: false,
error: 'Network error fetching PDF'
}, '*');
};
xhr.ontimeout = function() {
window.postMessage({
type: requestId,
success: false,
error: 'Timeout fetching PDF'
}, '*');
};
xhr.timeout = 30000;
xhr.send();
} catch (error) {
window.postMessage({
type: requestId,
success: false,
error: 'Setup error: ' + (error.message || String(error))
}, '*');
}
}
})();
`;
const messageHandler = (event: MessageEvent) => {
if (event.data?.type === requestId) {
window.removeEventListener("message", messageHandler);
if (script.parentNode) {
script.parentNode.removeChild(script);
}
if (event.data.success) {
resolve(event.data.text);
} else {
reject(
new Error(event.data.error || "Failed to extract PDF text"),
);
}
}
};
window.addEventListener("message", messageHandler);
(document.head || document.documentElement).appendChild(script);
setTimeout(() => {
window.removeEventListener("message", messageHandler);
if (script.parentNode) {
script.parentNode.removeChild(script);
}
reject(new Error("Timeout extracting PDF text"));
}, 60000);
});
}
const arrayBuffer = await fetchPDFAsArrayBuffer(url);
if (arrayBuffer.byteLength === 0) {
throw new Error("PDF response is empty");
}
const pdf = await pdfjs.getDocument({
data: arrayBuffer,
useSystemFonts: true,
}).promise;
let text = "";
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const content = await page.getTextContent();
text += content.items.map((item: any) => item.str).join(" ") + "\n";
}
return text;
} catch (error) {
console.error("[BetterSEQTA+] Failed to extract PDF text:", error);
throw error;
}
}
async function handleWeightings(mark: any, api: any) {
const assessmentID = mark.id;
const metaclassID = mark.metaclassID;
const userInfo = await getUserInfo();
const userID = userInfo.id;
const title = mark.title;
if (
api.storage.weightings[assessmentID] != undefined &&
api.storage.weightings[assessmentID] !== "processing"
) {
return;
}
api.storage.weightings = {
...api.storage.weightings,
[assessmentID]: "processing",
};
api.storage.assessments = {
...api.storage.assessments,
[title.trim()]: assessmentID,
};
try {
const filename =
"BetterSEQTA-" +
String(Math.floor(Math.random() * 1e15)).padStart(15, "0");
const printResponse = await fetch(
`${location.origin}/seqta/student/print/assessment`,
{
method: "POST",
headers: { "Content-Type": "application/json; charset=utf-8" },
credentials: "include",
body: JSON.stringify({
fileName: filename,
id: assessmentID,
metaclass: metaclassID,
student: userID,
}),
},
);
if (!printResponse.ok) {
throw new Error(
`Failed to generate PDF: ${printResponse.status} ${printResponse.statusText}`,
);
}
await new Promise((resolve) => setTimeout(resolve, 1000));
const pdfUrl = `${location.origin}/seqta/student/report/get?file=${filename}`;
if (pdfUrl.startsWith("blob:")) {
throw new Error(`Cannot fetch blob URL from extension: ${pdfUrl}`);
}
let text: string;
try {
text = await extractPDFText(pdfUrl);
} catch (error: any) {
if (
isFirefox &&
(error?.message?.includes("blob") ||
error?.message?.includes("Security") ||
error?.message?.includes("CSP"))
) {
await new Promise((resolve) => setTimeout(resolve, 2000));
text = await extractPDFText(pdfUrl);
} else {
throw new Error(`PDF extraction failed: ${error.message}`);
}
}
const match = text.match(/weight:\s*(\d+\.?\d*)/i);
api.storage.weightings = {
...api.storage.weightings,
[assessmentID]: match ? match[1] : "N/A",
};
} catch (error: any) {
api.storage.weightings = {
...api.storage.weightings,
[assessmentID]: "N/A",
};
}
}
export async function parseAssessments(api: any) {
const state = await ReactFiber.find(
"[class*='AssessmentList__items___']",
).getState();
const marks = state["marks"];
if (!marks) return;
await Promise.all(marks.map((mark: any) => handleWeightings(mark, api)));
}
export async function processAssessments(api: any, assessmentItems: Element[]) {
let weightedTotal = 0;
let totalWeight = 0;
let hasInaccurateWeighting = false;
let count = 0;
for (const assessmentItem of assessmentItems) {
const gradeElement = assessmentItem.querySelector(
`[class*='Thermoscore__text___']`,
);
if (!gradeElement) continue;
const grade = parseGrade(gradeElement.textContent || "");
if (grade <= 0) continue;
const titleEl = assessmentItem.querySelector(
`[class*='AssessmentItem__title___']`,
);
if (!titleEl) continue;
const title = titleEl.textContent?.trim();
if (!title) continue;
const assessmentID = api.storage.assessments?.[title];
const weighting = assessmentID
? api.storage.weightings?.[assessmentID]
: undefined;
createWeightLabel(assessmentItem, weighting);
if (
weighting === null ||
weighting === undefined ||
weighting === "N/A" ||
weighting === "processing"
) {
hasInaccurateWeighting = true;
weightedTotal += grade;
totalWeight += 1;
} else {
const weight = parseFloat(weighting);
if (!isNaN(weight) && weight >= 0) {
weightedTotal += grade * weight;
totalWeight += weight;
} else {
weightedTotal += grade;
totalWeight += 1;
hasInaccurateWeighting = true;
}
}
count++;
}
return {
weightedTotal,
totalWeight,
hasInaccurateWeighting,
count,
};
}
+85 -115
View File
@@ -14,7 +14,7 @@ let cachedUserInfo: any = null;
let LightDarkModeSnakeEggButton = 0;
async function getUserInfo() {
export async function getUserInfo() {
if (cachedUserInfo) return cachedUserInfo;
try {
@@ -30,11 +30,10 @@ async function getUserInfo() {
}),
});
const responseData = await response.json();
cachedUserInfo = responseData.payload;
cachedUserInfo = (await response.json()).payload;
return cachedUserInfo;
} catch (error) {
console.error("Error fetching user info:", error);
console.error("[BetterSEQTA+] Failed to get user info:", error);
throw error;
}
}
@@ -61,7 +60,7 @@ export async function AddBetterSEQTAElements() {
handleStudentData(),
]);
} catch (error) {
console.error("Error initializing UI elements:", error);
console.error("[BetterSEQTA+] Failed to initialize UI elements:", error);
}
setupEventListeners();
@@ -80,20 +79,18 @@ function createHomeButton(fragment: DocumentFragment, _: HTMLElement) {
div.classList.add("titlebar");
container.append(div);
const NewButton = stringToHTML(
/* html */`<li class="item" data-key="home" id="homebutton" data-path="/home" data-betterseqta="true"><label><svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="currentColor" d="M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z" /></svg><span>Home</span></label></li>`
fragment.appendChild(
stringToHTML(
/* html */ `<li class="item" data-key="home" id="homebutton" data-path="/home" data-betterseqta="true"><label><svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="currentColor" d="M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z" /></svg><span>Home</span></label></li>`,
).firstChild!,
);
if (NewButton.firstChild) {
fragment.appendChild(NewButton.firstChild);
}
}
async function handleUserInfo() {
try {
const info = await getUserInfo();
updateUserInfo(info);
updateUserInfo(await getUserInfo());
} catch (error) {
console.error("Error fetching and processing student data:", error);
console.error("[BetterSEQTA+] Failed to handle user info:", error);
}
}
@@ -117,30 +114,32 @@ function updateUserInfo(info: {
}) {
const titlebar = document.getElementsByClassName("titlebar")[0];
const userInfo = stringToHTML(/* html */ `
<div class="userInfosvgdiv tooltip">
<svg class="userInfosvg" viewBox="0 0 24 24"><path fill="var(--text-primary)" d="M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z"></path></svg>
<div class="tooltiptext topmenutooltip" id="logouttooltip"></div>
</div>
`).firstChild;
titlebar.append(userInfo!);
const userinfo = stringToHTML(/* html */ `
<div class="userInfo">
<div class="userInfoText">
<div style="display: flex; align-items: center;">
<p class="userInfohouse userInfoCode"></p>
<p class="userInfoName">${info.userDesc}</p>
</div>
<p class="userInfoCode">${info.meta.code} // ${info.meta.governmentID}</p>
titlebar.append(
stringToHTML(/* html */ `
<div class="userInfosvgdiv tooltip">
<svg class="userInfosvg" viewBox="0 0 24 24"><path fill="var(--text-primary)" d="M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z"></path></svg>
<div class="tooltiptext topmenutooltip" id="logouttooltip"></div>
</div>
</div>
`).firstChild;
titlebar.append(userinfo!);
`).firstChild!,
);
var logoutbutton = document.getElementsByClassName("logout")[0];
var userInfosvgdiv = document.getElementById("logouttooltip")!;
userInfosvgdiv.appendChild(logoutbutton);
titlebar.append(
stringToHTML(/* html */ `
<div class="userInfo">
<div class="userInfoText">
<div style="display: flex; align-items: center;">
<p class="userInfohouse userInfoCode"></p>
<p class="userInfoName">${info.userDesc}</p>
</div>
<p class="userInfoCode">${info.meta.code} // ${info.meta.governmentID}</p>
</div>
</div>
`).firstChild!,
);
document
.getElementById("logouttooltip")!
.appendChild(document.getElementsByClassName("logout")[0]);
}
async function handleStudentData() {
@@ -156,48 +155,40 @@ async function handleStudentData() {
},
);
const responseData = await response.json();
let students = responseData.payload;
await updateStudentInfo(students);
await updateStudentInfo((await response.json()).payload);
} catch (error) {
console.error("Error fetching and processing student data:", error);
console.error("[BetterSEQTA+] Failed to handle student data:", error);
}
}
async function updateStudentInfo(students: any) {
const info = await getUserInfo();
var index = students.findIndex(function (person: any) {
return (
const index = students.findIndex(
(person: any) =>
person.firstname == info.userDesc.split(" ")[0] &&
person.surname == info.userDesc.split(" ")[1]
);
});
person.surname == info.userDesc.split(" ")[1],
);
const houseelement = document.getElementsByClassName("userInfohouse")[0] as HTMLElement;
// Fallback to N/A
let text = 'N/A';
const houseelement = document.getElementsByClassName(
"userInfohouse",
)[0] as HTMLElement;
const student = students[index] ?? {};
let text = "N/A";
// If student has a house, prefer to show year + house. If no year, only show house.
if (student.house) {
text = `${student.year ?? ""}${student.house}`;
// If house_colour exists, compute colour
if (student.house_colour) {
houseelement.style.background = student.house_colour;
try {
const colorresult = GetThresholdOfColor(student.house_colour);
houseelement.style.color =
colorresult && colorresult > 300 ? "black" : "white";
} catch (err) {
// Colour calculation failed, no text colour set
} catch {
// Invalid color format, leave text color as default
}
}
} else if (student.year) {
// No house, only year will be shown
text = student.year;
}
@@ -205,15 +196,13 @@ async function updateStudentInfo(students: any) {
}
function createNewsButton(fragment: DocumentFragment, menu: HTMLElement) {
const NewsButtonStr =
'<li class="item" data-key="news" id="newsbutton" data-path="/news" data-betterseqta="true"><label><svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="currentColor" d="M20 3H4C2.89 3 2 3.89 2 5V19C2 20.11 2.89 21 4 21H20C21.11 21 22 20.11 22 19V5C22 3.89 21.11 3 20 3M5 7H10V13H5V7M19 17H5V15H19V17M19 13H12V11H19V13M19 9H12V7H19V9Z" /></svg><span>News</span></label></li>';
const NewsButton = stringToHTML(NewsButtonStr);
fragment.appendChild(
stringToHTML(
'<li class="item" data-key="news" id="newsbutton" data-path="/news" data-betterseqta="true"><label><svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="currentColor" d="M20 3H4C2.89 3 2 3.89 2 5V19C2 20.11 2.89 21 4 21H20C21.11 21 22 20.11 22 19V5C22 3.89 21.11 3 20 3M5 7H10V13H5V7M19 17H5V15H19V17M19 13H12V11H19V13M19 9H12V7H19V9Z" /></svg><span>News</span></label></li>',
).firstChild!,
);
if (NewsButton.firstChild) {
fragment.appendChild(NewsButton.firstChild);
}
let iconCover = document.createElement("div");
const iconCover = document.createElement("div");
iconCover.classList.add("icon-cover");
iconCover.id = "icon-cover";
menu.appendChild(iconCover);
@@ -252,46 +241,42 @@ function setupEventListeners() {
}
async function createSettingsButton() {
let SettingsButton = stringToHTML(/* html */ `
<button class="addedButton tooltip" id="AddedSettings">
<svg width="24" height="24" viewBox="0 0 24 24">
<g><g><path d="M23.182,6.923c-.29,0-3.662,2.122-4.142,2.4l-2.8-1.555V4.511l4.257-2.456a.518.518,0,0,0,.233-.408.479.479,0,0,0-.233-.407,6.511,6.511,0,1,0-3.327,12.107,6.582,6.582,0,0,0,6.148-4.374,5.228,5.228,0,0,0,.333-1.542A.461.461,0,0,0,23.182,6.923Z"></path><path d="M9.73,10.418,7.376,12.883c-.01.01-.021.016-.03.025L1.158,19.1a2.682,2.682,0,1,0,3.793,3.793l4.583-4.582,0,0,4.1-4.005-.037-.037A9.094,9.094,0,0,1,9.73,10.418ZM3.053,21.888A.894.894,0,1,1,3.946,21,.893.893,0,0,1,3.053,21.888Z"></path></g></g>
</svg>
${settingsState.onoff ? '<div class="tooltiptext topmenutooltip">BetterSEQTA+ Settings</div>' : ""}
</button>
`);
let ContentDiv = document.getElementById("content");
ContentDiv!.append(SettingsButton.firstChild!);
document.getElementById("content")!.append(
stringToHTML(/* html */ `
<button class="addedButton tooltip" id="AddedSettings">
<svg width="24" height="24" viewBox="0 0 24 24">
<g><g><path d="M23.182,6.923c-.29,0-3.662,2.122-4.142,2.4l-2.8-1.555V4.511l4.257-2.456a.518.518,0,0,0,.233-.408.479.479,0,0,0-.233-.407,6.511,6.511,0,1,0-3.327,12.107,6.582,6.582,0,0,0,6.148-4.374,5.228,5.228,0,0,0,.333-1.542A.461.461,0,0,0,23.182,6.923Z"></path><path d="M9.73,10.418,7.376,12.883c-.01.01-.021.016-.03.025L1.158,19.1a2.682,2.682,0,1,0,3.793,3.793l4.583-4.582,0,0,4.1-4.005-.037-.037A9.094,9.094,0,0,1,9.73,10.418ZM3.053,21.888A.894.894,0,1,1,3.946,21,.893.893,0,0,1,3.053,21.888Z"></path></g></g>
</svg>
${settingsState.onoff ? '<div class="tooltiptext topmenutooltip">BetterSEQTA+ Settings</div>' : ""}
</button>
`).firstChild!,
);
}
function GetLightDarkModeString() {
if (settingsState.DarkMode) {
return "Switch to light theme";
} else {
return "Switch to dark theme";
}
return settingsState.DarkMode
? "Switch to light theme"
: "Switch to dark theme";
}
async function addDarkLightToggle() {
const tooltipString = GetLightDarkModeString();
const SUN_ICON_SVG = /* html */ `<defs><clipPath id="__lottie_element_80"><rect width="24" height="24" x="0" y="0"></rect></clipPath></defs><g clip-path="url(#__lottie_element_80)"><g style="display: block;" transform="matrix(1,0,0,1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,-4 C-2.2100000381469727,-4 -4,-2.2100000381469727 -4,0 C-4,2.2100000381469727 -2.2100000381469727,4 0,4 C2.2100000381469727,4 4,2.2100000381469727 4,0 C4,-2.2100000381469727 2.2100000381469727,-4 0,-4z"></path></g></g><g style="display: block;" transform="matrix(1,0,0,1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,6 C-3.309999942779541,6 -6,3.309999942779541 -6,0 C-6,-3.309999942779541 -3.309999942779541,-6 0,-6 C3.309999942779541,-6 6,-3.309999942779541 6,0 C6,3.309999942779541 3.309999942779541,6 0,6z M8,-3.309999942779541 C8,-3.309999942779541 8,-8 8,-8 C8,-8 3.309999942779541,-8 3.309999942779541,-8 C3.309999942779541,-8 0,-11.3100004196167 0,-11.3100004196167 C0,-11.3100004196167 -3.309999942779541,-8 -3.309999942779541,-8 C-3.309999942779541,-8 -8,-8 -8,-8 C-8,-8 -8,-3.309999942779541 -8,-3.309999942779541 C-8,-3.309999942779541 -11.3100004196167,0 -11.3100004196167,0 C-11.3100004196167,0 -8,3.309999942779541 -8,3.309999942779541 C-8,3.309999942779541 -8,8 -8,8 C-8,8 -3.309999942779541,8 -3.309999942779541,8 C-3.309999942779541,8 0,11.3100004196167 0,11.3100004196167 C0,11.3100004196167 3.309999942779541,8 3.309999942779541,8 C3.309999942779541,8 8,8 8,8 C8,8 8,3.309999942779541 8,3.309999942779541 C8,3.309999942779541 11.3100004196167,0 11.3100004196167,0 C11.3100004196167,0 8,-3.309999942779541 8,-3.309999942779541z"></path></g></g></g>`;
const MOON_ICON_SVG = /* html */ `<defs><clipPath id="__lottie_element_263"><rect width="24" height="24" x="0" y="0"></rect></clipPath></defs><g clip-path="url(#__lottie_element_263)"><g style="display: block;" transform="matrix(1.5,0,0,1.5,7,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,-4 C-2.2100000381469727,-4 -1.2920000553131104,-2.2100000381469727 -1.2920000553131104,0 C-1.2920000553131104,2.2100000381469727 -2.2100000381469727,4 0,4 C2.2100000381469727,4 4,2.2100000381469727 4,0 C4,-2.2100000381469727 2.2100000381469727,-4 0,-4z"></path></g></g><g style="display: block;" transform="matrix(-1,0,0,-1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,6 C-3.309999942779541,6 -6,3.309999942779541 -6,0 C-6,-3.309999942779541 -3.309999942779541,-6 0,-6 C3.309999942779541,-6 6,-3.309999942779541 6,0 C6,3.309999942779541 3.309999942779541,6 0,6z M8,-3.309999942779541 C8,-3.309999942779541 8,-8 8,-8 C8,-8 3.309999942779541,-8 3.309999942779541,-8 C3.309999942779541,-8 0,-11.3100004196167 0,-11.3100004196167 C0,-11.3100004196167 -3.309999942779541,-8 -3.309999942779541,-8 C-3.309999942779541,-8 -8,-8 -8,-8 C-8,-8 -8,-3.309999942779541 -8,-3.309999942779541 C-8,-3.309999942779541 -11.3100004196167,0 -11.3100004196167,0 C-11.3100004196167,0 -8,3.309999942779541 -8,3.309999942779541 C-8,3.309999942779541 -8,8 -8,8 C-8,8 -3.309999942779541,8 -3.309999942779541,8 C-3.309999942779541,8 0,11.3100004196167 0,11.3100004196167 C0,11.3100004196167 3.309999942779541,8 3.309999942779541,8 C3.309999942779541,8 8,8 8,8 C8,8 8,3.309999942779541 8,3.309999942779541 C8,3.309999942779541 11.3100004196167,0 11.3100004196167,0 C11.3100004196167,0 8,-3.309999942779541 8,-3.309999942779541z"></path></g></g></g>`;
const initialSvgContent = settingsState.DarkMode ? SUN_ICON_SVG : MOON_ICON_SVG;
const LightDarkModeButton = stringToHTML(/* html */ `
<button class="addedButton DarkLightButton tooltip" id="LightDarkModeButton">
<svg xmlns="http://www.w3.org/2000/svg">${initialSvgContent}</svg>
<div class="tooltiptext topmenutooltip" id="darklighttooliptext">${tooltipString}</div>
</button>
`);
let ContentDiv = document.getElementById("content");
ContentDiv!.append(LightDarkModeButton.firstChild!);
document.getElementById("content")!.append(
stringToHTML(/* html */ `
<button class="addedButton DarkLightButton tooltip" id="LightDarkModeButton">
<svg xmlns="http://www.w3.org/2000/svg">${settingsState.DarkMode ? SUN_ICON_SVG : MOON_ICON_SVG}</svg>
<div class="tooltiptext topmenutooltip" id="darklighttooliptext">${GetLightDarkModeString()}</div>
</button>
`).firstChild!,
);
updateAllColors();
const lightDarkModeButtonElement = document.getElementById("LightDarkModeButton")!;
const lightDarkModeButtonElement = document.getElementById(
"LightDarkModeButton",
)!;
lightDarkModeButtonElement.addEventListener("click", async () => {
const darklightText = document.getElementById("darklighttooliptext");
@@ -303,7 +288,6 @@ async function addDarkLightToggle() {
LightDarkModeSnakeEggButton = 0;
}
if (
settingsState.originalDarkMode !== undefined &&
settingsState.selectedTheme
@@ -314,38 +298,24 @@ async function addDarkLightToggle() {
return;
}
if (!document.startViewTransition || !settingsState.animations || window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
settingsState.DarkMode = !settingsState.DarkMode;
updateAllColors();
const newSvgContent = settingsState.DarkMode ? SUN_ICON_SVG : MOON_ICON_SVG;
const svgElement = lightDarkModeButtonElement.querySelector("svg");
if (svgElement) svgElement.innerHTML = newSvgContent;
darklightText!.innerText = GetLightDarkModeString();
return;
}
settingsState.DarkMode = !settingsState.DarkMode;
updateAllColors();
const newSvgContent = settingsState.DarkMode ? SUN_ICON_SVG : MOON_ICON_SVG;
const svgElement = lightDarkModeButtonElement.querySelector("svg");
if (svgElement) svgElement.innerHTML = newSvgContent;
updateAllColors();
const svgElement = lightDarkModeButtonElement.querySelector("svg")!;
svgElement.innerHTML = settingsState.DarkMode
? SUN_ICON_SVG
: MOON_ICON_SVG;
darklightText!.innerText = GetLightDarkModeString();
});
}
function customizeMenuToggle() {
const menuToggle = document.getElementById("menuToggle");
if (menuToggle) {
menuToggle.innerHTML = "";
}
const menuToggle = document.getElementById("menuToggle")!;
menuToggle.innerHTML = "";
for (let i = 0; i < 3; i++) {
const line = document.createElement("div");
line.className = "hamburger-line";
menuToggle!.appendChild(line);
menuToggle.appendChild(line);
}
}