import { animate, spring, stagger } from 'motion' import loading, { AppendLoadingSymbol } from './seqta/ui/Loading' 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' import Color from 'color' import MenuitemSVGKey from './seqta/content/MenuItemSVGKey.json' import { MessageHandler } from './seqta/utils/listeners/MessageListener' import ShortcutLinks from './seqta/content/links.json' import Sortable from 'sortablejs' import assessmentsicon from './seqta/icons/assessmentsIcon' import browser from 'webextension-polyfill' import coursesicon from './seqta/icons/coursesIcon' import { delay } from "./seqta/utils/delay" import { enableCurrentTheme } from "./seqta/ui/themes/enableCurrent"; import iframeCSS from "./css/iframe.scss?raw" import stringToHTML from './seqta/utils/stringToHTML' import { updateAllColors } from './seqta/ui/colors/Manager' import { SettingsResizer } from "./seqta/ui/SettingsResizer"; import documentLoadCSS from './css/documentload.scss?inline' import injectedCSS from './css/injected.scss?inline' import { injectYouTubeVideo } from './seqta/ui/VideoLoader' import { initializeSettingsState, settingsState } from './seqta/utils/listeners/SettingsState' import { StorageChangeHandler } from './seqta/utils/listeners/StorageChanges' import { AddBetterSEQTAElements } from './seqta/ui/AddBetterSEQTAElements' import { eventManager, initializeEventManager } from './seqta/utils/listeners/EventManager' declare global { interface Window { chrome?: any } } let SettingsClicked = false export let MenuOptionsOpen = false let currentSelectedDate = new Date() let LessonInterval: any var MenuItemMutation = false var NonSEQTAPage = false var IsSEQTAPage = false // This check is placed outside of the document load event due to issues with EP (https://github.com/BetterSEQTA/BetterSEQTA-Plus/issues/84) const hasSEQTAText = document.childNodes[1].textContent?.includes('Copyright (c) SEQTA Software') init() async function init() { CheckForMenuList() const hasSEQTATitle = document.title.includes('SEQTA Learn') if (hasSEQTAText && hasSEQTATitle && !IsSEQTAPage) { IsSEQTAPage = true console.log('[BetterSEQTA+] Verified SEQTA Page') const documentLoadStyle = document.createElement('style') documentLoadStyle.textContent = documentLoadCSS document.head.appendChild(documentLoadStyle) enableCurrentTheme() try { // wait until settingsState has been loaded from storage await initializeSettingsState(); if (settingsState.onoff) { const injectedStyle = document.createElement('style') injectedStyle.textContent = injectedCSS document.head.appendChild(injectedStyle) } main() } catch (error: any) { console.error(error) } } if (!hasSEQTAText && !NonSEQTAPage) { NonSEQTAPage = true } } 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.log(`[BetterSEQTA+] Hiding ${menuItem} menu item`) } } const menuItemStyle: HTMLStyleElement = document.createElement('style') menuItemStyle.innerText = stylesheetInnerText document.head.appendChild(menuItemStyle) } catch (error) { console.error("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 div = document.createElement('div') div.classList.add('whatsnewImg') imagecont.appendChild(div) let textcontainer = document.createElement('div') textcontainer.classList.add('whatsnewTextContainer') let text = stringToHTML( /* html */ `

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] injectYouTubeVideo( 'JdDA45GYEUc', 'PLSlFV-9e6dvyvZJFPCtBMb3LSp-LGbrbI', document.querySelector('.whatsnewImg')!, true, true, '100%', '100%' ) 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(); } } async function DeleteWhatsNew() { const bkelement = document.getElementById('whatsnewbk') const popup = document.getElementsByClassName('whatsnewContainer')[0] 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 { console.log('[BetterSEQTA+] Waiting for element:', selector); if (usePolling) { return new Promise((resolve) => { const checkForElement = () => { const element = document.querySelector(selector); if (element) { console.log('[BetterSEQTA+] Element found:', selector); 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) => { console.log('[BetterSEQTA+] Element found:', selector); 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) { console.log('[BetterSEQTA+] Element found:', selector); 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, settingsState.DarkMode); 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, DarkMode: boolean): void { const iframeDocument = iframe.contentDocument; if (!iframeDocument) return; if (iframeDocument.readyState !== 'complete') { iframe.onload = () => { applyDarkModeToIframe(iframe, cssLink, DarkMode); }; return; } if (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); await handleSublink(sublink); } async function handleSublink(sublink: string | undefined): Promise { switch (sublink) { case 'news': await handleNewsPage(); break; case 'home': case undefined: window.location.replace(`${location.origin}/#?page=/home`); console.log('[BetterSEQTA+] Started Init') if (settingsState.onoff) loadHomePage() finishLoad(); break; default: await handleDefault(); break; } } async function handleNewsPage(): Promise { console.log('[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; await waitForElm('[data-message]'); animate( '[data-message]', { opacity: [0, 1], y: [10, 0] }, { delay: stagger(0.05), duration: 0.5, easing: [.22, .03, .26, 1] } ); } async function handleDashboard(node: Element): Promise { if (!(node instanceof HTMLElement)) return; if (!settingsState.animations) return; await waitForElm('.dashlet'); animate( '.dashboard > *', { opacity: [0, 1], y: [10, 0] }, { delay: stagger(0.1), duration: 0.5, easing: [.22, .03, .26, 1] } ); } async function handleDocuments(node: Element): Promise { if (!(node instanceof HTMLElement)) return; if (!settingsState.animations) return; await waitForElm('.document'); 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'); 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() documentTextColor() }, true, ) const observer = new MutationObserver(() => { documentTextColor() }) observer.observe(document!, { attributes: true, childList: true, subtree: true, attributeFilter: ['td'], }) } 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() { let menuorder = settingsState.menuorder if (!(menuorder && settingsState.onoff)) return; const observer = new MutationObserver(function (mutations_list) { mutations_list.forEach(function (mutation) { mutation.addedNodes.forEach(function (added_node) { const node = added_node 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') { // Assuming `node` is an
  • element containing a