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 { SettingsState } from "./types/storage" 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 { onError } from './seqta/utils/onError' 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 { settingsState } from './seqta/utils/listeners/SettingsState' import { StorageChangeHandler } from './seqta/utils/listeners/StorageChanges' import { AddBetterSEQTAElements } from './seqta/ui/AddBetterSEQTAElements' declare global { interface Window { chrome?: any } } export let isChrome = window.chrome 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 { const items = await browser.storage.local.get() as SettingsState if (items.onoff) { const injectedStyle = document.createElement('style') injectedStyle.textContent = injectedCSS document.head.appendChild(injectedStyle) } main(items) } catch (error: any) { onError(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)' } } export async function HideMenuItems(): Promise { try { const result = await browser.storage.local.get() as SettingsState let stylesheetInnerText: string = '' for (const [menuItem, { toggle }] of Object.entries(result.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.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] } ) browser.storage.local.remove(['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() }) } 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); } try { const { justupdated } = await browser.storage.local.get('justupdated'); if (justupdated && !document.getElementById('whatsnewbk')) { OpenWhatsNewPopup(); } } catch (error) { console.error("Error retrieving 'justupdated' from storage:", error); } } 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 function waitForElm(selector: string) { return new Promise((resolve) => { const querySelector = () => document.querySelector(selector); if (querySelector()) { return resolve(querySelector()); } const observer = new MutationObserver(() => { if (querySelector()) { resolve(querySelector()); observer.disconnect(); } }); if (document.body) { observer.observe(document.body, { childList: true, subtree: true, }); } else { document.addEventListener('DOMContentLoaded', () => { observer.observe(document.body, { childList: true, subtree: true, }); }); } }); } 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 { // Load the CSS file to overwrite iFrame default CSS const cssLink = document.createElement('style') cssLink.classList.add('iframecss') const cssContent = document.createTextNode(iframeCSS) cssLink.appendChild(cssContent) const observer = new MutationObserver(async (mutationsList) => { for (const mutation of mutationsList) { for (const node of mutation.addedNodes) { if (node.nodeName === 'IFRAME') { const iframe = node as HTMLIFrameElement try { const settings = await browser.storage.local.get('DarkMode') as SettingsState applyDarkModeToIframe(iframe, cssLink, settings.DarkMode); } catch (error) { console.error('Error applying dark mode:', error) } } } } }); if (document.body) { observer.observe(document.body, { childList: true, subtree: true, }); } else { document.addEventListener('DOMContentLoaded', () => { observer.observe(document.body, { childList: true, subtree: true, }); }); } } 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 /* 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.message) { // Check if added_node.firstChild.title is in block list } }) }) }) observer.observe(messagesParentElement, { subtree: true, childList: true, }); */ } async function LoadPageElements(): Promise { await AddBetterSEQTAElements(true) const sublink: string | undefined = window.location.href.split('/')[4] 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.classList.contains('messages')) { let element = document.getElementById('title')!.firstChild as HTMLElement element.innerText = 'Direct Messages' document.title = 'Direct Messages ― SEQTA Learn' SortMessagePageItems(added_node) waitForElm('[data-message]').then(() => { animate( '[data-message]', { opacity: [0, 1], y: [10, 0] }, { delay: stagger(0.05), duration: 0.5, easing: [.22, .03, .26, 1] } ) }) } else if (node.classList.contains('notices')) { CheckNoticeTextColour(added_node) } else if (node.classList.contains('dashboard')) { let ranOnce = false; waitForElm('.dashlet').then(() => { animate( '.dashboard > *', { opacity: [0, 1], y: [10, 0] }, { delay: stagger(0.1), duration: 0.5, easing: [.22, .03, .26, 1] } ) if (ranOnce) return; ranOnce = true; }) } else if (node.classList.contains('documents')) { let ranOnce = false; waitForElm('.document').then(() => { if (ranOnce) return; ranOnce = true; animate( '.documents tbody tr.document', { opacity: [0, 1], y: [10, 0] }, { delay: stagger(0.05), duration: 0.5, easing: [.22, .03, .26, 1] } ) }) } else if (node.classList.contains('reports')) { let ranOnce = false; waitForElm('.report').then(() => { if (ranOnce) return; ranOnce = true; animate( '.reports .item', { opacity: [0, 1], y: [10, 0] }, { delay: stagger(0.05, { start: 0.2 }), duration: 0.5, easing: [.22, .03, .26, 1] } ) }) } }) }) }) observer.observe(document.querySelector('#main') as HTMLElement, { subtree: false, childList: true, }) async function handleNewsPage(): Promise { console.log('[BetterSEQTA+] Started Init') const settings: SettingsState = await browser.storage.local.get() as SettingsState if (settings.onoff) { SendNewsPage() const notificationSettings: SettingsState = await browser.storage.local.get() as SettingsState if (notificationSettings.notificationcollector) { enableNotificationCollector() } finishLoad() } } async function handleDefault(): Promise { finishLoad() const settings: SettingsState = await browser.storage.local.get() as SettingsState if (settings.notificationcollector) { enableNotificationCollector() } } switch (sublink) { case 'news': await handleNewsPage() break case 'home': case undefined: window.location.replace(`${location.origin}/#?page=/home`) LoadInit() break default: await handleDefault() break } } function CheckNoticeTextColour(notice: any) { 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.classList.contains('notice')) { var hex = node.style.cssText.split(' ')[1] if (hex) { const hex1 = hex.slice(0,-1) var threshold = GetThresholdOfColor(hex1) if (settingsState.DarkMode && threshold < 100) { node.style.cssText = '--color: undefined;' } } } }) }) }) observer.observe(notice, { subtree: true, childList: true, }) } 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').then((elm: any) => { if (!elm.innerText.includes('BetterSEQTA')) LoadPageElements() }) // Waits for page to call on load, run scripts document.addEventListener( 'load', function () { updateIframesWithDarkMode() 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() { const result = browser.storage.local.get() function open (result: any) { let menuorder = result.menuorder if (menuorder && result.onoff) { 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