feat: Implement async scan, progress bar, and improve Paper detection
This commit is contained in:
parent
eb51afaea8
commit
fc96a10397
@ -10,10 +10,11 @@
|
|||||||
- [x] Setup SQLite or JSON storage for plugin data
|
- [x] Setup SQLite or JSON storage for plugin data
|
||||||
- [x] Create core data models
|
- [x] Create core data models
|
||||||
- [x] Build server/plugin directory scanner (basic implementation)
|
- [x] Build server/plugin directory scanner (basic implementation)
|
||||||
|
- [x] Implement asynchronous server scanning (non-blocking)
|
||||||
- [x] Implement JAR file parser for plugin.yml extraction
|
- [x] Implement JAR file parser for plugin.yml extraction
|
||||||
|
|
||||||
## Plugin Discovery (In Progress)
|
## Plugin Discovery (Needs Refinement)
|
||||||
- [x] Create server type detection (Paper, Spigot, etc.)
|
- [ ] Improve server type detection (Paper/Spigot distinction, etc.)
|
||||||
- [x] Implement plugins folder detection logic
|
- [x] Implement plugins folder detection logic
|
||||||
- [x] Design plugin metadata extraction system
|
- [x] Design plugin metadata extraction system
|
||||||
- [x] Build plugin hash identification system
|
- [x] Build plugin hash identification system
|
||||||
@ -35,7 +36,8 @@
|
|||||||
|
|
||||||
## UI Development (In Progress)
|
## UI Development (In Progress)
|
||||||
- [x] Design and implement main dashboard
|
- [x] Design and implement main dashboard
|
||||||
- [x] Create plugin list view with version indicators
|
- [x] Refine plugin list view styling and layout
|
||||||
|
- [x] Add progress indicator for server scanning
|
||||||
- [x] Build server folder selection interface
|
- [x] Build server folder selection interface
|
||||||
- [x] Implement plugin detail view
|
- [x] Implement plugin detail view
|
||||||
- [x] Add update notification system
|
- [x] Add update notification system
|
||||||
|
@ -3,13 +3,14 @@ use serde::{Serialize, Deserialize};
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use tauri::command;
|
use tauri::{command, Emitter};
|
||||||
use zip::ZipArchive;
|
use zip::ZipArchive;
|
||||||
use yaml_rust::{YamlLoader, Yaml};
|
use yaml_rust::{YamlLoader, Yaml};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use sha2::{Sha256, Digest};
|
use sha2::{Sha256, Digest};
|
||||||
use reqwest;
|
use reqwest;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
use tauri::AppHandle;
|
||||||
|
|
||||||
// Add the crawlers module
|
// Add the crawlers module
|
||||||
mod crawlers;
|
mod crawlers;
|
||||||
@ -323,11 +324,27 @@ fn yaml_str_array(yaml: &Yaml, key: &str) -> Option<Vec<String>> {
|
|||||||
|
|
||||||
/// Detect the server type based on files in the server directory
|
/// Detect the server type based on files in the server directory
|
||||||
fn detect_server_type(server_path: &Path) -> ServerType {
|
fn detect_server_type(server_path: &Path) -> ServerType {
|
||||||
// Check for Paper
|
// --- Check for Paper --- (Check before Spigot/Bukkit as Paper includes their files)
|
||||||
if server_path.join("cache").join("patched_1.19.2.jar").exists() ||
|
// Primary indicator: config/paper-global.yml (or similar variants)
|
||||||
server_path.join("paper.yml").exists() {
|
if server_path.join("config").join("paper-global.yml").exists() ||
|
||||||
|
server_path.join("config").join("paper.yml").exists() { // Also check for paper.yml in config, just in case
|
||||||
return ServerType::Paper;
|
return ServerType::Paper;
|
||||||
}
|
}
|
||||||
|
// Secondary indicator: paper.yml in root (less common, but check)
|
||||||
|
if server_path.join("paper.yml").exists() {
|
||||||
|
return ServerType::Paper;
|
||||||
|
}
|
||||||
|
// Tertiary indicator: Look for a jar file starting with "paper-" in root
|
||||||
|
if let Ok(entries) = fs::read_dir(server_path) {
|
||||||
|
for entry in entries.filter_map(Result::ok) {
|
||||||
|
if let Some(filename) = entry.file_name().to_str() {
|
||||||
|
if filename.starts_with("paper-") && filename.ends_with(".jar") {
|
||||||
|
return ServerType::Paper;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- End Paper Check ---
|
||||||
|
|
||||||
// Check for Spigot
|
// Check for Spigot
|
||||||
if server_path.join("spigot.yml").exists() {
|
if server_path.join("spigot.yml").exists() {
|
||||||
@ -389,6 +406,36 @@ fn detect_minecraft_version(server_path: &Path, server_type: &ServerType) -> Opt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Try parsing paper-global.yml for Paper servers ---
|
||||||
|
if server_type == &ServerType::Paper {
|
||||||
|
let paper_global_path = server_path.join("config").join("paper-global.yml");
|
||||||
|
if paper_global_path.exists() {
|
||||||
|
if let Ok(content) = fs::read_to_string(&paper_global_path) {
|
||||||
|
// Use yaml_rust::YamlLoader here
|
||||||
|
match yaml_rust::YamlLoader::load_from_str(&content) {
|
||||||
|
Ok(docs) if !docs.is_empty() => {
|
||||||
|
let doc = &docs[0];
|
||||||
|
// Common location for MC version, might differ
|
||||||
|
if let Some(version) = doc["misc"]["paper-version"].as_str() {
|
||||||
|
// Often includes build number, try to extract base MC version
|
||||||
|
let mc_version = version.split('-').next().unwrap_or(version);
|
||||||
|
return Some(mc_version.to_string());
|
||||||
|
}
|
||||||
|
// Fallback check, some older versions might store it differently
|
||||||
|
if let Some(version) = doc["settings"]["minecraft-version"].as_str() {
|
||||||
|
return Some(version.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to parse paper-global.yml: {}", e);
|
||||||
|
}
|
||||||
|
_ => { /* Empty or invalid YAML */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- End Paper version check ---
|
||||||
|
|
||||||
// Try from the server jar name pattern
|
// Try from the server jar name pattern
|
||||||
if let Ok(entries) = fs::read_dir(server_path) {
|
if let Ok(entries) = fs::read_dir(server_path) {
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
@ -470,14 +517,48 @@ fn get_plugins_directory(server_path: &Path, server_type: &ServerType) -> String
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct ScanResult {
|
pub struct ScanResult {
|
||||||
server_info: ServerInfo,
|
server_info: ServerInfo,
|
||||||
plugins: Vec<Plugin>,
|
plugins: Vec<Plugin>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Payload for progress events
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct ScanProgress {
|
||||||
|
processed: usize,
|
||||||
|
total: usize,
|
||||||
|
current_file: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
fn scan_server_directory(path: &str) -> Result<ScanResult, String> {
|
async fn scan_server_directory(app_handle: AppHandle, path: String) -> Result<(), String> {
|
||||||
|
// Spawn the scanning logic into a background thread
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
// Bring Manager trait into scope for this block
|
||||||
|
let result = perform_scan(&app_handle, &path).await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(scan_result) => {
|
||||||
|
// Emit completion event
|
||||||
|
if let Err(e) = app_handle.emit("scan_complete", scan_result) {
|
||||||
|
eprintln!("Failed to emit scan_complete event: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// Emit error event
|
||||||
|
if let Err(emit_err) = app_handle.emit("scan_error", e.to_string()) {
|
||||||
|
eprintln!("Failed to emit scan_error event: {}", emit_err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(()) // Return immediately
|
||||||
|
}
|
||||||
|
|
||||||
|
// The actual scanning logic, moved to a separate async function
|
||||||
|
async fn perform_scan(app_handle: &AppHandle, path: &str) -> Result<ScanResult, String> {
|
||||||
let server_path = Path::new(path);
|
let server_path = Path::new(path);
|
||||||
|
|
||||||
if !server_path.exists() {
|
if !server_path.exists() {
|
||||||
@ -498,96 +579,87 @@ fn scan_server_directory(path: &str) -> Result<ScanResult, String> {
|
|||||||
let plugins_dir = Path::new(&plugins_dir_str);
|
let plugins_dir = Path::new(&plugins_dir_str);
|
||||||
|
|
||||||
if !plugins_dir.exists() {
|
if !plugins_dir.exists() {
|
||||||
return Err(format!("Plugins directory not found at: {}", plugins_dir.display()));
|
return Err(format!(
|
||||||
|
"Plugins directory not found at: {}",
|
||||||
|
plugins_dir.display()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan for JAR files in the plugins directory
|
// --- Progress Reporting Setup ---
|
||||||
let mut plugins = Vec::new();
|
let mut jar_files_to_process: Vec<std::path::PathBuf> = Vec::new();
|
||||||
|
|
||||||
match fs::read_dir(&plugins_dir) {
|
match fs::read_dir(&plugins_dir) {
|
||||||
Ok(entries) => {
|
Ok(entries) => {
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
if let Ok(entry) = entry {
|
if let Ok(entry) = entry {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
|
||||||
// Check if this is a JAR file
|
|
||||||
if path.is_file() && path.extension().map_or(false, |ext| ext.eq_ignore_ascii_case("jar")) {
|
if path.is_file() && path.extension().map_or(false, |ext| ext.eq_ignore_ascii_case("jar")) {
|
||||||
match extract_plugin_metadata(&path) {
|
jar_files_to_process.push(path);
|
||||||
Ok(meta) => {
|
|
||||||
// Create a Plugin from PluginMeta
|
|
||||||
let plugin = Plugin {
|
|
||||||
name: meta.name,
|
|
||||||
version: meta.version,
|
|
||||||
latest_version: None, // Will be filled by update checker
|
|
||||||
description: meta.description,
|
|
||||||
authors: meta.authors,
|
|
||||||
has_update: false, // Will be determined by update checker
|
|
||||||
api_version: meta.api_version,
|
|
||||||
main_class: meta.main_class,
|
|
||||||
depend: meta.depend,
|
|
||||||
soft_depend: meta.soft_depend,
|
|
||||||
load_before: meta.load_before,
|
|
||||||
commands: meta.commands,
|
|
||||||
permissions: meta.permissions,
|
|
||||||
file_path: meta.file_path,
|
|
||||||
file_hash: meta.file_hash,
|
|
||||||
};
|
|
||||||
|
|
||||||
plugins.push(plugin);
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
// Log error but continue with other plugins
|
|
||||||
println!("Error reading plugin from {}: {}", path.display(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Err(format!("Failed to read plugins directory: {}", e));
|
return Err(format!("Failed to read plugins directory initially: {}", e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let total_plugins = jar_files_to_process.len();
|
||||||
|
let mut processed_plugins = 0;
|
||||||
|
// --- End Progress Reporting Setup ---
|
||||||
|
|
||||||
// If no plugins were found, fall back to mock data for testing
|
let mut plugins = Vec::new();
|
||||||
if plugins.is_empty() && server_type == ServerType::Unknown {
|
|
||||||
// For testing only - in production, we'd just return an empty list
|
for path in jar_files_to_process {
|
||||||
plugins = vec![
|
processed_plugins += 1;
|
||||||
Plugin {
|
let current_file = path.file_name().unwrap_or_default().to_string_lossy().to_string();
|
||||||
name: "EssentialsX".to_string(),
|
|
||||||
version: "2.19.0".to_string(),
|
// Emit progress
|
||||||
latest_version: Some("2.20.0".to_string()),
|
let progress = ScanProgress {
|
||||||
description: Some("Essential server tools for Minecraft".to_string()),
|
processed: processed_plugins,
|
||||||
authors: vec!["md_5".to_string(), "SupaHam".to_string()],
|
total: total_plugins,
|
||||||
has_update: true,
|
current_file: current_file.clone(),
|
||||||
api_version: Some("1.13".to_string()),
|
};
|
||||||
main_class: Some("com.earth2me.essentials.Essentials".to_string()),
|
if let Err(e) = app_handle.emit("scan_progress", progress) {
|
||||||
depend: None,
|
eprintln!("Failed to emit scan_progress event: {}", e);
|
||||||
soft_depend: None,
|
// Continue processing even if event emission fails
|
||||||
load_before: None,
|
}
|
||||||
commands: None,
|
|
||||||
permissions: None,
|
// Use spawn_blocking for CPU/IO-bound tasks within the async fn
|
||||||
file_path: "EssentialsX.jar".to_string(),
|
let meta_result = tauri::async_runtime::spawn_blocking(move || {
|
||||||
file_hash: calculate_file_hash("EssentialsX.jar").unwrap_or_else(|_| "unknown".to_string()),
|
extract_plugin_metadata(&path)
|
||||||
},
|
}).await;
|
||||||
Plugin {
|
|
||||||
name: "WorldEdit".to_string(),
|
match meta_result {
|
||||||
version: "7.2.8".to_string(),
|
Ok(Ok(meta)) => {
|
||||||
latest_version: Some("7.2.8".to_string()),
|
// Create a Plugin from PluginMeta
|
||||||
description: Some("In-game map editor".to_string()),
|
let plugin = Plugin {
|
||||||
authors: vec!["sk89q".to_string(), "wizjany".to_string()],
|
name: meta.name,
|
||||||
has_update: false,
|
version: meta.version,
|
||||||
api_version: Some("1.13".to_string()),
|
latest_version: None, // Will be filled by update checker
|
||||||
main_class: Some("com.sk89q.worldedit.bukkit.WorldEditPlugin".to_string()),
|
description: meta.description,
|
||||||
depend: None,
|
authors: meta.authors,
|
||||||
soft_depend: None,
|
has_update: false, // Will be determined by update checker
|
||||||
load_before: None,
|
api_version: meta.api_version,
|
||||||
commands: None,
|
main_class: meta.main_class,
|
||||||
permissions: None,
|
depend: meta.depend,
|
||||||
file_path: "WorldEdit.jar".to_string(),
|
soft_depend: meta.soft_depend,
|
||||||
file_hash: calculate_file_hash("WorldEdit.jar").unwrap_or_else(|_| "unknown".to_string()),
|
load_before: meta.load_before,
|
||||||
},
|
commands: meta.commands,
|
||||||
];
|
permissions: meta.permissions,
|
||||||
|
file_path: meta.file_path,
|
||||||
|
file_hash: meta.file_hash,
|
||||||
|
};
|
||||||
|
plugins.push(plugin);
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
// Log error but continue with other plugins
|
||||||
|
println!("Error reading plugin from {}: {}", current_file, e);
|
||||||
|
// Optionally emit a specific plugin error event here
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// This happens if the blocking task itself panics
|
||||||
|
println!("Task panicked for plugin {}: {}", current_file, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create server info
|
// Create server info
|
||||||
@ -595,7 +667,7 @@ fn scan_server_directory(path: &str) -> Result<ScanResult, String> {
|
|||||||
server_type,
|
server_type,
|
||||||
minecraft_version,
|
minecraft_version,
|
||||||
plugins_directory: plugins_dir_str,
|
plugins_directory: plugins_dir_str,
|
||||||
plugins_count: plugins.len(),
|
plugins_count: plugins.len(), // Use the count of successfully processed plugins
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(ScanResult {
|
Ok(ScanResult {
|
||||||
@ -696,9 +768,8 @@ fn get_crawler(repository: &RepositorySource) -> Option<Box<dyn RepositoryCrawle
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command to search for plugins in specified repositories
|
// Regular repository functions (not commands)
|
||||||
#[command]
|
pub fn lib_search_plugins_in_repositories(query: &str, repositories: Vec<RepositorySource>) -> Result<Vec<RepositoryPlugin>, String> {
|
||||||
pub fn search_repository_plugins(query: &str, repositories: Vec<RepositorySource>) -> Result<Vec<RepositoryPlugin>, String> {
|
|
||||||
let mut results: Vec<RepositoryPlugin> = Vec::new();
|
let mut results: Vec<RepositoryPlugin> = Vec::new();
|
||||||
|
|
||||||
// Try each requested repository
|
// Try each requested repository
|
||||||
@ -725,9 +796,7 @@ pub fn search_repository_plugins(query: &str, repositories: Vec<RepositorySource
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command to get plugin details from a specific repository
|
pub fn lib_get_plugin_details_from_repository(plugin_id: &str, repository: RepositorySource) -> Result<RepositoryPlugin, String> {
|
||||||
#[command]
|
|
||||||
pub fn get_repository_plugin_details(plugin_id: &str, repository: RepositorySource) -> Result<RepositoryPlugin, String> {
|
|
||||||
if let Some(crawler) = get_crawler(&repository) {
|
if let Some(crawler) = get_crawler(&repository) {
|
||||||
crawler.get_plugin_details(plugin_id).map_err(|e| e.to_string())
|
crawler.get_plugin_details(plugin_id).map_err(|e| e.to_string())
|
||||||
} else {
|
} else {
|
||||||
@ -735,9 +804,7 @@ pub fn get_repository_plugin_details(plugin_id: &str, repository: RepositorySour
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command to download a plugin from a repository
|
pub fn lib_download_plugin_from_repository(plugin_id: &str, version: &str, repository: RepositorySource, destination: &str) -> Result<String, String> {
|
||||||
#[command]
|
|
||||||
pub fn download_repository_plugin(plugin_id: &str, version: &str, repository: RepositorySource, destination: &str) -> Result<String, String> {
|
|
||||||
if let Some(crawler) = get_crawler(&repository) {
|
if let Some(crawler) = get_crawler(&repository) {
|
||||||
crawler
|
crawler
|
||||||
.download_plugin(plugin_id, version, Path::new(destination))
|
.download_plugin(plugin_id, version, Path::new(destination))
|
||||||
@ -747,6 +814,138 @@ pub fn download_repository_plugin(plugin_id: &str, version: &str, repository: Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A very simple proxy command
|
||||||
|
#[command]
|
||||||
|
fn plugin_proxy(action: &str, json_args: &str) -> Result<String, String> {
|
||||||
|
match action {
|
||||||
|
"search" => {
|
||||||
|
// Parse the JSON arguments
|
||||||
|
let args: serde_json::Value = serde_json::from_str(json_args)
|
||||||
|
.map_err(|e| format!("Invalid JSON: {}", e))?;
|
||||||
|
|
||||||
|
let query = args["query"].as_str()
|
||||||
|
.ok_or_else(|| "Missing query parameter".to_string())?;
|
||||||
|
|
||||||
|
// Convert repositories to RepositorySource
|
||||||
|
let repo_array = args["repositories"].as_array()
|
||||||
|
.ok_or_else(|| "Missing repositories parameter".to_string())?;
|
||||||
|
|
||||||
|
let mut repositories = Vec::new();
|
||||||
|
for repo in repo_array {
|
||||||
|
let repo_str = repo.as_str()
|
||||||
|
.ok_or_else(|| "Repository must be a string".to_string())?;
|
||||||
|
|
||||||
|
let repo_source = match repo_str {
|
||||||
|
"HangarMC" => RepositorySource::HangarMC,
|
||||||
|
"SpigotMC" => RepositorySource::SpigotMC,
|
||||||
|
"Modrinth" => RepositorySource::Modrinth,
|
||||||
|
"GitHub" => RepositorySource::GitHub,
|
||||||
|
"BukkitDev" => RepositorySource::BukkitDev,
|
||||||
|
_ => {
|
||||||
|
// Handle custom repositories
|
||||||
|
if repo_str.starts_with("Custom:") {
|
||||||
|
let url = repo_str.trim_start_matches("Custom:");
|
||||||
|
RepositorySource::Custom(url.to_string())
|
||||||
|
} else {
|
||||||
|
return Err(format!("Unknown repository: {}", repo_str));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
repositories.push(repo_source);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the implementation
|
||||||
|
match lib_search_plugins_in_repositories(query, repositories) {
|
||||||
|
Ok(results) => {
|
||||||
|
// Convert results to JSON
|
||||||
|
serde_json::to_string(&results)
|
||||||
|
.map_err(|e| format!("Failed to serialize results: {}", e))
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"details" => {
|
||||||
|
// Parse the JSON arguments
|
||||||
|
let args: serde_json::Value = serde_json::from_str(json_args)
|
||||||
|
.map_err(|e| format!("Invalid JSON: {}", e))?;
|
||||||
|
|
||||||
|
let plugin_id = args["plugin_id"].as_str()
|
||||||
|
.ok_or_else(|| "Missing plugin_id parameter".to_string())?;
|
||||||
|
|
||||||
|
let repo_str = args["repository"].as_str()
|
||||||
|
.ok_or_else(|| "Missing repository parameter".to_string())?;
|
||||||
|
|
||||||
|
// Convert repository to RepositorySource
|
||||||
|
let repository = match repo_str {
|
||||||
|
"HangarMC" => RepositorySource::HangarMC,
|
||||||
|
"SpigotMC" => RepositorySource::SpigotMC,
|
||||||
|
"Modrinth" => RepositorySource::Modrinth,
|
||||||
|
"GitHub" => RepositorySource::GitHub,
|
||||||
|
"BukkitDev" => RepositorySource::BukkitDev,
|
||||||
|
_ => {
|
||||||
|
// Handle custom repositories
|
||||||
|
if repo_str.starts_with("Custom:") {
|
||||||
|
let url = repo_str.trim_start_matches("Custom:");
|
||||||
|
RepositorySource::Custom(url.to_string())
|
||||||
|
} else {
|
||||||
|
return Err(format!("Unknown repository: {}", repo_str));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call the implementation
|
||||||
|
match lib_get_plugin_details_from_repository(plugin_id, repository) {
|
||||||
|
Ok(details) => {
|
||||||
|
// Convert details to JSON
|
||||||
|
serde_json::to_string(&details)
|
||||||
|
.map_err(|e| format!("Failed to serialize details: {}", e))
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"download" => {
|
||||||
|
// Parse the JSON arguments
|
||||||
|
let args: serde_json::Value = serde_json::from_str(json_args)
|
||||||
|
.map_err(|e| format!("Invalid JSON: {}", e))?;
|
||||||
|
|
||||||
|
let plugin_id = args["plugin_id"].as_str()
|
||||||
|
.ok_or_else(|| "Missing plugin_id parameter".to_string())?;
|
||||||
|
|
||||||
|
let version = args["version"].as_str()
|
||||||
|
.ok_or_else(|| "Missing version parameter".to_string())?;
|
||||||
|
|
||||||
|
let destination = args["destination"].as_str()
|
||||||
|
.ok_or_else(|| "Missing destination parameter".to_string())?;
|
||||||
|
|
||||||
|
let repo_str = args["repository"].as_str()
|
||||||
|
.ok_or_else(|| "Missing repository parameter".to_string())?;
|
||||||
|
|
||||||
|
// Convert repository to RepositorySource
|
||||||
|
let repository = match repo_str {
|
||||||
|
"HangarMC" => RepositorySource::HangarMC,
|
||||||
|
"SpigotMC" => RepositorySource::SpigotMC,
|
||||||
|
"Modrinth" => RepositorySource::Modrinth,
|
||||||
|
"GitHub" => RepositorySource::GitHub,
|
||||||
|
"BukkitDev" => RepositorySource::BukkitDev,
|
||||||
|
_ => {
|
||||||
|
// Handle custom repositories
|
||||||
|
if repo_str.starts_with("Custom:") {
|
||||||
|
let url = repo_str.trim_start_matches("Custom:");
|
||||||
|
RepositorySource::Custom(url.to_string())
|
||||||
|
} else {
|
||||||
|
return Err(format!("Unknown repository: {}", repo_str));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call the implementation
|
||||||
|
lib_download_plugin_from_repository(plugin_id, version, repository, destination)
|
||||||
|
},
|
||||||
|
_ => Err(format!("Unknown action: {}", action)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
@ -754,10 +953,9 @@ pub fn run() {
|
|||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
greet,
|
greet,
|
||||||
scan_server_directory,
|
scan_server_directory,
|
||||||
search_repository_plugins,
|
plugin_proxy
|
||||||
get_repository_plugin_details,
|
|
||||||
download_repository_plugin
|
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
22
src/App.css
22
src/App.css
@ -134,35 +134,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.plugins-list {
|
.plugins-list {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugins-list h2 {
|
.plugins-list h2 {
|
||||||
grid-column: 1 / -1;
|
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-card {
|
|
||||||
background-color: var(--surface-color);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-name {
|
.plugin-name {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
73
src/App.tsx
73
src/App.tsx
@ -1,6 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { open } from "@tauri-apps/plugin-dialog";
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
|
import { listen, UnlistenFn } from "@tauri-apps/api/event";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
type ServerType =
|
type ServerType =
|
||||||
@ -45,6 +46,12 @@ interface ScanResult {
|
|||||||
plugins: Plugin[];
|
plugins: Plugin[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ScanProgress {
|
||||||
|
processed: number;
|
||||||
|
total: number;
|
||||||
|
current_file: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface PluginDetailsProps {
|
interface PluginDetailsProps {
|
||||||
plugin: Plugin;
|
plugin: Plugin;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@ -160,6 +167,45 @@ function App() {
|
|||||||
const [scanComplete, setScanComplete] = useState(false);
|
const [scanComplete, setScanComplete] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [selectedPlugin, setSelectedPlugin] = useState<Plugin | null>(null);
|
const [selectedPlugin, setSelectedPlugin] = useState<Plugin | null>(null);
|
||||||
|
const [scanProgress, setScanProgress] = useState<ScanProgress | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let unlistenProgress: UnlistenFn | undefined;
|
||||||
|
let unlistenComplete: UnlistenFn | undefined;
|
||||||
|
let unlistenError: UnlistenFn | undefined;
|
||||||
|
|
||||||
|
const setupListeners = async () => {
|
||||||
|
unlistenProgress = await listen<ScanProgress>("scan_progress", (event) => {
|
||||||
|
setScanProgress(event.payload);
|
||||||
|
setError(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
unlistenComplete = await listen<ScanResult>("scan_complete", (event) => {
|
||||||
|
setServerInfo(event.payload.server_info);
|
||||||
|
setPlugins(event.payload.plugins);
|
||||||
|
setIsScanning(false);
|
||||||
|
setScanComplete(true);
|
||||||
|
setScanProgress(null);
|
||||||
|
setError(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
unlistenError = await listen<string>("scan_error", (event) => {
|
||||||
|
console.error("Scan Error:", event.payload);
|
||||||
|
setError(event.payload);
|
||||||
|
setIsScanning(false);
|
||||||
|
setScanProgress(null);
|
||||||
|
setScanComplete(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
setupListeners();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unlistenProgress?.();
|
||||||
|
unlistenComplete?.();
|
||||||
|
unlistenError?.();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
async function selectDirectory() {
|
async function selectDirectory() {
|
||||||
try {
|
try {
|
||||||
@ -188,20 +234,21 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function scanForPlugins() {
|
async function scanForPlugins() {
|
||||||
|
if (!serverPath || isScanning) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsScanning(true);
|
setIsScanning(true);
|
||||||
|
setScanComplete(false);
|
||||||
|
setPlugins([]);
|
||||||
|
setServerInfo(null);
|
||||||
|
setScanProgress(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// Call the Rust backend
|
await invoke("scan_server_directory", { path: serverPath });
|
||||||
const result = await invoke<ScanResult>("scan_server_directory", { path: serverPath });
|
|
||||||
|
|
||||||
setServerInfo(result.server_info);
|
|
||||||
setPlugins(result.plugins);
|
|
||||||
setIsScanning(false);
|
|
||||||
setScanComplete(true);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error scanning for plugins:", err);
|
console.error("Error invoking scan command:", err);
|
||||||
setError(err as string);
|
setError(`Failed to start scan: ${err as string}`);
|
||||||
setIsScanning(false);
|
setIsScanning(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -241,6 +288,14 @@ function App() {
|
|||||||
{isScanning ? "Scanning..." : "Scan for Plugins"}
|
{isScanning ? "Scanning..." : "Scan for Plugins"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{isScanning && scanProgress && (
|
||||||
|
<div className="scan-progress">
|
||||||
|
<p>Scanning: {scanProgress.current_file}</p>
|
||||||
|
<progress value={scanProgress.processed} max={scanProgress.total}></progress>
|
||||||
|
<span>{scanProgress.processed} / {scanProgress.total}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="error-message">
|
<div className="error-message">
|
||||||
{error}
|
{error}
|
||||||
|
Loading…
Reference in New Issue
Block a user