diff --git a/src/interface/pages/settings/general.svelte b/src/interface/pages/settings/general.svelte index 1360ebc9..ef02462e 100644 --- a/src/interface/pages/settings/general.svelte +++ b/src/interface/pages/settings/general.svelte @@ -195,26 +195,6 @@ onChange: (isOn: boolean) => settingsState.animations = isOn } }, - { - title: "Assessment Average", - description: "Shows your subject average for assessments.", - id: 8, - Component: Switch, - props: { - state: $settingsState.assessmentsAverage, - onChange: (isOn: boolean) => settingsState.assessmentsAverage = isOn - } - }, - { - title: "Letter Grade Averages", - description: "Shows the letter grade instead of the percentage in subject averages.", - id: 8, - Component: Switch, - props: { - state: $settingsState.lettergrade, - onChange: (isOn: boolean) => settingsState.lettergrade = isOn - } - }, { title: "12 Hour Time", description: "Prefer 12 hour time format for SEQTA", diff --git a/src/plugins/built-in/animated-background/index.ts b/src/plugins/built-in/animatedBackground/index.ts similarity index 100% rename from src/plugins/built-in/animated-background/index.ts rename to src/plugins/built-in/animatedBackground/index.ts diff --git a/src/plugins/built-in/animated-background/styles.css b/src/plugins/built-in/animatedBackground/styles.css similarity index 100% rename from src/plugins/built-in/animated-background/styles.css rename to src/plugins/built-in/animatedBackground/styles.css diff --git a/src/plugins/built-in/animated-background/utils/CreateBackground.ts b/src/plugins/built-in/animatedBackground/utils/CreateBackground.ts similarity index 100% rename from src/plugins/built-in/animated-background/utils/CreateBackground.ts rename to src/plugins/built-in/animatedBackground/utils/CreateBackground.ts diff --git a/src/plugins/built-in/animated-background/utils/RemoveBackground.ts b/src/plugins/built-in/animatedBackground/utils/RemoveBackground.ts similarity index 100% rename from src/plugins/built-in/animated-background/utils/RemoveBackground.ts rename to src/plugins/built-in/animatedBackground/utils/RemoveBackground.ts diff --git a/src/plugins/built-in/assessmentsAverage/index.ts b/src/plugins/built-in/assessmentsAverage/index.ts new file mode 100644 index 00000000..0d8dc4ba --- /dev/null +++ b/src/plugins/built-in/assessmentsAverage/index.ts @@ -0,0 +1,115 @@ +import { BasePlugin } from "@/plugins/core/settings"; +import { defineSettings, booleanSetting, Setting } from "@/plugins/core/settingsHelpers"; +import { type Plugin } from "@/plugins/core/types"; +import stringToHTML from "@/seqta/utils/stringToHTML"; +import { waitForElm } from "@/seqta/utils/waitForElm"; + +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(); + +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) => { + api.seqta.onMount(".assessmentsWrapper", async () => { + await waitForElm( + "#main > .assessmentsWrapper .assessments .AssessmentItem__AssessmentItem___2EZ95", + true, + 10, + 1000 + ) + + const assessmentsList = document.querySelector("#main > .assessmentsWrapper .assessments .AssessmentList__items___3LcmQ"); + if (!assessmentsList) return; + + const gradeElements = document.querySelectorAll(".Thermoscore__text___1NdvB"); + 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, + }; + + 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; + } + + 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 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)}%`; + + // Prevent duplicate + const existing = assessmentsList.querySelector(".AssessmentItem__title___2bELn"); + if (existing?.textContent === "Subject Average") return; + + const averageElement = stringToHTML(/* html */ ` +
+
+
+
+
Subject Average
+
+
+
+
+
+
${display}
+
+
+
+ `).firstChild; + + assessmentsList.insertBefore(averageElement!, assessmentsList.firstChild); + }); + } +}; + +export default assessmentsAveragePlugin; diff --git a/src/plugins/core/createAPI.ts b/src/plugins/core/createAPI.ts index ca5fd467..383c840f 100644 --- a/src/plugins/core/createAPI.ts +++ b/src/plugins/core/createAPI.ts @@ -42,58 +42,9 @@ function createSEQTAAPI(): SEQTAAPI { function createSettingsAPI(plugin: Plugin): SettingsAPI & { loaded: Promise } { const storageKey = `plugin.${plugin.id}.settings`; const listeners = new Map void>>(); - let settings: { [K in keyof T]: SettingValue }; - const storageListeners = new Set<(changes: { [key: string]: any }, area: string) => void>(); - // Initialize settings with defaults - const defaultSettings = {} as { [K in keyof T]: SettingValue }; - for (const key in plugin.settings) { - defaultSettings[key] = plugin.settings[key].default as SettingValue; - } - settings = defaultSettings; - - - // Create a promise that resolves when settings are loaded - const loaded = (async () => { - try { - const stored = await browser.storage.local.get(storageKey); - if (stored[storageKey]) { - Object.entries(stored[storageKey]).forEach(([key, value]) => { - if (key in settings) { - settings[key as keyof T] = value as any; - // Notify any listeners that might have been registered already - listeners.get(key as keyof T)?.forEach(callback => callback(value)); - } - }); - } - } catch (error) { - console.error(`[BetterSEQTA+] Error loading settings for plugin ${plugin.id}:`, error); - } - })(); - - // Listen for storage changes - const handleStorageChange = (changes: { [key: string]: any }, area: string) => { - if (area === 'local' && changes[storageKey]) { - const newValue = changes[storageKey].newValue; - if (newValue) { - // Update settings and notify listeners - Object.entries(newValue).forEach(([key, value]) => { - settings[key as keyof T] = value as any; - listeners.get(key as keyof T)?.forEach(callback => callback(value)); - }); - } - } - }; - browser.storage.onChanged.addListener(handleStorageChange); - storageListeners.add(handleStorageChange); - - const baseSettings = {} as { [K in keyof T]: SettingValue }; - for (const key in plugin.settings) { - baseSettings[key] = plugin.settings[key].default as SettingValue; - } - - const settingsWithMeta = { - ...baseSettings, + // Initialize with default values + const settingsWithMeta: any = { onChange: (key: K, callback: (value: SettingValue) => void) => { if (!listeners.has(key)) { listeners.set(key, new Set()); @@ -108,24 +59,72 @@ function createSettingsAPI(plugin: Plugin): Setting offChange: (key: K, callback: (value: SettingValue) => void) => { listeners.get(key)?.delete(callback); }, - loaded + loaded: Promise.resolve() // will be replaced below }; + // Fill with defaults first + for (const key in plugin.settings) { + settingsWithMeta[key] = plugin.settings[key].default; + } + + // Load stored settings and override defaults + const loaded = (async () => { + try { + const stored = await browser.storage.local.get(storageKey); + const storedSettings = stored[storageKey] as Partial>; + if (storedSettings) { + for (const key in storedSettings) { + if (key in settingsWithMeta) { + settingsWithMeta[key] = storedSettings[key]; + listeners.get(key as keyof T)?.forEach(cb => cb(storedSettings[key])); + } + } + } + } catch (error) { + console.error(`[BetterSEQTA+] Error loading settings for plugin ${plugin.id}:`, error); + } + })(); + + settingsWithMeta.loaded = loaded; + + // Listen for storage changes and update settingsWithMeta + const handleStorageChange = (changes: { [key: string]: browser.Storage.StorageChange }, area: string) => { + if (area !== 'local' || !(storageKey in changes)) return; + + const newValue = changes[storageKey].newValue as Partial> | undefined; + if (!newValue) return; + + for (const key in newValue) { + const typedKey = key as keyof T; + settingsWithMeta[typedKey] = newValue[typedKey]; + listeners.get(typedKey)?.forEach(cb => cb(newValue[typedKey])); + } + }; + + browser.storage.onChanged.addListener(handleStorageChange); + const proxy = new Proxy(settingsWithMeta, { get(target, prop) { - return target[prop as keyof typeof target]; + return target[prop]; }, set(target, prop, value) { - if (prop === 'onChange' || prop === 'offChange' || prop === 'loaded') return false; + if (['onChange', 'offChange', 'loaded'].includes(prop as string)) return false; - target[prop as keyof T] = value; - browser.storage.local.set({ [storageKey]: baseSettings }); // Only store base settings - listeners.get(prop as keyof T)?.forEach(callback => callback(value)); + target[prop] = value; + + // Reconstruct just the data keys for storage (excluding metadata methods) + const dataToStore: any = {}; + for (const key in plugin.settings) { + dataToStore[key] = target[key]; + } + + browser.storage.local.set({ [storageKey]: dataToStore }); + + listeners.get(prop as keyof T)?.forEach(cb => cb(value)); return true; } - }) as SettingsAPI; + }) as SettingsAPI & { loaded: Promise }; - return proxy; } diff --git a/src/plugins/index.ts b/src/plugins/index.ts index c38c3b90..7e7f99b9 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -4,8 +4,8 @@ import { PluginManager } from './core/manager'; import timetablePlugin from './built-in/timetable'; import notificationCollectorPlugin from './built-in/notificationCollector'; import themesPlugin from './built-in/themes'; -import animatedBackgroundPlugin from './built-in/animated-background'; - +import animatedBackgroundPlugin from './built-in/animatedBackground'; +import assessmentsAveragePlugin from './built-in/assessmentsAverage'; // Initialize plugin manager const pluginManager = PluginManager.getInstance(); @@ -14,6 +14,7 @@ pluginManager.registerPlugin(timetablePlugin); pluginManager.registerPlugin(notificationCollectorPlugin); pluginManager.registerPlugin(themesPlugin); pluginManager.registerPlugin(animatedBackgroundPlugin); +pluginManager.registerPlugin(assessmentsAveragePlugin); //pluginManager.registerPlugin(testPlugin); export { init as Monofile } from './monofile'; diff --git a/src/plugins/monofile.ts b/src/plugins/monofile.ts index 09fdcd3e..d5edc083 100644 --- a/src/plugins/monofile.ts +++ b/src/plugins/monofile.ts @@ -256,17 +256,6 @@ async function LoadPageElements(): Promise { handleNotices, ) - if (settingsState.assessmentsAverage) { - eventManager.register( - "assessmentsAdded", - { - elementType: "div", - className: "assessmentsWrapper", - }, - handleAssessments, - ) - } - RegisterClickListeners() await handleSublink(sublink) @@ -665,174 +654,4 @@ export function AppendElementsToDisabledPage() { } ` document.head.append(settingsStyle) -} - -/*async function CheckForMenuList() { - try { - await waitForElm("#menu > ul") - ObserveMenuItemPosition() - } catch (error) { - return - } -}*/ - -async function handleAssessments(node: Element): Promise { - if (!(node instanceof HTMLElement)) return - - // Wait for the assessments wrapper to be mounted - const assessmentsWrapper = await waitForElm( - "#main > .assessmentsWrapper .assessments .AssessmentItem__AssessmentItem___2EZ95", - true, - 50, - ) - if (!assessmentsWrapper) return - - // Grade conversion map for letter grades - const letterGradeMap: 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 to parse grade text into a number - function parseGrade(gradeText: string): number { - // Remove any whitespace - const trimmedGrade = gradeText.trim().toUpperCase() - // Check if it is a non-percent grade - if (trimmedGrade.includes("/")) { - const grade = trimmedGrade.split("/") - var a = grade[1] as unknown as number - var b = grade[0] as unknown as number - return (b / a) * 100 - } - // Check if it's a percentage - if (trimmedGrade.includes("%")) { - return parseFloat(trimmedGrade.replace("%", "")) || 0 - } - - // Check if it's a letter grade - if (Object.prototype.hasOwnProperty.call(letterGradeMap, trimmedGrade)) { - return letterGradeMap[trimmedGrade] - } - - return 0 - } - - // Function to calculate average of grades - function calculateAverageGrade(): number { - const gradeElements = document.querySelectorAll( - ".Thermoscore__text___1NdvB", - ) - let total = 0 - let count = 0 - - gradeElements.forEach((element) => { - const gradeText = element.textContent || "" - const grade = parseGrade(gradeText) - if (grade > 0) { - total += grade - count++ - } - }) - - return count > 0 ? total / count : 0 - } - - // Function to add the average assessment item - function addAverageAssessment() { - const numaverage = calculateAverageGrade() - if (numaverage === 0) return - - // Remove existing average section if it exists - const existingAverage = document.querySelector( - ".AssessmentItem__AssessmentItem___2EZ95:first-child", - ) - if ( - existingAverage?.querySelector(".AssessmentItem__title___2bELn") - ?.textContent === "Subject Average" - ) { - existingAverage.remove() - } - const preaverage = numaverage.toFixed(0) as unknown as number - const prepaverage = Math.ceil(preaverage / 5) * 5 - const NumberGradeMap: Record = { - 100: "A+", - 95: "A", - 90: "A-", - 85: "B+", - 80: "B", - 75: "B-", - 70: "C+", - 65: "C", - 60: "C-", - 55: "D+", - 50: "D", - 45: "D-", - 40: "E+", - 35: "E", - 30: "E-", - 0: "F", - } - var letteraverage = "N/A" - const check = Object.prototype.hasOwnProperty.call( - NumberGradeMap, - prepaverage, - ) - if (check) { - console.debug("[BetterSEQTA+ Debugger] Match found") - letteraverage = NumberGradeMap[prepaverage] - } else { - console.debug("[BetterSEQTA+ Debugger] No match found") - letteraverage = "N/A" - } - var average = "N/A" - if (settingsState.lettergrade) { - average = letteraverage - } else { - average = `${numaverage.toFixed(2)}%` - } - const averageElement = stringToHTML(/* html */ ` -
-
-
-
-
Subject Average
-
-
-
-
-
-
${average}
-
-
-
- `) - - // Insert at the beginning of the assessments list - const assessmentsList = document.querySelector( - ".assessments .AssessmentList__items___3LcmQ", - ) - if (assessmentsList && averageElement.firstChild) { - assessmentsList.insertBefore( - averageElement.firstChild, - assessmentsList.firstChild, - ) - } - } - - // Add the average assessment item - addAverageAssessment() -} +} \ No newline at end of file diff --git a/src/seqta/main.ts b/src/seqta/main.ts index 5921ed6b..647172bd 100644 --- a/src/seqta/main.ts +++ b/src/seqta/main.ts @@ -21,10 +21,6 @@ export async function main() { if (settingsState.onoff) { injectPageState() - if (typeof settingsState.assessmentsAverage == "undefined") { - settingsState.assessmentsAverage = true - } - // TEMP FIX for bug! -> this is a hack to get the injected.css file to have HMR in development mode as this import system is currently broken with crxjs if (import.meta.env.MODE === "development") { import("../css/injected.scss") diff --git a/src/seqta/utils/waitForElm.ts b/src/seqta/utils/waitForElm.ts index 2e15e2a8..46a3231e 100644 --- a/src/seqta/utils/waitForElm.ts +++ b/src/seqta/utils/waitForElm.ts @@ -5,14 +5,25 @@ export async function waitForElm( selector: string, usePolling: boolean = false, interval: number = 100, + maxIterations?: number ): Promise { if (usePolling) { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { + let iterations = 0; + if (maxIterations) { + iterations = 0; + } const checkForElement = () => { const element = document.querySelector(selector) if (element) { resolve(element) } else { + if (maxIterations) { + iterations++; + if (iterations >= maxIterations) { + reject(new Error("Element not found")); + } + } setTimeout(checkForElement, interval) } }