From 3aef2312d09d697d452c28b5ef254e5ff4d8b832 Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Fri, 30 Jan 2026 16:07:12 +1100 Subject: [PATCH] feat: cleanup and comment removal --- bun.lock | 27 + src/SEQTA.ts | 37 +- .../built-in/assessmentsAverage/index.ts | 53 +- .../built-in/assessmentsAverage/utils.ts | 515 ++++++++---------- src/seqta/ui/AddBetterSEQTAElements.ts | 198 +++---- src/seqta/utils/Loaders/LoadHomePage.ts | 507 +++++++---------- 6 files changed, 573 insertions(+), 764 deletions(-) diff --git a/bun.lock b/bun.lock index 1fd74ab4..fefefbae 100644 --- a/bun.lock +++ b/bun.lock @@ -42,6 +42,7 @@ "mathjs": "^14.4.0", "million": "^3.1.11", "motion": "^12.4.12", + "pdfjs-dist": "^5.4.530", "postcss": "^8.5.3", "react": "17", "react-best-gradient-color-picker": "3.0.11", @@ -265,6 +266,30 @@ "@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.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], @@ -1133,6 +1158,8 @@ "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=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], diff --git a/src/SEQTA.ts b/src/SEQTA.ts index 9d4dc8c7..e64cd716 100644 --- a/src/SEQTA.ts +++ b/src/SEQTA.ts @@ -25,37 +25,20 @@ 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"); - const documentLoadStyle = document.createElement("style"); - documentLoadStyle.textContent = documentLoadCSS; - document.head.appendChild(documentLoadStyle); + const style = document.createElement("style"); + style.textContent = documentLoadCSS; + document.head.appendChild(style); - const icons = - document.querySelectorAll('link[rel*="icon"]'); - - icons.forEach((link) => { - link.href = icon48; - }); + document + .querySelectorAll('link[rel*="icon"]') + .forEach((link) => { + link.href = icon48; + }); try { await initializeSettingsState(); @@ -80,7 +63,7 @@ async function init() { console.info( "[BetterSEQTA+] Successfully initialised BetterSEQTA+, starting to load assets.", ); - } catch (error: any) { + } catch (error) { console.error(error); } } diff --git a/src/plugins/built-in/assessmentsAverage/index.ts b/src/plugins/built-in/assessmentsAverage/index.ts index be3b6bd4..6db54831 100644 --- a/src/plugins/built-in/assessmentsAverage/index.ts +++ b/src/plugins/built-in/assessmentsAverage/index.ts @@ -8,9 +8,15 @@ 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"; +import { + clearStuck, + getClassByPattern, + initStorage, + letterToNumber, + parseAssessments, + processAssessments, +} from "./utils.ts"; -// Storage interface weightingsStorage { weightings: Record; assessments: Record; @@ -40,11 +46,7 @@ const assessmentsAveragePlugin: Plugin = { settings: instance.settings, run: async (api) => { - - // Ensure storage is ready for use await initStorage(api); - - // Clear any stuck "processing" states so they can retry clearStuck(api); api.seqta.onMount(".assessmentsWrapper", async () => { @@ -57,13 +59,11 @@ const assessmentsAveragePlugin: Plugin = { await parseAssessments(api); - // 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___"), @@ -86,7 +86,6 @@ const assessmentsAveragePlugin: Plugin = { "AssessmentItem__title___", ); - // Get Thermoscore classes const thermoscoreElement = document.querySelector( "[class*='Thermoscore__Thermoscore___']", ); @@ -105,36 +104,30 @@ const assessmentsAveragePlugin: Plugin = { "Thermoscore__text___", ); - // Find assessment list const assessmentsList = document.querySelector( "#main > .assessmentsWrapper .assessments [class*='AssessmentList__items___']", ); if (!assessmentsList) return; - // Get marks from React state to match with DOM elements const state = await ReactFiber.find( "[class*='AssessmentList__items___']", ).getState(); const marks = state["marks"]; if (!marks || !marks.length) return; - // Parse and average grades - - // Get all assessment items (excluding the average we might have added) const assessmentItems = Array.from( - assessmentsList.querySelectorAll(`[class*='AssessmentItem__AssessmentItem___']`), + assessmentsList.querySelectorAll( + `[class*='AssessmentItem__AssessmentItem___']`, + ), ).filter( (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, - } = await processAssessments(api, assessmentItems); + const { weightedTotal, totalWeight, hasInaccurateWeighting, count } = + await processAssessments(api, assessmentItems); if (!count || totalWeight === 0) return; @@ -153,13 +146,11 @@ const assessmentsAveragePlugin: Plugin = { ? letterAvg : `${avg.toFixed(2)}%`; - // Prevent duplicate const existing = assessmentsList.querySelector( `[class*='AssessmentItem__title___']`, ); if (existing?.textContent === "Subject Average") return; - // Build warning message if needed let warningHTML = ""; if (hasInaccurateWeighting) { warningHTML = /* html */ ` @@ -169,8 +160,8 @@ const assessmentsAveragePlugin: Plugin = { `; } - // Use the dynamic class names in the HTML template - const averageElement = stringToHTML(/* html */ ` + assessmentsList.insertBefore( + stringToHTML(/* html */ `
@@ -182,13 +173,13 @@ const assessmentsAveragePlugin: Plugin = {
-
${display}
+
${display}
- `).firstChild; - - assessmentsList.insertBefore(averageElement!, assessmentsList.firstChild); + `).firstChild!, + assessmentsList.firstChild, + ); }); }, }; diff --git a/src/plugins/built-in/assessmentsAverage/utils.ts b/src/plugins/built-in/assessmentsAverage/utils.ts index 320dd3a9..bb93c090 100644 --- a/src/plugins/built-in/assessmentsAverage/utils.ts +++ b/src/plugins/built-in/assessmentsAverage/utils.ts @@ -33,7 +33,6 @@ export 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)); @@ -72,76 +71,66 @@ function parseGrade(text: string): number { return letterToNumber[str] ?? 0; } -function createWeightLabel(assessmentItem: Element, weighting: string | undefined) { +function createWeightLabel( + assessmentItem: Element, + weighting: string | undefined, +) { const statsContainer = assessmentItem.querySelector( `[class*='AssessmentItem__stats___']`, ) as HTMLElement; - if (statsContainer) { - // Only add label if it hasn't been added before - if (!statsContainer.querySelector(".betterseqta-weight-label")) { - const label = statsContainer.querySelector( - `[class*='Label__Label___']`, - ) as HTMLElement; + if ( + !statsContainer || + statsContainer.querySelector(".betterseqta-weight-label") + ) + return; - if (label) { - // Clone average score node - const weightLabel = label.cloneNode(true) as HTMLElement; + const label = statsContainer.querySelector( + `[class*='Label__Label___']`, + ) as HTMLElement; - // Mark as added to prevent duplicates - weightLabel.classList.add("betterseqta-weight-label"); + if (!label) return; - const innerTextDiv = weightLabel.querySelector( - `[class*='Label__innerText___']`, - ); + const weightLabel = label.cloneNode(true) as HTMLElement; + weightLabel.classList.add("betterseqta-weight-label"); - // Repurpose for showing weight - if (innerTextDiv) innerTextDiv.textContent = "Weight"; + 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, - ); + const textNodes = Array.from(weightLabel.childNodes).filter( + (node) => node.nodeType === Node.TEXT_NODE, + ); - // Set weight value, discarding useless decimals (.0) - if (textNodes.length) { - textNodes[0].textContent = - weighting && weighting !== "processing" - ? `${Number(weighting) % 1 === 0 ? Number(weighting) : weighting}%` - : "N/A"; - } - - // Set position opposite to the average score node - statsContainer.style.position = "relative"; - weightLabel.style.position = "absolute"; - weightLabel.style.right = "0"; - weightLabel.style.top = "50%"; - weightLabel.style.transform = "translateY(-50%)"; - - statsContainer.appendChild(weightLabel); - } - } + 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); } -// Detect Firefox (has stricter CSP for blob URLs) -// Use userAgent instead of deprecated InstallTrigger -export const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 && - !navigator.userAgent.toLowerCase().includes('seamonkey') && - !navigator.userAgent.toLowerCase().includes('waterfox'); +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 { - // Detect if URL is a blob URL 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) { return new Promise((resolve, reject) => { - // Inject script into page context to fetch (bypasses Firefox CSP restrictions) const script = document.createElement("script"); const requestId = `pdf-fetch-${Date.now()}-${Math.random()}`; - - // Escape URL for use in script const escapedUrl = url.replace(/'/g, "\\'"); script.textContent = ` @@ -178,9 +167,7 @@ async function fetchPDFAsArrayBuffer(url: string): Promise { } if (event.data.success) { - // Convert back to ArrayBuffer - const uint8Array = new Uint8Array(event.data.data); - resolve(uint8Array.buffer); + resolve(new Uint8Array(event.data.data).buffer); } else { reject(new Error(event.data.error || "Failed to fetch PDF")); } @@ -190,7 +177,6 @@ async function fetchPDFAsArrayBuffer(url: string): Promise { window.addEventListener("message", messageHandler); (document.head || document.documentElement).appendChild(script); - // Timeout after 30 seconds setTimeout(() => { window.removeEventListener("message", messageHandler); if (script.parentNode) { @@ -199,241 +185,226 @@ async function fetchPDFAsArrayBuffer(url: string): Promise { reject(new Error("Timeout fetching PDF")); }, 30000); }); - } else { - // Regular URL - fetch normally, but check if response URL becomes blob - try { - const response = await fetch(url, { - credentials: "include", - redirect: "follow", - }); + } - // Check if response URL is a blob URL (server might redirect to blob) - if (response.url && response.url.startsWith("blob:")) { - // Re-fetch using page context - return await fetchPDFAsArrayBuffer(response.url); - } + try { + const response = await fetch(url, { + credentials: "include", + redirect: "follow", + }); - if (!response.ok) { - throw new Error( - `Failed to fetch PDF: ${response.status} ${response.statusText}`, - ); - } - - return await response.arrayBuffer(); - } catch (error: any) { - // If error mentions blob or security, try using page context - if ( - error?.message?.includes("blob") || - error?.message?.includes("Security") || - error?.message?.includes("CSP") - ) { - // Force use page context - return await fetchPDFAsArrayBuffer(url); - } - throw error; + 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 { - // For Firefox, do everything in page context to avoid blob URL CSP issues - if (isFirefox) { - return new Promise((resolve, reject) => { - const script = document.createElement("script"); - const requestId = `pdf-extract-${Date.now()}-${Math.random()}`; + try { + if (isFirefox) { + return new Promise((resolve, reject) => { + const script = document.createElement("script"); + const requestId = `pdf-extract-${Date.now()}-${Math.random()}`; - // Escape URL for use in script (handle both single and double quotes) - const escapedUrl = url - .replace(/\\/g, "\\\\") - .replace(/'/g, "\\'") - .replace(/"/g, '\\"'); + const escapedUrl = url + .replace(/\\/g, "\\\\") + .replace(/'/g, "\\'") + .replace(/"/g, '\\"'); - script.textContent = ` - (function() { - const requestId = '${requestId}'; - const url = '${escapedUrl}'; - - // Check if pdfjs is already loaded - if (window.pdfjsLib) { - extractPDF(); - } else { - // Load pdfjs in page context - const pdfjsScript = document.createElement('script'); - pdfjsScript.src = 'https://cdn.jsdelivr.net/npm/pdfjs-dist/build/pdf.min.js'; - pdfjsScript.type = 'text/javascript'; + script.textContent = ` + (function() { + const requestId = '${requestId}'; + const url = '${escapedUrl}'; - pdfjsScript.onload = function() { + if (window.pdfjsLib) { extractPDF(); - }; - pdfjsScript.onerror = function() { - window.postMessage({ - type: requestId, - success: false, - error: 'Failed to load pdfjs library' - }, '*'); - }; + } 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); + } - document.head.appendChild(pdfjsScript); - } - - function extractPDF() { - try { - // Disable worker for Firefox to avoid blob URL CSP issues - // Set to empty string to disable worker completely - window.pdfjsLib.GlobalWorkerOptions.workerSrc = ''; - - // Use XMLHttpRequest instead of fetch for better blob URL handling - 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; - } + function extractPDF() { + try { + window.pdfjsLib.GlobalWorkerOptions.workerSrc = ''; - try { - const arrayBuffer = xhr.response; - if (!arrayBuffer || arrayBuffer.byteLength === 0) { - throw new Error('PDF response is empty'); + 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; } - window.pdfjsLib.getDocument({ - data: arrayBuffer, - useSystemFonts: true, - verbosity: 0, - // Explicitly disable worker - 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) { + 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: 'ArrayBuffer error: ' + (error.message || String(error)) + error: 'Network error fetching PDF' }, '*'); - } - }; - - xhr.onerror = function() { + }; + + 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: 'Network error fetching PDF' + error: 'Setup error: ' + (error.message || String(error)) }, '*'); - }; - - 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"), + ); } } - })(); - `; + }; - const messageHandler = (event: MessageEvent) => { - if (event.data?.type === requestId) { + window.addEventListener("message", messageHandler); + (document.head || document.documentElement).appendChild(script); + + setTimeout(() => { 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); - - // Timeout after 60 seconds (PDF parsing can take time) - setTimeout(() => { - window.removeEventListener("message", messageHandler); - if (script.parentNode) { - script.parentNode.removeChild(script); - } - reject(new Error("Timeout extracting PDF text")); - }, 60000); - }); - } else { - // Chrome - use extension context - try { - const arrayBuffer = await fetchPDFAsArrayBuffer(url); - - if (arrayBuffer.byteLength === 0) { - throw new Error("PDF response is empty"); - } - - const loadingTask = pdfjs.getDocument({ - data: arrayBuffer, - useSystemFonts: true, + reject(new Error("Timeout extracting PDF text")); + }, 60000); }); - - const pdf = await loadingTask.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.log(error); - throw error; } + + 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; } } @@ -444,7 +415,6 @@ async function handleWeightings(mark: any, api: any) { const userID = userInfo.id; const title = mark.title; - // Skip if already processed (not "processing") if ( api.storage.weightings[assessmentID] != undefined && api.storage.weightings[assessmentID] !== "processing" @@ -452,13 +422,11 @@ async function handleWeightings(mark: any, api: any) { return; } - // Set to processing api.storage.weightings = { ...api.storage.weightings, [assessmentID]: "processing", }; - // Correlate assessment title with its ID api.storage.assessments = { ...api.storage.assessments, [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)); 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:")) { throw new Error(`Cannot fetch blob URL from extension: ${pdfUrl}`); } let text: string; try { - // For Firefox, extractPDFText already handles everything in page context - // For Chrome, it uses extension context text = await extractPDFText(pdfUrl); } catch (error: any) { if ( @@ -513,26 +477,17 @@ async function handleWeightings(mark: any, api: any) { error?.message?.includes("CSP")) ) { await new Promise((resolve) => setTimeout(resolve, 2000)); - try { - text = await extractPDFText(pdfUrl); - } catch (retryError: any) { - throw new Error( - `PDF extraction failed after retry: ${retryError.message}`, - ); - } + text = await extractPDFText(pdfUrl); } else { throw new Error(`PDF extraction failed: ${error.message}`); } } - // Match weighting from extracted text 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, - [assessmentID]: weight, + [assessmentID]: match ? match[1] : "N/A", }; } catch (error: any) { api.storage.weightings = { @@ -550,11 +505,9 @@ export async function parseAssessments(api: any) { const marks = state["marks"]; if (!marks) return; - // Dispatch for all assessments asynchronously 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[]) { let weightedTotal = 0; let totalWeight = 0; @@ -579,16 +532,13 @@ export async function processAssessments(api: any, assessmentItems: Element[]) { const title = titleEl.textContent?.trim(); if (!title) continue; - // Get correlated assessment ID in order to fetch weightings const assessmentID = api.storage.assessments?.[title]; const weighting = assessmentID ? api.storage.weightings?.[assessmentID] : undefined; - // Creates a weighting label next to the average score createWeightLabel(assessmentItem, weighting); - // Check if weighting is unavailable or still processing if ( weighting === null || weighting === undefined || @@ -596,18 +546,15 @@ export async function processAssessments(api: any, assessmentItems: Element[]) { weighting === "processing" ) { hasInaccurateWeighting = true; - // Fall back to equal weighting if unavailable weightedTotal += grade; totalWeight += 1; } else { const weight = parseFloat(weighting); - // If weight is a positive number, add to total if (!isNaN(weight) && weight >= 0) { weightedTotal += grade * weight; totalWeight += weight; } else { - // Invalid weight, use equal weighting weightedTotal += grade; totalWeight += 1; hasInaccurateWeighting = true; @@ -622,4 +569,4 @@ export async function processAssessments(api: any, assessmentItems: Element[]) { hasInaccurateWeighting, count, }; -} \ No newline at end of file +} diff --git a/src/seqta/ui/AddBetterSEQTAElements.ts b/src/seqta/ui/AddBetterSEQTAElements.ts index a285fe0b..d7c23d26 100644 --- a/src/seqta/ui/AddBetterSEQTAElements.ts +++ b/src/seqta/ui/AddBetterSEQTAElements.ts @@ -30,11 +30,10 @@ export 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 */`
  • ` + fragment.appendChild( + stringToHTML( + /* html */ `
  • `, + ).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 */ ` -
    - -
    -
    - `).firstChild; - titlebar.append(userInfo!); - - const userinfo = stringToHTML(/* html */ ` -
    -
    -
    -

    -

    ${info.userDesc}

    -
    -

    ${info.meta.code} // ${info.meta.governmentID}

    + titlebar.append( + stringToHTML(/* html */ ` +
    + +
    -
    - `).firstChild; - titlebar.append(userinfo!); + `).firstChild!, + ); - var logoutbutton = document.getElementsByClassName("logout")[0]; - var userInfosvgdiv = document.getElementById("logouttooltip")!; - userInfosvgdiv.appendChild(logoutbutton); + titlebar.append( + stringToHTML(/* html */ ` +
    +
    +
    +

    +

    ${info.userDesc}

    +
    +

    ${info.meta.code} // ${info.meta.governmentID}

    +
    +
    + `).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 = - '
  • '; - const NewsButton = stringToHTML(NewsButtonStr); + fragment.appendChild( + stringToHTML( + '
  • ', + ).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 */ ` - - `); - let ContentDiv = document.getElementById("content"); - ContentDiv!.append(SettingsButton.firstChild!); + document.getElementById("content")!.append( + stringToHTML(/* html */ ` + + `).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 */ ``; const MOON_ICON_SVG = /* html */ ``; - - const initialSvgContent = settingsState.DarkMode ? SUN_ICON_SVG : MOON_ICON_SVG; - const LightDarkModeButton = stringToHTML(/* html */ ` - - `); - - let ContentDiv = document.getElementById("content"); - ContentDiv!.append(LightDarkModeButton.firstChild!); + document.getElementById("content")!.append( + stringToHTML(/* html */ ` + + `).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); } } diff --git a/src/seqta/utils/Loaders/LoadHomePage.ts b/src/seqta/utils/Loaders/LoadHomePage.ts index 9f4e576c..967caeb2 100644 --- a/src/seqta/utils/Loaders/LoadHomePage.ts +++ b/src/seqta/utils/Loaders/LoadHomePage.ts @@ -30,20 +30,17 @@ export async function loadHomePage() { element?.classList.add("active"); const main = document.getElementById("main"); - if (!main) { - console.error("[BetterSEQTA+] Main element not found."); - return; - } - - const homeRoot = stringToHTML(`
    `); + if (!main) return; main.innerHTML = ""; - main.appendChild(homeRoot?.firstChild!); + main.appendChild( + stringToHTML(`
    `).firstChild!, + ); const homeContainer = document.getElementById("home-root"); if (!homeContainer) return; - const skeletonStructure = stringToHTML(/* html */` + const skeletonStructure = stringToHTML(/* html */ `
    @@ -101,25 +98,16 @@ export async function loadHomePage() { renderShortcuts(); - const date = new Date(); - const TodayFormatted = formatDate(date); + const TodayFormatted = formatDate(new Date()); - const [assessmentsPromise, classesPromise, prefsPromise] = [ + const [assessments, classes, prefs] = await Promise.all([ GetUpcomingAssessments(), - GetActiveClasses(), - fetch(`${location.origin}/seqta/student/load/prefs?`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ asArray: true, request: "userPrefs" }), }).then((res) => res.json()), - ]; - - const [assessments, classes, prefs] = await Promise.all([ - assessmentsPromise, - classesPromise, - prefsPromise, ]); callHomeTimetable(TodayFormatted, true); @@ -159,20 +147,20 @@ export async function loadHomePage() { } async function GetUpcomingAssessments() { - let func = fetch( - `${location.origin}/seqta/student/assessment/list/upcoming?`, - { + try { + return fetch(`${location.origin}/seqta/student/assessment/list/upcoming?`, { method: "POST", headers: { "Content-Type": "application/json; charset=utf-8", }, body: JSON.stringify({ student: 69 }), - }, - ); - - return func - .then((result) => result.json()) - .then((response) => response.payload); + }) + .then((result) => result.json()) + .then((response) => response.payload); + } catch (error) { + console.error("[BetterSEQTA+] Failed to get upcoming assessments:", error); + return []; + } } function setupTimetableListeners() { @@ -230,15 +218,10 @@ async function GetActiveClasses() { body: JSON.stringify({}), }, ); - - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - - const data = await response.json(); - return data.payload; + return (await response.json()).payload; } 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; const fetchNotices = async (date: string) => { - let data; + try { + const data = settingsState.mockNotices + ? getMockNotices() + : await ( + await fetch(`${location.origin}/seqta/student/load/notices?`, { + method: "POST", + headers: { "Content-Type": "application/json; charset=utf-8" }, + body: JSON.stringify({ date }), + }) + ).json(); - if (settingsState.mockNotices) { - data = getMockNotices(); - } else { - const response = await fetch( - `${location.origin}/seqta/student/load/notices?`, - { - method: "POST", - headers: { "Content-Type": "application/json; charset=utf-8" }, - body: JSON.stringify({ date }), - }, - ); - data = await response.json(); + processNotices(data, labelArray); + } catch (error) { + console.error("[BetterSEQTA+] Failed to fetch notices:", error); } - - processNotices(data, labelArray); }; const debouncedInputChange = debounce((e: Event) => { - const target = e.target as HTMLInputElement; - fetchNotices(target.value); + fetchNotices((e.target as HTMLInputElement).value); }, 250); dateControl?.addEventListener("input", debouncedInputChange); @@ -290,16 +270,8 @@ function debounce any>( } function comparedate(obj1: any, obj2: any) { - if (obj1.date < obj2.date) { - return -1; - } - if (obj1.date > obj2.date) { - return 1; - } - return 0; + return obj1.date < obj2.date ? -1 : obj1.date > obj2.date ? 1 : 0; } - - function processNotices(response: any, labelArray: string[]) { const NoticeContainer = document.getElementById("notice-container"); if (!NoticeContainer) return; @@ -343,14 +315,14 @@ function processNoticeColor(colour: string): string | undefined { } function createNoticeElement(notice: any, colour: string | undefined): Node { - const textPreview = notice.contents - .replace(/<[^>]*>/g, "") - .replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "") - .replace(/\s+/g, " ") - .trim() - .substring(0, 150) - + (notice.contents.length > 150 ? "..." : ""); - + const textPreview = + notice.contents + .replace(/<[^>]*>/g, "") + .replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "") + .replace(/\s+/g, " ") + .trim() + .substring(0, 150) + (notice.contents.length > 150 ? "..." : ""); + const noticeId = `notice-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const htmlContent = ` @@ -369,12 +341,10 @@ function createNoticeElement(notice: any, colour: string | undefined): Node {
    `; const element = stringToHTML(htmlContent).firstChild as HTMLElement; - if (element) { - element.addEventListener("click", () => - openNoticeModal(notice, colour, element), - ); - } - return element!; + element.addEventListener("click", () => + openNoticeModal(notice, colour, element), + ); + return element; } function openNoticeModal( @@ -386,15 +356,11 @@ function openNoticeModal( .replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "") .replace(/ +/, " "); - const existingModal = document.getElementById("notice-modal"); - if (existingModal) { - existingModal.remove(); - } + document.getElementById("notice-modal")?.remove(); const sourceRect = sourceElement.getBoundingClientRect(); let scrollY = Math.round(window.scrollY); let scrollX = Math.round(window.scrollX); - let sourceLeft = sourceRect.left; let sourceTop = sourceRect.top; let sourceWidth = sourceRect.width; @@ -476,7 +442,6 @@ function openNoticeModal( let targetHeight = Math.round( Math.min(Math.max(measuredHeight, 200), viewportHeight * 0.85), ); - let targetLeft = Math.round((viewportWidth - targetWidth) / 2); let targetTop = Math.round((viewportHeight - targetHeight) / 2) + scrollY; @@ -585,13 +550,10 @@ function openNoticeModal( const newTargetWidth = Math.round( Math.min(Math.max(newSourceWidth, 800), newViewportWidth - 40), ); - - // Just measure the existing modal content const currentHeight = unifiedContent.getBoundingClientRect().height; const newTargetHeight = Math.round( Math.min(Math.max(currentHeight, 200), newViewportHeight * 0.85), ); - const newTargetLeft = Math.round((newViewportWidth - newTargetWidth) / 2); const newTargetTop = Math.round((newViewportHeight - newTargetHeight) / 2) + newScrollY; @@ -656,116 +618,92 @@ function callHomeTimetable(date: string, change?: any) { xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8"); xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (loadingTimeout) { - clearTimeout(loadingTimeout); - loadingTimeout = null; - } + if (xhr.readyState !== 4) return; - const DayContainer = document.getElementById("day-container")!; + if (loadingTimeout) { + clearTimeout(loadingTimeout); + loadingTimeout = null; + } - try { - var serverResponse = JSON.parse(xhr.response); - let lessonArray: Array = []; + const DayContainer = document.getElementById("day-container")!; - if (serverResponse.payload.items.length > 0) { - if (DayContainer.innerText || change) { - for (let i = 0; i < serverResponse.payload.items.length; i++) { - lessonArray.push(serverResponse.payload.items[i]); + var serverResponse = JSON.parse(xhr.response); + let lessonArray: Array = []; + + if (serverResponse.payload.items.length > 0) { + if (DayContainer.innerText || change) { + for (let i = 0; i < serverResponse.payload.items.length; i++) { + lessonArray.push(serverResponse.payload.items[i]); + } + lessonArray.sort(function (a, b) { + return a.from.localeCompare(b.from); + }); + + GetLessonColours().then((colours) => { + for (let i = 0; i < lessonArray.length; i++) { + let subjectname = + lessonArray[i].type == "tutorial" + ? `timetable.tutor.${lessonArray[i].tutorID}` + : `timetable.subject.colour.${lessonArray[i].code}`; + let subject = colours.find( + (element: any) => element.name === subjectname, + ); + + if (!subject) { + lessonArray[i].colour = "--item-colour: #8e8e8e;"; + } else { + lessonArray[i].colour = `--item-colour: ${subject.value};`; + if (GetThresholdOfColor(subject.value) > 300) { + lessonArray[i].invert = true; + } } - lessonArray.sort(function (a, b) { - return a.from.localeCompare(b.from); - }); - GetLessonColours().then((colours) => { - let subjects = colours; - for (let i = 0; i < lessonArray.length; i++) { - - let subjectname = ((lessonArray[i].type == "tutorial") ? `timetable.tutor.${lessonArray[i].tutorID}` : `timetable.subject.colour.${lessonArray[i].code}`); + lessonArray[i].from = lessonArray[i].from.substring(0, 5); + lessonArray[i].until = lessonArray[i].until.substring(0, 5); - let subject = subjects.find( - (element: any) => element.name === subjectname, - ); - if (!subject) { - lessonArray[i].colour = "--item-colour: #8e8e8e;"; - } else { - lessonArray[i].colour = `--item-colour: ${subject.value};`; - let result = GetThresholdOfColor(subject.value); + if (settingsState.timeFormat === "12") { + lessonArray[i].from = convertTo12HourFormat(lessonArray[i].from); + lessonArray[i].until = convertTo12HourFormat( + lessonArray[i].until, + ); + } - if (result > 300) { - lessonArray[i].invert = true; - } - } - - lessonArray[i].from = lessonArray[i].from.substring(0, 5); - lessonArray[i].until = lessonArray[i].until.substring(0, 5); - - if (settingsState.timeFormat === "12") { - lessonArray[i].from = convertTo12HourFormat( - lessonArray[i].from, - ); - lessonArray[i].until = convertTo12HourFormat( - lessonArray[i].until, - ); - } - - lessonArray[i].attendanceTitle = CheckUnmarkedAttendance( - lessonArray[i].attendance, - ); - } - - DayContainer.innerText = ""; - for (let i = 0; i < lessonArray.length; i++) { - var div = makeLessonDiv(lessonArray[i], i + 1); - - if (lessonArray[i].invert) { - const div1 = div.firstChild! as HTMLElement; - div1.classList.add("day-inverted"); - } - - DayContainer.append(div.firstChild as HTMLElement); - } - - DayContainer.classList.remove("loading"); - - const today = new Date(); - if (currentSelectedDate.getDate() == today.getDate()) { - for (let i = 0; i < lessonArray.length; i++) { - CheckCurrentLesson(lessonArray[i], i + 1); - } - - CheckCurrentLessonAll(lessonArray); - } - }); + lessonArray[i].attendanceTitle = CheckUnmarkedAttendance( + lessonArray[i].attendance, + ); + } + + DayContainer.innerText = ""; + for (let i = 0; i < lessonArray.length; i++) { + const div = makeLessonDiv(lessonArray[i], i + 1); + if (lessonArray[i].invert) { + (div.firstChild! as HTMLElement).classList.add("day-inverted"); + } + DayContainer.append(div.firstChild as HTMLElement); } - } else { - DayContainer.innerHTML = ""; - var dummyDay = document.createElement("div"); - dummyDay.classList.add("day-empty"); - let img = document.createElement("img"); - img.src = browser.runtime.getURL(LogoLight); - let text = document.createElement("p"); - text.innerText = "No lessons available."; - dummyDay.append(img); - dummyDay.append(text); - DayContainer.append(dummyDay); 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 = ` - -

    Error loading lessons. Please try again.

    - `; - DayContainer.append(errorDiv); + const today = new Date(); + if (currentSelectedDate.getDate() == today.getDate()) { + for (let i = 0; i < lessonArray.length; i++) { + CheckCurrentLesson(lessonArray[i], i + 1); + } + CheckCurrentLessonAll(lessonArray); + } + }); } + } else { + DayContainer.innerHTML = ""; + const dummyDay = document.createElement("div"); + dummyDay.classList.add("day-empty"); + const img = document.createElement("img"); + img.src = browser.runtime.getURL(LogoLight); + const text = document.createElement("p"); + text.innerText = "No lessons available."; + dummyDay.append(img, text); + DayContainer.append(dummyDay); + DayContainer.classList.remove("loading"); } }; xhr.send( @@ -855,8 +793,6 @@ async function CheckCurrentLesson(lesson: any, num: number) { } function makeLessonDiv(lesson: any, num: number) { - if (!lesson) throw new Error("No lesson provided."); - const { code, colour, @@ -869,14 +805,14 @@ function makeLessonDiv(lesson: any, num: number) { programmeID, metaID, assessments, - type + type, } = lesson; let lessonString = `
    -

    ${(type == "class") ? description : (type == "tutorial") ? "Tutorial" : "Unknown"}

    +

    ${type == "class" ? description : type == "tutorial" ? "Tutorial" : "Unknown"}

    ${staff || "Unknown"}

    -

    ${(type == "class") ? room : (type == "tutorial") ? "N/A" : "Unknown"}

    +

    ${type == "class" ? room : type == "tutorial" ? "N/A" : "Unknown"}

    ${from || "Unknown"} - ${until || "Unknown"}

    ${attendanceTitle || "Unknown"}
    `; @@ -922,64 +858,48 @@ function buildAssessmentURL(programmeID: any, metaID: any, itemID = "") { } function CheckUnmarkedAttendance(lessonattendance: any) { - if (lessonattendance) { - var lesson = lessonattendance.label; - } else { - lesson = " "; - } - return lesson; + return lessonattendance ? lessonattendance.label : " "; } async function CreateUpcomingSection(assessments: any, activeSubjects: any) { - let upcomingitemcontainer = document.querySelector("#upcoming-items"); - let overdueDates = []; - let upcomingDates = {}; - - var Today = new Date(); + const upcomingitemcontainer = document.querySelector("#upcoming-items"); + const overdueDates = []; + const upcomingDates = {}; + const Today = new Date(); for (let i = 0; i < assessments.length; i++) { - const assessment = assessments[i]; - let assessmentdue = new Date(assessment.due); - - CheckSpecialDay(Today, assessmentdue); - if (assessmentdue < Today) { - if (!CheckSpecialDay(Today, assessmentdue)) { - overdueDates.push(assessment); - assessments.splice(i, 1); - i--; - } + const assessmentdue = new Date(assessments[i].due); + if (assessmentdue < Today && !CheckSpecialDay(Today, assessmentdue)) { + overdueDates.push(assessments[i]); + assessments.splice(i, 1); + i--; } } - var TomorrowDate = new Date(); - TomorrowDate.setDate(TomorrowDate.getDate() + 1); - const colours = await GetLessonColours(); - let subjects = colours; for (let i = 0; i < assessments.length; i++) { - let subjectname = `timetable.subject.colour.${assessments[i].code}`; - - let subject = subjects.find((element: any) => element.name === subjectname); - + const subject = colours.find( + (element: any) => + element.name === `timetable.subject.colour.${assessments[i].code}`, + ); if (!subject) { assessments[i].colour = "--item-colour: #8e8e8e;"; } else { assessments[i].colour = `--item-colour: ${subject.value};`; - GetThresholdOfColor(subject.value); } } for (let i = 0; i < activeSubjects.length; i++) { const element = activeSubjects[i]; - let subjectname = `timetable.subject.colour.${element.code}`; - let colour = colours.find((element: any) => element.name === subjectname); + const colour = colours.find( + (c: any) => c.name === `timetable.subject.colour.${element.code}`, + ); if (!colour) { element.colour = "--item-colour: #8e8e8e;"; } else { element.colour = `--item-colour: ${colour.value};`; - let result = GetThresholdOfColor(colour.value); - if (result > 300) { + if (GetThresholdOfColor(colour.value) > 300) { element.invert = true; } } @@ -987,52 +907,35 @@ async function CreateUpcomingSection(assessments: any, activeSubjects: any) { CreateFilters(activeSubjects); - let type; - let class_; - for (let i = 0; i < assessments.length; i++) { const element: any = assessments[i]; if (!upcomingDates[element.due as keyof typeof upcomingDates]) { - let dateObj: any = new Object(); - dateObj.div = CreateElement( - (type = "div"), - (class_ = "upcoming-date-container"), - ); - dateObj.assessments = []; + const dateObj: any = { + div: CreateElement("div", "upcoming-date-container"), + assessments: [], + }; (upcomingDates[element.due as keyof typeof upcomingDates] as any) = dateObj; } - let assessmentDateDiv = + const assessmentDateDiv = upcomingDates[element.due as keyof typeof upcomingDates]; - if (assessmentDateDiv) { (assessmentDateDiv as any).assessments.push(element); } } for (var date in upcomingDates) { - let assessmentdue = new Date( + const assessmentdue = new Date( ( upcomingDates[date as keyof typeof upcomingDates] as any ).assessments[0].due, ); - let specialcase = CheckSpecialDay(Today, assessmentdue); - let assessmentDate; - - if (specialcase) { - let datecase: string = specialcase!; - assessmentDate = createAssessmentDateDiv( - date, - upcomingDates[date as keyof typeof upcomingDates], - - datecase, - ); - } else { - assessmentDate = createAssessmentDateDiv( - date, - upcomingDates[date as keyof typeof upcomingDates], - ); - } + const specialcase = CheckSpecialDay(Today, assessmentdue); + const assessmentDate = createAssessmentDateDiv( + date, + upcomingDates[date as keyof typeof upcomingDates], + specialcase, + ); if (specialcase === "Yesterday") { upcomingitemcontainer!.insertBefore( @@ -1044,7 +947,7 @@ async function CreateUpcomingSection(assessments: any, activeSubjects: any) { } } FilterUpcomingAssessments(settingsState.subjectfilters); - + if (assessments.length === 0) { upcomingitemcontainer!.innerHTML = `
    @@ -1055,77 +958,68 @@ async function CreateUpcomingSection(assessments: any, activeSubjects: any) { } function createAssessmentDateDiv(date: string, value: any, datecase?: any) { - var options = { + const options = { weekday: "long" as "long", month: "long" as "long", day: "numeric" as "numeric", }; const FormattedDate = new Date(date); - const assessments = value.assessments; const container = value.div; - let DateTitleDiv = document.createElement("div"); + const DateTitleDiv = document.createElement("div"); DateTitleDiv.classList.add("upcoming-date-title"); if (datecase) { - let datetitle = document.createElement("h5"); + const datetitle = document.createElement("h5"); datetitle.classList.add("upcoming-special-day"); datetitle.innerText = datecase; DateTitleDiv.append(datetitle); container.setAttribute("data-day", datecase); } - let DateTitle = document.createElement("h5"); + const DateTitle = document.createElement("h5"); DateTitle.innerText = FormattedDate.toLocaleDateString("en-AU", options); DateTitleDiv.append(DateTitle); - container.append(DateTitleDiv); - let assessmentContainer = document.createElement("div"); + const assessmentContainer = document.createElement("div"); assessmentContainer.classList.add("upcoming-date-assessments"); for (let i = 0; i < assessments.length; i++) { const element = assessments[i]; - let item = document.createElement("div"); + const item = document.createElement("div"); item.classList.add("upcoming-assessment"); item.setAttribute("data-subject", element.code); item.id = `assessment${element.id}`; - item.style.cssText = element.colour; - let titlediv = document.createElement("div"); + const titlediv = document.createElement("div"); titlediv.classList.add("upcoming-subject-title"); - - let titlesvg = + titlediv.append( stringToHTML(` - `).firstChild; - titlediv.append(titlesvg!); + `).firstChild!, + ); - let detailsdiv = document.createElement("div"); + const detailsdiv = document.createElement("div"); detailsdiv.classList.add("upcoming-details"); - let detailstitle = document.createElement("h5"); + const detailstitle = document.createElement("h5"); detailstitle.innerText = `${element.subject} assessment`; - let subject = document.createElement("p"); + const subject = document.createElement("p"); subject.innerText = element.title; subject.classList.add("upcoming-assessment-title"); subject.onclick = function () { document.querySelector("#menu ul")!.classList.add("noscroll"); location.href = `../#?page=/assessments/${element.programmeID}:${element.metaclassID}&item=${element.id}`; }; - detailsdiv.append(detailstitle); - detailsdiv.append(subject); - - item.append(titlediv); - item.append(detailsdiv); + detailsdiv.append(detailstitle, subject); + item.append(titlediv, detailsdiv); assessmentContainer.append(item); fetch(`${location.origin}/seqta/student/assessment/submissions/get`, { method: "POST", - headers: { - "Content-Type": "application/json; charset=utf-8", - }, + headers: { "Content-Type": "application/json; charset=utf-8" }, body: JSON.stringify({ assessment: element.id, metaclass: element.metaclassID, @@ -1136,8 +1030,7 @@ function createAssessmentDateDiv(date: string, value: any, datecase?: any) { .then((response) => { if (response.payload.length > 0) { const assessment = document.querySelector(`#assessment${element.id}`); - - let submittedtext = document.createElement("div"); + const submittedtext = document.createElement("div"); submittedtext.classList.add("upcoming-submittedtext"); submittedtext.innerText = "Submitted"; assessment!.append(submittedtext); @@ -1175,36 +1068,37 @@ function CheckSpecialDay(date1: Date, date2: Date) { } async function GetLessonColours() { - let func = fetch(`${location.origin}/seqta/student/load/prefs?`, { - method: "POST", - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - body: JSON.stringify({ request: "userPrefs", asArray: true, user: 69 }), - }); - return func - .then((result) => result.json()) - .then((response) => response.payload); + try { + return fetch(`${location.origin}/seqta/student/load/prefs?`, { + method: "POST", + headers: { "Content-Type": "application/json; charset=utf-8" }, + body: JSON.stringify({ request: "userPrefs", asArray: true, user: 69 }), + }) + .then((result) => result.json()) + .then((response) => response.payload); + } catch (error) { + console.error("[BetterSEQTA+] Failed to get lesson colours:", error); + return []; + } } 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++) { const element = subjects[i]; - if (!Object.prototype.hasOwnProperty.call(filteroptions, element.code)) { filteroptions[element.code] = true; settingsState.subjectfilters = filteroptions; } - let elementdiv = CreateSubjectFilter( - element.code, - element.colour, - filteroptions[element.code], + filterdiv!.append( + CreateSubjectFilter( + element.code, + element.colour, + filteroptions[element.code], + ), ); - - filterdiv!.append(elementdiv); } } @@ -1213,23 +1107,20 @@ function CreateSubjectFilter( itemcolour: string, checked: any, ) { - let label = CreateElement("label", "upcoming-checkbox-container"); + const label = CreateElement("label", "upcoming-checkbox-container"); label.innerText = subjectcode; - let input1 = CreateElement("input"); - const input = input1 as HTMLInputElement; + const input = CreateElement("input") as HTMLInputElement; input.type = "checkbox"; input.checked = checked; input.id = `filter-${subjectcode}`; label.style.cssText = itemcolour; - let span = CreateElement("span", "upcoming-checkmark"); - label.append(input); - label.append(span); + const span = CreateElement("span", "upcoming-checkmark"); + label.append(input, span); input.addEventListener("change", function (change) { - let filters = settingsState.subjectfilters; - let id = (change.target as HTMLInputElement)!.id.split("-")[1]; - filters[id] = (change.target as HTMLInputElement)!.checked; - + const filters = settingsState.subjectfilters; + const id = (change.target as HTMLInputElement).id.split("-")[1]; + filters[id] = (change.target as HTMLInputElement).checked; settingsState.subjectfilters = filters; });