// Third-party libraries import Color from 'color' import Sortable from 'sortablejs' import browser from 'webextension-polyfill' import { animate, spring, stagger } from 'motion' // Internal utilities and functions import { delay } from '@/seqta/utils/delay' import stringToHTML from '@/seqta/utils/stringToHTML' import { MessageHandler } from '@/seqta/utils/listeners/MessageListener' import { initializeSettingsState, settingsState } from '@/seqta/utils/listeners/SettingsState' import { StorageChangeHandler } from '@/seqta/utils/listeners/StorageChanges' import { eventManager } from '@/seqta/utils/listeners/EventManager' // UI and theme management import loading, { AppendLoadingSymbol } from '@/seqta/ui/Loading' import { enableCurrentTheme } from '@/seqta/ui/themes/enableCurrent' import { updateAllColors } from '@/seqta/ui/colors/Manager' import { SettingsResizer } from '@/seqta/ui/SettingsResizer' import { AddBetterSEQTAElements } from '@/seqta/ui/AddBetterSEQTAElements' // JSON content import MenuitemSVGKey from '@/seqta/content/MenuItemSVGKey.json' import ShortcutLinks from '@/seqta/content/links.json' // Icons and fonts import IconFamily from '@/resources/fonts/IconFamily.woff' import LogoLight from '@/resources/icons/betterseqta-light-icon.png' import LogoLightOutline from '@/resources/icons/betterseqta-light-outline.png' import icon48 from '@/resources/icons/icon-48.png?base64' import assessmentsicon from '@/seqta/icons/assessmentsIcon' import coursesicon from '@/seqta/icons/coursesIcon' // Stylesheets import iframeCSS from '@/css/iframe.scss?raw' import injectedCSS from '@/css/injected.scss?inline' import documentLoadCSS from '@/css/documentload.scss?inline' import renderSvelte from '@/interface/main' import Settings from '@/interface/pages/settings.svelte' import { settingsPopup } from './interface/hooks/SettingsPopup' import { migrateBackgrounds } from './seqta/utils/migrateBackgrounds' let SettingsClicked = false export let MenuOptionsOpen = false let currentSelectedDate = new Date() let LessonInterval: any var IsSEQTAPage = false let hasSEQTAText = false // This check is placed outside of the document load event due to issues with EP (https://github.com/BetterSEQTA/BetterSEQTA-Plus/issues/84) if (document.childNodes[1]) { hasSEQTAText = document.childNodes[1].textContent?.includes('Copyright (c) SEQTA Software') ?? false init() } async function init() { CheckForMenuList() const hasSEQTATitle = document.title.includes('SEQTA Learn') if (hasSEQTAText && hasSEQTATitle && !IsSEQTAPage) { IsSEQTAPage = true console.info('[BetterSEQTA+] Verified SEQTA Page') const documentLoadStyle = document.createElement('style') documentLoadStyle.textContent = documentLoadCSS document.head.appendChild(documentLoadStyle) const icon = document.querySelector('link[rel*="icon"]')! as HTMLLinkElement icon.href = icon48 try { // wait until settingsState has been loaded from storage await initializeSettingsState(); if (settingsState.onoff) { enableCurrentTheme() // 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') } else { const injectedStyle = document.createElement('style') injectedStyle.textContent = injectedCSS document.head.appendChild(injectedStyle) } } console.info('[BetterSEQTA+] Successfully initalised BetterSEQTA+, starting to load assets.') main() } catch (error: any) { console.error(error) } } } function SetDisplayNone(ElementName: string) { return `li[data-key=${ElementName}]{display:var(--menuHidden) !important; transition: 1s;}` } export function enableAnimatedBackground() { if (settingsState.animatedbk) { CreateBackground() } else { RemoveBackground() document.getElementById('container')!.style.background = 'var(--background-secondary)' } } async function HideMenuItems(): Promise { try { let stylesheetInnerText: string = '' for (const [menuItem, { toggle }] of Object.entries(settingsState.menuitems)) { if (!toggle) { stylesheetInnerText += SetDisplayNone(menuItem) console.info(`[BetterSEQTA+] Hiding ${menuItem} menu item`) } } const menuItemStyle: HTMLStyleElement = document.createElement('style') menuItemStyle.innerText = stylesheetInnerText document.head.appendChild(menuItemStyle) } catch (error) { console.error("[BetterSEQTA+] An error occurred:", error) } } export function OpenWhatsNewPopup() { const background = document.createElement('div') background.id = 'whatsnewbk' background.classList.add('whatsnewBackground') const container = document.createElement('div') container.classList.add('whatsnewContainer') var header: any = stringToHTML( /* html */ `

What's New

BetterSEQTA+ V${browser.runtime.getManifest().version}

` ).firstChild let imagecont = document.createElement('div') imagecont.classList.add('whatsnewImgContainer') let video = document.createElement('video') let source = document.createElement('source') // Perhaps we host this on a server and then grab it instead of having it locally? source.setAttribute('src', 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Plus/main/src/resources/update-video.mp4') video.autoplay = true video.muted = true video.loop = true video.appendChild(source) video.classList.add('whatsnewImg') imagecont.appendChild(video) let textcontainer = document.createElement('div') textcontainer.classList.add('whatsnewTextContainer') let text = stringToHTML( /* html */ `

