From 3ecd7205ed499124fa2fbb2f677c9f6c66490fd9 Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Sun, 30 Mar 2025 08:49:13 +1100 Subject: [PATCH] feat: add global theme toggle --- src/background.ts | 1 - src/interface/index.html | 4 +- src/interface/pages/settings/general.svelte | 18 +++++ .../built-in/notificationCollector/index.ts | 44 ++++-------- src/plugins/built-in/test/index.ts | 11 ++- src/plugins/built-in/timetable/index.ts | 59 +++++----------- src/plugins/core/createAPI.ts | 17 +++-- src/plugins/core/manager.ts | 70 ++++++++++++++++++- src/plugins/core/types.ts | 7 +- .../EnableNotificationCollector.ts | 24 ------- .../DisableNotificationCollector.ts | 13 ---- src/seqta/utils/Loaders/LoadHomePage.ts | 6 -- src/seqta/utils/listeners/StorageChanges.ts | 11 --- src/types/storage.ts | 1 - 14 files changed, 145 insertions(+), 141 deletions(-) delete mode 100644 src/seqta/utils/CreateEnable/EnableNotificationCollector.ts delete mode 100644 src/seqta/utils/DisableRemove/DisableNotificationCollector.ts diff --git a/src/background.ts b/src/background.ts index 638a952b..4360540b 100644 --- a/src/background.ts +++ b/src/background.ts @@ -65,7 +65,6 @@ const DefaultValues: SettingsState = { bksliderinput: "50", transparencyEffects: false, lessonalert: true, - notificationcollector: true, defaultmenuorder: [], menuitems: { assessments: { toggle: true }, diff --git a/src/interface/index.html b/src/interface/index.html index b324d8bb..0a3f1b8a 100644 --- a/src/interface/index.html +++ b/src/interface/index.html @@ -5,8 +5,8 @@ BetterSEQTA+ Settings - -
+ +
\ No newline at end of file diff --git a/src/interface/pages/settings/general.svelte b/src/interface/pages/settings/general.svelte index faa95790..61db53ab 100644 --- a/src/interface/pages/settings/general.svelte +++ b/src/interface/pages/settings/general.svelte @@ -25,6 +25,7 @@ interface Plugin { pluginId: string; name: string; + description: string; settings: Record; } @@ -78,6 +79,23 @@ pluginSettings.forEach(plugin => { if (Object.keys(plugin.settings).length === 0) return; + // Add enable/disable toggle if plugin has disableToggle set + if ((plugin as any).disableToggle) { + entries.push({ + title: `Enable ${plugin.name}`, + description: `${plugin.description}`, + id: getPluginSettingId(plugin.pluginId, 'enabled'), + Component: Switch, + props: { + state: pluginSettingsValues[plugin.pluginId]?.enabled ?? true, + onChange: (value: boolean) => { + updatePluginSetting(plugin.pluginId, 'enabled', value); + // The plugin manager will handle the actual enabling/disabling + } + } + }); + } + Object.entries(plugin.settings).forEach(([key, setting]) => { const id = getPluginSettingId(plugin.pluginId, key); diff --git a/src/plugins/built-in/notificationCollector/index.ts b/src/plugins/built-in/notificationCollector/index.ts index 0fd297dc..880cb03a 100644 --- a/src/plugins/built-in/notificationCollector/index.ts +++ b/src/plugins/built-in/notificationCollector/index.ts @@ -1,29 +1,18 @@ import type { Plugin } from '../../core/types'; -import { BasePlugin, BooleanSetting } from '../../core/settings'; interface NotificationCollectorStorage { lastNotificationCount: number; lastCheckedTime: string; } -class NotificationCollectorPluginClass extends BasePlugin { - @BooleanSetting({ - default: true, - title: "Notification Collector", - description: "Uncaps the 9+ limit for notifications, showing the real number.", - }) - enabled!: boolean; -} - -// Create an instance to extract settings -const settingsInstance = new NotificationCollectorPluginClass(); - -const notificationCollectorPlugin: Plugin = { +const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = { id: 'notificationCollector', name: 'Notification Collector', description: 'Collects and displays SEQTA notifications', version: '1.0.0', - settings: settingsInstance.settings, + settings: {}, + disableToggle: true, + run: async (api) => { let pollInterval: number | null = null; @@ -80,30 +69,21 @@ const notificationCollectorPlugin: Plugin 9) { + alertDiv.textContent = "9+"; + } else { + alertDiv.textContent = api.storage.lastNotificationCount.toString(); + } } } }; - if (api.settings.enabled) { - api.seqta.onMount(".notifications__bubble___1EkSQ", (_) => { - startPolling(); - }); - } - - const enabledCallback = (value: any) => { - if (value) { - startPolling(); - } else { - stopPolling(); - } - }; - - api.settings.onChange('enabled', enabledCallback); + api.seqta.onMount(".notifications__bubble___1EkSQ", (_) => { + startPolling(); + }); return () => { stopPolling(); - api.settings.offChange('enabled', enabledCallback); }; } }; diff --git a/src/plugins/built-in/test/index.ts b/src/plugins/built-in/test/index.ts index 2788ac12..d77bc5c1 100644 --- a/src/plugins/built-in/test/index.ts +++ b/src/plugins/built-in/test/index.ts @@ -5,9 +5,9 @@ class TestPluginClass extends BasePlugin { @BooleanSetting({ default: true, title: "Test Plugin", - description: "A test plugin for BetterSEQTA+", + description: "Some random setting", }) - enabled!: boolean; + someSetting!: boolean; } const settingsInstance = new TestPluginClass(); @@ -22,9 +22,14 @@ const testPlugin: Plugin = { run: async (api) => { console.log('Test plugin running'); - api.seqta.onPageChange((page) => { + const { unregister } = api.seqta.onPageChange((page) => { console.log('Page changed to', page); }); + + return () => { + console.log('Test plugin stopped'); + unregister(); + } } }; diff --git a/src/plugins/built-in/timetable/index.ts b/src/plugins/built-in/timetable/index.ts index e3254d8a..af25fb85 100644 --- a/src/plugins/built-in/timetable/index.ts +++ b/src/plugins/built-in/timetable/index.ts @@ -2,53 +2,32 @@ import { settingsState } from '@/seqta/utils/listeners/SettingsState'; import type { Plugin } from '../../core/types'; import { convertTo12HourFormat } from '@/seqta/utils/convertTo12HourFormat'; import { waitForElm } from '@/seqta/utils/waitForElm'; -import { BasePlugin, BooleanSetting } from '../../core/settings'; -// Define only the typed settings - no need for redundant interface -class TimetablePluginClass extends BasePlugin { - @BooleanSetting({ - default: true, - title: "Timetable Enhancer", - description: "Adds extra features to the timetable view." - }) - enabled!: boolean; -} - -// Create an instance to extract settings -const settingsInstance = new TimetablePluginClass(); - -const timetablePlugin: Plugin = { +const timetablePlugin: Plugin<{}, {}> = { id: 'timetable', name: 'Timetable Enhancer', description: 'Adds extra features to the timetable view', version: '1.0.0', - settings: settingsInstance.settings, + settings: {}, + disableToggle: true, + run: async (api) => { - if (api.settings.enabled) { - api.seqta.onMount('.timetablepage', handleTimetable) - } - - const enabledCallback = (value: any) => { - if (value) { - api.seqta.onMount('.timetablepage', handleTimetable) - } else { - const timetablePage = document.querySelector('.timetablepage') - if (timetablePage) { - const zoomControls = document.querySelector('.timetable-zoom-controls') - if (zoomControls) zoomControls.remove() - - const hideControls = document.querySelector('.timetable-hide-controls') - if (hideControls) hideControls.remove() - - resetTimetableStyles() - } - } - } - - api.settings.onChange('enabled', enabledCallback) - + const { unregister } = api.seqta.onMount('.timetablepage', handleTimetable) + return () => { - api.settings.offChange('enabled', enabledCallback) + // Call the unregister function to remove the mount listener + unregister(); + + const timetablePage = document.querySelector('.timetablepage') + if (timetablePage) { + const zoomControls = document.querySelector('.timetable-zoom-controls') + if (zoomControls) zoomControls.remove() + + const hideControls = document.querySelector('.timetable-hide-controls') + if (hideControls) hideControls.remove() + + resetTimetableStyles() + } } } }; diff --git a/src/plugins/core/createAPI.ts b/src/plugins/core/createAPI.ts index b1e1cf7d..d24df593 100644 --- a/src/plugins/core/createAPI.ts +++ b/src/plugins/core/createAPI.ts @@ -6,7 +6,7 @@ import browser from 'webextension-polyfill'; function createSEQTAAPI(): SEQTAAPI { return { onMount: (selector, callback) => { - eventManager.register( + return eventManager.register( `${selector}Added`, { customCheck: (element) => element.matches(selector), @@ -22,11 +22,20 @@ function createSEQTAAPI(): SEQTAAPI { return path.split('/')[0]; }, onPageChange: (callback) => { - window.addEventListener('hashchange', () => { + const handler = () => { const page = window.location.hash.split('?page=/')[1] || ''; callback(page.split('/')[0]); - }); - }, + }; + + window.addEventListener('hashchange', handler); + + // Return an unregister function + return { + unregister: () => { + window.removeEventListener('hashchange', handler); + } + }; + } }; } diff --git a/src/plugins/core/manager.ts b/src/plugins/core/manager.ts index 78017c90..b329f8f3 100644 --- a/src/plugins/core/manager.ts +++ b/src/plugins/core/manager.ts @@ -1,5 +1,16 @@ import type { Plugin, PluginSettings } from './types'; import { createPluginAPI } from './createAPI'; +import browser from 'webextension-polyfill'; + +interface PluginSettingsStorage { + enabled?: boolean; + [key: string]: any; +} + +interface StorageChange { + oldValue?: T; + newValue?: T; +} export class PluginManager { private static instance: PluginManager; @@ -9,7 +20,9 @@ export class PluginManager { private cleanupFunctions: Map void> = new Map(); private listeners: Map void>> = new Map(); - private constructor() {} + private constructor() { + this.setupPluginStateListener(); + } public static getInstance(): PluginManager { if (!PluginManager.instance) { @@ -66,6 +79,17 @@ export class PluginManager { try { const api = createPluginAPI(plugin); + // Check if plugin is enabled before starting + if (plugin.disableToggle) { + const settings = await browser.storage.local.get(`plugin.${pluginId}.settings`); + const pluginSettings = settings[`plugin.${pluginId}.settings`] as PluginSettingsStorage | undefined; + const enabled = pluginSettings?.enabled ?? true; + if (!enabled) { + console.info(`Plugin "${pluginId}" is disabled, skipping initialization`); + return; + } + } + // Wait for both settings and storage to be loaded before starting the plugin await Promise.all([ (api.settings as any).loaded, @@ -145,9 +169,21 @@ export class PluginManager { }]; }); + if (plugin.disableToggle) { + settingsEntries.push([ + 'enabled', { + id: 'enabled', + title: plugin.name, + description: plugin.description, + type: 'boolean', + default: true + } + ]) + } return { pluginId: id, name: plugin.name, + description: plugin.description, settings: Object.fromEntries(settingsEntries) }; }); @@ -177,4 +213,36 @@ export class PluginManager { listeners.delete(callback); } } + + // Add handler for plugin enable/disable state changes + private async handlePluginStateChange(pluginId: string, enabled: boolean): Promise { + if (enabled) { + await this.startPlugin(pluginId); + } else { + await this.stopPlugin(pluginId); + } + } + + // Add listener for plugin settings changes + private setupPluginStateListener(): void { + browser.storage.onChanged.addListener((changes: { [key: string]: StorageChange }, area: string) => { + if (area !== 'local') return; + + for (const [key, change] of Object.entries(changes)) { + const match = key.match(/^plugin\.(.+)\.settings$/); + if (!match) continue; + + const pluginId = match[1]; + const plugin = this.plugins.get(pluginId); + if (!plugin?.disableToggle) continue; + + const enabled = (change.newValue as PluginSettingsStorage)?.enabled ?? true; + const wasEnabled = (change.oldValue as PluginSettingsStorage)?.enabled ?? true; + + if (enabled !== wasEnabled) { + this.handlePluginStateChange(pluginId, enabled); + } + } + }); + } } \ No newline at end of file diff --git a/src/plugins/core/types.ts b/src/plugins/core/types.ts index 269df35b..097d232f 100644 --- a/src/plugins/core/types.ts +++ b/src/plugins/core/types.ts @@ -56,10 +56,10 @@ export type SettingsAPI = { } export interface SEQTAAPI { - onMount: (selector: string, callback: (element: Element) => void) => void; + onMount: (selector: string, callback: (element: Element) => void) => { unregister: () => void }; getFiber: (selector: string) => ReactFiber; getCurrentPage: () => string; - onPageChange: (callback: (page: string) => void) => void; + onPageChange: (callback: (page: string) => void) => { unregister: () => void }; } export interface StorageAPI { @@ -101,5 +101,6 @@ export interface Plugin { description: string; version: string; settings: T; - run: (api: PluginAPI) => void | Promise | (() => void) | Promise<() => void>; + disableToggle?: boolean; // Optional flag to show/hide the plugin's enable/disable toggle in settings + run: (api: PluginAPI) => void | Promise | (() => void) | Promise<(() => void)>; } \ No newline at end of file diff --git a/src/seqta/utils/CreateEnable/EnableNotificationCollector.ts b/src/seqta/utils/CreateEnable/EnableNotificationCollector.ts deleted file mode 100644 index 8477d668..00000000 --- a/src/seqta/utils/CreateEnable/EnableNotificationCollector.ts +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 10edf229..00000000 --- a/src/seqta/utils/DisableRemove/DisableNotificationCollector.ts +++ /dev/null @@ -1,13 +0,0 @@ -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/Loaders/LoadHomePage.ts b/src/seqta/utils/Loaders/LoadHomePage.ts index 167f1a28..c3664fe1 100644 --- a/src/seqta/utils/Loaders/LoadHomePage.ts +++ b/src/seqta/utils/Loaders/LoadHomePage.ts @@ -19,8 +19,6 @@ 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() @@ -249,10 +247,6 @@ export async function loadHomePage() { } } - if (settingsState.notificationcollector) { - enableNotificationCollector() - } - return cleanup } diff --git a/src/seqta/utils/listeners/StorageChanges.ts b/src/seqta/utils/listeners/StorageChanges.ts index c61485fe..5ccf07ca 100644 --- a/src/seqta/utils/listeners/StorageChanges.ts +++ b/src/seqta/utils/listeners/StorageChanges.ts @@ -5,8 +5,6 @@ import { updateAllColors } from '@/seqta/ui/colors/Manager'; 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"; @@ -27,7 +25,6 @@ export class StorageChangeHandler { settingsState.register('onoff', this.handleOnOffChange.bind(this)); settingsState.register('shortcuts', this.handleShortcutsChange.bind(this)); settingsState.register('customshortcuts', this.handleCustomShortcutsChange.bind(this)); - settingsState.register('notificationcollector', this.handleNotificationCollectorChange.bind(this)); settingsState.register('bksliderinput', updateBgDurations.bind(this)); settingsState.register('animatedbk', this.handleAnimatedBkChange.bind(this)); settingsState.register('transparencyEffects', this.handleTransparencyEffectsChange.bind(this)); @@ -43,14 +40,6 @@ export class StorageChangeHandler { browser.runtime.sendMessage({ type: 'reloadTabs' }); } - private handleNotificationCollectorChange(newValue: boolean) { - if (newValue) { - enableNotificationCollector(); - } else { - disableNotificationCollector(); - } - } - private handleCustomShortcutsChange(newValue: CustomShortcut[], oldValue: CustomShortcut[]) { if (newValue) { if (newValue.length > oldValue.length) { diff --git a/src/types/storage.ts b/src/types/storage.ts index ee42560d..fd3a7ffb 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -25,7 +25,6 @@ export interface SettingsState { welcome: ToggleItem; }; menuorder: any[]; - notificationcollector: boolean; onoff: boolean; selectedColor: string; originalSelectedColor: string;