This document provides practical examples for common plugin use cases using the time-tracker-plugin-sdk from crates.io.
A simple plugin that responds to commands.
use time_tracker_plugin_sdk::{Plugin, PluginInfo, PluginAPIInterface};
use serde_json;
pub struct BasicPlugin {
info: PluginInfo,
}
impl BasicPlugin {
pub fn new() -> Self {
Self {
info: PluginInfo {
id: "basic-plugin".to_string(),
name: "Basic Plugin".to_string(),
version: "1.0.0".to_string(),
description: Some("A basic example plugin".to_string()),
},
}
}
}
impl Plugin for BasicPlugin {
fn info(&self) -> &PluginInfo {
&self.info
}
fn initialize(&mut self, _api: &dyn PluginAPIInterface) -> Result<(), String> {
println!("BasicPlugin: Initialized");
Ok(())
}
fn invoke_command(&self, command: &str, params: serde_json::Value, _api: &dyn PluginAPIInterface) -> Result<serde_json::Value, String> {
match command {
"hello" => {
Ok(serde_json::json!({
"message": "Hello from BasicPlugin!",
"params": params
}))
}
_ => Err(format!("Unknown command: {}", command))
}
}
fn shutdown(&self) -> Result<(), String> {
println!("BasicPlugin: Shutdown");
Ok(())
}
fn get_schema_extensions(&self) -> Vec<time_tracker_plugin_sdk::SchemaExtension> {
vec![]
}
fn get_frontend_bundle(&self) -> Option<Vec<u8>> {
None
}
}import React, { useState } from 'react';
export const BasicPluginWidget: React.FC = () => {
const [message, setMessage] = useState<string>('');
const handleCommand = async () => {
// Call plugin command via Tauri
// const result = await invoke('invoke_plugin_command', {
// pluginId: 'basic-plugin',
// command: 'hello',
// params: {}
// });
// setMessage(result.message);
};
return (
<div className="p-4 bg-blue-50 rounded-lg">
<h3 className="text-lg font-semibold mb-2">Basic Plugin</h3>
<button onClick={handleCommand} className="px-4 py-2 bg-blue-500 text-white rounded">
Call Command
</button>
{message && <p className="mt-2">{message}</p>}
</div>
);
};
export default {
BasicPluginWidget,
};Extend activities with custom fields.
use time_tracker_plugin_sdk::{Plugin, PluginInfo, PluginAPIInterface, EntityType, SchemaChange, ModelField, TableColumn};
pub struct ActivityExtensionPlugin {
info: PluginInfo,
}
impl ActivityExtensionPlugin {
pub fn new() -> Self {
Self {
info: PluginInfo {
id: "activity-extension".to_string(),
name: "Activity Extension".to_string(),
version: "1.0.0".to_string(),
description: Some("Adds priority field to activities".to_string()),
},
}
}
}
impl Plugin for ActivityExtensionPlugin {
fn info(&self) -> &PluginInfo {
&self.info
}
fn initialize(&mut self, api: &dyn PluginAPIInterface) -> Result<(), String> {
// Add a priority column to activities table
api.register_schema_extension(
EntityType::Activity,
vec![
SchemaChange::AddColumn {
table: "activities".to_string(),
column: "priority".to_string(),
column_type: "INTEGER".to_string(),
default: Some("0".to_string()),
foreign_key: None,
},
],
)?;
// Register model extension so the field is available in Rust types
api.register_model_extension(
EntityType::Activity,
vec![
ModelField {
name: "priority".to_string(),
type_: "Option<i32>".to_string(),
optional: true,
},
],
)?;
Ok(())
}
fn invoke_command(&self, command: &str, params: serde_json::Value, api: &dyn PluginAPIInterface) -> Result<serde_json::Value, String> {
match command {
"set_priority" => {
// Use call_db_method to interact with database
api.call_db_method("update_activity", serde_json::json!({
"id": params["id"],
"priority": params["priority"]
}))
}
_ => Err(format!("Unknown command: {}", command))
}
}
fn shutdown(&self) -> Result<(), String> {
Ok(())
}
fn get_schema_extensions(&self) -> Vec<time_tracker_plugin_sdk::SchemaExtension> {
vec![]
}
fn get_frontend_bundle(&self) -> Option<Vec<u8>> {
None
}
}Create a custom table for plugin data.
use time_tracker_plugin_sdk::{Plugin, PluginInfo, PluginAPIInterface, EntityType, SchemaChange, TableColumn, ForeignKey};
pub struct NotesPlugin {
info: PluginInfo,
}
impl NotesPlugin {
pub fn new() -> Self {
Self {
info: PluginInfo {
id: "notes-plugin".to_string(),
name: "Notes Plugin".to_string(),
version: "1.0.0".to_string(),
description: Some("Add notes to activities".to_string()),
},
}
}
}
impl Plugin for NotesPlugin {
fn info(&self) -> &PluginInfo {
&self.info
}
fn initialize(&mut self, api: &dyn PluginAPIInterface) -> Result<(), String> {
// Create a custom table for notes
api.register_schema_extension(
EntityType::Activity,
vec![
SchemaChange::CreateTable {
table: "activity_notes".to_string(),
columns: vec![
TableColumn {
name: "id".to_string(),
column_type: "INTEGER".to_string(),
primary_key: true,
nullable: false,
default: None,
foreign_key: None,
},
TableColumn {
name: "activity_id".to_string(),
column_type: "INTEGER".to_string(),
primary_key: false,
nullable: false,
default: None,
foreign_key: Some(ForeignKey {
table: "activities".to_string(),
column: "id".to_string(),
}),
},
TableColumn {
name: "note".to_string(),
column_type: "TEXT".to_string(),
primary_key: false,
nullable: false,
default: None,
foreign_key: None,
},
TableColumn {
name: "created_at".to_string(),
column_type: "INTEGER".to_string(),
primary_key: false,
nullable: false,
default: Some("(strftime('%s', 'now'))".to_string()),
foreign_key: None,
},
],
},
SchemaChange::AddIndex {
table: "activity_notes".to_string(),
index: "idx_activity_notes_activity_id".to_string(),
columns: vec!["activity_id".to_string()],
},
],
)?;
Ok(())
}
fn invoke_command(&self, command: &str, params: serde_json::Value, api: &dyn PluginAPIInterface) -> Result<serde_json::Value, String> {
match command {
"add_note" => {
// Note: You'll need to implement the database method in the core app
// or use raw SQL through call_db_method
api.call_db_method("add_activity_note", params)
}
"get_notes" => {
api.call_db_method("get_activity_notes", params)
}
_ => Err(format!("Unknown command: {}", command))
}
}
fn shutdown(&self) -> Result<(), String> {
Ok(())
}
fn get_schema_extensions(&self) -> Vec<time_tracker_plugin_sdk::SchemaExtension> {
vec![]
}
fn get_frontend_bundle(&self) -> Option<Vec<u8>> {
None
}
}React component for plugin settings.
import React, { useState, useEffect } from 'react';
interface PluginSettings {
enabled: boolean;
threshold: number;
notifications: boolean;
}
export const MyPluginSettings: React.FC = () => {
const [settings, setSettings] = useState<PluginSettings>({
enabled: false,
threshold: 60,
notifications: true,
});
const [loading, setLoading] = useState(true);
useEffect(() => {
// Load settings via plugin command
// const loadSettings = async () => {
// const saved = await invoke('invoke_plugin_command', {
// pluginId: 'my-plugin',
// command: 'get_settings',
// params: {}
// });
// if (saved) setSettings(saved);
// setLoading(false);
// };
// loadSettings();
setLoading(false);
}, []);
const saveSettings = async () => {
// Save settings via plugin command
// await invoke('invoke_plugin_command', {
// pluginId: 'my-plugin',
// command: 'save_settings',
// params: { settings }
// });
alert('Settings saved!');
};
if (loading) {
return <div>Loading...</div>;
}
return (
<div className="p-4 space-y-4">
<h2 className="text-xl font-bold">My Plugin Settings</h2>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={settings.enabled}
onChange={(e) => setSettings({ ...settings, enabled: e.target.checked })}
/>
<span>Enable plugin</span>
</label>
<div>
<label className="block mb-1">Threshold (seconds)</label>
<input
type="number"
value={settings.threshold}
onChange={(e) => setSettings({ ...settings, threshold: parseInt(e.target.value) })}
className="border rounded px-2 py-1"
/>
</div>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={settings.notifications}
onChange={(e) => setSettings({ ...settings, notifications: e.target.checked })}
/>
<span>Enable notifications</span>
</label>
<button
onClick={saveSettings}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Save Settings
</button>
</div>
);
};
export default {
MyPluginSettings,
};- SDK Version: Always use the same SDK version that matches your target TimeTracker version
- Schema Extensions: Register schema extensions in
initialize()before using them - Error Handling: Always return proper
Resulttypes from trait methods - Resource Cleanup: Clean up resources in
shutdown() - Database Access: Use
call_db_method()to interact with the database through the core app - Frontend Communication: Use Tauri commands to communicate between frontend and backend
- Testing: Test your plugin on all target platforms before releasing
Add the SDK to your Cargo.toml:
[dependencies]
time-tracker-plugin-sdk = "0.2.10" # Use the published version from crates.ioThe SDK is published on crates.io and should be used by all plugins instead of local path or git dependencies.