feat: add global theme toggle

This commit is contained in:
SethBurkart123
2025-03-30 08:49:13 +11:00
parent 6147e96cc9
commit 3ecd7205ed
14 changed files with 145 additions and 141 deletions
-1
View File
@@ -65,7 +65,6 @@ const DefaultValues: SettingsState = {
bksliderinput: "50", bksliderinput: "50",
transparencyEffects: false, transparencyEffects: false,
lessonalert: true, lessonalert: true,
notificationcollector: true,
defaultmenuorder: [], defaultmenuorder: [],
menuitems: { menuitems: {
assessments: { toggle: true }, assessments: { toggle: true },
+2 -2
View File
@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BetterSEQTA+ Settings</title> <title>BetterSEQTA+ Settings</title>
</head> </head>
<body> <body class="h-[600px]">
<div id="app"></div> <div id="app" style="height: 100%;"></div>
<script type="module" src="./index.ts"></script> <script type="module" src="./index.ts"></script>
</body> </body>
</html> </html>
@@ -25,6 +25,7 @@
interface Plugin { interface Plugin {
pluginId: string; pluginId: string;
name: string; name: string;
description: string;
settings: Record<string, PluginSetting>; settings: Record<string, PluginSetting>;
} }
@@ -78,6 +79,23 @@
pluginSettings.forEach(plugin => { pluginSettings.forEach(plugin => {
if (Object.keys(plugin.settings).length === 0) return; 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]) => { Object.entries(plugin.settings).forEach(([key, setting]) => {
const id = getPluginSettingId(plugin.pluginId, key); const id = getPluginSettingId(plugin.pluginId, key);
@@ -1,29 +1,18 @@
import type { Plugin } from '../../core/types'; import type { Plugin } from '../../core/types';
import { BasePlugin, BooleanSetting } from '../../core/settings';
interface NotificationCollectorStorage { interface NotificationCollectorStorage {
lastNotificationCount: number; lastNotificationCount: number;
lastCheckedTime: string; lastCheckedTime: string;
} }
class NotificationCollectorPluginClass extends BasePlugin { const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
@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<typeof settingsInstance.settings, NotificationCollectorStorage> = {
id: 'notificationCollector', id: 'notificationCollector',
name: 'Notification Collector', name: 'Notification Collector',
description: 'Collects and displays SEQTA notifications', description: 'Collects and displays SEQTA notifications',
version: '1.0.0', version: '1.0.0',
settings: settingsInstance.settings, settings: {},
disableToggle: true,
run: async (api) => { run: async (api) => {
let pollInterval: number | null = null; let pollInterval: number | null = null;
@@ -80,30 +69,21 @@ const notificationCollectorPlugin: Plugin<typeof settingsInstance.settings, Noti
pollInterval = null; pollInterval = null;
const alertDiv = document.querySelector(".notifications__bubble___1EkSQ") as HTMLElement; const alertDiv = document.querySelector(".notifications__bubble___1EkSQ") as HTMLElement;
if (alertDiv) { if (alertDiv) {
alertDiv.textContent = "9+"; if (api.storage.lastNotificationCount > 9) {
alertDiv.textContent = "9+";
} else {
alertDiv.textContent = api.storage.lastNotificationCount.toString();
}
} }
} }
}; };
if (api.settings.enabled) { api.seqta.onMount(".notifications__bubble___1EkSQ", (_) => {
api.seqta.onMount(".notifications__bubble___1EkSQ", (_) => { startPolling();
startPolling(); });
});
}
const enabledCallback = (value: any) => {
if (value) {
startPolling();
} else {
stopPolling();
}
};
api.settings.onChange('enabled', enabledCallback);
return () => { return () => {
stopPolling(); stopPolling();
api.settings.offChange('enabled', enabledCallback);
}; };
} }
}; };
+8 -3
View File
@@ -5,9 +5,9 @@ class TestPluginClass extends BasePlugin {
@BooleanSetting({ @BooleanSetting({
default: true, default: true,
title: "Test Plugin", title: "Test Plugin",
description: "A test plugin for BetterSEQTA+", description: "Some random setting",
}) })
enabled!: boolean; someSetting!: boolean;
} }
const settingsInstance = new TestPluginClass(); const settingsInstance = new TestPluginClass();
@@ -22,9 +22,14 @@ const testPlugin: Plugin<typeof settingsInstance.settings> = {
run: async (api) => { run: async (api) => {
console.log('Test plugin running'); console.log('Test plugin running');
api.seqta.onPageChange((page) => { const { unregister } = api.seqta.onPageChange((page) => {
console.log('Page changed to', page); console.log('Page changed to', page);
}); });
return () => {
console.log('Test plugin stopped');
unregister();
}
} }
}; };
+19 -40
View File
@@ -2,53 +2,32 @@ import { settingsState } from '@/seqta/utils/listeners/SettingsState';
import type { Plugin } from '../../core/types'; import type { Plugin } from '../../core/types';
import { convertTo12HourFormat } from '@/seqta/utils/convertTo12HourFormat'; import { convertTo12HourFormat } from '@/seqta/utils/convertTo12HourFormat';
import { waitForElm } from '@/seqta/utils/waitForElm'; import { waitForElm } from '@/seqta/utils/waitForElm';
import { BasePlugin, BooleanSetting } from '../../core/settings';
// Define only the typed settings - no need for redundant interface const timetablePlugin: Plugin<{}, {}> = {
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<typeof settingsInstance.settings> = {
id: 'timetable', id: 'timetable',
name: 'Timetable Enhancer', name: 'Timetable Enhancer',
description: 'Adds extra features to the timetable view', description: 'Adds extra features to the timetable view',
version: '1.0.0', version: '1.0.0',
settings: settingsInstance.settings, settings: {},
disableToggle: true,
run: async (api) => { run: async (api) => {
if (api.settings.enabled) { const { unregister } = api.seqta.onMount('.timetablepage', handleTimetable)
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)
return () => { 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()
}
} }
} }
}; };
+13 -4
View File
@@ -6,7 +6,7 @@ import browser from 'webextension-polyfill';
function createSEQTAAPI(): SEQTAAPI { function createSEQTAAPI(): SEQTAAPI {
return { return {
onMount: (selector, callback) => { onMount: (selector, callback) => {
eventManager.register( return eventManager.register(
`${selector}Added`, `${selector}Added`,
{ {
customCheck: (element) => element.matches(selector), customCheck: (element) => element.matches(selector),
@@ -22,11 +22,20 @@ function createSEQTAAPI(): SEQTAAPI {
return path.split('/')[0]; return path.split('/')[0];
}, },
onPageChange: (callback) => { onPageChange: (callback) => {
window.addEventListener('hashchange', () => { const handler = () => {
const page = window.location.hash.split('?page=/')[1] || ''; const page = window.location.hash.split('?page=/')[1] || '';
callback(page.split('/')[0]); callback(page.split('/')[0]);
}); };
},
window.addEventListener('hashchange', handler);
// Return an unregister function
return {
unregister: () => {
window.removeEventListener('hashchange', handler);
}
};
}
}; };
} }
+69 -1
View File
@@ -1,5 +1,16 @@
import type { Plugin, PluginSettings } from './types'; import type { Plugin, PluginSettings } from './types';
import { createPluginAPI } from './createAPI'; import { createPluginAPI } from './createAPI';
import browser from 'webextension-polyfill';
interface PluginSettingsStorage {
enabled?: boolean;
[key: string]: any;
}
interface StorageChange<T = any> {
oldValue?: T;
newValue?: T;
}
export class PluginManager { export class PluginManager {
private static instance: PluginManager; private static instance: PluginManager;
@@ -9,7 +20,9 @@ export class PluginManager {
private cleanupFunctions: Map<string, () => void> = new Map(); private cleanupFunctions: Map<string, () => void> = new Map();
private listeners: Map<string, Set<(...args: any[]) => void>> = new Map(); private listeners: Map<string, Set<(...args: any[]) => void>> = new Map();
private constructor() {} private constructor() {
this.setupPluginStateListener();
}
public static getInstance(): PluginManager { public static getInstance(): PluginManager {
if (!PluginManager.instance) { if (!PluginManager.instance) {
@@ -66,6 +79,17 @@ export class PluginManager {
try { try {
const api = createPluginAPI(plugin); 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 // Wait for both settings and storage to be loaded before starting the plugin
await Promise.all([ await Promise.all([
(api.settings as any).loaded, (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 { return {
pluginId: id, pluginId: id,
name: plugin.name, name: plugin.name,
description: plugin.description,
settings: Object.fromEntries(settingsEntries) settings: Object.fromEntries(settingsEntries)
}; };
}); });
@@ -177,4 +213,36 @@ export class PluginManager {
listeners.delete(callback); listeners.delete(callback);
} }
} }
// Add handler for plugin enable/disable state changes
private async handlePluginStateChange(pluginId: string, enabled: boolean): Promise<void> {
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);
}
}
});
}
} }
+4 -3
View File
@@ -56,10 +56,10 @@ export type SettingsAPI<T extends PluginSettings> = {
} }
export interface SEQTAAPI { export interface SEQTAAPI {
onMount: (selector: string, callback: (element: Element) => void) => void; onMount: (selector: string, callback: (element: Element) => void) => { unregister: () => void };
getFiber: (selector: string) => ReactFiber; getFiber: (selector: string) => ReactFiber;
getCurrentPage: () => string; getCurrentPage: () => string;
onPageChange: (callback: (page: string) => void) => void; onPageChange: (callback: (page: string) => void) => { unregister: () => void };
} }
export interface StorageAPI<T = any> { export interface StorageAPI<T = any> {
@@ -101,5 +101,6 @@ export interface Plugin<T extends PluginSettings = PluginSettings, S = any> {
description: string; description: string;
version: string; version: string;
settings: T; settings: T;
run: (api: PluginAPI<T, S>) => void | Promise<void> | (() => void) | Promise<() => void>; disableToggle?: boolean; // Optional flag to show/hide the plugin's enable/disable toggle in settings
run: (api: PluginAPI<T, S>) => void | Promise<void> | (() => void) | Promise<(() => void)>;
} }
@@ -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",
}),
)
}
@@ -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+"
}
}
}
-6
View File
@@ -19,8 +19,6 @@ import { CreateElement } from "@/seqta/utils/CreateEnable/CreateElement"
import { convertTo12HourFormat } from "../convertTo12HourFormat" import { convertTo12HourFormat } from "../convertTo12HourFormat"
import { enableNotificationCollector } from "@/seqta/utils/CreateEnable/EnableNotificationCollector"
let LessonInterval: any let LessonInterval: any
let currentSelectedDate = new Date() let currentSelectedDate = new Date()
@@ -249,10 +247,6 @@ export async function loadHomePage() {
} }
} }
if (settingsState.notificationcollector) {
enableNotificationCollector()
}
return cleanup return cleanup
} }
@@ -5,8 +5,6 @@ import { updateAllColors } from '@/seqta/ui/colors/Manager';
import { addShortcuts } from "@/seqta/utils/Adders/AddShortcuts"; import { addShortcuts } from "@/seqta/utils/Adders/AddShortcuts";
import { CreateBackground } from "@/seqta/utils/CreateEnable/CreateBackground"; import { CreateBackground } from "@/seqta/utils/CreateEnable/CreateBackground";
import { CreateCustomShortcutDiv } from "@/seqta/utils/CreateEnable/CreateCustomShortcutDiv"; 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 { FilterUpcomingAssessments } from "@/seqta/utils/FilterUpcomingAssessments";
import { RemoveBackground } from "@/seqta/utils/DisableRemove/RemoveBackground"; import { RemoveBackground } from "@/seqta/utils/DisableRemove/RemoveBackground";
import { RemoveShortcutDiv } from "@/seqta/utils/DisableRemove/RemoveShortcutDiv"; import { RemoveShortcutDiv } from "@/seqta/utils/DisableRemove/RemoveShortcutDiv";
@@ -27,7 +25,6 @@ export class StorageChangeHandler {
settingsState.register('onoff', this.handleOnOffChange.bind(this)); settingsState.register('onoff', this.handleOnOffChange.bind(this));
settingsState.register('shortcuts', this.handleShortcutsChange.bind(this)); settingsState.register('shortcuts', this.handleShortcutsChange.bind(this));
settingsState.register('customshortcuts', this.handleCustomShortcutsChange.bind(this)); settingsState.register('customshortcuts', this.handleCustomShortcutsChange.bind(this));
settingsState.register('notificationcollector', this.handleNotificationCollectorChange.bind(this));
settingsState.register('bksliderinput', updateBgDurations.bind(this)); settingsState.register('bksliderinput', updateBgDurations.bind(this));
settingsState.register('animatedbk', this.handleAnimatedBkChange.bind(this)); settingsState.register('animatedbk', this.handleAnimatedBkChange.bind(this));
settingsState.register('transparencyEffects', this.handleTransparencyEffectsChange.bind(this)); settingsState.register('transparencyEffects', this.handleTransparencyEffectsChange.bind(this));
@@ -43,14 +40,6 @@ export class StorageChangeHandler {
browser.runtime.sendMessage({ type: 'reloadTabs' }); browser.runtime.sendMessage({ type: 'reloadTabs' });
} }
private handleNotificationCollectorChange(newValue: boolean) {
if (newValue) {
enableNotificationCollector();
} else {
disableNotificationCollector();
}
}
private handleCustomShortcutsChange(newValue: CustomShortcut[], oldValue: CustomShortcut[]) { private handleCustomShortcutsChange(newValue: CustomShortcut[], oldValue: CustomShortcut[]) {
if (newValue) { if (newValue) {
if (newValue.length > oldValue.length) { if (newValue.length > oldValue.length) {
-1
View File
@@ -25,7 +25,6 @@ export interface SettingsState {
welcome: ToggleItem; welcome: ToggleItem;
}; };
menuorder: any[]; menuorder: any[];
notificationcollector: boolean;
onoff: boolean; onoff: boolean;
selectedColor: string; selectedColor: string;
originalSelectedColor: string; originalSelectedColor: string;