diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 7af01f3..b850fe0 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -319,11 +319,24 @@ pub fn get_app_config_internal() -> Result { } #[command] -pub async fn update_app_config(config: AppConfig) -> Result<(), String> { +pub async fn update_app_config( + config: AppConfig, + app_handle: tauri::AppHandle, +) -> Result<(), String> { + use tauri::Emitter; + let mut guard = get_config_manager()?; if let Some(config_manager) = guard.as_mut() { config_manager.config = config; - config_manager.save_config() + let result = config_manager.save_config(); + + // 保存成功后发送配置更新事件 + if result.is_ok() { + app_handle.emit("config-updated", ()).ok(); + info!("配置已更新,已发送 config-updated 事件"); + } + + result } else { Err("配置管理器未初始化".to_string()) } diff --git a/src-tauri/src/env_commands.rs b/src-tauri/src/env_commands.rs index f7e12ea..4dc0220 100644 --- a/src-tauri/src/env_commands.rs +++ b/src-tauri/src/env_commands.rs @@ -46,15 +46,9 @@ pub async fn switch_environment_version( ) -> Result<(), String> { info!("切换 {} 到版本 {}", language, version); let manager = env_manager.lock().await; - let result = manager.switch_version(&language, &version).await; - - if result.is_ok() { - // 发送配置更新事件通知前端刷新配置 - app_handle.emit("config-updated", ()).ok(); - info!("已发送配置更新事件"); - } - - result + manager + .switch_version(&language, &version, app_handle) + .await } #[tauri::command] @@ -64,3 +58,14 @@ pub async fn get_supported_environment_languages( let manager = env_manager.lock().await; Ok(manager.get_supported_languages()) } + +#[tauri::command] +pub async fn uninstall_environment_version( + language: String, + version: String, + env_manager: State<'_, EnvironmentManagerState>, +) -> Result<(), String> { + info!("卸载 {} 版本 {}", language, version); + let manager = env_manager.lock().await; + manager.uninstall_version(&language, &version).await +} diff --git a/src-tauri/src/env_manager.rs b/src-tauri/src/env_manager.rs index dcf8ff6..6b76d20 100644 --- a/src-tauri/src/env_manager.rs +++ b/src-tauri/src/env_manager.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use log::{error, info}; +use log::{error, info, warn}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::PathBuf; @@ -24,6 +24,7 @@ pub struct EnvironmentInfo { pub current_version: Option, pub installed_versions: Vec, pub available_versions: Vec, + pub error: Option, // 错误信息(如获取可用版本失败) } // 下载进度事件 @@ -67,7 +68,7 @@ pub trait EnvironmentProvider: Send + Sync { ) -> Result; // 切换到指定版本 - async fn switch_version(&self, version: &str) -> Result<(), String>; + async fn switch_version(&self, version: &str, app_handle: AppHandle) -> Result<(), String>; // 获取当前激活的版本 async fn get_current_version(&self) -> Result, String>; @@ -75,6 +76,9 @@ pub trait EnvironmentProvider: Send + Sync { // 获取安装目录 #[allow(dead_code)] fn get_install_dir(&self) -> PathBuf; + + // 卸载指定版本 + async fn uninstall_version(&self, version: &str) -> Result<(), String>; } // 环境管理器 @@ -105,16 +109,22 @@ impl EnvironmentManager { let current_version = provider.get_current_version().await.ok().flatten(); let installed_versions = provider.get_installed_versions().await.unwrap_or_default(); - let available_versions = provider - .fetch_available_versions() - .await - .unwrap_or_default(); + + // 获取可用版本,如果失败也要返回已安装版本,并在错误信息中说明 + let (available_versions, error) = match provider.fetch_available_versions().await { + Ok(versions) => (versions, None), + Err(e) => { + warn!("获取可用版本失败: {}", e); + (vec![], Some(format!("获取可用版本失败: {}", e))) + } + }; Ok(EnvironmentInfo { language: language.to_string(), current_version, installed_versions, available_versions, + error, }) } @@ -133,19 +143,34 @@ impl EnvironmentManager { provider.download_and_install(version, app_handle).await } - pub async fn switch_version(&self, language: &str, version: &str) -> Result<(), String> { + pub async fn switch_version( + &self, + language: &str, + version: &str, + app_handle: AppHandle, + ) -> Result<(), String> { let provider = self .providers .get(language) .ok_or_else(|| format!("暂未支持 {} 语言,请前往 github 提供 issues", language))?; info!("切换 {} 到版本 {}", language, version); - provider.switch_version(version).await + provider.switch_version(version, app_handle).await } pub fn get_supported_languages(&self) -> Vec { self.providers.keys().cloned().collect() } + + pub async fn uninstall_version(&self, language: &str, version: &str) -> Result<(), String> { + let provider = self + .providers + .get(language) + .ok_or_else(|| format!("暂未支持 {} 语言,请前往 github 提供 issues", language))?; + + info!("卸载 {} 版本 {}", language, version); + provider.uninstall_version(version).await + } } // 辅助函数:发送下载进度事件 diff --git a/src-tauri/src/env_providers/clojure.rs b/src-tauri/src/env_providers/clojure.rs index 872cda9..176f91d 100644 --- a/src-tauri/src/env_providers/clojure.rs +++ b/src-tauri/src/env_providers/clojure.rs @@ -1,3 +1,4 @@ +use super::metadata::{Metadata, fetch_metadata_from_cdn, is_cdn_enabled, is_fallback_enabled}; use crate::env_manager::{ DownloadStatus, EnvironmentProvider, EnvironmentVersion, emit_download_progress, }; @@ -22,25 +23,6 @@ struct GithubAsset { size: u64, } -// CDN Metadata 结构 -#[derive(Debug, Deserialize, Serialize, Clone)] -struct MetadataRelease { - version: String, // 版本号,如 "1.11.1.1262" - display_name: String, // 显示名称,如 "Clojure 1.11.1.1262" - published_at: String, // 发布时间 - download_url: String, // CDN 下载地址 - github_url: String, // GitHub 官方下载地址(作为备用) - file_name: String, // 文件名,如 "clojure-tools-1.11.1.1262.tar.gz" - size: u64, // 文件大小(字节) - supported_platforms: Vec, // 支持的平台,如 ["macos", "linux", "windows"] -} - -#[derive(Debug, Deserialize, Serialize)] -struct Metadata { - language: String, // 语言名称 "clojure" - releases: Vec, -} - #[derive(Debug, Deserialize, Serialize)] struct CachedReleases { releases: Vec, @@ -185,56 +167,6 @@ impl ClojureEnvironmentProvider { Ok(versions) } - // 从 CDN 获取 metadata.json - async fn fetch_metadata_from_cdn(&self) -> Result { - use crate::config::get_app_config_internal; - - let config = get_app_config_internal().map_err(|e| format!("读取配置失败: {}", e))?; - - let cdn_enabled = config - .environment_mirror - .as_ref() - .and_then(|m| m.enabled) - .unwrap_or(false); - - if !cdn_enabled { - return Err("CDN 未启用".to_string()); - } - - let base_url = config - .environment_mirror - .as_ref() - .and_then(|m| m.base_url.as_ref()) - .ok_or("CDN 地址未配置")?; - - let metadata_url = format!("{}/clojure/metadata.json", base_url); - info!("从 CDN 获取 Clojure metadata: {}", metadata_url); - - let client = reqwest::Client::builder() - .user_agent("CodeForge") - .timeout(Duration::from_secs(30)) - .build() - .map_err(|e| format!("创建 HTTP 客户端失败: {}", e))?; - - let response = client - .get(&metadata_url) - .send() - .await - .map_err(|e| format!("请求 CDN metadata 失败: {}", e))?; - - if !response.status().is_success() { - return Err(format!("CDN 返回错误状态码: {}", response.status())); - } - - let metadata: Metadata = response - .json() - .await - .map_err(|e| format!("解析 metadata.json 失败: {}", e))?; - - info!("成功从 CDN 获取 {} 个版本", metadata.releases.len()); - Ok(metadata) - } - async fn fetch_github_releases(&self) -> Result, String> { if let Some(cached_releases) = self.read_cache() { return Ok(cached_releases); @@ -594,7 +526,12 @@ impl ClojureEnvironmentProvider { Ok(()) } - async fn update_plugin_config(&self, version: &str, install_path: &str) -> Result<(), String> { + async fn update_plugin_config( + &self, + version: &str, + install_path: &str, + app_handle: &AppHandle, + ) -> Result<(), String> { use crate::config::{get_app_config_internal, update_app_config}; info!( @@ -616,7 +553,7 @@ impl ClojureEnvironmentProvider { } } - update_app_config(config) + update_app_config(config, app_handle.clone()) .await .map_err(|e| format!("保存配置失败: {}", e))?; @@ -631,30 +568,26 @@ impl EnvironmentProvider for ClojureEnvironmentProvider { } async fn fetch_available_versions(&self) -> Result, String> { - use crate::config::get_app_config_internal; - - // 优先尝试从 CDN 获取 metadata.json - match self.fetch_metadata_from_cdn().await { - Ok(metadata) => { - info!("使用 CDN metadata 获取版本列表"); - return self.parse_metadata_to_versions(metadata); - } - Err(e) => { - warn!("CDN metadata 获取失败: {}", e); + // 检查 CDN 是否启用 + if is_cdn_enabled() { + match fetch_metadata_from_cdn("clojure").await { + Ok(metadata) => { + info!("使用 CDN metadata 获取版本列表"); + return self.parse_metadata_to_versions(metadata); + } + Err(e) => { + warn!("CDN metadata 获取失败: {}", e); - // 检查是否启用 fallback - let fallback_enabled = get_app_config_internal() - .ok() - .and_then(|config| config.environment_mirror) - .and_then(|mirror| mirror.fallback_enabled) - .unwrap_or(false); + // 检查是否启用 fallback + if !is_fallback_enabled() { + return Err(format!("CDN metadata 获取失败,未启用自动回退: {}", e)); + } - if !fallback_enabled { - return Err(format!("CDN metadata 获取失败,未启用自动回退: {}", e)); + info!("fallback 已启用,回退到 GitHub API"); } - - info!("fallback 已启用,回退到 GitHub API"); } + } else { + info!("CDN 未启用,使用 GitHub API"); } let releases = self.fetch_github_releases().await?; @@ -794,7 +727,7 @@ impl EnvironmentProvider for ClojureEnvironmentProvider { // 清理临时解压目录 std::fs::remove_dir_all(&temp_extract_dir).ok(); - self.update_plugin_config(version, &install_path.to_string_lossy()) + self.update_plugin_config(version, &install_path.to_string_lossy(), &app_handle) .await?; emit_download_progress( @@ -810,7 +743,7 @@ impl EnvironmentProvider for ClojureEnvironmentProvider { Ok(install_path.to_string_lossy().to_string()) } - async fn switch_version(&self, version: &str) -> Result<(), String> { + async fn switch_version(&self, version: &str, app_handle: AppHandle) -> Result<(), String> { info!("切换 Clojure 版本到 {}", version); if !self.is_version_installed(version) { @@ -819,7 +752,7 @@ impl EnvironmentProvider for ClojureEnvironmentProvider { let install_path = self.get_version_install_path(version); - self.update_plugin_config(version, &install_path.to_string_lossy()) + self.update_plugin_config(version, &install_path.to_string_lossy(), &app_handle) .await?; info!("成功切换到 Clojure {}", version); @@ -854,4 +787,22 @@ impl EnvironmentProvider for ClojureEnvironmentProvider { fn get_install_dir(&self) -> PathBuf { self.install_dir.clone() } + + async fn uninstall_version(&self, version: &str) -> Result<(), String> { + let version_dir = self.install_dir.join(version); + + if !version_dir.exists() { + return Err(format!("版本 {} 未安装", version)); + } + + let current_version = self.get_current_version().await.ok().flatten(); + if current_version.as_deref() == Some(version) { + return Err(format!("无法卸载当前正在使用的版本 {}", version)); + } + + std::fs::remove_dir_all(&version_dir).map_err(|e| format!("删除版本目录失败: {}", e))?; + + info!("已卸载 Clojure 版本 {}", version); + Ok(()) + } } diff --git a/src-tauri/src/env_providers/metadata.rs b/src-tauri/src/env_providers/metadata.rs new file mode 100644 index 0000000..0663284 --- /dev/null +++ b/src-tauri/src/env_providers/metadata.rs @@ -0,0 +1,94 @@ +use log::info; +use serde::{Deserialize, Serialize}; + +// 通用的 CDN Metadata 结构 +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct MetadataRelease { + pub version: String, + pub display_name: String, + pub published_at: String, + pub download_url: String, + pub github_url: String, + pub file_name: String, + pub size: u64, + #[serde(default)] + pub supported_platforms: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Metadata { + #[serde(default)] + pub language: String, + pub releases: Vec, +} + +// 通用的 CDN 配置检查函数 +pub fn is_cdn_enabled() -> bool { + use crate::config::get_app_config_internal; + + get_app_config_internal() + .ok() + .and_then(|config| config.environment_mirror) + .and_then(|mirror| mirror.enabled) + .unwrap_or(false) +} + +// 通用的 fallback 配置检查函数 +pub fn is_fallback_enabled() -> bool { + use crate::config::get_app_config_internal; + + get_app_config_internal() + .ok() + .and_then(|config| config.environment_mirror) + .and_then(|mirror| mirror.fallback_enabled) + .unwrap_or(false) +} + +// 通用的从 CDN 获取 metadata 函数 +pub async fn fetch_metadata_from_cdn(language: &str) -> Result { + use crate::config::get_app_config_internal; + + let config = get_app_config_internal().map_err(|e| format!("读取配置失败: {}", e))?; + + let cdn_enabled = config + .environment_mirror + .as_ref() + .and_then(|m| m.enabled) + .unwrap_or(false); + + if !cdn_enabled { + return Err("CDN 未启用".to_string()); + } + + let base_url = config + .environment_mirror + .as_ref() + .and_then(|m| m.base_url.as_ref()) + .ok_or("CDN 地址未配置")?; + + let metadata_url = format!("{}/{}/metadata.json", base_url, language); + info!("从 CDN 获取 {} metadata: {}", language, metadata_url); + + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + .map_err(|e| format!("创建 HTTP 客户端失败: {}", e))?; + + let response = client + .get(&metadata_url) + .send() + .await + .map_err(|e| format!("请求 CDN metadata 失败: {}", e))?; + + if !response.status().is_success() { + return Err(format!("CDN 返回错误状态码: {}", response.status())); + } + + let metadata: Metadata = response + .json() + .await + .map_err(|e| format!("解析 metadata.json 失败: {}", e))?; + + info!("成功从 CDN 获取 {} 个版本", metadata.releases.len()); + Ok(metadata) +} diff --git a/src-tauri/src/env_providers/mod.rs b/src-tauri/src/env_providers/mod.rs index 3904cbf..78c2f03 100644 --- a/src-tauri/src/env_providers/mod.rs +++ b/src-tauri/src/env_providers/mod.rs @@ -1,4 +1,5 @@ pub mod clojure; +pub mod metadata; pub mod scala; pub use clojure::ClojureEnvironmentProvider; diff --git a/src-tauri/src/env_providers/scala.rs b/src-tauri/src/env_providers/scala.rs index 555086f..d975617 100644 --- a/src-tauri/src/env_providers/scala.rs +++ b/src-tauri/src/env_providers/scala.rs @@ -1,3 +1,4 @@ +use super::metadata::{Metadata, fetch_metadata_from_cdn, is_cdn_enabled, is_fallback_enabled}; use crate::env_manager::{ DownloadStatus, EnvironmentProvider, EnvironmentVersion, download_with_fallback, emit_download_progress, @@ -250,6 +251,51 @@ impl ScalaEnvironmentProvider { false } + // 将 CDN metadata 转换为 EnvironmentVersion 列表 + fn parse_metadata_to_versions( + &self, + metadata: Metadata, + ) -> Result, String> { + let mut versions = Vec::new(); + + for release in metadata.releases { + let version = release.version.clone(); + let is_installed = self.is_version_installed(&version); + + // 如果已安装,查找实际的包含 bin 目录的路径 + let install_path = if is_installed { + let version_dir = self.get_version_install_path(&version); + let mut actual_path = version_dir.clone(); + + if let Ok(entries) = std::fs::read_dir(&version_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() && path.join("bin").exists() { + actual_path = path; + break; + } + } + } + + Some(actual_path.to_string_lossy().to_string()) + } else { + None + }; + + versions.push(EnvironmentVersion { + version: version.clone(), + download_url: release.download_url.clone(), + fallback_url: Some(release.github_url.clone()), + install_path, + is_installed, + size: Some(release.size), + release_date: Some(release.published_at.clone()), + }); + } + + Ok(versions) + } + // 下载文件并显示进度 async fn download_file( &self, @@ -414,7 +460,12 @@ impl ScalaEnvironmentProvider { } // 更新配置以使用新版本 - async fn update_plugin_config(&self, version: &str, install_path: &str) -> Result<(), String> { + async fn update_plugin_config( + &self, + version: &str, + install_path: &str, + app_handle: &AppHandle, + ) -> Result<(), String> { use crate::config::{get_app_config_internal, update_app_config}; info!( @@ -444,7 +495,7 @@ impl ScalaEnvironmentProvider { } } - update_app_config(config) + update_app_config(config, app_handle.clone()) .await .map_err(|e| format!("保存配置失败: {}", e))?; @@ -459,6 +510,28 @@ impl EnvironmentProvider for ScalaEnvironmentProvider { } async fn fetch_available_versions(&self) -> Result, String> { + // 检查 CDN 是否启用 + if is_cdn_enabled() { + match fetch_metadata_from_cdn("scala").await { + Ok(metadata) => { + info!("使用 CDN metadata 获取版本列表"); + return self.parse_metadata_to_versions(metadata); + } + Err(e) => { + warn!("CDN metadata 获取失败: {}", e); + + // 检查是否启用 fallback + if !is_fallback_enabled() { + return Err(format!("CDN metadata 获取失败,未启用自动回退: {}", e)); + } + + info!("fallback 已启用,回退到 GitHub API"); + } + } + } else { + info!("CDN 未启用,使用 GitHub API"); + } + let releases = self.fetch_github_releases().await?; let pattern = Self::get_download_pattern(); @@ -622,7 +695,7 @@ impl EnvironmentProvider for ScalaEnvironmentProvider { } // 更新插件配置 - self.update_plugin_config(version, &actual_install_path.to_string_lossy()) + self.update_plugin_config(version, &actual_install_path.to_string_lossy(), &app_handle) .await?; emit_download_progress( @@ -638,7 +711,7 @@ impl EnvironmentProvider for ScalaEnvironmentProvider { Ok(actual_install_path.to_string_lossy().to_string()) } - async fn switch_version(&self, version: &str) -> Result<(), String> { + async fn switch_version(&self, version: &str, app_handle: AppHandle) -> Result<(), String> { info!("切换 Scala 版本到 {}", version); if !self.is_version_installed(version) { @@ -659,7 +732,7 @@ impl EnvironmentProvider for ScalaEnvironmentProvider { } } - self.update_plugin_config(version, &actual_install_path.to_string_lossy()) + self.update_plugin_config(version, &actual_install_path.to_string_lossy(), &app_handle) .await?; info!("成功切换到 Scala {}", version); @@ -699,4 +772,22 @@ impl EnvironmentProvider for ScalaEnvironmentProvider { fn get_install_dir(&self) -> PathBuf { self.install_dir.clone() } + + async fn uninstall_version(&self, version: &str) -> Result<(), String> { + let version_dir = self.install_dir.join(version); + + if !version_dir.exists() { + return Err(format!("版本 {} 未安装", version)); + } + + let current_version = self.get_current_version().await.ok().flatten(); + if current_version.as_deref() == Some(version) { + return Err(format!("无法卸载当前正在使用的版本 {}", version)); + } + + std::fs::remove_dir_all(&version_dir).map_err(|e| format!("删除版本目录失败: {}", e))?; + + info!("已卸载 Scala 版本 {}", version); + Ok(()) + } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index b3a960b..37edb26 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -19,7 +19,7 @@ mod utils; use crate::env_commands::{ EnvironmentManagerState, download_and_install_version, get_environment_info, - get_supported_environment_languages, switch_environment_version, + get_supported_environment_languages, switch_environment_version, uninstall_environment_version, }; use crate::env_manager::EnvironmentManager; use crate::env_providers::{ClojureEnvironmentProvider, ScalaEnvironmentProvider}; @@ -93,6 +93,7 @@ fn main() { get_environment_info, download_and_install_version, switch_environment_version, + uninstall_environment_version, get_supported_environment_languages, // 应用信息命令 get_app_info, diff --git a/src/components/setting/EnvironmentManager.vue b/src/components/setting/EnvironmentManager.vue index 5a98c93..559c852 100644 --- a/src/components/setting/EnvironmentManager.vue +++ b/src/components/setting/EnvironmentManager.vue @@ -42,99 +42,126 @@ - -
- -
-

已安装版本

-
-
-
- - {{ version.version }} - - 当前 - -
- -
+ +
+
+
+ + + + + + {{ downloadStatusText }} +
+ + {{ downloadProgress.percentage.toFixed(1) }}% +
+
+
+
+
+
- -
-

可下载版本

- - -
-
-
- - - - - - {{ downloadStatusText }} - + +
+
+ + {{ environmentInfo.error }} +
+
+ + +
+ + + -
-
-
-
- {{ version.version }} - - 已安装 - -
-
- {{ formatSize(version.size) }} - {{ formatDate(version.release_date) }} + + +