3.4.0 - Major Performance Update

  • Completely rebuilt the extension popup using Svelte for dramatically improved performance
  • Added a brand new background store with search functionality and downloadable backgrounds
  • Significant code cleanup and optimization across the extension
  • Improved overall responsiveness and load times
  • Smoother animations and improved scrolling
  • Fixed Firefox compatibility issues
  • Other minor bug fixes and under the hood improvements
  • 3.3.1 - Hot Fix

  • Fixed assessments not loading when no notices are available
  • 3.3.0 - Overhauled Theming System

  • Added a theme store!
  • Added the new theme creator!
  • Fixed Notices not working on home page
  • Fixed dark/light button labels inverted
  • Switched to GitHub for hosting the update video
  • Fixed an issue where the settings menu wouldn't change theme
  • Fixed custom shortcuts not allowing ports to be used
  • Fixed occasional flashing when using animations
  • Fixed loading of the tab icon
  • Made animations toggle apply to settings
  • Small styling improvements
  • Other minor bug fixes
  • 3.2.7 - Minor Improvements

  • Improved performance!
  • Fixed a bug where the icon wasn't showing up
  • 3.2.6 - Bug fixes and performance improvements

  • Improved contrast for notifications
  • Added 12-hour time format toggle
  • Using external update video to ensure smaller package size
  • Refactored underlying code to improve performance
  • Removed old theme system *revamp coming soon*
  • Improved notices contrast
  • Remove Telemetry completely - as we weren't using it too much
  • Added Error handling to settings interface
  • Fixed HTML message editor cursor becoming misaligned
  • Enabled spellcheck inside of direct messages
  • Fixed timetable dates being misaligned
  • Other minor bug fixes and under the hood improvements
  • 3.2.5 - More Bug Fixes

  • New direct message scroll animations
  • Added error message for brave browser shields breaking backgrounds
  • Fixed homepage assessment tooltips being cut off
  • Improved direct message styling
  • Made settings panel auto size to height of screen
  • Fixed timetable dates not visible
  • Other minor bug fixes
  • 3.2.4 - Bug Fixes

  • Added an open changelog button to settings
  • Fixed a memory overflow bug with Education Perfect
  • Fixed a bug where the background wouldn't change instantly
  • Fixed news feed not loading
  • Fixed home items duplicating
  • Fixed Upcoming assessments not showing
  • 3.2.2 - Minor Improvements

  • Added Settings open-close animation
  • Minor Bug Fixes
  • 3.2.0 - Custom Themes

  • Added transparency (blur) effects
  • Added custom themes
  • Added colour picker history
  • Heaps of bug fixes
  • 3.1.3 - Custom Backgrounds

  • Added custom backgrounds with support for images and videos
  • Overhauled topbar
  • New animated hamburger icon
  • Minor bug fixes
  • 3.1.2 - New settings menu!

  • Overhauled the settings menu
  • Added custom gradients
  • Added HEAPS of animations
  • Fixed a bug where shortcuts don't show up
  • Other minor bugs fixed
  • 3.1.1 - Minor Bug fixes

  • Fixed assessments overlapping
  • Fixed houses not displaying if they aren't a specific color
  • Fixed Chrome Webstore Link
  • 3.1.0 - Design Improvements

  • Minor UI improvements
  • Added Animation Speed Slider
  • Animation now enables and disables without reloading SEQTA
  • Changed logo
  • 3.0.0 - BetterSEQTA+ *Complete Overhaul*

  • Redesigned appearance
  • Upgraded to manifest V3 (longer support)
  • Fixed transitional glitches
  • Under the hood improvements
  • Fixed News Feed
  • 2.0.7 - Added support to other domains + Minor bug fixes

  • Fixed BetterSEQTA+ not loading on some pages
  • Fixed text colour of notices being unreadable
  • Fixed pages not reloading when saving changes
  • 2.0.2 - Minor bug fixes

  • Fixed indicator for current lesson
  • Fixed text colour for DM messages list in Light mode
  • Fixed user info text colour
  • Sleek New Layout

  • Updated with a new font and presentation, BetterSEQTA+ has never looked better.
  • New Updated Sidebar

  • Condensed appearance with new updated icons.
  • Independent Light Mode and Dark Mode

  • Dark mode and Light mode are now available to pick alongside your chosen Theme Colour. Your Theme Colour will now become an accent colour for the page. Light/Dark mode can be toggled with the new button, found in the top-right of the menu bar.
  • Create Custom Shortcuts

  • Found in the BetterSEQTA+ Settings menu, custom shortcuts can now be created with a name and URL of your choice.
  • `, ).firstChild let footer = stringToHTML( /* html */ `
    Report bugs and feedback:
    `).firstChild let exitbutton = document.createElement('div') exitbutton.id = 'whatsnewclosebutton' container.append(header) container.append(imagecont) container.append(textcontainer) container.append(text as ChildNode) container.append(footer as ChildNode) container.append(exitbutton) background.append(container) document.getElementById('container')!.append(background) let bkelement = document.getElementById('whatsnewbk') let popup = document.getElementsByClassName('whatsnewContainer')[0] if (settingsState.animations) { animate( [popup, bkelement as HTMLElement], { scale: [0, 1], opacity: [0, 1] }, { easing: spring({ stiffness: 220, damping: 18 }) } ) animate( '.whatsnewTextContainer *', { opacity: [0, 1], y: [10, 0] }, { delay: stagger(0.05, { start: 0.1 }), duration: 0.5, easing: [.22, .03, .26, 1] } ) } delete settingsState.justupdated bkelement!.addEventListener('click', function (event) { // Check if the click event originated from the element itself and not any of its children if (event.target === bkelement) { DeleteWhatsNew() } }); var closeelement = document.getElementById('whatsnewclosebutton') closeelement!.addEventListener('click', function () { DeleteWhatsNew() }) } export function OpenAboutPage() { const background = document.createElement('div') background.id = 'whatsnewbk' background.classList.add('whatsnewBackground') const container = document.createElement('div') container.classList.add('whatsnewContainer') var header: any = stringToHTML( /* html */ `

    About

    BetterSEQTA+ V${browser.runtime.getManifest().version}

    ` ).firstChild let text = stringToHTML( /* html */ `

    BetterSEQTA+ is a fork of BetterSEQTA which was originally developed by Nulkem, which was discontinued. BetterSEQTA+ continued development of BetterSEQTA, while incorporating a plethora of features.

    We are currently working on fixing bugs and adding good features. If you want to make a feature request or report a bug, you can do so on GitHub (find icon below).

    Credits

    Nulkem created the original extension, was ported to Manifest V3 by MEGA-Dawg68, and is under active development by Crazypersonalph and SethBurkart123.

    `, ).firstChild let footer = stringToHTML( /* html */ `
    Report bugs and feedback:
    `).firstChild let exitbutton = document.createElement('div') exitbutton.id = 'whatsnewclosebutton' container.append(header) container.append(text as ChildNode) container.append(footer as ChildNode) container.append(exitbutton) background.append(container) document.getElementById('container')!.append(background) let bkelement = document.getElementById('whatsnewbk') let popup = document.getElementsByClassName('whatsnewContainer')[0] if (settingsState.animations) { animate( [popup, bkelement as HTMLElement], { scale: [0, 1], opacity: [0, 1] }, { easing: spring({ stiffness: 220, damping: 18 }) } ) animate( '.whatsnewTextContainer *', { opacity: [0, 1], y: [10, 0] }, { delay: stagger(0.05, { start: 0.1 }), duration: 0.5, easing: [.22, .03, .26, 1] } ) } delete settingsState.justupdated bkelement!.addEventListener('click', function (event) { // Check if the click event originated from the element itself and not any of its children if (event.target === bkelement) { DeleteWhatsNew() } }); var closeelement = document.getElementById('whatsnewclosebutton') closeelement!.addEventListener('click', function () { DeleteWhatsNew() }) } export async function finishLoad() { try { document.querySelector('.legacy-root')?.classList.remove('hidden'); const loadingbk = document.getElementById('loading'); loadingbk?.classList.add('closeLoading'); await delay(501); loadingbk?.remove(); } catch (err) { console.error("Error during loading cleanup:", err); } if (settingsState.justupdated && !document.getElementById('whatsnewbk')) { OpenWhatsNewPopup(); /* Background Migration script */ } migrateBackgrounds(); } async function DeleteWhatsNew() { const bkelement = document.getElementById('whatsnewbk') const popup = document.getElementsByClassName('whatsnewContainer')[0] if (!settingsState.animations) { bkelement?.remove() return } animate( [popup, bkelement!], { opacity: [1, 0], scale: [1, 0] }, { easing: [.22, .03, .26, 1] } ).finished.then(() => { bkelement?.remove() }); } export function CreateBackground() { var bkCheck = document.getElementsByClassName('bg') if (bkCheck.length !== 0) { return } // Creating and inserting 3 divs containing the background applied to the pages var bklocation = document.getElementById('container') var menu = document.getElementById('menu') var bk = document.createElement('div') bk.classList.add('bg') bklocation!.insertBefore(bk, menu) var bk2 = document.createElement('div') bk2.classList.add('bg') bk2.classList.add('bg2') bklocation!.insertBefore(bk2, menu) var bk3 = document.createElement('div') bk3.classList.add('bg') bk3.classList.add('bg3') bklocation!.insertBefore(bk3, menu) } export function RemoveBackground() { var bk = document.getElementsByClassName('bg') var bk2 = document.getElementsByClassName('bg2') var bk3 = document.getElementsByClassName('bg3') if (bk.length == 0 || bk2.length == 0 || bk3.length == 0) return bk[0].remove() bk2[0].remove() bk3[0].remove() } export async function waitForElm(selector: string, usePolling: boolean = false, interval: number = 100): Promise { if (usePolling) { return new Promise((resolve) => { const checkForElement = () => { const element = document.querySelector(selector); if (element) { resolve(element); } else { setTimeout(checkForElement, interval); } }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', checkForElement); } else { checkForElement(); } }); } else { return new Promise((resolve) => { const registerObserver = () => { const { unregister } = eventManager.register(`${selector}`, { customCheck: (element) => element.matches(selector) }, (element) => { resolve(element); unregister(); // Remove the listener once the element is found }); return unregister; }; let unregister = null; if (document.readyState === 'loading') { // DOM is still loading, wait for it to be ready document.addEventListener('DOMContentLoaded', () => { unregister = registerObserver(); }); } else { unregister = registerObserver(); } const querySelector = () => document.querySelector(selector); const element = querySelector(); if (element) { if (unregister) unregister(); resolve(element); return; } }); } } export function GetCSSElement(file: string) { const cssFile = browser.runtime.getURL(file) const fileref = document.createElement('link') fileref.setAttribute('rel', 'stylesheet') fileref.setAttribute('type', 'text/css') fileref.setAttribute('href', cssFile) return fileref } function removeThemeTagsFromNotices () { // Grabs an array of the notice iFrames const userHTMLArray = document.getElementsByClassName('userHTML') // Iterates through the array, applying the iFrame css for (const item of userHTMLArray) { // Grabs the HTML of the body tag const item1 = item as HTMLIFrameElement const body = item1.contentWindow!.document.querySelectorAll('body')[0] if (body) { // Replaces the theme tag with nothing const bodyText = body.innerHTML body.innerHTML = bodyText.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, '').replace(/ +/, ' ') } } } async function updateIframesWithDarkMode(): Promise { const cssLink = document.createElement('style'); cssLink.classList.add('iframecss'); const cssContent = document.createTextNode(iframeCSS); cssLink.appendChild(cssContent); eventManager.register('iframeAdded', { elementType: 'iframe', customCheck: (element: Element) => !element.classList.contains('iframecss'), }, (element) => { const iframe = element as HTMLIFrameElement; try { applyDarkModeToIframe(iframe, cssLink); if (element.classList.contains('cke_wysiwyg_frame')) { (async () => { await delay(100); iframe.contentDocument?.body.setAttribute('spellcheck', 'true'); })(); } } catch (error) { console.error('Error applying dark mode:', error); } }); } function applyDarkModeToIframe(iframe: HTMLIFrameElement, cssLink: HTMLStyleElement): void { const iframeDocument = iframe.contentDocument; if (!iframeDocument) return; iframe.onload = () => { applyDarkModeToIframe(iframe, cssLink); }; if (settingsState.DarkMode) { iframeDocument.documentElement.classList.add('dark') } const head = iframeDocument.head; if (head && !head.innerHTML.includes('iframecss')) { head.innerHTML += cssLink.outerHTML; } } function SortMessagePageItems(messagesParentElement: any) { let filterbutton = document.createElement('div') filterbutton.classList.add('messages-filterbutton') filterbutton.innerText = 'Filter' let header = document.getElementsByClassName( 'MessageList__MessageList___3DxoC', )[0].firstChild as HTMLElement header.append(filterbutton) messagesParentElement } async function LoadPageElements(): Promise { await AddBetterSEQTAElements(); const sublink: string | undefined = window.location.href.split('/')[4]; eventManager.register('messagesAdded', { elementType: 'div', className: 'messages', }, handleMessages); eventManager.register('noticesAdded', { elementType: 'div', className: 'notices', }, CheckNoticeTextColour); eventManager.register('dashboardAdded', { elementType: 'div', className: 'dashboard', }, handleDashboard); eventManager.register('documentsAdded', { elementType: 'div', className: 'documents', }, handleDocuments); eventManager.register('reportsAdded', { elementType: 'div', className: 'reports', }, handleReports); eventManager.register('timetableAdded', { elementType: 'div', className: 'timetablepage', }, handleTimetable); await handleSublink(sublink); } async function handleSublink(sublink: string | undefined): Promise { switch (sublink) { case 'news': await handleNewsPage(); break; case undefined: window.location.replace(`${location.origin}/#?page=/${settingsState.defaultPage}`); if (settingsState.defaultPage === 'home') loadHomePage() if (settingsState.defaultPage === 'timetable') handleTimetable() if (settingsState.defaultPage === 'documents') handleDocuments(document.querySelector('.documents')!) if (settingsState.defaultPage === 'reports') handleReports(document.querySelector('.reports')!) if (settingsState.defaultPage === 'messages') handleMessages(document.querySelector('.messages')!) finishLoad(); break; case 'home': window.location.replace(`${location.origin}/#?page=/home`); console.info('[BetterSEQTA+] Started Init') if (settingsState.onoff) loadHomePage() finishLoad(); break; case 'timetable': handleTimetable() await handleDefault() break; case 'documents': handleDocuments(document.querySelector('.documents')!) await handleDefault() break; case 'reports': handleReports(document.querySelector('.reports')!) await handleDefault() break; case 'messages': handleMessages(document.querySelector('.messages')!) await handleDefault() break; case 'dashboard': handleDashboard(document.querySelector('.dashboard')!) await handleDefault() break; default: await handleDefault() break; } } async function handleTimetable(): Promise { await waitForElm('.time', true, 10) if (settingsState.timeFormat == '12') { const times = document.querySelectorAll('.timetablepage .times .time') for (const time of times) { if (!time.textContent) continue time.textContent = convertTo12HourFormat(time.textContent, true) } } } async function handleNewsPage(): Promise { console.info('[BetterSEQTA+] Started Init'); if (settingsState.onoff) { SendNewsPage(); if (settingsState.notificationcollector) { enableNotificationCollector(); } finishLoad(); } } async function handleDefault(): Promise { finishLoad(); if (settingsState.notificationcollector) { enableNotificationCollector(); } } async function handleMessages(node: Element): Promise { if (!(node instanceof HTMLElement)) return; const element = document.getElementById('title')!.firstChild as HTMLElement; element.innerText = 'Direct Messages'; document.title = 'Direct Messages ― SEQTA Learn'; SortMessagePageItems(node); if (!settingsState.animations) return; // Hides messages on page load const style = document.createElement('style') style.classList.add('messageHider') style.innerHTML = '[data-message]{opacity: 0 !important;}' document.head.append(style) await waitForElm('[data-message]', true, 10); const messages = Array.from(document.querySelectorAll('[data-message]')).slice(0, 35); animate( messages, { opacity: [0, 1], y: [10, 0] }, { delay: stagger(0.05), duration: 0.5, easing: [.22, .03, .26, 1] } ); document.head.querySelector('style.messageHider')?.remove() } async function handleDashboard(node: Element): Promise { if (!(node instanceof HTMLElement)) return; if (!settingsState.animations) return; const style = document.createElement('style') style.classList.add('dashboardHider') style.innerHTML = '.dashboard{opacity: 0 !important;}' document.head.append(style) await waitForElm('.dashlet', true, 10); animate( '.dashboard > *', { opacity: [0, 1], y: [10, 0] }, { delay: stagger(0.1), duration: 0.5, easing: [.22, .03, .26, 1] } ); document.head.querySelector('style.dashboardHider')?.remove() } async function handleDocuments(node: Element): Promise { if (!(node instanceof HTMLElement)) return; if (!settingsState.animations) return; await waitForElm('.document', true, 10); animate( '.documents tbody tr.document', { opacity: [0, 1], y: [10, 0] }, { delay: stagger(0.05), duration: 0.5, easing: [.22, .03, .26, 1] } ); } async function handleReports(node: Element): Promise { if (!(node instanceof HTMLElement)) return; if (!settingsState.animations) return; await waitForElm('.report', true, 10); animate( '.reports .item', { opacity: [0, 1], y: [10, 0] }, { delay: stagger(0.05, { start: 0.2 }), duration: 0.5, easing: [.22, .03, .26, 1] } ); } function CheckNoticeTextColour(notice: any) { eventManager.register('noticeAdded', { elementType: 'div', className: 'notice', parentElement: notice }, (node) => { var hex = (node as HTMLElement).style.cssText.split(' ')[1]; if (hex) { const hex1 = hex.slice(0,-1); var threshold = GetThresholdOfColor(hex1); if (settingsState.DarkMode && threshold < 100) { (node as HTMLElement).style.cssText = '--color: undefined;'; } } }); } export function tryLoad() { waitForElm('.login').then(() => { finishLoad() }) waitForElm('.day-container').then(() => { finishLoad() }) waitForElm('[data-key=welcome]').then((elm: any) => { elm.classList.remove('active') }) waitForElm('.code', true, 50).then((elm: any) => { if (!elm.innerText.includes('BetterSEQTA')) LoadPageElements() }) updateIframesWithDarkMode() // Waits for page to call on load, run scripts document.addEventListener( 'load', function () { removeThemeTagsFromNotices() }, true, ) } function ChangeMenuItemPositions(storage: any) { let menuorder = storage var menuList = document.querySelector('#menu')!.firstChild!.childNodes let listorder = [] for (let i = 0; i < menuList.length; i++) { const menu = menuList[i] as HTMLElement let a = menuorder.indexOf(menu.dataset.key) listorder.push(a) } var newArr = [] for (var i = 0; i < listorder.length; i++) { newArr[listorder[i]] = menuList[i] } let listItemsDOM = document.getElementById('menu')!.firstChild for (let i = 0; i < newArr.length; i++) { const element = newArr[i] if (element) { const elem = element as HTMLElement elem.setAttribute('data-checked', 'true') listItemsDOM!.appendChild(element) } } } export async function ObserveMenuItemPosition() { await waitForElm('#menu > ul > li') await delay(100) eventManager.register('menuList', { parentElement: document.querySelector('#menu')!.firstChild as Element, }, (element: Element) => { const node = element as HTMLElement; if (!node?.dataset?.checked && !MenuOptionsOpen) { const key = MenuitemSVGKey[node?.dataset?.key! as keyof typeof MenuitemSVGKey]; if (key) { ReplaceMenuSVG( node, MenuitemSVGKey[node.dataset.key as keyof typeof MenuitemSVGKey], ); } else if (node?.firstChild?.nodeName === 'LABEL') { const label = node.firstChild as HTMLElement; let textNode = label.lastChild as HTMLElement; if (textNode.nodeType === 3 && textNode.parentNode && textNode.parentNode.nodeName !== 'SPAN') { const span = document.createElement('span'); span.textContent = textNode.nodeValue; label.replaceChild(span, textNode); } } ChangeMenuItemPositions(settingsState.menuorder); } }); } function main() { if (typeof settingsState.onoff === 'undefined') { browser.runtime.sendMessage({ type: 'setDefaultStorage' }) } const handleDisabled = () => { waitForElm('.code', true, 50).then(AppendElementsToDisabledPage) } if (settingsState.onoff) { console.info('[BetterSEQTA+] Enabled') if (settingsState.DarkMode) document.documentElement.classList.add('dark') document.querySelector('.legacy-root')?.classList.add('hidden') new StorageChangeHandler(); new MessageHandler() updateAllColors() loading() InjectCustomIcons() HideMenuItems() tryLoad() } else { handleDisabled() window.addEventListener('load', handleDisabled) } } function InjectCustomIcons() { console.info('[BetterSEQTA+] Injecting Icons') const style = document.createElement('style') style.setAttribute('type', 'text/css') style.innerHTML = ` @font-face { font-family: 'IconFamily'; src: url('${browser.runtime.getURL(IconFamily)}') format('woff'); font-weight: normal; font-style: normal; }` document.head.appendChild(style) } export function AppendElementsToDisabledPage() { console.info("[BetterSEQTA+] Appending elements to disabled page") AddBetterSEQTAElements() let settingsStyle = document.createElement('style') settingsStyle.innerHTML = /* css */` .addedButton { position: absolute !important; right: 50px; width: 35px; height: 35px; padding: 6px !important; overflow: unset !important; border-radius: 50%; margin: 7px !important; cursor: pointer; color: white !important; } .addedButton svg { margin: 6px; } .outside-container { top: 48px !important; } #ExtensionPopup { border-radius: 1rem; box-shadow: 0px 0px 20px -2px rgba(0, 0, 0, 0.6); transform-origin: 70% 0; } ` document.head.append(settingsStyle) } export const closeExtensionPopup = (extensionPopup?: HTMLElement) => { if (!extensionPopup) extensionPopup = document.getElementById('ExtensionPopup')! extensionPopup.classList.add('hide') if (settingsState.animations) { animate((progress) => { extensionPopup.style.opacity = Math.max(0, 1 - progress).toString() extensionPopup.style.transform = `scale(${Math.max(0, 1 - progress)})` }, { easing: spring({ stiffness: 520, damping: 20 }) }) } else { extensionPopup.style.opacity = '0' extensionPopup.style.transform = 'scale(0)' } settingsPopup.triggerClose() SettingsClicked = false } export function addExtensionSettings() { const extensionPopup = document.createElement('div') extensionPopup.classList.add('outside-container', 'hide') extensionPopup.id = 'ExtensionPopup' const extensionContainer = document.querySelector('#container') as HTMLDivElement if (extensionContainer) extensionContainer.appendChild(extensionPopup) // create shadow dom and render svelte app try { const shadow = extensionPopup.attachShadow({ mode: 'open' }); requestIdleCallback(() => renderSvelte(Settings, shadow)); } catch (err) { console.error(err) } const container = document.getElementById('container') new SettingsResizer(); container!.onclick = (event) => { if (!SettingsClicked) return; if (!(event.target as HTMLElement).closest('#AddedSettings')) { if (event.target == extensionPopup) return; closeExtensionPopup() } } } export function OpenMenuOptions() { var container = document.getElementById('container') var menu = document.getElementById('menu') if (settingsState.defaultmenuorder.length == 0) { let childnodes = menu!.firstChild!.childNodes let newdefaultmenuorder = [] for (let i = 0; i < childnodes.length; i++) { const element = childnodes[i] newdefaultmenuorder.push((element as HTMLElement).dataset.key) settingsState.defaultmenuorder = newdefaultmenuorder } } let childnodes = menu!.firstChild!.childNodes if (settingsState.defaultmenuorder.length != childnodes.length) { for (let i = 0; i < childnodes.length; i++) { const element = childnodes[i] if (!settingsState.defaultmenuorder.indexOf((element as HTMLElement).dataset.key)) { let newdefaultmenuorder = settingsState.defaultmenuorder newdefaultmenuorder.push((element as HTMLElement).dataset.key) settingsState.defaultmenuorder = newdefaultmenuorder } } } MenuOptionsOpen = true var cover = document.createElement('div') cover.classList.add('notMenuCover') menu!.style.zIndex = '20' menu!.style.setProperty('--menuHidden', 'flex') container!.append(cover) var menusettings = document.createElement('div') menusettings.classList.add('editmenuoption-container') var defaultbutton = document.createElement('div') defaultbutton.classList.add('editmenuoption') defaultbutton.innerText = 'Restore Default' defaultbutton.id = 'restoredefaultoption' var savebutton = document.createElement('div') savebutton.classList.add('editmenuoption') savebutton.innerText = 'Save' savebutton.id = 'restoredefaultoption' menusettings.appendChild(defaultbutton) menusettings.appendChild(savebutton) menu!.appendChild(menusettings) var ListItems = menu!.firstChild!.childNodes for (let i = 0; i < ListItems.length; i++) { const element1 = ListItems[i] const element = element1 as HTMLElement (element as HTMLElement).classList.add('draggable'); if ((element as HTMLElement).classList.contains('hasChildren')) { (element as HTMLElement).classList.remove('active'); (element.firstChild as HTMLElement).classList.remove('noscroll'); } let MenuItemToggle = stringToHTML( `
    ` ).firstChild; (element as HTMLElement).append(MenuItemToggle!) if (!element.dataset.betterseqta) { const a = document.createElement('section') a.innerHTML = element.innerHTML cloneAttributes(a, element) menu!.firstChild!.insertBefore(a, element) element.remove() } } if (Object.keys(settingsState.menuitems).length == 0) { menubuttons = menu!.firstChild!.childNodes let menuItems = {} as any for (var i = 0; i < menubuttons.length; i++) { var id = (menubuttons[i] as HTMLElement).dataset.key const element: any = {} element.toggle = true; (menuItems[id as keyof typeof menuItems] as any) = element; } settingsState.menuitems = menuItems } var menubuttons: any = document.getElementsByClassName('menuitem') let menuItems = settingsState.menuitems as any let buttons = document.getElementsByClassName('menuitem') for (let i = 0; i < buttons.length; i++) { let id = buttons[i].id as string | undefined if (menuItems[id as keyof typeof menuItems]) { (buttons[i] as HTMLInputElement).checked = menuItems[id as keyof typeof menuItems].toggle } else { (buttons[i] as HTMLInputElement).checked = true } (buttons[i] as HTMLInputElement).checked = true } try { var el = document.querySelector('#menu > ul') var sortable = Sortable.create((el as HTMLElement), { draggable: '.draggable', dataIdAttr: 'data-key', animation: 150, easing: "cubic-bezier(.5,0,.5,1)", onEnd: function() { saveNewOrder(sortable) }, }); } catch (err) { console.error(err) } function changeDisplayProperty(element: any) { if (!element.checked) { element.parentNode.parentNode.style.display = 'var(--menuHidden)' } if (element.checked) { element.parentNode.parentNode.style.setProperty( 'display', 'flex', 'important', ) } } function StoreMenuSettings() { let menu = document.getElementById('menu') const menuItems: any = {} let menubuttons = menu!.firstChild!.childNodes const button = document.getElementsByClassName('menuitem') for (let i = 0; i < menubuttons.length; i++) { const id = (menubuttons[i] as HTMLElement).dataset.key const element: any = {} element.toggle = (button[i] as HTMLInputElement).checked menuItems[id as keyof typeof menuItems] = element } settingsState.menuitems = menuItems } for (let i = 0; i < menubuttons.length; i++) { const element = menubuttons[i] element.addEventListener('change', () => { element.parentElement.parentElement.getAttribute('data-key') StoreMenuSettings() changeDisplayProperty(element) }) } function closeAll() { menusettings?.remove() cover?.remove() MenuOptionsOpen = false menu!.style.setProperty('--menuHidden', 'none') for (let i = 0; i < ListItems.length; i++) { const element1 = ListItems[i] const element = element1 as HTMLElement element.classList.remove('draggable') element.setAttribute('draggable', 'false') if (!element.dataset.betterseqta) { const a = document.createElement('li') a.innerHTML = element.innerHTML cloneAttributes(a, element) menu!.firstChild!.insertBefore(a, element) element.remove() } } let switches = menu!.querySelectorAll('.onoffswitch') for (let i = 0; i < switches.length; i++) { switches[i].remove() } } cover?.addEventListener('click', closeAll) savebutton?.addEventListener('click', closeAll) defaultbutton?.addEventListener('click', function() { const options = settingsState.defaultmenuorder settingsState.menuorder = options ChangeMenuItemPositions(options) for (let i = 0; i < menubuttons.length; i++) { const element = menubuttons[i] element.checked = true element.parentNode.parentNode.style.setProperty( 'display', 'flex', 'important', ) } saveNewOrder(sortable) }) } function saveNewOrder(sortable: any) { var order = sortable.toArray() settingsState.menuorder = order } function cloneAttributes(target: any, source: any) { [...source.attributes].forEach((attr) => { target.setAttribute(attr.nodeName, attr.nodeValue) }) } function ReplaceMenuSVG(element: HTMLElement, svg: string) { let item = element.firstChild as HTMLElement item!.firstChild!.remove() item.innerHTML = `${item.innerHTML}` let newsvg = stringToHTML(svg).firstChild item.insertBefore((newsvg as Node), item.firstChild) } export function setupSettingsButton() { var AddedSettings = document.getElementById('AddedSettings'); var extensionPopup = document.getElementById('ExtensionPopup'); AddedSettings!.addEventListener('click', async () => { if (SettingsClicked) { closeExtensionPopup(extensionPopup as HTMLElement); } else { if (settingsState.animations) { animate((progress) => { extensionPopup!.style.opacity = progress.toString() extensionPopup!.style.transform = `scale(${progress})` }, { easing: spring({ stiffness: 280, damping: 20 }) }); } else { extensionPopup!.style.opacity = '1' extensionPopup!.style.transform = 'scale(1)' extensionPopup!.style.transition = 'opacity 0s linear, transform 0s linear' } extensionPopup!.classList.remove('hide'); SettingsClicked = true; } }); } async function CheckCurrentLesson(lesson: any, num: number) { const { from: startTime, until: endTime, code, description, room, staff } = lesson; const currentDate = new Date(); // Create Date objects for start and end times const [startHour, startMinute] = startTime.split(':').map(Number); const [endHour, endMinute] = endTime.split(':').map(Number); const startDate = new Date(currentDate); startDate.setHours(startHour, startMinute, 0); const endDate = new Date(currentDate); endDate.setHours(endHour, endMinute, 0); // Check if the current time is within the lesson time range const isValidTime = startDate < currentDate && endDate > currentDate; const elementId = `${code}${num}`; const element = document.getElementById(elementId); if (!element) { clearInterval(LessonInterval); return; } const isCurrentDate = currentSelectedDate.toLocaleDateString('en-au') === currentDate.toLocaleDateString('en-au'); if (isCurrentDate) { if (isValidTime) { element.classList.add('activelesson'); } else { element.classList.remove('activelesson'); } } const minutesUntilStart = Math.floor((startDate.getTime() - currentDate.getTime()) / 60000); if (minutesUntilStart !== 5 || settingsState.lessonalert || !window.Notification) return; if (Notification.permission !== 'granted') await Notification.requestPermission(); try { new Notification('Next Lesson in 5 Minutes:', { body: `Subject: ${description}${room ? `\nRoom: ${room}` : ''}${staff ? `\nTeacher: ${staff}` : ''}`, }); } catch (error) { console.error(error); } } export function GetThresholdOfColor(color: any) { if (!color) return 0 // Case-insensitive regular expression for matching RGBA colors const rgbaRegex = /rgba?\(([^)]+)\)/gi // Check if the color string is a gradient (linear or radial) if (color.includes('gradient')) { let gradientThresholds = [] // Find and replace all instances of RGBA in the gradient let match while ((match = rgbaRegex.exec(color)) !== null) { // Extract the individual components (r, g, b, a) const rgbaString = match[1] const [r, g, b] = rgbaString.split(',').map(str => str.trim()) // Compute the threshold using your existing algorithm const threshold = Math.sqrt(parseInt(r) ** 2 + parseInt(g) ** 2 + parseInt(b) ** 2) // Store the computed threshold gradientThresholds.push(threshold) } // Calculate the average threshold const averageThreshold = gradientThresholds.reduce((acc, val) => acc + val, 0) / gradientThresholds.length return averageThreshold } else { // Handle the color as a simple RGBA (or hex, or whatever the Color library supports) const rgb = Color.rgb(color).object() return Math.sqrt(rgb.r ** 2 + rgb.g ** 2 + rgb.b ** 2) } } function CheckCurrentLessonAll(lessons: any) { // Checks each lesson and sets an interval to run every 60 seconds to continue updating LessonInterval = setInterval( function () { for (let i = 0; i < lessons.length; i++) { CheckCurrentLesson(lessons[i], i + 1) } }.bind(lessons), 60000, ) } // Helper function to build the assessment URL function buildAssessmentURL(programmeID: any, metaID: any, itemID = '') { const base = '../#?page=/assessments/' return itemID ? `${base}${programmeID}:${metaID}&item=${itemID}` : `${base}${programmeID}:${metaID}` } // Function to create a lesson div element from a lesson object function makeLessonDiv(lesson: any, num: number) { if (!lesson) throw new Error('No lesson provided.') const { code, colour, description, staff, room, from, until, attendanceTitle, programmeID, metaID, assessments } = lesson // Construct the base lesson string with default values using ternary operators let lessonString = `

    ${description || 'Unknown'}

    ${staff || 'Unknown'}

    ${room || 'Unknown'}

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

    ${attendanceTitle || 'Unknown'}
    ` // Add buttons for assessments and courses if applicable if (programmeID !== 0) { lessonString += `
    ${assessmentsicon}
    ${coursesicon}
    ` } // Add assessments if they exist if (assessments && assessments.length > 0) { const assessmentString = assessments.map((element: any) => `

    ${element.title}

    ` ).join('') lessonString += `
    ${assessmentString}
    ` } lessonString += '
    ' return stringToHTML(lessonString) } function CheckUnmarkedAttendance(lessonattendance: any) { if (lessonattendance) { var lesson = lessonattendance.label } else { lesson = ' ' } return lesson } function convertTo12HourFormat(time: string, noMinutes: boolean = false): string { let [hours, minutes] = time.split(':').map(Number); let period = 'AM'; if (hours >= 12) { period = 'PM'; if (hours > 12) hours -= 12; } else if (hours === 0) { hours = 12; } let hoursStr = hours.toString(); if (hoursStr.length === 2 && hoursStr.startsWith('0')) { hoursStr = hoursStr.substring(1); } return `${hoursStr}${noMinutes ? '' : `:${minutes.toString().padStart(2, '0')}`} ${period}`; } function callHomeTimetable(date: string, change?: any) { // Creates a HTTP Post Request to the SEQTA page for the students timetable var xhr = new XMLHttpRequest() xhr.open('POST', `${location.origin}/seqta/student/load/timetable?`, true) // Sets the response type to json xhr.setRequestHeader('Content-Type', 'application/json; charset=utf-8') xhr.onreadystatechange = function () { // Once the response is ready if (xhr.readyState === 4) { var serverResponse = JSON.parse(xhr.response) let lessonArray: Array = [] const DayContainer = document.getElementById('day-container')! // If items in response: 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) }) // If items in the response, set each corresponding value into divs // lessonArray = lessonArray.splice(1) GetLessonColours().then((colours) => { let subjects = colours for (let i = 0; i < lessonArray.length; i++) { let subjectname = `timetable.subject.colour.${lessonArray[i].code}` 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 (result > 300) { lessonArray[i].invert = true } } // Removes seconds from the start and end times 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) } // Checks if attendance is unmarked, and sets the string to " ". lessonArray[i].attendanceTitle = CheckUnmarkedAttendance( lessonArray[i].attendance, ) } // If on home page, apply each lesson to HTML with information in each div DayContainer.innerText = '' for (let i = 0; i < lessonArray.length; i++) { var div = makeLessonDiv(lessonArray[i], i + 1) // Append each of the lessons into the day-container if (lessonArray[i].invert) { const div1 = div.firstChild! as HTMLElement div1.classList.add('day-inverted') } DayContainer.append(div.firstChild as HTMLElement) } const today = new Date() if (currentSelectedDate.getDate() == today.getDate()) { for (let i = 0; i < lessonArray.length; i++) { CheckCurrentLesson(lessonArray[i], i + 1) } // For each lesson, check the start and end times CheckCurrentLessonAll(lessonArray) } }) } } 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) } } } xhr.send( JSON.stringify({ // Information sent to SEQTA page as a request with the dates and student number from: date, until: date, // Funny number student: 69, }), ) } async function GetUpcomingAssessments() { let func = 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) } async function GetActiveClasses() { try { const response = await fetch(`${location.origin}/seqta/student/load/subjects?`, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify({}) }) if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`) } const data = await response.json() return data.payload } catch (error) { console.error('Oops! There was a problem fetching active classes:', error) } } function comparedate(obj1: any, obj2: any) { if (obj1.date < obj2.date) { return -1 } if (obj1.date > obj2.date) { return 1 } return 0 } function CreateElement(type: string, class_?: any, id?: any, innerText?: string, innerHTML?: string, style?: string) { let element = document.createElement(type) if (class_ !== undefined) { element.classList.add(class_) } if (id !== undefined) { element.id = id } if (innerText !== undefined) { element.innerText = innerText } if (innerHTML !== undefined) { element.innerHTML = innerHTML } if (style !== undefined) { element.style.cssText = style } return element } function createAssessmentDateDiv(date: string, value: any, datecase?: any) { var 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') DateTitleDiv.classList.add('upcoming-date-title') if (datecase) { let 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') DateTitle.innerText = FormattedDate.toLocaleDateString('en-AU', options) DateTitleDiv.append(DateTitle) container.append(DateTitleDiv) let 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') 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') titlediv.classList.add('upcoming-subject-title') let titlesvg = stringToHTML(` `).firstChild titlediv.append(titlesvg!) let detailsdiv = document.createElement('div') detailsdiv.classList.add('upcoming-details') let detailstitle = document.createElement('h5') detailstitle.innerText = `${element.subject} assessment` let 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) assessmentContainer.append(item) fetch(`${location.origin}/seqta/student/assessment/submissions/get`, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8', }, body: JSON.stringify({ assessment: element.id, metaclass: element.metaclassID, student: 69, }), }) .then((result) => result.json()) .then((response) => { if (response.payload.length > 0) { const assessment = document.querySelector(`#assessment${element.id}`) // ticksvg = stringToHTML(``).firstChild // ticksvg.classList.add('upcoming-tick') // assessment.append(ticksvg) let submittedtext = document.createElement('div') submittedtext.classList.add('upcoming-submittedtext') submittedtext.innerText = 'Submitted' assessment!.append(submittedtext) } }) } container.append(assessmentContainer) return container } function CheckSpecialDay(date1: Date, date2: Date) { if ( date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() - 1 === date2.getDate() ) { return 'Yesterday' } if ( date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate() ) { return 'Today' } if ( date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() + 1 === date2.getDate() ) { return 'Tomorrow' } } function CreateSubjectFilter(subjectcode: any, itemcolour: string, checked: any) { let label = CreateElement('label', 'upcoming-checkbox-container') label.innerText = subjectcode let input1 = CreateElement('input') const input = input1 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) 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 settingsState.subjectfilters = filters }) return label } function CreateFilters(subjects: any) { let filteroptions = settingsState.subjectfilters let filterdiv = document.querySelector('#upcoming-filters') for (let i = 0; i < subjects.length; i++) { const element = subjects[i] // eslint-disable-next-line 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(elementdiv) } } async function CreateUpcomingSection(assessments: any, activeSubjects: any) { let upcomingitemcontainer = document.querySelector('#upcoming-items') let overdueDates = [] let upcomingDates = {} var Today = new Date() // Removes overdue assessments from the upcoming assessments array and pushes to overdue array 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-- } } } 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) if (!subject) { assessments[i].colour = '--item-colour: #8e8e8e;' } else { assessments[i].colour = `--item-colour: ${subject.value};` GetThresholdOfColor(subject.value); // result (originally) result = GetThresholdOfColor } } 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) if (!colour) { element.colour = '--item-colour: #8e8e8e;' } else { element.colour = `--item-colour: ${colour.value};` let result = GetThresholdOfColor(colour.value) if (result > 300) { element.invert = true } } } CreateFilters(activeSubjects) // @ts-ignore let type // @ts-ignore 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( // TODO: not sure whats going on here? // eslint-disable-next-line type = "div", // eslint-disable-next-line class_ = "upcoming-date-container", ) dateObj.assessments = []; (upcomingDates[element.due as keyof typeof upcomingDates] as any) = dateObj } let 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((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], // eslint-disable-next-line datecase, ) } else { assessmentDate = createAssessmentDateDiv(date, upcomingDates[date as keyof typeof upcomingDates]) } if (specialcase === 'Yesterday') { upcomingitemcontainer!.insertBefore( assessmentDate, upcomingitemcontainer!.firstChild, ) } else { upcomingitemcontainer!.append(assessmentDate) } } FilterUpcomingAssessments(settingsState.subjectfilters) } function AddPlaceHolderToParent(parent: any, numberofassessments: any) { let textcontainer = CreateElement('div', 'upcoming-blank') let textblank = CreateElement('p', 'upcoming-hiddenassessment') let s = '' if (numberofassessments > 1) { s = 's' } textblank.innerText = `${numberofassessments} hidden assessment${s} due` textcontainer.append(textblank) textcontainer.setAttribute('data-hidden', 'true') parent.append(textcontainer) } export function FilterUpcomingAssessments(subjectoptions: any) { for (var item in subjectoptions) { let subjectdivs = document.querySelectorAll(`[data-subject="${item}"]`) for (let i = 0; i < subjectdivs.length; i++) { const element = subjectdivs[i] if (!subjectoptions[item]) { element.classList.add('hidden') } if (subjectoptions[item]) { element.classList.remove('hidden') } (element.parentNode! as HTMLElement).classList.remove('hidden') let children = element.parentNode!.parentNode!.children for (let i = 0; i < children.length; i++) { const element = children[i] if (element.hasAttribute('data-hidden')) { element.remove() } } if ( element.parentNode!.children.length == element.parentNode!.querySelectorAll('.hidden').length ) { if (element.parentNode!.querySelectorAll('.hidden').length > 0) { if (!(element.parentNode!.parentNode! as HTMLElement).hasAttribute('data-day')) { (element.parentNode!.parentNode! as HTMLElement).classList.add('hidden') } else { AddPlaceHolderToParent( element.parentNode!.parentNode, element.parentNode!.querySelectorAll('.hidden').length, ) } } } else { (element.parentNode!.parentNode! as HTMLElement).classList.remove('hidden') } } } } 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) } export function CreateCustomShortcutDiv(element: any) { // Creates the stucture and element information for each seperate shortcut var shortcut = document.createElement('a') shortcut.setAttribute('href', element.url) shortcut.setAttribute('target', '_blank') var shortcutdiv = document.createElement('div') shortcutdiv.classList.add('shortcut') shortcutdiv.classList.add('customshortcut') let image = stringToHTML( ` ${element.icon} `, ).firstChild; (image as HTMLElement).classList.add('shortcuticondiv') var text = document.createElement('p') text.textContent = element.name shortcutdiv.append(image!) shortcutdiv.append(text) shortcut.append(shortcutdiv) document.getElementById('shortcuts')!.append(shortcut) } export function RemoveShortcutDiv(elements: any) { if (elements.length === 0) return elements.forEach((element: any) => { const shortcuts = document.querySelectorAll('.shortcut') shortcuts.forEach((shortcut) => { const anchorElement = shortcut.parentElement; // the element is the parent const textElement = shortcut.querySelector('p'); //

    is a direct child of .shortcut const title = textElement ? textElement.textContent : '' let shouldRemove = title === element.name // Check href only if element.url exists if (element.url) { shouldRemove = shouldRemove && (anchorElement!.getAttribute('href') === element.url) } if (shouldRemove) { anchorElement!.remove() } }) }) } async function AddCustomShortcutsToPage() { let customshortcuts: any = settingsState.customshortcuts if (customshortcuts.length > 0) { for (let i = 0; i < customshortcuts.length; i++) { const element = customshortcuts[i] CreateCustomShortcutDiv(element) } } } export async function loadHomePage() { // Sends the html data for the home page console.info('[BetterSEQTA+] Started Loading Home Page') document.title = 'Home ― SEQTA Learn' const element = document.querySelector('[data-key=home]') await delay(8) // Apply the active class to indicate clicked on home button element!.classList.add('active') // Remove all current elements in the main div to add new elements const main = document.getElementById('main') if (!main) { console.error('Main element not found.') return } else { main!.innerHTML = '' } currentSelectedDate = new Date() // Creates the root of the home page added to the main div let homeContainer = stringToHTML(/* html */`

    `) // Appends the html file to main div // Note: firstChild of html is done due to needing to grab the body from the stringToHTML function main.append(homeContainer?.firstChild!) // Gets the current date const date = new Date() // Creates the shortcut container into the home container const Shortcut = stringToHTML(/* html */`
    `) // Appends the shortcut container into the home container document.getElementById('home-container')?.append(Shortcut?.firstChild!) // Creates the container div for the timetable portion of the home page const Timetable = stringToHTML(/* html */`

    Today\'s Lessons

    `) // Appends the timetable container into the home container document.getElementById('home-container')?.append(Timetable?.firstChild!) // Formats the current date used send a request for timetable and notices later const TodayFormatted = date.getFullYear() + '-' + ((date.getMonth() + 1) < 10 ? '0' : '') + (date.getMonth() + 1) + '-' + (date.getDate() < 10 ? '0' : '') + date.getDate() callHomeTimetable(TodayFormatted, true) const timetablearrowback = document.getElementById('home-timetable-back') const timetablearrowforward = document.getElementById('home-timetable-forward') function SetTimetableSubtitle() { var homelessonsubtitle = document.getElementById('home-lesson-subtitle') const date = new Date() if ( date.getFullYear() == currentSelectedDate.getFullYear() && date.getMonth() == currentSelectedDate.getMonth() ) { if (date.getDate() == currentSelectedDate.getDate()) { // Change text to Today's Lessons homelessonsubtitle!.innerText = 'Today\'s Lessons' } else if (date.getDate() - 1 == currentSelectedDate.getDate()) { // Change text to Yesterday's Lessons homelessonsubtitle!.innerText = 'Yesterday\'s Lessons' } else if (date.getDate() + 1 == currentSelectedDate.getDate()) { // Change text to Tomorrow's Lessons homelessonsubtitle!.innerText = 'Tomorrow\'s Lessons' } else { // Change text to date of the day homelessonsubtitle!.innerText = `${currentSelectedDate.toLocaleString( 'en-us', { weekday: 'short' }, )} ${currentSelectedDate.toLocaleDateString('en-au')}` } } else { // Change text to date of the day homelessonsubtitle!.innerText = `${currentSelectedDate.toLocaleString( 'en-us', { weekday: 'short' }, )} ${currentSelectedDate.toLocaleDateString('en-au')}` } } function changeTimetable(value: any) { currentSelectedDate.setDate(currentSelectedDate.getDate() + value) let FormattedDate = currentSelectedDate.getFullYear() + '-' + (currentSelectedDate.getMonth() + 1) + '-' + currentSelectedDate.getDate() callHomeTimetable(FormattedDate, true) SetTimetableSubtitle() } timetablearrowback!.addEventListener('click', function () { changeTimetable(-1) }) timetablearrowforward!.addEventListener('click', function () { changeTimetable(1) }) addShortcuts(settingsState.shortcuts) AddCustomShortcutsToPage() // Creates the upcoming container and appends to the home container const upcomingcontainer = document.createElement('div') upcomingcontainer.classList.add('upcoming-container') upcomingcontainer.classList.add('border') const upcomingtitlediv = CreateElement('div', 'upcoming-title') const upcomingtitle = document.createElement('h2') upcomingtitle.classList.add('home-subtitle') upcomingtitle.innerText = 'Upcoming Assessments' upcomingtitlediv.append(upcomingtitle) let upcomingfilterdiv = CreateElement( 'div', 'upcoming-filters', 'upcoming-filters', ) upcomingtitlediv.append(upcomingfilterdiv) upcomingcontainer.append(upcomingtitlediv) const upcomingitems = document.createElement('div') upcomingitems.id = 'upcoming-items' upcomingitems.classList.add('upcoming-items') upcomingcontainer.append(upcomingitems) document.getElementById('home-container')!.append(upcomingcontainer) // Creates the notices container into the home container const NoticesStr = /* html */ `

    Notices

    ` var Notices = stringToHTML(NoticesStr) // Appends the shortcut container into the home container document.getElementById('home-container')!.append(Notices.firstChild!) // HERE!!! if (settingsState.animations) { animate( '.home-container > div', { opacity: [0, 1], y: [10, 0] }, { delay: stagger(0.2, { start: 0 }), duration: 0.6, easing: [.22, .03, .26, 1] } ) } callHomeTimetable(TodayFormatted) const GetPrefs = await fetch(`${location.origin}/seqta/student/load/prefs?`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ asArray: true, request: 'userPrefs' }) }) const response = await GetPrefs.json() const labelArray = response.payload.filter((item: any) => item.name === 'notices.filters').map((item: any) => item.value) if (labelArray.length !== 0) { const labelArray = response.payload.filter((item: any) => item.name === 'notices.filters').map((item: any) => item.value)[0].split(' ') const xhr2 = new XMLHttpRequest() xhr2.open( 'POST', `${location.origin}/seqta/student/load/notices?`, true ) xhr2.setRequestHeader('Content-Type', 'application/json; charset=utf-8') xhr2.onreadystatechange = function () { if (xhr2.readyState === 4) { processNotices(xhr2.response, labelArray); } }; const dateControl = document.querySelector('input[type="date"]') as HTMLInputElement; xhr2.send(JSON.stringify({ date: dateControl.value })); function onInputChange(e: any) { xhr2.open('POST', `${location.origin}/seqta/student/load/notices?`, true); xhr2.setRequestHeader('Content-Type', 'application/json; charset=utf-8'); xhr2.send(JSON.stringify({ date: e.target.value })); xhr2.onreadystatechange = function () { if (xhr2.readyState === 4) { processNotices(xhr2.response, labelArray); } }; } dateControl.addEventListener('input', onInputChange); } if (settingsState.notificationcollector) { enableNotificationCollector() } const assessments = await GetUpcomingAssessments() const classes = await GetActiveClasses() let activeClass; // Gets all subjects for the student for (let i = 0; i < classes.length; i++) { const element = classes[i]; if (element.hasOwnProperty("active")) { // Finds the active class list with the current subjects activeClass = classes[i] } } let activeSubjects = [] if (activeClass?.subjects) { activeSubjects = activeClass.subjects } let activeSubjectCodes = [] // Gets the code for each of the subjects and puts them in an array for (let i = 0; i < activeSubjects.length; i++) { activeSubjectCodes.push(activeSubjects[i].code) } let CurrentAssessments = [] for (let i = 0; i < assessments.length; i++) { if (activeSubjectCodes.includes(assessments[i].code)) { CurrentAssessments.push(assessments[i]) } } CurrentAssessments.sort(comparedate) await CreateUpcomingSection(CurrentAssessments, activeSubjects) } function processNotices(responseText: any, labelArray: any) { const NoticesPayload = JSON.parse(responseText); const NoticeContainer = document.getElementById('notice-container'); if (NoticesPayload.payload.length === 0) { if (!NoticeContainer?.innerText) { const dummyNotice = document.createElement('div'); dummyNotice.textContent = 'No notices for today.'; dummyNotice.classList.add('dummynotice'); NoticeContainer?.append(dummyNotice); } } else { if (!NoticeContainer?.innerText) { document.querySelectorAll('.notice').forEach(e => e.remove()); NoticesPayload.payload.forEach((notice: any) => { if (labelArray.includes(JSON.stringify(notice.label))) { let htmlContent = `

    ${notice.title}

    ${notice.label_title !== undefined ? `
    ${notice.label_title}
    ` : ''}
    ${notice.staff}
    ${notice.contents.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, '').replace(/ +/, ' ')}
    `; const NewNotice = stringToHTML(htmlContent).firstChild; let colour = notice.colour; if (typeof colour === 'string') { const rgb = GetThresholdOfColor(colour); if (rgb < 100 && settingsState.DarkMode) { colour = undefined; } } (NewNotice as HTMLElement).style.cssText = `--colour: ${colour}`; NoticeContainer!.append(NewNotice!); } }); } } } export function addShortcuts(shortcuts: any) { for (let i = 0; i < shortcuts.length; i++) { const currentShortcut = shortcuts[i] if (currentShortcut?.enabled) { const Itemname = (currentShortcut?.name ?? '').replace(/\s/g, '') const linkDetails = ShortcutLinks?.[Itemname as keyof typeof ShortcutLinks] if (linkDetails) { createNewShortcut( linkDetails.link, linkDetails.icon, linkDetails.viewBox, currentShortcut?.name ) } else { console.warn(`No link details found for '${Itemname}'`) } } } } export function enableNotificationCollector() { var xhr3 = new XMLHttpRequest() xhr3.open('POST', `${location.origin}/seqta/student/heartbeat?`, true) xhr3.setRequestHeader( 'Content-Type', 'application/json; charset=utf-8' ) xhr3.onreadystatechange = function () { if (xhr3.readyState === 4) { var Notifications = JSON.parse(xhr3.response) var alertdiv = document.getElementsByClassName( 'notifications__bubble___1EkSQ' )[0] if (typeof alertdiv == 'undefined') { console.info('[BetterSEQTA+] No notifications currently') } else { alertdiv.textContent = Notifications.payload.notifications.length } } } xhr3.send( JSON.stringify({ timestamp: '1970-01-01 00:00:00.0', hash: '#?page=/home', }) ) } export function disableNotificationCollector() { var alertdiv = document.getElementsByClassName('notifications__bubble___1EkSQ')[0] if (typeof alertdiv != 'undefined') { var currentNumber = parseInt(alertdiv.textContent!) if (currentNumber < 9) { alertdiv.textContent = currentNumber.toString() } else { alertdiv.textContent = '9+' } } } function createNewShortcut(link: any, icon: any, viewBox: any, title: any) { // Creates the stucture and element information for each seperate shortcut let shortcut = document.createElement('a') shortcut.setAttribute('href', link) shortcut.setAttribute('target', '_blank') let shortcutdiv = document.createElement('div') shortcutdiv.classList.add('shortcut') let image = stringToHTML( ``, ).firstChild; (image! as HTMLElement).classList.add('shortcuticondiv') let text = document.createElement('p') text.textContent = title shortcutdiv.append(image as HTMLElement) shortcutdiv.append(text) shortcut.append(shortcutdiv) document.getElementById('shortcuts')!.appendChild(shortcut) } export function SendNewsPage() { setTimeout(function () { // Sends the html data for the home page console.info('[BetterSEQTA+] Started Loading News Page') document.title = 'News ― SEQTA Learn' var element = document.querySelector('[data-key=news]') // Apply the active class to indicate clicked on home button element!.classList.add('active') // Remove all current elements in the main div to add new elements var main = document.getElementById('main') main!.innerHTML = '' // Creates the root of the home page added to the main div var htmlStr = '

    Latest Headlines - ABC News

    ' var html = stringToHTML(htmlStr) // Appends the html file to main div // Note : firstChild of html is done due to needing to grab the body from the stringToHTML function main!.append(html.firstChild!) const titlediv = document.getElementById('title')!.firstChild; (titlediv! as HTMLElement).innerText = 'News' AppendLoadingSymbol('newsloading', '#news-container') browser.runtime.sendMessage({ type: 'sendNews' }).then(function (response) { let newsarticles = response.news.articles var newscontainer = document.querySelector('#news-container') document.getElementById('newsloading')?.remove() for (let i = 0; i < newsarticles.length; i++) { let newsarticle = document.createElement('a') newsarticle.classList.add('NewsArticle') newsarticle.href = newsarticles[i].url newsarticle.target = '_blank' let articleimage = document.createElement('div') articleimage.classList.add('articleimage') if (newsarticles[i].urlToImage == 'null') { articleimage.style.backgroundImage = `url(${browser.runtime.getURL(LogoLightOutline)})` articleimage.style.width = '20%' articleimage.style.margin = '0 7.5%' } else { articleimage.style.backgroundImage = `url(${newsarticles[i].urlToImage})` } let articletext = document.createElement('div') articletext.classList.add('ArticleText') let title = document.createElement('a') title.innerText = newsarticles[i].title title.href = newsarticles[i].url title.target = '_blank' let description = document.createElement('p') description.innerHTML = newsarticles[i].description articletext.append(title) articletext.append(description) newsarticle.append(articleimage) newsarticle.append(articletext) newscontainer?.append(newsarticle) } }) }, 8) } async function CheckForMenuList() { try { if (document.getElementById('menu')?.firstChild) { ObserveMenuItemPosition() } } catch (error) { return; } }