diff --git a/src/SEQTA.ts b/src/SEQTA.ts index 12115f38..41d9f0da 100644 --- a/src/SEQTA.ts +++ b/src/SEQTA.ts @@ -9,6 +9,7 @@ import browser from "webextension-polyfill"; import * as plugins from "@/plugins"; import { main } from "@/seqta/main"; import { delay } from "./seqta/utils/delay"; +import { initializeHideSensitiveToggle } from "@/seqta/utils/hideSensitiveToggle"; export let MenuOptionsOpen = false; @@ -70,6 +71,8 @@ async function init() { await plugins.initializePlugins(); } + initializeHideSensitiveToggle(); + console.info( "[BetterSEQTA+] Successfully initialised BetterSEQTA+, starting to load assets.", ); diff --git a/src/interface/pages/settings/general.svelte b/src/interface/pages/settings/general.svelte index 1273d3e5..91a8a600 100644 --- a/src/interface/pages/settings/general.svelte +++ b/src/interface/pages/settings/general.svelte @@ -10,7 +10,6 @@ import type { SettingsList } from "@/interface/types/SettingsProps" import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts" import PickerSwatch from "@/interface/components/PickerSwatch.svelte" - import hideSensitiveContent from "@/seqta/ui/dev/hideSensitiveContent" import { getAllPluginSettings } from "@/plugins" import type { BooleanSetting, StringSetting, NumberSetting, SelectSetting, ButtonSetting, HotkeySetting, ComponentSetting } from "@/plugins/core/types" @@ -322,9 +321,9 @@

Replace sensitive content with mock data

