From c9f0f9cf16817adb6fa2b468553d7843e5d149b1 Mon Sep 17 00:00:00 2001 From: Alphons Joseph <93847055+Crazypersonalph@users.noreply.github.com> Date: Wed, 12 Mar 2025 21:45:23 +0800 Subject: [PATCH] start modularisation and breaking down the monofile --- .../components/themes/ThemeSelector.svelte | 2 +- src/interface/pages/settings.svelte | 5 +- src/plugins/monofile.ts | 2199 +---------------- src/seqta/ui/AddBetterSEQTAElements.ts | 8 +- .../utils/Adders/AddExtensionSettings.ts | 36 + src/seqta/utils/Adders/AddShortcuts.ts | 46 + .../utils/Closers/closeExtensionPopup.ts | 34 + .../utils/CreateEnable/CreateBackground.ts | 23 + .../CreateEnable/CreateCustomShortcutDiv.ts | 37 + src/seqta/utils/CreateEnable/CreateElement.ts | 26 + .../CreateEnable/EnableAnimatedBackground.ts | 13 + .../EnableNotificationCollector.ts | 24 + .../DisableNotificationCollector.ts | 13 + .../utils/DisableRemove/RemoveBackground.ts | 10 + .../utils/DisableRemove/RemoveShortcutDiv.ts | 24 + src/seqta/utils/FilterUpcomingAssessments.ts | 67 + src/seqta/utils/Loaders/LoadHomePage.ts | 1049 ++++++++ src/seqta/utils/Openers/OpenAboutPage.ts | 107 + src/seqta/utils/Openers/OpenMenuOptions.ts | 256 ++ src/seqta/utils/SendNewsPage.ts | 103 + src/seqta/utils/Whatsnew.ts | 314 +++ src/seqta/utils/convertTo12HourFormat.ts | 21 + src/seqta/utils/listeners/MessageListener.ts | 4 +- src/seqta/utils/listeners/StorageChanges.ts | 22 +- src/seqta/utils/setupSettingsButton.ts | 33 + 25 files changed, 2284 insertions(+), 2192 deletions(-) create mode 100644 src/seqta/utils/Adders/AddExtensionSettings.ts create mode 100644 src/seqta/utils/Adders/AddShortcuts.ts create mode 100644 src/seqta/utils/Closers/closeExtensionPopup.ts create mode 100644 src/seqta/utils/CreateEnable/CreateBackground.ts create mode 100644 src/seqta/utils/CreateEnable/CreateCustomShortcutDiv.ts create mode 100644 src/seqta/utils/CreateEnable/CreateElement.ts create mode 100644 src/seqta/utils/CreateEnable/EnableAnimatedBackground.ts create mode 100644 src/seqta/utils/CreateEnable/EnableNotificationCollector.ts create mode 100644 src/seqta/utils/DisableRemove/DisableNotificationCollector.ts create mode 100644 src/seqta/utils/DisableRemove/RemoveBackground.ts create mode 100644 src/seqta/utils/DisableRemove/RemoveShortcutDiv.ts create mode 100644 src/seqta/utils/FilterUpcomingAssessments.ts create mode 100644 src/seqta/utils/Loaders/LoadHomePage.ts create mode 100644 src/seqta/utils/Openers/OpenAboutPage.ts create mode 100644 src/seqta/utils/Openers/OpenMenuOptions.ts create mode 100644 src/seqta/utils/SendNewsPage.ts create mode 100644 src/seqta/utils/Whatsnew.ts create mode 100644 src/seqta/utils/convertTo12HourFormat.ts create mode 100644 src/seqta/utils/setupSettingsButton.ts diff --git a/src/interface/components/themes/ThemeSelector.svelte b/src/interface/components/themes/ThemeSelector.svelte index cc59f700..9d490729 100644 --- a/src/interface/components/themes/ThemeSelector.svelte +++ b/src/interface/components/themes/ThemeSelector.svelte @@ -10,7 +10,7 @@ import { deleteTheme } from '@/seqta/ui/themes/deleteTheme' import { OpenStorePage } from '@/seqta/ui/renderStore' import { themeUpdates } from '@/interface/hooks/ThemeUpdates' - import { closeExtensionPopup } from '@/plugins/monofile' + import { closeExtensionPopup } from '@/seqta/utils/Closers/closeExtensionPopup' let themes = $state(null); let { isEditMode } = $props<{ isEditMode: boolean }>(); diff --git a/src/interface/pages/settings.svelte b/src/interface/pages/settings.svelte index 2382b5da..7094039e 100644 --- a/src/interface/pages/settings.svelte +++ b/src/interface/pages/settings.svelte @@ -9,7 +9,10 @@ import { onMount } from 'svelte' import { initializeSettingsState, settingsState } from '@/seqta/utils/listeners/SettingsState' - import { closeExtensionPopup, OpenAboutPage, OpenWhatsNewPopup } from "@/plugins/monofile" + import { closeExtensionPopup } from "@/seqta/utils/Closers/closeExtensionPopup" + import { OpenAboutPage } from "@/seqta/utils/Openers/OpenAboutPage" + import { OpenWhatsNewPopup } from "@/seqta/utils/Whatsnew" + import ColourPicker from '../components/ColourPicker.svelte' import { settingsPopup } from '../hooks/SettingsPopup' diff --git a/src/plugins/monofile.ts b/src/plugins/monofile.ts index e5263387..208254c5 100644 --- a/src/plugins/monofile.ts +++ b/src/plugins/monofile.ts @@ -1,6 +1,6 @@ // Third-party libraries -import Sortable from "sortablejs" + import browser from "webextension-polyfill" import { animate, stagger } from "motion" @@ -21,36 +21,40 @@ import { eventManager } from "@/seqta/utils/listeners/EventManager" import RegisterClickListeners from "@/seqta/utils/listeners/ClickListeners" import { AddBetterSEQTAElements } from "@/seqta/ui/AddBetterSEQTAElements" import { enableCurrentTheme } from "@/seqta/ui/themes/enableCurrent" -import loading, { AppendLoadingSymbol } from "@/seqta/ui/Loading" -import { SettingsResizer } from "@/seqta/ui/SettingsResizer" +import loading from "@/seqta/ui/Loading" import { updateAllColors } from "@/seqta/ui/colors/Manager" import pageState from "@/pageState.js?url" // 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" -import kofi from "@/resources/kofi.png" + +import { convertTo12HourFormat } from "@/seqta/utils/convertTo12HourFormat" // 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" -let SettingsClicked = false -export let MenuOptionsOpen = false -let currentSelectedDate = new Date() -let LessonInterval: any + + + +import { MenuOptionsOpen, ChangeMenuItemPositions } from "@/seqta/utils/Openers/OpenMenuOptions" +import { SendNewsPage } from "@/seqta/utils/SendNewsPage" + +import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage" + +import { enableNotificationCollector } from "@/seqta/utils/CreateEnable/EnableNotificationCollector" + +import { OpenWhatsNewPopup } from "@/seqta/utils/Whatsnew" + + + var IsSEQTAPage = false let hasSEQTAText = false @@ -114,15 +118,7 @@ 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 { @@ -144,296 +140,7 @@ async function HideMenuItems(): Promise { } } -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") - - 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.5 - News, Bug Fixes, and improvements!

