mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 18:10:18 +05:30
feat: plugin system with manifest-based discovery
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "writing-studio",
|
||||
"version": "1.0.0",
|
||||
"description": "Goal tracking, manuscript panels, snapshots, and sprint engine for writers",
|
||||
"author": "ConcreteInfo",
|
||||
"builtin": true
|
||||
}
|
||||
@@ -4,3 +4,4 @@ pub mod git;
|
||||
pub mod app;
|
||||
pub mod pdf;
|
||||
pub mod dialog;
|
||||
pub mod plugins;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
use crate::error::AppError;
|
||||
use crate::plugin_manager::PluginMetadata;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_plugins() -> Vec<PluginMetadata> {
|
||||
// This would be connected to the app state in a full implementation
|
||||
// For now, return the built-in plugins
|
||||
vec![
|
||||
PluginMetadata {
|
||||
name: "writing-studio".to_string(),
|
||||
version: "1.0.0".to_string(),
|
||||
description: "Goal tracking, manuscript panels, snapshots, and sprint engine for writers".to_string(),
|
||||
author: "ConcreteInfo".to_string(),
|
||||
builtin: true,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_plugin(name: String) -> Option<PluginMetadata> {
|
||||
match name.as_str() {
|
||||
"writing-studio" => Some(PluginMetadata {
|
||||
name: "writing-studio".to_string(),
|
||||
version: "1.0.0".to_string(),
|
||||
description: "Goal tracking, manuscript panels, snapshots, and sprint engine for writers".to_string(),
|
||||
author: "ConcreteInfo".to_string(),
|
||||
builtin: true,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod menu;
|
||||
pub mod tray;
|
||||
pub mod pdf_ops;
|
||||
pub mod plugin_manager;
|
||||
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
@@ -42,6 +43,8 @@ pub fn run() {
|
||||
crate::commands::dialog::open_file_dialog,
|
||||
crate::commands::dialog::save_file_dialog,
|
||||
crate::commands::dialog::select_folder_dialog,
|
||||
crate::commands::plugins::list_plugins,
|
||||
crate::commands::plugins::get_plugin,
|
||||
])
|
||||
.setup(|app| {
|
||||
log::info!("MarkdownConverter starting up");
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use crate::error::AppError;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PluginMetadata {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub description: String,
|
||||
pub author: String,
|
||||
pub builtin: bool,
|
||||
}
|
||||
|
||||
pub struct PluginManager {
|
||||
plugins: HashMap<String, PluginMetadata>,
|
||||
enabled: Vec<String>,
|
||||
}
|
||||
|
||||
impl PluginManager {
|
||||
pub fn new() -> Self {
|
||||
PluginManager {
|
||||
plugins: HashMap::new(),
|
||||
enabled: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_plugins(&mut self, plugin_dir: PathBuf) -> Result<(), AppError> {
|
||||
if !plugin_dir.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for entry in walkdir::WalkDir::new(&plugin_dir)
|
||||
.max_depth(1)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
{
|
||||
let path = entry.path();
|
||||
if !path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let manifest_path = path.join("manifest.json");
|
||||
if manifest_path.exists() {
|
||||
let content = std::fs::read_to_string(&manifest_path)
|
||||
.map_err(|e| AppError::Config(format!("Failed to read manifest: {}", e)))?;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct RawPluginMetadata {
|
||||
name: String,
|
||||
version: String,
|
||||
description: String,
|
||||
author: String,
|
||||
builtin: Option<bool>,
|
||||
}
|
||||
|
||||
let raw: RawPluginMetadata = serde_json::from_str(&content)
|
||||
.map_err(|e| AppError::Config(format!("Failed to parse manifest: {}", e)))?;
|
||||
|
||||
let metadata = PluginMetadata {
|
||||
name: raw.name,
|
||||
version: raw.version,
|
||||
description: raw.description,
|
||||
author: raw.author,
|
||||
builtin: raw.builtin.unwrap_or(false),
|
||||
};
|
||||
|
||||
self.plugins.insert(metadata.name.clone(), metadata);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn enable_plugin(&mut self, name: &str) {
|
||||
if !self.enabled.contains(&name.to_string()) {
|
||||
self.enabled.push(name.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disable_plugin(&mut self, name: &str) {
|
||||
self.enabled.retain(|n| n != name);
|
||||
}
|
||||
|
||||
pub fn get_enabled(&self) -> Vec<String> {
|
||||
self.enabled.clone()
|
||||
}
|
||||
|
||||
pub fn list_plugins(&self) -> Vec<PluginMetadata> {
|
||||
self.plugins.values().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn get_plugin(&self, name: &str) -> Option<PluginMetadata> {
|
||||
self.plugins.get(name).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PluginManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -85,4 +85,12 @@ export const events = {
|
||||
onToggleBottomPanel: (callback) => listen('toggle-bottom-panel', callback),
|
||||
onConversionComplete: (callback) => listen('conversion-complete', callback),
|
||||
onConversionStatus: (callback) => listen('conversion-status', callback),
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// PLUGIN OPERATIONS
|
||||
// ============================================
|
||||
export const plugins = {
|
||||
list: () => invoke('list_plugins', {}),
|
||||
get: (name) => invoke('get_plugin', { name }),
|
||||
};
|
||||
Reference in New Issue
Block a user