-
diff --git a/src/plugins/built-in/assessmentsOverview/api.ts b/src/plugins/built-in/assessmentsOverview/api.ts index 84fd349d..264a0805 100644 --- a/src/plugins/built-in/assessmentsOverview/api.ts +++ b/src/plugins/built-in/assessmentsOverview/api.ts @@ -9,6 +9,9 @@ interface PrefItem { value: string; } +import { settingsState } from "@/seqta/utils/listeners/SettingsState"; +import { getMockAssessmentsData } from "@/seqta/ui/dev/hideSensitiveContent"; + let cache: { time: number; data: any } | null = null; const CACHE_MS = 10 * 60 * 1000; const student = 69; @@ -102,6 +105,10 @@ async function loadSubmissions(student: number, assessments: any[]) { } export async function getAssessmentsData() { + if (settingsState.mockNotices) { + return getMockAssessmentsData(); + } + if (cache && Date.now() - cache.time < CACHE_MS) return cache.data; const [subjects, colors, upcoming] = await Promise.all([ loadSubjects(), diff --git a/src/seqta/ui/dev/hideSensitiveContent.ts b/src/seqta/ui/dev/hideSensitiveContent.ts index e3f6443a..4d7b6581 100644 --- a/src/seqta/ui/dev/hideSensitiveContent.ts +++ b/src/seqta/ui/dev/hideSensitiveContent.ts @@ -7,6 +7,21 @@ interface ContentConfig { [key: string]: ElementConfig; } +// Track processed elements to avoid re-randomizing +const processedElements = new WeakSet(); + +function debounce(func: Function, wait: number): Function { + let timeout: NodeJS.Timeout; + return function executedFunction(...args: any[]) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} + function getRandomElement(array: string[]): string { return array[Math.floor(Math.random() * array.length)]; } @@ -164,9 +179,32 @@ const contentConfig: ContentConfig = { }, }, forumTopics: { - selector: "#menu .sub ul li label", + selector: "#menu .sub ul li:not([data-colour]):not(.hasChildren) label", action: (element) => { - element.textContent = "Forum Topic Redacted"; + // Only redact if not in assessments section + const assessmentsSection = element.closest('[data-key="assessments"]'); + if (!assessmentsSection) { + element.textContent = "Forum Topic Redacted"; + } + }, + }, + assessmentSubjects: { + selector: '[data-key="assessments"] .sub ul li[data-colour] label', + action: (element) => { + element.textContent = getRandomElement(mockData.subjects); + }, + }, + assessmentYearGroups: { + selector: '[data-key="assessments"] .sub ul li.hasChildren:not([data-colour]) label', + action: (element) => { + const yearGroup = Math.floor(Math.random() * 5) + 8; // Years 8-12 + element.textContent = `Year ${yearGroup}`; + }, + }, + assessmentSubYearGroups: { + selector: '[data-key="assessments"] .sub .sub ul li[data-colour] label', + action: (element) => { + element.textContent = getRandomElement(mockData.subjects); }, }, courseNames: { @@ -541,11 +579,168 @@ export function getMockNotices() { }; } -export default function hideSensitiveContent() { +export function getMockAssessmentsData() { + const subjects = mockData.subjects.slice(0, 5).map((title, i) => ({ + code: `SUBJ${i + 1}`, + programme: i + 1, + metaclass: i + 1, + title, + })); + + const colors: Record = {}; + subjects.forEach((s) => { + colors[s.code] = `hsl(${Math.floor(Math.random() * 360)},70%,60%)`; + }); + + const statusTemplates = [ + // Marked with scores (70-90%) - goes to MARKS_RELEASED + { submitted: true, score: () => Math.floor(Math.random() * 21) + 70, dayOffset: () => Math.floor(Math.random() * -30) - 7 }, // Past due, marked with score + { submitted: true, score: () => Math.floor(Math.random() * 21) + 70, dayOffset: () => Math.floor(Math.random() * -14) - 1 }, // Recently marked with score + { submitted: true, score: () => Math.floor(Math.random() * 21) + 70, dayOffset: () => Math.floor(Math.random() * -7) }, // Very recently marked with score + + // Submitted but unmarked - goes to SUBMITTED + { submitted: true, score: null, dayOffset: () => Math.floor(Math.random() * -5) - 1 }, // Recently submitted, awaiting marking + { submitted: true, score: null, dayOffset: () => Math.floor(Math.random() * -3) }, // Very recently submitted, awaiting marking + { submitted: true, score: null, dayOffset: () => Math.floor(Math.random() * -2) }, // Just submitted, awaiting marking + + // Due soon (not submitted) - only a couple + { submitted: false, score: null, dayOffset: () => 0 }, // Due today + { submitted: false, score: null, dayOffset: () => Math.floor(Math.random() * 3) + 2 }, // Due in next few days + + // Due later (not submitted) - most assessments + { submitted: false, score: null, dayOffset: () => Math.floor(Math.random() * 7) + 8 }, // Due in 1-2 weeks + { submitted: false, score: null, dayOffset: () => Math.floor(Math.random() * 14) + 14 }, // Due in 2-4 weeks + { submitted: false, score: null, dayOffset: () => Math.floor(Math.random() * 21) + 21 }, // Due in 3-6 weeks + { submitted: false, score: null, dayOffset: () => Math.floor(Math.random() * 14) + 35 }, // Due in 5-7 weeks + + // Few overdue (not submitted) - less common + { submitted: false, score: null, dayOffset: () => Math.floor(Math.random() * -3) - 1 }, // Recently overdue + ]; + + const assessments = Array.from({ length: 12 }, (_, i) => { + const subj = subjects[i % subjects.length]; + const template = statusTemplates[i % statusTemplates.length]; + const due = new Date(); + due.setDate(due.getDate() + template.dayOffset()); + + const assessment: any = { + id: i + 1, + title: mockData.assessmentTitles[i % mockData.assessmentTitles.length], + code: subj.code, + programmeID: subj.programme, + metaclassID: subj.metaclass, + due: due.toISOString(), + submitted: template.submitted, + }; + + if (template.score && typeof template.score === 'function') { + assessment.percentage = template.score(); // This triggers MARKS_RELEASED + assessment.results = { + percentage: template.score() // This displays the thermometer + }; + } + + return assessment; + }); + + return { assessments, subjects, colors }; +} + +// Create a debounced processing function +const debouncedProcessElements = debounce(processNewElements, 1); + +function processNewElements() { Object.entries(contentConfig).forEach(([_, { selector, action }]) => { const elements = document.querySelectorAll(selector); elements.forEach((element: Element) => { - action(element); + // Only process elements that haven't been processed before + if (!processedElements.has(element)) { + action(element); + processedElements.add(element); + } }); }); } + +let observer: MutationObserver | null = null; +let intervalId: NodeJS.Timeout | null = null; + +export default function hideSensitiveContent() { + // Initial processing of existing elements + processNewElements(); + + // Set up MutationObserver if not already created + if (!observer) { + observer = new MutationObserver((mutations) => { + let shouldProcess = false; + + mutations.forEach((mutation) => { + // Check for both childList and subtree changes + if (mutation.type === 'childList') { + // Check added nodes + if (mutation.addedNodes.length > 0) { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE) { + const element = node as Element; + // Check if the added element or its children match any of our selectors + for (const config of Object.values(contentConfig)) { + if (element.matches?.(config.selector) || element.querySelector?.(config.selector)) { + shouldProcess = true; + break; + } + } + } + }); + } + + // Also trigger on large DOM replacements (like page navigation) + if (mutation.addedNodes.length > 5 || mutation.removedNodes.length > 5) { + shouldProcess = true; + } + } + + // Check for attribute changes that might affect our selectors + if (mutation.type === 'attributes') { + const target = mutation.target as Element; + for (const config of Object.values(contentConfig)) { + if (target.matches?.(config.selector)) { + shouldProcess = true; + break; + } + } + } + }); + + if (shouldProcess) { + debouncedProcessElements(); + } + }); + + // Start observing with more comprehensive options + observer.observe(document.documentElement, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['class', 'id'] // Watch for class/id changes that might affect our selectors + }); + } + + // Fallback: periodic check for new elements (especially useful for SPA navigation) + if (!intervalId) { + intervalId = setInterval(() => { + debouncedProcessElements(); + }, 500); // Check every 500ms as a fallback + } +} + +// Function to stop observing (useful for cleanup) +export function stopHidingSensitiveContent() { + if (observer) { + observer.disconnect(); + observer = null; + } + if (intervalId) { + clearInterval(intervalId); + intervalId = null; + } +} diff --git a/src/seqta/utils/hideSensitiveToggle.ts b/src/seqta/utils/hideSensitiveToggle.ts new file mode 100644 index 00000000..4da06dc2 --- /dev/null +++ b/src/seqta/utils/hideSensitiveToggle.ts @@ -0,0 +1,18 @@ +import { settingsState } from "./listeners/SettingsState"; +import hideSensitiveContent from "@/seqta/ui/dev/hideSensitiveContent"; + +function maybeHide() { + if (settingsState.hideSensitiveContent) { + hideSensitiveContent(); + } +} + +export function initializeHideSensitiveToggle() { + maybeHide(); + window.addEventListener("hashchange", maybeHide); + settingsState.register("hideSensitiveContent", (val) => { + if (val) { + maybeHide(); + } + }); +} diff --git a/src/types/storage.ts b/src/types/storage.ts index 74c7790d..a085567d 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -37,6 +37,7 @@ export interface SettingsState { originalDarkMode?: boolean; newsSource?: string; mockNotices?: boolean; + hideSensitiveContent?: boolean; // depreciated keys animatedbk: boolean;