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/package.json b/package.json index e9ddc124..9632d079 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,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", diff --git a/src/SEQTA.ts b/src/SEQTA.ts index 70773ca1..d94c7b71 100644 --- a/src/SEQTA.ts +++ b/src/SEQTA.ts @@ -25,24 +25,8 @@ if (document.childNodes[1]) { init(); } -/** - * Initializes BetterSEQTA+ on a SEQTA page. - * - * This function performs the following steps: - * 1. Verifies that the current page is a SEQTA page. - * 2. Injects CSS styles for document loading. - * 3. Changes the page's favicon. - * 4. Initializes the extension's settings state. - * 5. Sets default storage if settings are not already defined. - * 6. Calls the main function to apply core BetterSEQTA+ modifications. - * 7. Initializes legacy and new plugins if the extension is enabled. - * 8. Logs success or error messages during initialization. - */ async function init() { - const hasSEQTATitle = document.title.includes("SEQTA Learn"); - - if (hasSEQTAText && hasSEQTATitle && !IsSEQTAPage) { - // Verify we are on a SEQTA page + if (hasSEQTAText && document.title.includes("SEQTA Learn") && !IsSEQTAPage) { IsSEQTAPage = true; console.info("[BetterSEQTA+] Verified SEQTA Page"); @@ -98,7 +82,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 d3ef0051..6db54831 100644 --- a/src/plugins/built-in/assessmentsAverage/index.ts +++ b/src/plugins/built-in/assessmentsAverage/index.ts @@ -7,6 +7,20 @@ import { import { type Plugin } from "@/plugins/core/types"; import stringToHTML from "@/seqta/utils/stringToHTML"; import { waitForElm } from "@/seqta/utils/waitForElm"; +import ReactFiber from "@/seqta/utils/ReactFiber.ts"; +import { + clearStuck, + getClassByPattern, + initStorage, + letterToNumber, + parseAssessments, + processAssessments, +} from "./utils.ts"; + +interface weightingsStorage { + weightings: Record; + assessments: Record; +} const settings = defineSettings({ lettergrade: booleanSetting({ @@ -23,7 +37,7 @@ class AssessmentsAveragePluginClass extends BasePlugin { const instance = new AssessmentsAveragePluginClass(); -const assessmentsAveragePlugin: Plugin = { +const assessmentsAveragePlugin: Plugin = { id: "assessments-average", name: "Assessment Averages", description: "Adds an average grade to the Assessments page", @@ -32,8 +46,10 @@ const assessmentsAveragePlugin: Plugin = { settings: instance.settings, run: async (api) => { + await initStorage(api); + clearStuck(api); + api.seqta.onMount(".assessmentsWrapper", async () => { - // Wait for any assessment item to load first await waitForElm( "#main > .assessmentsWrapper .assessments [class*='AssessmentItem__AssessmentItem___']", true, @@ -41,26 +57,13 @@ const assessmentsAveragePlugin: Plugin = { 1000, ); - // Helper function to find actual class names by their base pattern - const getClassByPattern = ( - element: Element | Document, - basePattern: string, - ): string => { - // Find all classes on the element - const classes = Array.from(element.querySelectorAll("*")) - .flatMap((el) => Array.from(el.classList)) - .filter((className) => className.startsWith(basePattern)); + await parseAssessments(api); - return classes.length ? classes[0] : ""; - }; - - // Find actual class names from the DOM const sampleAssessmentItem = document.querySelector( "[class*='AssessmentItem__AssessmentItem___']", ); if (!sampleAssessmentItem) return; - // Extract all necessary class patterns from a sample assessment item const assessmentItemClass = Array.from(sampleAssessmentItem.classList).find((c) => c.startsWith("AssessmentItem__AssessmentItem___"), @@ -83,7 +86,6 @@ const assessmentsAveragePlugin: Plugin = { "AssessmentItem__title___", ); - // Get Thermoscore classes const thermoscoreElement = document.querySelector( "[class*='Thermoscore__Thermoscore___']", ); @@ -102,62 +104,34 @@ const assessmentsAveragePlugin: Plugin = { "Thermoscore__text___", ); - // Find assessment list const assessmentsList = document.querySelector( "#main > .assessmentsWrapper .assessments [class*='AssessmentList__items___']", ); if (!assessmentsList) return; - const gradeElements = document.querySelectorAll( - "[class*='Thermoscore__text___']", + const state = await ReactFiber.find( + "[class*='AssessmentList__items___']", + ).getState(); + const marks = state["marks"]; + if (!marks || !marks.length) return; + + const assessmentItems = Array.from( + assessmentsList.querySelectorAll( + `[class*='AssessmentItem__AssessmentItem___']`, + ), + ).filter( + (item) => + !item + .querySelector(`[class*='AssessmentItem__title___']`) + ?.textContent?.includes("Subject Average"), ); - if (!gradeElements.length) return; - // Parse and average grades - const letterToNumber: Record = { - "A+": 100, - A: 95, - "A-": 90, - "B+": 85, - B: 80, - "B-": 75, - "C+": 70, - C: 65, - "C-": 60, - "D+": 55, - D: 50, - "D-": 45, - "E+": 40, - E: 35, - "E-": 30, - F: 0, - }; + const { weightedTotal, totalWeight, hasInaccurateWeighting, count } = + await processAssessments(api, assessmentItems); - function parseGrade(text: string): number { - const str = text.trim().toUpperCase(); - if (str.includes("/")) { - const [raw, max] = str.split("/").map((n) => parseFloat(n)); - return (raw / max) * 100; - } - if (str.includes("%")) { - return parseFloat(str.replace("%", "")) || 0; - } - return letterToNumber[str] ?? 0; - } + if (!count || totalWeight === 0) return; - let total = 0; - let count = 0; - gradeElements.forEach((el) => { - const grade = parseGrade(el.textContent || ""); - if (grade > 0) { - total += grade; - count++; - } - }); - - if (!count) return; - - const avg = total / count; + const avg = weightedTotal / totalWeight; const rounded = Math.ceil(avg / 5) * 5; const numberToLetter = Object.entries(letterToNumber).reduce( (acc, [k, v]) => { @@ -172,31 +146,40 @@ const assessmentsAveragePlugin: Plugin = { ? letterAvg : `${avg.toFixed(2)}%`; - // Prevent duplicate const existing = assessmentsList.querySelector( `[class*='AssessmentItem__title___']`, ); if (existing?.textContent === "Subject Average") return; - // Use the dynamic class names in the HTML template - const averageElement = stringToHTML(/* html */ ` + let warningHTML = ""; + if (hasInaccurateWeighting) { + warningHTML = /* html */ ` +
+ ⚠ Some weightings unavailable +
+ `; + } + + assessmentsList.insertBefore( + stringToHTML(/* html */ `
Subject Average
+ ${warningHTML}
-
${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 new file mode 100644 index 00000000..bb93c090 --- /dev/null +++ b/src/plugins/built-in/assessmentsAverage/utils.ts @@ -0,0 +1,572 @@ +import { getUserInfo } from "@/seqta/ui/AddBetterSEQTAElements.ts"; +import ReactFiber from "@/seqta/utils/ReactFiber.ts"; +import * as pdfjs from "pdfjs-dist"; +pdfjs.GlobalWorkerOptions.workerSrc = + "https://cdn.jsdelivr.net/npm/pdfjs-dist/build/pdf.worker.min.mjs"; + +export async function initStorage(api: any) { + await api.storage.loaded; + + if (!api.storage.weightings) { + api.storage.weightings = {}; + } + if (!api.storage.assessments) { + api.storage.assessments = {}; + } +} + +export function clearStuck(api: any) { + let hasStuckProcessing = false; + for (const key in api.storage.weightings) { + if (api.storage.weightings[key] === "processing") { + delete api.storage.weightings[key]; + hasStuckProcessing = true; + } + } + if (hasStuckProcessing) { + api.storage.weightings = { ...api.storage.weightings }; + } +} + +// Helper function to find actual class names by their base pattern +export const getClassByPattern = ( + element: Element | Document, + basePattern: string, +): string => { + const classes = Array.from(element.querySelectorAll("*")) + .flatMap((el) => Array.from(el.classList)) + .filter((className) => className.startsWith(basePattern)); + + return classes.length ? classes[0] : ""; +}; + +export const letterToNumber: Record = { + "A+": 100, + A: 95, + "A-": 90, + "B+": 85, + B: 80, + "B-": 75, + "C+": 70, + C: 65, + "C-": 60, + "D+": 55, + D: 50, + "D-": 45, + "E+": 40, + E: 35, + "E-": 30, + F: 0, +}; + +function parseGrade(text: string): number { + const str = text.trim().toUpperCase(); + if (str.includes("/")) { + const [raw, max] = str.split("/").map((n) => parseFloat(n)); + return (raw / max) * 100; + } + if (str.includes("%")) { + return parseFloat(str.replace("%", "")) || 0; + } + return letterToNumber[str] ?? 0; +} + +function createWeightLabel( + assessmentItem: Element, + weighting: string | undefined, +) { + const statsContainer = assessmentItem.querySelector( + `[class*='AssessmentItem__stats___']`, + ) as HTMLElement; + + if ( + !statsContainer || + statsContainer.querySelector(".betterseqta-weight-label") + ) + return; + + const label = statsContainer.querySelector( + `[class*='Label__Label___']`, + ) as HTMLElement; + + if (!label) return; + + const weightLabel = label.cloneNode(true) as HTMLElement; + weightLabel.classList.add("betterseqta-weight-label"); + + const innerTextDiv = weightLabel.querySelector( + `[class*='Label__innerText___']`, + ); + if (innerTextDiv) innerTextDiv.textContent = "Weight"; + + const textNodes = Array.from(weightLabel.childNodes).filter( + (node) => node.nodeType === Node.TEXT_NODE, + ); + + if (textNodes.length) { + textNodes[0].textContent = + weighting && weighting !== "processing" + ? `${Number(weighting) % 1 === 0 ? Number(weighting) : weighting}%` + : "N/A"; + } + + statsContainer.style.position = "relative"; + weightLabel.style.position = "absolute"; + weightLabel.style.right = "0"; + weightLabel.style.top = "50%"; + weightLabel.style.transform = "translateY(-50%)"; + + statsContainer.appendChild(weightLabel); +} + +export const isFirefox = + navigator.userAgent.toLowerCase().indexOf("firefox") > -1 && + !navigator.userAgent.toLowerCase().includes("seamonkey") && + !navigator.userAgent.toLowerCase().includes("waterfox"); + +async function fetchPDFAsArrayBuffer(url: string): Promise { + const isBlobUrl = url.startsWith("blob:"); + + if (isBlobUrl || isFirefox) { + return new Promise((resolve, reject) => { + const script = document.createElement("script"); + const requestId = `pdf-fetch-${Date.now()}-${Math.random()}`; + const escapedUrl = url.replace(/'/g, "\\'"); + + script.textContent = ` + (function() { + fetch('${escapedUrl}') + .then(response => { + if (!response.ok) { + throw new Error('HTTP ' + response.status + ': ' + response.statusText); + } + return response.arrayBuffer(); + }) + .then(arrayBuffer => { + window.postMessage({ + type: '${requestId}', + success: true, + data: Array.from(new Uint8Array(arrayBuffer)) + }, '*'); + }) + .catch(error => { + window.postMessage({ + type: '${requestId}', + success: false, + error: error.message || String(error) + }, '*'); + }); + })(); + `; + + const messageHandler = (event: MessageEvent) => { + if (event.data?.type === requestId) { + window.removeEventListener("message", messageHandler); + if (script.parentNode) { + script.parentNode.removeChild(script); + } + + if (event.data.success) { + resolve(new Uint8Array(event.data.data).buffer); + } else { + reject(new Error(event.data.error || "Failed to fetch PDF")); + } + } + }; + + window.addEventListener("message", messageHandler); + (document.head || document.documentElement).appendChild(script); + + setTimeout(() => { + window.removeEventListener("message", messageHandler); + if (script.parentNode) { + script.parentNode.removeChild(script); + } + reject(new Error("Timeout fetching PDF")); + }, 30000); + }); + } + + try { + const response = await fetch(url, { + credentials: "include", + redirect: "follow", + }); + + if (response.url && response.url.startsWith("blob:")) { + return await fetchPDFAsArrayBuffer(response.url); + } + + if (!response.ok) { + throw new Error( + `Failed to fetch PDF: ${response.status} ${response.statusText}`, + ); + } + + return await response.arrayBuffer(); + } catch (error: any) { + if ( + error?.message?.includes("blob") || + error?.message?.includes("Security") || + error?.message?.includes("CSP") + ) { + return await fetchPDFAsArrayBuffer(url); + } + throw error; + } +} + +export async function extractPDFText(url: string): Promise { + try { + if (isFirefox) { + return new Promise((resolve, reject) => { + const script = document.createElement("script"); + const requestId = `pdf-extract-${Date.now()}-${Math.random()}`; + + const escapedUrl = url + .replace(/\\/g, "\\\\") + .replace(/'/g, "\\'") + .replace(/"/g, '\\"'); + + script.textContent = ` + (function() { + const requestId = '${requestId}'; + const url = '${escapedUrl}'; + + if (window.pdfjsLib) { + extractPDF(); + } else { + const pdfjsScript = document.createElement('script'); + pdfjsScript.src = 'https://cdn.jsdelivr.net/npm/pdfjs-dist/build/pdf.min.js'; + pdfjsScript.type = 'text/javascript'; + + pdfjsScript.onload = function() { + extractPDF(); + }; + pdfjsScript.onerror = function() { + window.postMessage({ + type: requestId, + success: false, + error: 'Failed to load pdfjs library' + }, '*'); + }; + + document.head.appendChild(pdfjsScript); + } + + function extractPDF() { + try { + window.pdfjsLib.GlobalWorkerOptions.workerSrc = ''; + + const xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.responseType = 'arraybuffer'; + xhr.withCredentials = true; + + xhr.onload = function() { + if (xhr.status !== 200) { + window.postMessage({ + type: requestId, + success: false, + error: 'HTTP ' + xhr.status + ': ' + xhr.statusText + }, '*'); + return; + } + + try { + const arrayBuffer = xhr.response; + if (!arrayBuffer || arrayBuffer.byteLength === 0) { + throw new Error('PDF response is empty'); + } + + window.pdfjsLib.getDocument({ + data: arrayBuffer, + useSystemFonts: true, + verbosity: 0, + useWorkerFetch: false, + isEvalSupported: false + }).promise + .then(pdf => { + const pagePromises = []; + for (let i = 1; i <= pdf.numPages; i++) { + pagePromises.push( + pdf.getPage(i).then(page => { + return page.getTextContent().then(content => { + return content.items.map(item => item.str).join(' '); + }); + }) + ); + } + return Promise.all(pagePromises); + }) + .then(pages => { + const text = pages.join('\\n'); + window.postMessage({ + type: requestId, + success: true, + text: text + }, '*'); + }) + .catch(error => { + window.postMessage({ + type: requestId, + success: false, + error: 'PDF parsing error: ' + (error.message || String(error)) + }, '*'); + }); + } catch (error) { + window.postMessage({ + type: requestId, + success: false, + error: 'ArrayBuffer error: ' + (error.message || String(error)) + }, '*'); + } + }; + + xhr.onerror = function() { + window.postMessage({ + type: requestId, + success: false, + error: 'Network error fetching PDF' + }, '*'); + }; + + xhr.ontimeout = function() { + window.postMessage({ + type: requestId, + success: false, + error: 'Timeout fetching PDF' + }, '*'); + }; + + xhr.timeout = 30000; + xhr.send(); + } catch (error) { + window.postMessage({ + type: requestId, + success: false, + error: 'Setup error: ' + (error.message || String(error)) + }, '*'); + } + } + })(); + `; + + const messageHandler = (event: MessageEvent) => { + if (event.data?.type === requestId) { + window.removeEventListener("message", messageHandler); + if (script.parentNode) { + script.parentNode.removeChild(script); + } + + if (event.data.success) { + resolve(event.data.text); + } else { + reject( + new Error(event.data.error || "Failed to extract PDF text"), + ); + } + } + }; + + window.addEventListener("message", messageHandler); + (document.head || document.documentElement).appendChild(script); + + setTimeout(() => { + window.removeEventListener("message", messageHandler); + if (script.parentNode) { + script.parentNode.removeChild(script); + } + reject(new Error("Timeout extracting PDF text")); + }, 60000); + }); + } + + const arrayBuffer = await fetchPDFAsArrayBuffer(url); + + if (arrayBuffer.byteLength === 0) { + throw new Error("PDF response is empty"); + } + + const pdf = await pdfjs.getDocument({ + data: arrayBuffer, + useSystemFonts: true, + }).promise; + + let text = ""; + + for (let i = 1; i <= pdf.numPages; i++) { + const page = await pdf.getPage(i); + const content = await page.getTextContent(); + text += content.items.map((item: any) => item.str).join(" ") + "\n"; + } + + return text; + } catch (error) { + console.error("[BetterSEQTA+] Failed to extract PDF text:", error); + throw error; + } +} + +async function handleWeightings(mark: any, api: any) { + const assessmentID = mark.id; + const metaclassID = mark.metaclassID; + const userInfo = await getUserInfo(); + const userID = userInfo.id; + const title = mark.title; + + if ( + api.storage.weightings[assessmentID] != undefined && + api.storage.weightings[assessmentID] !== "processing" + ) { + return; + } + + api.storage.weightings = { + ...api.storage.weightings, + [assessmentID]: "processing", + }; + + api.storage.assessments = { + ...api.storage.assessments, + [title.trim()]: assessmentID, + }; + + try { + const filename = + "BetterSEQTA-" + + String(Math.floor(Math.random() * 1e15)).padStart(15, "0"); + + const printResponse = await fetch( + `${location.origin}/seqta/student/print/assessment`, + { + method: "POST", + headers: { "Content-Type": "application/json; charset=utf-8" }, + credentials: "include", + body: JSON.stringify({ + fileName: filename, + id: assessmentID, + metaclass: metaclassID, + student: userID, + }), + }, + ); + + if (!printResponse.ok) { + throw new Error( + `Failed to generate PDF: ${printResponse.status} ${printResponse.statusText}`, + ); + } + + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const pdfUrl = `${location.origin}/seqta/student/report/get?file=${filename}`; + + if (pdfUrl.startsWith("blob:")) { + throw new Error(`Cannot fetch blob URL from extension: ${pdfUrl}`); + } + + let text: string; + try { + text = await extractPDFText(pdfUrl); + } catch (error: any) { + if ( + isFirefox && + (error?.message?.includes("blob") || + error?.message?.includes("Security") || + error?.message?.includes("CSP")) + ) { + await new Promise((resolve) => setTimeout(resolve, 2000)); + text = await extractPDFText(pdfUrl); + } else { + throw new Error(`PDF extraction failed: ${error.message}`); + } + } + + const match = text.match(/weight:\s*(\d+\.?\d*)/i); + + api.storage.weightings = { + ...api.storage.weightings, + [assessmentID]: match ? match[1] : "N/A", + }; + } catch (error: any) { + api.storage.weightings = { + ...api.storage.weightings, + [assessmentID]: "N/A", + }; + } +} + +export async function parseAssessments(api: any) { + const state = await ReactFiber.find( + "[class*='AssessmentList__items___']", + ).getState(); + + const marks = state["marks"]; + if (!marks) return; + + await Promise.all(marks.map((mark: any) => handleWeightings(mark, api))); +} + +export async function processAssessments(api: any, assessmentItems: Element[]) { + let weightedTotal = 0; + let totalWeight = 0; + let hasInaccurateWeighting = false; + let count = 0; + + for (const assessmentItem of assessmentItems) { + const gradeElement = assessmentItem.querySelector( + `[class*='Thermoscore__text___']`, + ); + + if (!gradeElement) continue; + + const grade = parseGrade(gradeElement.textContent || ""); + if (grade <= 0) continue; + + const titleEl = assessmentItem.querySelector( + `[class*='AssessmentItem__title___']`, + ); + if (!titleEl) continue; + + const title = titleEl.textContent?.trim(); + if (!title) continue; + + const assessmentID = api.storage.assessments?.[title]; + const weighting = assessmentID + ? api.storage.weightings?.[assessmentID] + : undefined; + + createWeightLabel(assessmentItem, weighting); + + if ( + weighting === null || + weighting === undefined || + weighting === "N/A" || + weighting === "processing" + ) { + hasInaccurateWeighting = true; + weightedTotal += grade; + totalWeight += 1; + } else { + const weight = parseFloat(weighting); + + if (!isNaN(weight) && weight >= 0) { + weightedTotal += grade * weight; + totalWeight += weight; + } else { + weightedTotal += grade; + totalWeight += 1; + hasInaccurateWeighting = true; + } + } + count++; + } + + return { + weightedTotal, + totalWeight, + hasInaccurateWeighting, + count, + }; +} diff --git a/src/seqta/ui/AddBetterSEQTAElements.ts b/src/seqta/ui/AddBetterSEQTAElements.ts index e7b5df62..d7c23d26 100644 --- a/src/seqta/ui/AddBetterSEQTAElements.ts +++ b/src/seqta/ui/AddBetterSEQTAElements.ts @@ -14,7 +14,7 @@ let cachedUserInfo: any = null; let LightDarkModeSnakeEggButton = 0; -async function getUserInfo() { +export async function getUserInfo() { if (cachedUserInfo) return cachedUserInfo; try { @@ -30,11 +30,10 @@ async function getUserInfo() { }), }); - const responseData = await response.json(); - cachedUserInfo = responseData.payload; + cachedUserInfo = (await response.json()).payload; return cachedUserInfo; } catch (error) { - console.error("Error fetching user info:", error); + console.error("[BetterSEQTA+] Failed to get user info:", error); throw error; } } @@ -61,7 +60,7 @@ export async function AddBetterSEQTAElements() { handleStudentData(), ]); } catch (error) { - console.error("Error initializing UI elements:", error); + console.error("[BetterSEQTA+] Failed to initialize UI elements:", error); } setupEventListeners(); @@ -80,20 +79,18 @@ function createHomeButton(fragment: DocumentFragment, _: HTMLElement) { div.classList.add("titlebar"); container.append(div); - const NewButton = stringToHTML( - /* html */`
  • ` + 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; });