mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
feat: add plugin system
This commit is contained in:
+207
@@ -0,0 +1,207 @@
|
|||||||
|
# BetterSEQTA+ Plugin System
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
The BetterSEQTA+ plugin system is designed to provide a clean, type-safe, and developer-friendly way to extend the functionality of BetterSEQTA+. While initially focused on built-in plugins, the architecture is designed to potentially support external plugins in the future.
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### Plugin Structure
|
||||||
|
Each plugin is a simple object that contains metadata and a run function:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const examplePlugin = {
|
||||||
|
id: 'example',
|
||||||
|
name: 'Example Plugin',
|
||||||
|
description: 'Does something cool',
|
||||||
|
version: '1.0.0',
|
||||||
|
settings: {
|
||||||
|
enabled: { type: 'boolean', default: true },
|
||||||
|
color: { type: 'string', default: '#ff0000' }
|
||||||
|
},
|
||||||
|
|
||||||
|
run: (api) => {
|
||||||
|
// Plugin logic here
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plugin API
|
||||||
|
Plugins receive a powerful API object that provides access to:
|
||||||
|
|
||||||
|
- **Settings**: Type-safe settings management with direct property access
|
||||||
|
- **SEQTA Integration**: React component mounting and state management
|
||||||
|
- **Storage**: Persistent storage capabilities
|
||||||
|
- **Events**: Communication system
|
||||||
|
|
||||||
|
### Settings System
|
||||||
|
Settings are defined with TypeScript types for safety and accessed like regular properties:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In your plugin
|
||||||
|
api.settings.myOption = true;
|
||||||
|
const value = api.settings.myOption;
|
||||||
|
|
||||||
|
// Watch for changes
|
||||||
|
api.settings.onChange('myOption', (newValue) => {
|
||||||
|
console.log('Option changed:', newValue);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEQTA Integration
|
||||||
|
Plugins can interact with SEQTA's React components:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Listen for component mounting
|
||||||
|
api.seqta.onMount('.timetable-view', (element) => {
|
||||||
|
// Access the DOM element directly
|
||||||
|
console.log('Timetable mounted:', element);
|
||||||
|
|
||||||
|
// If you need React access, use getFiber
|
||||||
|
const fiber = api.seqta.getFiber('.timetable-view');
|
||||||
|
fiber.setState(prevState => ({
|
||||||
|
...prevState,
|
||||||
|
someValue: true
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get specific component
|
||||||
|
const fiber = api.seqta.getFiber('.timetable-cell');
|
||||||
|
const props = await fiber.getProps();
|
||||||
|
|
||||||
|
// Listen for page changes
|
||||||
|
api.seqta.onPageChange((page) => {
|
||||||
|
if (page === 'timetable') {
|
||||||
|
// Handle timetable page
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Status
|
||||||
|
|
||||||
|
### Phase 1: Core Infrastructure ✅
|
||||||
|
- [x] Create basic plugin type definitions
|
||||||
|
- [x] Implement plugin manager
|
||||||
|
- [x] Set up basic API structure
|
||||||
|
- [x] Create plugin loading system
|
||||||
|
|
||||||
|
### Phase 2: Settings System ✅
|
||||||
|
- [x] Design settings storage structure
|
||||||
|
- [x] Implement settings proxy system
|
||||||
|
- [x] Add settings change notifications
|
||||||
|
- [x] Create settings validation
|
||||||
|
|
||||||
|
### Phase 3: SEQTA Integration ✅
|
||||||
|
- [x] Implement component mount detection
|
||||||
|
- [x] Create ReactFiber wrapper
|
||||||
|
- [x] Add page change detection
|
||||||
|
- [x] Create component state utilities
|
||||||
|
|
||||||
|
### Phase 4: Plugin API Features ✅
|
||||||
|
- [x] Storage system
|
||||||
|
- [x] Event system
|
||||||
|
- [x] Error handling
|
||||||
|
- [ ] Plugin lifecycle hooks
|
||||||
|
|
||||||
|
### Phase 5: Migration & Testing 🚧
|
||||||
|
- [ ] Convert existing features to plugins
|
||||||
|
- [ ] Create plugin testing utilities
|
||||||
|
- [ ] Add plugin documentation
|
||||||
|
- [ ] Create example plugins
|
||||||
|
|
||||||
|
### Phase 6: Future Enhancements 📝
|
||||||
|
- [ ] Plugin dependencies system
|
||||||
|
- [ ] Plugin hot-reloading
|
||||||
|
- [ ] External plugin support
|
||||||
|
- [ ] Plugin marketplace infrastructure
|
||||||
|
|
||||||
|
## Plugin Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const timetablePlugin = {
|
||||||
|
id: 'timetable',
|
||||||
|
name: 'Timetable Enhancer',
|
||||||
|
description: 'Adds extra features to the timetable view',
|
||||||
|
version: '1.0.0',
|
||||||
|
settings: {
|
||||||
|
showWeekends: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Show weekend days in the timetable'
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
type: 'select',
|
||||||
|
options: ['light', 'dark', 'auto'],
|
||||||
|
default: 'auto',
|
||||||
|
description: 'Timetable theme'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
run: async (api) => {
|
||||||
|
// Listen for timetable mount
|
||||||
|
api.seqta.onMount('.timetable-view', (element) => {
|
||||||
|
// Get React access since we need to modify state
|
||||||
|
const fiber = api.seqta.getFiber('.timetable-view');
|
||||||
|
|
||||||
|
// Apply settings
|
||||||
|
if (api.settings.showWeekends) {
|
||||||
|
fiber.setState(prevState => ({
|
||||||
|
...prevState,
|
||||||
|
showWeekends: true
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for settings changes
|
||||||
|
api.settings.onChange('theme', async (newTheme) => {
|
||||||
|
const timetable = api.seqta.getFiber('.timetable-view');
|
||||||
|
if (newTheme !== 'auto') {
|
||||||
|
await timetable.setProp('theme', newTheme);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
plugins/
|
||||||
|
core/
|
||||||
|
types.ts # Core type definitions
|
||||||
|
createAPI.ts # API implementation
|
||||||
|
manager.ts # Plugin manager
|
||||||
|
built-in/ # Built-in plugins
|
||||||
|
timetable/
|
||||||
|
assessments/
|
||||||
|
etc...
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Type Definitions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface BSAPI<TSettings> {
|
||||||
|
seqta: {
|
||||||
|
onMount: (selector: string, callback: (fiber: ReactFiber) => void) => void;
|
||||||
|
getFiber: (selector: string) => ReactFiber;
|
||||||
|
getCurrentPage: () => string;
|
||||||
|
onPageChange: (callback: (page: string) => void) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
settings: TSettings & {
|
||||||
|
onChange: <K extends keyof TSettings>(
|
||||||
|
key: K,
|
||||||
|
callback: (value: TSettings[K]) => void
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
storage: {
|
||||||
|
get: (key: string) => Promise<any>;
|
||||||
|
set: (key: string, value: any) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
events: {
|
||||||
|
on: (event: string, callback: (...args: any[]) => void) => void;
|
||||||
|
emit: (event: string, ...args: any[]) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
+10
-3
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
settingsState,
|
settingsState,
|
||||||
} from "@/seqta/utils/listeners/SettingsState"
|
} from "@/seqta/utils/listeners/SettingsState"
|
||||||
@@ -41,10 +40,18 @@ async function init() {
|
|||||||
await main()
|
await main()
|
||||||
|
|
||||||
if (settingsState.onoff) {
|
if (settingsState.onoff) {
|
||||||
Object.values(plugins).forEach(plugin => {
|
// Initialize legacy plugins
|
||||||
|
const legacyPlugins = [plugins.Monofile, plugins.Themes];
|
||||||
|
legacyPlugins.forEach(plugin => {
|
||||||
|
if (typeof plugin === 'function') {
|
||||||
plugin();
|
plugin();
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize new plugin system
|
||||||
|
await plugins.initializePlugins();
|
||||||
|
}
|
||||||
|
|
||||||
console.info(
|
console.info(
|
||||||
"[BetterSEQTA+] Successfully initialised BetterSEQTA+, starting to load assets.",
|
"[BetterSEQTA+] Successfully initialised BetterSEQTA+, starting to load assets.",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
|
||||||
|
import type { Plugin, PluginSettings } from '../../core/types';
|
||||||
|
|
||||||
|
interface NotificationCollectorSettings extends PluginSettings {
|
||||||
|
enabled: {
|
||||||
|
type: 'boolean';
|
||||||
|
default: boolean;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const notificationCollectorPlugin: Plugin<NotificationCollectorSettings> = {
|
||||||
|
id: 'notificationCollector',
|
||||||
|
name: 'Notification Collector',
|
||||||
|
description: 'Collects and displays SEQTA notifications',
|
||||||
|
version: '1.0.0',
|
||||||
|
settings: {
|
||||||
|
enabled: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
title: 'Notification Collector',
|
||||||
|
description: 'Uncaps the 9+ limit for notifications, showing the real number.',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
run: async (api) => {
|
||||||
|
let pollInterval: number | null = null;
|
||||||
|
|
||||||
|
const checkNotifications = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${location.origin}/seqta/student/heartbeat?`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
timestamp: "1970-01-01 00:00:00.0",
|
||||||
|
hash: "#?page=/home",
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const alertDiv = document.querySelector(".notifications__bubble___1EkSQ") as HTMLElement;
|
||||||
|
|
||||||
|
if (alertDiv) {
|
||||||
|
alertDiv.textContent = data.payload.notifications.length.toString();
|
||||||
|
} else {
|
||||||
|
console.info("[BetterSEQTA+] No notifications currently");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[BetterSEQTA+] Error fetching notifications:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start polling when enabled
|
||||||
|
const startPolling = () => {
|
||||||
|
if (pollInterval) return; // Already polling
|
||||||
|
checkNotifications();
|
||||||
|
pollInterval = window.setInterval(checkNotifications, 30000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stop polling when disabled
|
||||||
|
const stopPolling = () => {
|
||||||
|
if (pollInterval) {
|
||||||
|
window.clearInterval(pollInterval);
|
||||||
|
pollInterval = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start/stop based on initial enabled state
|
||||||
|
if (settingsState.notificationcollector) {
|
||||||
|
api.seqta.onMount(".notifications__bubble___1EkSQ", (_) => {
|
||||||
|
startPolling();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store callbacks for cleanup
|
||||||
|
const enabledCallback = (enabled: boolean) => {
|
||||||
|
if (enabled) {
|
||||||
|
startPolling();
|
||||||
|
} else {
|
||||||
|
stopPolling();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle settings changes
|
||||||
|
api.settings.onChange('enabled', enabledCallback);
|
||||||
|
|
||||||
|
// Return cleanup function
|
||||||
|
return () => {
|
||||||
|
stopPolling();
|
||||||
|
api.settings.offChange('enabled', enabledCallback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default notificationCollectorPlugin;
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
const timetablePlugin: Plugin = {
|
||||||
|
id: 'timetable',
|
||||||
|
name: 'Timetable Enhancer',
|
||||||
|
description: 'Adds extra features to the timetable view',
|
||||||
|
version: '1.0.0',
|
||||||
|
settings: {},
|
||||||
|
|
||||||
|
run: async (api) => {
|
||||||
|
api.seqta.onMount('.timetablepage', handleTimetable)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function handleTimetable(): Promise<void> {
|
||||||
|
await waitForElm(".time", true, 10)
|
||||||
|
|
||||||
|
// Store original heights when timetable loads
|
||||||
|
const lessons = document.querySelectorAll(".dailycal .lesson")
|
||||||
|
lessons.forEach((lesson: Element) => {
|
||||||
|
const lessonEl = lesson as HTMLElement
|
||||||
|
lessonEl.setAttribute(
|
||||||
|
"data-original-height",
|
||||||
|
lessonEl.offsetHeight.toString(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Existing time format code
|
||||||
|
if (settingsState.timeFormat == "12") {
|
||||||
|
const times = document.querySelectorAll(".timetablepage .times .time")
|
||||||
|
for (const time of times) {
|
||||||
|
if (!time.textContent) continue
|
||||||
|
time.textContent = convertTo12HourFormat(time.textContent, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTimetableZoom()
|
||||||
|
handleTimetableAssessmentHide()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTimetableZoom(): void {
|
||||||
|
console.log("Initializing timetable zoom controls")
|
||||||
|
|
||||||
|
// Lazy initialize state variables only when function is first called
|
||||||
|
let timetableZoomLevel = 1
|
||||||
|
let baseContainerHeight: number | null = null
|
||||||
|
const originalEntryPositions = new Map<
|
||||||
|
Element,
|
||||||
|
{ topRatio: number; heightRatio: number }
|
||||||
|
>()
|
||||||
|
|
||||||
|
// Create zoom controls
|
||||||
|
const zoomControls = document.createElement("div")
|
||||||
|
zoomControls.className = "timetable-zoom-controls"
|
||||||
|
|
||||||
|
const zoomIn = document.createElement("button")
|
||||||
|
zoomIn.className = "uiButton timetable-zoom iconFamily"
|
||||||
|
zoomIn.innerHTML = "" // Using unicode for zoom in icon
|
||||||
|
|
||||||
|
const zoomOut = document.createElement("button")
|
||||||
|
zoomOut.className = "uiButton timetable-zoom iconFamily"
|
||||||
|
zoomOut.innerHTML = "" // Using unicode for zoom out icon
|
||||||
|
|
||||||
|
zoomControls.appendChild(zoomOut)
|
||||||
|
zoomControls.appendChild(zoomIn)
|
||||||
|
|
||||||
|
const toolbar = document.getElementById("toolbar")
|
||||||
|
toolbar?.appendChild(zoomControls)
|
||||||
|
|
||||||
|
const initializePositions = () => {
|
||||||
|
// Get the base container height from the first TD
|
||||||
|
const firstDayColumn = document.querySelector(
|
||||||
|
".dailycal .content .days td",
|
||||||
|
) as HTMLElement
|
||||||
|
if (!firstDayColumn) return false
|
||||||
|
|
||||||
|
baseContainerHeight =
|
||||||
|
parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight
|
||||||
|
|
||||||
|
// Store original ratios
|
||||||
|
const entries = document.querySelectorAll(".entriesWrapper .entry")
|
||||||
|
entries.forEach((entry: Element) => {
|
||||||
|
const entryEl = entry as HTMLElement
|
||||||
|
|
||||||
|
// Calculate ratios relative to detected base height
|
||||||
|
if (baseContainerHeight === null) return
|
||||||
|
const topRatio = parseInt(entryEl.style.top) / baseContainerHeight
|
||||||
|
const heightRatio = parseInt(entryEl.style.height) / baseContainerHeight
|
||||||
|
|
||||||
|
originalEntryPositions.set(entry, { topRatio, heightRatio })
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateZoom = () => {
|
||||||
|
// Initialize positions if not already done
|
||||||
|
if (baseContainerHeight === null && !initializePositions()) {
|
||||||
|
console.error("Failed to initialize positions")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug(`Updating zoom level to: ${timetableZoomLevel}`)
|
||||||
|
|
||||||
|
// Calculate new container height
|
||||||
|
if (baseContainerHeight === null) return
|
||||||
|
const newContainerHeight = baseContainerHeight * timetableZoomLevel
|
||||||
|
|
||||||
|
// Update all day columns (TDs)
|
||||||
|
const dayColumns = document.querySelectorAll(".dailycal .content .days td")
|
||||||
|
dayColumns.forEach((td: Element) => {
|
||||||
|
(td as HTMLElement).style.height = `${newContainerHeight}px`
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update all entries using stored ratios
|
||||||
|
const entries = document.querySelectorAll(".entriesWrapper .entry")
|
||||||
|
entries.forEach((entry: Element) => {
|
||||||
|
const entryEl = entry as HTMLElement
|
||||||
|
const originalRatios = originalEntryPositions.get(entry)
|
||||||
|
|
||||||
|
if (originalRatios) {
|
||||||
|
// Calculate new positions from original ratios
|
||||||
|
const newTop = originalRatios.topRatio * newContainerHeight
|
||||||
|
const newHeight = originalRatios.heightRatio * newContainerHeight
|
||||||
|
|
||||||
|
// Apply new values
|
||||||
|
entryEl.style.top = `${Math.round(newTop)}px`
|
||||||
|
entryEl.style.height = `${Math.round(newHeight)}px`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update time column to match
|
||||||
|
const timeColumn = document.querySelector(".times")
|
||||||
|
if (timeColumn) {
|
||||||
|
const times = timeColumn.querySelectorAll(".time")
|
||||||
|
const timeHeight = newContainerHeight / times.length
|
||||||
|
times.forEach((time: Element) => {
|
||||||
|
(time as HTMLElement).style.height = `${timeHeight}px`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
entries[Math.round((entries.length - 1) / 2)].scrollIntoView({
|
||||||
|
behavior: "instant",
|
||||||
|
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 {
|
||||||
|
const hideControls = document.createElement("div")
|
||||||
|
hideControls.className = "timetable-hide-controls"
|
||||||
|
|
||||||
|
const hideOn = document.createElement("button")
|
||||||
|
hideOn.className = "uiButton timetable-hide iconFamily"
|
||||||
|
hideOn.innerHTML = "👁"
|
||||||
|
|
||||||
|
hideControls.appendChild(hideOn)
|
||||||
|
|
||||||
|
const toolbar = document.getElementById("toolbar")
|
||||||
|
toolbar?.appendChild(hideControls)
|
||||||
|
|
||||||
|
function hideElements(): void {
|
||||||
|
const entries = document.querySelectorAll(".entry")
|
||||||
|
entries.forEach((entry: Element) => {
|
||||||
|
const entryEl = entry as HTMLElement
|
||||||
|
if (!entryEl.classList.contains("assessment") && !(entryEl.style.opacity === "0.3")) {
|
||||||
|
entryEl.style.opacity = "0.3"
|
||||||
|
} else {
|
||||||
|
entryEl.style.opacity = "1"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
hideOn.addEventListener("click", () => {
|
||||||
|
hideElements()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default timetablePlugin;
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import type { Plugin, PluginAPI, PluginSettings, SEQTAAPI, SettingsAPI, StorageAPI, EventsAPI } from './types';
|
||||||
|
import { eventManager } from '@/seqta/utils/listeners/EventManager';
|
||||||
|
import ReactFiber from '@/seqta/utils/ReactFiber';
|
||||||
|
import browser from 'webextension-polyfill';
|
||||||
|
|
||||||
|
function createSEQTAAPI(): SEQTAAPI {
|
||||||
|
return {
|
||||||
|
onMount: (selector, callback) => {
|
||||||
|
eventManager.register(
|
||||||
|
`${selector}Added`,
|
||||||
|
{
|
||||||
|
customCheck: (element) => element.matches(selector),
|
||||||
|
},
|
||||||
|
callback
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getFiber: (selector) => {
|
||||||
|
return ReactFiber.find(selector);
|
||||||
|
},
|
||||||
|
getCurrentPage: () => {
|
||||||
|
const path = window.location.hash.split('?page=/')[1] || '';
|
||||||
|
return path.split('/')[0];
|
||||||
|
},
|
||||||
|
onPageChange: (callback) => {
|
||||||
|
window.addEventListener('hashchange', () => {
|
||||||
|
const page = window.location.hash.split('?page=/')[1] || '';
|
||||||
|
callback(page.split('/')[0]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): SettingsAPI<T> {
|
||||||
|
const storageKey = `plugin.${plugin.id}.settings`;
|
||||||
|
const listeners = new Map<keyof T, Set<(value: any) => void>>();
|
||||||
|
let settings: { [K in keyof T]: T[K]['default'] };
|
||||||
|
|
||||||
|
// Initialize settings with defaults
|
||||||
|
settings = Object.entries(plugin.settings).reduce((acc, [key, setting]) => {
|
||||||
|
acc[key as keyof T] = setting.default;
|
||||||
|
return acc;
|
||||||
|
}, {} as { [K in keyof T]: T[K]['default'] });
|
||||||
|
|
||||||
|
// Load saved settings
|
||||||
|
browser.storage.local.get(storageKey).then((stored) => {
|
||||||
|
if (stored[storageKey]) {
|
||||||
|
Object.assign(settings, stored[storageKey]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a proxy to handle direct property access
|
||||||
|
const proxy = new Proxy(settings, {
|
||||||
|
get(target, prop: string) {
|
||||||
|
if (prop === 'onChange') {
|
||||||
|
return (key: keyof T, callback: (value: any) => void) => {
|
||||||
|
if (!listeners.has(key)) {
|
||||||
|
listeners.set(key, new Set());
|
||||||
|
}
|
||||||
|
listeners.get(key)!.add(callback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return target[prop as keyof T];
|
||||||
|
},
|
||||||
|
set(target, prop: string, value: any) {
|
||||||
|
if (prop === 'onChange') return false;
|
||||||
|
target[prop as keyof T] = value;
|
||||||
|
browser.storage.local.set({ [storageKey]: target });
|
||||||
|
listeners.get(prop as keyof T)?.forEach(callback => callback(value));
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
}) as SettingsAPI<T>;
|
||||||
|
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStorageAPI(pluginId: string): StorageAPI {
|
||||||
|
const prefix = `plugin.${pluginId}.storage.`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
get: async <T>(key: string) => {
|
||||||
|
const result = await browser.storage.local.get(prefix + key);
|
||||||
|
return result[prefix + key] as T || null;
|
||||||
|
},
|
||||||
|
set: async <T>(key: string, value: T) => {
|
||||||
|
await browser.storage.local.set({ [prefix + key]: value });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEventsAPI(pluginId: string): EventsAPI {
|
||||||
|
const prefix = `plugin.${pluginId}.`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
on: (event, callback) => {
|
||||||
|
document.addEventListener(prefix + event, ((e: CustomEvent) => {
|
||||||
|
callback(...(e.detail || []));
|
||||||
|
}) as EventListener);
|
||||||
|
},
|
||||||
|
emit: (event, ...args) => {
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(prefix + event, {
|
||||||
|
detail: args.length > 0 ? args : null
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPluginAPI<T extends PluginSettings>(plugin: Plugin<T>): PluginAPI<T> {
|
||||||
|
return {
|
||||||
|
seqta: createSEQTAAPI(),
|
||||||
|
settings: createSettingsAPI(plugin),
|
||||||
|
storage: createStorageAPI(plugin.id),
|
||||||
|
events: createEventsAPI(plugin.id),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import type { Plugin, PluginSettings } from './types';
|
||||||
|
import { createPluginAPI } from './createAPI';
|
||||||
|
|
||||||
|
export class PluginManager {
|
||||||
|
private static instance: PluginManager;
|
||||||
|
private plugins: Map<string, Plugin<any>> = new Map();
|
||||||
|
private runningPlugins: Map<string, boolean> = new Map();
|
||||||
|
private eventBacklog: Map<string, any[]> = new Map();
|
||||||
|
private cleanupFunctions: Map<string, () => void> = new Map();
|
||||||
|
private listeners: Map<string, Set<(...args: any[]) => void>> = new Map();
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
public static getInstance(): PluginManager {
|
||||||
|
if (!PluginManager.instance) {
|
||||||
|
PluginManager.instance = new PluginManager();
|
||||||
|
}
|
||||||
|
return PluginManager.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispatchPluginEvent(pluginId: string, event: string, args?: any) {
|
||||||
|
const fullEventName = `plugin.${pluginId}.${event}`;
|
||||||
|
|
||||||
|
if (this.runningPlugins.get(pluginId)) {
|
||||||
|
// If plugin is running, dispatch immediately
|
||||||
|
document.dispatchEvent(new CustomEvent(fullEventName, { detail: args }));
|
||||||
|
} else {
|
||||||
|
// Otherwise queue it
|
||||||
|
const key = `${pluginId}:${event}`;
|
||||||
|
if (!this.eventBacklog.has(key)) {
|
||||||
|
this.eventBacklog.set(key, []);
|
||||||
|
}
|
||||||
|
this.eventBacklog.get(key)!.push(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processBackloggedEvents(pluginId: string) {
|
||||||
|
for (const [key, argsList] of this.eventBacklog.entries()) {
|
||||||
|
const [eventPluginId, event] = key.split(':');
|
||||||
|
if (eventPluginId === pluginId) {
|
||||||
|
for (const args of argsList) {
|
||||||
|
this.dispatchPluginEvent(pluginId, event, args);
|
||||||
|
}
|
||||||
|
this.eventBacklog.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public registerPlugin<T extends PluginSettings>(plugin: Plugin<T>): void {
|
||||||
|
if (this.plugins.has(plugin.id)) {
|
||||||
|
throw new Error(`Plugin with id "${plugin.id}" is already registered`);
|
||||||
|
}
|
||||||
|
this.plugins.set(plugin.id, plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async startPlugin(pluginId: string): Promise<void> {
|
||||||
|
const plugin = this.plugins.get(pluginId);
|
||||||
|
if (!plugin) {
|
||||||
|
throw new Error(`Plugin "${pluginId}" not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.runningPlugins.get(pluginId)) {
|
||||||
|
console.warn(`Plugin "${pluginId}" is already running`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const api = createPluginAPI(plugin);
|
||||||
|
const result = await plugin.run(api);
|
||||||
|
if (typeof result === 'function') {
|
||||||
|
this.cleanupFunctions.set(plugin.id, result);
|
||||||
|
}
|
||||||
|
this.runningPlugins.set(pluginId, true);
|
||||||
|
console.info(`Plugin "${pluginId}" started successfully`);
|
||||||
|
|
||||||
|
// Process any backlogged events
|
||||||
|
await this.processBackloggedEvents(pluginId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[BetterSEQTA+] Failed to start plugin ${pluginId}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async startAllPlugins(): Promise<void> {
|
||||||
|
const startPromises = Array.from(this.plugins.keys()).map(id =>
|
||||||
|
this.startPlugin(id).catch(error => {
|
||||||
|
console.error(`Failed to start plugin "${id}":`, error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.allSettled(startPromises);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stopPlugin(pluginId: string): Promise<void> {
|
||||||
|
const cleanup = this.cleanupFunctions.get(pluginId);
|
||||||
|
if (cleanup) {
|
||||||
|
cleanup();
|
||||||
|
this.cleanupFunctions.delete(pluginId);
|
||||||
|
}
|
||||||
|
this.runningPlugins.set(pluginId, false);
|
||||||
|
console.info(`Plugin "${pluginId}" stopped`);
|
||||||
|
this.emit('plugin.stopped', pluginId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public stopAllPlugins(): void {
|
||||||
|
Array.from(this.plugins.keys()).forEach(id => this.stopPlugin(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPlugin(pluginId: string): Plugin | undefined {
|
||||||
|
return this.plugins.get(pluginId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAllPlugins(): Plugin[] {
|
||||||
|
return Array.from(this.plugins.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
public isPluginRunning(pluginId: string): boolean {
|
||||||
|
return this.runningPlugins.get(pluginId) || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private emit(event: string, ...args: any[]): void {
|
||||||
|
const listeners = this.listeners.get(event);
|
||||||
|
if (listeners) {
|
||||||
|
listeners.forEach(listener => listener(...args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public on(event: string, callback: (...args: any[]) => void): void {
|
||||||
|
if (!this.listeners.has(event)) {
|
||||||
|
this.listeners.set(event, new Set());
|
||||||
|
}
|
||||||
|
this.listeners.get(event)!.add(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public off(event: string, callback: (...args: any[]) => void): void {
|
||||||
|
const listeners = this.listeners.get(event);
|
||||||
|
if (listeners) {
|
||||||
|
listeners.delete(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import ReactFiber from '@/seqta/utils/ReactFiber';
|
||||||
|
|
||||||
|
interface BooleanSetting {
|
||||||
|
type: 'boolean';
|
||||||
|
default: boolean;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StringSetting {
|
||||||
|
type: 'string';
|
||||||
|
default: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NumberSetting {
|
||||||
|
type: 'number';
|
||||||
|
default: number;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectSetting<T extends string> {
|
||||||
|
type: 'select';
|
||||||
|
options: readonly T[];
|
||||||
|
default: T;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PluginSetting = BooleanSetting | StringSetting | NumberSetting | SelectSetting<string>;
|
||||||
|
|
||||||
|
// Plugin settings configuration
|
||||||
|
export type PluginSettings = {
|
||||||
|
[key: string]: PluginSetting;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper type to extract the actual value type from a setting
|
||||||
|
type SettingValue<T extends PluginSetting> = T extends BooleanSetting ? boolean :
|
||||||
|
T extends StringSetting ? string :
|
||||||
|
T extends NumberSetting ? number :
|
||||||
|
T extends SelectSetting<infer O> ? O :
|
||||||
|
never;
|
||||||
|
|
||||||
|
// Settings API interface
|
||||||
|
export type SettingsAPI<T extends PluginSettings> = {
|
||||||
|
[K in keyof T]: SettingValue<T[K]>;
|
||||||
|
} & {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SEQTA API interface
|
||||||
|
export interface SEQTAAPI {
|
||||||
|
onMount: (selector: string, callback: (element: Element) => void) => void;
|
||||||
|
getFiber: (selector: string) => ReactFiber;
|
||||||
|
getCurrentPage: () => string;
|
||||||
|
onPageChange: (callback: (page: string) => void) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storage API interface
|
||||||
|
export interface StorageAPI {
|
||||||
|
get: <T>(key: string) => Promise<T | null>;
|
||||||
|
set: <T>(key: string, value: T) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events API interface
|
||||||
|
export interface EventsAPI {
|
||||||
|
on: (event: string, callback: (...args: any[]) => void) => void;
|
||||||
|
emit: (event: string, ...args: any[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete Plugin API interface
|
||||||
|
export interface PluginAPI<T extends PluginSettings> {
|
||||||
|
seqta: SEQTAAPI;
|
||||||
|
settings: SettingsAPI<T>;
|
||||||
|
storage: StorageAPI;
|
||||||
|
events: EventsAPI;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin interface
|
||||||
|
export interface Plugin<T extends PluginSettings = PluginSettings> {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
version: string;
|
||||||
|
settings: T;
|
||||||
|
run: (api: PluginAPI<T>) => void | Promise<void> | (() => void) | Promise<() => void>;
|
||||||
|
}
|
||||||
+22
-2
@@ -1,2 +1,22 @@
|
|||||||
export { init as Monofile } from './monofile'
|
import { PluginManager } from './core/manager';
|
||||||
export { init as Themes } from './themes'
|
import timetablePlugin from './built-in/timetable';
|
||||||
|
import notificationCollectorPlugin from './built-in/notificationCollector';
|
||||||
|
|
||||||
|
// Initialize plugin manager
|
||||||
|
const pluginManager = PluginManager.getInstance();
|
||||||
|
|
||||||
|
// Register built-in plugins
|
||||||
|
pluginManager.registerPlugin(timetablePlugin);
|
||||||
|
pluginManager.registerPlugin(notificationCollectorPlugin);
|
||||||
|
|
||||||
|
// Legacy plugin exports
|
||||||
|
export { init as Monofile } from './monofile';
|
||||||
|
export { init as Themes } from './themes';
|
||||||
|
|
||||||
|
// New plugin system initialization
|
||||||
|
export async function initializePlugins(): Promise<void> {
|
||||||
|
await pluginManager.startAllPlugins();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-export plugin manager for direct access if needed
|
||||||
|
export { pluginManager };
|
||||||
+3
-193
@@ -13,11 +13,9 @@ import {
|
|||||||
settingsState,
|
settingsState,
|
||||||
} from "@/seqta/utils/listeners/SettingsState"
|
} from "@/seqta/utils/listeners/SettingsState"
|
||||||
import { StorageChangeHandler } from "@/seqta/utils/listeners/StorageChanges"
|
import { StorageChangeHandler } from "@/seqta/utils/listeners/StorageChanges"
|
||||||
import { convertTo12HourFormat } from "@/seqta/utils/convertTo12HourFormat"
|
|
||||||
import { eventManager } from "@/seqta/utils/listeners/EventManager"
|
import { eventManager } from "@/seqta/utils/listeners/EventManager"
|
||||||
|
|
||||||
// UI and theme management
|
// UI and theme management
|
||||||
import { enableNotificationCollector } from "@/seqta/utils/CreateEnable/EnableNotificationCollector"
|
|
||||||
import RegisterClickListeners from "@/seqta/utils/listeners/ClickListeners"
|
import RegisterClickListeners from "@/seqta/utils/listeners/ClickListeners"
|
||||||
import { AddBetterSEQTAElements } from "@/seqta/ui/AddBetterSEQTAElements"
|
import { AddBetterSEQTAElements } from "@/seqta/ui/AddBetterSEQTAElements"
|
||||||
import { updateAllColors } from "@/seqta/ui/colors/Manager"
|
import { updateAllColors } from "@/seqta/ui/colors/Manager"
|
||||||
@@ -240,14 +238,14 @@ async function LoadPageElements(): Promise<void> {
|
|||||||
handleReports,
|
handleReports,
|
||||||
)
|
)
|
||||||
|
|
||||||
eventManager.register(
|
/* eventManager.register(
|
||||||
"timetableAdded",
|
"timetableAdded",
|
||||||
{
|
{
|
||||||
elementType: "div",
|
elementType: "div",
|
||||||
className: "timetablepage",
|
className: "timetablepage",
|
||||||
},
|
},
|
||||||
handleTimetable,
|
handleTimetable,
|
||||||
)
|
) */
|
||||||
|
|
||||||
eventManager.register(
|
eventManager.register(
|
||||||
"noticesAdded",
|
"noticesAdded",
|
||||||
@@ -274,159 +272,6 @@ async function LoadPageElements(): Promise<void> {
|
|||||||
await handleSublink(sublink)
|
await handleSublink(sublink)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTimetableZoom(): void {
|
|
||||||
console.log("Initializing timetable zoom controls")
|
|
||||||
|
|
||||||
// Lazy initialize state variables only when function is first called
|
|
||||||
let timetableZoomLevel = 1
|
|
||||||
let baseContainerHeight: number | null = null
|
|
||||||
const originalEntryPositions = new Map<
|
|
||||||
Element,
|
|
||||||
{ topRatio: number; heightRatio: number }
|
|
||||||
>()
|
|
||||||
|
|
||||||
// Create zoom controls
|
|
||||||
const zoomControls = document.createElement("div")
|
|
||||||
zoomControls.className = "timetable-zoom-controls"
|
|
||||||
|
|
||||||
const zoomIn = document.createElement("button")
|
|
||||||
zoomIn.className = "uiButton timetable-zoom iconFamily"
|
|
||||||
zoomIn.innerHTML = "" // Using unicode for zoom in icon
|
|
||||||
|
|
||||||
const zoomOut = document.createElement("button")
|
|
||||||
zoomOut.className = "uiButton timetable-zoom iconFamily"
|
|
||||||
zoomOut.innerHTML = "" // Using unicode for zoom out icon
|
|
||||||
|
|
||||||
zoomControls.appendChild(zoomOut)
|
|
||||||
zoomControls.appendChild(zoomIn)
|
|
||||||
|
|
||||||
const toolbar = document.getElementById("toolbar")
|
|
||||||
toolbar?.appendChild(zoomControls)
|
|
||||||
|
|
||||||
const initializePositions = () => {
|
|
||||||
// Get the base container height from the first TD
|
|
||||||
const firstDayColumn = document.querySelector(
|
|
||||||
".dailycal .content .days td",
|
|
||||||
) as HTMLElement
|
|
||||||
if (!firstDayColumn) return false
|
|
||||||
|
|
||||||
baseContainerHeight =
|
|
||||||
parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight
|
|
||||||
|
|
||||||
// Store original ratios
|
|
||||||
const entries = document.querySelectorAll(".entriesWrapper .entry")
|
|
||||||
entries.forEach((entry: Element) => {
|
|
||||||
const entryEl = entry as HTMLElement
|
|
||||||
|
|
||||||
// Calculate ratios relative to detected base height
|
|
||||||
if (baseContainerHeight === null) return
|
|
||||||
const topRatio = parseInt(entryEl.style.top) / baseContainerHeight
|
|
||||||
const heightRatio = parseInt(entryEl.style.height) / baseContainerHeight
|
|
||||||
|
|
||||||
originalEntryPositions.set(entry, { topRatio, heightRatio })
|
|
||||||
})
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateZoom = () => {
|
|
||||||
// Initialize positions if not already done
|
|
||||||
if (baseContainerHeight === null && !initializePositions()) {
|
|
||||||
console.error("Failed to initialize positions")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug(`Updating zoom level to: ${timetableZoomLevel}`)
|
|
||||||
|
|
||||||
// Calculate new container height
|
|
||||||
if (baseContainerHeight === null) return
|
|
||||||
const newContainerHeight = baseContainerHeight * timetableZoomLevel
|
|
||||||
|
|
||||||
// Update all day columns (TDs)
|
|
||||||
const dayColumns = document.querySelectorAll(".dailycal .content .days td")
|
|
||||||
dayColumns.forEach((td: Element) => {
|
|
||||||
(td as HTMLElement).style.height = `${newContainerHeight}px`
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update all entries using stored ratios
|
|
||||||
const entries = document.querySelectorAll(".entriesWrapper .entry")
|
|
||||||
entries.forEach((entry: Element) => {
|
|
||||||
const entryEl = entry as HTMLElement
|
|
||||||
const originalRatios = originalEntryPositions.get(entry)
|
|
||||||
|
|
||||||
if (originalRatios) {
|
|
||||||
// Calculate new positions from original ratios
|
|
||||||
const newTop = originalRatios.topRatio * newContainerHeight
|
|
||||||
const newHeight = originalRatios.heightRatio * newContainerHeight
|
|
||||||
|
|
||||||
// Apply new values
|
|
||||||
entryEl.style.top = `${Math.round(newTop)}px`
|
|
||||||
entryEl.style.height = `${Math.round(newHeight)}px`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update time column to match
|
|
||||||
const timeColumn = document.querySelector(".times")
|
|
||||||
if (timeColumn) {
|
|
||||||
const times = timeColumn.querySelectorAll(".time")
|
|
||||||
const timeHeight = newContainerHeight / times.length
|
|
||||||
times.forEach((time: Element) => {
|
|
||||||
(time as HTMLElement).style.height = `${timeHeight}px`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
entries[Math.round((entries.length - 1) / 2)].scrollIntoView({
|
|
||||||
behavior: "instant",
|
|
||||||
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 {
|
|
||||||
const hideControls = document.createElement("div") // Creates the div element which houses the eye icon
|
|
||||||
hideControls.className = "timetable-hide-controls"
|
|
||||||
|
|
||||||
const hideOn = document.createElement("button") // Creates the actual button which is clicked
|
|
||||||
hideOn.className = "uiButton timetable-hide iconFamily"
|
|
||||||
hideOn.innerHTML = "👁" // Using unicode for hide icon
|
|
||||||
|
|
||||||
hideControls.appendChild(hideOn)
|
|
||||||
|
|
||||||
const toolbar = document.getElementById("toolbar") // Appends the new button to the toolbar
|
|
||||||
toolbar?.appendChild(hideControls)
|
|
||||||
|
|
||||||
function hideElements(): void {
|
|
||||||
const entries = document.querySelectorAll(".entry") // Gets all the timetables entries on the page, and loops through
|
|
||||||
entries.forEach((entry: Element) => {
|
|
||||||
const entryEl = entry as HTMLElement
|
|
||||||
if (!entryEl.classList.contains("assessment") && !(entryEl.style.opacity === "0.3")) { // If the entry is not an assessment, and hasn't already been hidden, hide it.
|
|
||||||
entryEl.style.opacity = "0.3"
|
|
||||||
} else { // Otherwise, it should be shown.
|
|
||||||
entryEl.style.opacity = "1"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
hideOn.addEventListener("click", () => { // Listen for when the button is pressed
|
|
||||||
hideElements()
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleNotices(node: Element): Promise<void> {
|
async function handleNotices(node: Element): Promise<void> {
|
||||||
if (!(node instanceof HTMLElement)) return
|
if (!(node instanceof HTMLElement)) return
|
||||||
if (!settingsState.animations) return
|
if (!settingsState.animations) return
|
||||||
@@ -454,11 +299,8 @@ async function handleSublink(sublink: string | undefined): Promise<void> {
|
|||||||
await handleNewsPage()
|
await handleNewsPage()
|
||||||
break
|
break
|
||||||
case undefined:
|
case undefined:
|
||||||
window.location.replace(
|
window.location.replace(`${location.origin}/#?page=/${settingsState.defaultPage}`)
|
||||||
`${location.origin}/#?page=/${settingsState.defaultPage}`,
|
|
||||||
)
|
|
||||||
if (settingsState.defaultPage === "home") loadHomePage()
|
if (settingsState.defaultPage === "home") loadHomePage()
|
||||||
if (settingsState.defaultPage === "timetable") handleTimetable()
|
|
||||||
if (settingsState.defaultPage === "documents")
|
if (settingsState.defaultPage === "documents")
|
||||||
handleDocuments(document.querySelector(".documents")!)
|
handleDocuments(document.querySelector(".documents")!)
|
||||||
if (settingsState.defaultPage === "reports")
|
if (settingsState.defaultPage === "reports")
|
||||||
@@ -481,48 +323,16 @@ async function handleSublink(sublink: string | undefined): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleTimetable(): Promise<void> {
|
|
||||||
await waitForElm(".time", true, 10)
|
|
||||||
|
|
||||||
// Store original heights when timetable loads
|
|
||||||
const lessons = document.querySelectorAll(".dailycal .lesson")
|
|
||||||
lessons.forEach((lesson: Element) => {
|
|
||||||
const lessonEl = lesson as HTMLElement
|
|
||||||
lessonEl.setAttribute(
|
|
||||||
"data-original-height",
|
|
||||||
lessonEl.offsetHeight.toString(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Existing time format code
|
|
||||||
if (settingsState.timeFormat == "12") {
|
|
||||||
const times = document.querySelectorAll(".timetablepage .times .time")
|
|
||||||
for (const time of times) {
|
|
||||||
if (!time.textContent) continue
|
|
||||||
time.textContent = convertTo12HourFormat(time.textContent, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTimetableZoom()
|
|
||||||
handleTimetableAssessmentHide()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleNewsPage(): Promise<void> {
|
async function handleNewsPage(): Promise<void> {
|
||||||
console.info("[BetterSEQTA+] Started Init")
|
console.info("[BetterSEQTA+] Started Init")
|
||||||
if (settingsState.onoff) {
|
if (settingsState.onoff) {
|
||||||
SendNewsPage()
|
SendNewsPage()
|
||||||
if (settingsState.notificationcollector) {
|
|
||||||
enableNotificationCollector()
|
|
||||||
}
|
|
||||||
finishLoad()
|
finishLoad()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDefault(): Promise<void> {
|
async function handleDefault(): Promise<void> {
|
||||||
finishLoad()
|
finishLoad()
|
||||||
if (settingsState.notificationcollector) {
|
|
||||||
enableNotificationCollector()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMessages(node: Element): Promise<void> {
|
async function handleMessages(node: Element): Promise<void> {
|
||||||
|
|||||||
Reference in New Issue
Block a user