import { BasePlugin } from "@/plugins/core/settings"; import { booleanSetting, defineSettings, Setting, } from "@/plugins/core/settingsHelpers"; import { type Plugin } from "@/plugins/core/types"; import stringToHTML from "@/seqta/utils/stringToHTML"; import { waitForElm } from "@/seqta/utils/waitForElm"; import { clearStuck, getClassByPattern, initStorage, injectWeightingsTab, letterToNumber, parseAssessments, processAssessments, } from "./utils.ts"; interface weightingsStorage { weightings: Record; assessments: Record; weightingOverrides: Record; } const settings = defineSettings({ lettergrade: booleanSetting({ default: false, title: "Letter Grades", description: "Display the average as a letter instead of a percentage", }), }); class AssessmentsAveragePluginClass extends BasePlugin { @Setting(settings.lettergrade) lettergrade!: boolean; } const instance = new AssessmentsAveragePluginClass(); let overrideListenerController: AbortController | null = null; const assessmentsAveragePlugin: Plugin = { id: "assessments-average", name: "Assessment Averages", description: "Adds an average grade to the Assessments page", version: "1.0.0", disableToggle: true, settings: instance.settings, run: async (api) => { await initStorage(api); clearStuck(api); api.seqta.onMount(".assessmentsWrapper", async () => { await waitForElm( "#main > .assessmentsWrapper .assessments [class*='AssessmentItem__AssessmentItem___']", true, 10, 1000, ); await parseAssessments(api); await renderSubjectAverage(api); overrideListenerController?.abort(); overrideListenerController = new AbortController(); document.addEventListener( "betterseqta:overrideChanged", () => renderSubjectAverage(api), { signal: overrideListenerController.signal }, ); const wrapper = document.querySelector(".assessmentsWrapper"); if (wrapper) { const observer = new MutationObserver(() => { applySubjectColourToOverallResult(); }); observer.observe(wrapper, { childList: true, subtree: true }); setTimeout(() => observer.disconnect(), 10000); } }); api.seqta.onMount("[class*='SelectedAssessment__']", () => { injectWeightingsTab(api); }); }, }; let renderInFlight = false; async function renderSubjectAverage(api: any) { if (renderInFlight) return; renderInFlight = true; try { const assessmentsList = document.querySelector( "#main > .assessmentsWrapper .assessments [class*='AssessmentList__items___']", ); if (!assessmentsList) return; // Remove existing subject average before re-rendering Array.from( assessmentsList.querySelectorAll(`[class*='AssessmentItem__title___']`), ) .find((el) => el.textContent === "Subject Average") ?.closest("[class*='AssessmentItem__AssessmentItem___']") ?.remove(); const sampleAssessmentItem = document.querySelector( "[class*='AssessmentItem__AssessmentItem___']", ); if (!sampleAssessmentItem) return; const assessmentItemClass = Array.from(sampleAssessmentItem.classList).find((c) => c.startsWith("AssessmentItem__AssessmentItem___"), ) || ""; const metaContainerClass = getClassByPattern( sampleAssessmentItem, "AssessmentItem__metaContainer___", ); const metaClass = getClassByPattern( sampleAssessmentItem, "AssessmentItem__meta___", ); const simpleResultClass = getClassByPattern( sampleAssessmentItem, "AssessmentItem__simpleResult___", ); const titleClass = getClassByPattern( sampleAssessmentItem, "AssessmentItem__title___", ); const assessmentItems = Array.from( assessmentsList.querySelectorAll( `[class*='AssessmentItem__AssessmentItem___']`, ), ).filter( (item) => !item .querySelector(`[class*='AssessmentItem__title___']`) ?.textContent?.includes("Subject Average"), ); const { weightedTotal, totalWeight, hasInaccurateWeighting, count } = await processAssessments(api, assessmentItems); if (!count || totalWeight === 0) return; const thermoscoreElement = document.querySelector( "[class*='Thermoscore__Thermoscore___']", ); if (!thermoscoreElement) return; const thermoscoreClass = Array.from(thermoscoreElement.classList).find((c) => c.startsWith("Thermoscore__Thermoscore___"), ) || ""; const fillClass = getClassByPattern( thermoscoreElement, "Thermoscore__fill___", ); const textClass = getClassByPattern( thermoscoreElement, "Thermoscore__text___", ); const avg = weightedTotal / totalWeight; const rounded = Math.ceil(avg / 5) * 5; const numberToLetter = Object.entries(letterToNumber).reduce( (acc, [k, v]) => { acc[v] = k; return acc; }, {} as Record, ); const letterAvg = numberToLetter[rounded] ?? "N/A"; const display = api.settings.lettergrade ? letterAvg : `${avg.toFixed(2)}%`; let warningHTML = ""; if (hasInaccurateWeighting) { warningHTML = /* html */ `
⚠ Some weightings unavailable
`; } assessmentsList.insertBefore( stringToHTML(/* html */ `
Subject Average
${warningHTML}
${display}
`).firstChild!, assessmentsList.firstChild, ); applySubjectColourToOverallResult(); } finally { renderInFlight = false; } } function applySubjectColourToOverallResult() { const selectedAssessmentItem = document.querySelector( "[class*='AssessmentItem__AssessmentItem___'][class*='selected___']", ) || document.querySelector( "[class*='Collapsible__content___'] [class*='AssessmentItem__AssessmentItem___']", ); const assessmentThermoscore = selectedAssessmentItem?.querySelector( "[class*='Thermoscore__Thermoscore___']", ) as HTMLElement | null; const overallResult = document.querySelector( "[class*='OverallResult__OverallResult___']", ) as HTMLElement | null; const assessableCriterionHeaders = document.querySelectorAll( "[class*='AssessableCriterion__header___']", ); if (assessmentThermoscore && (overallResult || assessableCriterionHeaders.length > 0)) { const accentColour = getComputedStyle(assessmentThermoscore).getPropertyValue("--assessment-accent-colour").trim() || getComputedStyle(assessmentThermoscore).getPropertyValue("--fill-colour").trim() || getComputedStyle(assessmentThermoscore.closest("[class*='Collapsible__Collapsible___']") || assessmentThermoscore).getPropertyValue("--assessment-accent-colour").trim() || getComputedStyle(assessmentThermoscore.closest("[class*='Collapsible__Collapsible___']") || assessmentThermoscore).getPropertyValue("--item-colour").trim(); if (accentColour) { overallResult?.style.setProperty("--assessment-accent-colour", accentColour); overallResult?.style.setProperty("--fill-colour", accentColour); assessableCriterionHeaders.forEach((el) => { (el as HTMLElement).style.setProperty("--assessment-accent-colour", accentColour); (el as HTMLElement).style.setProperty("--fill-colour", accentColour); }); } } } export default assessmentsAveragePlugin;