mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
Merge branch 'en-masse-upgrade' of https://github.com/BetterSEQTA/BetterSEQTA-Plus into en-masse-upgrade
This commit is contained in:
@@ -11,6 +11,105 @@
|
|||||||
import PickerSwatch from "@/interface/components/PickerSwatch.svelte"
|
import PickerSwatch from "@/interface/components/PickerSwatch.svelte"
|
||||||
import hideSensitiveContent from "@/seqta/ui/dev/hideSensitiveContent"
|
import hideSensitiveContent from "@/seqta/ui/dev/hideSensitiveContent"
|
||||||
|
|
||||||
|
import { getAllPluginSettings } from "@/plugins"
|
||||||
|
|
||||||
|
interface PluginSetting {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
type: string;
|
||||||
|
default: any;
|
||||||
|
options?: Array<{value: string, label: string}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Plugin {
|
||||||
|
pluginId: string;
|
||||||
|
name: string;
|
||||||
|
settings: Record<string, PluginSetting>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginSettings = getAllPluginSettings() as Plugin[];
|
||||||
|
const pluginSettingsValues = $state<Record<string, Record<string, any>>>({});
|
||||||
|
let nextPluginSettingId = 1000;
|
||||||
|
const pluginSettingMap = new Map<number, {pluginId: string, settingKey: string}>();
|
||||||
|
|
||||||
|
function getPluginSettingId(pluginId: string, settingKey: string): number {
|
||||||
|
const id = nextPluginSettingId++;
|
||||||
|
pluginSettingMap.set(id, {pluginId, settingKey});
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPluginSettings() {
|
||||||
|
for (const plugin of pluginSettings) {
|
||||||
|
if (Object.keys(plugin.settings).length === 0) continue;
|
||||||
|
|
||||||
|
const storageKey = `plugin.${plugin.pluginId}.settings`;
|
||||||
|
const stored = await browser.storage.local.get(storageKey);
|
||||||
|
|
||||||
|
pluginSettingsValues[plugin.pluginId] = stored[storageKey] || {};
|
||||||
|
|
||||||
|
for (const [key, setting] of Object.entries(plugin.settings)) {
|
||||||
|
if (pluginSettingsValues[plugin.pluginId][key] === undefined) {
|
||||||
|
pluginSettingsValues[plugin.pluginId][key] = setting.default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePluginSetting(pluginId: string, key: string, value: any) {
|
||||||
|
const storageKey = `plugin.${pluginId}.settings`;
|
||||||
|
|
||||||
|
if (!pluginSettingsValues[pluginId]) {
|
||||||
|
pluginSettingsValues[pluginId] = {};
|
||||||
|
}
|
||||||
|
pluginSettingsValues[pluginId][key] = value;
|
||||||
|
|
||||||
|
const stored = await browser.storage.local.get(storageKey);
|
||||||
|
const currentSettings = (stored[storageKey] || {}) as Record<string, any>;
|
||||||
|
|
||||||
|
currentSettings[key] = value;
|
||||||
|
|
||||||
|
await browser.storage.local.set({ [storageKey]: currentSettings });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPluginSettingEntries() {
|
||||||
|
const entries: any[] = [];
|
||||||
|
|
||||||
|
pluginSettings.forEach(plugin => {
|
||||||
|
if (Object.keys(plugin.settings).length === 0) return;
|
||||||
|
|
||||||
|
Object.entries(plugin.settings).forEach(([key, setting]) => {
|
||||||
|
const id = getPluginSettingId(plugin.pluginId, key);
|
||||||
|
|
||||||
|
entries.push({
|
||||||
|
title: setting.title || key,
|
||||||
|
description: setting.description || '',
|
||||||
|
id,
|
||||||
|
Component: setting.type === 'boolean' ? Switch :
|
||||||
|
setting.type === 'select' ? Select :
|
||||||
|
setting.type === 'number' ? Slider :
|
||||||
|
setting.type === 'string' ? (setting.options ? Select : null) : Switch,
|
||||||
|
props: {
|
||||||
|
state: pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default,
|
||||||
|
onChange: (value: any) => {
|
||||||
|
if (setting.type === 'number' && typeof value === 'string') {
|
||||||
|
value = parseFloat(value);
|
||||||
|
}
|
||||||
|
updatePluginSetting(plugin.pluginId, key, value);
|
||||||
|
},
|
||||||
|
options: setting.options
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
loadPluginSettings();
|
||||||
|
})
|
||||||
|
|
||||||
const { showColourPicker } = $props<{ showColourPicker: () => void }>();
|
const { showColourPicker } = $props<{ showColourPicker: () => void }>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -28,7 +127,6 @@
|
|||||||
|
|
||||||
<div class="flex flex-col divide-y divide-zinc-100 dark:divide-zinc-700">
|
<div class="flex flex-col divide-y divide-zinc-100 dark:divide-zinc-700">
|
||||||
{#each [
|
{#each [
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "Transparency Effects",
|
title: "Transparency Effects",
|
||||||
description: "Enables transparency effects on certain elements such as blur. (May impact battery life)",
|
description: "Enables transparency effects on certain elements such as blur. (May impact battery life)",
|
||||||
@@ -88,16 +186,6 @@
|
|||||||
onChange: (isOn: boolean) => settingsState.animations = isOn
|
onChange: (isOn: boolean) => settingsState.animations = isOn
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: "Notification Collector",
|
|
||||||
description: "Uncaps the 9+ limit for notifications, showing the real number.",
|
|
||||||
id: 7,
|
|
||||||
Component: Switch,
|
|
||||||
props: {
|
|
||||||
state: $settingsState.notificationcollector,
|
|
||||||
onChange: (isOn: boolean) => settingsState.notificationcollector = isOn
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "Assessment Average",
|
title: "Assessment Average",
|
||||||
description: "Shows your subject average for assessments.",
|
description: "Shows your subject average for assessments.",
|
||||||
@@ -179,6 +267,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
...getPluginSettingEntries(),
|
||||||
{
|
{
|
||||||
title: "BetterSEQTA+",
|
title: "BetterSEQTA+",
|
||||||
description: "Enables BetterSEQTA+ features",
|
description: "Enables BetterSEQTA+ features",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
|
|
||||||
import type { Plugin, PluginSettings } from '../../core/types';
|
import type { Plugin, PluginSettings } from '../../core/types';
|
||||||
|
|
||||||
interface NotificationCollectorSettings extends PluginSettings {
|
interface NotificationCollectorSettings extends PluginSettings {
|
||||||
@@ -53,29 +52,29 @@ const notificationCollectorPlugin: Plugin<NotificationCollectorSettings> = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start polling when enabled
|
|
||||||
const startPolling = () => {
|
const startPolling = () => {
|
||||||
if (pollInterval) return; // Already polling
|
if (pollInterval) return; // Already polling
|
||||||
checkNotifications();
|
checkNotifications();
|
||||||
pollInterval = window.setInterval(checkNotifications, 30000);
|
pollInterval = window.setInterval(checkNotifications, 30000);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Stop polling when disabled
|
|
||||||
const stopPolling = () => {
|
const stopPolling = () => {
|
||||||
if (pollInterval) {
|
if (pollInterval) {
|
||||||
window.clearInterval(pollInterval);
|
window.clearInterval(pollInterval);
|
||||||
pollInterval = null;
|
pollInterval = null;
|
||||||
|
const alertDiv = document.querySelector(".notifications__bubble___1EkSQ") as HTMLElement;
|
||||||
|
if (alertDiv) {
|
||||||
|
alertDiv.textContent = "9+";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start/stop based on initial enabled state
|
if (api.settings.enabled) {
|
||||||
if (settingsState.notificationcollector) {
|
|
||||||
api.seqta.onMount(".notifications__bubble___1EkSQ", (_) => {
|
api.seqta.onMount(".notifications__bubble___1EkSQ", (_) => {
|
||||||
startPolling();
|
startPolling();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store callbacks for cleanup
|
|
||||||
const enabledCallback = (enabled: boolean) => {
|
const enabledCallback = (enabled: boolean) => {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
startPolling();
|
startPolling();
|
||||||
@@ -84,10 +83,8 @@ const notificationCollectorPlugin: Plugin<NotificationCollectorSettings> = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle settings changes
|
|
||||||
api.settings.onChange('enabled', enabledCallback);
|
api.settings.onChange('enabled', enabledCallback);
|
||||||
|
|
||||||
// Return cleanup function
|
|
||||||
return () => {
|
return () => {
|
||||||
stopPolling();
|
stopPolling();
|
||||||
api.settings.offChange('enabled', enabledCallback);
|
api.settings.offChange('enabled', enabledCallback);
|
||||||
|
|||||||
@@ -1,20 +1,112 @@
|
|||||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
|
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
|
||||||
import type { Plugin } from '../../core/types';
|
import type { Plugin, PluginSettings } 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';
|
||||||
|
|
||||||
const timetablePlugin: Plugin = {
|
interface TimetableSettings extends PluginSettings {
|
||||||
|
enabled: {
|
||||||
|
type: 'boolean';
|
||||||
|
default: boolean;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const timetablePlugin: Plugin<TimetableSettings> = {
|
||||||
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: {},
|
settings: {
|
||||||
|
enabled: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
title: 'Timetable Enhancer',
|
||||||
|
description: 'Adds extra features to the timetable view.',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
run: async (api) => {
|
run: async (api) => {
|
||||||
|
if (api.settings.enabled) {
|
||||||
api.seqta.onMount('.timetablepage', handleTimetable)
|
api.seqta.onMount('.timetablepage', handleTimetable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const enabledCallback = (enabled: boolean) => {
|
||||||
|
if (enabled) {
|
||||||
|
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 () => {
|
||||||
|
api.settings.offChange('enabled', enabledCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Store event handlers globally for cleanup
|
||||||
|
const zoomHandlers = new WeakMap<Element, { zoomIn: () => void; zoomOut: () => void }>()
|
||||||
|
|
||||||
|
function resetTimetableStyles(): void {
|
||||||
|
const firstDayColumn = document.querySelector(".dailycal .content .days td") as HTMLElement
|
||||||
|
if (!firstDayColumn) return
|
||||||
|
|
||||||
|
const baseContainerHeight = parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight
|
||||||
|
|
||||||
|
const dayColumns = document.querySelectorAll(".dailycal .content .days td")
|
||||||
|
dayColumns.forEach((td: Element) => {
|
||||||
|
(td as HTMLElement).style.height = `${baseContainerHeight}px`
|
||||||
|
})
|
||||||
|
|
||||||
|
const timeColumn = document.querySelector(".times")
|
||||||
|
if (timeColumn) {
|
||||||
|
const times = timeColumn.querySelectorAll(".time")
|
||||||
|
const timeHeight = baseContainerHeight / times.length
|
||||||
|
times.forEach((time: Element) => {
|
||||||
|
(time as HTMLElement).style.height = `${timeHeight}px`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const lessons = document.querySelectorAll(".dailycal .lesson")
|
||||||
|
lessons.forEach((lesson: Element) => {
|
||||||
|
const lessonEl = lesson as HTMLElement
|
||||||
|
const originalHeight = lessonEl.getAttribute('data-original-height')
|
||||||
|
if (originalHeight) {
|
||||||
|
lessonEl.style.height = `${originalHeight}px`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const entries = document.querySelectorAll(".entry")
|
||||||
|
entries.forEach((entry: Element) => {
|
||||||
|
const entryEl = entry as HTMLElement
|
||||||
|
entryEl.style.opacity = '1'
|
||||||
|
})
|
||||||
|
|
||||||
|
const zoomControls = document.querySelector('.timetable-zoom-controls')
|
||||||
|
if (zoomControls) {
|
||||||
|
const handlers = zoomHandlers.get(zoomControls)
|
||||||
|
if (handlers) {
|
||||||
|
const zoomIn = zoomControls.querySelector('.timetable-zoom:nth-child(2)')
|
||||||
|
const zoomOut = zoomControls.querySelector('.timetable-zoom:nth-child(1)')
|
||||||
|
if (zoomIn) zoomIn.removeEventListener('click', handlers.zoomIn)
|
||||||
|
if (zoomOut) zoomOut.removeEventListener('click', handlers.zoomOut)
|
||||||
|
zoomHandlers.delete(zoomControls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleTimetable(): Promise<void> {
|
async function handleTimetable(): Promise<void> {
|
||||||
await waitForElm(".time", true, 10)
|
await waitForElm(".time", true, 10)
|
||||||
|
|
||||||
@@ -58,11 +150,11 @@ function handleTimetableZoom(): void {
|
|||||||
|
|
||||||
const zoomIn = document.createElement("button")
|
const zoomIn = document.createElement("button")
|
||||||
zoomIn.className = "uiButton timetable-zoom iconFamily"
|
zoomIn.className = "uiButton timetable-zoom iconFamily"
|
||||||
zoomIn.innerHTML = "" // Using unicode for zoom in icon
|
zoomIn.innerHTML = "" // Unicode for zoom in icon (custom iconfamily)
|
||||||
|
|
||||||
const zoomOut = document.createElement("button")
|
const zoomOut = document.createElement("button")
|
||||||
zoomOut.className = "uiButton timetable-zoom iconFamily"
|
zoomOut.className = "uiButton timetable-zoom iconFamily"
|
||||||
zoomOut.innerHTML = "" // Using unicode for zoom out icon
|
zoomOut.innerHTML = "" // Unicode for zoom out icon (custom iconfamily)
|
||||||
|
|
||||||
zoomControls.appendChild(zoomOut)
|
zoomControls.appendChild(zoomOut)
|
||||||
zoomControls.appendChild(zoomIn)
|
zoomControls.appendChild(zoomIn)
|
||||||
@@ -70,6 +162,27 @@ function handleTimetableZoom(): void {
|
|||||||
const toolbar = document.getElementById("toolbar")
|
const toolbar = document.getElementById("toolbar")
|
||||||
toolbar?.appendChild(zoomControls)
|
toolbar?.appendChild(zoomControls)
|
||||||
|
|
||||||
|
// Store event listener references
|
||||||
|
const zoomInHandler = () => {
|
||||||
|
if (timetableZoomLevel < 2) {
|
||||||
|
timetableZoomLevel += 0.2
|
||||||
|
updateZoom()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const zoomOutHandler = () => {
|
||||||
|
if (timetableZoomLevel > 0.6) {
|
||||||
|
timetableZoomLevel -= 0.2
|
||||||
|
updateZoom()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zoomIn.addEventListener("click", zoomInHandler)
|
||||||
|
zoomOut.addEventListener("click", zoomOutHandler)
|
||||||
|
|
||||||
|
// Store references for cleanup
|
||||||
|
zoomHandlers.set(zoomControls, { zoomIn: zoomInHandler, zoomOut: zoomOutHandler })
|
||||||
|
|
||||||
const initializePositions = () => {
|
const initializePositions = () => {
|
||||||
// Get the base container height from the first TD
|
// Get the base container height from the first TD
|
||||||
const firstDayColumn = document.querySelector(
|
const firstDayColumn = document.querySelector(
|
||||||
@@ -147,20 +260,6 @@ function handleTimetableZoom(): void {
|
|||||||
block: "center",
|
block: "center",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
zoomIn.addEventListener("click", () => {
|
|
||||||
if (timetableZoomLevel < 2) {
|
|
||||||
timetableZoomLevel += 0.2
|
|
||||||
updateZoom()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
zoomOut.addEventListener("click", () => {
|
|
||||||
if (timetableZoomLevel > 0.6) {
|
|
||||||
timetableZoomLevel -= 0.2
|
|
||||||
updateZoom()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTimetableAssessmentHide(): void {
|
function handleTimetableAssessmentHide(): void {
|
||||||
|
|||||||
+128
-15
@@ -30,7 +30,7 @@ function createSEQTAAPI(): SEQTAAPI {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): SettingsAPI<T> {
|
function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): SettingsAPI<T> & { loaded: Promise<void> } {
|
||||||
const storageKey = `plugin.${plugin.id}.settings`;
|
const storageKey = `plugin.${plugin.id}.settings`;
|
||||||
const listeners = new Map<keyof T, Set<(value: any) => void>>();
|
const listeners = new Map<keyof T, Set<(value: any) => void>>();
|
||||||
let settings: { [K in keyof T]: T[K]['default'] };
|
let settings: { [K in keyof T]: T[K]['default'] };
|
||||||
@@ -41,10 +41,35 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
|
|||||||
return acc;
|
return acc;
|
||||||
}, {} as { [K in keyof T]: T[K]['default'] });
|
}, {} as { [K in keyof T]: T[K]['default'] });
|
||||||
|
|
||||||
// Load saved settings
|
// Create a promise that resolves when settings are loaded
|
||||||
browser.storage.local.get(storageKey).then((stored) => {
|
const loaded = (async () => {
|
||||||
|
try {
|
||||||
|
const stored = await browser.storage.local.get(storageKey);
|
||||||
if (stored[storageKey]) {
|
if (stored[storageKey]) {
|
||||||
Object.assign(settings, stored[storageKey]);
|
Object.entries(stored[storageKey]).forEach(([key, value]) => {
|
||||||
|
if (key in settings) {
|
||||||
|
settings[key as keyof T] = value as any;
|
||||||
|
// Notify any listeners that might have been registered already
|
||||||
|
listeners.get(key as keyof T)?.forEach(callback => callback(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[BetterSEQTA+] Error loading settings for plugin ${plugin.id}:`, error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Listen for storage changes
|
||||||
|
browser.storage.onChanged.addListener((changes, area) => {
|
||||||
|
if (area === 'local' && changes[storageKey]) {
|
||||||
|
const newValue = changes[storageKey].newValue;
|
||||||
|
if (newValue) {
|
||||||
|
// Update settings and notify listeners
|
||||||
|
Object.entries(newValue).forEach(([key, value]) => {
|
||||||
|
settings[key as keyof T] = value as any;
|
||||||
|
listeners.get(key as keyof T)?.forEach(callback => callback(value));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -59,33 +84,121 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
|
|||||||
listeners.get(key)!.add(callback);
|
listeners.get(key)!.add(callback);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (prop === 'offChange') {
|
||||||
|
return (key: keyof T, callback: (value: any) => void) => {
|
||||||
|
listeners.get(key)?.delete(callback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (prop === 'loaded') {
|
||||||
|
return loaded;
|
||||||
|
}
|
||||||
return target[prop as keyof T];
|
return target[prop as keyof T];
|
||||||
},
|
},
|
||||||
set(target, prop: string, value: any) {
|
set(target, prop: string, value: any) {
|
||||||
if (prop === 'onChange') return false;
|
if (prop === 'onChange' || prop === 'offChange' || prop === 'loaded') return false;
|
||||||
target[prop as keyof T] = value;
|
target[prop as keyof T] = value;
|
||||||
browser.storage.local.set({ [storageKey]: target });
|
|
||||||
|
// Store all settings under the plugin's settings key
|
||||||
|
browser.storage.local.set({
|
||||||
|
[storageKey]: target
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notify listeners
|
||||||
listeners.get(prop as keyof T)?.forEach(callback => callback(value));
|
listeners.get(prop as keyof T)?.forEach(callback => callback(value));
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
}) as SettingsAPI<T>;
|
}) as SettingsAPI<T> & { loaded: Promise<void> };
|
||||||
|
|
||||||
return proxy;
|
return proxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createStorageAPI(pluginId: string): StorageAPI {
|
function createStorageAPI(pluginId: string): StorageAPI {
|
||||||
const prefix = `plugin.${pluginId}.storage.`;
|
const prefix = `plugin.${pluginId}.storage.`;
|
||||||
|
const cache: Record<string, any> = {};
|
||||||
|
const listeners = new Map<string, Set<(value: any) => void>>();
|
||||||
|
|
||||||
return {
|
// Load all existing storage values for this plugin
|
||||||
get: async <T>(key: string) => {
|
const loadStoragePromise = (async () => {
|
||||||
const result = await browser.storage.local.get(prefix + key);
|
try {
|
||||||
return result[prefix + key] as T || null;
|
const allStorage = await browser.storage.local.get(null);
|
||||||
},
|
|
||||||
set: async <T>(key: string, value: T) => {
|
// Filter for this plugin's storage keys and populate cache
|
||||||
await browser.storage.local.set({ [prefix + key]: value });
|
Object.entries(allStorage).forEach(([key, value]) => {
|
||||||
},
|
if (key.startsWith(prefix)) {
|
||||||
|
const shortKey = key.slice(prefix.length);
|
||||||
|
cache[shortKey] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[BetterSEQTA+] Error loading storage for plugin ${pluginId}:`, error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Listen for storage changes
|
||||||
|
browser.storage.onChanged.addListener((changes, area) => {
|
||||||
|
if (area === 'local') {
|
||||||
|
Object.entries(changes).forEach(([key, change]) => {
|
||||||
|
if (key.startsWith(prefix)) {
|
||||||
|
const shortKey = key.slice(prefix.length);
|
||||||
|
cache[shortKey] = change.newValue;
|
||||||
|
|
||||||
|
// Notify listeners
|
||||||
|
listeners.get(shortKey)?.forEach(callback => callback(change.newValue));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the proxy for direct property access
|
||||||
|
return new Proxy(cache, {
|
||||||
|
get(target, prop: string) {
|
||||||
|
if (prop === 'get') {
|
||||||
|
return async <T>(key: string) => {
|
||||||
|
return target[key] as T || null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (prop === 'set') {
|
||||||
|
return async <T>(key: string, value: T) => {
|
||||||
|
target[key] = value;
|
||||||
|
await browser.storage.local.set({ [prefix + key]: value });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (prop === 'onChange') {
|
||||||
|
return (key: string, callback: (value: any) => void) => {
|
||||||
|
if (!listeners.has(key)) {
|
||||||
|
listeners.set(key, new Set());
|
||||||
|
}
|
||||||
|
listeners.get(key)!.add(callback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (prop === 'offChange') {
|
||||||
|
return (key: string, callback: (value: any) => void) => {
|
||||||
|
listeners.get(key)?.delete(callback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (prop === 'loaded') {
|
||||||
|
return loadStoragePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct property access
|
||||||
|
return target[prop];
|
||||||
|
},
|
||||||
|
set(target, prop: string, value: any) {
|
||||||
|
if (['get', 'set', 'onChange', 'offChange', 'loaded'].includes(prop)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cache and store in browser storage
|
||||||
|
target[prop] = value;
|
||||||
|
browser.storage.local.set({ [prefix + prop]: value });
|
||||||
|
|
||||||
|
// Notify listeners
|
||||||
|
listeners.get(prop)?.forEach(callback => callback(value));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}) as StorageAPI;
|
||||||
|
}
|
||||||
|
|
||||||
function createEventsAPI(pluginId: string): EventsAPI {
|
function createEventsAPI(pluginId: string): EventsAPI {
|
||||||
const prefix = `plugin.${pluginId}.`;
|
const prefix = `plugin.${pluginId}.`;
|
||||||
|
|||||||
@@ -21,11 +21,10 @@ export class PluginManager {
|
|||||||
public dispatchPluginEvent(pluginId: string, event: string, args?: any) {
|
public dispatchPluginEvent(pluginId: string, event: string, args?: any) {
|
||||||
const fullEventName = `plugin.${pluginId}.${event}`;
|
const fullEventName = `plugin.${pluginId}.${event}`;
|
||||||
|
|
||||||
|
// Dispatch plugin event if it's running otherwise queue it
|
||||||
if (this.runningPlugins.get(pluginId)) {
|
if (this.runningPlugins.get(pluginId)) {
|
||||||
// If plugin is running, dispatch immediately
|
|
||||||
document.dispatchEvent(new CustomEvent(fullEventName, { detail: args }));
|
document.dispatchEvent(new CustomEvent(fullEventName, { detail: args }));
|
||||||
} else {
|
} else {
|
||||||
// Otherwise queue it
|
|
||||||
const key = `${pluginId}:${event}`;
|
const key = `${pluginId}:${event}`;
|
||||||
if (!this.eventBacklog.has(key)) {
|
if (!this.eventBacklog.has(key)) {
|
||||||
this.eventBacklog.set(key, []);
|
this.eventBacklog.set(key, []);
|
||||||
@@ -66,6 +65,13 @@ export class PluginManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const api = createPluginAPI(plugin);
|
const api = createPluginAPI(plugin);
|
||||||
|
|
||||||
|
// Wait for both settings and storage to be loaded before starting the plugin
|
||||||
|
await Promise.all([
|
||||||
|
(api.settings as any).loaded,
|
||||||
|
api.storage.loaded
|
||||||
|
]);
|
||||||
|
|
||||||
const result = await plugin.run(api);
|
const result = await plugin.run(api);
|
||||||
if (typeof result === 'function') {
|
if (typeof result === 'function') {
|
||||||
this.cleanupFunctions.set(plugin.id, result);
|
this.cleanupFunctions.set(plugin.id, result);
|
||||||
@@ -115,6 +121,38 @@ export class PluginManager {
|
|||||||
return Array.from(this.plugins.values());
|
return Array.from(this.plugins.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getAllPluginSettings(): Array<{
|
||||||
|
pluginId: string;
|
||||||
|
name: string;
|
||||||
|
settings: {
|
||||||
|
[key: string]: {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
type: string;
|
||||||
|
default: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}> {
|
||||||
|
return Array.from(this.plugins.entries()).map(([id, plugin]) => {
|
||||||
|
const settingsEntries = Object.entries(plugin.settings).map(([key, setting]) => {
|
||||||
|
return [key, {
|
||||||
|
id: key,
|
||||||
|
title: (setting as any).title || key,
|
||||||
|
description: (setting as any).description || '',
|
||||||
|
type: (setting as any).type,
|
||||||
|
default: (setting as any).default
|
||||||
|
}];
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
pluginId: id,
|
||||||
|
name: plugin.name,
|
||||||
|
settings: Object.fromEntries(settingsEntries)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public isPluginRunning(pluginId: string): boolean {
|
public isPluginRunning(pluginId: string): boolean {
|
||||||
return this.runningPlugins.get(pluginId) || false;
|
return this.runningPlugins.get(pluginId) || false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ interface SelectSetting<T extends string> {
|
|||||||
|
|
||||||
type PluginSetting = BooleanSetting | StringSetting | NumberSetting | SelectSetting<string>;
|
type PluginSetting = BooleanSetting | StringSetting | NumberSetting | SelectSetting<string>;
|
||||||
|
|
||||||
// Plugin settings configuration
|
|
||||||
export type PluginSettings = {
|
export type PluginSettings = {
|
||||||
[key: string]: PluginSetting;
|
[key: string]: PluginSetting;
|
||||||
}
|
}
|
||||||
@@ -43,15 +42,14 @@ type SettingValue<T extends PluginSetting> = T extends BooleanSetting ? boolean
|
|||||||
T extends SelectSetting<infer O> ? O :
|
T extends SelectSetting<infer O> ? O :
|
||||||
never;
|
never;
|
||||||
|
|
||||||
// Settings API interface
|
|
||||||
export type SettingsAPI<T extends PluginSettings> = {
|
export type SettingsAPI<T extends PluginSettings> = {
|
||||||
[K in keyof T]: SettingValue<T[K]>;
|
[K in keyof T]: SettingValue<T[K]>;
|
||||||
} & {
|
} & {
|
||||||
onChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => void;
|
onChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => void;
|
||||||
offChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => void;
|
offChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => void;
|
||||||
|
loaded: Promise<void>; // Promise that resolves when settings are loaded
|
||||||
}
|
}
|
||||||
|
|
||||||
// SEQTA API interface
|
|
||||||
export interface SEQTAAPI {
|
export interface SEQTAAPI {
|
||||||
onMount: (selector: string, callback: (element: Element) => void) => void;
|
onMount: (selector: string, callback: (element: Element) => void) => void;
|
||||||
getFiber: (selector: string) => ReactFiber;
|
getFiber: (selector: string) => ReactFiber;
|
||||||
@@ -59,19 +57,20 @@ export interface SEQTAAPI {
|
|||||||
onPageChange: (callback: (page: string) => void) => void;
|
onPageChange: (callback: (page: string) => void) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Storage API interface
|
|
||||||
export interface StorageAPI {
|
export interface StorageAPI {
|
||||||
get: <T>(key: string) => Promise<T | null>;
|
get: <T>(key: string) => Promise<T | null>;
|
||||||
set: <T>(key: string, value: T) => Promise<void>;
|
set: <T>(key: string, value: T) => Promise<void>;
|
||||||
|
onChange: (key: string, callback: (value: any) => void) => void;
|
||||||
|
offChange: (key: string, callback: (value: any) => void) => void;
|
||||||
|
loaded: Promise<void>;
|
||||||
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Events API interface
|
|
||||||
export interface EventsAPI {
|
export interface EventsAPI {
|
||||||
on: (event: string, callback: (...args: any[]) => void) => void;
|
on: (event: string, callback: (...args: any[]) => void) => void;
|
||||||
emit: (event: string, ...args: any[]) => void;
|
emit: (event: string, ...args: any[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complete Plugin API interface
|
|
||||||
export interface PluginAPI<T extends PluginSettings> {
|
export interface PluginAPI<T extends PluginSettings> {
|
||||||
seqta: SEQTAAPI;
|
seqta: SEQTAAPI;
|
||||||
settings: SettingsAPI<T>;
|
settings: SettingsAPI<T>;
|
||||||
@@ -79,7 +78,6 @@ export interface PluginAPI<T extends PluginSettings> {
|
|||||||
events: EventsAPI;
|
events: EventsAPI;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plugin interface
|
|
||||||
export interface Plugin<T extends PluginSettings = PluginSettings> {
|
export interface Plugin<T extends PluginSettings = PluginSettings> {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ pluginManager.registerPlugin(notificationCollectorPlugin);
|
|||||||
export { init as Monofile } from './monofile';
|
export { init as Monofile } from './monofile';
|
||||||
export { init as Themes } from './themes';
|
export { init as Themes } from './themes';
|
||||||
|
|
||||||
// New plugin system initialization
|
|
||||||
export async function initializePlugins(): Promise<void> {
|
export async function initializePlugins(): Promise<void> {
|
||||||
await pluginManager.startAllPlugins();
|
await pluginManager.startAllPlugins();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-export plugin manager for direct access if needed
|
|
||||||
export { pluginManager };
|
export { pluginManager };
|
||||||
|
|
||||||
|
export function getAllPluginSettings() {
|
||||||
|
return pluginManager.getAllPluginSettings();
|
||||||
|
}
|
||||||
@@ -74,6 +74,7 @@ class StorageManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async saveToStorage(): Promise<void> {
|
private async saveToStorage(): Promise<void> {
|
||||||
|
// @ts-expect-error
|
||||||
await browser.storage.local.set(this.data);
|
await browser.storage.local.set(this.data);
|
||||||
this.notifySubscribers();
|
this.notifySubscribers();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user