mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
feat: add docs and dev plugins
This commit is contained in:
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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!
|
||||
Reference in New Issue
Block a user