feat(settings): add initial addon implementation with demo views
Creates complete addon structure with manifest, main/renderer modules, shared types, and demo views. Includes comprehensive README documenting addon API, lifecycle methods, permissions, capabilities, and contribution points. Implements settings panel and demo repository view to showcase addon features.
This commit is contained in:
305
README.md
Normal file
305
README.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# My First Addon
|
||||
|
||||
A comprehensive demo addon for GitCaddy that showcases all available addon features and APIs.
|
||||
|
||||
## Overview
|
||||
|
||||
This addon demonstrates:
|
||||
|
||||
- **Addon Manifest** (`addon.json`) - How to define your addon's metadata, permissions, and contributions
|
||||
- **Main Process Module** (`main/index.js`) - Lifecycle methods, IPC, settings, and method invocations
|
||||
- **Renderer Process Module** (`renderer/index.js`) - UI components and renderer-side communication
|
||||
- **Custom Views** - HTML-based views for repository panels and settings
|
||||
- **Shared Types** - Code shared between main and renderer processes
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
myfirst-addon/
|
||||
├── addon.json # Addon manifest (required)
|
||||
├── main/
|
||||
│ └── index.js # Main process module (required)
|
||||
├── renderer/
|
||||
│ └── index.js # Renderer process module (optional)
|
||||
├── shared/
|
||||
│ └── types.js # Shared constants and types
|
||||
├── views/
|
||||
│ ├── demo.html # Demo repository view
|
||||
│ └── settings.html # Settings panel view
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Addon Manifest (addon.json)
|
||||
|
||||
The manifest defines everything about your addon:
|
||||
|
||||
### Required Fields
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `id` | Unique identifier (reverse domain notation: `com.yourcompany.addon-name`) |
|
||||
| `name` | Display name shown in Addon Manager |
|
||||
| `version` | Semantic version (major.minor.patch) |
|
||||
| `minAppVersion` | Minimum GitCaddy version required |
|
||||
| `description` | Short description for the addon list |
|
||||
| `main` | Path to main process JavaScript module |
|
||||
|
||||
### Optional Fields
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `renderer` | Path to renderer process module for UI components |
|
||||
| `author` | Author information (name, email, url) |
|
||||
| `host` | Native host configuration for .NET/Go/Rust addons |
|
||||
| `permissions` | Required permissions (shown to user before install) |
|
||||
| `capabilities` | What your addon can do |
|
||||
| `contributes` | UI elements your addon adds |
|
||||
|
||||
### Permissions
|
||||
|
||||
```json
|
||||
"permissions": [
|
||||
"repository:read", // Read repository data
|
||||
"repository:write", // Modify repository
|
||||
"diff:read", // Read diff/changes
|
||||
"commit:read", // Read commit history
|
||||
"branch:read", // Read branch information
|
||||
"network:localhost", // Make local network requests
|
||||
"network:external", // Make external network requests
|
||||
"settings:store", // Store addon settings
|
||||
"notifications:show" // Show notifications
|
||||
]
|
||||
```
|
||||
|
||||
### Capabilities
|
||||
|
||||
```json
|
||||
"capabilities": [
|
||||
"commit-message-generation", // Can generate commit messages
|
||||
"repository-analysis", // Can analyze repositories
|
||||
"custom-view" // Has custom UI views
|
||||
]
|
||||
```
|
||||
|
||||
## Main Process Module
|
||||
|
||||
The main module handles:
|
||||
|
||||
### Lifecycle Methods
|
||||
|
||||
```javascript
|
||||
class MyAddon {
|
||||
// Called once when addon is loaded
|
||||
async initialize(context) { }
|
||||
|
||||
// Called when addon is enabled
|
||||
async activate() { }
|
||||
|
||||
// Called when addon is disabled
|
||||
async deactivate() { }
|
||||
|
||||
// Called when addon is unloaded
|
||||
dispose() { }
|
||||
}
|
||||
```
|
||||
|
||||
### Context Object
|
||||
|
||||
The `context` object provides:
|
||||
|
||||
```javascript
|
||||
context.addonId // Your addon's ID
|
||||
context.addonPath // Path to addon directory
|
||||
context.manifest // Parsed addon.json
|
||||
context.log // Logger (.debug, .info, .warn, .error)
|
||||
context.settings // Settings storage
|
||||
context.events // Event emitter
|
||||
context.ipc // IPC communication
|
||||
context.appState // Access to app state
|
||||
context.hostPort // Port of native host (if configured)
|
||||
```
|
||||
|
||||
### Invoke Method
|
||||
|
||||
GitCaddy calls your addon's `invoke` method for all actions:
|
||||
|
||||
```javascript
|
||||
async invoke(method, args) {
|
||||
switch (method) {
|
||||
case 'myAction':
|
||||
return this.myAction(args[0], args[1]);
|
||||
default:
|
||||
throw new Error(`Unknown method: ${method}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
### Toolbar Buttons
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "my-button",
|
||||
"tooltip": "Button Tooltip",
|
||||
"icon": { "type": "octicon", "value": "rocket" },
|
||||
"position": "after-push-pull",
|
||||
"onClick": {
|
||||
"action": "show-view",
|
||||
"target": "my-view"
|
||||
},
|
||||
"badge": {
|
||||
"method": "getBadgeContent"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Position options: `start`, `end`, `after-push-pull`, `after-branch`
|
||||
|
||||
### Menu Items
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "my-menu-item",
|
||||
"menu": "repository",
|
||||
"label": "My Action",
|
||||
"accelerator": "CmdOrCtrl+Shift+M",
|
||||
"onClick": {
|
||||
"action": "invoke-method",
|
||||
"method": "menuAction"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Repository Views
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "my-view",
|
||||
"title": "My View",
|
||||
"icon": "rocket",
|
||||
"renderer": {
|
||||
"type": "iframe",
|
||||
"source": "views/my-view.html"
|
||||
},
|
||||
"context": ["repository"]
|
||||
}
|
||||
```
|
||||
|
||||
Context options: `repository`, `diff`, `commit`, `always`
|
||||
|
||||
### Context Menu Items
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "file-action",
|
||||
"context": "file-list",
|
||||
"label": "Process Files",
|
||||
"onClick": {
|
||||
"action": "invoke-method",
|
||||
"method": "processFiles"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Context options: `file-list`, `commit-list`
|
||||
|
||||
### Settings Definitions
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "enableFeature",
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Enable the main feature"
|
||||
}
|
||||
```
|
||||
|
||||
Types: `boolean`, `string`, `number`, `select`
|
||||
|
||||
## View Communication
|
||||
|
||||
Views communicate with the addon via the `window.gitcaddy` bridge:
|
||||
|
||||
```javascript
|
||||
// Wait for bridge to be ready
|
||||
if (window.gitcaddy) {
|
||||
init();
|
||||
} else {
|
||||
window.addEventListener('gitcaddy-ready', init);
|
||||
}
|
||||
|
||||
// Invoke addon methods
|
||||
const result = await window.gitcaddy.invoke('myMethod', arg1, arg2);
|
||||
|
||||
// Send one-way messages
|
||||
window.gitcaddy.send('my-channel', { data: 'value' });
|
||||
|
||||
// Listen for messages
|
||||
window.gitcaddy.on('addon-message', (data) => {
|
||||
console.log('Received:', data);
|
||||
});
|
||||
|
||||
// Logging
|
||||
window.gitcaddy.log.info('Something happened');
|
||||
window.gitcaddy.log.error('Something went wrong');
|
||||
```
|
||||
|
||||
## Native Hosts
|
||||
|
||||
For addons that need native code (.NET, Go, Rust, etc.):
|
||||
|
||||
```json
|
||||
"host": {
|
||||
"directory": "host",
|
||||
"executable": "MyAddon.Host",
|
||||
"platforms": {
|
||||
"win32-x64": "host/win-x64/MyAddon.Host.exe",
|
||||
"linux-x64": "host/linux-x64/MyAddon.Host",
|
||||
"darwin-x64": "host/darwin-x64/MyAddon.Host",
|
||||
"darwin-arm64": "host/darwin-arm64/MyAddon.Host"
|
||||
},
|
||||
"healthCheck": "/health",
|
||||
"startupTimeout": 15000
|
||||
}
|
||||
```
|
||||
|
||||
Communicate with your host via HTTP:
|
||||
|
||||
```javascript
|
||||
const response = await fetch(`http://127.0.0.1:${this.context.hostPort}/my-endpoint`);
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### For Development (Sideloading)
|
||||
|
||||
1. Copy this folder to GitCaddy's addons directory:
|
||||
- Windows: `%APPDATA%/GitCaddy/addons/`
|
||||
- macOS: `~/Library/Application Support/GitCaddy/addons/`
|
||||
- Linux: `~/.config/GitCaddy/addons/`
|
||||
|
||||
2. Restart GitCaddy or reload addons from the Addon Manager
|
||||
|
||||
### For Distribution
|
||||
|
||||
Package your addon as a `.zip` file and distribute it. Users can install via:
|
||||
- Addon Manager's "Install from file" option
|
||||
- Dropping the zip into the addons directory
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Use the logger** - Always use `context.log` instead of `console.log` for proper addon logging
|
||||
2. **Handle errors** - Wrap async operations in try/catch and log errors appropriately
|
||||
3. **Clean up** - Always dispose of event listeners and resources in the `dispose` method
|
||||
4. **Secure API keys** - Never expose API keys in logs or UI; use the `hasApiKey` pattern
|
||||
5. **Test on all platforms** - If using native hosts, ensure they work on Windows, macOS, and Linux
|
||||
|
||||
## Resources
|
||||
|
||||
- [Octicons](https://primer.style/octicons/) - Icon library for toolbar buttons
|
||||
- [GitCaddy Documentation](https://gitcaddy.com/docs) - Full addon API documentation
|
||||
|
||||
## License
|
||||
|
||||
MIT - Use this as a template for your own addons!
|
||||
186
addon.json
Normal file
186
addon.json
Normal file
@@ -0,0 +1,186 @@
|
||||
{
|
||||
"$schema": "https://gitcaddy.com/schemas/addon-manifest.json",
|
||||
|
||||
"_comment_id": "Unique identifier for your addon. Use reverse domain notation.",
|
||||
"id": "com.example.myfirst-addon",
|
||||
|
||||
"_comment_name": "Display name shown in the Addon Manager",
|
||||
"name": "My First Addon",
|
||||
|
||||
"_comment_version": "Semantic version (major.minor.patch)",
|
||||
"version": "1.0.0",
|
||||
|
||||
"_comment_minAppVersion": "Minimum GitCaddy version required",
|
||||
"minAppVersion": "0.1.0",
|
||||
|
||||
"_comment_description": "Short description for the addon list",
|
||||
"description": "A demo addon showcasing all GitCaddy addon features with detailed examples.",
|
||||
|
||||
"_comment_author": "Author information (shown in addon details)",
|
||||
"author": {
|
||||
"name": "Your Name",
|
||||
"email": "you@example.com",
|
||||
"url": "https://example.com"
|
||||
},
|
||||
|
||||
"_comment_main": "Path to the main process JavaScript module",
|
||||
"main": "main/index.js",
|
||||
|
||||
"_comment_renderer": "Optional: Path to renderer process module for UI components",
|
||||
"renderer": "renderer/index.js",
|
||||
|
||||
"_comment_host": "Optional: Native host process configuration (for .NET, Go, Rust, etc.)",
|
||||
"_comment_host_note": "Remove this section if your addon is pure JavaScript",
|
||||
|
||||
"_comment_permissions": "Required permissions - user sees these before installing",
|
||||
"permissions": [
|
||||
"repository:read",
|
||||
"repository:write",
|
||||
"diff:read",
|
||||
"commit:read",
|
||||
"branch:read",
|
||||
"network:localhost",
|
||||
"network:external",
|
||||
"settings:store",
|
||||
"notifications:show"
|
||||
],
|
||||
|
||||
"_comment_capabilities": "What your addon can do - GitCaddy uses these to find addons",
|
||||
"capabilities": [
|
||||
"commit-message-generation",
|
||||
"repository-analysis",
|
||||
"custom-view"
|
||||
],
|
||||
|
||||
"_comment_contributes": "UI elements your addon adds to GitCaddy",
|
||||
"contributes": {
|
||||
|
||||
"_comment_toolbarButtons": "Buttons in the main toolbar",
|
||||
"toolbarButtons": [
|
||||
{
|
||||
"id": "my-action-button",
|
||||
"tooltip": "My Action Button",
|
||||
"_comment_icon": "Use octicon names from https://primer.style/octicons/",
|
||||
"icon": { "type": "octicon", "value": "rocket" },
|
||||
"_comment_position": "Where to place: start, end, after-push-pull, after-branch",
|
||||
"position": "after-push-pull",
|
||||
"_comment_onClick": "Action when clicked",
|
||||
"onClick": {
|
||||
"_comment_action_options": "show-view, invoke-method, open-url",
|
||||
"action": "show-view",
|
||||
"target": "demo-view"
|
||||
},
|
||||
"_comment_badge": "Optional: Show a badge with dynamic content",
|
||||
"badge": {
|
||||
"method": "getBadgeContent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "quick-action",
|
||||
"tooltip": "Quick Action (invokes method directly)",
|
||||
"icon": { "type": "octicon", "value": "zap" },
|
||||
"position": "end",
|
||||
"onClick": {
|
||||
"action": "invoke-method",
|
||||
"method": "quickAction",
|
||||
"args": ["arg1", "arg2"]
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"_comment_menuItems": "Items added to GitCaddy menus",
|
||||
"menuItems": [
|
||||
{
|
||||
"id": "my-menu-item",
|
||||
"menu": "repository",
|
||||
"label": "My Addon Action",
|
||||
"accelerator": "CmdOrCtrl+Shift+M",
|
||||
"onClick": {
|
||||
"action": "invoke-method",
|
||||
"method": "menuAction"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"_comment_repositoryViews": "Custom views shown in the repository panel",
|
||||
"repositoryViews": [
|
||||
{
|
||||
"id": "demo-view",
|
||||
"title": "Demo View",
|
||||
"icon": "rocket",
|
||||
"_comment_renderer_type": "iframe loads HTML, react uses React component",
|
||||
"renderer": {
|
||||
"type": "iframe",
|
||||
"source": "views/demo.html"
|
||||
},
|
||||
"_comment_context": "When to show: repository, diff, commit, always",
|
||||
"context": ["repository"]
|
||||
}
|
||||
],
|
||||
|
||||
"_comment_settings": "Settings panel for your addon",
|
||||
"settings": {
|
||||
"id": "settings",
|
||||
"title": "My First Addon Settings",
|
||||
"renderer": {
|
||||
"type": "iframe",
|
||||
"source": "views/settings.html"
|
||||
}
|
||||
},
|
||||
|
||||
"_comment_settingsDefinitions": "Define settings schema for validation and defaults",
|
||||
"settingsDefinitions": [
|
||||
{
|
||||
"key": "enableFeature",
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Enable the main feature"
|
||||
},
|
||||
{
|
||||
"key": "apiKey",
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "API key for external service"
|
||||
},
|
||||
{
|
||||
"key": "mode",
|
||||
"type": "select",
|
||||
"default": "normal",
|
||||
"description": "Operating mode",
|
||||
"options": [
|
||||
{ "label": "Normal", "value": "normal" },
|
||||
{ "label": "Advanced", "value": "advanced" },
|
||||
{ "label": "Debug", "value": "debug" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "maxItems",
|
||||
"type": "number",
|
||||
"default": 10,
|
||||
"description": "Maximum items to process"
|
||||
}
|
||||
],
|
||||
|
||||
"_comment_contextMenuItems": "Items added to right-click context menus",
|
||||
"contextMenuItems": [
|
||||
{
|
||||
"id": "file-context-action",
|
||||
"context": "file-list",
|
||||
"label": "Process with My Addon",
|
||||
"onClick": {
|
||||
"action": "invoke-method",
|
||||
"method": "processFiles"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "commit-context-action",
|
||||
"context": "commit-list",
|
||||
"label": "Analyze Commit",
|
||||
"onClick": {
|
||||
"action": "invoke-method",
|
||||
"method": "analyzeCommit"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
494
main/index.js
Normal file
494
main/index.js
Normal file
@@ -0,0 +1,494 @@
|
||||
/**
|
||||
* My First Addon - Main Process Module
|
||||
*
|
||||
* This file runs in Electron's main process and handles:
|
||||
* - Addon lifecycle (initialize, activate, deactivate, dispose)
|
||||
* - Settings management
|
||||
* - IPC communication with renderer process
|
||||
* - Method invocations from GitCaddy
|
||||
* - Event handling
|
||||
*
|
||||
* The main process has full Node.js access and can:
|
||||
* - Read/write files
|
||||
* - Make network requests
|
||||
* - Spawn child processes
|
||||
* - Access system APIs
|
||||
*/
|
||||
|
||||
// Import shared types (optional - for code organization)
|
||||
const { DEFAULT_SETTINGS } = require('../shared/types');
|
||||
|
||||
/**
|
||||
* Main Addon Class
|
||||
*
|
||||
* GitCaddy will instantiate this class and call its lifecycle methods.
|
||||
* The class must be the default export.
|
||||
*/
|
||||
class MyFirstAddon {
|
||||
constructor() {
|
||||
// Context provided by GitCaddy (set in initialize)
|
||||
this.context = null;
|
||||
|
||||
// Local settings cache
|
||||
this.settings = { ...DEFAULT_SETTINGS };
|
||||
|
||||
// Track disposables for cleanup
|
||||
this.disposables = [];
|
||||
|
||||
// Track if addon is active
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// LIFECYCLE METHODS
|
||||
// These are called by GitCaddy at specific points
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Initialize the addon.
|
||||
*
|
||||
* Called once when the addon is first loaded.
|
||||
* Use this to set up initial state and register event listeners.
|
||||
*
|
||||
* @param {IAddonMainContext} context - Context provided by GitCaddy
|
||||
*/
|
||||
async initialize(context) {
|
||||
this.context = context;
|
||||
|
||||
// Log that we're initializing
|
||||
context.log.info('MyFirstAddon initializing...');
|
||||
|
||||
// Load settings from storage
|
||||
await this.loadSettings();
|
||||
|
||||
// Log loaded settings (be careful not to log sensitive data!)
|
||||
context.log.debug('Settings loaded:', {
|
||||
enableFeature: this.settings.enableFeature,
|
||||
mode: this.settings.mode,
|
||||
maxItems: this.settings.maxItems,
|
||||
// Don't log apiKey!
|
||||
});
|
||||
|
||||
// Register for settings changes
|
||||
const settingsDisposable = context.settings.onDidChange((key, value) => {
|
||||
this.onSettingChanged(key, value);
|
||||
});
|
||||
this.disposables.push(settingsDisposable);
|
||||
|
||||
// Register for repository events
|
||||
const repoDisposable = context.events.on('repository-changed', (repo) => {
|
||||
this.onRepositoryChanged(repo);
|
||||
});
|
||||
this.disposables.push(repoDisposable);
|
||||
|
||||
// Register for commit events
|
||||
const commitDisposable = context.events.on('commit-created', (commit) => {
|
||||
this.onCommitCreated(commit);
|
||||
});
|
||||
this.disposables.push(commitDisposable);
|
||||
|
||||
context.log.info('MyFirstAddon initialized successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the addon.
|
||||
*
|
||||
* Called when the addon is enabled/activated.
|
||||
* Start any background processes or services here.
|
||||
*/
|
||||
async activate() {
|
||||
this.context?.log.info('MyFirstAddon activating...');
|
||||
this.isActive = true;
|
||||
|
||||
// Example: Start a periodic task
|
||||
// this.startPeriodicTask();
|
||||
|
||||
this.context?.log.info('MyFirstAddon activated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate the addon.
|
||||
*
|
||||
* Called when the addon is disabled.
|
||||
* Stop background processes but don't fully clean up.
|
||||
*/
|
||||
async deactivate() {
|
||||
this.context?.log.info('MyFirstAddon deactivating...');
|
||||
this.isActive = false;
|
||||
|
||||
// Example: Stop periodic tasks
|
||||
// this.stopPeriodicTask();
|
||||
|
||||
this.context?.log.info('MyFirstAddon deactivated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of the addon.
|
||||
*
|
||||
* Called when the addon is being unloaded.
|
||||
* Clean up all resources, event listeners, etc.
|
||||
*/
|
||||
dispose() {
|
||||
this.context?.log.info('MyFirstAddon disposing...');
|
||||
|
||||
// Dispose all registered disposables
|
||||
for (const disposable of this.disposables) {
|
||||
disposable.dispose();
|
||||
}
|
||||
this.disposables = [];
|
||||
|
||||
// Clear references
|
||||
this.context = null;
|
||||
this.isActive = false;
|
||||
|
||||
console.log('MyFirstAddon disposed');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// INVOKE METHOD
|
||||
// GitCaddy calls this to invoke addon functionality
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Handle method invocations from GitCaddy.
|
||||
*
|
||||
* This is the main entry point for GitCaddy to call your addon's functionality.
|
||||
* Methods can be invoked from:
|
||||
* - Toolbar buttons (onClick.action = "invoke-method")
|
||||
* - Menu items
|
||||
* - Context menus
|
||||
* - Other addons
|
||||
* - The renderer process
|
||||
*
|
||||
* @param {string} method - Method name to invoke
|
||||
* @param {unknown[]} args - Arguments passed to the method
|
||||
* @returns {Promise<unknown>} - Result of the method
|
||||
*/
|
||||
async invoke(method, args) {
|
||||
this.context?.log.debug(`Invoke called: ${method}`, args);
|
||||
|
||||
switch (method) {
|
||||
// ---- Settings Methods ----
|
||||
case 'getSettings':
|
||||
return this.getSettings();
|
||||
|
||||
case 'updateSettings':
|
||||
return this.updateSettings(args[0]);
|
||||
|
||||
// ---- Badge Methods ----
|
||||
case 'getBadgeContent':
|
||||
return this.getBadgeContent();
|
||||
|
||||
// ---- Action Methods ----
|
||||
case 'quickAction':
|
||||
return this.quickAction(args[0], args[1]);
|
||||
|
||||
case 'menuAction':
|
||||
return this.menuAction();
|
||||
|
||||
case 'processFiles':
|
||||
return this.processFiles(args[0]);
|
||||
|
||||
case 'analyzeCommit':
|
||||
return this.analyzeCommit(args[0]);
|
||||
|
||||
// ---- Demo Methods ----
|
||||
case 'getDemoData':
|
||||
return this.getDemoData();
|
||||
|
||||
case 'performAction':
|
||||
return this.performAction(args[0]);
|
||||
|
||||
// ---- Capability Methods ----
|
||||
// These are called by GitCaddy for registered capabilities
|
||||
case 'generateCommitMessage':
|
||||
return this.generateCommitMessage(args[0]);
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown method: ${method}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SETTINGS METHODS
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Load settings from GitCaddy's settings store.
|
||||
*/
|
||||
async loadSettings() {
|
||||
if (!this.context) return;
|
||||
|
||||
const stored = this.context.settings.getAll();
|
||||
this.settings = {
|
||||
enableFeature: stored.enableFeature ?? DEFAULT_SETTINGS.enableFeature,
|
||||
apiKey: stored.apiKey ?? DEFAULT_SETTINGS.apiKey,
|
||||
mode: stored.mode ?? DEFAULT_SETTINGS.mode,
|
||||
maxItems: stored.maxItems ?? DEFAULT_SETTINGS.maxItems,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle setting changes.
|
||||
*/
|
||||
onSettingChanged(key, value) {
|
||||
this.context?.log.debug(`Setting changed: ${key} =`, value);
|
||||
|
||||
if (key in this.settings) {
|
||||
this.settings[key] = value;
|
||||
}
|
||||
|
||||
// React to specific setting changes
|
||||
if (key === 'mode') {
|
||||
this.context?.log.info(`Mode changed to: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current settings (for UI).
|
||||
*/
|
||||
getSettings() {
|
||||
return {
|
||||
...this.settings,
|
||||
// Don't expose the actual API key, just whether it's set
|
||||
hasApiKey: !!this.settings.apiKey,
|
||||
apiKey: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update settings.
|
||||
*/
|
||||
async updateSettings(updates) {
|
||||
if (!this.context) return;
|
||||
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
if (value !== undefined) {
|
||||
await this.context.settings.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// BADGE METHODS
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Get badge content for toolbar button.
|
||||
*
|
||||
* Called by GitCaddy to update the badge on your toolbar button.
|
||||
* Return null/undefined to hide the badge.
|
||||
*
|
||||
* @returns {string|number|null} Badge content
|
||||
*/
|
||||
getBadgeContent() {
|
||||
// Example: Return a count or status indicator
|
||||
if (!this.settings.enableFeature) {
|
||||
return null; // Hide badge when feature is disabled
|
||||
}
|
||||
|
||||
// Return a number, string, or null
|
||||
return '3'; // Example: "3 items"
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ACTION METHODS
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Quick action triggered by toolbar button.
|
||||
*/
|
||||
async quickAction(arg1, arg2) {
|
||||
this.context?.log.info('Quick action triggered!', { arg1, arg2 });
|
||||
|
||||
// Show a notification
|
||||
this.context?.ipc.send('show-notification', {
|
||||
title: 'Quick Action',
|
||||
body: `Action executed with args: ${arg1}, ${arg2}`,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu action triggered from Repository menu.
|
||||
*/
|
||||
async menuAction() {
|
||||
this.context?.log.info('Menu action triggered!');
|
||||
|
||||
// Example: Get the current repository
|
||||
const repo = this.context?.appState?.getCurrentRepository();
|
||||
|
||||
if (repo) {
|
||||
this.context?.log.info('Current repository:', repo.path);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Process selected files from context menu.
|
||||
*/
|
||||
async processFiles(files) {
|
||||
this.context?.log.info('Processing files:', files);
|
||||
|
||||
// Example file processing
|
||||
const results = [];
|
||||
for (const file of files || []) {
|
||||
results.push({
|
||||
path: file.path,
|
||||
status: file.status,
|
||||
processed: true,
|
||||
});
|
||||
}
|
||||
|
||||
return { files: results };
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze a commit from context menu.
|
||||
*/
|
||||
async analyzeCommit(commit) {
|
||||
this.context?.log.info('Analyzing commit:', commit?.sha);
|
||||
|
||||
return {
|
||||
sha: commit?.sha,
|
||||
analysis: 'This is a demo analysis result',
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// DEMO METHODS (for the demo view)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Get demo data for the view.
|
||||
*/
|
||||
getDemoData() {
|
||||
return {
|
||||
addonId: this.context?.addonId,
|
||||
addonPath: this.context?.addonPath,
|
||||
isActive: this.isActive,
|
||||
settings: this.getSettings(),
|
||||
features: [
|
||||
'Toolbar buttons',
|
||||
'Menu items',
|
||||
'Context menus',
|
||||
'Custom views',
|
||||
'Settings storage',
|
||||
'IPC communication',
|
||||
'Event handling',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a demo action.
|
||||
*/
|
||||
async performAction(actionType) {
|
||||
this.context?.log.info(`Performing action: ${actionType}`);
|
||||
|
||||
switch (actionType) {
|
||||
case 'notify':
|
||||
this.context?.ipc.send('show-notification', {
|
||||
title: 'Hello from Addon!',
|
||||
body: 'This notification was triggered by the addon.',
|
||||
});
|
||||
return { message: 'Notification sent!' };
|
||||
|
||||
case 'log':
|
||||
this.context?.log.info('This is an info log from the demo');
|
||||
this.context?.log.warn('This is a warning log from the demo');
|
||||
this.context?.log.debug('This is a debug log from the demo');
|
||||
return { message: 'Check the logs!' };
|
||||
|
||||
case 'settings':
|
||||
return this.getSettings();
|
||||
|
||||
default:
|
||||
return { message: `Unknown action: ${actionType}` };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CAPABILITY METHODS
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Generate commit message (if you registered 'commit-message-generation' capability).
|
||||
*
|
||||
* GitCaddy will call this when the user requests a commit message.
|
||||
*/
|
||||
async generateCommitMessage(context) {
|
||||
this.context?.log.info('Generating commit message...');
|
||||
|
||||
// Example: Simple commit message based on files
|
||||
const fileCount = context.files?.length || 0;
|
||||
const summary = `chore: update ${fileCount} file(s)`;
|
||||
|
||||
return {
|
||||
summary,
|
||||
description: 'This is a demo commit message from My First Addon.',
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// EVENT HANDLERS
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Handle repository changes.
|
||||
*/
|
||||
onRepositoryChanged(repo) {
|
||||
this.context?.log.debug('Repository changed:', repo?.path);
|
||||
|
||||
// Example: Refresh badge when repo changes
|
||||
// The badge will be refreshed automatically by GitCaddy
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle new commits.
|
||||
*/
|
||||
onCommitCreated(commit) {
|
||||
this.context?.log.debug('New commit created:', commit?.sha);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CONTEXT INTERFACE (for reference)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* @typedef {Object} IAddonMainContext
|
||||
* @property {string} addonId - Unique addon identifier
|
||||
* @property {string} addonPath - Path to addon directory
|
||||
* @property {Object} manifest - Parsed addon.json
|
||||
* @property {IAddonLogger} log - Logger instance
|
||||
* @property {IAddonSettings} settings - Settings storage
|
||||
* @property {IAddonEvents} events - Event emitter
|
||||
* @property {IAddonIPC} ipc - IPC communication
|
||||
* @property {number} [hostPort] - Port of native host (if configured)
|
||||
* @property {IAppStateProxy} [appState] - Access to app state
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} IAddonLogger
|
||||
* @property {function} debug - Log debug message
|
||||
* @property {function} info - Log info message
|
||||
* @property {function} warn - Log warning message
|
||||
* @property {function} error - Log error message
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} IAddonSettings
|
||||
* @property {function} get - Get a setting value
|
||||
* @property {function} set - Set a setting value
|
||||
* @property {function} getAll - Get all settings
|
||||
* @property {function} onDidChange - Register for setting changes
|
||||
*/
|
||||
|
||||
// Export the addon class as default
|
||||
module.exports = MyFirstAddon;
|
||||
module.exports.default = MyFirstAddon;
|
||||
215
renderer/index.js
Normal file
215
renderer/index.js
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* My First Addon - Renderer Process Module
|
||||
*
|
||||
* This file runs in Electron's renderer process and handles:
|
||||
* - React components for GitCaddy's UI
|
||||
* - Communication with the main process via IPC
|
||||
* - UI state management
|
||||
*
|
||||
* The renderer process has limited access compared to main process:
|
||||
* - No direct file system access
|
||||
* - No spawning child processes
|
||||
* - Must use IPC to communicate with main process
|
||||
*/
|
||||
|
||||
const { DEFAULT_SETTINGS } = require('../shared/types');
|
||||
|
||||
/**
|
||||
* Renderer Module Class
|
||||
*
|
||||
* GitCaddy will instantiate this class for UI integration.
|
||||
*/
|
||||
class MyFirstAddonRenderer {
|
||||
constructor() {
|
||||
this.context = null;
|
||||
this.settings = { ...DEFAULT_SETTINGS };
|
||||
this.disposables = [];
|
||||
this.isActivated = false;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// LIFECYCLE METHODS
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Initialize the renderer module.
|
||||
*
|
||||
* @param {IAddonRendererContext} context - Context provided by GitCaddy
|
||||
*/
|
||||
async initialize(context) {
|
||||
this.context = context;
|
||||
context.log.info('MyFirstAddon renderer initializing...');
|
||||
|
||||
// Load settings
|
||||
await this.loadSettings();
|
||||
|
||||
// Listen for settings changes
|
||||
const settingsDisposable = context.settings.onDidChange((key, value) => {
|
||||
this.onSettingChanged(key, value);
|
||||
});
|
||||
this.disposables.push(settingsDisposable);
|
||||
|
||||
// Listen for messages from main process
|
||||
const ipcDisposable = context.ipc.on('addon-message', (data) => {
|
||||
this.onMessage(data);
|
||||
});
|
||||
this.disposables.push(ipcDisposable);
|
||||
|
||||
context.log.info('MyFirstAddon renderer initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the addon is activated.
|
||||
*/
|
||||
onActivated() {
|
||||
this.isActivated = true;
|
||||
this.context?.log.info('MyFirstAddon renderer: addon activated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the addon is deactivated.
|
||||
*/
|
||||
onDeactivated() {
|
||||
this.isActivated = false;
|
||||
this.context?.log.info('MyFirstAddon renderer: addon deactivated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a React component for a specific UI slot.
|
||||
*
|
||||
* GitCaddy calls this to get custom UI components.
|
||||
* Return null if you don't have a component for the slot.
|
||||
*
|
||||
* @param {string} slot - UI slot identifier
|
||||
* @returns {React.ComponentType|null} React component or null
|
||||
*/
|
||||
getComponent(slot) {
|
||||
// Example slots: 'commit-summary-input', 'sidebar-panel', etc.
|
||||
this.context?.log.debug(`getComponent called for slot: ${slot}`);
|
||||
|
||||
// This addon doesn't provide custom React components
|
||||
// If you want to add components, you'd return them here:
|
||||
//
|
||||
// switch (slot) {
|
||||
// case 'sidebar-panel':
|
||||
// return MySidebarComponent;
|
||||
// default:
|
||||
// return null;
|
||||
// }
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of the renderer module.
|
||||
*/
|
||||
dispose() {
|
||||
this.context?.log.info('MyFirstAddon renderer disposing...');
|
||||
|
||||
for (const disposable of this.disposables) {
|
||||
disposable.dispose();
|
||||
}
|
||||
this.disposables = [];
|
||||
|
||||
this.context = null;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SETTINGS
|
||||
// ============================================================
|
||||
|
||||
async loadSettings() {
|
||||
if (!this.context) return;
|
||||
|
||||
const stored = this.context.settings.getAll();
|
||||
this.settings = {
|
||||
enableFeature: stored.enableFeature ?? DEFAULT_SETTINGS.enableFeature,
|
||||
apiKey: stored.apiKey ?? DEFAULT_SETTINGS.apiKey,
|
||||
mode: stored.mode ?? DEFAULT_SETTINGS.mode,
|
||||
maxItems: stored.maxItems ?? DEFAULT_SETTINGS.maxItems,
|
||||
};
|
||||
}
|
||||
|
||||
onSettingChanged(key, value) {
|
||||
if (key in this.settings) {
|
||||
this.settings[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// IPC COMMUNICATION
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Handle messages from the main process.
|
||||
*/
|
||||
onMessage(data) {
|
||||
this.context?.log.debug('Received message from main:', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the main process.
|
||||
*/
|
||||
sendMessage(channel, data) {
|
||||
this.context?.ipc.send(channel, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke a method on the main process and wait for response.
|
||||
*/
|
||||
async invokeMain(method, ...args) {
|
||||
return this.context?.ipc.invoke(method, ...args);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PUBLIC API
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Get current settings.
|
||||
*/
|
||||
getSettings() {
|
||||
return { ...this.settings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a feature is enabled.
|
||||
*/
|
||||
isFeatureEnabled() {
|
||||
return this.settings.enableFeature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current mode.
|
||||
*/
|
||||
getMode() {
|
||||
return this.settings.mode;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// RENDERER CONTEXT INTERFACE (for reference)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* @typedef {Object} IAddonRendererContext
|
||||
* @property {string} addonId - Unique addon identifier
|
||||
* @property {Object} manifest - Parsed addon.json
|
||||
* @property {IAddonLogger} log - Logger instance
|
||||
* @property {IAddonSettings} settings - Settings storage
|
||||
* @property {IAddonIPC} ipc - IPC communication
|
||||
* @property {IAppStateProxy} appState - Access to app state
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} IAppStateProxy
|
||||
* @property {function} getCurrentRepository - Get current repository
|
||||
* @property {function} getSelectedFiles - Get selected files
|
||||
* @property {function} getCurrentDiff - Get current diff
|
||||
* @property {function} getCommitMessage - Get commit message
|
||||
* @property {function} getRecentCommits - Get recent commits
|
||||
*/
|
||||
|
||||
// Export the renderer class as default
|
||||
module.exports = MyFirstAddonRenderer;
|
||||
module.exports.default = MyFirstAddonRenderer;
|
||||
41
shared/types.js
Normal file
41
shared/types.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Shared Types and Constants
|
||||
*
|
||||
* This file contains type definitions and constants shared between
|
||||
* the main process and renderer process.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Default settings for the addon.
|
||||
* These should match the settingsDefinitions in addon.json.
|
||||
*/
|
||||
const DEFAULT_SETTINGS = {
|
||||
enableFeature: true,
|
||||
apiKey: '',
|
||||
mode: 'normal',
|
||||
maxItems: 10,
|
||||
};
|
||||
|
||||
/**
|
||||
* Available modes for the addon.
|
||||
*/
|
||||
const MODES = {
|
||||
NORMAL: 'normal',
|
||||
ADVANCED: 'advanced',
|
||||
DEBUG: 'debug',
|
||||
};
|
||||
|
||||
/**
|
||||
* Event names used by the addon.
|
||||
*/
|
||||
const EVENTS = {
|
||||
SETTINGS_CHANGED: 'settings-changed',
|
||||
ACTION_COMPLETED: 'action-completed',
|
||||
ERROR_OCCURRED: 'error-occurred',
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_SETTINGS,
|
||||
MODES,
|
||||
EVENTS,
|
||||
};
|
||||
589
views/demo.html
Normal file
589
views/demo.html
Normal file
@@ -0,0 +1,589 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>My First Addon - Demo</title>
|
||||
<style>
|
||||
/* ============================================================
|
||||
STYLES
|
||||
GitCaddy injects CSS variables for theming. Use them!
|
||||
============================================================ */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
/* Use GitCaddy's theme colors */
|
||||
font-family: var(--font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
||||
font-size: var(--font-size, 13px);
|
||||
color: var(--foreground, #cccccc);
|
||||
background-color: var(--background, #1e1e1e);
|
||||
padding: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
color: var(--foreground, #cccccc);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid var(--border, #3c3c3c);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2em;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1em;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 12px;
|
||||
color: var(--foreground-muted, #999999);
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: var(--background-secondary, #252526);
|
||||
border: 1px solid var(--border, #3c3c3c);
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 140px 1fr;
|
||||
gap: 8px 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: var(--foreground-muted, #999999);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: var(--foreground, #cccccc);
|
||||
font-family: var(--font-family-mono, 'Consolas', 'Monaco', monospace);
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
list-style: none;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.feature-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background-color: var(--background, #1e1e1e);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.feature-list li::before {
|
||||
content: "\2713"; /* Checkmark */
|
||||
color: var(--success, #4ec9b0);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--button-foreground, #ffffff);
|
||||
background-color: var(--button-background, #0e639c);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: var(--button-hover-background, #1177bb);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
background-color: var(--button-active-background, #0d5a8c);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--button-secondary-background, #3c3c3c);
|
||||
color: var(--button-secondary-foreground, #cccccc);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: var(--button-secondary-hover-background, #4c4c4c);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background-color: var(--success-background, rgba(78, 201, 176, 0.2));
|
||||
color: var(--success, #4ec9b0);
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
background-color: var(--warning-background, rgba(252, 186, 3, 0.2));
|
||||
color: var(--warning, #fcba03);
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background-color: var(--background, #1e1e1e);
|
||||
border: 1px solid var(--border, #3c3c3c);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
font-family: var(--font-family-mono, 'Consolas', 'Monaco', monospace);
|
||||
font-size: 12px;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.log-output {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid var(--border, #3c3c3c);
|
||||
}
|
||||
|
||||
.log-entry:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: var(--foreground-muted, #666666);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.log-info { color: var(--info, #75beff); }
|
||||
.log-warn { color: var(--warning, #fcba03); }
|
||||
.log-error { color: var(--error, #f14c4c); }
|
||||
.log-debug { color: var(--foreground-muted, #666666); }
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--border, #3c3c3c);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--foreground-muted, #999999);
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--foreground, #cccccc);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: var(--foreground, #cccccc);
|
||||
border-bottom-color: var(--accent, #0e639c);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid var(--foreground-muted, #666666);
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>My First Addon</h1>
|
||||
<p>This demo showcases the GitCaddy addon API features.</p>
|
||||
|
||||
<!-- Tabs Navigation -->
|
||||
<div class="tabs">
|
||||
<button class="tab active" data-tab="overview">Overview</button>
|
||||
<button class="tab" data-tab="actions">Actions</button>
|
||||
<button class="tab" data-tab="ipc">IPC Demo</button>
|
||||
<button class="tab" data-tab="logs">Logs</button>
|
||||
</div>
|
||||
|
||||
<!-- Overview Tab -->
|
||||
<div id="overview" class="tab-content active">
|
||||
<div class="section">
|
||||
<h2>Addon Information</h2>
|
||||
<div class="info-grid">
|
||||
<span class="info-label">Addon ID:</span>
|
||||
<span class="info-value" id="addon-id">Loading...</span>
|
||||
|
||||
<span class="info-label">Addon Path:</span>
|
||||
<span class="info-value" id="addon-path">Loading...</span>
|
||||
|
||||
<span class="info-label">Status:</span>
|
||||
<span class="info-value" id="addon-status">
|
||||
<span class="status-badge status-inactive">Inactive</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Current Settings</h2>
|
||||
<div class="info-grid" id="settings-display">
|
||||
<span class="info-label">Loading...</span>
|
||||
<span class="info-value"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Features Demonstrated</h2>
|
||||
<ul class="feature-list" id="features-list">
|
||||
<li>Loading features...</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions Tab -->
|
||||
<div id="actions" class="tab-content">
|
||||
<div class="section">
|
||||
<h2>Test Actions</h2>
|
||||
<p>Click the buttons below to test various addon actions.</p>
|
||||
|
||||
<h3>Notifications</h3>
|
||||
<p>Send a notification to the user.</p>
|
||||
<div class="button-group">
|
||||
<button class="btn" onclick="performAction('notify')">
|
||||
Show Notification
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h3>Logging</h3>
|
||||
<p>Write messages to the addon log.</p>
|
||||
<div class="button-group">
|
||||
<button class="btn btn-secondary" onclick="performAction('log')">
|
||||
Write Test Logs
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h3>Settings</h3>
|
||||
<p>Retrieve current settings from the addon.</p>
|
||||
<div class="button-group">
|
||||
<button class="btn btn-secondary" onclick="performAction('settings')">
|
||||
Get Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Action Result</h2>
|
||||
<div class="code-block" id="action-result">
|
||||
Click an action button to see the result here.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IPC Demo Tab -->
|
||||
<div id="ipc" class="tab-content">
|
||||
<div class="section">
|
||||
<h2>IPC Communication</h2>
|
||||
<p>
|
||||
Addons can communicate between renderer and main process using IPC.
|
||||
This demonstrates sending messages and invoking methods.
|
||||
</p>
|
||||
|
||||
<h3>Send Message</h3>
|
||||
<p>Send a one-way message to the main process.</p>
|
||||
<div class="button-group">
|
||||
<button class="btn btn-secondary" onclick="sendMessage()">
|
||||
Send Message
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h3>Invoke Method</h3>
|
||||
<p>Call a method on the main process and wait for response.</p>
|
||||
<div class="button-group">
|
||||
<button class="btn" onclick="invokeMethod()">
|
||||
Invoke getDemoData
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>IPC Result</h2>
|
||||
<div class="code-block" id="ipc-result">
|
||||
Use the buttons above to test IPC communication.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs Tab -->
|
||||
<div id="logs" class="tab-content">
|
||||
<div class="section">
|
||||
<h2>Activity Log</h2>
|
||||
<p>Real-time log of addon activity in this view.</p>
|
||||
<div class="code-block log-output" id="log-output">
|
||||
<div class="log-entry">
|
||||
<span class="log-time">[--:--:--]</span>
|
||||
<span class="log-info">Waiting for activity...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button class="btn btn-secondary" onclick="clearLogs()">
|
||||
Clear Logs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================
|
||||
SCRIPTS
|
||||
Communication with GitCaddy happens via window.gitcaddy
|
||||
============================================================ -->
|
||||
<script>
|
||||
// ============================================================
|
||||
// GITCADDY BRIDGE
|
||||
// GitCaddy injects a bridge object for communication
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* The GitCaddy bridge provides:
|
||||
* - gitcaddy.invoke(method, ...args) - Call addon main process methods
|
||||
* - gitcaddy.send(channel, data) - Send one-way message to main
|
||||
* - gitcaddy.on(channel, callback) - Listen for messages from main
|
||||
* - gitcaddy.log.info/warn/error/debug() - Log messages
|
||||
*
|
||||
* The bridge is injected automatically when the view loads.
|
||||
*/
|
||||
|
||||
// Wait for the bridge to be ready
|
||||
function whenReady(callback) {
|
||||
if (window.gitcaddy) {
|
||||
callback();
|
||||
} else {
|
||||
window.addEventListener('gitcaddy-ready', callback);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// INITIALIZATION
|
||||
// ============================================================
|
||||
|
||||
whenReady(async () => {
|
||||
log('info', 'Demo view initialized');
|
||||
|
||||
// Load addon data
|
||||
try {
|
||||
const data = await window.gitcaddy.invoke('getDemoData');
|
||||
updateDisplay(data);
|
||||
log('info', 'Loaded addon data');
|
||||
} catch (err) {
|
||||
log('error', 'Failed to load addon data: ' + err.message);
|
||||
}
|
||||
|
||||
// Listen for messages from main process
|
||||
window.gitcaddy.on('addon-message', (data) => {
|
||||
log('info', 'Received message: ' + JSON.stringify(data));
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// DISPLAY UPDATES
|
||||
// ============================================================
|
||||
|
||||
function updateDisplay(data) {
|
||||
// Update addon info
|
||||
document.getElementById('addon-id').textContent = data.addonId || 'Unknown';
|
||||
document.getElementById('addon-path').textContent = data.addonPath || 'Unknown';
|
||||
|
||||
// Update status badge
|
||||
const statusEl = document.getElementById('addon-status');
|
||||
if (data.isActive) {
|
||||
statusEl.innerHTML = '<span class="status-badge status-active">Active</span>';
|
||||
} else {
|
||||
statusEl.innerHTML = '<span class="status-badge status-inactive">Inactive</span>';
|
||||
}
|
||||
|
||||
// Update settings
|
||||
const settingsEl = document.getElementById('settings-display');
|
||||
if (data.settings) {
|
||||
settingsEl.innerHTML = Object.entries(data.settings)
|
||||
.filter(([key]) => key !== 'apiKey') // Don't show API key
|
||||
.map(([key, value]) => `
|
||||
<span class="info-label">${key}:</span>
|
||||
<span class="info-value">${JSON.stringify(value)}</span>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Update features list
|
||||
const featuresEl = document.getElementById('features-list');
|
||||
if (data.features && data.features.length > 0) {
|
||||
featuresEl.innerHTML = data.features
|
||||
.map(f => `<li>${f}</li>`)
|
||||
.join('');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// TAB NAVIGATION
|
||||
// ============================================================
|
||||
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
// Update tab buttons
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
|
||||
// Update tab content
|
||||
const tabId = tab.dataset.tab;
|
||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||
document.getElementById(tabId).classList.add('active');
|
||||
|
||||
log('debug', `Switched to ${tabId} tab`);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// ACTION HANDLERS
|
||||
// ============================================================
|
||||
|
||||
async function performAction(actionType) {
|
||||
const resultEl = document.getElementById('action-result');
|
||||
resultEl.innerHTML = '<span class="loading"></span> Executing...';
|
||||
log('info', `Performing action: ${actionType}`);
|
||||
|
||||
try {
|
||||
const result = await window.gitcaddy.invoke('performAction', actionType);
|
||||
resultEl.textContent = JSON.stringify(result, null, 2);
|
||||
log('info', `Action result: ${JSON.stringify(result)}`);
|
||||
} catch (err) {
|
||||
resultEl.textContent = 'Error: ' + err.message;
|
||||
log('error', `Action failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// IPC HANDLERS
|
||||
// ============================================================
|
||||
|
||||
function sendMessage() {
|
||||
const resultEl = document.getElementById('ipc-result');
|
||||
|
||||
if (!window.gitcaddy) {
|
||||
resultEl.textContent = 'Error: GitCaddy bridge not available';
|
||||
return;
|
||||
}
|
||||
|
||||
// Send a one-way message
|
||||
window.gitcaddy.send('demo-message', {
|
||||
type: 'greeting',
|
||||
message: 'Hello from the demo view!',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
resultEl.textContent = 'Message sent! Check main process logs.';
|
||||
log('info', 'Sent message to main process');
|
||||
}
|
||||
|
||||
async function invokeMethod() {
|
||||
const resultEl = document.getElementById('ipc-result');
|
||||
resultEl.innerHTML = '<span class="loading"></span> Invoking...';
|
||||
log('info', 'Invoking getDemoData method');
|
||||
|
||||
try {
|
||||
const result = await window.gitcaddy.invoke('getDemoData');
|
||||
resultEl.textContent = JSON.stringify(result, null, 2);
|
||||
log('info', 'Method invocation successful');
|
||||
} catch (err) {
|
||||
resultEl.textContent = 'Error: ' + err.message;
|
||||
log('error', `Method invocation failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// LOGGING
|
||||
// ============================================================
|
||||
|
||||
function log(level, message) {
|
||||
const logEl = document.getElementById('log-output');
|
||||
const now = new Date();
|
||||
const time = now.toTimeString().split(' ')[0];
|
||||
|
||||
const entry = document.createElement('div');
|
||||
entry.className = 'log-entry';
|
||||
entry.innerHTML = `
|
||||
<span class="log-time">[${time}]</span>
|
||||
<span class="log-${level}">${escapeHtml(message)}</span>
|
||||
`;
|
||||
|
||||
logEl.appendChild(entry);
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
|
||||
// Also log to GitCaddy's logger if available
|
||||
if (window.gitcaddy?.log) {
|
||||
window.gitcaddy.log[level]?.(message);
|
||||
}
|
||||
}
|
||||
|
||||
function clearLogs() {
|
||||
const logEl = document.getElementById('log-output');
|
||||
logEl.innerHTML = `
|
||||
<div class="log-entry">
|
||||
<span class="log-time">[${new Date().toTimeString().split(' ')[0]}]</span>
|
||||
<span class="log-info">Logs cleared</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
586
views/settings.html
Normal file
586
views/settings.html
Normal file
@@ -0,0 +1,586 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>My First Addon - Settings</title>
|
||||
<style>
|
||||
/* ============================================================
|
||||
SETTINGS VIEW STYLES
|
||||
Demonstrates all setting types available in GitCaddy addons
|
||||
============================================================ */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
||||
font-size: var(--font-size, 13px);
|
||||
color: var(--foreground, #cccccc);
|
||||
background-color: var(--background, #1e1e1e);
|
||||
padding: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.4em;
|
||||
margin-bottom: 8px;
|
||||
color: var(--foreground, #cccccc);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--foreground-muted, #999999);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* Form Sections */
|
||||
.form-section {
|
||||
background-color: var(--background-secondary, #252526);
|
||||
border: 1px solid var(--border, #3c3c3c);
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-section h2 {
|
||||
font-size: 1.1em;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border, #3c3c3c);
|
||||
}
|
||||
|
||||
/* Form Groups */
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
color: var(--foreground, #cccccc);
|
||||
}
|
||||
|
||||
.form-description {
|
||||
font-size: 12px;
|
||||
color: var(--foreground-muted, #999999);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Input Styles */
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
color: var(--input-foreground, #cccccc);
|
||||
background-color: var(--input-background, #3c3c3c);
|
||||
border: 1px solid var(--input-border, #3c3c3c);
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: var(--focus-border, #0e639c);
|
||||
box-shadow: 0 0 0 1px var(--focus-border, #0e639c);
|
||||
}
|
||||
|
||||
.form-input:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: var(--input-placeholder, #666666);
|
||||
}
|
||||
|
||||
/* Password Input with Toggle */
|
||||
.input-with-button {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.input-with-button .form-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Select Styles */
|
||||
.form-select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
color: var(--input-foreground, #cccccc);
|
||||
background-color: var(--input-background, #3c3c3c);
|
||||
border: 1px solid var(--input-border, #3c3c3c);
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.form-select:focus {
|
||||
border-color: var(--focus-border, #0e639c);
|
||||
}
|
||||
|
||||
/* Checkbox Styles */
|
||||
.checkbox-wrapper {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.checkbox-wrapper input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-top: 2px;
|
||||
accent-color: var(--accent, #0e639c);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-wrapper .checkbox-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.checkbox-wrapper .checkbox-title {
|
||||
font-weight: 500;
|
||||
color: var(--foreground, #cccccc);
|
||||
}
|
||||
|
||||
.checkbox-wrapper .checkbox-description {
|
||||
font-size: 12px;
|
||||
color: var(--foreground-muted, #999999);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Number Input */
|
||||
.form-input[type="number"] {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
/* Range Slider */
|
||||
.range-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.form-range {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
-webkit-appearance: none;
|
||||
background: var(--input-background, #3c3c3c);
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-range::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: var(--accent, #0e639c);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.range-value {
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
font-family: var(--font-family-mono, monospace);
|
||||
color: var(--foreground-muted, #999999);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--button-foreground, #ffffff);
|
||||
background-color: var(--button-background, #0e639c);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: var(--button-hover-background, #1177bb);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--button-secondary-background, #3c3c3c);
|
||||
color: var(--button-secondary-foreground, #cccccc);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: var(--button-secondary-hover-background, #4c4c4c);
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 4px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Button Group */
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--border, #3c3c3c);
|
||||
}
|
||||
|
||||
/* Status Messages */
|
||||
.status-message {
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
margin-top: 16px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.status-message.success {
|
||||
display: block;
|
||||
background-color: var(--success-background, rgba(78, 201, 176, 0.15));
|
||||
color: var(--success, #4ec9b0);
|
||||
border: 1px solid var(--success, #4ec9b0);
|
||||
}
|
||||
|
||||
.status-message.error {
|
||||
display: block;
|
||||
background-color: var(--error-background, rgba(241, 76, 76, 0.15));
|
||||
color: var(--error, #f14c4c);
|
||||
border: 1px solid var(--error, #f14c4c);
|
||||
}
|
||||
|
||||
/* Code Example Section */
|
||||
.code-example {
|
||||
background-color: var(--background, #1e1e1e);
|
||||
border: 1px solid var(--border, #3c3c3c);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
font-family: var(--font-family-mono, 'Consolas', 'Monaco', monospace);
|
||||
font-size: 12px;
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.code-comment {
|
||||
color: var(--foreground-muted, #6a9955);
|
||||
}
|
||||
|
||||
.code-key {
|
||||
color: var(--info, #9cdcfe);
|
||||
}
|
||||
|
||||
.code-string {
|
||||
color: var(--warning, #ce9178);
|
||||
}
|
||||
|
||||
.code-number {
|
||||
color: var(--success, #b5cea8);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>My First Addon Settings</h1>
|
||||
<p class="subtitle">Configure your addon preferences below.</p>
|
||||
|
||||
<!-- General Settings -->
|
||||
<div class="form-section">
|
||||
<h2>General Settings</h2>
|
||||
|
||||
<!-- Boolean Setting (Checkbox) -->
|
||||
<div class="form-group">
|
||||
<div class="checkbox-wrapper">
|
||||
<input type="checkbox" id="enableFeature" checked>
|
||||
<div class="checkbox-label">
|
||||
<div class="checkbox-title">Enable Feature</div>
|
||||
<div class="checkbox-description">
|
||||
Toggle the main functionality of this addon on or off.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Select Setting (Dropdown) -->
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="mode">Operating Mode</label>
|
||||
<select id="mode" class="form-select">
|
||||
<option value="normal">Normal</option>
|
||||
<option value="advanced">Advanced</option>
|
||||
<option value="debug">Debug</option>
|
||||
</select>
|
||||
<p class="form-description">
|
||||
Select the operating mode for the addon.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Settings -->
|
||||
<div class="form-section">
|
||||
<h2>API Configuration</h2>
|
||||
|
||||
<!-- String Setting (Password) -->
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="apiKey">API Key</label>
|
||||
<div class="input-with-button">
|
||||
<input type="password" id="apiKey" class="form-input" placeholder="Enter your API key">
|
||||
<button class="btn btn-secondary btn-small" onclick="togglePassword()">Show</button>
|
||||
</div>
|
||||
<p class="form-description">
|
||||
Your API key for external service integration. Keep this secret!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Settings -->
|
||||
<div class="form-section">
|
||||
<h2>Advanced Settings</h2>
|
||||
|
||||
<!-- Number Setting -->
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="maxItems">Maximum Items</label>
|
||||
<input type="number" id="maxItems" class="form-input" value="10" min="1" max="100">
|
||||
<p class="form-description">
|
||||
Maximum number of items to process (1-100).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Range Slider Example -->
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="threshold">Threshold</label>
|
||||
<div class="range-wrapper">
|
||||
<input type="range" id="threshold" class="form-range" min="0" max="100" value="50">
|
||||
<span class="range-value" id="thresholdValue">50</span>
|
||||
</div>
|
||||
<p class="form-description">
|
||||
Sensitivity threshold for processing (0-100).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Schema Reference -->
|
||||
<div class="form-section">
|
||||
<h2>Settings Schema Reference</h2>
|
||||
<p style="margin-bottom: 12px; color: var(--foreground-muted);">
|
||||
This is how settings are defined in addon.json:
|
||||
</p>
|
||||
<div class="code-example"><span class="code-comment">// In addon.json -> contributes -> settingsDefinitions</span>
|
||||
[
|
||||
{
|
||||
<span class="code-key">"key"</span>: <span class="code-string">"enableFeature"</span>,
|
||||
<span class="code-key">"type"</span>: <span class="code-string">"boolean"</span>,
|
||||
<span class="code-key">"default"</span>: <span class="code-number">true</span>,
|
||||
<span class="code-key">"description"</span>: <span class="code-string">"Enable the main feature"</span>
|
||||
},
|
||||
{
|
||||
<span class="code-key">"key"</span>: <span class="code-string">"mode"</span>,
|
||||
<span class="code-key">"type"</span>: <span class="code-string">"select"</span>,
|
||||
<span class="code-key">"default"</span>: <span class="code-string">"normal"</span>,
|
||||
<span class="code-key">"options"</span>: [
|
||||
{ <span class="code-key">"label"</span>: <span class="code-string">"Normal"</span>, <span class="code-key">"value"</span>: <span class="code-string">"normal"</span> },
|
||||
{ <span class="code-key">"label"</span>: <span class="code-string">"Advanced"</span>, <span class="code-key">"value"</span>: <span class="code-string">"advanced"</span> }
|
||||
]
|
||||
}
|
||||
]</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="button-group">
|
||||
<button class="btn" onclick="saveSettings()">Save Settings</button>
|
||||
<button class="btn btn-secondary" onclick="resetSettings()">Reset to Defaults</button>
|
||||
</div>
|
||||
|
||||
<!-- Status Message -->
|
||||
<div id="statusMessage" class="status-message"></div>
|
||||
|
||||
<script>
|
||||
// ============================================================
|
||||
// SETTINGS VIEW SCRIPT
|
||||
// ============================================================
|
||||
|
||||
// Default settings (should match addon.json settingsDefinitions)
|
||||
const DEFAULT_SETTINGS = {
|
||||
enableFeature: true,
|
||||
apiKey: '',
|
||||
mode: 'normal',
|
||||
maxItems: 10
|
||||
};
|
||||
|
||||
// Current settings
|
||||
let currentSettings = { ...DEFAULT_SETTINGS };
|
||||
|
||||
// ============================================================
|
||||
// INITIALIZATION
|
||||
// ============================================================
|
||||
|
||||
// Wait for GitCaddy bridge
|
||||
function whenReady(callback) {
|
||||
if (window.gitcaddy) {
|
||||
callback();
|
||||
} else {
|
||||
window.addEventListener('gitcaddy-ready', callback);
|
||||
}
|
||||
}
|
||||
|
||||
whenReady(async () => {
|
||||
console.log('Settings view initializing...');
|
||||
|
||||
// Load settings from storage
|
||||
try {
|
||||
const stored = await window.gitcaddy.invoke('getSettings');
|
||||
if (stored) {
|
||||
currentSettings = { ...DEFAULT_SETTINGS, ...stored };
|
||||
// Note: API key might be hidden in the response
|
||||
if (stored.hasApiKey) {
|
||||
document.getElementById('apiKey').placeholder = 'API key is set (hidden)';
|
||||
}
|
||||
}
|
||||
updateUI();
|
||||
console.log('Settings loaded');
|
||||
} catch (err) {
|
||||
console.error('Failed to load settings:', err);
|
||||
showStatus('error', 'Failed to load settings: ' + err.message);
|
||||
}
|
||||
|
||||
// Set up event listeners
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// UI UPDATES
|
||||
// ============================================================
|
||||
|
||||
function updateUI() {
|
||||
document.getElementById('enableFeature').checked = currentSettings.enableFeature;
|
||||
document.getElementById('mode').value = currentSettings.mode;
|
||||
// Don't show the actual API key for security
|
||||
// document.getElementById('apiKey').value = currentSettings.apiKey;
|
||||
document.getElementById('maxItems').value = currentSettings.maxItems;
|
||||
}
|
||||
|
||||
function setupEventListeners() {
|
||||
// Range slider value display
|
||||
const threshold = document.getElementById('threshold');
|
||||
const thresholdValue = document.getElementById('thresholdValue');
|
||||
|
||||
threshold.addEventListener('input', () => {
|
||||
thresholdValue.textContent = threshold.value;
|
||||
});
|
||||
|
||||
// Auto-save on change (optional - you could remove this)
|
||||
const inputs = document.querySelectorAll('input, select');
|
||||
inputs.forEach(input => {
|
||||
input.addEventListener('change', () => {
|
||||
// Mark as changed but don't auto-save
|
||||
console.log(`Setting changed: ${input.id}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ACTIONS
|
||||
// ============================================================
|
||||
|
||||
async function saveSettings() {
|
||||
const settings = {
|
||||
enableFeature: document.getElementById('enableFeature').checked,
|
||||
mode: document.getElementById('mode').value,
|
||||
maxItems: parseInt(document.getElementById('maxItems').value, 10)
|
||||
};
|
||||
|
||||
// Only include API key if it was changed (not empty placeholder)
|
||||
const apiKeyInput = document.getElementById('apiKey');
|
||||
if (apiKeyInput.value) {
|
||||
settings.apiKey = apiKeyInput.value;
|
||||
}
|
||||
|
||||
try {
|
||||
await window.gitcaddy.invoke('updateSettings', settings);
|
||||
currentSettings = { ...currentSettings, ...settings };
|
||||
|
||||
showStatus('success', 'Settings saved successfully!');
|
||||
console.log('Settings saved:', settings);
|
||||
|
||||
// Clear the API key input after saving
|
||||
if (apiKeyInput.value) {
|
||||
apiKeyInput.value = '';
|
||||
apiKeyInput.placeholder = 'API key is set (hidden)';
|
||||
}
|
||||
} catch (err) {
|
||||
showStatus('error', 'Failed to save settings: ' + err.message);
|
||||
console.error('Failed to save settings:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function resetSettings() {
|
||||
if (!confirm('Are you sure you want to reset all settings to their defaults?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await window.gitcaddy.invoke('updateSettings', DEFAULT_SETTINGS);
|
||||
currentSettings = { ...DEFAULT_SETTINGS };
|
||||
updateUI();
|
||||
|
||||
// Reset API key placeholder
|
||||
const apiKeyInput = document.getElementById('apiKey');
|
||||
apiKeyInput.value = '';
|
||||
apiKeyInput.placeholder = 'Enter your API key';
|
||||
|
||||
showStatus('success', 'Settings reset to defaults!');
|
||||
console.log('Settings reset to defaults');
|
||||
} catch (err) {
|
||||
showStatus('error', 'Failed to reset settings: ' + err.message);
|
||||
console.error('Failed to reset settings:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function togglePassword() {
|
||||
const input = document.getElementById('apiKey');
|
||||
const button = event.target;
|
||||
|
||||
if (input.type === 'password') {
|
||||
input.type = 'text';
|
||||
button.textContent = 'Hide';
|
||||
} else {
|
||||
input.type = 'password';
|
||||
button.textContent = 'Show';
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// STATUS MESSAGES
|
||||
// ============================================================
|
||||
|
||||
function showStatus(type, message) {
|
||||
const statusEl = document.getElementById('statusMessage');
|
||||
statusEl.textContent = message;
|
||||
statusEl.className = 'status-message ' + type;
|
||||
|
||||
// Auto-hide after 3 seconds
|
||||
setTimeout(() => {
|
||||
statusEl.className = 'status-message';
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user