feat: add docs and dev plugins

This commit is contained in:
SethBurkart123
2025-03-18 22:15:40 +11:00
parent 7a76d3f4eb
commit 1c63c06b72
18 changed files with 3855 additions and 54 deletions
+60
View File
@@ -0,0 +1,60 @@
# BetterSEQTA+ Documentation
🚧 DOCS UNDER CONSTRUCTION! 🚧
Welcome to the BetterSEQTA+ documentation! This documentation will help you understand how BetterSEQTA+ works and how to extend it with plugins and new features.
## Table of Contents
### Getting Started
- [Project Overview](./README.md) - This file
- [Installation Guide](./installation.md) - How to install and set up BetterSEQTA+
- [Contributing Guide](../CONTRIBUTING.md) - How to contribute to BetterSEQTA+
### Plugin System
- [Plugin System Overview](./plugins/README.md) - Overview of the plugin system
- [Creating Your First Plugin](./plugins/creating-plugins.md) - Guide to creating a simple plugin
### Settings System
- [Settings System Overview](./settings/README.md) - How the type-safe settings system works
- [Creating Plugins with Settings](./settings/creating-plugins.md) - How to use the decorator-based settings in plugins
- [Creating Custom UI Components](./settings/custom-ui-components.md) - How to create custom UI components for settings
### Advanced Topics
- [TypeScript Type System](./advanced/typescript.md) - How BetterSEQTA+ leverages TypeScript for type safety
- [Plugin API Reference](./advanced/plugin-api.md) - Detailed reference for the Plugin API
- [Storage API Reference](./advanced/storage-api.md) - Detailed reference for the Storage API
## Core Concepts
BetterSEQTA+ is built around several core concepts:
1. **Plugin System**: BetterSEQTA+ uses a plugin system to extend SEQTA with new features. Plugins are self-contained pieces of code that can be enabled or disabled by the user.
2. **Type-Safe Settings**: Each plugin can define settings that are type-safe and automatically rendered in the settings UI. The settings system uses TypeScript decorators to make it easy to define settings with proper typing.
3. **Storage API**: Plugins can use the Storage API to persist data between sessions. The Storage API is also type-safe, ensuring that plugins can only access their own data.
4. **SEQTA Integration**: BetterSEQTA+ integrates with SEQTA Learn by injecting code into the page. This allows plugins to modify the SEQTA UI and add new features.
## Getting Help
If you need help with BetterSEQTA+, you can:
- [Open an Issue](https://github.com/SeqtaLearning/betterseqta-plus/issues) - Report bugs or request features
- [Join the Discord](https://discord.gg/YzmbnCDkat) - Chat with the community
- [Email the Maintainers](mailto:betterseqta@example.com) - Contact the maintainers directly
## Contributing to the Documentation
We welcome contributions to the documentation! If you find something unclear or missing, please open an issue or submit a pull request.
To contribute to the documentation:
1. Fork the repository
2. Make your changes to the documentation files
3. Submit a pull request with a clear description of your changes
## License
BetterSEQTA+ is licensed under the [MIT License](../LICENSE).
+421
View File
@@ -0,0 +1,421 @@
# Plugin API Reference
This document provides a comprehensive reference for the BetterSEQTA+ Plugin API. The Plugin API is the primary interface through which plugins interact with BetterSEQTA+ and SEQTA Learn.
## Overview
The Plugin API consists of several sub-APIs:
```typescript
export interface PluginAPI<T extends PluginSettings, S = any> {
seqta: SEQTAAPI;
settings: SettingsAPI<T>;
storage: TypedStorageAPI<S>;
events: EventsAPI;
}
```
Each plugin receives an instance of this API when it is initialized, with the appropriate generic types for its settings and storage.
## SEQTA API
The SEQTA API provides methods for interacting with the SEQTA Learn interface.
```typescript
export interface SEQTAAPI {
onPageLoad(path: string, callback: PageLoadCallback): () => void;
getCurrentPath(): string;
waitForElement(selector: string, options?: WaitForElementOptions): Promise<Element>;
createStyleElement(css: string): HTMLStyleElement;
}
```
### `onPageLoad(path: string, callback: PageLoadCallback): () => void`
Registers a callback to be called when a specific page is loaded in SEQTA Learn.
**Parameters:**
- `path`: The URL path to match (e.g., `/timetable`, `/assessments`). Can be a string or a regular expression.
- `callback`: A function to be called when the page is loaded.
**Returns:** A function that, when called, will remove the page load listener.
**Example:**
```typescript
const removeListener = api.seqta.onPageLoad('/timetable', () => {
console.log('Timetable page loaded!');
});
// Later, to remove the listener
removeListener();
```
### `getCurrentPath(): string`
Gets the current URL path in SEQTA Learn.
**Returns:** The current URL path as a string.
**Example:**
```typescript
const currentPath = api.seqta.getCurrentPath();
console.log(`Current path: ${currentPath}`);
```
### `waitForElement(selector: string, options?: WaitForElementOptions): Promise<Element>`
Waits for an element matching the given selector to appear in the DOM.
**Parameters:**
- `selector`: A CSS selector to match the element.
- `options`: (Optional) An object with the following properties:
- `timeout`: The maximum time to wait for the element, in milliseconds. Default: 5000.
- `interval`: The interval between checks, in milliseconds. Default: 100.
**Returns:** A Promise that resolves to the matched element, or rejects if the timeout is reached.
**Example:**
```typescript
try {
const timetableElement = await api.seqta.waitForElement('.timetable');
console.log('Timetable element found:', timetableElement);
} catch (error) {
console.error('Timetable element not found:', error);
}
```
### `createStyleElement(css: string): HTMLStyleElement`
Creates a style element with the given CSS and adds it to the document head.
**Parameters:**
- `css`: The CSS to add to the style element.
**Returns:** The created style element.
**Example:**
```typescript
const styleElement = api.seqta.createStyleElement(`
.timetable {
background-color: #f5f5f5;
}
.timetable-cell {
border: 1px solid #ccc;
}
`);
// Later, to remove the style
styleElement.remove();
```
## Settings API
The Settings API provides type-safe access to plugin settings.
```typescript
export interface SettingsAPI<T extends PluginSettings> {
get<K extends keyof T>(key: K): SettingValue<T[K]>;
set<K extends keyof T>(key: K, value: SettingValue<T[K]>): void;
onChange<K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void): () => void;
getAll(): { [K in keyof T]: SettingValue<T[K]> };
}
```
### `get<K extends keyof T>(key: K): SettingValue<T[K]>`
Gets the value of a setting.
**Parameters:**
- `key`: The key of the setting to get.
**Returns:** The value of the setting.
**Example:**
```typescript
const isEnabled = api.settings.get('enabled');
console.log(`Plugin enabled: ${isEnabled}`);
```
### `set<K extends keyof T>(key: K, value: SettingValue<T[K]>): void`
Sets the value of a setting.
**Parameters:**
- `key`: The key of the setting to set.
- `value`: The new value for the setting.
**Example:**
```typescript
api.settings.set('enabled', true);
console.log('Plugin enabled!');
```
### `onChange<K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void): () => void`
Registers a callback to be called when a setting changes.
**Parameters:**
- `key`: The key of the setting to watch.
- `callback`: A function to be called when the setting changes.
**Returns:** A function that, when called, will remove the change listener.
**Example:**
```typescript
const removeListener = api.settings.onChange('enabled', (newValue) => {
console.log(`Plugin enabled changed to: ${newValue}`);
if (newValue) {
// Enable functionality
} else {
// Disable functionality
}
});
// Later, to remove the listener
removeListener();
```
### `getAll(): { [K in keyof T]: SettingValue<T[K]> }`
Gets all settings as an object.
**Returns:** An object containing all settings.
**Example:**
```typescript
const allSettings = api.settings.getAll();
console.log('All settings:', allSettings);
```
## Storage API
The Storage API provides type-safe persistent storage for plugin data.
```typescript
export interface TypedStorageAPI<S = any> {
get<K extends keyof S>(key: K): S[K] | undefined;
set<K extends keyof S>(key: K, value: S[K]): void;
onChange<K extends keyof S>(key: K, callback: (value: S[K]) => void): () => void;
getAll(): Partial<S>;
clear(): void;
}
```
### `get<K extends keyof S>(key: K): S[K] | undefined`
Gets a value from storage.
**Parameters:**
- `key`: The key of the value to get.
**Returns:** The stored value, or `undefined` if it doesn't exist.
**Example:**
```typescript
const lastRun = api.storage.get('lastRun');
console.log(`Last run: ${lastRun || 'Never'}`);
```
### `set<K extends keyof S>(key: K, value: S[K]): void`
Sets a value in storage.
**Parameters:**
- `key`: The key of the value to set.
- `value`: The new value to store.
**Example:**
```typescript
api.storage.set('lastRun', new Date().toISOString());
console.log('Last run updated!');
```
### `onChange<K extends keyof S>(key: K, callback: (value: S[K]) => void): () => void`
Registers a callback to be called when a stored value changes.
**Parameters:**
- `key`: The key of the value to watch.
- `callback`: A function to be called when the value changes.
**Returns:** A function that, when called, will remove the change listener.
**Example:**
```typescript
const removeListener = api.storage.onChange('lastRun', (newValue) => {
console.log(`Last run updated to: ${newValue}`);
});
// Later, to remove the listener
removeListener();
```
### `getAll(): Partial<S>`
Gets all stored values as an object.
**Returns:** An object containing all stored values.
**Example:**
```typescript
const allStoredValues = api.storage.getAll();
console.log('All stored values:', allStoredValues);
```
### `clear(): void`
Clears all stored values.
**Example:**
```typescript
api.storage.clear();
console.log('All stored values cleared!');
```
## Events API
The Events API allows plugins to emit and listen for events.
```typescript
export interface EventsAPI {
on<T = any>(event: string, callback: (data: T) => void): () => void;
emit<T = any>(event: string, data: T): void;
}
```
### `on<T = any>(event: string, callback: (data: T) => void): () => void`
Registers a callback to be called when an event is emitted.
**Parameters:**
- `event`: The name of the event to listen for.
- `callback`: A function to be called when the event is emitted.
**Returns:** A function that, when called, will remove the event listener.
**Example:**
```typescript
const removeListener = api.events.on('assessmentLoaded', (data) => {
console.log('Assessment loaded:', data);
});
// Later, to remove the listener
removeListener();
```
### `emit<T = any>(event: string, data: T): void`
Emits an event with the given data.
**Parameters:**
- `event`: The name of the event to emit.
- `data`: The data to include with the event.
**Example:**
```typescript
api.events.emit('myPluginEvent', { message: 'Hello from My Plugin!' });
```
## Using the Plugin API in Practice
### Combining APIs for Complex Functionality
The true power of the Plugin API comes from combining the different sub-APIs to create complex functionality. Here's an example of a plugin that enhances the timetable view:
```typescript
run: (api) => {
if (!api.settings.get('enabled')) {
return;
}
// Initialize storage if needed
if (api.storage.get('zoomLevel') === undefined) {
api.storage.set('zoomLevel', 1);
}
// Add styles based on current zoom level
const updateStyles = () => {
const zoomLevel = api.storage.get('zoomLevel');
const styleElement = api.seqta.createStyleElement(`
.timetable-cell {
transform: scale(${zoomLevel});
}
`);
return styleElement;
};
let currentStyleElement = updateStyles();
// Listen for storage changes
const removeStorageListener = api.storage.onChange('zoomLevel', () => {
// Remove old styles and add new ones
currentStyleElement.remove();
currentStyleElement = updateStyles();
});
// Add UI controls
const removePageListener = api.seqta.onPageLoad('/timetable', async () => {
try {
const timetableElement = await api.seqta.waitForElement('.timetable');
// Create controls
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
const zoomInButton = controlsDiv.querySelector('button:first-child');
const zoomOutButton = controlsDiv.querySelector('button:last-child');
zoomInButton.addEventListener('click', () => {
const currentZoom = api.storage.get('zoomLevel');
api.storage.set('zoomLevel', Math.min(currentZoom + 0.1, 2));
});
zoomOutButton.addEventListener('click', () => {
const currentZoom = api.storage.get('zoomLevel');
api.storage.set('zoomLevel', Math.max(currentZoom - 0.1, 0.5));
});
// Emit an event
api.events.emit('timetableEnhanced', { zoomLevel: api.storage.get('zoomLevel') });
} catch (error) {
console.error('Error enhancing timetable:', error);
}
});
// Return cleanup function
return () => {
removeStorageListener();
removePageListener();
currentStyleElement.remove();
};
}
```
### Error Handling
Always handle errors gracefully to prevent your plugin from crashing:
```typescript
try {
// Your code
} catch (error) {
console.error('Plugin error:', error);
}
```
### Performance Considerations
Be mindful of performance when using the Plugin API:
1. Use `onPageLoad` efficiently to avoid unnecessary work.
2. Clean up event listeners and DOM elements when they're no longer needed.
3. Use `waitForElement` with appropriate timeouts to avoid hanging indefinitely.
## Next Steps
- [Explore the Storage API in Detail](./storage-api.md)
- [Learn About Third-Party Plugins](./third-party-plugins.md)
- [Contribute to BetterSEQTA+](../contributing.md)
+583
View File
@@ -0,0 +1,583 @@
# Storage API Guide
The Storage API is a powerful component of BetterSEQTA+ that allows plugins to store and retrieve data persistently. This guide covers the TypedStorageAPI in detail, including advanced usage patterns and best practices.
## Overview
The Storage API provides a type-safe, persistent storage mechanism for plugins. Each plugin has its own storage namespace, ensuring that plugins cannot interfere with each other's data.
The Storage API is generic, allowing plugins to define their own storage structure through TypeScript interfaces:
```typescript
export interface TypedStorageAPI<S = any> {
get<K extends keyof S>(key: K): S[K] | undefined;
set<K extends keyof S>(key: K, value: S[K]): void;
onChange<K extends keyof S>(key: K, callback: (value: S[K]) => void): () => void;
getAll(): Partial<S>;
clear(): void;
}
```
## Defining Your Storage Structure
Before using the Storage API, you should define the structure of your plugin's storage using a TypeScript interface:
```typescript
interface MyPluginStorage {
lastRun: string;
userPreferences: {
theme: 'light' | 'dark';
fontSize: number;
};
savedItems: string[];
}
```
Then, when creating your plugin, specify this interface as the second generic parameter to the `Plugin` interface:
```typescript
const myPlugin: Plugin<MyPluginSettings, MyPluginStorage> = {
// Plugin implementation
};
```
## Using the Storage API
### Getting and Setting Values
The most basic operations are getting and setting values:
```typescript
// Get a value (returns undefined if not set)
const lastRun = api.storage.get('lastRun');
// Set a value
api.storage.set('lastRun', new Date().toISOString());
// Get a nested value
const theme = api.storage.get('userPreferences')?.theme;
// Set a nested value (make sure to preserve existing properties)
const preferences = api.storage.get('userPreferences') || { theme: 'light', fontSize: 14 };
api.storage.set('userPreferences', { ...preferences, theme: 'dark' });
```
### Working with Complex Objects
When working with complex objects, it's important to remember that the Storage API works with references. To update a property of a complex object, you need to create a new object with the updated property:
```typescript
// Get the current preferences
const preferences = api.storage.get('userPreferences') || { theme: 'light', fontSize: 14 };
// Update a property (wrong way - changes won't be detected)
// preferences.theme = 'dark';
// api.storage.set('userPreferences', preferences);
// Update a property (correct way)
api.storage.set('userPreferences', { ...preferences, theme: 'dark' });
```
### Working with Arrays
Similarly, when working with arrays, you need to create a new array to trigger change detection:
```typescript
// Get the current items
const items = api.storage.get('savedItems') || [];
// Add an item (wrong way - changes won't be detected)
// items.push('new item');
// api.storage.set('savedItems', items);
// Add an item (correct way)
api.storage.set('savedItems', [...items, 'new item']);
// Remove an item
api.storage.set('savedItems', items.filter(item => item !== 'item to remove'));
```
### Handling Default Values
When getting a value that might not exist yet, you should provide a default value:
```typescript
const preferences = api.storage.get('userPreferences') || { theme: 'light', fontSize: 14 };
```
Or, as part of your plugin initialization:
```typescript
run: (api) => {
// Initialize storage with default values
if (api.storage.get('lastRun') === undefined) {
api.storage.set('lastRun', new Date().toISOString());
}
if (api.storage.get('userPreferences') === undefined) {
api.storage.set('userPreferences', { theme: 'light', fontSize: 14 });
}
if (api.storage.get('savedItems') === undefined) {
api.storage.set('savedItems', []);
}
// Rest of plugin logic
};
```
## Advanced Usage
### Reacting to Storage Changes
The Storage API allows you to register callbacks that will be called when a value changes:
```typescript
const removeListener = api.storage.onChange('userPreferences', (newPreferences) => {
console.log('User preferences changed:', newPreferences);
// Update UI based on new preferences
if (newPreferences?.theme === 'dark') {
document.body.classList.add('dark-theme');
} else {
document.body.classList.remove('dark-theme');
}
});
// Later, to remove the listener
removeListener();
```
This is particularly useful for updating the UI in response to storage changes, whether those changes were made by your plugin or by the user through a settings panel.
### Synchronizing with Settings
In some cases, you might want to synchronize certain storage values with settings. For example, you might want to save the user's preferences as settings:
```typescript
// When user preferences change
api.storage.onChange('userPreferences', (newPreferences) => {
// Update the settings
api.settings.set('theme', newPreferences?.theme || 'light');
api.settings.set('fontSize', newPreferences?.fontSize || 14);
});
// When settings change
api.settings.onChange('theme', (newTheme) => {
// Update the storage
const preferences = api.storage.get('userPreferences') || { theme: 'light', fontSize: 14 };
api.storage.set('userPreferences', { ...preferences, theme: newTheme });
});
```
### Clearing Storage
You can clear all stored values for your plugin:
```typescript
api.storage.clear();
```
This is useful when you want to reset your plugin to its default state.
### Getting All Stored Values
You can get all stored values as an object:
```typescript
const allStoredValues = api.storage.getAll();
console.log('All stored values:', allStoredValues);
```
This is useful for debugging or for implementing a "reset to defaults" feature.
## Storage Persistence
The Storage API persists data using browser storage mechanisms (e.g., `localStorage`). This means that the data will be available across page refreshes and browser restarts, but will not be shared across different devices or browsers.
The persistence is handled automatically by BetterSEQTA+, so you don't need to worry about saving or loading data explicitly.
## Type Safety Considerations
The TypedStorageAPI is designed to be type-safe, but there are a few things to keep in mind:
1. **Keys Must Exist in Interface**: You can only use keys that are defined in your storage interface.
2. **Values Must Match Type**: The values you set must match the types defined in your interface.
3. **Default Values for Complex Types**: When getting a value that might not exist, make sure to provide a default value with the correct type.
## Best Practices
### 1. Define a Clear Storage Structure
Define a clear and well-documented storage structure using a TypeScript interface. This makes it easier to understand what data your plugin is storing and how it's organized.
```typescript
interface MyPluginStorage {
/**
* The timestamp of the last time the plugin was run.
* Format: ISO 8601 string
*/
lastRun: string;
/**
* User-specific preferences for the plugin.
*/
userPreferences: {
/**
* The user's preferred theme.
*/
theme: 'light' | 'dark';
/**
* The user's preferred font size in pixels.
*/
fontSize: number;
};
/**
* A list of items saved by the user.
*/
savedItems: string[];
}
```
### 2. Initialize Storage Early
Initialize your storage with default values as early as possible, ideally at the beginning of your plugin's `run` method. This ensures that the values are available throughout your plugin.
```typescript
run: (api) => {
// Initialize storage with default values
const initializeStorage = () => {
if (api.storage.get('lastRun') === undefined) {
api.storage.set('lastRun', new Date().toISOString());
}
if (api.storage.get('userPreferences') === undefined) {
api.storage.set('userPreferences', { theme: 'light', fontSize: 14 });
}
if (api.storage.get('savedItems') === undefined) {
api.storage.set('savedItems', []);
}
};
initializeStorage();
// Rest of plugin logic
};
```
### 3. Handle Missing Values Gracefully
Always handle the case where a value might not exist yet. This can happen if the user is running your plugin for the first time, or if there was an issue with storage.
```typescript
const preferences = api.storage.get('userPreferences');
const theme = preferences?.theme || 'light';
const fontSize = preferences?.fontSize || 14;
```
### 4. Clean Up Listeners
If you register change listeners, make sure to clean them up when your plugin is stopped. This prevents memory leaks and ensures that the listeners are not called after your plugin is disabled.
```typescript
run: (api) => {
// Register listeners
const listeners = [
api.storage.onChange('userPreferences', handlePreferencesChange),
api.storage.onChange('savedItems', handleSavedItemsChange),
];
// Return cleanup function
return () => {
// Clean up listeners
listeners.forEach(removeListener => removeListener());
};
};
```
### 5. Batch Updates When Possible
If you need to update multiple values, consider batching them to reduce the number of storage operations:
```typescript
// Instead of this:
api.storage.set('userPreferences', { ...preferences, theme: 'dark' });
api.storage.set('lastRun', new Date().toISOString());
api.storage.set('savedItems', [...items, 'new item']);
// Consider using a helper function:
const batchUpdate = () => {
const preferences = api.storage.get('userPreferences') || { theme: 'light', fontSize: 14 };
const items = api.storage.get('savedItems') || [];
api.storage.set('userPreferences', { ...preferences, theme: 'dark' });
api.storage.set('lastRun', new Date().toISOString());
api.storage.set('savedItems', [...items, 'new item']);
};
batchUpdate();
```
## Example: A Complete Plugin with Storage
Here's a complete example of a plugin that uses the Storage API effectively:
```typescript
interface NotesPluginStorage {
notes: {
id: string;
title: string;
content: string;
createdAt: string;
updatedAt: string;
}[];
activeNoteId: string | null;
view: 'list' | 'detail';
}
const notesPlugin: Plugin<NotesPluginSettings, NotesPluginStorage> = {
id: 'notes',
name: 'Notes',
description: 'A simple notes plugin for BetterSEQTA+',
version: '1.0.0',
settings: {
enabled: {
type: 'boolean',
default: true,
title: 'Enable Notes',
description: 'Turn the notes plugin on or off',
},
autoSave: {
type: 'boolean',
default: true,
title: 'Auto Save',
description: 'Automatically save notes as you type',
},
},
run: (api) => {
if (!api.settings.get('enabled')) {
return;
}
// Initialize storage with default values
if (api.storage.get('notes') === undefined) {
api.storage.set('notes', []);
}
if (api.storage.get('activeNoteId') === undefined) {
api.storage.set('activeNoteId', null);
}
if (api.storage.get('view') === undefined) {
api.storage.set('view', 'list');
}
// Create and render the UI
let notesContainer: HTMLElement | null = null;
let removePageListener: () => void;
const renderUI = async () => {
const pageContainer = await api.seqta.waitForElement('#page-container');
if (!notesContainer) {
notesContainer = document.createElement('div');
notesContainer.className = 'notes-plugin-container';
pageContainer.appendChild(notesContainer);
}
renderNotes();
};
const renderNotes = () => {
if (!notesContainer) return;
const notes = api.storage.get('notes') || [];
const activeNoteId = api.storage.get('activeNoteId');
const view = api.storage.get('view');
if (view === 'list') {
notesContainer.innerHTML = `
<div class="notes-header">
<h2>Notes</h2>
<button class="add-note-btn">Add Note</button>
</div>
<div class="notes-list">
${notes.length === 0
? '<p>No notes yet. Click "Add Note" to create one.</p>'
: notes.map(note => `
<div class="note-item ${note.id === activeNoteId ? 'active' : ''}">
<h3>${note.title}</h3>
<p>${note.content.substring(0, 50)}${note.content.length > 50 ? '...' : ''}</p>
<div class="note-actions">
<button class="view-note-btn" data-id="${note.id}">View</button>
<button class="delete-note-btn" data-id="${note.id}">Delete</button>
</div>
</div>
`).join('')}
</div>
`;
// Add event listeners
notesContainer.querySelector('.add-note-btn')?.addEventListener('click', addNote);
notesContainer.querySelectorAll('.view-note-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const id = (e.target as HTMLElement).getAttribute('data-id');
if (id) viewNote(id);
});
});
notesContainer.querySelectorAll('.delete-note-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const id = (e.target as HTMLElement).getAttribute('data-id');
if (id) deleteNote(id);
});
});
} else if (view === 'detail') {
const activeNote = notes.find(note => note.id === activeNoteId);
if (!activeNote) {
api.storage.set('view', 'list');
renderNotes();
return;
}
notesContainer.innerHTML = `
<div class="notes-header">
<button class="back-btn">Back to List</button>
<h2>Editing Note</h2>
</div>
<div class="note-detail">
<input type="text" class="note-title" value="${activeNote.title}">
<textarea class="note-content">${activeNote.content}</textarea>
<div class="note-actions">
<button class="save-note-btn">Save</button>
</div>
</div>
`;
// Add event listeners
notesContainer.querySelector('.back-btn')?.addEventListener('click', () => {
api.storage.set('view', 'list');
renderNotes();
});
const titleInput = notesContainer.querySelector('.note-title') as HTMLInputElement;
const contentTextarea = notesContainer.querySelector('.note-content') as HTMLTextAreaElement;
if (api.settings.get('autoSave')) {
let timeout: number;
const autoSave = () => {
clearTimeout(timeout);
timeout = setTimeout(() => {
updateNote(activeNoteId, titleInput.value, contentTextarea.value);
}, 500) as unknown as number;
};
titleInput.addEventListener('input', autoSave);
contentTextarea.addEventListener('input', autoSave);
}
notesContainer.querySelector('.save-note-btn')?.addEventListener('click', () => {
updateNote(activeNoteId, titleInput.value, contentTextarea.value);
api.storage.set('view', 'list');
renderNotes();
});
}
};
const addNote = () => {
const notes = api.storage.get('notes') || [];
const newNote = {
id: Date.now().toString(),
title: 'New Note',
content: '',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
api.storage.set('notes', [...notes, newNote]);
api.storage.set('activeNoteId', newNote.id);
api.storage.set('view', 'detail');
renderNotes();
};
const viewNote = (id: string) => {
api.storage.set('activeNoteId', id);
api.storage.set('view', 'detail');
renderNotes();
};
const updateNote = (id: string, title: string, content: string) => {
const notes = api.storage.get('notes') || [];
const updatedNotes = notes.map(note =>
note.id === id
? { ...note, title, content, updatedAt: new Date().toISOString() }
: note
);
api.storage.set('notes', updatedNotes);
};
const deleteNote = (id: string) => {
const notes = api.storage.get('notes') || [];
const updatedNotes = notes.filter(note => note.id !== id);
api.storage.set('notes', updatedNotes);
if (api.storage.get('activeNoteId') === id) {
api.storage.set('activeNoteId', null);
}
renderNotes();
};
// Register listeners
const storageListeners = [
api.storage.onChange('notes', renderNotes),
api.storage.onChange('activeNoteId', renderNotes),
api.storage.onChange('view', renderNotes),
];
// Set up page load listener
removePageListener = api.seqta.onPageLoad('*', renderUI);
// Return cleanup function
return () => {
// Remove event listeners
storageListeners.forEach(removeListener => removeListener());
removePageListener();
// Remove UI
notesContainer?.remove();
notesContainer = null;
};
},
};
export default notesPlugin;
```
## Summary
The Storage API is a powerful tool for maintaining state in your BetterSEQTA+ plugins. By following the best practices outlined in this guide, you can create robust and reliable plugins that provide a great user experience.
Key takeaways:
1. Define a clear storage structure using TypeScript interfaces
2. Initialize storage early with default values
3. Handle missing values gracefully
4. Clean up listeners when your plugin is stopped
5. Use the onChange method to react to storage changes
With these principles in mind, you can leverage the full power of the Storage API in your plugins.
## Next Steps
- [Explore the Plugin API](./plugin-api.md)
- [Learn About Third-Party Plugins](./third-party-plugins.md)
- [Contribute to BetterSEQTA+](../contributing.md)
+550
View File
@@ -0,0 +1,550 @@
# Developing Third-Party Plugins
BetterSEQTA+ supports third-party plugins, allowing developers to extend its functionality beyond what's provided by the built-in plugins. This guide covers everything you need to know about developing, distributing, and installing third-party plugins.
## Introduction to Third-Party Plugins
Third-party plugins are plugins developed outside of the main BetterSEQTA+ codebase. They can be created by anyone and distributed to users who want to extend their BetterSEQTA+ experience.
Unlike built-in plugins, which are included with BetterSEQTA+, third-party plugins must be installed separately by users. This allows for a wide range of extensions without bloating the core application.
## Plugin Structure
A third-party plugin is a JavaScript or TypeScript module that exports a plugin object conforming to the `Plugin` interface. It can be distributed as a single file or as a package with multiple files.
### Basic Structure
```typescript
// my-awesome-plugin.ts
import { Plugin, PluginAPI, PluginSettings } from 'betterseqta-plugin-api';
export interface MyAwesomePluginSettings extends PluginSettings {
enabled: {
type: 'boolean';
default: true;
title: 'Enable My Awesome Plugin';
description: 'Turn my awesome plugin on or off';
};
// Add more settings as needed
}
export interface MyAwesomePluginStorage {
lastRun: string;
// Add more storage fields as needed
}
const myAwesomePlugin: Plugin<MyAwesomePluginSettings, MyAwesomePluginStorage> = {
id: 'my-awesome-plugin',
name: 'My Awesome Plugin',
description: 'A simple plugin for BetterSEQTA+',
version: '1.0.0',
author: 'Your Name',
license: 'MIT',
settings: {
enabled: {
type: 'boolean',
default: true,
title: 'Enable My Awesome Plugin',
description: 'Turn my awesome plugin on or off',
},
// Initialize your settings here
},
run: (api) => {
// Your plugin logic goes here
console.log('My Awesome Plugin is running!');
// Return a cleanup function (optional but recommended)
return () => {
console.log('My Awesome Plugin is cleaning up!');
// Cleanup logic goes here
};
},
};
export default myAwesomePlugin;
```
### Plugin Manifest
For plugins that consist of multiple files or that need additional resources, a manifest file is recommended. This file provides metadata about the plugin and points to the main plugin file.
```json
// plugin.json
{
"id": "my-awesome-plugin",
"name": "My Awesome Plugin",
"description": "A simple plugin for BetterSEQTA+",
"version": "1.0.0",
"author": "Your Name",
"license": "MIT",
"main": "index.js",
"dependencies": {
"betterseqta-plus": "^1.0.0"
}
}
```
## Development Environment
### Setting Up Your Development Environment
1. Clone the BetterSEQTA+ repository or create a new project:
```bash
git clone https://github.com/yourusername/betterseqta-plus-plugin.git
cd betterseqta-plus-plugin
```
2. Initialize a new npm project:
```bash
npm init -y
```
3. Install the necessary dependencies:
```bash
npm install --save-dev typescript webpack webpack-cli @types/node
npm install --save betterseqta-plugin-api
```
4. Set up TypeScript configuration:
```json
// tsconfig.json
{
"compilerOptions": {
"target": "es2020",
"module": "esnext",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"declaration": true,
"outDir": "dist",
"lib": ["es2020", "dom"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
```
5. Set up webpack configuration:
```javascript
// webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.ts',
mode: 'production',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: 'index.js',
path: path.resolve(__dirname, 'dist'),
library: {
type: 'umd',
name: 'MyAwesomePlugin',
},
globalObject: 'this',
},
externals: {
'betterseqta-plugin-api': 'betterseqta-plugin-api',
},
};
```
6. Create your plugin in the `src` directory:
```bash
mkdir -p src
touch src/index.ts
```
7. Add build scripts to your `package.json`:
```json
"scripts": {
"build": "webpack",
"dev": "webpack --watch"
}
```
### Developing Your Plugin
1. Implement your plugin in `src/index.ts` following the structure shown above.
2. Build your plugin:
```bash
npm run build
```
3. For development, you can use the watch mode:
```bash
npm run dev
```
### Testing Your Plugin
There are several ways to test your plugin during development:
#### Method 1: Plugin Development Mode
BetterSEQTA+ provides a development mode for testing plugins:
1. Open BetterSEQTA+ settings
2. Navigate to the "Developer" section
3. Enable "Plugin Development Mode"
4. Click "Load Local Plugin" and select your plugin's directory or main file
#### Method 2: Manual Installation
You can manually install your plugin in a development environment:
1. Build your plugin
2. Copy the output file to the BetterSEQTA+ plugins directory:
```bash
cp dist/index.js ~/.betterseqta/plugins/my-awesome-plugin/
```
3. Reload BetterSEQTA+
## Packaging and Distribution
### Creating a Plugin Package
A plugin package should include:
1. **The plugin code**: Compiled JavaScript file(s)
2. **A manifest file**: `plugin.json` with metadata
3. **Documentation**: README.md and other documentation
4. **License**: A license file
Example file structure:
```
my-awesome-plugin/
├── index.js # Compiled plugin code
├── plugin.json # Plugin manifest
├── README.md # Documentation
└── LICENSE # License file
```
### Publishing Your Plugin
You can publish your plugin in several ways:
#### 1. GitHub Repository
Host your plugin on GitHub:
1. Create a new repository
2. Push your plugin code
3. Create releases for new versions
4. Users can install it using the GitHub URL
#### 2. npm Package
Publish your plugin as an npm package:
1. Prepare your package:
```json
// package.json
{
"name": "betterseqta-plugin-my-awesome",
"version": "1.0.0",
"description": "An awesome plugin for BetterSEQTA+",
"main": "dist/index.js",
"files": [
"dist",
"plugin.json",
"README.md",
"LICENSE"
],
"keywords": [
"betterseqta",
"plugin"
],
"author": "Your Name",
"license": "MIT"
}
```
2. Build your plugin:
```bash
npm run build
```
3. Publish to npm:
```bash
npm publish
```
#### 3. BetterSEQTA+ Plugin Directory
Submit your plugin to the official BetterSEQTA+ plugin directory:
1. Ensure your plugin follows all guidelines
2. Create a pull request to add your plugin to the directory
3. Once approved, your plugin will be available in the BetterSEQTA+ plugin browser
### Creating a Plugin Listing
Your plugin listing should include:
1. **Name and Description**: Clear, concise name and description
2. **Screenshots**: Showcase your plugin in action
3. **Features**: List of key features
4. **Installation Instructions**: How to install your plugin
5. **Configuration**: How to configure your plugin
6. **Support Information**: Where users can get help
## Plugin Installation Guide
Include instructions for users to install your plugin:
### Method 1: Using the Plugin Browser
1. Open BetterSEQTA+
2. Go to Settings → Plugins → Browse
3. Search for "My Awesome Plugin"
4. Click "Install"
### Method 2: Manual Installation
1. Download the plugin files
2. Create a folder in the BetterSEQTA+ plugins directory:
```bash
mkdir -p ~/.betterseqta/plugins/my-awesome-plugin
```
3. Copy the plugin files to the folder:
```bash
cp -r * ~/.betterseqta/plugins/my-awesome-plugin/
```
4. Restart BetterSEQTA+
### Method 3: Using npm
If your plugin is published on npm:
```bash
npm install -g betterseqta-plugin-my-awesome
```
## Best Practices for Plugin Development
### Security Considerations
1. **Respect User Privacy**: Don't collect unnecessary data
2. **Secure Data Handling**: Encrypt sensitive data
3. **Minimize Permissions**: Only request the permissions you need
4. **Code Review**: Get others to review your code for security issues
### Performance Optimization
1. **Minimize DOM Operations**: Batch DOM operations when possible
2. **Use Event Delegation**: Instead of adding many individual event listeners
3. **Lazy Loading**: Load resources only when needed
4. **Throttle and Debounce**: Limit frequent events like scroll or resize
### User Experience
1. **Clear UI**: Keep your UI simple and intuitive
2. **Consistent Design**: Follow SEQTA's design language
3. **Responsive Feedback**: Provide feedback for user actions
4. **Error Handling**: Gracefully handle errors and inform the user
### Accessibility
1. **Keyboard Navigation**: Ensure all features are accessible via keyboard
2. **Screen Reader Support**: Use appropriate ARIA attributes
3. **Color Contrast**: Ensure sufficient contrast for text
4. **Font Size**: Allow for text resizing
### Maintenance
1. **Version Control**: Use semantic versioning
2. **Changelog**: Maintain a changelog
3. **Documentation**: Keep documentation up to date
4. **Issue Tracking**: Set up an issue tracker for bug reports and feature requests
## Advanced Topics
### Plugin Communication
Plugins can communicate with each other using the Events API:
```typescript
// Plugin A: Emit an event
api.events.emit('pluginA:dataUpdated', { data: 'some data' });
// Plugin B: Listen for the event
api.events.on('pluginA:dataUpdated', (data) => {
console.log('Data from Plugin A:', data);
});
```
### Plugin Dependencies
If your plugin depends on other plugins, you should specify this in your manifest:
```json
// plugin.json
{
"id": "my-awesome-plugin",
"name": "My Awesome Plugin",
"dependencies": {
"another-plugin": "^1.0.0"
}
}
```
Your plugin's `run` method should check if the dependencies are available:
```typescript
run: (api) => {
// Check if dependencies are available
if (!window.betterseqta.plugins.isPluginLoaded('another-plugin')) {
console.error('My Awesome Plugin requires Another Plugin to be installed and enabled');
return;
}
// Plugin logic
}
```
### Plugin Configuration UI
For complex plugins, you might want to provide a custom settings UI beyond what the automatic settings generation provides:
```typescript
settings: {
enabled: {
type: 'boolean',
default: true,
title: 'Enable My Awesome Plugin',
description: 'Turn my awesome plugin on or off',
},
customUI: {
type: 'custom',
render: (container, value, onChange) => {
// Create a custom UI
const div = document.createElement('div');
div.innerHTML = `
<h3>Custom Settings</h3>
<p>This is a custom settings UI.</p>
<button>Click Me</button>
`;
// Add event listeners
div.querySelector('button').addEventListener('click', () => {
// Do something
onChange({ clicked: true });
});
// Append to container
container.appendChild(div);
// Return a cleanup function
return () => {
// Clean up event listeners
div.querySelector('button').removeEventListener('click', handleClick);
};
}
}
}
```
### Internationalization
For plugins with international users, consider adding support for multiple languages:
```typescript
// Define translations
const translations = {
en: {
title: 'My Awesome Plugin',
description: 'A simple plugin for BetterSEQTA+',
button: 'Click Me',
},
fr: {
title: 'Mon Plugin Génial',
description: 'Un plugin simple pour BetterSEQTA+',
button: 'Cliquez-moi',
},
};
// Get the current language
const language = navigator.language.split('-')[0];
const t = translations[language] || translations.en;
// Use translations
console.log(t.title);
```
## Troubleshooting and FAQ
### Common Issues
#### "Plugin not found" error
- Make sure your plugin is installed in the correct directory
- Check that the plugin ID in your code matches the one in the manifest
#### "Plugin failed to load" error
- Check the console for error messages
- Ensure your plugin's code is compatible with the current version of BetterSEQTA+
#### "Settings not saving" issue
- Make sure you're using the Settings API correctly
- Check that your settings have the correct types
### FAQ
#### Q: Can I use external libraries in my plugin?
A: Yes, you can include external libraries. However, be mindful of the size and performance impact.
#### Q: How do I update my plugin?
A: Update the code, increment the version number, and publish the new version. Users will be notified of the update.
#### Q: Can I monetize my plugin?
A: There's no built-in payment system, but you can offer premium versions or accept donations.
#### Q: How do I debug my plugin?
A: Use the browser's developer tools to debug your plugin. BetterSEQTA+ also provides debugging tools in the developer settings.
## Contributing to the Plugin Ecosystem
### Reporting Issues
If you find a bug in the plugin API, report it on the BetterSEQTA+ GitHub repository:
1. Go to the Issues tab
2. Click "New Issue"
3. Select "Plugin API Bug"
4. Fill in the details
### Contributing Documentation
Improvements to the plugin documentation are always welcome:
1. Fork the repository
2. Make your changes
3. Submit a pull request
### Sharing Your Plugins
Share your plugins with the community:
1. Announce your plugin on the BetterSEQTA+ forum
2. Create a GitHub repository for your plugin
3. Submit your plugin to the plugin directory
## Conclusion
Developing third-party plugins for BetterSEQTA+ is a rewarding way to customize and extend the platform. By following these guidelines, you can create high-quality plugins that enhance the experience for yourself and other users.
Remember that the plugin ecosystem thrives on community contributions. Share your plugins, collaborate with other developers, and help make BetterSEQTA+ even better for everyone!
+262
View File
@@ -0,0 +1,262 @@
# Contributing to BetterSEQTA+
Thank you for your interest in contributing to BetterSEQTA+! This document provides guidelines and instructions for contributing to the project.
## Table of Contents
- [Code of Conduct](#code-of-conduct)
- [Getting Started](#getting-started)
- [Setting Up Your Development Environment](#setting-up-your-development-environment)
- [Project Structure](#project-structure)
- [Contributing Code](#contributing-code)
- [Branching Strategy](#branching-strategy)
- [Pull Request Process](#pull-request-process)
- [Coding Standards](#coding-standards)
- [Reporting Bugs](#reporting-bugs)
- [Suggesting Features](#suggesting-features)
- [Writing Documentation](#writing-documentation)
- [Community](#community)
## Code of Conduct
BetterSEQTA+ is committed to providing a welcoming and inclusive environment for all contributors. We expect all participants to adhere to our Code of Conduct, which promotes respectful and harassment-free interaction.
Key points:
- Be respectful and inclusive
- Focus on what is best for the community
- Show empathy towards other community members
- Be open to constructive feedback
## Getting Started
### Setting Up Your Development Environment
1. **Fork the Repository**
Start by forking the BetterSEQTA+ repository to your GitHub account.
2. **Clone Your Fork**
```bash
git clone https://github.com/yourusername/betterseqta-plus.git
cd betterseqta-plus
```
3. **Install Dependencies**
```bash
npm install
```
4. **Set Up Development Environment**
```bash
npm run dev
```
5. **Install in Chrome/Firefox**
Follow the [installation instructions](./installation.md#development-installation) to load the development version into your browser.
### Project Structure
Understanding the project structure will help you navigate the codebase:
```
betterseqta-plus/
├── src/ # Source code
│ ├── plugins/ # Plugin system
│ │ ├── built-in/ # Built-in plugins
│ │ ├── core/ # Plugin core functionality
│ ├── settings/ # Settings system
│ ├── utils/ # Utility functions
│ ├── extension/ # Browser extension code
├── docs/ # Documentation
├── test/ # Test files
├── dist/ # Build output (generated)
├── package.json # Project dependencies
├── tsconfig.json # TypeScript configuration
└── README.md # Project README
```
## Contributing Code
### Branching Strategy
We follow a simple branching strategy:
- `main` - The main development branch
- `feature/*` - Feature branches
- `bugfix/*` - Bug fix branches
- `docs/*` - Documentation branches
Always create a new branch for your changes:
```bash
git checkout -b feature/my-new-feature
```
### Pull Request Process
1. **Keep PRs Focused**
Each pull request should address a single concern. If you're working on multiple features, create separate PRs for each.
2. **Write Clear Commit Messages**
Follow the conventional commits format:
```
feat: add new feature
fix: resolve bug with timetable
docs: update installation instructions
```
3. **Update Documentation**
If your changes require documentation updates, include them in the same PR.
4. **Run Tests**
Make sure all tests pass before submitting your PR:
```bash
npm test
```
5. **Submit Your PR**
When you're ready, push your branch and create a pull request on GitHub.
6. **Code Review**
All PRs will be reviewed by maintainers. Be responsive to feedback and make requested changes.
7. **Merge**
Once approved, a maintainer will merge your PR.
### Coding Standards
We follow TypeScript best practices and have a consistent code style:
1. **Use TypeScript**
All new code should be written in TypeScript with proper typing.
2. **Follow Existing Patterns**
Match the coding style of the existing codebase.
3. **Write Tests**
Add tests for new features and bug fixes.
4. **Document Your Code**
Add comments for complex logic and JSDoc comments for functions.
5. **Use Linters**
We use ESLint and Prettier. Run them before submitting your PR:
```bash
npm run lint
npm run format
```
## Reporting Bugs
If you find a bug, please report it by creating an issue on GitHub:
1. **Search Existing Issues**
Check if the bug has already been reported.
2. **Use the Bug Report Template**
Fill in all sections of the bug report template:
- Description
- Steps to reproduce
- Expected behavior
- Actual behavior
- Screenshots (if applicable)
- Environment (browser, OS, etc.)
3. **Be Specific**
The more details you provide, the easier it will be to fix the bug.
## Suggesting Features
We welcome feature suggestions! To suggest a new feature:
1. **Search Existing Suggestions**
Check if your idea has already been suggested.
2. **Use the Feature Request Template**
Fill in all sections of the feature request template:
- Description
- Use case
- Potential implementation
- Alternatives considered
3. **Be Patient**
Feature requests are evaluated based on alignment with project goals, feasibility, and maintainer bandwidth.
## Writing Documentation
Good documentation is crucial for the project. To contribute to documentation:
1. **Identify Gaps**
Look for areas where documentation is missing or unclear.
2. **Follow Documentation Style**
Maintain a consistent style and format.
3. **Use Clear Language**
Write in simple, clear English. Avoid jargon when possible.
4. **Include Examples**
Code examples and screenshots help users understand.
5. **Submit a PR**
Follow the same process as code contributions, but create a branch with a `docs/` prefix.
## Community
Join our community channels to discuss the project, get help, and connect with other contributors:
- **Discord Server**: [Join our Discord](https://discord.gg/betterseqta)
- **GitHub Discussions**: For longer-form conversations
- **GitHub Issues**: For bug reports and feature requests
## Creating Plugins
If you're interested in creating plugins for BetterSEQTA+, check out our plugin development guides:
- [Creating Your First Plugin](./plugins/creating-plugins.md)
- [Plugin API Reference](./advanced/plugin-api.md)
## Recognition
Contributors are recognized in several ways:
1. **CONTRIBUTORS.md**: All contributors are listed in this file
2. **Release Notes**: Significant contributions are highlighted in release notes
3. **Community Recognition**: Regular shout-outs in community channels
## Questions?
If you have any questions about contributing, please:
1. Check the documentation
2. Ask in the Discord server
3. Open a GitHub Discussion
Thank you for contributing to BetterSEQTA+! Your efforts help make SEQTA better for students and teachers everywhere.
+182
View File
@@ -0,0 +1,182 @@
# Installing BetterSEQTA+
This guide will walk you through the process of installing and setting up BetterSEQTA+ for development or usage.
## Prerequisites
Before you begin, make sure you have the following installed:
- [Node.js](https://nodejs.org/) (v16 or higher)
- [npm](https://www.npmjs.com/) (v7 or higher) or [Bun](https://bun.sh/) (recommended)
- A modern web browser (Chrome, Firefox, or Edge)
## Installation Methods
There are two ways to install BetterSEQTA+:
1. **For Users**: Install the browser extension
2. **For Developers**: Clone the repository and set up the development environment
## For Users: Installing the Browser Extension
BetterSEQTA+ is available as a browser extension for Chrome, Firefox, and Edge.
### Chrome/Edge
1. Visit the [Chrome Web Store page for BetterSEQTA+](https://chrome.google.com/webstore/detail/betterseqta)
2. Click the "Add to Chrome" button
3. Confirm the installation when prompted
4. The extension will be installed and ready to use
### Firefox
1. Visit the [Firefox Add-ons page for BetterSEQTA+](https://addons.mozilla.org/en-US/firefox/addon/betterseqta)
2. Click the "Add to Firefox" button
3. Confirm the installation when prompted
4. The extension will be installed and ready to use
## For Developers: Setting Up the Development Environment
If you want to develop for BetterSEQTA+ or modify the code, follow these steps:
### 1. Clone the Repository
```bash
git clone https://github.com/SeqtaLearning/betterseqta-plus.git
cd betterseqta-plus
```
### 2. Install Dependencies
Using npm:
```bash
npm install
```
Using Bun (recommended):
```bash
bun install
```
### 3. Set Up Environment Variables
Copy the example environment file:
```bash
cp .env.submit.example .env
```
Edit the `.env` file with your SEQTA credentials and settings.
### 4. Start the Development Server
Using npm:
```bash
npm run dev
```
Using Bun:
```bash
bun run dev
```
This will start a development server and build the extension in watch mode.
### 5. Load the Extension in Your Browser
#### Chrome/Edge
1. Open Chrome/Edge and navigate to `chrome://extensions` or `edge://extensions`
2. Enable "Developer mode" using the toggle in the top right
3. Click "Load unpacked" and select the `dist` folder in your BetterSEQTA+ directory
4. The extension should now appear in your extensions list
#### Firefox
1. Open Firefox and navigate to `about:debugging#/runtime/this-firefox`
2. Click "Load Temporary Add-on..."
3. Select the `manifest.json` file in the `dist` folder
4. The extension should now appear in your add-ons list
### 6. Test Your Changes
After making changes to the code, the development server will automatically rebuild the extension. However, you may need to reload the extension in your browser to see the changes:
1. Go to the extensions page in your browser
2. Find BetterSEQTA+ and click the reload icon
3. Refresh any SEQTA Learn pages you have open
## Troubleshooting Installation
### Common Issues
#### "Cannot find module" errors
If you see errors about missing modules, try:
```bash
rm -rf node_modules
npm install
```
Or with Bun:
```bash
rm -rf node_modules
bun install
```
#### Extension not appearing in SEQTA
Make sure:
- You're visiting a SEQTA Learn page
- The extension is enabled
- You've refreshed the page after installing the extension
#### Development build not updating
Try:
1. Stopping the development server
2. Clearing your browser cache
3. Removing the extension from your browser
4. Rebuilding the extension
5. Loading it again
## Updating BetterSEQTA+
### For Users
Browser extensions update automatically, but you can manually check for updates:
- **Chrome/Edge**: Go to `chrome://extensions` or `edge://extensions`, enable Developer mode, and click "Update"
- **Firefox**: Go to `about:addons`, click the gear icon, and select "Check for Updates"
### For Developers
If you're working on the code, pull the latest changes and reinstall dependencies:
```bash
git pull
npm install
npm run dev
```
Or with Bun:
```bash
git pull
bun install
bun run dev
```
## Next Steps
Now that you have BetterSEQTA+ installed, you can:
- [Configure your settings](./settings/README.md)
- [Create your own plugins](./plugins/creating-plugins.md)
- [Contribute to the project](../CONTRIBUTING.md)
+155
View File
@@ -0,0 +1,155 @@
# BetterSEQTA+ Plugin System
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.
## What is a Plugin?
A plugin is a self-contained piece of code that adds functionality to BetterSEQTA+. Plugins can:
- 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
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:
```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)>;
}
```
### 2. Plugin API
When a plugin is run, it receives an instance of the `PluginAPI`, which provides access to various services and utilities:
```typescript
export interface PluginAPI<T extends PluginSettings, S = any> {
seqta: SEQTAAPI;
settings: SettingsAPI<T>;
storage: TypedStorageAPI<S>;
events: EventsAPI;
}
```
- **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
### 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
```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;
};
}
```
### Storage Example
```typescript
interface MyPluginStorage {
lastRefresh: string;
savedItems: string[];
userPreferences: {
theme: 'light' | 'dark';
fontSize: number;
};
}
```
## Decorator-Based Settings
BetterSEQTA+ also offers a more modern, decorator-based approach to defining settings. For more information, see [Creating Plugins with Settings](../settings/creating-plugins.md).
## Plugin API Reference
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).
## Best Practices
When creating plugins for BetterSEQTA+, consider these best practices:
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.
## Next Steps
- [Creating Your First Plugin](./creating-plugins.md)
- [Plugin API Reference](../advanced/plugin-api.md)
- [Typed Storage API](../advanced/storage-api.md)
+269
View File
@@ -0,0 +1,269 @@
# 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.get('enabled')) {
return;
}
// Initialize storage with default values if needed
if (api.storage.get('lastRun') === undefined) {
api.storage.set('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.get('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.get('lastRun');
// Set a stored value
api.storage.set('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)
+301
View File
@@ -0,0 +1,301 @@
# BetterSEQTA+ Settings System
BetterSEQTA+ includes a powerful, type-safe settings system that uses TypeScript decorators to create a seamless API for plugin developers. This document explains how the settings system works and how to extend it.
## Table of Contents
- [Overview](#overview)
- [Existing Setting Types](#existing-setting-types)
- [Using Settings in Plugins](#using-settings-in-plugins)
- [Adding New Setting Types](#adding-new-setting-types)
- [Rendering in the UI](#rendering-in-the-ui)
## Overview
The settings system is built around TypeScript decorators and uses TypeScript's type system to provide type safety for plugin settings. The system consists of a few key components:
1. **Setting Type Interfaces** in `src/plugins/core/types.ts` - Define the structure of the setting
2. **Setting Decorator Options** in `src/plugins/core/settings.ts` - Define the options for the decorator
3. **Setting Decorators** in `src/plugins/core/settings.ts` - Register the setting in the plugin
4. **BasePlugin Class** in `src/plugins/core/settings.ts` - Base class that handles the settings
## Existing Setting Types
BetterSEQTA+ currently supports the following setting types:
- **Boolean Settings** - Simple on/off toggle
- **String Settings** - Text input with optional validation
- **Number Settings** - Numeric input with optional min/max/step
- **Select Settings** - Dropdown selection from predefined options
Each setting type has a corresponding interface, options interface, and decorator.
## Using Settings in Plugins
Here's how to use the settings system in a plugin:
```typescript
import { BasePlugin, BooleanSetting, StringSetting } from '../../core/settings';
// Define the plugin settings class
class MyPluginClass extends BasePlugin {
@BooleanSetting({
default: true,
title: "Enable Feature",
description: "Enables the awesome feature."
})
enabled!: boolean;
@StringSetting({
default: "Default Value",
title: "Custom Text",
description: "Enter your custom text here.",
maxLength: 100
})
customText!: string;
}
// Create an instance to extract settings
const settingsInstance = new MyPluginClass();
// Use in plugin definition
const myPlugin = {
id: 'my-plugin',
name: 'My Plugin',
description: 'Does awesome things',
version: '1.0.0',
settings: settingsInstance.settings,
run: async (api) => {
// Access settings via api.settings
if (api.settings.enabled) {
console.log(api.settings.customText);
}
// Listen for settings changes
api.settings.onChange('enabled', (value) => {
console.log(`Enabled changed to: ${value}`);
});
}
};
```
## Adding New Setting Types
To add a new setting type, you need to follow these steps:
### 1. Define the Setting Interface in `src/plugins/core/types.ts`
```typescript
export interface ColorSetting {
type: 'color';
default: string; // HEX color code
title: string;
description?: string;
presets?: string[]; // Optional color presets
}
// Update the PluginSetting type to include the new setting type
export type PluginSetting = BooleanSetting | StringSetting | NumberSetting |
SelectSetting<string> | ColorSetting;
// Update the SettingValue type helper
type SettingValue<T extends PluginSetting> = T extends BooleanSetting ? boolean :
T extends StringSetting ? string :
T extends NumberSetting ? number :
T extends SelectSetting<infer O> ? O :
T extends ColorSetting ? string : // Add this line
never;
```
### 2. Define the Options Interface in `src/plugins/core/settings.ts`
```typescript
interface ColorSettingOptions extends BaseSettingOptions {
default: string; // HEX color
presets?: string[];
}
```
### 3. Create the Decorator Function in `src/plugins/core/settings.ts`
```typescript
export function ColorSetting(options: ColorSettingOptions): PropertyDecorator {
return (target: Object, propertyKey: string | symbol) => {
// Ensure the settings property exists on the constructor's prototype
const proto = target.constructor.prototype;
if (!proto.hasOwnProperty('settings')) {
proto.settings = {};
}
// Add the setting to the prototype's settings object
proto.settings[propertyKey] = {
type: 'color',
...options
};
};
}
```
### 4. Create a Corresponding UI Component (if needed)
If your setting type needs a custom UI component, create one in the `src/interface/components` directory.
For example, you might create a `ColorPicker.svelte` component.
### 5. Update the Settings UI in `src/interface/pages/settings/general.svelte`
Update the `getPluginSettingEntries` function to handle your new setting type:
```javascript
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 === 'color' ? ColorPicker : // Add this line
setting.type === 'string' ? (setting.options ? Select : null) : Switch,
props: {
state: pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default,
onChange: (value: any) => {
updatePluginSetting(plugin.pluginId, key, value);
},
options: setting.options,
presets: setting.presets // Add this line if needed for your component
}
});
```
## Rendering in the UI
The settings UI is handled in `src/interface/pages/settings/general.svelte`. This file does a few key things:
1. Loads settings for all plugins from storage
2. Maps setting types to UI components
3. Handles updating settings when users interact with the UI
For most setting types, you'll need to ensure there's a corresponding Svelte component in the `src/interface/components` directory that can render and edit the setting value.
## Example: Adding a Color Setting
Here's a complete example of adding a color setting type:
1. Define the setting interface in `types.ts`:
```typescript
export interface ColorSetting {
type: 'color';
default: string;
title: string;
description?: string;
presets?: string[];
}
export type PluginSetting = BooleanSetting | StringSetting | NumberSetting |
SelectSetting<string> | ColorSetting;
type SettingValue<T extends PluginSetting> = T extends BooleanSetting ? boolean :
T extends StringSetting ? string :
T extends NumberSetting ? number :
T extends SelectSetting<infer O> ? O :
T extends ColorSetting ? string :
never;
```
2. Create the options interface and decorator in `settings.ts`:
```typescript
interface ColorSettingOptions extends BaseSettingOptions {
default: string;
presets?: string[];
}
export function ColorSetting(options: ColorSettingOptions): PropertyDecorator {
return (target: Object, propertyKey: string | symbol) => {
const proto = target.constructor.prototype;
if (!proto.hasOwnProperty('settings')) {
proto.settings = {};
}
proto.settings[propertyKey] = {
type: 'color',
...options
};
};
}
```
3. Create a ColorPicker component in `src/interface/components/ColorPicker.svelte`:
```html
<script lang="ts">
export let state = "#000000";
export let onChange = (value: string) => {};
export let presets: string[] = ["#ff0000", "#00ff00", "#0000ff"];
</script>
<div class="color-picker">
<input
type="color"
value={state}
on:change={(e) => onChange(e.currentTarget.value)}
/>
<div class="presets">
{#each presets as preset}
<button
class="preset"
style="background-color: {preset}"
on:click={() => onChange(preset)}
></button>
{/each}
</div>
</div>
<style>
.color-picker {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.presets {
display: flex;
gap: 0.25rem;
}
.preset {
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
border: 1px solid #ccc;
cursor: pointer;
}
</style>
```
4. Update the UI renderer in `general.svelte`:
```javascript
Component: setting.type === 'boolean' ? Switch :
setting.type === 'select' ? Select :
setting.type === 'number' ? Slider :
setting.type === 'color' ? ColorPicker :
setting.type === 'string' ? (setting.options ? Select : null) : Switch,
```
5. Use the new setting type in a plugin:
```typescript
class ThemePlugin extends BasePlugin {
@ColorSetting({
default: "#4285f4",
title: "Primary Color",
description: "The main color for the theme",
presets: ["#4285f4", "#ea4335", "#fbbc05", "#34a853"]
})
primaryColor!: string;
}
```
With these steps, you've added a completely new setting type to the BetterSEQTA+ plugin system!
+335
View File
@@ -0,0 +1,335 @@
# Creating Plugins with Decorator-Based Settings
This guide will walk you through creating a BetterSEQTA+ plugin using the new decorator-based settings system.
## Prerequisites
- Understand basic TypeScript concepts (classes, interfaces, decorators)
- Familiarity with the BetterSEQTA+ plugin system
## Plugin Structure
A typical plugin consists of:
1. A settings class that defines the plugin's settings using decorators
2. The plugin definition object
3. The actual plugin functionality
## Step by Step Guide
### 1. Create a Plugin File
Start by creating a new file in the `src/plugins/built-in` directory. For example, `myFeature/index.ts`.
### 2. Define Storage Type (Optional)
If your plugin needs to store data, define a storage interface:
```typescript
interface MyFeatureStorage {
lastUsed: string;
favoriteItems: string[];
}
```
### 3. Create a Settings Class
Create a class that extends `BasePlugin` and use decorators to define settings:
```typescript
import { BasePlugin, BooleanSetting, StringSetting, NumberSetting, SelectSetting } from '../../core/settings';
class MyFeaturePluginClass extends BasePlugin {
@BooleanSetting({
default: true,
title: "Enable My Feature",
description: "Enables the awesome new feature."
})
enabled!: boolean;
@StringSetting({
default: "Default text",
title: "Custom Message",
description: "Sets a custom message for the feature",
maxLength: 100
})
message!: string;
@NumberSetting({
default: 5,
title: "Refresh Interval",
description: "How often to refresh the data (in seconds)",
min: 1,
max: 60,
step: 1
})
refreshInterval!: number;
@SelectSetting({
default: "small",
options: ["small", "medium", "large"] as const,
title: "Display Size",
description: "Control how large the feature appears"
})
displaySize!: "small" | "medium" | "large";
}
```
### 4. Create a Plugin Instance
Create an instance of your settings class and define the plugin object:
```typescript
// Create an instance to extract settings
const settingsInstance = new MyFeaturePluginClass();
const myFeaturePlugin: Plugin<typeof settingsInstance.settings, MyFeatureStorage> = {
id: 'myFeature',
name: 'My Awesome Feature',
description: 'Adds an awesome new feature to SEQTA',
version: '1.0.0',
settings: settingsInstance.settings,
run: async (api) => {
// Plugin implementation goes here
}
};
export default myFeaturePlugin;
```
### 5. Implement Plugin Functionality
Implement your plugin's functionality in the `run` function:
```typescript
run: async (api) => {
// Initialize storage with defaults if needed
if (api.storage.lastUsed === undefined) {
api.storage.lastUsed = new Date().toISOString();
}
if (api.storage.favoriteItems === undefined) {
api.storage.favoriteItems = [];
}
// Only run if enabled
if (!api.settings.enabled) return;
// Main plugin logic
const initializeFeature = () => {
console.log(`Initializing feature with message: ${api.settings.message}`);
console.log(`Using display size: ${api.settings.displaySize}`);
// Set up refreshing
const intervalId = setInterval(() => {
refreshData();
}, api.settings.refreshInterval * 1000);
// Clean up function returned here
return () => {
clearInterval(intervalId);
console.log('Feature cleaned up');
};
};
const refreshData = () => {
console.log('Refreshing data...');
api.storage.lastUsed = new Date().toISOString();
};
// Listen for elements we need
api.seqta.onMount('.some-element', (element) => {
// Do something when element appears
});
// Listen for settings changes
api.settings.onChange('refreshInterval', (newValue) => {
console.log(`Refresh interval changed to ${newValue} seconds`);
});
// Return cleanup function
return initializeFeature();
}
```
### 6. Register the Plugin
Make sure your plugin is registered in the plugin system. In the `src/plugins/index.ts` file, add your plugin to the list of built-in plugins:
```typescript
import myFeaturePlugin from './built-in/myFeature';
// Add your plugin to this array
const builtInPlugins = [
// ... other plugins
myFeaturePlugin,
];
```
## Advanced Features
### Reacting to Settings Changes
You can listen for settings changes with the `onChange` method:
```typescript
api.settings.onChange('enabled', (value) => {
if (value) {
// Setting was turned on
initialize();
} else {
// Setting was turned off
cleanup();
}
});
```
### Using Storage
The storage API lets you persist data between sessions:
```typescript
// Read from storage
const favorites = api.storage.favoriteItems;
// Write to storage
api.storage.favoriteItems = [...favorites, 'new item'];
// Listen for storage changes
api.storage.onChange('favoriteItems', (newValue) => {
console.log('Favorites updated:', newValue);
});
```
### Cleaning Up
Always return a cleanup function from your plugin's `run` method if you have any resources to clean up:
```typescript
run: async (api) => {
// Set up resources
const intervalId = setInterval(() => {
// Do something
}, 1000);
// Return cleanup function
return () => {
clearInterval(intervalId);
// Clean up any other resources
};
}
```
## Best Practices
1. **Initialize Storage Values**: Always check if storage values are undefined and set defaults
2. **Handle Enabled State**: Check if your plugin is enabled before running main functionality
3. **Use TypeScript**: Take advantage of TypeScript's type system to ensure type safety
4. **Clean Up Resources**: Always clean up resources when a plugin is disabled
5. **Document Settings**: Use clear titles and descriptions for your settings
## Complete Example
Here's a complete example of a simple plugin that changes the color of elements:
```typescript
import { BasePlugin, BooleanSetting, ColorSetting } from '../../core/settings';
import type { Plugin } from '../../core/types';
interface ColorChangerStorage {
lastApplied: string;
}
class ColorChangerPluginClass extends BasePlugin {
@BooleanSetting({
default: true,
title: "Enable Color Changer",
description: "Applies custom colors to elements on the page."
})
enabled!: boolean;
@ColorSetting({
default: "#4285f4",
title: "Heading Color",
description: "Color for headings on the page",
presets: ["#4285f4", "#ea4335", "#fbbc05", "#34a853"]
})
headingColor!: string;
@ColorSetting({
default: "#34a853",
title: "Button Color",
description: "Color for buttons on the page",
presets: ["#4285f4", "#ea4335", "#fbbc05", "#34a853"]
})
buttonColor!: string;
}
const settingsInstance = new ColorChangerPluginClass();
const colorChangerPlugin: Plugin<typeof settingsInstance.settings, ColorChangerStorage> = {
id: 'colorChanger',
name: 'Color Changer',
description: 'Changes colors of various elements on the page',
version: '1.0.0',
settings: settingsInstance.settings,
run: async (api) => {
if (api.storage.lastApplied === undefined) {
api.storage.lastApplied = new Date().toISOString();
}
const applyColors = () => {
if (!api.settings.enabled) return;
// Apply heading color
document.querySelectorAll('h1, h2, h3').forEach(heading => {
(heading as HTMLElement).style.color = api.settings.headingColor;
});
// Apply button color
document.querySelectorAll('button').forEach(button => {
(button as HTMLElement).style.backgroundColor = api.settings.buttonColor;
});
api.storage.lastApplied = new Date().toISOString();
};
// Apply colors initially
applyColors();
// Apply colors when DOM changes
api.seqta.onMount('h1, h2, h3, button', applyColors);
// Listen for color changes
api.settings.onChange('headingColor', applyColors);
api.settings.onChange('buttonColor', applyColors);
api.settings.onChange('enabled', (enabled) => {
if (enabled) {
applyColors();
} else {
// Reset colors
document.querySelectorAll('h1, h2, h3').forEach(heading => {
(heading as HTMLElement).style.color = '';
});
document.querySelectorAll('button').forEach(button => {
(button as HTMLElement).style.backgroundColor = '';
});
}
});
// No cleanup needed for this plugin
return () => {};
}
};
export default colorChangerPlugin;
```
This plugin demonstrates:
- Using multiple setting types including a custom color setting
- Handling the enabled state
- Initializing storage
- Listening for setting changes
- Applying and resetting styles based on settings
- Proper cleanup when disabled
+541
View File
@@ -0,0 +1,541 @@
# Creating Custom UI Components for Settings
When adding new setting types to BetterSEQTA+, you'll often need to create custom UI components to render and edit these settings. This guide covers how to create Svelte components for the settings UI and how to integrate them with the settings system.
## Understanding the Settings UI
Settings in BetterSEQTA+ are rendered by the `src/interface/pages/settings/general.svelte` component. This component:
1. Loads settings from all plugins
2. Maps setting types to appropriate UI components
3. Renders the settings UI
4. Handles updates when settings are changed
## Basic Component Requirements
Every setting UI component should follow these conventions:
1. **Accept a `state` prop** for the current value
2. **Accept an `onChange` prop** for updating the value
3. **Accept any additional props** specific to the setting type (e.g., `options`, `min`, `max`)
4. **Handle user input** and call `onChange` with the new value
## Creating a Basic Component
Here's an example of a basic Svelte component for a custom setting type:
```svelte
<!-- src/interface/components/MyCustomSetting.svelte -->
<script lang="ts">
// Current value
export let state: any = null;
// Callback for updates
export let onChange = (newValue: any) => {};
// Other props specific to your setting type
export let customOption: string = "default";
// Local state or methods if needed
function handleChange(event: Event) {
const value = (event.target as HTMLInputElement).value;
onChange(value);
}
</script>
<div class="my-custom-setting">
<input
type="text"
value={state}
on:input={handleChange}
data-option={customOption}
/>
</div>
<style>
.my-custom-setting {
/* Your component styles */
}
</style>
```
## Example: Slider Component
BetterSEQTA+ includes a Slider component for number settings:
```svelte
<!-- src/interface/components/Slider.svelte -->
<script lang="ts">
export let state: number | string = 0;
export let onChange = (value: number) => {};
export let min = 0;
export let max = 100;
export let step = 1;
let stringValue = typeof state === "string" ? state : state.toString();
function handleChange(e: Event) {
const input = e.target as HTMLInputElement;
const newValue = parseFloat(input.value);
stringValue = input.value;
onChange(newValue);
}
</script>
<div class="relative flex items-center">
<input
type="range"
class="w-24 accent-indigo-500"
min={min}
max={max}
step={step}
value={state}
on:input={handleChange}
/>
<span class="ml-2 text-xs text-zinc-500 dark:text-zinc-400 w-8">{stringValue}</span>
</div>
```
## Example: Color Picker Component
Here's a more complex example of a color picker component:
```svelte
<!-- src/interface/components/ColorPicker.svelte -->
<script lang="ts">
export let state = "#000000";
export let onChange = (value: string) => {};
export let presets: string[] = ["#ff0000", "#00ff00", "#0000ff"];
let isOpen = false;
function handleColorChange(e: Event) {
const input = e.target as HTMLInputElement;
onChange(input.value);
}
function selectPreset(color: string) {
onChange(color);
isOpen = false;
}
function togglePicker() {
isOpen = !isOpen;
}
</script>
<div class="color-picker relative">
<button
class="color-swatch"
style="background-color: {state}"
on:click={togglePicker}
aria-label="Open color picker"
></button>
{#if isOpen}
<div class="picker-popup">
<input
type="color"
value={state}
on:input={handleColorChange}
/>
<div class="presets">
{#each presets as preset}
<button
class="preset-swatch"
style="background-color: {preset}"
on:click={() => selectPreset(preset)}
aria-label={`Select color ${preset}`}
></button>
{/each}
</div>
</div>
{/if}
</div>
<style>
.color-picker {
position: relative;
}
.color-swatch {
width: 2rem;
height: 1.5rem;
border-radius: 0.25rem;
border: 1px solid #ccc;
cursor: pointer;
}
.picker-popup {
position: absolute;
top: 100%;
left: 0;
margin-top: 0.5rem;
padding: 0.5rem;
background-color: white;
border: 1px solid #ccc;
border-radius: 0.25rem;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
z-index: 10;
}
.presets {
display: flex;
gap: 0.25rem;
margin-top: 0.5rem;
}
.preset-swatch {
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
border: 1px solid #ccc;
cursor: pointer;
}
</style>
```
## Integrating with the Settings System
Once you've created your component, you need to update `general.svelte` to use it for your custom setting type.
### 1. Import Your Component
At the top of `src/interface/pages/settings/general.svelte`, add an import for your component:
```typescript
import ColorPicker from "../../components/ColorPicker.svelte"
```
### 2. Update Component Mapping
Find the `getPluginSettingEntries` function in `general.svelte` and update the component mapping:
```typescript
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 === 'color' ? ColorPicker : // Add your component here
setting.type === 'string' ? (setting.options ? Select : null) : Switch,
props: {
state: pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default,
onChange: (value: any) => {
updatePluginSetting(plugin.pluginId, key, value);
},
options: setting.options,
// Add any additional props your component needs
presets: setting.presets,
min: setting.min,
max: setting.max,
step: setting.step
}
});
});
});
return entries;
}
```
## Handling Different UI Needs
Different setting types may have different UI needs:
### Toggle Switches
For boolean settings, a toggle switch is usually appropriate:
```svelte
<script lang="ts">
export let state = false;
export let onChange = (value: boolean) => {};
</script>
<button
class="switch"
class:active={state}
on:click={() => onChange(!state)}
>
<div class="toggle"></div>
</button>
<style>
.switch {
position: relative;
width: 40px;
height: 24px;
background-color: #ccc;
border-radius: 12px;
cursor: pointer;
}
.switch.active {
background-color: #4CAF50;
}
.toggle {
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
background-color: white;
border-radius: 50%;
transition: transform 0.2s;
}
.switch.active .toggle {
transform: translateX(16px);
}
</style>
```
### Text Inputs
For string settings, a text input with validation:
```svelte
<script lang="ts">
export let state = "";
export let onChange = (value: string) => {};
export let maxLength: number | undefined = undefined;
export let pattern: string | undefined = undefined;
let error = "";
function validate(value: string) {
if (maxLength && value.length > maxLength) {
error = `Value must be under ${maxLength} characters`;
return false;
}
if (pattern && !new RegExp(pattern).test(value)) {
error = "Value doesn't match the required pattern";
return false;
}
error = "";
return true;
}
function handleInput(e: Event) {
const input = e.target as HTMLInputElement;
const newValue = input.value;
if (validate(newValue)) {
onChange(newValue);
}
}
</script>
<div class="text-input">
<input
type="text"
value={state}
on:input={handleInput}
maxlength={maxLength}
pattern={pattern}
/>
{#if error}
<div class="error">{error}</div>
{/if}
</div>
<style>
.text-input {
position: relative;
}
input {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 0.25rem;
}
.error {
color: red;
font-size: 0.75rem;
margin-top: 0.25rem;
}
</style>
```
### Complex Interactive Components
For more complex settings, you may need more interactive components with dropdowns, modals, or other features. Consider using additional Svelte features like:
- `{#if}...{/if}` blocks for conditional rendering
- Svelte transitions for animations
- Svelte actions for DOM interactions
- Svelte stores for shared state
## Best Practices
1. **Keep Components Focused**: Each component should do one thing well
2. **Use TypeScript**: Define proper types for your props
3. **Handle Errors**: Validate input and show meaningful error messages
4. **Use Clear UI**: Make it obvious how to interact with the component
5. **Add Accessibility**: Include proper ARIA attributes and keyboard handling
6. **Support Theming**: Use CSS variables or design system tokens for consistent styling
7. **Test Edge Cases**: Ensure your component handles all possible inputs
## Complete Example
Here's a complete example of a custom file picker component:
```svelte
<!-- src/interface/components/FilePicker.svelte -->
<script lang="ts">
export let state: string | null = null;
export let onChange = (value: string | null) => {};
export let accept = ".txt,.pdf,.doc,.docx";
export let maxSize = 1024 * 1024 * 5; // 5MB
let error = "";
let fileName = state ? state.split('/').pop() : "No file selected";
let inputEl: HTMLInputElement;
function handleFileChange(e: Event) {
const input = e.target as HTMLInputElement;
const files = input.files;
if (!files || files.length === 0) {
onChange(null);
fileName = "No file selected";
error = "";
return;
}
const file = files[0];
// Validate file size
if (file.size > maxSize) {
error = `File too large. Maximum size is ${maxSize / (1024 * 1024)}MB.`;
input.value = "";
return;
}
error = "";
fileName = file.name;
// Read file as data URL
const reader = new FileReader();
reader.onload = (e) => {
if (e.target && typeof e.target.result === 'string') {
onChange(e.target.result);
}
};
reader.readAsDataURL(file);
}
function clearFile() {
if (inputEl) inputEl.value = "";
onChange(null);
fileName = "No file selected";
error = "";
}
</script>
<div class="file-picker">
<div class="file-input">
<button class="browse-btn" on:click={() => inputEl.click()}>
Browse...
</button>
<span class="file-name">{fileName}</span>
{#if state}
<button class="clear-btn" on:click={clearFile}>
</button>
{/if}
</div>
<input
type="file"
bind:this={inputEl}
on:change={handleFileChange}
{accept}
class="hidden"
/>
{#if error}
<div class="error">{error}</div>
{/if}
</div>
<style>
.file-picker {
width: 100%;
}
.file-input {
display: flex;
align-items: center;
border: 1px solid #ccc;
border-radius: 0.25rem;
padding: 0.25rem;
}
.browse-btn {
background-color: #f0f0f0;
border: 1px solid #ccc;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
margin-right: 0.5rem;
cursor: pointer;
}
.file-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.875rem;
}
.clear-btn {
color: #999;
background: none;
border: none;
cursor: pointer;
padding: 0 0.5rem;
}
.hidden {
display: none;
}
.error {
color: red;
font-size: 0.75rem;
margin-top: 0.25rem;
}
</style>
```
To use this in the settings system, you would:
1. Define a `FileSetting` interface in `types.ts`
2. Create a `FileSetting` decorator in `settings.ts`
3. Update the `getPluginSettingEntries` function in `general.svelte`
This component demonstrates:
- Handling file input (a more complex input type)
- Input validation
- Error handling
- Multiple interactive elements
- Binding to DOM elements
- Clean UI that follows platform conventions
@@ -1,33 +1,29 @@
import type { Plugin, PluginSettings } from '../../core/types';
interface NotificationCollectorSettings extends PluginSettings {
enabled: {
type: 'boolean';
default: boolean;
title: string;
description: string;
};
}
import type { Plugin } from '../../core/types';
import { BasePlugin, BooleanSetting } from '../../core/settings';
interface NotificationCollectorStorage {
lastNotificationCount: number;
lastCheckedTime: string;
}
const notificationCollectorPlugin: Plugin<NotificationCollectorSettings, NotificationCollectorStorage> = {
class NotificationCollectorPluginClass extends BasePlugin {
@BooleanSetting({
default: true,
title: "Notification Collector",
description: "Uncaps the 9+ limit for notifications, showing the real number.",
})
enabled!: boolean;
}
// Create an instance to extract settings
const settingsInstance = new NotificationCollectorPluginClass();
const notificationCollectorPlugin: Plugin<typeof settingsInstance.settings, NotificationCollectorStorage> = {
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.',
}
},
settings: settingsInstance.settings,
run: async (api) => {
let pollInterval: number | null = null;
@@ -95,8 +91,8 @@ const notificationCollectorPlugin: Plugin<NotificationCollectorSettings, Notific
});
}
const enabledCallback = (enabled: boolean) => {
if (enabled) {
const enabledCallback = (value: any) => {
if (value) {
startPolling();
} else {
stopPolling();
+31
View File
@@ -0,0 +1,31 @@
import type { Plugin } from '../../core/types';
import { BasePlugin, BooleanSetting } from '../../core/settings';
class TestPluginClass extends BasePlugin {
@BooleanSetting({
default: true,
title: "Test Plugin",
description: "A test plugin for BetterSEQTA+",
})
enabled!: boolean;
}
const settingsInstance = new TestPluginClass();
const testPlugin: Plugin<typeof settingsInstance.settings> = {
id: 'test',
name: 'Test Plugin',
description: 'A test plugin for BetterSEQTA+',
version: '1.0.0',
settings: settingsInstance.settings,
run: async (api) => {
console.log('Test plugin running');
api.seqta.onPageChange((page) => {
console.log('Page changed to', page);
});
}
};
export default testPlugin;
+21 -27
View File
@@ -1,38 +1,35 @@
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
import type { Plugin, PluginSettings } from '../../core/types';
import type { Plugin } from '../../core/types';
import { convertTo12HourFormat } from '@/seqta/utils/convertTo12HourFormat';
import { waitForElm } from '@/seqta/utils/waitForElm';
import { BasePlugin, BooleanSetting } from '../../core/settings';
interface TimetableSettings extends PluginSettings {
enabled: {
type: 'boolean';
default: boolean;
title: string;
description: string;
};
// Define only the typed settings - no need for redundant interface
class TimetablePluginClass extends BasePlugin {
@BooleanSetting({
default: true,
title: "Timetable Enhancer",
description: "Adds extra features to the timetable view."
})
enabled!: boolean;
}
const timetablePlugin: Plugin<TimetableSettings> = {
// Create an instance to extract settings
const settingsInstance = new TimetablePluginClass();
const timetablePlugin: Plugin<typeof settingsInstance.settings> = {
id: 'timetable',
name: 'Timetable Enhancer',
description: 'Adds extra features to the timetable view',
version: '1.0.0',
settings: {
enabled: {
type: 'boolean',
default: true,
title: 'Timetable Enhancer',
description: 'Adds extra features to the timetable view.',
}
},
settings: settingsInstance.settings,
run: async (api) => {
if (api.settings.enabled) {
api.seqta.onMount('.timetablepage', handleTimetable)
}
const enabledCallback = (enabled: boolean) => {
if (enabled) {
const enabledCallback = (value: any) => {
if (value) {
api.seqta.onMount('.timetablepage', handleTimetable)
} else {
const timetablePage = document.querySelector('.timetablepage')
@@ -277,19 +274,16 @@ function handleTimetableAssessmentHide(): void {
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"
if (!entryEl.classList.contains("assessment")) {
entryEl.style.opacity = entryEl.style.opacity === "0.3" ? "1" : "0.3"
}
})
}
hideOn.addEventListener("click", () => {
hideElements()
})
hideOn.addEventListener("click", hideElements)
}
export default timetablePlugin;
+108
View File
@@ -0,0 +1,108 @@
import type { PluginSettings } from './types';
// Base interfaces for our settings
interface BaseSettingOptions {
title: string;
description?: string;
}
interface BooleanSettingOptions extends BaseSettingOptions {
default: boolean;
}
interface StringSettingOptions extends BaseSettingOptions {
default: string;
maxLength?: number;
pattern?: string;
}
interface NumberSettingOptions extends BaseSettingOptions {
default: number;
min?: number;
max?: number;
step?: number;
}
interface SelectSettingOptions<T extends string> extends BaseSettingOptions {
default: T;
options: readonly T[];
}
// The actual decorators
export function BooleanSetting(options: BooleanSettingOptions): PropertyDecorator {
return (target: Object, propertyKey: string | symbol) => {
// Ensure the settings property exists on the constructor's prototype
const proto = target.constructor.prototype;
if (!proto.hasOwnProperty('settings')) {
proto.settings = {};
}
// Add the setting to the prototype's settings object
proto.settings[propertyKey] = {
type: 'boolean',
...options
};
};
}
export function StringSetting(options: StringSettingOptions): PropertyDecorator {
return (target: Object, propertyKey: string | symbol) => {
// Ensure the settings property exists on the constructor's prototype
const proto = target.constructor.prototype;
if (!proto.hasOwnProperty('settings')) {
proto.settings = {};
}
// Add the setting to the prototype's settings object
proto.settings[propertyKey] = {
type: 'string',
...options
};
};
}
export function NumberSetting(options: NumberSettingOptions): PropertyDecorator {
return (target: Object, propertyKey: string | symbol) => {
// Ensure the settings property exists on the constructor's prototype
const proto = target.constructor.prototype;
if (!proto.hasOwnProperty('settings')) {
proto.settings = {};
}
// Add the setting to the prototype's settings object
proto.settings[propertyKey] = {
type: 'number',
...options
};
};
}
export function SelectSetting<T extends string>(options: SelectSettingOptions<T>): PropertyDecorator {
return (target: Object, propertyKey: string | symbol) => {
// Ensure the settings property exists on the constructor's prototype
const proto = target.constructor.prototype;
if (!proto.hasOwnProperty('settings')) {
proto.settings = {};
}
// Add the setting to the prototype's settings object
proto.settings[propertyKey] = {
type: 'select',
...options
};
};
}
// Base plugin class that handles settings
export abstract class BasePlugin<T extends PluginSettings = PluginSettings> {
// The settings property will be populated by decorators
settings: T = {} as T;
constructor() {
// Copy settings from the prototype to the instance
// This ensures that each instance has its own settings object
if (this.constructor.prototype.settings) {
this.settings = { ...this.constructor.prototype.settings };
}
}
}
+10 -5
View File
@@ -1,27 +1,32 @@
import ReactFiber from '@/seqta/utils/ReactFiber';
interface BooleanSetting {
export interface BooleanSetting {
type: 'boolean';
default: boolean;
title: string;
description?: string;
}
interface StringSetting {
export interface StringSetting {
type: 'string';
default: string;
title: string;
description?: string;
maxLength?: number;
pattern?: string;
}
interface NumberSetting {
export interface NumberSetting {
type: 'number';
default: number;
title: string;
description?: string;
min?: number;
max?: number;
step?: number;
}
interface SelectSetting<T extends string> {
export interface SelectSetting<T extends string> {
type: 'select';
options: readonly T[];
default: T;
@@ -29,7 +34,7 @@ interface SelectSetting<T extends string> {
description?: string;
}
type PluginSetting = BooleanSetting | StringSetting | NumberSetting | SelectSetting<string>;
export type PluginSetting = BooleanSetting | StringSetting | NumberSetting | SelectSetting<string>;
export type PluginSettings = {
[key: string]: PluginSetting;
+4
View File
@@ -1,6 +1,9 @@
import { PluginManager } from './core/manager';
// plugins
import timetablePlugin from './built-in/timetable';
import notificationCollectorPlugin from './built-in/notificationCollector';
import testPlugin from './built-in/test';
// Initialize plugin manager
const pluginManager = PluginManager.getInstance();
@@ -8,6 +11,7 @@ const pluginManager = PluginManager.getInstance();
// Register built-in plugins
pluginManager.registerPlugin(timetablePlugin);
pluginManager.registerPlugin(notificationCollectorPlugin);
pluginManager.registerPlugin(testPlugin);
// Legacy plugin exports
export { init as Monofile } from './monofile';
+4
View File
@@ -21,6 +21,10 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Decorators */
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"paths": {
"@/*": ["./src/*"]
},