-
  • Added alternative news sources
  • -
  • Notifications now open direct messages
  • -
  • Added Toggle for Letter/Percent Grades
  • -
  • Added fullscreen to the theme creator CSS editor
  • -
  • Added warning if BetterSEQTA is installed
  • -
  • Removed max width from theme creator
  • -
  • Fixed discord icon colour in light mode
  • -
  • Fixed subject averages not showing up with letter grades
  • -
  • Tweaked compose UI
  • - -

    3.4.4 - Bug Fixes and Improvements

    -
  • Added vertical zoom to the timetable
  • -
  • Fixed theme importing failing when images were included
  • -
  • Removed broken gradients on the backgrounds of certain buttons
  • -
  • Fixed timetable quickbar arrow receiving the wrong colour
  • -
  • Auto-applied selected theme after saving in theme creator
  • -
  • Fixed a bug where timetable was clipped at certain times
  • -
  • Fixed custom sidebar layouts not applying on page load
  • -
  • Improved spacing of the message editor buttons
  • -
  • Added HEX colour input to the theme creator
  • -
  • Fixed theme application in the creator
  • -
  • Performance improvements
  • -
  • Other minor bug fixes
  • - -

    3.4.3 - Minor Bug Fixes

    -
  • Fixed a bug where timetable colours couldn't be changed
  • -
  • Other minor bug fixes
  • - -

    3.4.2 - Minor Bug Fixes

    -
  • Fixed a bug where Assessment Average wasn't enabled by default
  • -
  • Fixed floating menus would sometimes be placed behind other elements
  • - -

    3.4.1 - Bug Fixes and Performance Improvements

    -
  • Added a new "Subject Average" section to the assessments page
  • -
  • Fixed a bug where animations wouldn't play correctly
  • -
  • Added loading animations to the home page
  • -
  • Under the hood performance improvements
  • -
  • Improved animation performance
  • -
  • Better Animations!
  • -
  • Minor style tweaks
  • - -

    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: - - - - - - - - - - - - - - - -
    - -
    - - Buy Me a Coffee at ko-fi.com - -
    -
    - `).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] }, - { - type: "spring", - stiffness: 220, - damping: 18, - }, - ) - - animate( - ".whatsnewTextContainer *", - { opacity: [0, 1], y: [10, 0] }, - { - delay: stagger(0.05, { startDelay: 0.1 }), - duration: 0.5, - ease: [0.22, 0.03, 0.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() - }) -} function injectMainScript() { const mainScript = document.createElement("script") @@ -462,107 +169,7 @@ export function hideSideBar() { } } -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] }, - { - type: "spring", - stiffness: 220, - damping: 18, - }, - ) - - animate( - ".whatsnewTextContainer *", - { opacity: [0, 1], y: [10, 0] }, - { - delay: stagger(0.05, { startDelay: 0.1 }), - duration: 0.5, - ease: [0.22, 0.03, 0.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 { @@ -581,58 +188,11 @@ export async function finishLoad() { } } -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] }, - { ease: [0.22, 0.03, 0.26, 1] }, - ).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 GetCSSElement(file: string) { @@ -1198,35 +758,7 @@ export function tryLoad() { ) } -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) - } - } -} function ReplaceMenuSVG(element: HTMLElement, svg: string) { let item = element.firstChild as HTMLElement @@ -1425,1603 +957,6 @@ export function AppendElementsToDisabledPage() { document.head.append(settingsStyle) } -export const closeExtensionPopup = (extensionPopup?: HTMLElement) => { - if (!extensionPopup) - extensionPopup = document.getElementById("ExtensionPopup")! - - extensionPopup.classList.add("hide") - if (settingsState.animations) { - animate(1, 0, { - onUpdate: (progress) => { - extensionPopup.style.opacity = Math.max(0, progress).toString() - extensionPopup.style.transform = `scale(${Math.max(0, progress)})` - }, - type: "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) - }) -} - -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(0, 1, { - onUpdate: (progress) => { - extensionPopup!.style.opacity = progress.toString() - extensionPopup!.style.transform = `scale(${progress})` - }, - type: "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) - } -} - - - -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 = /* html */ ` -
    -

    ${description || "Unknown"}

    -

    ${staff || "Unknown"}

    -

    ${room || "Unknown"}

    -

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

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

    ${element.title}

    `, - ) - .join("") - - lessonString += /* html */ ` -
    - - - -
    ${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() { - console.info("[BetterSEQTA+] Started Loading Home Page") - - // Wait for the DOM to finish clearing - await delay(10) - - document.title = "Home ― SEQTA Learn" - const element = document.querySelector("[data-key=home]") - element?.classList.add("active") - - // Cache DOM queries - const main = document.getElementById("main") - if (!main) { - console.error("[BetterSEQTA+] Main element not found.") - return - } - - // Create root container first - const homeRoot = stringToHTML( - /* html */ `

    `, - ) - - // Clear main and add home root - main.innerHTML = "" - main.appendChild(homeRoot?.firstChild!) - - // Get reference to home container for all subsequent additions - const homeContainer = document.getElementById("home-root") - if (!homeContainer) return - - const skeletonStructure = stringToHTML(/* html */ ` -
    -
    -
    -
    -
    -
    -

    Today's Lessons

    -
    - - - - - - -
    -
    -
    -
    -
    -
    -
    -

    Upcoming Assessments

    -
    -
    -
    -
    -
    -
    -
    -

    Notices

    - -
    -
    -
    -
    -
    `) - - // Add skeleton structure - homeContainer.appendChild(skeletonStructure.firstChild!) - - // Run animations if enabled - if (settingsState.animations) { - animate( - ".home-container > div", - { opacity: [0, 1], y: [10, 0], scale: [0.99, 1] }, - { - delay: stagger(0.15, { startDelay: 0.1 }), - type: "spring", - stiffness: 341, - damping: 20, - mass: 1, - }, - ) - } - - // Setup event listeners with cleanup - const cleanup = setupTimetableListeners() - - // Initialize shortcuts immediately - try { - addShortcuts(settingsState.shortcuts) - } catch (err: any) { - console.error("[BetterSEQTA+] Error adding shortcuts:", err.message || err) - } - AddCustomShortcutsToPage() - - // Get current date - const date = new Date() - const TodayFormatted = formatDate(date) - - // Start all data fetching in parallel - const [timetablePromise, assessmentsPromise, classesPromise, prefsPromise] = [ - // Timetable data - fetch(`${location.origin}/seqta/student/load/timetable?`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - from: TodayFormatted, - until: TodayFormatted, - student: 69, - }), - }).then((res) => res.json()), - - // Assessments data - GetUpcomingAssessments(), - - // Classes data - GetActiveClasses(), - - // Preferences data - fetch(`${location.origin}/seqta/student/load/prefs?`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ asArray: true, request: "userPrefs" }), - }).then((res) => res.json()), - ] - - // Process all data in parallel - const [timetableData, assessments, classes, prefs] = await Promise.all([ - timetablePromise, - assessmentsPromise, - classesPromise, - prefsPromise, - ]) - - // Process timetable data - const dayContainer = document.getElementById("day-container") - if (dayContainer && timetableData.payload.items.length > 0) { - const lessonArray = timetableData.payload.items.sort((a: any, b: any) => - a.from.localeCompare(b.from), - ) - const colours = await GetLessonColours() - - // Process and display lessons - dayContainer.innerHTML = "" - for (let i = 0; i < lessonArray.length; i++) { - const lesson = lessonArray[i] - const subjectname = `timetable.subject.colour.${lesson.code}` - const subject = colours.find( - (element: any) => element.name === subjectname, - ) - - lesson.colour = subject - ? `--item-colour: ${subject.value};` - : "--item-colour: #8e8e8e;" - lesson.from = lesson.from.substring(0, 5) - lesson.until = lesson.until.substring(0, 5) - - if (settingsState.timeFormat === "12") { - lesson.from = convertTo12HourFormat(lesson.from) - lesson.until = convertTo12HourFormat(lesson.until) - } - - lesson.attendanceTitle = CheckUnmarkedAttendance(lesson.attendance) - - const div = makeLessonDiv(lesson, i + 1) - if (GetThresholdOfColor(subject?.value) > 300) { - const firstChild = div.firstChild as HTMLElement - if (firstChild) { - firstChild.classList.add("day-inverted") - } - } - dayContainer.appendChild(div.firstChild!) - } - - // Check current lessons - if (currentSelectedDate.getDate() === date.getDate()) { - for (let i = 0; i < lessonArray.length; i++) { - CheckCurrentLesson(lessonArray[i], i + 1) - } - CheckCurrentLessonAll(lessonArray) - } - } else if (dayContainer) { - dayContainer.innerHTML = /* html */ ` -
    - -

    No lessons available.

    -
    ` - } - dayContainer?.classList.remove("loading") - - // Process assessments data - const activeClass = classes.find((c: any) => c.hasOwnProperty("active")) - const activeSubjects = activeClass?.subjects || [] - const activeSubjectCodes = activeSubjects.map((s: any) => s.code) - const currentAssessments = assessments - .filter((a: any) => activeSubjectCodes.includes(a.code)) - .sort(comparedate) - - const upcomingItems = document.getElementById("upcoming-items") - if (upcomingItems) { - await CreateUpcomingSection(currentAssessments, activeSubjects) - upcomingItems.classList.remove("loading") - } - - // Process notices data - const labelArray = prefs.payload - .filter((item: any) => item.name === "notices.filters") - .map((item: any) => item.value) - - if (labelArray.length > 0) { - const noticeContainer = document.getElementById("notice-container") - if (noticeContainer) { - const dateControl = document.querySelector( - 'input[type="date"]', - ) as HTMLInputElement - if (dateControl) { - dateControl.value = TodayFormatted - setupNotices(labelArray[0].split(" "), TodayFormatted) - } - noticeContainer.classList.remove("loading") - } - } - - if (settingsState.notificationcollector) { - enableNotificationCollector() - } - - return cleanup -} - -// Helper functions -function formatDate(date: Date): string { - const year = date.getFullYear() - const month = (date.getMonth() + 1).toString().padStart(2, "0") - const day = date.getDate().toString().padStart(2, "0") - return `${year}-${month}-${day}` -} - -function setupTimetableListeners() { - const listeners: Array<() => void> = [] - const timetableBack = document.getElementById("home-timetable-back") - const timetableForward = document.getElementById("home-timetable-forward") - - function changeTimetable(value: number) { - currentSelectedDate.setDate(currentSelectedDate.getDate() + value) - const formattedDate = formatDate(currentSelectedDate) - callHomeTimetable(formattedDate, true) - SetTimetableSubtitle() - } - - const backHandler = () => changeTimetable(-1) - const forwardHandler = () => changeTimetable(1) - - timetableBack?.addEventListener("click", backHandler) - timetableForward?.addEventListener("click", forwardHandler) - - listeners.push( - () => timetableBack?.removeEventListener("click", backHandler), - () => timetableForward?.removeEventListener("click", forwardHandler), - ) - - return () => listeners.forEach((cleanup) => cleanup()) -} - -function setupNotices(labelArray: string[], date: string) { - const dateControl = document.querySelector( - 'input[type="date"]', - ) as HTMLInputElement - - const fetchNotices = async (date: string) => { - const response = await fetch( - `${location.origin}/seqta/student/load/notices?`, - { - method: "POST", - headers: { "Content-Type": "application/json; charset=utf-8" }, - body: JSON.stringify({ date }), - }, - ) - const data = await response.json() - processNotices(data, labelArray) - } - - // Debounce the input handler - const debouncedInputChange = debounce((e: Event) => { - const target = e.target as HTMLInputElement - fetchNotices(target.value) - }, 250) - - dateControl?.addEventListener("input", debouncedInputChange) - fetchNotices(date) - - return () => dateControl?.removeEventListener("input", debouncedInputChange) -} - -function debounce any>( - func: T, - wait: number, -): (...args: Parameters) => void { - let timeout: NodeJS.Timeout - return (...args: Parameters) => { - clearTimeout(timeout) - timeout = setTimeout(() => func(...args), wait) - } -} - -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 async function SendNewsPage() { - console.info("[BetterSEQTA+] Started Loading News Page") - document.title = "News ― SEQTA Learn" - await delay(100) - - const element = document.querySelector("[data-key=news]") - element!.classList.add("active") - - // Remove all current elements in the main div to add new elements - const main = document.getElementById("main") - main!.innerHTML = "" - - const html = stringToHTML(/* html */ ` -
    -
    -

    Latest Headlines in ${settingsState.newsSource ? settingsState.newsSource.charAt(0).toUpperCase() + settingsState.newsSource.slice(1) : "Australia"}

    -
    -
    `) - - main!.append(html.firstChild!) - - const titlediv = document.getElementById("title")!.firstChild - ;(titlediv! as HTMLElement).innerText = "News" - AppendLoadingSymbol("newsloading", "#news-container") - - const response = (await browser.runtime.sendMessage({ - type: "sendNews", - source: settingsState.newsSource, - })) as any - const newscontainer = document.querySelector("#news-container") - document.getElementById("newsloading")?.remove() - - // Create a document fragment to batch DOM operations - const fragment = document.createDocumentFragment() - - // Map over articles to create elements - response.news.articles.forEach((article: any) => { - const newsarticle = document.createElement("a") - newsarticle.classList.add("NewsArticle") - newsarticle.href = article.url - newsarticle.target = "_blank" - - const articleimage = document.createElement("div") - articleimage.classList.add("articleimage") - - if (article.urlToImage == "null" || article.urlToImage == null) { - articleimage.style.cssText = ` - background-image: url(${browser.runtime.getURL(LogoLightOutline)}); - width: 20%; - margin: 0 7.5%; - ` - } else { - articleimage.style.backgroundImage = `url(${article.urlToImage})` - } - - const articletext = document.createElement("div") - articletext.classList.add("ArticleText") - - const title = document.createElement("a") - title.innerText = article.title - title.href = article.url - title.target = "_blank" - - const description = document.createElement("p") - - article.description = - article.description.length > 400 - ? article.description.substring(0, 400) + "..." - : article.description - description.innerHTML = article.description - - articletext.append(title, description) - newsarticle.append(articleimage, articletext) - fragment.append(newsarticle) - }) - - // Single DOM update to append all articles - newscontainer?.append(fragment) - - if (!settingsState.animations) return - - const articles = Array.from(document.querySelectorAll(".NewsArticle")) - - animate( - articles.slice(0, 20), - { opacity: [0, 1], y: [10, 0], scale: [0.99, 1] }, - { - delay: stagger(0.1), - type: "spring", - stiffness: 341, - damping: 20, - mass: 1, - }, - ) -} - async function CheckForMenuList() { try { await waitForElm("#menu > ul") @@ -3031,98 +966,6 @@ async function CheckForMenuList() { } } -function SetTimetableSubtitle() { - const homelessonsubtitle = document.getElementById("home-lesson-subtitle") - if (!homelessonsubtitle) return - - const date = new Date() - const isSameMonth = - date.getFullYear() === currentSelectedDate.getFullYear() && - date.getMonth() === currentSelectedDate.getMonth() - - if (isSameMonth) { - const dayDiff = date.getDate() - currentSelectedDate.getDate() - switch (dayDiff) { - case 0: - homelessonsubtitle.innerText = "Today's Lessons" - break - case 1: - homelessonsubtitle.innerText = "Yesterday's Lessons" - break - case -1: - homelessonsubtitle.innerText = "Tomorrow's Lessons" - break - default: - homelessonsubtitle.innerText = formatDateString(currentSelectedDate) - } - } else { - homelessonsubtitle.innerText = formatDateString(currentSelectedDate) - } -} - -function formatDateString(date: Date): string { - return `${date.toLocaleString("en-us", { weekday: "short" })} ${date.toLocaleDateString("en-au")}` -} - -function processNotices(response: any, labelArray: string[]) { - const NoticeContainer = document.getElementById("notice-container") - if (!NoticeContainer) return - - // Clear existing notices - NoticeContainer.innerHTML = "" - - const notices = response.payload - if (!notices.length) { - const dummyNotice = document.createElement("div") - dummyNotice.textContent = "No notices for today." - dummyNotice.classList.add("dummynotice") - NoticeContainer.append(dummyNotice) - return - } - - // Create document fragment for batch DOM updates - const fragment = document.createDocumentFragment() - - // Process notices in batch - notices.forEach((notice: any) => { - if (labelArray.includes(JSON.stringify(notice.label))) { - const colour = processNoticeColor(notice.colour) - const noticeElement = createNoticeElement(notice, colour) - fragment.appendChild(noticeElement) - } - }) - - // Single DOM update - NoticeContainer.appendChild(fragment) -} - -function processNoticeColor(colour: string): string | undefined { - if (typeof colour === "string") { - const rgb = GetThresholdOfColor(colour) - if (rgb < 100 && settingsState.DarkMode) { - return undefined - } - } - return colour -} - -function createNoticeElement(notice: any, colour: string | undefined): Node { - const htmlContent = ` -
    -

    ${notice.title}

    - ${notice.label_title !== undefined ? `
    ${notice.label_title}
    ` : ""} -
    ${notice.staff}
    - ${notice.contents.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "").replace(/ +/, " ")} -
    -
    ` - - const element = stringToHTML(htmlContent).firstChild - if (element instanceof HTMLElement) { - element.style.setProperty("--colour", colour ?? "") - } - return element! -} - async function handleAssessments(node: Element): Promise { if (!(node instanceof HTMLElement)) return diff --git a/src/seqta/ui/AddBetterSEQTAElements.ts b/src/seqta/ui/AddBetterSEQTAElements.ts index 107515db..f46cdc10 100644 --- a/src/seqta/ui/AddBetterSEQTAElements.ts +++ b/src/seqta/ui/AddBetterSEQTAElements.ts @@ -1,4 +1,10 @@ -import { addExtensionSettings, enableAnimatedBackground, loadHomePage, SendNewsPage, setupSettingsButton } from "@/plugins/monofile"; +import { addExtensionSettings } from "@/seqta/utils/Adders/AddExtensionSettings"; +import { enableAnimatedBackground } from "@/seqta/utils/CreateEnable/EnableAnimatedBackground"; +import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage"; +import { SendNewsPage } from "@/seqta/utils/SendNewsPage"; +import { setupSettingsButton } from "@/seqta/utils/setupSettingsButton"; + + import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour"; import { updateBgDurations } from "./Animation"; import { appendBackgroundToUI } from "./ImageBackgrounds"; diff --git a/src/seqta/utils/Adders/AddExtensionSettings.ts b/src/seqta/utils/Adders/AddExtensionSettings.ts new file mode 100644 index 00000000..2c2cbebe --- /dev/null +++ b/src/seqta/utils/Adders/AddExtensionSettings.ts @@ -0,0 +1,36 @@ +import { SettingsClicked, changeSettingsClicked, closeExtensionPopup } from "../Closers/closeExtensionPopup" +import renderSvelte from "@/interface/main" +import { SettingsResizer } from "@/seqta/ui/SettingsResizer" +import Settings from "@/interface/pages/settings.svelte" + +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 + changeSettingsClicked(closeExtensionPopup()) + } + } + } \ No newline at end of file diff --git a/src/seqta/utils/Adders/AddShortcuts.ts b/src/seqta/utils/Adders/AddShortcuts.ts new file mode 100644 index 00000000..6799712e --- /dev/null +++ b/src/seqta/utils/Adders/AddShortcuts.ts @@ -0,0 +1,46 @@ +import ShortcutLinks from "@/seqta/content/links.json" +import stringToHTML from "../stringToHTML" + +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}'`) + } + } + } +} + +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) +} \ No newline at end of file diff --git a/src/seqta/utils/Closers/closeExtensionPopup.ts b/src/seqta/utils/Closers/closeExtensionPopup.ts new file mode 100644 index 00000000..64bb79bb --- /dev/null +++ b/src/seqta/utils/Closers/closeExtensionPopup.ts @@ -0,0 +1,34 @@ +import { settingsState } from "@/seqta/utils/listeners/SettingsState"; +import { animate } from "motion" + +import { settingsPopup } from "@/interface/hooks/SettingsPopup" + +export let SettingsClicked = false + +export const closeExtensionPopup = (extensionPopup?: HTMLElement) => { + if (!extensionPopup) + extensionPopup = document.getElementById("ExtensionPopup")! + + extensionPopup.classList.add("hide") + if (settingsState.animations) { + animate(1, 0, { + onUpdate: (progress) => { + extensionPopup.style.opacity = Math.max(0, progress).toString() + extensionPopup.style.transform = `scale(${Math.max(0, progress)})` + }, + type: "spring", + stiffness: 520, + damping: 20, + }) + } else { + extensionPopup.style.opacity = "0" + extensionPopup.style.transform = "scale(0)" + } + + settingsPopup.triggerClose() + return SettingsClicked = false + } + + export function changeSettingsClicked(newVal: boolean) { + SettingsClicked = newVal + } \ No newline at end of file diff --git a/src/seqta/utils/CreateEnable/CreateBackground.ts b/src/seqta/utils/CreateEnable/CreateBackground.ts new file mode 100644 index 00000000..9047e217 --- /dev/null +++ b/src/seqta/utils/CreateEnable/CreateBackground.ts @@ -0,0 +1,23 @@ +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) +} \ No newline at end of file diff --git a/src/seqta/utils/CreateEnable/CreateCustomShortcutDiv.ts b/src/seqta/utils/CreateEnable/CreateCustomShortcutDiv.ts new file mode 100644 index 00000000..7a32d1c0 --- /dev/null +++ b/src/seqta/utils/CreateEnable/CreateCustomShortcutDiv.ts @@ -0,0 +1,37 @@ +import stringToHTML from "../stringToHTML" + +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) +} \ No newline at end of file diff --git a/src/seqta/utils/CreateEnable/CreateElement.ts b/src/seqta/utils/CreateEnable/CreateElement.ts new file mode 100644 index 00000000..ff5af4b7 --- /dev/null +++ b/src/seqta/utils/CreateEnable/CreateElement.ts @@ -0,0 +1,26 @@ +export 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 + } \ No newline at end of file diff --git a/src/seqta/utils/CreateEnable/EnableAnimatedBackground.ts b/src/seqta/utils/CreateEnable/EnableAnimatedBackground.ts new file mode 100644 index 00000000..c1b77fec --- /dev/null +++ b/src/seqta/utils/CreateEnable/EnableAnimatedBackground.ts @@ -0,0 +1,13 @@ +import { settingsState } from "../listeners/SettingsState" +import { CreateBackground } from "./CreateBackground" +import { RemoveBackground } from "../DisableRemove/RemoveBackground" + +export function enableAnimatedBackground() { + if (settingsState.animatedbk) { + CreateBackground() + } else { + RemoveBackground() + document.getElementById("container")!.style.background = + "var(--background-secondary)" + } +} \ No newline at end of file diff --git a/src/seqta/utils/CreateEnable/EnableNotificationCollector.ts b/src/seqta/utils/CreateEnable/EnableNotificationCollector.ts new file mode 100644 index 00000000..8477d668 --- /dev/null +++ b/src/seqta/utils/CreateEnable/EnableNotificationCollector.ts @@ -0,0 +1,24 @@ +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", + }), + ) +} \ No newline at end of file diff --git a/src/seqta/utils/DisableRemove/DisableNotificationCollector.ts b/src/seqta/utils/DisableRemove/DisableNotificationCollector.ts new file mode 100644 index 00000000..10edf229 --- /dev/null +++ b/src/seqta/utils/DisableRemove/DisableNotificationCollector.ts @@ -0,0 +1,13 @@ +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+" + } + } +} \ No newline at end of file diff --git a/src/seqta/utils/DisableRemove/RemoveBackground.ts b/src/seqta/utils/DisableRemove/RemoveBackground.ts new file mode 100644 index 00000000..c58b15c0 --- /dev/null +++ b/src/seqta/utils/DisableRemove/RemoveBackground.ts @@ -0,0 +1,10 @@ +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() +} \ No newline at end of file diff --git a/src/seqta/utils/DisableRemove/RemoveShortcutDiv.ts b/src/seqta/utils/DisableRemove/RemoveShortcutDiv.ts new file mode 100644 index 00000000..a0b607a0 --- /dev/null +++ b/src/seqta/utils/DisableRemove/RemoveShortcutDiv.ts @@ -0,0 +1,24 @@ +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() + } + }) + }) +} \ No newline at end of file diff --git a/src/seqta/utils/FilterUpcomingAssessments.ts b/src/seqta/utils/FilterUpcomingAssessments.ts new file mode 100644 index 00000000..bacf9861 --- /dev/null +++ b/src/seqta/utils/FilterUpcomingAssessments.ts @@ -0,0 +1,67 @@ +import { CreateElement } from "@/seqta/utils/CreateEnable/CreateElement" + +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", + ) + } + } + } +} + +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) +} \ No newline at end of file diff --git a/src/seqta/utils/Loaders/LoadHomePage.ts b/src/seqta/utils/Loaders/LoadHomePage.ts new file mode 100644 index 00000000..e49c5caf --- /dev/null +++ b/src/seqta/utils/Loaders/LoadHomePage.ts @@ -0,0 +1,1049 @@ +import { delay } from "../delay" +import stringToHTML from "../stringToHTML" +import { animate, stagger } from "motion" +import { settingsState } from "../listeners/SettingsState" + +import { addShortcuts } from "../Adders/AddShortcuts" + +import browser from "webextension-polyfill" +import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour" +import LogoLight from "@/resources/icons/betterseqta-light-icon.png" +import { CreateCustomShortcutDiv } from "@/seqta/utils/CreateEnable/CreateCustomShortcutDiv" + +import assessmentsicon from "@/seqta/icons/assessmentsIcon" +import coursesicon from "@/seqta/icons/coursesIcon" + +import { FilterUpcomingAssessments } from "@/seqta/utils/FilterUpcomingAssessments" + +import { CreateElement } from "@/seqta/utils/CreateEnable/CreateElement" + +import { convertTo12HourFormat } from "../convertTo12HourFormat" + +import { enableNotificationCollector } from "@/seqta/utils/CreateEnable/EnableNotificationCollector" + +let LessonInterval: any +let currentSelectedDate = new Date() + +export async function loadHomePage() { + console.info("[BetterSEQTA+] Started Loading Home Page") + + // Wait for the DOM to finish clearing + await delay(10) + + document.title = "Home ― SEQTA Learn" + const element = document.querySelector("[data-key=home]") + element?.classList.add("active") + + // Cache DOM queries + const main = document.getElementById("main") + if (!main) { + console.error("[BetterSEQTA+] Main element not found.") + return + } + + // Create root container first + const homeRoot = stringToHTML( + /* html */ `

    `, + ) + + // Clear main and add home root + main.innerHTML = "" + main.appendChild(homeRoot?.firstChild!) + + // Get reference to home container for all subsequent additions + const homeContainer = document.getElementById("home-root") + if (!homeContainer) return + + const skeletonStructure = stringToHTML(/* html */ ` +
    +
    +
    +
    +
    +
    +

    Today's Lessons

    +
    + + + + + + +
    +
    +
    +
    +
    +
    +
    +

    Upcoming Assessments

    +
    +
    +
    +
    +
    +
    +
    +

    Notices

    + +
    +
    +
    +
    +
    `) + + // Add skeleton structure + homeContainer.appendChild(skeletonStructure.firstChild!) + + // Run animations if enabled + if (settingsState.animations) { + animate( + ".home-container > div", + { opacity: [0, 1], y: [10, 0], scale: [0.99, 1] }, + { + delay: stagger(0.15, { startDelay: 0.1 }), + type: "spring", + stiffness: 341, + damping: 20, + mass: 1, + }, + ) + } + + // Setup event listeners with cleanup + const cleanup = setupTimetableListeners() + + // Initialize shortcuts immediately + try { + addShortcuts(settingsState.shortcuts) + } catch (err: any) { + console.error("[BetterSEQTA+] Error adding shortcuts:", err.message || err) + } + AddCustomShortcutsToPage() + + // Get current date + const date = new Date() + const TodayFormatted = formatDate(date) + + // Start all data fetching in parallel + const [timetablePromise, assessmentsPromise, classesPromise, prefsPromise] = [ + // Timetable data + fetch(`${location.origin}/seqta/student/load/timetable?`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + from: TodayFormatted, + until: TodayFormatted, + student: 69, + }), + }).then((res) => res.json()), + + // Assessments data + GetUpcomingAssessments(), + + // Classes data + GetActiveClasses(), + + // Preferences data + fetch(`${location.origin}/seqta/student/load/prefs?`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ asArray: true, request: "userPrefs" }), + }).then((res) => res.json()), + ] + + // Process all data in parallel + const [timetableData, assessments, classes, prefs] = await Promise.all([ + timetablePromise, + assessmentsPromise, + classesPromise, + prefsPromise, + ]) + + // Process timetable data + const dayContainer = document.getElementById("day-container") + if (dayContainer && timetableData.payload.items.length > 0) { + const lessonArray = timetableData.payload.items.sort((a: any, b: any) => + a.from.localeCompare(b.from), + ) + const colours = await GetLessonColours() + + // Process and display lessons + dayContainer.innerHTML = "" + for (let i = 0; i < lessonArray.length; i++) { + const lesson = lessonArray[i] + const subjectname = `timetable.subject.colour.${lesson.code}` + const subject = colours.find( + (element: any) => element.name === subjectname, + ) + + lesson.colour = subject + ? `--item-colour: ${subject.value};` + : "--item-colour: #8e8e8e;" + lesson.from = lesson.from.substring(0, 5) + lesson.until = lesson.until.substring(0, 5) + + if (settingsState.timeFormat === "12") { + lesson.from = convertTo12HourFormat(lesson.from) + lesson.until = convertTo12HourFormat(lesson.until) + } + + lesson.attendanceTitle = CheckUnmarkedAttendance(lesson.attendance) + + const div = makeLessonDiv(lesson, i + 1) + if (GetThresholdOfColor(subject?.value) > 300) { + const firstChild = div.firstChild as HTMLElement + if (firstChild) { + firstChild.classList.add("day-inverted") + } + } + dayContainer.appendChild(div.firstChild!) + } + + // Check current lessons + if (currentSelectedDate.getDate() === date.getDate()) { + for (let i = 0; i < lessonArray.length; i++) { + CheckCurrentLesson(lessonArray[i], i + 1) + } + CheckCurrentLessonAll(lessonArray) + } + } else if (dayContainer) { + dayContainer.innerHTML = /* html */ ` +
    + +

    No lessons available.

    +
    ` + } + dayContainer?.classList.remove("loading") + + // Process assessments data + const activeClass = classes.find((c: any) => c.hasOwnProperty("active")) + const activeSubjects = activeClass?.subjects || [] + const activeSubjectCodes = activeSubjects.map((s: any) => s.code) + const currentAssessments = assessments + .filter((a: any) => activeSubjectCodes.includes(a.code)) + .sort(comparedate) + + const upcomingItems = document.getElementById("upcoming-items") + if (upcomingItems) { + await CreateUpcomingSection(currentAssessments, activeSubjects) + upcomingItems.classList.remove("loading") + } + + // Process notices data + const labelArray = prefs.payload + .filter((item: any) => item.name === "notices.filters") + .map((item: any) => item.value) + + if (labelArray.length > 0) { + const noticeContainer = document.getElementById("notice-container") + if (noticeContainer) { + const dateControl = document.querySelector( + 'input[type="date"]', + ) as HTMLInputElement + if (dateControl) { + dateControl.value = TodayFormatted + setupNotices(labelArray[0].split(" "), TodayFormatted) + } + noticeContainer.classList.remove("loading") + } + } + + if (settingsState.notificationcollector) { + enableNotificationCollector() + } + + return cleanup + } + + 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) + } + + function setupTimetableListeners() { + const listeners: Array<() => void> = [] + const timetableBack = document.getElementById("home-timetable-back") + const timetableForward = document.getElementById("home-timetable-forward") + + function changeTimetable(value: number) { + currentSelectedDate.setDate(currentSelectedDate.getDate() + value) + const formattedDate = formatDate(currentSelectedDate) + callHomeTimetable(formattedDate, true) + SetTimetableSubtitle() + } + + const backHandler = () => changeTimetable(-1) + const forwardHandler = () => changeTimetable(1) + + timetableBack?.addEventListener("click", backHandler) + timetableForward?.addEventListener("click", forwardHandler) + + listeners.push( + () => timetableBack?.removeEventListener("click", backHandler), + () => timetableForward?.removeEventListener("click", forwardHandler), + ) + + return () => listeners.forEach((cleanup) => cleanup()) + } + + function formatDate(date: Date): string { + const year = date.getFullYear() + const month = (date.getMonth() + 1).toString().padStart(2, "0") + const day = date.getDate().toString().padStart(2, "0") + return `${year}-${month}-${day}` + } + + 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 setupNotices(labelArray: string[], date: string) { + const dateControl = document.querySelector( + 'input[type="date"]', + ) as HTMLInputElement + + const fetchNotices = async (date: string) => { + const response = await fetch( + `${location.origin}/seqta/student/load/notices?`, + { + method: "POST", + headers: { "Content-Type": "application/json; charset=utf-8" }, + body: JSON.stringify({ date }), + }, + ) + const data = await response.json() + processNotices(data, labelArray) + } + + // Debounce the input handler + const debouncedInputChange = debounce((e: Event) => { + const target = e.target as HTMLInputElement + fetchNotices(target.value) + }, 250) + + dateControl?.addEventListener("input", debouncedInputChange) + fetchNotices(date) + + return () => dateControl?.removeEventListener("input", debouncedInputChange) + } + + function debounce any>( + func: T, + wait: number, + ): (...args: Parameters) => void { + let timeout: NodeJS.Timeout + return (...args: Parameters) => { + clearTimeout(timeout) + timeout = setTimeout(() => func(...args), wait) + } + } + + function comparedate(obj1: any, obj2: any) { + if (obj1.date < obj2.date) { + return -1 + } + if (obj1.date > obj2.date) { + return 1 + } + return 0 + } + + 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) + } + } + } + + function processNotices(response: any, labelArray: string[]) { + const NoticeContainer = document.getElementById("notice-container") + if (!NoticeContainer) return + + // Clear existing notices + NoticeContainer.innerHTML = "" + + const notices = response.payload + if (!notices.length) { + const dummyNotice = document.createElement("div") + dummyNotice.textContent = "No notices for today." + dummyNotice.classList.add("dummynotice") + NoticeContainer.append(dummyNotice) + return + } + + // Create document fragment for batch DOM updates + const fragment = document.createDocumentFragment() + + // Process notices in batch + notices.forEach((notice: any) => { + if (labelArray.includes(JSON.stringify(notice.label))) { + const colour = processNoticeColor(notice.colour) + const noticeElement = createNoticeElement(notice, colour) + fragment.appendChild(noticeElement) + } + }) + + // Single DOM update + NoticeContainer.appendChild(fragment) + } + + function processNoticeColor(colour: string): string | undefined { + if (typeof colour === "string") { + const rgb = GetThresholdOfColor(colour) + if (rgb < 100 && settingsState.DarkMode) { + return undefined + } + } + return colour + } + + function createNoticeElement(notice: any, colour: string | undefined): Node { + const htmlContent = ` +
    +

    ${notice.title}

    + ${notice.label_title !== undefined ? `
    ${notice.label_title}
    ` : ""} +
    ${notice.staff}
    + ${notice.contents.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "").replace(/ +/, " ")} +
    +
    ` + + const element = stringToHTML(htmlContent).firstChild + if (element instanceof HTMLElement) { + element.style.setProperty("--colour", colour ?? "") + } + return element! + } + + 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, + }), + ) + } + + 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, + ) + } + + 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) + } + } + + 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 = /* html */ ` +
    +

    ${description || "Unknown"}

    +

    ${staff || "Unknown"}

    +

    ${room || "Unknown"}

    +

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

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

    ${element.title}

    `, + ) + .join("") + + lessonString += /* html */ ` +
    + + + +
    ${assessmentString}
    +
    + ` + } + + lessonString += "
    " + + return stringToHTML(lessonString) + } + + function buildAssessmentURL(programmeID: any, metaID: any, itemID = "") { + const base = "../#?page=/assessments/" + return itemID + ? `${base}${programmeID}:${metaID}&item=${itemID}` + : `${base}${programmeID}:${metaID}` + } + + function CheckUnmarkedAttendance(lessonattendance: any) { + if (lessonattendance) { + var lesson = lessonattendance.label + } else { + lesson = " " + } + return lesson + } + + 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 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" + } + } + + 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) + } + + 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) + } + } + + 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 SetTimetableSubtitle() { + const homelessonsubtitle = document.getElementById("home-lesson-subtitle") + if (!homelessonsubtitle) return + + const date = new Date() + const isSameMonth = + date.getFullYear() === currentSelectedDate.getFullYear() && + date.getMonth() === currentSelectedDate.getMonth() + + if (isSameMonth) { + const dayDiff = date.getDate() - currentSelectedDate.getDate() + switch (dayDiff) { + case 0: + homelessonsubtitle.innerText = "Today's Lessons" + break + case 1: + homelessonsubtitle.innerText = "Yesterday's Lessons" + break + case -1: + homelessonsubtitle.innerText = "Tomorrow's Lessons" + break + default: + homelessonsubtitle.innerText = formatDateString(currentSelectedDate) + } + } else { + homelessonsubtitle.innerText = formatDateString(currentSelectedDate) + } + } + + function formatDateString(date: Date): string { + return `${date.toLocaleString("en-us", { weekday: "short" })} ${date.toLocaleDateString("en-au")}` + } \ No newline at end of file diff --git a/src/seqta/utils/Openers/OpenAboutPage.ts b/src/seqta/utils/Openers/OpenAboutPage.ts new file mode 100644 index 00000000..c0325608 --- /dev/null +++ b/src/seqta/utils/Openers/OpenAboutPage.ts @@ -0,0 +1,107 @@ +import stringToHTML from "../stringToHTML" +import browser from "webextension-polyfill" +import { settingsState } from "../listeners/SettingsState" +import { animate, stagger } from "motion" +import { DeleteWhatsNew } from "../Whatsnew" + +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 */ ` +
    + `).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] }, + { + type: "spring", + stiffness: 220, + damping: 18, + }, + ) + + animate( + ".whatsnewTextContainer *", + { opacity: [0, 1], y: [10, 0] }, + { + delay: stagger(0.05, { startDelay: 0.1 }), + duration: 0.5, + ease: [0.22, 0.03, 0.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() + }) + } \ No newline at end of file diff --git a/src/seqta/utils/Openers/OpenMenuOptions.ts b/src/seqta/utils/Openers/OpenMenuOptions.ts new file mode 100644 index 00000000..40dd20f4 --- /dev/null +++ b/src/seqta/utils/Openers/OpenMenuOptions.ts @@ -0,0 +1,256 @@ + +import { settingsState } from "../listeners/SettingsState" +import stringToHTML from "../stringToHTML" +import Sortable from "sortablejs" + +export let MenuOptionsOpen = false + + +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) + }) + } + + export 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) + } + } + } \ No newline at end of file diff --git a/src/seqta/utils/SendNewsPage.ts b/src/seqta/utils/SendNewsPage.ts new file mode 100644 index 00000000..5d2ba67f --- /dev/null +++ b/src/seqta/utils/SendNewsPage.ts @@ -0,0 +1,103 @@ +import { AppendLoadingSymbol } from "@/seqta/ui/Loading" +import stringToHTML from "./stringToHTML" +import { delay } from "./delay" +import { settingsState } from "./listeners/SettingsState" +import browser from "webextension-polyfill" +import LogoLightOutline from "@/resources/icons/betterseqta-light-outline.png" +import { animate, stagger } from "motion" + +export async function SendNewsPage() { + console.info("[BetterSEQTA+] Started Loading News Page") + document.title = "News ― SEQTA Learn" + await delay(100) + + const element = document.querySelector("[data-key=news]") + element!.classList.add("active") + + // Remove all current elements in the main div to add new elements + const main = document.getElementById("main") + main!.innerHTML = "" + + const html = stringToHTML(/* html */ ` +
    +
    +

    Latest Headlines in ${settingsState.newsSource ? settingsState.newsSource.charAt(0).toUpperCase() + settingsState.newsSource.slice(1) : "Australia"}

    +
    +
    `) + + main!.append(html.firstChild!) + + const titlediv = document.getElementById("title")!.firstChild + ;(titlediv! as HTMLElement).innerText = "News" + AppendLoadingSymbol("newsloading", "#news-container") + + const response = (await browser.runtime.sendMessage({ + type: "sendNews", + source: settingsState.newsSource, + })) as any + const newscontainer = document.querySelector("#news-container") + document.getElementById("newsloading")?.remove() + + // Create a document fragment to batch DOM operations + const fragment = document.createDocumentFragment() + + // Map over articles to create elements + response.news.articles.forEach((article: any) => { + const newsarticle = document.createElement("a") + newsarticle.classList.add("NewsArticle") + newsarticle.href = article.url + newsarticle.target = "_blank" + + const articleimage = document.createElement("div") + articleimage.classList.add("articleimage") + + if (article.urlToImage == "null" || article.urlToImage == null) { + articleimage.style.cssText = ` + background-image: url(${browser.runtime.getURL(LogoLightOutline)}); + width: 20%; + margin: 0 7.5%; + ` + } else { + articleimage.style.backgroundImage = `url(${article.urlToImage})` + } + + const articletext = document.createElement("div") + articletext.classList.add("ArticleText") + + const title = document.createElement("a") + title.innerText = article.title + title.href = article.url + title.target = "_blank" + + const description = document.createElement("p") + + article.description = + article.description.length > 400 + ? article.description.substring(0, 400) + "..." + : article.description + description.innerHTML = article.description + + articletext.append(title, description) + newsarticle.append(articleimage, articletext) + fragment.append(newsarticle) + }) + + // Single DOM update to append all articles + newscontainer?.append(fragment) + + if (!settingsState.animations) return + + const articles = Array.from(document.querySelectorAll(".NewsArticle")) + + animate( + articles.slice(0, 20), + { opacity: [0, 1], y: [10, 0], scale: [0.99, 1] }, + { + delay: stagger(0.1), + type: "spring", + stiffness: 341, + damping: 20, + mass: 1, + }, + ) +} \ No newline at end of file diff --git a/src/seqta/utils/Whatsnew.ts b/src/seqta/utils/Whatsnew.ts new file mode 100644 index 00000000..9172c3ba --- /dev/null +++ b/src/seqta/utils/Whatsnew.ts @@ -0,0 +1,314 @@ +import { settingsState } from "./listeners/SettingsState" +import { animate, stagger } from "motion" +import stringToHTML from "./stringToHTML" +import browser from "webextension-polyfill" +import kofi from "@/resources/kofi.png" + +export 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] }, + { ease: [0.22, 0.03, 0.26, 1] }, + ).then(() => { + bkelement?.remove() + }) +} + +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") + + 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.5 - News, Bug Fixes, and improvements!

    +
  • Added alternative news sources
  • +
  • Notifications now open direct messages
  • +
  • Added Toggle for Letter/Percent Grades
  • +
  • Added fullscreen to the theme creator CSS editor
  • +
  • Added warning if BetterSEQTA is installed
  • +
  • Removed max width from theme creator
  • +
  • Fixed discord icon colour in light mode
  • +
  • Fixed subject averages not showing up with letter grades
  • +
  • Tweaked compose UI
  • + +

    3.4.4 - Bug Fixes and Improvements

    +
  • Added vertical zoom to the timetable
  • +
  • Fixed theme importing failing when images were included
  • +
  • Removed broken gradients on the backgrounds of certain buttons
  • +
  • Fixed timetable quickbar arrow receiving the wrong colour
  • +
  • Auto-applied selected theme after saving in theme creator
  • +
  • Fixed a bug where timetable was clipped at certain times
  • +
  • Fixed custom sidebar layouts not applying on page load
  • +
  • Improved spacing of the message editor buttons
  • +
  • Added HEX colour input to the theme creator
  • +
  • Fixed theme application in the creator
  • +
  • Performance improvements
  • +
  • Other minor bug fixes
  • + +

    3.4.3 - Minor Bug Fixes

    +
  • Fixed a bug where timetable colours couldn't be changed
  • +
  • Other minor bug fixes
  • + +

    3.4.2 - Minor Bug Fixes

    +
  • Fixed a bug where Assessment Average wasn't enabled by default
  • +
  • Fixed floating menus would sometimes be placed behind other elements
  • + +

    3.4.1 - Bug Fixes and Performance Improvements

    +
  • Added a new "Subject Average" section to the assessments page
  • +
  • Fixed a bug where animations wouldn't play correctly
  • +
  • Added loading animations to the home page
  • +
  • Under the hood performance improvements
  • +
  • Improved animation performance
  • +
  • Better Animations!
  • +
  • Minor style tweaks
  • + +

    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: + + + + + + + + + + + + + + + +
    + +
    + + Buy Me a Coffee at ko-fi.com + +
    +
    + `).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] }, + { + type: "spring", + stiffness: 220, + damping: 18, + }, + ) + + animate( + ".whatsnewTextContainer *", + { opacity: [0, 1], y: [10, 0] }, + { + delay: stagger(0.05, { startDelay: 0.1 }), + duration: 0.5, + ease: [0.22, 0.03, 0.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() + }) + } \ No newline at end of file diff --git a/src/seqta/utils/convertTo12HourFormat.ts b/src/seqta/utils/convertTo12HourFormat.ts new file mode 100644 index 00000000..3831ba46 --- /dev/null +++ b/src/seqta/utils/convertTo12HourFormat.ts @@ -0,0 +1,21 @@ +export 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}` + } \ No newline at end of file diff --git a/src/seqta/utils/listeners/MessageListener.ts b/src/seqta/utils/listeners/MessageListener.ts index 2d0fd792..f1368e30 100644 --- a/src/seqta/utils/listeners/MessageListener.ts +++ b/src/seqta/utils/listeners/MessageListener.ts @@ -1,6 +1,8 @@ import browser from 'webextension-polyfill' -import { closeExtensionPopup, MenuOptionsOpen, OpenMenuOptions } from '@/plugins/monofile'; +import { closeExtensionPopup } from "@/seqta/utils/Closers/closeExtensionPopup" +import { MenuOptionsOpen, OpenMenuOptions } from "@/seqta/utils/Openers/OpenMenuOptions" + import { deleteTheme } from '@/seqta/ui/themes/deleteTheme'; import { getAvailableThemes } from '@/seqta/ui/themes/getAvailableThemes'; import { saveTheme } from '@/seqta/ui/themes/saveTheme'; diff --git a/src/seqta/utils/listeners/StorageChanges.ts b/src/seqta/utils/listeners/StorageChanges.ts index 3b7d13ff..c61485fe 100644 --- a/src/seqta/utils/listeners/StorageChanges.ts +++ b/src/seqta/utils/listeners/StorageChanges.ts @@ -1,15 +1,17 @@ import { settingsState } from './SettingsState'; import { updateAllColors } from '@/seqta/ui/colors/Manager'; -import { - addShortcuts, - CreateBackground, - CreateCustomShortcutDiv, - disableNotificationCollector, - enableNotificationCollector, - FilterUpcomingAssessments, - RemoveBackground, - RemoveShortcutDiv, -} from '@/plugins/monofile'; + + +import { addShortcuts } from "@/seqta/utils/Adders/AddShortcuts"; +import { CreateBackground } from "@/seqta/utils/CreateEnable/CreateBackground"; +import { CreateCustomShortcutDiv } from "@/seqta/utils/CreateEnable/CreateCustomShortcutDiv"; +import { disableNotificationCollector } from "@/seqta/utils/DisableRemove/DisableNotificationCollector"; +import { enableNotificationCollector } from "@/seqta/utils/CreateEnable/EnableNotificationCollector"; +import { FilterUpcomingAssessments } from "@/seqta/utils/FilterUpcomingAssessments"; +import { RemoveBackground } from "@/seqta/utils/DisableRemove/RemoveBackground"; +import { RemoveShortcutDiv } from "@/seqta/utils/DisableRemove/RemoveShortcutDiv"; + + import { updateBgDurations } from '@/seqta/ui/Animation'; import browser from 'webextension-polyfill'; import type { CustomShortcut } from '@/types/storage'; diff --git a/src/seqta/utils/setupSettingsButton.ts b/src/seqta/utils/setupSettingsButton.ts new file mode 100644 index 00000000..d1ec3e4f --- /dev/null +++ b/src/seqta/utils/setupSettingsButton.ts @@ -0,0 +1,33 @@ +import { closeExtensionPopup, SettingsClicked, changeSettingsClicked } from "./Closers/closeExtensionPopup" +import { animate } from "motion" +import { settingsState } from "./listeners/SettingsState" + +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(0, 1, { + onUpdate: (progress) => { + extensionPopup!.style.opacity = progress.toString() + extensionPopup!.style.transform = `scale(${progress})` + }, + type: "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") + changeSettingsClicked(true) + } + }) + } \ No newline at end of file