docs: improve plugin documentation

This commit is contained in:
SethBurkart123
2025-03-31 19:25:06 +11:00
parent 77582a4d00
commit 68e8c89b35
10 changed files with 531 additions and 1582 deletions
+223 -121
View File
@@ -1,155 +1,257 @@
# BetterSEQTA+ Plugin System
# Creating Plugins for BetterSEQTA+
BetterSEQTA+ features a powerful plugin system that allows developers to extend and customize the functionality of SEQTA Learn. This document provides an overview of how the plugin system works and how to get started with creating your own plugins.
Hey there! 👋 So you want to create a plugin for BetterSEQTA+? That's awesome! This guide will walk you through everything you need to know, from the very basics to more advanced features. Don't worry if you're new to this - we'll explain everything step by step.
## What is a Plugin?
A plugin is a self-contained piece of code that adds functionality to BetterSEQTA+. Plugins can:
In BetterSEQTA+, a plugin is like a mini-app that adds new features to SEQTA. Think of it as a piece of LEGO that you can snap onto SEQTA to make it do new things. For example, you could create a plugin that:
- Changes how SEQTA looks
- Adds new buttons or features
- Shows extra information on your timetable
- Collects notifications in a better way
- Really, anything you can imagine!
- Add new UI elements to SEQTA Learn
- Modify existing UI elements
- Add new features to SEQTA Learn
- Modify or extend existing features
- Store and retrieve user data
- Respond to events in SEQTA Learn
## Your First Plugin
Each plugin is isolated from other plugins, with its own settings, storage, and lifecycle. This ensures that plugins can be enabled, disabled, or removed without affecting other parts of the system.
## Plugin Architecture
The BetterSEQTA+ plugin system consists of several key components:
### 1. Plugin Interface
All plugins implement the `Plugin` interface, which defines the structure and lifecycle methods of a plugin:
Let's create a super simple plugin together. We'll make one that adds a friendly message to the SEQTA homepage. Here's what we'll need:
```typescript
export interface Plugin<T extends PluginSettings = PluginSettings, S = any> {
id: string;
name: string;
description: string;
version: string;
settings: T;
run: (api: PluginAPI<T, S>) => void | Promise<void> | (() => void) | Promise<(() => void)>;
}
import type { Plugin } from '@/plugins/core/types';
const myFirstPlugin: Plugin = {
// Every plugin needs these basic details
id: 'my-first-plugin',
name: 'My First Plugin',
description: 'Adds a friendly message to SEQTA',
version: '1.0.0',
// This tells BetterSEQTA+ that users can turn our plugin on/off
disableToggle: true,
// This is where the magic happens!
run: async (api) => {
// Wait for the homepage to load
api.seqta.onMount('.home-page', (homePage) => {
// Create our message
const message = document.createElement('div');
message.textContent = 'Hello from my first plugin! 🎉';
message.style.padding = '20px';
message.style.backgroundColor = '#e9f5ff';
message.style.borderRadius = '8px';
message.style.margin = '20px';
// Add it to the page
homePage.prepend(message);
});
// Return a cleanup function that removes our message when the plugin is disabled
return () => {
const message = document.querySelector('.home-page > div');
message?.remove();
};
}
};
export default myFirstPlugin;
```
### 2. Plugin API
Let's break down what's happening here:
When a plugin is run, it receives an instance of the `PluginAPI`, which provides access to various services and utilities:
1. First, we import the `Plugin` type that tells TypeScript what a plugin should look like
2. We create our plugin object with some basic information:
- `id`: A unique name for your plugin (use lowercase and dashes)
- `name`: A friendly name that users will see
- `description`: Explain what your plugin does
- `version`: Your plugin's version number
3. We set `disableToggle: true` so users can turn our plugin on/off in settings
4. The `run` function is where we put our plugin's code
5. We use `api.seqta.onMount` to wait for the homepage to load
6. We create and style a message element
7. We return a cleanup function that removes our changes when the plugin is disabled
## The Plugin API
When your plugin runs, it gets access to a powerful API that lets you do all sorts of things. Let's look at what you can do:
### SEQTA API (`api.seqta`)
This helps you interact with SEQTA's pages:
```typescript
export interface PluginAPI<T extends PluginSettings, S = any> {
seqta: SEQTAAPI;
settings: SettingsAPI<T>;
storage: TypedStorageAPI<S>;
events: EventsAPI;
}
// Wait for an element to appear on the page
api.seqta.onMount('.some-class', (element) => {
// Do something with the element
});
// Know when the user changes pages
api.seqta.onPageChange((page) => {
console.log('User went to:', page);
});
// Get the current page
const currentPage = api.seqta.getCurrentPage();
```
- **SEQTA API**: Provides methods for interacting with the SEQTA Learn UI
- **Settings API**: Provides type-safe access to plugin settings
- **Storage API**: Provides type-safe persistent storage for plugin data
- **Events API**: Allows plugins to emit and listen for events
### Settings API (`api.settings`)
### 3. Plugin Manager
The Plugin Manager is responsible for loading, starting, stopping, and managing plugins. It handles the lifecycle of each plugin and ensures that plugins have access to the resources they need.
### 4. Plugin Registry
The Plugin Registry is a central repository of all available plugins. Built-in plugins are automatically registered, and additional plugins can be registered dynamically.
## Plugin Lifecycle
Plugins follow a simple lifecycle:
1. **Registration**: The plugin is registered with the Plugin Manager
2. **Loading**: The plugin's settings and storage are loaded
3. **Running**: The plugin's `run` method is called with the Plugin API
4. **Cleanup**: If the plugin returns a cleanup function, it is called when the plugin is stopped
## Creating a Plugin
Creating a plugin for BetterSEQTA+ involves a few simple steps:
1. Define your plugin's interface
2. Implement the Plugin interface
3. Register your plugin with the Plugin Manager
For a detailed guide on creating plugins, see [Creating Your First Plugin](./creating-plugins.md).
## Built-in Plugins
BetterSEQTA+ comes with several built-in plugins that provide core functionality:
- **Timetable**: Enhances the SEQTA timetable view
- **Notification Collector**: Improves the notification system
- **Theme Customizer**: Allows customization of the SEQTA theme
- **Assessment Enhancer**: Adds features to the assessment view
These plugins serve as good examples of how to use the plugin system effectively.
## Type-Safe Settings and Storage
One of the key features of the BetterSEQTA+ plugin system is its type-safe settings and storage. Using TypeScript generics, plugins can define the structure of their settings and storage, ensuring that they are used correctly throughout the codebase.
### Settings Example
Want to let users customize your plugin? Use settings!
```typescript
interface MyPluginSettings extends PluginSettings {
enabled: {
type: 'boolean';
default: boolean;
title: string;
description: string;
};
refreshInterval: {
type: 'number';
default: number;
title: string;
description: string;
min: number;
max: number;
};
import { BasePlugin } from '@/plugins/core/settings';
import { booleanSetting, defineSettings, Setting } from '@/plugins/core/settingsHelpers';
// Define your settings
const settings = defineSettings({
showMessage: booleanSetting({
default: true,
title: "Show Welcome Message",
description: "Show a friendly message on the homepage",
})
});
// Create a class for your plugin
class MyPluginClass extends BasePlugin<typeof settings> {
@Setting(settings.showMessage)
showMessage!: boolean;
}
// Create your plugin
const settingsInstance = new MyPluginClass();
const myPlugin: Plugin<typeof settings> = {
// ... other plugin details ...
settings: settingsInstance.settings,
run: async (api) => {
// Use the setting
if (api.settings.showMessage) {
// Show the message
}
// Listen for setting changes
api.settings.onChange('showMessage', (newValue) => {
if (newValue) {
// Show the message
} else {
// Hide the message
}
});
}
};
```
### Storage Example
### Storage API (`api.storage`)
Need to save some data? The storage API has got you covered:
```typescript
interface MyPluginStorage {
lastRefresh: string;
savedItems: string[];
userPreferences: {
theme: 'light' | 'dark';
fontSize: number;
};
}
// Save some data
await api.storage.set('lastVisit', new Date().toISOString());
// Get it back later
const lastVisit = await api.storage.get('lastVisit');
// Listen for changes
api.storage.onChange('lastVisit', (newValue) => {
console.log('Last visit updated:', newValue);
});
```
## Decorator-Based Settings
### Events API (`api.events`)
BetterSEQTA+ also offers a more modern, decorator-based approach to defining settings. For more information, see [Creating Plugins with Settings](../settings/creating-plugins.md).
Want your plugin to be able to interface with other plugins? Then use events!
## Plugin API Reference
```typescript
// Listen for an event
api.events.on('myCustomEvent', (data) => {
console.log('Got event:', data);
});
The Plugin API provides a rich set of features for interacting with SEQTA Learn. For a complete reference, see [Plugin API Reference](../advanced/plugin-api.md).
// Send an event
api.events.emit('myCustomEvent', { some: 'data' });
```
## Adding Styles
Want to make your plugin look pretty? You can add CSS styles:
```typescript
const myPlugin: Plugin = {
// ... other plugin details ...
// Add your CSS here
styles: `
.my-plugin-message {
background: linear-gradient(135deg, #6e8efb, #a777e3);
color: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
margin: 20px;
animation: slide-in 0.3s ease-out;
}
@keyframes slide-in {
from { transform: translateY(-20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
`,
run: async (api) => {
// Your plugin code here
}
};
```
## Best Practices
When creating plugins for BetterSEQTA+, consider these best practices:
Here are some tips to make your plugin awesome:
1. **Use TypeScript**: Take advantage of TypeScript's type system to ensure type safety in your plugins.
2. **Keep Plugins Focused**: Each plugin should do one thing well.
3. **Handle Cleanup**: Always return a cleanup function from your plugin's `run` method to ensure proper resource management.
4. **Document Your Code**: Add clear documentation to your code, especially for public APIs.
5. **Test Thoroughly**: Test your plugins in different environments and with different configurations.
6. **Follow UI Guidelines**: When adding UI elements, follow the SEQTA Learn UI guidelines to maintain a consistent experience.
7. **Optimize Performance**: Be mindful of performance impact, especially for plugins that run on every page.
1. **Always Clean Up**: When your plugin is disabled, clean up any changes you made:
```typescript
run: async (api) => {
// Add stuff to the page
const element = document.createElement('div');
document.body.appendChild(element);
// Return a cleanup function
return () => {
element.remove();
};
}
```
## Next Steps
2. **Use TypeScript**: It helps catch errors before they happen and makes your code easier to understand.
- [Creating Your First Plugin](./creating-plugins.md)
- [Plugin API Reference](../advanced/plugin-api.md)
- [Typed Storage API](../advanced/storage-api.md)
3. **Test Your Plugin**: Make sure it works in different situations:
- When SEQTA is loading
- When the user switches pages
- When the plugin is enabled/disabled
- When settings are changed
4. **Keep It Fast**: Don't slow down SEQTA:
- Use `onMount` instead of intervals or timeouts
- Clean up event listeners when they're not needed
- Don't do heavy calculations on the main thread
5. **Make It User-Friendly**:
- Add clear settings with good descriptions
- Use `disableToggle: true` so users can turn it off if needed
- Add helpful error messages if something goes wrong
## Examples
Want to see more examples? Check out our built-in plugins:
- [themes](../../src/plugins/built-in/themes/index.ts): Shows how to change SEQTA's appearance
- [notificationCollector](../../src/plugins/built-in/notificationCollector/index.ts): Shows how to work with SEQTA's notifications
- [timetable](../../src/plugins/built-in/timetable/index.ts): Shows how to modify SEQTA's timetable view
- [assessmentsAverage](../../src/plugins/built-in/assessmentsAverage/index.ts): Shows how to add new features to existing pages
## Need Help?
Got stuck? No worries! Here's where you can get help:
- Join our [Discord server](https://discord.gg/YzmbnCDkat)
- Check out the built-in plugins in the `src/plugins/built-in` folder
- Open an issue on our [GitHub page](https://github.com/betterseqta/betterseqta-plus/issues)
Happy coding and feel free to checkout the api reference [here](./api-reference.md)
+288
View File
@@ -0,0 +1,288 @@
# Plugin API Reference
This document provides detailed technical information about BetterSEQTA+'s plugin APIs. For a beginner-friendly introduction, see [Creating Your First Plugin](./README.md).
## Plugin Interface
The core `Plugin` interface that all plugins must implement:
```typescript
interface Plugin<T extends PluginSettings = PluginSettings, S = any> {
id: string; // Unique identifier for the plugin
name: string; // Display name
description: string; // Plugin description
version: string; // Semantic version (e.g. "1.0.0")
settings: T; // Plugin settings (type-safe)
styles?: string; // Optional CSS styles
disableToggle?: boolean; // Whether to show enable/disable toggle
run: (api: PluginAPI<T, S>) => void | Promise<void> | (() => void) | Promise<(() => void)>;
}
```
## SEQTA API
The `SEQTAAPI` interface provides methods for interacting with SEQTA's UI:
```typescript
interface SEQTAAPI {
// Wait for an element to appear in the DOM
onMount(
selector: string, // CSS selector
callback: (el: Element) => void
): { unregister: () => void };
// Get React fiber for debugging/advanced usage
getFiber(selector: string): ReactFiber;
// Get current SEQTA page
getCurrentPage(): string;
// Listen for page changes
onPageChange(
callback: (page: string) => void
): { unregister: () => void };
}
```
## Settings API
The settings system provides type-safe plugin configuration:
```typescript
interface SettingsAPI<T extends PluginSettings> {
// Access setting values
[K in keyof T]: SettingValue<T[K]>;
// Listen for setting changes
onChange<K extends keyof T>(
key: K,
callback: (value: SettingValue<T[K]>) => void
): { unregister: () => void };
// Remove change listener
offChange<K extends keyof T>(
key: K,
callback: (value: SettingValue<T[K]>) => void
): void;
// Promise that resolves when settings are loaded
loaded: Promise<void>;
}
```
### Setting Types
Available setting types:
```typescript
// Boolean toggle
booleanSetting({
default: boolean;
title: string;
description: string;
});
// Text input
stringSetting({
default: string;
title: string;
description: string;
placeholder?: string;
});
// Number input
numberSetting({
default: number;
title: string;
description: string;
min?: number;
max?: number;
step?: number;
});
// Dropdown select
selectSetting<T extends string>({
default: T;
title: string;
description: string;
options: Array<{
value: T;
label: string;
}>;
});
```
### Using Settings
Two ways to define settings:
1. Using the BasePlugin class (recommended):
```typescript
const settings = defineSettings({
mySetting: booleanSetting({...})
});
class MyPlugin extends BasePlugin<typeof settings> {
@Setting(settings.mySetting)
mySetting!: boolean;
}
```
2. Direct object (simpler but less type-safe):
```typescript
const settings = {
mySetting: booleanSetting({...})
};
```
## Storage API
Persistent storage for plugin data:
```typescript
interface StorageAPI<T = any> {
// Get a stored value
get<K extends keyof T>(key: K): Promise<T[K] | undefined>;
// Set a value
set<K extends keyof T>(key: K, value: T[K]): Promise<void>;
// Delete a value
delete<K extends keyof T>(key: K): Promise<void>;
// Listen for changes
onChange<K extends keyof T>(
key: K,
callback: (value: T[K]) => void
): { unregister: () => void };
// Promise that resolves when storage is loaded
loaded: Promise<void>;
}
```
Storage is:
- Persistent across page reloads
- Isolated per plugin (plugins can't access each other's storage)
- Type-safe when using TypeScript
- Automatically synchronized across tabs
## Events API
Inter-plugin communication system:
```typescript
interface EventsAPI {
// Listen for an event
on(
event: string,
callback: (...args: any[]) => void
): { unregister: () => void };
// Emit an event
emit(event: string, ...args: any[]): void;
}
```
Event naming conventions:
- Use `plugin.{pluginId}.{eventName}` for plugin-specific events
- Use `seqta.{eventName}` for SEQTA-related events
- Use `global.{eventName}` for system-wide events
## Plugin Lifecycle
1. **Registration**:
```typescript
PluginManager.getInstance().registerPlugin(myPlugin);
```
2. **Initialization**:
- Plugin's `run` function is called
- Settings and storage are loaded
- CSS styles are injected (if any)
3. **Running**:
- Plugin can use all APIs
- Can listen for events and changes
- Can modify SEQTA's UI
4. **Cleanup**:
- When plugin is disabled or unloaded
- Cleanup function from `run` is called
- CSS styles are removed
- Event listeners are cleaned up
## Type Safety
TypeScript types for type-safe plugins:
```typescript
// Plugin with settings and storage types
interface MyPluginSettings {
theme: string;
notifications: boolean;
}
interface MyPluginStorage {
lastVisit: string;
userData: { name: string; id: number };
}
const myPlugin: Plugin<MyPluginSettings, MyPluginStorage> = {
// TypeScript will ensure type safety for:
// - Settings access and changes
// - Storage operations
// - Event payloads (when typed)
};
```
## Error Handling
Best practices for plugin error handling:
```typescript
run: async (api) => {
try {
// Initialization
await someAsyncOperation();
// Return cleanup
return () => {
try {
// Cleanup code
} catch (error) {
console.error('Plugin cleanup failed:', error);
}
};
} catch (error) {
// Log error but don't crash
console.error('Plugin initialization failed:', error);
// Still return cleanup to ensure proper shutdown
return () => {};
}
}
```
## Performance Considerations
1. **DOM Operations**:
- Use `onMount` instead of polling
- Batch DOM updates
- Use CSS classes instead of inline styles
- Remove listeners when not needed
2. **Storage**:
- Cache frequently accessed values
- Batch storage operations
- Don't store large objects
3. **Events**:
- Clean up listeners
- Use typed events
- Don't emit events too frequently
4. **Settings**:
- Use appropriate setting types
- Provide good defaults
- Handle setting changes efficiently
-269
View File
@@ -1,269 +0,0 @@
# Creating Your First Plugin
This guide will walk you through the process of creating a plugin for BetterSEQTA+, from setup to implementation to testing.
## Prerequisites
Before you start creating a plugin, make sure you have:
- Basic knowledge of TypeScript
- Familiarity with the BetterSEQTA+ codebase
- A development environment set up according to the [Installation Guide](../installation.md)
## Plugin Structure
A typical BetterSEQTA+ plugin consists of:
1. **Plugin Definition**: A TypeScript file that defines the plugin's metadata and functionality
2. **Settings Interface**: (Optional) A TypeScript interface that defines the plugin's settings
3. **Storage Interface**: (Optional) A TypeScript interface that defines the plugin's storage structure
## Step 1: Planning Your Plugin
Before you start coding, take some time to plan your plugin:
1. **Identify the Problem**: What issue or need does your plugin address?
2. **Define the Scope**: What specific features will your plugin include?
3. **Consider the User Experience**: How will users interact with your plugin?
## Step 2: Creating the Plugin File
Create a new TypeScript file for your plugin. The convention is to place it in the `src/plugins/` directory, either in the `built-in` folder or a new folder if it's a third-party plugin.
```typescript
// src/plugins/my-plugin/index.ts
import { Plugin, PluginAPI, PluginSettings } from '../../core/types';
export interface MyPluginSettings extends PluginSettings {
enabled: {
type: 'boolean';
default: true;
title: 'Enable My Plugin';
description: 'Turn my plugin on or off';
};
// Add more settings as needed
}
export interface MyPluginStorage {
lastRun: string;
// Add more storage fields as needed
}
const myPlugin: Plugin<MyPluginSettings, MyPluginStorage> = {
id: 'my-plugin',
name: 'My Plugin',
description: 'A simple plugin for BetterSEQTA+',
version: '1.0.0',
settings: {
enabled: {
type: 'boolean',
default: true,
title: 'Enable My Plugin',
description: 'Turn my plugin on or off',
},
// Initialize your settings here
},
run: (api) => {
if (!api.settings.enabled) {
return;
}
// Initialize storage with default values if needed
if (api.storage.lastRun === undefined) {
api.storage.lastRun = new Date().toISOString();
}
// Your plugin logic goes here
console.log('My Plugin is running!');
// Access the SEQTA API
api.seqta.onPageLoad('/timetable', () => {
// Code to run when the timetable page loads
});
// Return a cleanup function (optional but recommended)
return () => {
console.log('My Plugin is cleaning up!');
// Cleanup logic goes here
};
},
};
export default myPlugin;
```
## Step 3: Registering Your Plugin
To make your plugin available to BetterSEQTA+, you need to register it with the Plugin Manager. For built-in plugins, you can add your plugin to the `src/plugins/built-in/index.ts` file:
```typescript
// src/plugins/built-in/index.ts
import myPlugin from './my-plugin';
// Other imports...
export const builtInPlugins = [
myPlugin,
// Other plugins...
];
```
For third-party plugins, you'll need to follow a different approach, as detailed in [Third-Party Plugins](../advanced/third-party-plugins.md).
## Step 4: Implementing Your Plugin Logic
The main functionality of your plugin goes in the `run` method. Here are some common patterns:
### Responding to Page Loads
```typescript
api.seqta.onPageLoad('/timetable', () => {
// Code to run when the timetable page loads
});
```
### Modifying the UI
```typescript
api.seqta.onPageLoad('/timetable', () => {
const timetableElement = document.querySelector('.timetable');
if (timetableElement) {
// Modify the timetable element
const controlsDiv = document.createElement('div');
controlsDiv.className = 'my-plugin-controls';
controlsDiv.innerHTML = '<button>Zoom In</button><button>Zoom Out</button>';
timetableElement.appendChild(controlsDiv);
// Add event listeners
controlsDiv.querySelector('button:first-child').addEventListener('click', () => {
// Zoom in logic
});
}
});
```
### Working with Settings
```typescript
// Get a setting value
const isEnabled = api.settings.enabled;
// Listen for settings changes
api.settings.onChange('enabled', (newValue) => {
if (newValue) {
// Enable functionality
} else {
// Disable functionality
}
});
```
### Working with Storage
```typescript
// Get a stored value
const lastRun = api.storage.lastRun;
// Set a stored value
api.storage.lastRun = new Date().toISOString();
// Listen for storage changes
api.storage.onChange('lastRun', (newValue) => {
console.log(`Last run updated to: ${newValue}`);
});
```
### Working with Events
```typescript
// Listen for events
api.events.on('assessmentLoaded', (data) => {
console.log(`Assessment loaded: ${data.id}`);
});
// Emit an event
api.events.emit('myPluginEvent', { message: 'Hello from My Plugin!' });
```
## Step 5: Testing Your Plugin
To test your plugin:
1. Run the development server:
```
npm run dev
```
2. Open SEQTA Learn in your browser with BetterSEQTA+ enabled.
3. Check the console for any error messages.
4. Verify that your plugin works as expected.
## Step 6: Adding Plugin Settings UI
If your plugin has settings, they will automatically appear in the BetterSEQTA+ settings panel. The UI is generated based on the settings interface you defined.
For more control over the settings UI, you can use the decorator-based settings system. See [Creating Plugins with Settings](../settings/creating-plugins.md) for more information.
## Best Practices for Plugin Development
1. **Follow TypeScript Best Practices**: Use proper typing for all variables and functions.
2. **Handle Errors Gracefully**: Wrap your code in try-catch blocks to prevent crashes.
```typescript
try {
// Your code
} catch (error) {
console.error('My Plugin Error:', error);
}
```
3. **Clean Up After Yourself**: Always return a cleanup function from your plugin's `run` method.
```typescript
const cleanup = () => {
// Remove event listeners, DOM elements, etc.
};
return cleanup;
```
4. **Document Your Code**: Add comments to explain complex logic or unusual patterns.
5. **Keep It Simple**: Start with a simple plugin and add features incrementally.
## Example Plugins
For inspiration, check out these example plugins in the BetterSEQTA+ codebase:
1. **Timetable Plugin**: Enhances the SEQTA timetable view with zoom controls and filtering options.
- Location: `src/plugins/built-in/timetable/index.ts`
2. **Notification Collector**: Improves the notification system in SEQTA Learn.
- Location: `src/plugins/built-in/notification-collector/index.ts`
## Troubleshooting
### Plugin Not Loading
- Check that your plugin is properly registered
- Verify that there are no TypeScript errors
- Look for error messages in the console
### Plugin Not Working as Expected
- Ensure that your plugin's `enabled` setting is true
- Check that your selectors match the SEQTA DOM structure
- Use `console.log` statements to debug your code
### TypeScript Errors
- Make sure your interfaces are properly defined
- Check that you're using the correct types for the plugin API
- Verify that your plugin implements the `Plugin` interface correctly
## Next Steps
- [Learn About Type-Safe Settings](../settings/creating-plugins.md)
- [Explore the Plugin API](../advanced/plugin-api.md)
- [Contribute to BetterSEQTA+](../contributing.md)