feat: cleanup and comment removal

This commit is contained in:
SethBurkart123
2026-01-30 16:07:12 +11:00
parent cc3f06b383
commit 3aef2312d0
6 changed files with 573 additions and 764 deletions
+27
View File
@@ -42,6 +42,7 @@
"mathjs": "^14.4.0", "mathjs": "^14.4.0",
"million": "^3.1.11", "million": "^3.1.11",
"motion": "^12.4.12", "motion": "^12.4.12",
"pdfjs-dist": "^5.4.530",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"react": "17", "react": "17",
"react-best-gradient-color-picker": "3.0.11", "react-best-gradient-color-picker": "3.0.11",
@@ -265,6 +266,30 @@
"@msgpack/msgpack": ["@msgpack/msgpack@3.1.2", "", {}, "sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ=="], "@msgpack/msgpack": ["@msgpack/msgpack@3.1.2", "", {}, "sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ=="],
"@napi-rs/canvas": ["@napi-rs/canvas@0.1.89", "", { "optionalDependencies": { "@napi-rs/canvas-android-arm64": "0.1.89", "@napi-rs/canvas-darwin-arm64": "0.1.89", "@napi-rs/canvas-darwin-x64": "0.1.89", "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.89", "@napi-rs/canvas-linux-arm64-gnu": "0.1.89", "@napi-rs/canvas-linux-arm64-musl": "0.1.89", "@napi-rs/canvas-linux-riscv64-gnu": "0.1.89", "@napi-rs/canvas-linux-x64-gnu": "0.1.89", "@napi-rs/canvas-linux-x64-musl": "0.1.89", "@napi-rs/canvas-win32-arm64-msvc": "0.1.89", "@napi-rs/canvas-win32-x64-msvc": "0.1.89" } }, "sha512-7GjmkMirJHejeALCqUnZY3QwID7bbumOiLrqq2LKgxrdjdmxWQBTc6rcASa2u8wuWrH7qo4/4n/VNrOwCoKlKg=="],
"@napi-rs/canvas-android-arm64": ["@napi-rs/canvas-android-arm64@0.1.89", "", { "os": "android", "cpu": "arm64" }, "sha512-CXxQTXsjtQqKGENS8Ejv9pZOFJhOPIl2goenS+aU8dY4DygvkyagDhy/I07D1YLqrDtPvLEX5zZHt8qUdnuIpQ=="],
"@napi-rs/canvas-darwin-arm64": ["@napi-rs/canvas-darwin-arm64@0.1.89", "", { "os": "darwin", "cpu": "arm64" }, "sha512-k29cR/Zl20WLYM7M8YePevRu2VQRaKcRedYr1V/8FFHkyIQ8kShEV+MPoPGi+znvmd17Eqjy2Pk2F2kpM2umVg=="],
"@napi-rs/canvas-darwin-x64": ["@napi-rs/canvas-darwin-x64@0.1.89", "", { "os": "darwin", "cpu": "x64" }, "sha512-iUragqhBrA5FqU13pkhYBDbUD1WEAIlT8R2+fj6xHICY2nemzwMUI8OENDhRh7zuL06YDcRwENbjAVxOmaX9jg=="],
"@napi-rs/canvas-linux-arm-gnueabihf": ["@napi-rs/canvas-linux-arm-gnueabihf@0.1.89", "", { "os": "linux", "cpu": "arm" }, "sha512-y3SM9sfDWasY58ftoaI09YBFm35Ig8tosZqgahLJ2WGqawCusGNPV9P0/4PsrLOCZqGg629WxexQMY25n7zcvA=="],
"@napi-rs/canvas-linux-arm64-gnu": ["@napi-rs/canvas-linux-arm64-gnu@0.1.89", "", { "os": "linux", "cpu": "arm64" }, "sha512-NEoF9y8xq5fX8HG8aZunBom1ILdTwt7ayBzSBIwrmitk7snj4W6Fz/yN/ZOmlM1iyzHDNX5Xn0n+VgWCF8BEdA=="],
"@napi-rs/canvas-linux-arm64-musl": ["@napi-rs/canvas-linux-arm64-musl@0.1.89", "", { "os": "linux", "cpu": "arm64" }, "sha512-UQQkIEzV12/l60j1ziMjZ+mtodICNUbrd205uAhbyTw0t60CrC/EsKb5/aJWGq1wM0agvcgZV72JJCKfLS6+4w=="],
"@napi-rs/canvas-linux-riscv64-gnu": ["@napi-rs/canvas-linux-riscv64-gnu@0.1.89", "", { "os": "linux", "cpu": "none" }, "sha512-1/VmEoFaIO6ONeeEMGoWF17wOYZOl5hxDC1ios2Bkz/oQjbJJ8DY/X22vWTmvuUKWWhBVlo63pxLGZbjJU/heA=="],
"@napi-rs/canvas-linux-x64-gnu": ["@napi-rs/canvas-linux-x64-gnu@0.1.89", "", { "os": "linux", "cpu": "x64" }, "sha512-ebLuqkCuaPIkKgKH9q4+pqWi1tkPOfiTk5PM1LKR1tB9iO9sFNVSIgwEp+SJreTSbA2DK5rW8lQXiN78SjtcvA=="],
"@napi-rs/canvas-linux-x64-musl": ["@napi-rs/canvas-linux-x64-musl@0.1.89", "", { "os": "linux", "cpu": "x64" }, "sha512-w+5qxHzplvA4BkHhCaizNMLLXiI+CfP84YhpHm/PqMub4u8J0uOAv+aaGv40rYEYra5hHRWr9LUd6cfW32o9/A=="],
"@napi-rs/canvas-win32-arm64-msvc": ["@napi-rs/canvas-win32-arm64-msvc@0.1.89", "", { "os": "win32", "cpu": "arm64" }, "sha512-DmyXa5lJHcjOsDC78BM3bnEECqbK3xASVMrKfvtT/7S7Z8NGQOugvu+L7b41V6cexCd34mBWgMOsjoEBceeB1Q=="],
"@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/canvas-win32-x64-msvc@0.1.89", "", { "os": "win32", "cpu": "x64" }, "sha512-WMej0LZrIqIncQcx0JHaMXlnAG7sncwJh7obs/GBgp0xF9qABjwoRwIooMWCZkSansapKGNUHhamY6qEnFN7gA=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "1.2.0" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "1.2.0" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
@@ -1133,6 +1158,8 @@
"pathval": ["pathval@1.1.1", "", {}, "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ=="], "pathval": ["pathval@1.1.1", "", {}, "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ=="],
"pdfjs-dist": ["pdfjs-dist@5.4.530", "", { "optionalDependencies": { "@napi-rs/canvas": "^0.1.84" } }, "sha512-r1hWsSIGGmyYUAHR26zSXkxYWLXLMd6AwqcaFYG9YUZ0GBf5GvcjJSeo512tabM4GYFhxhl5pMCmPr7Q72Rq2Q=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
+8 -25
View File
@@ -25,35 +25,18 @@ if (document.childNodes[1]) {
init(); 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() { async function init() {
const hasSEQTATitle = document.title.includes("SEQTA Learn"); if (hasSEQTAText && document.title.includes("SEQTA Learn") && !IsSEQTAPage) {
if (hasSEQTAText && hasSEQTATitle && !IsSEQTAPage) {
// Verify we are on a SEQTA page
IsSEQTAPage = true; IsSEQTAPage = true;
console.info("[BetterSEQTA+] Verified SEQTA Page"); console.info("[BetterSEQTA+] Verified SEQTA Page");
const documentLoadStyle = document.createElement("style"); const style = document.createElement("style");
documentLoadStyle.textContent = documentLoadCSS; style.textContent = documentLoadCSS;
document.head.appendChild(documentLoadStyle); document.head.appendChild(style);
const icons = document
document.querySelectorAll<HTMLLinkElement>('link[rel*="icon"]'); .querySelectorAll<HTMLLinkElement>('link[rel*="icon"]')
.forEach((link) => {
icons.forEach((link) => {
link.href = icon48; link.href = icon48;
}); });
@@ -80,7 +63,7 @@ async function init() {
console.info( console.info(
"[BetterSEQTA+] Successfully initialised BetterSEQTA+, starting to load assets.", "[BetterSEQTA+] Successfully initialised BetterSEQTA+, starting to load assets.",
); );
} catch (error: any) { } catch (error) {
console.error(error); console.error(error);
} }
} }
@@ -8,9 +8,15 @@ import { type Plugin } from "@/plugins/core/types";
import stringToHTML from "@/seqta/utils/stringToHTML"; import stringToHTML from "@/seqta/utils/stringToHTML";
import { waitForElm } from "@/seqta/utils/waitForElm"; import { waitForElm } from "@/seqta/utils/waitForElm";
import ReactFiber from "@/seqta/utils/ReactFiber.ts"; import ReactFiber from "@/seqta/utils/ReactFiber.ts";
import { clearStuck, getClassByPattern, initStorage, letterToNumber, parseAssessments, processAssessments} from "./utils.ts"; import {
clearStuck,
getClassByPattern,
initStorage,
letterToNumber,
parseAssessments,
processAssessments,
} from "./utils.ts";
// Storage
interface weightingsStorage { interface weightingsStorage {
weightings: Record<string, string>; weightings: Record<string, string>;
assessments: Record<string, string>; assessments: Record<string, string>;
@@ -40,11 +46,7 @@ const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
settings: instance.settings, settings: instance.settings,
run: async (api) => { run: async (api) => {
// Ensure storage is ready for use
await initStorage(api); await initStorage(api);
// Clear any stuck "processing" states so they can retry
clearStuck(api); clearStuck(api);
api.seqta.onMount(".assessmentsWrapper", async () => { api.seqta.onMount(".assessmentsWrapper", async () => {
@@ -57,13 +59,11 @@ const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
await parseAssessments(api); await parseAssessments(api);
// Find actual class names from the DOM
const sampleAssessmentItem = document.querySelector( const sampleAssessmentItem = document.querySelector(
"[class*='AssessmentItem__AssessmentItem___']", "[class*='AssessmentItem__AssessmentItem___']",
); );
if (!sampleAssessmentItem) return; if (!sampleAssessmentItem) return;
// Extract all necessary class patterns from a sample assessment item
const assessmentItemClass = const assessmentItemClass =
Array.from(sampleAssessmentItem.classList).find((c) => Array.from(sampleAssessmentItem.classList).find((c) =>
c.startsWith("AssessmentItem__AssessmentItem___"), c.startsWith("AssessmentItem__AssessmentItem___"),
@@ -86,7 +86,6 @@ const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
"AssessmentItem__title___", "AssessmentItem__title___",
); );
// Get Thermoscore classes
const thermoscoreElement = document.querySelector( const thermoscoreElement = document.querySelector(
"[class*='Thermoscore__Thermoscore___']", "[class*='Thermoscore__Thermoscore___']",
); );
@@ -105,36 +104,30 @@ const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
"Thermoscore__text___", "Thermoscore__text___",
); );
// Find assessment list
const assessmentsList = document.querySelector( const assessmentsList = document.querySelector(
"#main > .assessmentsWrapper .assessments [class*='AssessmentList__items___']", "#main > .assessmentsWrapper .assessments [class*='AssessmentList__items___']",
); );
if (!assessmentsList) return; if (!assessmentsList) return;
// Get marks from React state to match with DOM elements
const state = await ReactFiber.find( const state = await ReactFiber.find(
"[class*='AssessmentList__items___']", "[class*='AssessmentList__items___']",
).getState(); ).getState();
const marks = state["marks"]; const marks = state["marks"];
if (!marks || !marks.length) return; if (!marks || !marks.length) return;
// Parse and average grades
// Get all assessment items (excluding the average we might have added)
const assessmentItems = Array.from( const assessmentItems = Array.from(
assessmentsList.querySelectorAll(`[class*='AssessmentItem__AssessmentItem___']`), assessmentsList.querySelectorAll(
`[class*='AssessmentItem__AssessmentItem___']`,
),
).filter( ).filter(
(item) => (item) =>
!item.querySelector(`[class*='AssessmentItem__title___']`)?.textContent?.includes("Subject Average"), !item
.querySelector(`[class*='AssessmentItem__title___']`)
?.textContent?.includes("Subject Average"),
); );
// Tally up weightedTotal, totalWeight, count, determine if weighting is accurate, and display a weight label per assessment const { weightedTotal, totalWeight, hasInaccurateWeighting, count } =
const { await processAssessments(api, assessmentItems);
weightedTotal,
totalWeight,
hasInaccurateWeighting,
count,
} = await processAssessments(api, assessmentItems);
if (!count || totalWeight === 0) return; if (!count || totalWeight === 0) return;
@@ -153,13 +146,11 @@ const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
? letterAvg ? letterAvg
: `${avg.toFixed(2)}%`; : `${avg.toFixed(2)}%`;
// Prevent duplicate
const existing = assessmentsList.querySelector( const existing = assessmentsList.querySelector(
`[class*='AssessmentItem__title___']`, `[class*='AssessmentItem__title___']`,
); );
if (existing?.textContent === "Subject Average") return; if (existing?.textContent === "Subject Average") return;
// Build warning message if needed
let warningHTML = ""; let warningHTML = "";
if (hasInaccurateWeighting) { if (hasInaccurateWeighting) {
warningHTML = /* html */ ` warningHTML = /* html */ `
@@ -169,8 +160,8 @@ const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
`; `;
} }
// Use the dynamic class names in the HTML template assessmentsList.insertBefore(
const averageElement = stringToHTML(/* html */ ` stringToHTML(/* html */ `
<div class="${assessmentItemClass}"> <div class="${assessmentItemClass}">
<div class="${metaContainerClass}"> <div class="${metaContainerClass}">
<div class="${metaClass}"> <div class="${metaClass}">
@@ -182,13 +173,13 @@ const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
</div> </div>
<div class="${thermoscoreClass}"> <div class="${thermoscoreClass}">
<div class="${fillClass}" style="width: ${avg.toFixed(2)}%"> <div class="${fillClass}" style="width: ${avg.toFixed(2)}%">
<div class="${textClass}" title="${hasInaccurateWeighting ? display + ' (some weightings unavailable)' : display}">${display}</div> <div class="${textClass}" title="${hasInaccurateWeighting ? display + " (some weightings unavailable)" : display}">${display}</div>
</div> </div>
</div> </div>
</div> </div>
`).firstChild; `).firstChild!,
assessmentsList.firstChild,
assessmentsList.insertBefore(averageElement!, assessmentsList.firstChild); );
}); });
}, },
}; };
@@ -33,7 +33,6 @@ export const getClassByPattern = (
element: Element | Document, element: Element | Document,
basePattern: string, basePattern: string,
): string => { ): string => {
// Find all classes on the element
const classes = Array.from(element.querySelectorAll("*")) const classes = Array.from(element.querySelectorAll("*"))
.flatMap((el) => Array.from(el.classList)) .flatMap((el) => Array.from(el.classList))
.filter((className) => className.startsWith(basePattern)); .filter((className) => className.startsWith(basePattern));
@@ -72,37 +71,38 @@ function parseGrade(text: string): number {
return letterToNumber[str] ?? 0; return letterToNumber[str] ?? 0;
} }
function createWeightLabel(assessmentItem: Element, weighting: string | undefined) { function createWeightLabel(
assessmentItem: Element,
weighting: string | undefined,
) {
const statsContainer = assessmentItem.querySelector( const statsContainer = assessmentItem.querySelector(
`[class*='AssessmentItem__stats___']`, `[class*='AssessmentItem__stats___']`,
) as HTMLElement; ) as HTMLElement;
if (statsContainer) { if (
// Only add label if it hasn't been added before !statsContainer ||
if (!statsContainer.querySelector(".betterseqta-weight-label")) { statsContainer.querySelector(".betterseqta-weight-label")
)
return;
const label = statsContainer.querySelector( const label = statsContainer.querySelector(
`[class*='Label__Label___']`, `[class*='Label__Label___']`,
) as HTMLElement; ) as HTMLElement;
if (label) { if (!label) return;
// Clone average score node
const weightLabel = label.cloneNode(true) as HTMLElement;
// Mark as added to prevent duplicates const weightLabel = label.cloneNode(true) as HTMLElement;
weightLabel.classList.add("betterseqta-weight-label"); weightLabel.classList.add("betterseqta-weight-label");
const innerTextDiv = weightLabel.querySelector( const innerTextDiv = weightLabel.querySelector(
`[class*='Label__innerText___']`, `[class*='Label__innerText___']`,
); );
// Repurpose for showing weight
if (innerTextDiv) innerTextDiv.textContent = "Weight"; if (innerTextDiv) innerTextDiv.textContent = "Weight";
const textNodes = Array.from(weightLabel.childNodes).filter( const textNodes = Array.from(weightLabel.childNodes).filter(
(node) => node.nodeType === Node.TEXT_NODE, (node) => node.nodeType === Node.TEXT_NODE,
); );
// Set weight value, discarding useless decimals (.0)
if (textNodes.length) { if (textNodes.length) {
textNodes[0].textContent = textNodes[0].textContent =
weighting && weighting !== "processing" weighting && weighting !== "processing"
@@ -110,7 +110,6 @@ function createWeightLabel(assessmentItem: Element, weighting: string | undefine
: "N/A"; : "N/A";
} }
// Set position opposite to the average score node
statsContainer.style.position = "relative"; statsContainer.style.position = "relative";
weightLabel.style.position = "absolute"; weightLabel.style.position = "absolute";
weightLabel.style.right = "0"; weightLabel.style.right = "0";
@@ -118,30 +117,20 @@ function createWeightLabel(assessmentItem: Element, weighting: string | undefine
weightLabel.style.transform = "translateY(-50%)"; weightLabel.style.transform = "translateY(-50%)";
statsContainer.appendChild(weightLabel); statsContainer.appendChild(weightLabel);
}
}
}
} }
// Detect Firefox (has stricter CSP for blob URLs) export const isFirefox =
// Use userAgent instead of deprecated InstallTrigger navigator.userAgent.toLowerCase().indexOf("firefox") > -1 &&
export const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 && !navigator.userAgent.toLowerCase().includes("seamonkey") &&
!navigator.userAgent.toLowerCase().includes('seamonkey') && !navigator.userAgent.toLowerCase().includes("waterfox");
!navigator.userAgent.toLowerCase().includes('waterfox');
async function fetchPDFAsArrayBuffer(url: string): Promise<ArrayBuffer> { async function fetchPDFAsArrayBuffer(url: string): Promise<ArrayBuffer> {
// Detect if URL is a blob URL
const isBlobUrl = url.startsWith("blob:"); const isBlobUrl = url.startsWith("blob:");
// For Firefox, ALWAYS use page context to avoid any CSP issues
// For blob URLs in any browser, use page context
if (isBlobUrl || isFirefox) { if (isBlobUrl || isFirefox) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Inject script into page context to fetch (bypasses Firefox CSP restrictions)
const script = document.createElement("script"); const script = document.createElement("script");
const requestId = `pdf-fetch-${Date.now()}-${Math.random()}`; const requestId = `pdf-fetch-${Date.now()}-${Math.random()}`;
// Escape URL for use in script
const escapedUrl = url.replace(/'/g, "\\'"); const escapedUrl = url.replace(/'/g, "\\'");
script.textContent = ` script.textContent = `
@@ -178,9 +167,7 @@ async function fetchPDFAsArrayBuffer(url: string): Promise<ArrayBuffer> {
} }
if (event.data.success) { if (event.data.success) {
// Convert back to ArrayBuffer resolve(new Uint8Array(event.data.data).buffer);
const uint8Array = new Uint8Array(event.data.data);
resolve(uint8Array.buffer);
} else { } else {
reject(new Error(event.data.error || "Failed to fetch PDF")); reject(new Error(event.data.error || "Failed to fetch PDF"));
} }
@@ -190,7 +177,6 @@ async function fetchPDFAsArrayBuffer(url: string): Promise<ArrayBuffer> {
window.addEventListener("message", messageHandler); window.addEventListener("message", messageHandler);
(document.head || document.documentElement).appendChild(script); (document.head || document.documentElement).appendChild(script);
// Timeout after 30 seconds
setTimeout(() => { setTimeout(() => {
window.removeEventListener("message", messageHandler); window.removeEventListener("message", messageHandler);
if (script.parentNode) { if (script.parentNode) {
@@ -199,17 +185,15 @@ async function fetchPDFAsArrayBuffer(url: string): Promise<ArrayBuffer> {
reject(new Error("Timeout fetching PDF")); reject(new Error("Timeout fetching PDF"));
}, 30000); }, 30000);
}); });
} else { }
// Regular URL - fetch normally, but check if response URL becomes blob
try { try {
const response = await fetch(url, { const response = await fetch(url, {
credentials: "include", credentials: "include",
redirect: "follow", redirect: "follow",
}); });
// Check if response URL is a blob URL (server might redirect to blob)
if (response.url && response.url.startsWith("blob:")) { if (response.url && response.url.startsWith("blob:")) {
// Re-fetch using page context
return await fetchPDFAsArrayBuffer(response.url); return await fetchPDFAsArrayBuffer(response.url);
} }
@@ -221,28 +205,24 @@ async function fetchPDFAsArrayBuffer(url: string): Promise<ArrayBuffer> {
return await response.arrayBuffer(); return await response.arrayBuffer();
} catch (error: any) { } catch (error: any) {
// If error mentions blob or security, try using page context
if ( if (
error?.message?.includes("blob") || error?.message?.includes("blob") ||
error?.message?.includes("Security") || error?.message?.includes("Security") ||
error?.message?.includes("CSP") error?.message?.includes("CSP")
) { ) {
// Force use page context
return await fetchPDFAsArrayBuffer(url); return await fetchPDFAsArrayBuffer(url);
} }
throw error; throw error;
} }
}
} }
export async function extractPDFText(url: string): Promise<string> { export async function extractPDFText(url: string): Promise<string> {
// For Firefox, do everything in page context to avoid blob URL CSP issues try {
if (isFirefox) { if (isFirefox) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const script = document.createElement("script"); const script = document.createElement("script");
const requestId = `pdf-extract-${Date.now()}-${Math.random()}`; const requestId = `pdf-extract-${Date.now()}-${Math.random()}`;
// Escape URL for use in script (handle both single and double quotes)
const escapedUrl = url const escapedUrl = url
.replace(/\\/g, "\\\\") .replace(/\\/g, "\\\\")
.replace(/'/g, "\\'") .replace(/'/g, "\\'")
@@ -253,11 +233,9 @@ export async function extractPDFText(url: string): Promise<string> {
const requestId = '${requestId}'; const requestId = '${requestId}';
const url = '${escapedUrl}'; const url = '${escapedUrl}';
// Check if pdfjs is already loaded
if (window.pdfjsLib) { if (window.pdfjsLib) {
extractPDF(); extractPDF();
} else { } else {
// Load pdfjs in page context
const pdfjsScript = document.createElement('script'); const pdfjsScript = document.createElement('script');
pdfjsScript.src = 'https://cdn.jsdelivr.net/npm/pdfjs-dist/build/pdf.min.js'; pdfjsScript.src = 'https://cdn.jsdelivr.net/npm/pdfjs-dist/build/pdf.min.js';
pdfjsScript.type = 'text/javascript'; pdfjsScript.type = 'text/javascript';
@@ -278,11 +256,8 @@ export async function extractPDFText(url: string): Promise<string> {
function extractPDF() { function extractPDF() {
try { try {
// Disable worker for Firefox to avoid blob URL CSP issues
// Set to empty string to disable worker completely
window.pdfjsLib.GlobalWorkerOptions.workerSrc = ''; window.pdfjsLib.GlobalWorkerOptions.workerSrc = '';
// Use XMLHttpRequest instead of fetch for better blob URL handling
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open('GET', url, true); xhr.open('GET', url, true);
xhr.responseType = 'arraybuffer'; xhr.responseType = 'arraybuffer';
@@ -308,7 +283,6 @@ export async function extractPDFText(url: string): Promise<string> {
data: arrayBuffer, data: arrayBuffer,
useSystemFonts: true, useSystemFonts: true,
verbosity: 0, verbosity: 0,
// Explicitly disable worker
useWorkerFetch: false, useWorkerFetch: false,
isEvalSupported: false isEvalSupported: false
}).promise }).promise
@@ -388,7 +362,9 @@ export async function extractPDFText(url: string): Promise<string> {
if (event.data.success) { if (event.data.success) {
resolve(event.data.text); resolve(event.data.text);
} else { } else {
reject(new Error(event.data.error || "Failed to extract PDF text")); reject(
new Error(event.data.error || "Failed to extract PDF text"),
);
} }
} }
}; };
@@ -396,7 +372,6 @@ export async function extractPDFText(url: string): Promise<string> {
window.addEventListener("message", messageHandler); window.addEventListener("message", messageHandler);
(document.head || document.documentElement).appendChild(script); (document.head || document.documentElement).appendChild(script);
// Timeout after 60 seconds (PDF parsing can take time)
setTimeout(() => { setTimeout(() => {
window.removeEventListener("message", messageHandler); window.removeEventListener("message", messageHandler);
if (script.parentNode) { if (script.parentNode) {
@@ -405,21 +380,18 @@ export async function extractPDFText(url: string): Promise<string> {
reject(new Error("Timeout extracting PDF text")); reject(new Error("Timeout extracting PDF text"));
}, 60000); }, 60000);
}); });
} else { }
// Chrome - use extension context
try {
const arrayBuffer = await fetchPDFAsArrayBuffer(url); const arrayBuffer = await fetchPDFAsArrayBuffer(url);
if (arrayBuffer.byteLength === 0) { if (arrayBuffer.byteLength === 0) {
throw new Error("PDF response is empty"); throw new Error("PDF response is empty");
} }
const loadingTask = pdfjs.getDocument({ const pdf = await pdfjs.getDocument({
data: arrayBuffer, data: arrayBuffer,
useSystemFonts: true, useSystemFonts: true,
}); }).promise;
const pdf = await loadingTask.promise;
let text = ""; let text = "";
@@ -431,10 +403,9 @@ export async function extractPDFText(url: string): Promise<string> {
return text; return text;
} catch (error) { } catch (error) {
console.log(error); console.error("[BetterSEQTA+] Failed to extract PDF text:", error);
throw error; throw error;
} }
}
} }
async function handleWeightings(mark: any, api: any) { async function handleWeightings(mark: any, api: any) {
@@ -444,7 +415,6 @@ async function handleWeightings(mark: any, api: any) {
const userID = userInfo.id; const userID = userInfo.id;
const title = mark.title; const title = mark.title;
// Skip if already processed (not "processing")
if ( if (
api.storage.weightings[assessmentID] != undefined && api.storage.weightings[assessmentID] != undefined &&
api.storage.weightings[assessmentID] !== "processing" api.storage.weightings[assessmentID] !== "processing"
@@ -452,13 +422,11 @@ async function handleWeightings(mark: any, api: any) {
return; return;
} }
// Set to processing
api.storage.weightings = { api.storage.weightings = {
...api.storage.weightings, ...api.storage.weightings,
[assessmentID]: "processing", [assessmentID]: "processing",
}; };
// Correlate assessment title with its ID
api.storage.assessments = { api.storage.assessments = {
...api.storage.assessments, ...api.storage.assessments,
[title.trim()]: assessmentID, [title.trim()]: assessmentID,
@@ -490,20 +458,16 @@ async function handleWeightings(mark: any, api: any) {
); );
} }
// Wait a bit for the PDF to be generated
await new Promise((resolve) => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
const pdfUrl = `${location.origin}/seqta/student/report/get?file=${filename}`; const pdfUrl = `${location.origin}/seqta/student/report/get?file=${filename}`;
// Check if URL is a blob URL (which extensions can't access)
if (pdfUrl.startsWith("blob:")) { if (pdfUrl.startsWith("blob:")) {
throw new Error(`Cannot fetch blob URL from extension: ${pdfUrl}`); throw new Error(`Cannot fetch blob URL from extension: ${pdfUrl}`);
} }
let text: string; let text: string;
try { try {
// For Firefox, extractPDFText already handles everything in page context
// For Chrome, it uses extension context
text = await extractPDFText(pdfUrl); text = await extractPDFText(pdfUrl);
} catch (error: any) { } catch (error: any) {
if ( if (
@@ -513,26 +477,17 @@ async function handleWeightings(mark: any, api: any) {
error?.message?.includes("CSP")) error?.message?.includes("CSP"))
) { ) {
await new Promise((resolve) => setTimeout(resolve, 2000)); await new Promise((resolve) => setTimeout(resolve, 2000));
try {
text = await extractPDFText(pdfUrl); text = await extractPDFText(pdfUrl);
} catch (retryError: any) {
throw new Error(
`PDF extraction failed after retry: ${retryError.message}`,
);
}
} else { } else {
throw new Error(`PDF extraction failed: ${error.message}`); throw new Error(`PDF extraction failed: ${error.message}`);
} }
} }
// Match weighting from extracted text
const match = text.match(/weight:\s*(\d+\.?\d*)/i); const match = text.match(/weight:\s*(\d+\.?\d*)/i);
const weight = match ? match[1] : "N/A";
// Store and correlate weight with assessment ID
api.storage.weightings = { api.storage.weightings = {
...api.storage.weightings, ...api.storage.weightings,
[assessmentID]: weight, [assessmentID]: match ? match[1] : "N/A",
}; };
} catch (error: any) { } catch (error: any) {
api.storage.weightings = { api.storage.weightings = {
@@ -550,11 +505,9 @@ export async function parseAssessments(api: any) {
const marks = state["marks"]; const marks = state["marks"];
if (!marks) return; if (!marks) return;
// Dispatch for all assessments asynchronously
await Promise.all(marks.map((mark: any) => handleWeightings(mark, api))); await Promise.all(marks.map((mark: any) => handleWeightings(mark, api)));
} }
// Tally up weightedTotal, totalWeight, count, determine if weighting is accurate, and display a weight label per assessment
export async function processAssessments(api: any, assessmentItems: Element[]) { export async function processAssessments(api: any, assessmentItems: Element[]) {
let weightedTotal = 0; let weightedTotal = 0;
let totalWeight = 0; let totalWeight = 0;
@@ -579,16 +532,13 @@ export async function processAssessments(api: any, assessmentItems: Element[]) {
const title = titleEl.textContent?.trim(); const title = titleEl.textContent?.trim();
if (!title) continue; if (!title) continue;
// Get correlated assessment ID in order to fetch weightings
const assessmentID = api.storage.assessments?.[title]; const assessmentID = api.storage.assessments?.[title];
const weighting = assessmentID const weighting = assessmentID
? api.storage.weightings?.[assessmentID] ? api.storage.weightings?.[assessmentID]
: undefined; : undefined;
// Creates a weighting label next to the average score
createWeightLabel(assessmentItem, weighting); createWeightLabel(assessmentItem, weighting);
// Check if weighting is unavailable or still processing
if ( if (
weighting === null || weighting === null ||
weighting === undefined || weighting === undefined ||
@@ -596,18 +546,15 @@ export async function processAssessments(api: any, assessmentItems: Element[]) {
weighting === "processing" weighting === "processing"
) { ) {
hasInaccurateWeighting = true; hasInaccurateWeighting = true;
// Fall back to equal weighting if unavailable
weightedTotal += grade; weightedTotal += grade;
totalWeight += 1; totalWeight += 1;
} else { } else {
const weight = parseFloat(weighting); const weight = parseFloat(weighting);
// If weight is a positive number, add to total
if (!isNaN(weight) && weight >= 0) { if (!isNaN(weight) && weight >= 0) {
weightedTotal += grade * weight; weightedTotal += grade * weight;
totalWeight += weight; totalWeight += weight;
} else { } else {
// Invalid weight, use equal weighting
weightedTotal += grade; weightedTotal += grade;
totalWeight += 1; totalWeight += 1;
hasInaccurateWeighting = true; hasInaccurateWeighting = true;
+59 -89
View File
@@ -30,11 +30,10 @@ export async function getUserInfo() {
}), }),
}); });
const responseData = await response.json(); cachedUserInfo = (await response.json()).payload;
cachedUserInfo = responseData.payload;
return cachedUserInfo; return cachedUserInfo;
} catch (error) { } catch (error) {
console.error("Error fetching user info:", error); console.error("[BetterSEQTA+] Failed to get user info:", error);
throw error; throw error;
} }
} }
@@ -61,7 +60,7 @@ export async function AddBetterSEQTAElements() {
handleStudentData(), handleStudentData(),
]); ]);
} catch (error) { } catch (error) {
console.error("Error initializing UI elements:", error); console.error("[BetterSEQTA+] Failed to initialize UI elements:", error);
} }
setupEventListeners(); setupEventListeners();
@@ -80,20 +79,18 @@ function createHomeButton(fragment: DocumentFragment, _: HTMLElement) {
div.classList.add("titlebar"); div.classList.add("titlebar");
container.append(div); container.append(div);
const NewButton = stringToHTML( fragment.appendChild(
/* 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>` 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() { async function handleUserInfo() {
try { try {
const info = await getUserInfo(); updateUserInfo(await getUserInfo());
updateUserInfo(info);
} catch (error) { } catch (error) {
console.error("Error fetching and processing student data:", error); console.error("[BetterSEQTA+] Failed to handle user info:", error);
} }
} }
@@ -117,15 +114,17 @@ function updateUserInfo(info: {
}) { }) {
const titlebar = document.getElementsByClassName("titlebar")[0]; const titlebar = document.getElementsByClassName("titlebar")[0];
const userInfo = stringToHTML(/* html */ ` titlebar.append(
stringToHTML(/* html */ `
<div class="userInfosvgdiv tooltip"> <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> <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 class="tooltiptext topmenutooltip" id="logouttooltip"></div>
</div> </div>
`).firstChild; `).firstChild!,
titlebar.append(userInfo!); );
const userinfo = stringToHTML(/* html */ ` titlebar.append(
stringToHTML(/* html */ `
<div class="userInfo"> <div class="userInfo">
<div class="userInfoText"> <div class="userInfoText">
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
@@ -135,12 +134,12 @@ function updateUserInfo(info: {
<p class="userInfoCode">${info.meta.code} // ${info.meta.governmentID}</p> <p class="userInfoCode">${info.meta.code} // ${info.meta.governmentID}</p>
</div> </div>
</div> </div>
`).firstChild; `).firstChild!,
titlebar.append(userinfo!); );
var logoutbutton = document.getElementsByClassName("logout")[0]; document
var userInfosvgdiv = document.getElementById("logouttooltip")!; .getElementById("logouttooltip")!
userInfosvgdiv.appendChild(logoutbutton); .appendChild(document.getElementsByClassName("logout")[0]);
} }
async function handleStudentData() { async function handleStudentData() {
@@ -156,48 +155,40 @@ async function handleStudentData() {
}, },
); );
const responseData = await response.json(); await updateStudentInfo((await response.json()).payload);
let students = responseData.payload;
await updateStudentInfo(students);
} catch (error) { } 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) { async function updateStudentInfo(students: any) {
const info = await getUserInfo(); const info = await getUserInfo();
var index = students.findIndex(function (person: any) { const index = students.findIndex(
return ( (person: any) =>
person.firstname == info.userDesc.split(" ")[0] && 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; const houseelement = document.getElementsByClassName(
"userInfohouse",
// Fallback to N/A )[0] as HTMLElement;
let text = 'N/A';
const student = students[index] ?? {}; 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) { if (student.house) {
text = `${student.year ?? ""}${student.house}`; text = `${student.year ?? ""}${student.house}`;
// If house_colour exists, compute colour
if (student.house_colour) { if (student.house_colour) {
houseelement.style.background = student.house_colour; houseelement.style.background = student.house_colour;
try { try {
const colorresult = GetThresholdOfColor(student.house_colour); const colorresult = GetThresholdOfColor(student.house_colour);
houseelement.style.color = houseelement.style.color =
colorresult && colorresult > 300 ? "black" : "white"; colorresult && colorresult > 300 ? "black" : "white";
} catch {
} catch (err) { // Invalid color format, leave text color as default
// Colour calculation failed, no text colour set
} }
} }
} else if (student.year) { } else if (student.year) {
// No house, only year will be shown
text = student.year; text = student.year;
} }
@@ -205,15 +196,13 @@ async function updateStudentInfo(students: any) {
} }
function createNewsButton(fragment: DocumentFragment, menu: HTMLElement) { function createNewsButton(fragment: DocumentFragment, menu: HTMLElement) {
const NewsButtonStr = fragment.appendChild(
'<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>'; stringToHTML(
const NewsButton = stringToHTML(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>',
).firstChild!,
);
if (NewsButton.firstChild) { const iconCover = document.createElement("div");
fragment.appendChild(NewsButton.firstChild);
}
let iconCover = document.createElement("div");
iconCover.classList.add("icon-cover"); iconCover.classList.add("icon-cover");
iconCover.id = "icon-cover"; iconCover.id = "icon-cover";
menu.appendChild(iconCover); menu.appendChild(iconCover);
@@ -252,46 +241,42 @@ function setupEventListeners() {
} }
async function createSettingsButton() { async function createSettingsButton() {
let SettingsButton = stringToHTML(/* html */ ` document.getElementById("content")!.append(
stringToHTML(/* html */ `
<button class="addedButton tooltip" id="AddedSettings"> <button class="addedButton tooltip" id="AddedSettings">
<svg width="24" height="24" viewBox="0 0 24 24"> <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> <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> </svg>
${settingsState.onoff ? '<div class="tooltiptext topmenutooltip">BetterSEQTA+ Settings</div>' : ""} ${settingsState.onoff ? '<div class="tooltiptext topmenutooltip">BetterSEQTA+ Settings</div>' : ""}
</button> </button>
`); `).firstChild!,
let ContentDiv = document.getElementById("content"); );
ContentDiv!.append(SettingsButton.firstChild!);
} }
function GetLightDarkModeString() { function GetLightDarkModeString() {
if (settingsState.DarkMode) { return settingsState.DarkMode
return "Switch to light theme"; ? "Switch to light theme"
} else { : "Switch to dark theme";
return "Switch to dark theme";
}
} }
async function addDarkLightToggle() { 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 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 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; document.getElementById("content")!.append(
stringToHTML(/* html */ `
const LightDarkModeButton = stringToHTML(/* html */ `
<button class="addedButton DarkLightButton tooltip" id="LightDarkModeButton"> <button class="addedButton DarkLightButton tooltip" id="LightDarkModeButton">
<svg xmlns="http://www.w3.org/2000/svg">${initialSvgContent}</svg> <svg xmlns="http://www.w3.org/2000/svg">${settingsState.DarkMode ? SUN_ICON_SVG : MOON_ICON_SVG}</svg>
<div class="tooltiptext topmenutooltip" id="darklighttooliptext">${tooltipString}</div> <div class="tooltiptext topmenutooltip" id="darklighttooliptext">${GetLightDarkModeString()}</div>
</button> </button>
`); `).firstChild!,
);
let ContentDiv = document.getElementById("content");
ContentDiv!.append(LightDarkModeButton.firstChild!);
updateAllColors(); updateAllColors();
const lightDarkModeButtonElement = document.getElementById("LightDarkModeButton")!; const lightDarkModeButtonElement = document.getElementById(
"LightDarkModeButton",
)!;
lightDarkModeButtonElement.addEventListener("click", async () => { lightDarkModeButtonElement.addEventListener("click", async () => {
const darklightText = document.getElementById("darklighttooliptext"); const darklightText = document.getElementById("darklighttooliptext");
@@ -303,7 +288,6 @@ async function addDarkLightToggle() {
LightDarkModeSnakeEggButton = 0; LightDarkModeSnakeEggButton = 0;
} }
if ( if (
settingsState.originalDarkMode !== undefined && settingsState.originalDarkMode !== undefined &&
settingsState.selectedTheme settingsState.selectedTheme
@@ -314,38 +298,24 @@ async function addDarkLightToggle() {
return; return;
} }
if (!document.startViewTransition || !settingsState.animations || window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
settingsState.DarkMode = !settingsState.DarkMode; settingsState.DarkMode = !settingsState.DarkMode;
updateAllColors(); updateAllColors();
const newSvgContent = settingsState.DarkMode ? SUN_ICON_SVG : MOON_ICON_SVG; const svgElement = lightDarkModeButtonElement.querySelector("svg")!;
const svgElement = lightDarkModeButtonElement.querySelector("svg"); svgElement.innerHTML = settingsState.DarkMode
if (svgElement) svgElement.innerHTML = newSvgContent; ? SUN_ICON_SVG
darklightText!.innerText = GetLightDarkModeString(); : MOON_ICON_SVG;
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;
darklightText!.innerText = GetLightDarkModeString(); darklightText!.innerText = GetLightDarkModeString();
}); });
} }
function customizeMenuToggle() { function customizeMenuToggle() {
const menuToggle = document.getElementById("menuToggle"); const menuToggle = document.getElementById("menuToggle")!;
if (menuToggle) {
menuToggle.innerHTML = ""; menuToggle.innerHTML = "";
}
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
const line = document.createElement("div"); const line = document.createElement("div");
line.className = "hamburger-line"; line.className = "hamburger-line";
menuToggle!.appendChild(line); menuToggle.appendChild(line);
} }
} }
+115 -224
View File
@@ -30,20 +30,17 @@ export async function loadHomePage() {
element?.classList.add("active"); element?.classList.add("active");
const main = document.getElementById("main"); const main = document.getElementById("main");
if (!main) { if (!main) return;
console.error("[BetterSEQTA+] Main element not found.");
return;
}
const homeRoot = stringToHTML(`<div id="home-root" class="home-root"></div>`);
main.innerHTML = ""; main.innerHTML = "";
main.appendChild(homeRoot?.firstChild!); main.appendChild(
stringToHTML(`<div id="home-root" class="home-root"></div>`).firstChild!,
);
const homeContainer = document.getElementById("home-root"); const homeContainer = document.getElementById("home-root");
if (!homeContainer) return; if (!homeContainer) return;
const skeletonStructure = stringToHTML(/* html */` const skeletonStructure = stringToHTML(/* html */ `
<div class="home-container" id="home-container"> <div class="home-container" id="home-container">
<div class="border shortcut-container"> <div class="border shortcut-container">
<div class="border shortcuts" id="shortcuts"></div> <div class="border shortcuts" id="shortcuts"></div>
@@ -101,25 +98,16 @@ export async function loadHomePage() {
renderShortcuts(); renderShortcuts();
const date = new Date(); const TodayFormatted = formatDate(new Date());
const TodayFormatted = formatDate(date);
const [assessmentsPromise, classesPromise, prefsPromise] = [ const [assessments, classes, prefs] = await Promise.all([
GetUpcomingAssessments(), GetUpcomingAssessments(),
GetActiveClasses(), GetActiveClasses(),
fetch(`${location.origin}/seqta/student/load/prefs?`, { fetch(`${location.origin}/seqta/student/load/prefs?`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ asArray: true, request: "userPrefs" }), body: JSON.stringify({ asArray: true, request: "userPrefs" }),
}).then((res) => res.json()), }).then((res) => res.json()),
];
const [assessments, classes, prefs] = await Promise.all([
assessmentsPromise,
classesPromise,
prefsPromise,
]); ]);
callHomeTimetable(TodayFormatted, true); callHomeTimetable(TodayFormatted, true);
@@ -159,20 +147,20 @@ export async function loadHomePage() {
} }
async function GetUpcomingAssessments() { async function GetUpcomingAssessments() {
let func = fetch( try {
`${location.origin}/seqta/student/assessment/list/upcoming?`, return fetch(`${location.origin}/seqta/student/assessment/list/upcoming?`, {
{
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json; charset=utf-8", "Content-Type": "application/json; charset=utf-8",
}, },
body: JSON.stringify({ student: 69 }), body: JSON.stringify({ student: 69 }),
}, })
);
return func
.then((result) => result.json()) .then((result) => result.json())
.then((response) => response.payload); .then((response) => response.payload);
} catch (error) {
console.error("[BetterSEQTA+] Failed to get upcoming assessments:", error);
return [];
}
} }
function setupTimetableListeners() { function setupTimetableListeners() {
@@ -230,15 +218,10 @@ async function GetActiveClasses() {
body: JSON.stringify({}), body: JSON.stringify({}),
}, },
); );
return (await response.json()).payload;
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
return data.payload;
} catch (error) { } catch (error) {
console.error("Oops! There was a problem fetching active classes:", error); console.error("[BetterSEQTA+] Failed to get active classes:", error);
return [];
} }
} }
@@ -248,28 +231,25 @@ function setupNotices(labelArray: string[], date: string) {
) as HTMLInputElement; ) as HTMLInputElement;
const fetchNotices = async (date: string) => { const fetchNotices = async (date: string) => {
let data; try {
const data = settingsState.mockNotices
if (settingsState.mockNotices) { ? getMockNotices()
data = getMockNotices(); : await (
} else { await fetch(`${location.origin}/seqta/student/load/notices?`, {
const response = await fetch(
`${location.origin}/seqta/student/load/notices?`,
{
method: "POST", method: "POST",
headers: { "Content-Type": "application/json; charset=utf-8" }, headers: { "Content-Type": "application/json; charset=utf-8" },
body: JSON.stringify({ date }), body: JSON.stringify({ date }),
}, })
); ).json();
data = await response.json();
}
processNotices(data, labelArray); processNotices(data, labelArray);
} catch (error) {
console.error("[BetterSEQTA+] Failed to fetch notices:", error);
}
}; };
const debouncedInputChange = debounce((e: Event) => { const debouncedInputChange = debounce((e: Event) => {
const target = e.target as HTMLInputElement; fetchNotices((e.target as HTMLInputElement).value);
fetchNotices(target.value);
}, 250); }, 250);
dateControl?.addEventListener("input", debouncedInputChange); dateControl?.addEventListener("input", debouncedInputChange);
@@ -290,16 +270,8 @@ function debounce<T extends (...args: any[]) => any>(
} }
function comparedate(obj1: any, obj2: any) { function comparedate(obj1: any, obj2: any) {
if (obj1.date < obj2.date) { return obj1.date < obj2.date ? -1 : obj1.date > obj2.date ? 1 : 0;
return -1;
}
if (obj1.date > obj2.date) {
return 1;
}
return 0;
} }
function processNotices(response: any, labelArray: string[]) { function processNotices(response: any, labelArray: string[]) {
const NoticeContainer = document.getElementById("notice-container"); const NoticeContainer = document.getElementById("notice-container");
if (!NoticeContainer) return; if (!NoticeContainer) return;
@@ -343,13 +315,13 @@ function processNoticeColor(colour: string): string | undefined {
} }
function createNoticeElement(notice: any, colour: string | undefined): Node { function createNoticeElement(notice: any, colour: string | undefined): Node {
const textPreview = notice.contents const textPreview =
notice.contents
.replace(/<[^>]*>/g, "") .replace(/<[^>]*>/g, "")
.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "") .replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "")
.replace(/\s+/g, " ") .replace(/\s+/g, " ")
.trim() .trim()
.substring(0, 150) .substring(0, 150) + (notice.contents.length > 150 ? "..." : "");
+ (notice.contents.length > 150 ? "..." : "");
const noticeId = `notice-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const noticeId = `notice-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
@@ -369,12 +341,10 @@ function createNoticeElement(notice: any, colour: string | undefined): Node {
</div>`; </div>`;
const element = stringToHTML(htmlContent).firstChild as HTMLElement; const element = stringToHTML(htmlContent).firstChild as HTMLElement;
if (element) {
element.addEventListener("click", () => element.addEventListener("click", () =>
openNoticeModal(notice, colour, element), openNoticeModal(notice, colour, element),
); );
} return element;
return element!;
} }
function openNoticeModal( function openNoticeModal(
@@ -386,15 +356,11 @@ function openNoticeModal(
.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "") .replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "")
.replace(/ +/, " "); .replace(/ +/, " ");
const existingModal = document.getElementById("notice-modal"); document.getElementById("notice-modal")?.remove();
if (existingModal) {
existingModal.remove();
}
const sourceRect = sourceElement.getBoundingClientRect(); const sourceRect = sourceElement.getBoundingClientRect();
let scrollY = Math.round(window.scrollY); let scrollY = Math.round(window.scrollY);
let scrollX = Math.round(window.scrollX); let scrollX = Math.round(window.scrollX);
let sourceLeft = sourceRect.left; let sourceLeft = sourceRect.left;
let sourceTop = sourceRect.top; let sourceTop = sourceRect.top;
let sourceWidth = sourceRect.width; let sourceWidth = sourceRect.width;
@@ -476,7 +442,6 @@ function openNoticeModal(
let targetHeight = Math.round( let targetHeight = Math.round(
Math.min(Math.max(measuredHeight, 200), viewportHeight * 0.85), Math.min(Math.max(measuredHeight, 200), viewportHeight * 0.85),
); );
let targetLeft = Math.round((viewportWidth - targetWidth) / 2); let targetLeft = Math.round((viewportWidth - targetWidth) / 2);
let targetTop = Math.round((viewportHeight - targetHeight) / 2) + scrollY; let targetTop = Math.round((viewportHeight - targetHeight) / 2) + scrollY;
@@ -585,13 +550,10 @@ function openNoticeModal(
const newTargetWidth = Math.round( const newTargetWidth = Math.round(
Math.min(Math.max(newSourceWidth, 800), newViewportWidth - 40), Math.min(Math.max(newSourceWidth, 800), newViewportWidth - 40),
); );
// Just measure the existing modal content
const currentHeight = unifiedContent.getBoundingClientRect().height; const currentHeight = unifiedContent.getBoundingClientRect().height;
const newTargetHeight = Math.round( const newTargetHeight = Math.round(
Math.min(Math.max(currentHeight, 200), newViewportHeight * 0.85), Math.min(Math.max(currentHeight, 200), newViewportHeight * 0.85),
); );
const newTargetLeft = Math.round((newViewportWidth - newTargetWidth) / 2); const newTargetLeft = Math.round((newViewportWidth - newTargetWidth) / 2);
const newTargetTop = const newTargetTop =
Math.round((newViewportHeight - newTargetHeight) / 2) + newScrollY; Math.round((newViewportHeight - newTargetHeight) / 2) + newScrollY;
@@ -656,7 +618,8 @@ function callHomeTimetable(date: string, change?: any) {
xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8"); xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8");
xhr.onreadystatechange = function () { xhr.onreadystatechange = function () {
if (xhr.readyState === 4) { if (xhr.readyState !== 4) return;
if (loadingTimeout) { if (loadingTimeout) {
clearTimeout(loadingTimeout); clearTimeout(loadingTimeout);
loadingTimeout = null; loadingTimeout = null;
@@ -664,7 +627,6 @@ function callHomeTimetable(date: string, change?: any) {
const DayContainer = document.getElementById("day-container")!; const DayContainer = document.getElementById("day-container")!;
try {
var serverResponse = JSON.parse(xhr.response); var serverResponse = JSON.parse(xhr.response);
let lessonArray: Array<any> = []; let lessonArray: Array<any> = [];
@@ -678,21 +640,20 @@ function callHomeTimetable(date: string, change?: any) {
}); });
GetLessonColours().then((colours) => { GetLessonColours().then((colours) => {
let subjects = colours;
for (let i = 0; i < lessonArray.length; i++) { for (let i = 0; i < lessonArray.length; i++) {
let subjectname =
let subjectname = ((lessonArray[i].type == "tutorial") ? `timetable.tutor.${lessonArray[i].tutorID}` : `timetable.subject.colour.${lessonArray[i].code}`); lessonArray[i].type == "tutorial"
? `timetable.tutor.${lessonArray[i].tutorID}`
let subject = subjects.find( : `timetable.subject.colour.${lessonArray[i].code}`;
let subject = colours.find(
(element: any) => element.name === subjectname, (element: any) => element.name === subjectname,
); );
if (!subject) { if (!subject) {
lessonArray[i].colour = "--item-colour: #8e8e8e;"; lessonArray[i].colour = "--item-colour: #8e8e8e;";
} else { } else {
lessonArray[i].colour = `--item-colour: ${subject.value};`; lessonArray[i].colour = `--item-colour: ${subject.value};`;
let result = GetThresholdOfColor(subject.value); if (GetThresholdOfColor(subject.value) > 300) {
if (result > 300) {
lessonArray[i].invert = true; lessonArray[i].invert = true;
} }
} }
@@ -701,9 +662,7 @@ function callHomeTimetable(date: string, change?: any) {
lessonArray[i].until = lessonArray[i].until.substring(0, 5); lessonArray[i].until = lessonArray[i].until.substring(0, 5);
if (settingsState.timeFormat === "12") { if (settingsState.timeFormat === "12") {
lessonArray[i].from = convertTo12HourFormat( lessonArray[i].from = convertTo12HourFormat(lessonArray[i].from);
lessonArray[i].from,
);
lessonArray[i].until = convertTo12HourFormat( lessonArray[i].until = convertTo12HourFormat(
lessonArray[i].until, lessonArray[i].until,
); );
@@ -716,13 +675,10 @@ function callHomeTimetable(date: string, change?: any) {
DayContainer.innerText = ""; DayContainer.innerText = "";
for (let i = 0; i < lessonArray.length; i++) { for (let i = 0; i < lessonArray.length; i++) {
var div = makeLessonDiv(lessonArray[i], i + 1); const div = makeLessonDiv(lessonArray[i], i + 1);
if (lessonArray[i].invert) { if (lessonArray[i].invert) {
const div1 = div.firstChild! as HTMLElement; (div.firstChild! as HTMLElement).classList.add("day-inverted");
div1.classList.add("day-inverted");
} }
DayContainer.append(div.firstChild as HTMLElement); DayContainer.append(div.firstChild as HTMLElement);
} }
@@ -733,40 +689,22 @@ function callHomeTimetable(date: string, change?: any) {
for (let i = 0; i < lessonArray.length; i++) { for (let i = 0; i < lessonArray.length; i++) {
CheckCurrentLesson(lessonArray[i], i + 1); CheckCurrentLesson(lessonArray[i], i + 1);
} }
CheckCurrentLessonAll(lessonArray); CheckCurrentLessonAll(lessonArray);
} }
}); });
} }
} else { } else {
DayContainer.innerHTML = ""; DayContainer.innerHTML = "";
var dummyDay = document.createElement("div"); const dummyDay = document.createElement("div");
dummyDay.classList.add("day-empty"); dummyDay.classList.add("day-empty");
let img = document.createElement("img"); const img = document.createElement("img");
img.src = browser.runtime.getURL(LogoLight); img.src = browser.runtime.getURL(LogoLight);
let text = document.createElement("p"); const text = document.createElement("p");
text.innerText = "No lessons available."; text.innerText = "No lessons available.";
dummyDay.append(img); dummyDay.append(img, text);
dummyDay.append(text);
DayContainer.append(dummyDay); DayContainer.append(dummyDay);
DayContainer.classList.remove("loading"); DayContainer.classList.remove("loading");
} }
} catch (error) {
console.error("Error loading timetable data:", error);
DayContainer.classList.remove("loading");
DayContainer.innerHTML = "";
const errorDiv = document.createElement("div");
errorDiv.classList.add("day-empty");
errorDiv.innerHTML = `
<img src="${browser.runtime.getURL(LogoLight)}" />
<p>Error loading lessons. Please try again.</p>
`;
DayContainer.append(errorDiv);
}
}
}; };
xhr.send( xhr.send(
JSON.stringify({ JSON.stringify({
@@ -855,8 +793,6 @@ async function CheckCurrentLesson(lesson: any, num: number) {
} }
function makeLessonDiv(lesson: any, num: number) { function makeLessonDiv(lesson: any, num: number) {
if (!lesson) throw new Error("No lesson provided.");
const { const {
code, code,
colour, colour,
@@ -869,14 +805,14 @@ function makeLessonDiv(lesson: any, num: number) {
programmeID, programmeID,
metaID, metaID,
assessments, assessments,
type type,
} = lesson; } = lesson;
let lessonString = ` let lessonString = `
<div class="day" id="${code + num}" style="${colour}"> <div class="day" id="${code + num}" style="${colour}">
<h2>${(type == "class") ? description : (type == "tutorial") ? "Tutorial" : "Unknown"}</h2> <h2>${type == "class" ? description : type == "tutorial" ? "Tutorial" : "Unknown"}</h2>
<h3>${staff || "Unknown"}</h3> <h3>${staff || "Unknown"}</h3>
<h3>${(type == "class") ? room : (type == "tutorial") ? "N/A" : "Unknown"}</h3> <h3>${type == "class" ? room : type == "tutorial" ? "N/A" : "Unknown"}</h3>
<h4>${from || "Unknown"} - ${until || "Unknown"}</h4> <h4>${from || "Unknown"} - ${until || "Unknown"}</h4>
<h5>${attendanceTitle || "Unknown"}</h5> <h5>${attendanceTitle || "Unknown"}</h5>
`; `;
@@ -922,64 +858,48 @@ function buildAssessmentURL(programmeID: any, metaID: any, itemID = "") {
} }
function CheckUnmarkedAttendance(lessonattendance: any) { function CheckUnmarkedAttendance(lessonattendance: any) {
if (lessonattendance) { return lessonattendance ? lessonattendance.label : " ";
var lesson = lessonattendance.label;
} else {
lesson = " ";
}
return lesson;
} }
async function CreateUpcomingSection(assessments: any, activeSubjects: any) { async function CreateUpcomingSection(assessments: any, activeSubjects: any) {
let upcomingitemcontainer = document.querySelector("#upcoming-items"); const upcomingitemcontainer = document.querySelector("#upcoming-items");
let overdueDates = []; const overdueDates = [];
let upcomingDates = {}; const upcomingDates = {};
const Today = new Date();
var Today = new Date();
for (let i = 0; i < assessments.length; i++) { for (let i = 0; i < assessments.length; i++) {
const assessment = assessments[i]; const assessmentdue = new Date(assessments[i].due);
let assessmentdue = new Date(assessment.due); if (assessmentdue < Today && !CheckSpecialDay(Today, assessmentdue)) {
overdueDates.push(assessments[i]);
CheckSpecialDay(Today, assessmentdue);
if (assessmentdue < Today) {
if (!CheckSpecialDay(Today, assessmentdue)) {
overdueDates.push(assessment);
assessments.splice(i, 1); assessments.splice(i, 1);
i--; i--;
} }
} }
}
var TomorrowDate = new Date();
TomorrowDate.setDate(TomorrowDate.getDate() + 1);
const colours = await GetLessonColours(); const colours = await GetLessonColours();
let subjects = colours;
for (let i = 0; i < assessments.length; i++) { for (let i = 0; i < assessments.length; i++) {
let subjectname = `timetable.subject.colour.${assessments[i].code}`; const subject = colours.find(
(element: any) =>
let subject = subjects.find((element: any) => element.name === subjectname); element.name === `timetable.subject.colour.${assessments[i].code}`,
);
if (!subject) { if (!subject) {
assessments[i].colour = "--item-colour: #8e8e8e;"; assessments[i].colour = "--item-colour: #8e8e8e;";
} else { } else {
assessments[i].colour = `--item-colour: ${subject.value};`; assessments[i].colour = `--item-colour: ${subject.value};`;
GetThresholdOfColor(subject.value);
} }
} }
for (let i = 0; i < activeSubjects.length; i++) { for (let i = 0; i < activeSubjects.length; i++) {
const element = activeSubjects[i]; const element = activeSubjects[i];
let subjectname = `timetable.subject.colour.${element.code}`; const colour = colours.find(
let colour = colours.find((element: any) => element.name === subjectname); (c: any) => c.name === `timetable.subject.colour.${element.code}`,
);
if (!colour) { if (!colour) {
element.colour = "--item-colour: #8e8e8e;"; element.colour = "--item-colour: #8e8e8e;";
} else { } else {
element.colour = `--item-colour: ${colour.value};`; element.colour = `--item-colour: ${colour.value};`;
let result = GetThresholdOfColor(colour.value); if (GetThresholdOfColor(colour.value) > 300) {
if (result > 300) {
element.invert = true; element.invert = true;
} }
} }
@@ -987,52 +907,35 @@ async function CreateUpcomingSection(assessments: any, activeSubjects: any) {
CreateFilters(activeSubjects); CreateFilters(activeSubjects);
let type;
let class_;
for (let i = 0; i < assessments.length; i++) { for (let i = 0; i < assessments.length; i++) {
const element: any = assessments[i]; const element: any = assessments[i];
if (!upcomingDates[element.due as keyof typeof upcomingDates]) { if (!upcomingDates[element.due as keyof typeof upcomingDates]) {
let dateObj: any = new Object(); const dateObj: any = {
dateObj.div = CreateElement( div: CreateElement("div", "upcoming-date-container"),
(type = "div"), assessments: [],
(class_ = "upcoming-date-container"), };
);
dateObj.assessments = [];
(upcomingDates[element.due as keyof typeof upcomingDates] as any) = (upcomingDates[element.due as keyof typeof upcomingDates] as any) =
dateObj; dateObj;
} }
let assessmentDateDiv = const assessmentDateDiv =
upcomingDates[element.due as keyof typeof upcomingDates]; upcomingDates[element.due as keyof typeof upcomingDates];
if (assessmentDateDiv) { if (assessmentDateDiv) {
(assessmentDateDiv as any).assessments.push(element); (assessmentDateDiv as any).assessments.push(element);
} }
} }
for (var date in upcomingDates) { for (var date in upcomingDates) {
let assessmentdue = new Date( const assessmentdue = new Date(
( (
upcomingDates[date as keyof typeof upcomingDates] as any upcomingDates[date as keyof typeof upcomingDates] as any
).assessments[0].due, ).assessments[0].due,
); );
let specialcase = CheckSpecialDay(Today, assessmentdue); const specialcase = CheckSpecialDay(Today, assessmentdue);
let assessmentDate; const assessmentDate = createAssessmentDateDiv(
if (specialcase) {
let datecase: string = specialcase!;
assessmentDate = createAssessmentDateDiv(
date, date,
upcomingDates[date as keyof typeof upcomingDates], upcomingDates[date as keyof typeof upcomingDates],
specialcase,
datecase,
); );
} else {
assessmentDate = createAssessmentDateDiv(
date,
upcomingDates[date as keyof typeof upcomingDates],
);
}
if (specialcase === "Yesterday") { if (specialcase === "Yesterday") {
upcomingitemcontainer!.insertBefore( upcomingitemcontainer!.insertBefore(
@@ -1055,77 +958,68 @@ async function CreateUpcomingSection(assessments: any, activeSubjects: any) {
} }
function createAssessmentDateDiv(date: string, value: any, datecase?: any) { function createAssessmentDateDiv(date: string, value: any, datecase?: any) {
var options = { const options = {
weekday: "long" as "long", weekday: "long" as "long",
month: "long" as "long", month: "long" as "long",
day: "numeric" as "numeric", day: "numeric" as "numeric",
}; };
const FormattedDate = new Date(date); const FormattedDate = new Date(date);
const assessments = value.assessments; const assessments = value.assessments;
const container = value.div; const container = value.div;
let DateTitleDiv = document.createElement("div"); const DateTitleDiv = document.createElement("div");
DateTitleDiv.classList.add("upcoming-date-title"); DateTitleDiv.classList.add("upcoming-date-title");
if (datecase) { if (datecase) {
let datetitle = document.createElement("h5"); const datetitle = document.createElement("h5");
datetitle.classList.add("upcoming-special-day"); datetitle.classList.add("upcoming-special-day");
datetitle.innerText = datecase; datetitle.innerText = datecase;
DateTitleDiv.append(datetitle); DateTitleDiv.append(datetitle);
container.setAttribute("data-day", datecase); container.setAttribute("data-day", datecase);
} }
let DateTitle = document.createElement("h5"); const DateTitle = document.createElement("h5");
DateTitle.innerText = FormattedDate.toLocaleDateString("en-AU", options); DateTitle.innerText = FormattedDate.toLocaleDateString("en-AU", options);
DateTitleDiv.append(DateTitle); DateTitleDiv.append(DateTitle);
container.append(DateTitleDiv); container.append(DateTitleDiv);
let assessmentContainer = document.createElement("div"); const assessmentContainer = document.createElement("div");
assessmentContainer.classList.add("upcoming-date-assessments"); assessmentContainer.classList.add("upcoming-date-assessments");
for (let i = 0; i < assessments.length; i++) { for (let i = 0; i < assessments.length; i++) {
const element = assessments[i]; const element = assessments[i];
let item = document.createElement("div"); const item = document.createElement("div");
item.classList.add("upcoming-assessment"); item.classList.add("upcoming-assessment");
item.setAttribute("data-subject", element.code); item.setAttribute("data-subject", element.code);
item.id = `assessment${element.id}`; item.id = `assessment${element.id}`;
item.style.cssText = element.colour; item.style.cssText = element.colour;
let titlediv = document.createElement("div"); const titlediv = document.createElement("div");
titlediv.classList.add("upcoming-subject-title"); titlediv.classList.add("upcoming-subject-title");
titlediv.append(
let titlesvg =
stringToHTML(`<svg viewBox="0 0 24 24" style="width:35px;height:35px;fill:white;"> stringToHTML(`<svg viewBox="0 0 24 24" style="width:35px;height:35px;fill:white;">
<path d="M6 20H13V22H6C4.89 22 4 21.11 4 20V4C4 2.9 4.89 2 6 2H18C19.11 2 20 2.9 20 4V12.54L18.5 11.72L18 12V4H13V12L10.5 9.75L8 12V4H6V20M24 17L18.5 14L13 17L18.5 20L24 17M15 19.09V21.09L18.5 23L22 21.09V19.09L18.5 21L15 19.09Z"></path> <path d="M6 20H13V22H6C4.89 22 4 21.11 4 20V4C4 2.9 4.89 2 6 2H18C19.11 2 20 2.9 20 4V12.54L18.5 11.72L18 12V4H13V12L10.5 9.75L8 12V4H6V20M24 17L18.5 14L13 17L18.5 20L24 17M15 19.09V21.09L18.5 23L22 21.09V19.09L18.5 21L15 19.09Z"></path>
</svg>`).firstChild; </svg>`).firstChild!,
titlediv.append(titlesvg!); );
let detailsdiv = document.createElement("div"); const detailsdiv = document.createElement("div");
detailsdiv.classList.add("upcoming-details"); detailsdiv.classList.add("upcoming-details");
let detailstitle = document.createElement("h5"); const detailstitle = document.createElement("h5");
detailstitle.innerText = `${element.subject} assessment`; detailstitle.innerText = `${element.subject} assessment`;
let subject = document.createElement("p"); const subject = document.createElement("p");
subject.innerText = element.title; subject.innerText = element.title;
subject.classList.add("upcoming-assessment-title"); subject.classList.add("upcoming-assessment-title");
subject.onclick = function () { subject.onclick = function () {
document.querySelector("#menu ul")!.classList.add("noscroll"); document.querySelector("#menu ul")!.classList.add("noscroll");
location.href = `../#?page=/assessments/${element.programmeID}:${element.metaclassID}&item=${element.id}`; location.href = `../#?page=/assessments/${element.programmeID}:${element.metaclassID}&item=${element.id}`;
}; };
detailsdiv.append(detailstitle); detailsdiv.append(detailstitle, subject);
detailsdiv.append(subject); item.append(titlediv, detailsdiv);
item.append(titlediv);
item.append(detailsdiv);
assessmentContainer.append(item); assessmentContainer.append(item);
fetch(`${location.origin}/seqta/student/assessment/submissions/get`, { fetch(`${location.origin}/seqta/student/assessment/submissions/get`, {
method: "POST", method: "POST",
headers: { headers: { "Content-Type": "application/json; charset=utf-8" },
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify({ body: JSON.stringify({
assessment: element.id, assessment: element.id,
metaclass: element.metaclassID, metaclass: element.metaclassID,
@@ -1136,8 +1030,7 @@ function createAssessmentDateDiv(date: string, value: any, datecase?: any) {
.then((response) => { .then((response) => {
if (response.payload.length > 0) { if (response.payload.length > 0) {
const assessment = document.querySelector(`#assessment${element.id}`); const assessment = document.querySelector(`#assessment${element.id}`);
const submittedtext = document.createElement("div");
let submittedtext = document.createElement("div");
submittedtext.classList.add("upcoming-submittedtext"); submittedtext.classList.add("upcoming-submittedtext");
submittedtext.innerText = "Submitted"; submittedtext.innerText = "Submitted";
assessment!.append(submittedtext); assessment!.append(submittedtext);
@@ -1175,36 +1068,37 @@ function CheckSpecialDay(date1: Date, date2: Date) {
} }
async function GetLessonColours() { async function GetLessonColours() {
let func = fetch(`${location.origin}/seqta/student/load/prefs?`, { try {
return fetch(`${location.origin}/seqta/student/load/prefs?`, {
method: "POST", method: "POST",
headers: { headers: { "Content-Type": "application/json; charset=utf-8" },
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify({ request: "userPrefs", asArray: true, user: 69 }), body: JSON.stringify({ request: "userPrefs", asArray: true, user: 69 }),
}); })
return func
.then((result) => result.json()) .then((result) => result.json())
.then((response) => response.payload); .then((response) => response.payload);
} catch (error) {
console.error("[BetterSEQTA+] Failed to get lesson colours:", error);
return [];
}
} }
function CreateFilters(subjects: any) { function CreateFilters(subjects: any) {
let filteroptions = settingsState.subjectfilters; const filteroptions = settingsState.subjectfilters;
const filterdiv = document.querySelector("#upcoming-filters");
let filterdiv = document.querySelector("#upcoming-filters");
for (let i = 0; i < subjects.length; i++) { for (let i = 0; i < subjects.length; i++) {
const element = subjects[i]; const element = subjects[i];
if (!Object.prototype.hasOwnProperty.call(filteroptions, element.code)) { if (!Object.prototype.hasOwnProperty.call(filteroptions, element.code)) {
filteroptions[element.code] = true; filteroptions[element.code] = true;
settingsState.subjectfilters = filteroptions; settingsState.subjectfilters = filteroptions;
} }
let elementdiv = CreateSubjectFilter( filterdiv!.append(
CreateSubjectFilter(
element.code, element.code,
element.colour, element.colour,
filteroptions[element.code], filteroptions[element.code],
),
); );
filterdiv!.append(elementdiv);
} }
} }
@@ -1213,23 +1107,20 @@ function CreateSubjectFilter(
itemcolour: string, itemcolour: string,
checked: any, checked: any,
) { ) {
let label = CreateElement("label", "upcoming-checkbox-container"); const label = CreateElement("label", "upcoming-checkbox-container");
label.innerText = subjectcode; label.innerText = subjectcode;
let input1 = CreateElement("input"); const input = CreateElement("input") as HTMLInputElement;
const input = input1 as HTMLInputElement;
input.type = "checkbox"; input.type = "checkbox";
input.checked = checked; input.checked = checked;
input.id = `filter-${subjectcode}`; input.id = `filter-${subjectcode}`;
label.style.cssText = itemcolour; label.style.cssText = itemcolour;
let span = CreateElement("span", "upcoming-checkmark"); const span = CreateElement("span", "upcoming-checkmark");
label.append(input); label.append(input, span);
label.append(span);
input.addEventListener("change", function (change) { input.addEventListener("change", function (change) {
let filters = settingsState.subjectfilters; const filters = settingsState.subjectfilters;
let id = (change.target as HTMLInputElement)!.id.split("-")[1]; const id = (change.target as HTMLInputElement).id.split("-")[1];
filters[id] = (change.target as HTMLInputElement)!.checked; filters[id] = (change.target as HTMLInputElement).checked;
settingsState.subjectfilters = filters; settingsState.subjectfilters = filters;
}); });