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