diff --git a/apps/desktop/src-tauri/src/business/services/directory_settings_service.rs b/apps/desktop/src-tauri/src/business/services/directory_settings_service.rs new file mode 100644 index 0000000..4405d61 --- /dev/null +++ b/apps/desktop/src-tauri/src/business/services/directory_settings_service.rs @@ -0,0 +1,392 @@ +use crate::config::{AppConfig, DirectorySettings, DirectorySettingType}; +use anyhow::Result; +use std::sync::{Arc, Mutex}; +use tracing::{info, warn, error}; + +/// 目录设置管理服务 +/// 遵循 Tauri 开发规范的服务层设计原则 +pub struct DirectorySettingsService { + config: Arc>, +} + +impl DirectorySettingsService { + /// 创建新的目录设置服务实例 + pub fn new() -> Self { + let config = AppConfig::load(); + Self { + config: Arc::new(Mutex::new(config)), + } + } + + /// 从现有配置创建服务实例 + pub fn from_config(config: AppConfig) -> Self { + Self { + config: Arc::new(Mutex::new(config)), + } + } + + /// 获取所有目录设置 + pub fn get_directory_settings(&self) -> Result { + let config = self.config.lock().map_err(|e| { + error!("获取配置锁失败: {}", e); + anyhow::anyhow!("配置访问失败") + })?; + + Ok(config.directory_settings.clone()) + } + + /// 获取特定类型的目录设置 + pub fn get_directory_setting(&self, setting_type: DirectorySettingType) -> Result> { + let config = self.config.lock().map_err(|e| { + error!("获取配置锁失败: {}", e); + anyhow::anyhow!("配置访问失败") + })?; + + let result = config.get_directory_setting(setting_type.clone()).cloned(); + info!("获取目录设置 {:?}: {:?}", setting_type, result); + Ok(result) + } + + /// 更新特定类型的目录设置 + pub fn update_directory_setting(&self, setting_type: DirectorySettingType, path: String) -> Result<()> { + // 验证路径是否存在 + if !std::path::Path::new(&path).exists() { + warn!("设置的目录路径不存在: {}", path); + return Err(anyhow::anyhow!("目录路径不存在: {}", path)); + } + + let mut config = self.config.lock().map_err(|e| { + error!("获取配置锁失败: {}", e); + anyhow::anyhow!("配置访问失败") + })?; + + config.update_directory_setting(setting_type.clone(), path.clone()); + + // 保存配置到文件 + if let Err(e) = config.save() { + error!("保存配置失败: {}", e); + return Err(anyhow::anyhow!("保存配置失败: {}", e)); + } + + info!("更新目录设置 {:?}: {}", setting_type, path); + Ok(()) + } + + /// 批量更新目录设置 + pub fn update_multiple_directory_settings(&self, settings: Vec<(DirectorySettingType, String)>) -> Result<()> { + // 验证所有路径 + for (setting_type, path) in &settings { + if !std::path::Path::new(path).exists() { + warn!("设置的目录路径不存在: {} (类型: {:?})", path, setting_type); + return Err(anyhow::anyhow!("目录路径不存在: {}", path)); + } + } + + let mut config = self.config.lock().map_err(|e| { + error!("获取配置锁失败: {}", e); + anyhow::anyhow!("配置访问失败") + })?; + + // 批量更新 + for (setting_type, path) in settings { + config.update_directory_setting(setting_type.clone(), path.clone()); + info!("批量更新目录设置 {:?}: {}", setting_type, path); + } + + // 保存配置到文件 + if let Err(e) = config.save() { + error!("保存配置失败: {}", e); + return Err(anyhow::anyhow!("保存配置失败: {}", e)); + } + + info!("批量更新目录设置完成"); + Ok(()) + } + + /// 清除特定类型的目录设置 + pub fn clear_directory_setting(&self, setting_type: DirectorySettingType) -> Result<()> { + let mut config = self.config.lock().map_err(|e| { + error!("获取配置锁失败: {}", e); + anyhow::anyhow!("配置访问失败") + })?; + + match setting_type { + DirectorySettingType::MaterialImport => { + config.directory_settings.material_import_directory = None; + } + DirectorySettingType::TemplateImport => { + config.directory_settings.template_import_directory = None; + } + DirectorySettingType::JianyingExport => { + config.directory_settings.jianying_export_directory = None; + } + DirectorySettingType::ProjectExport => { + config.directory_settings.project_export_directory = None; + } + DirectorySettingType::ThumbnailExport => { + config.directory_settings.thumbnail_export_directory = None; + } + } + + // 保存配置到文件 + if let Err(e) = config.save() { + error!("保存配置失败: {}", e); + return Err(anyhow::anyhow!("保存配置失败: {}", e)); + } + + info!("清除目录设置 {:?}", setting_type); + Ok(()) + } + + /// 重置所有目录设置为默认值 + pub fn reset_all_directory_settings(&self) -> Result<()> { + let mut config = self.config.lock().map_err(|e| { + error!("获取配置锁失败: {}", e); + anyhow::anyhow!("配置访问失败") + })?; + + config.directory_settings = DirectorySettings::default(); + + // 保存配置到文件 + if let Err(e) = config.save() { + error!("保存配置失败: {}", e); + return Err(anyhow::anyhow!("保存配置失败: {}", e)); + } + + info!("重置所有目录设置为默认值"); + Ok(()) + } + + /// 设置自动记忆功能开关 + pub fn set_auto_remember_directories(&self, enabled: bool) -> Result<()> { + let mut config = self.config.lock().map_err(|e| { + error!("获取配置锁失败: {}", e); + anyhow::anyhow!("配置访问失败") + })?; + + config.directory_settings.auto_remember_directories = enabled; + + // 保存配置到文件 + if let Err(e) = config.save() { + error!("保存配置失败: {}", e); + return Err(anyhow::anyhow!("保存配置失败: {}", e)); + } + + info!("设置自动记忆功能: {}", enabled); + Ok(()) + } + + /// 检查是否启用自动记忆功能 + pub fn is_auto_remember_enabled(&self) -> Result { + let config = self.config.lock().map_err(|e| { + error!("获取配置锁失败: {}", e); + anyhow::anyhow!("配置访问失败") + })?; + + Ok(config.directory_settings.auto_remember_directories) + } + + /// 获取配置的克隆副本 + pub fn get_config_clone(&self) -> Result { + let config = self.config.lock().map_err(|e| { + error!("获取配置锁失败: {}", e); + anyhow::anyhow!("配置访问失败") + })?; + + Ok(config.clone()) + } + + /// 验证目录路径是否有效 + pub fn validate_directory_path(path: &str) -> bool { + let path_obj = std::path::Path::new(path); + path_obj.exists() && path_obj.is_dir() + } + + /// 获取目录设置的显示名称 + pub fn get_setting_display_name(setting_type: &DirectorySettingType) -> &'static str { + match setting_type { + DirectorySettingType::MaterialImport => "素材导入目录", + DirectorySettingType::TemplateImport => "模板导入目录", + DirectorySettingType::JianyingExport => "剪影导出目录", + DirectorySettingType::ProjectExport => "项目导出目录", + DirectorySettingType::ThumbnailExport => "缩略图导出目录", + } + } +} + +impl Default for DirectorySettingsService { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[test] + fn test_directory_settings_service_creation() { + let service = DirectorySettingsService::new(); + assert!(service.get_directory_settings().is_ok()); + } + + #[test] + fn test_update_directory_setting() { + let service = DirectorySettingsService::new(); + let temp_dir = tempdir().unwrap(); + let temp_path = temp_dir.path().to_string_lossy().to_string(); + + let result = service.update_directory_setting( + DirectorySettingType::MaterialImport, + temp_path.clone() + ); + assert!(result.is_ok()); + + let setting = service.get_directory_setting(DirectorySettingType::MaterialImport).unwrap(); + assert_eq!(setting, Some(temp_path)); + } + + #[test] + fn test_update_nonexistent_directory() { + let service = DirectorySettingsService::new(); + let result = service.update_directory_setting( + DirectorySettingType::MaterialImport, + "/non/existent/path".to_string() + ); + assert!(result.is_err()); + } + + #[test] + fn test_batch_update_directory_settings() { + let service = DirectorySettingsService::new(); + let temp_dir1 = tempdir().unwrap(); + let temp_dir2 = tempdir().unwrap(); + let temp_path1 = temp_dir1.path().to_string_lossy().to_string(); + let temp_path2 = temp_dir2.path().to_string_lossy().to_string(); + + let settings = vec![ + (DirectorySettingType::MaterialImport, temp_path1.clone()), + (DirectorySettingType::TemplateImport, temp_path2.clone()), + ]; + + let result = service.update_multiple_directory_settings(settings); + assert!(result.is_ok()); + + let material_setting = service.get_directory_setting(DirectorySettingType::MaterialImport).unwrap(); + let template_setting = service.get_directory_setting(DirectorySettingType::TemplateImport).unwrap(); + + assert_eq!(material_setting, Some(temp_path1)); + assert_eq!(template_setting, Some(temp_path2)); + } + + #[test] + fn test_clear_directory_setting() { + let service = DirectorySettingsService::new(); + let temp_dir = tempdir().unwrap(); + let temp_path = temp_dir.path().to_string_lossy().to_string(); + + // 先设置一个目录 + service.update_directory_setting( + DirectorySettingType::MaterialImport, + temp_path + ).unwrap(); + + // 验证设置成功 + let setting = service.get_directory_setting(DirectorySettingType::MaterialImport).unwrap(); + assert!(setting.is_some()); + + // 清除设置 + service.clear_directory_setting(DirectorySettingType::MaterialImport).unwrap(); + + // 验证已清除 + let setting = service.get_directory_setting(DirectorySettingType::MaterialImport).unwrap(); + assert!(setting.is_none()); + } + + #[test] + fn test_reset_all_directory_settings() { + let service = DirectorySettingsService::new(); + let temp_dir = tempdir().unwrap(); + let temp_path = temp_dir.path().to_string_lossy().to_string(); + + // 设置一些目录 + service.update_directory_setting( + DirectorySettingType::MaterialImport, + temp_path.clone() + ).unwrap(); + service.update_directory_setting( + DirectorySettingType::TemplateImport, + temp_path + ).unwrap(); + + // 重置所有设置 + service.reset_all_directory_settings().unwrap(); + + // 验证所有设置都被重置 + let material_setting = service.get_directory_setting(DirectorySettingType::MaterialImport).unwrap(); + let template_setting = service.get_directory_setting(DirectorySettingType::TemplateImport).unwrap(); + + assert!(material_setting.is_none()); + assert!(template_setting.is_none()); + } + + #[test] + fn test_validate_directory_path() { + let temp_dir = tempdir().unwrap(); + let temp_path = temp_dir.path().to_string_lossy(); + + assert!(DirectorySettingsService::validate_directory_path(&temp_path)); + assert!(!DirectorySettingsService::validate_directory_path("/non/existent/path")); + } + + #[test] + fn test_auto_remember_setting() { + let service = DirectorySettingsService::new(); + + // 默认应该是启用的 + assert!(service.is_auto_remember_enabled().unwrap()); + + // 禁用自动记忆 + service.set_auto_remember_directories(false).unwrap(); + assert!(!service.is_auto_remember_enabled().unwrap()); + + // 重新启用 + service.set_auto_remember_directories(true).unwrap(); + assert!(service.is_auto_remember_enabled().unwrap()); + } + + #[test] + fn test_get_setting_display_name() { + assert_eq!( + DirectorySettingsService::get_setting_display_name(&DirectorySettingType::MaterialImport), + "素材导入目录" + ); + assert_eq!( + DirectorySettingsService::get_setting_display_name(&DirectorySettingType::TemplateImport), + "模板导入目录" + ); + assert_eq!( + DirectorySettingsService::get_setting_display_name(&DirectorySettingType::JianyingExport), + "剪影导出目录" + ); + assert_eq!( + DirectorySettingsService::get_setting_display_name(&DirectorySettingType::ProjectExport), + "项目导出目录" + ); + assert_eq!( + DirectorySettingsService::get_setting_display_name(&DirectorySettingType::ThumbnailExport), + "缩略图导出目录" + ); + } + + #[test] + fn test_get_config_clone() { + let service = DirectorySettingsService::new(); + let config = service.get_config_clone(); + assert!(config.is_ok()); + + let config = config.unwrap(); + assert_eq!(config.directory_settings.auto_remember_directories, true); + } +} diff --git a/apps/desktop/src-tauri/src/business/services/mod.rs b/apps/desktop/src-tauri/src/business/services/mod.rs index d4bb059..c691fdc 100644 --- a/apps/desktop/src-tauri/src/business/services/mod.rs +++ b/apps/desktop/src-tauri/src/business/services/mod.rs @@ -33,6 +33,7 @@ pub mod video_generation_service; pub mod conversation_service; pub mod jianying_export; pub mod template_segment_weight_service; +pub mod directory_settings_service; #[cfg(test)] pub mod tests; diff --git a/apps/desktop/src-tauri/src/config.rs b/apps/desktop/src-tauri/src/config.rs index 9cd67ac..f1337c6 100644 --- a/apps/desktop/src-tauri/src/config.rs +++ b/apps/desktop/src-tauri/src/config.rs @@ -11,6 +11,25 @@ pub struct AppConfig { pub window_size: WindowSize, pub recent_projects: Vec, pub default_project_path: Option, + pub directory_settings: DirectorySettings, +} + +/// 全局目录设置 +/// 存储各种导入导出操作的默认目录路径 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct DirectorySettings { + /// 素材导入默认目录 + pub material_import_directory: Option, + /// 模板导入默认目录 + pub template_import_directory: Option, + /// 剪影导出默认目录 + pub jianying_export_directory: Option, + /// 项目导出默认目录 + pub project_export_directory: Option, + /// 缩略图导出默认目录 + pub thumbnail_export_directory: Option, + /// 是否启用自动记忆功能 + pub auto_remember_directories: bool, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -19,6 +38,19 @@ pub struct WindowSize { pub height: f64, } +impl Default for DirectorySettings { + fn default() -> Self { + DirectorySettings { + material_import_directory: None, + template_import_directory: None, + jianying_export_directory: None, + project_export_directory: None, + thumbnail_export_directory: None, + auto_remember_directories: true, + } + } +} + impl Default for AppConfig { fn default() -> Self { AppConfig { @@ -31,6 +63,7 @@ impl Default for AppConfig { }, recent_projects: Vec::new(), default_project_path: None, + directory_settings: DirectorySettings::default(), } } } @@ -83,10 +116,62 @@ impl AppConfig { pub fn add_recent_project(&mut self, project_path: String) { self.recent_projects.retain(|p| p != &project_path); self.recent_projects.insert(0, project_path); - + // 保持最近项目列表不超过10个 if self.recent_projects.len() > 10 { self.recent_projects.truncate(10); } } + + /// 更新目录设置 + pub fn update_directory_setting(&mut self, setting_type: DirectorySettingType, path: String) { + match setting_type { + DirectorySettingType::MaterialImport => { + self.directory_settings.material_import_directory = Some(path); + } + DirectorySettingType::TemplateImport => { + self.directory_settings.template_import_directory = Some(path); + } + DirectorySettingType::JianyingExport => { + self.directory_settings.jianying_export_directory = Some(path); + } + DirectorySettingType::ProjectExport => { + self.directory_settings.project_export_directory = Some(path); + } + DirectorySettingType::ThumbnailExport => { + self.directory_settings.thumbnail_export_directory = Some(path); + } + } + } + + /// 获取目录设置 + pub fn get_directory_setting(&self, setting_type: DirectorySettingType) -> Option<&String> { + match setting_type { + DirectorySettingType::MaterialImport => { + self.directory_settings.material_import_directory.as_ref() + } + DirectorySettingType::TemplateImport => { + self.directory_settings.template_import_directory.as_ref() + } + DirectorySettingType::JianyingExport => { + self.directory_settings.jianying_export_directory.as_ref() + } + DirectorySettingType::ProjectExport => { + self.directory_settings.project_export_directory.as_ref() + } + DirectorySettingType::ThumbnailExport => { + self.directory_settings.thumbnail_export_directory.as_ref() + } + } + } +} + +/// 目录设置类型枚举 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum DirectorySettingType { + MaterialImport, + TemplateImport, + JianyingExport, + ProjectExport, + ThumbnailExport, } diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 3593fb9..fb2b8cb 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -35,7 +35,10 @@ pub fn run() { commands::project_commands::validate_project_path, commands::project_commands::get_default_project_name, commands::system_commands::select_directory, + commands::system_commands::select_directory_with_options, commands::system_commands::select_file, + commands::system_commands::select_file_with_options, + commands::system_commands::save_file_with_options, commands::system_commands::open_file_directory, commands::system_commands::play_video_segment, commands::system_commands::get_app_info, @@ -192,6 +195,7 @@ pub fn run() { commands::template_commands::import_template_optimized, commands::template_commands::update_segment_matching_rule, commands::template_commands::get_segment_matching_rule, + commands::template_commands::select_template_file, // 项目-模板绑定管理命令 commands::project_template_binding_commands::create_project_template_binding, commands::project_template_binding_commands::update_project_template_binding, @@ -247,6 +251,7 @@ pub fn run() { commands::template_matching_result_commands::get_matching_result_status_options, commands::template_matching_result_commands::export_matching_result_to_jianying, commands::template_matching_result_commands::export_matching_result_to_jianying_v2, + commands::template_matching_result_commands::select_jianying_export_path, // 导出记录命令 commands::export_record_commands::get_export_record, commands::export_record_commands::list_export_records, @@ -387,7 +392,18 @@ pub fn run() { commands::template_segment_weight_commands::update_template_segment_weight, commands::template_segment_weight_commands::has_custom_segment_weights, commands::template_segment_weight_commands::get_template_weight_statistics, - commands::template_segment_weight_commands::get_segment_weights_for_categories + commands::template_segment_weight_commands::get_segment_weights_for_categories, + // 目录设置管理命令 + commands::directory_settings_commands::get_directory_settings, + commands::directory_settings_commands::get_directory_setting, + commands::directory_settings_commands::update_directory_setting, + commands::directory_settings_commands::batch_update_directory_settings, + commands::directory_settings_commands::clear_directory_setting, + commands::directory_settings_commands::reset_all_directory_settings, + commands::directory_settings_commands::set_auto_remember_directories, + commands::directory_settings_commands::is_auto_remember_enabled, + commands::directory_settings_commands::validate_directory_path, + commands::directory_settings_commands::select_and_save_directory ]) .setup(|app| { // 初始化日志系统 diff --git a/apps/desktop/src-tauri/src/presentation/commands/directory_settings_commands.rs b/apps/desktop/src-tauri/src/presentation/commands/directory_settings_commands.rs new file mode 100644 index 0000000..f95d603 --- /dev/null +++ b/apps/desktop/src-tauri/src/presentation/commands/directory_settings_commands.rs @@ -0,0 +1,397 @@ +use crate::business::services::directory_settings_service::DirectorySettingsService; +use crate::config::{DirectorySettings, DirectorySettingType}; +use serde::{Deserialize, Serialize}; +use tauri::command; +use tracing::{info, warn, error}; + +/// 目录设置响应结构 +#[derive(Debug, Serialize, Deserialize)] +pub struct DirectorySettingsResponse { + pub material_import_directory: Option, + pub template_import_directory: Option, + pub jianying_export_directory: Option, + pub project_export_directory: Option, + pub thumbnail_export_directory: Option, + pub auto_remember_directories: bool, +} + +impl From for DirectorySettingsResponse { + fn from(settings: DirectorySettings) -> Self { + Self { + material_import_directory: settings.material_import_directory, + template_import_directory: settings.template_import_directory, + jianying_export_directory: settings.jianying_export_directory, + project_export_directory: settings.project_export_directory, + thumbnail_export_directory: settings.thumbnail_export_directory, + auto_remember_directories: settings.auto_remember_directories, + } + } +} + +/// 目录设置更新请求 +#[derive(Debug, Serialize, Deserialize)] +pub struct UpdateDirectorySettingRequest { + pub setting_type: String, + pub path: String, +} + +/// 批量目录设置更新请求 +#[derive(Debug, Serialize, Deserialize)] +pub struct BatchUpdateDirectorySettingsRequest { + pub settings: Vec, +} + +/// 获取所有目录设置 +#[command] +pub async fn get_directory_settings() -> Result { + info!("获取目录设置"); + + let service = DirectorySettingsService::new(); + + match service.get_directory_settings() { + Ok(settings) => { + info!("成功获取目录设置"); + Ok(DirectorySettingsResponse::from(settings)) + } + Err(e) => { + error!("获取目录设置失败: {}", e); + Err(format!("获取目录设置失败: {}", e)) + } + } +} + +/// 获取特定类型的目录设置 +#[command] +pub async fn get_directory_setting(setting_type: String) -> Result, String> { + info!("获取目录设置: {}", setting_type); + + let setting_type_enum = parse_directory_setting_type(&setting_type)?; + let service = DirectorySettingsService::new(); + + match service.get_directory_setting(setting_type_enum) { + Ok(setting) => { + info!("成功获取目录设置 {}: {:?}", setting_type, setting); + Ok(setting) + } + Err(e) => { + error!("获取目录设置失败: {}", e); + Err(format!("获取目录设置失败: {}", e)) + } + } +} + +/// 更新目录设置 +#[command] +pub async fn update_directory_setting(setting_type: String, path: String) -> Result<(), String> { + info!("更新目录设置: {} -> {}", setting_type, path); + + let setting_type_enum = parse_directory_setting_type(&setting_type)?; + let service = DirectorySettingsService::new(); + + match service.update_directory_setting(setting_type_enum, path.clone()) { + Ok(()) => { + info!("成功更新目录设置: {} -> {}", setting_type, path); + Ok(()) + } + Err(e) => { + error!("更新目录设置失败: {}", e); + Err(format!("更新目录设置失败: {}", e)) + } + } +} + +/// 批量更新目录设置 +#[command] +pub async fn batch_update_directory_settings(request: BatchUpdateDirectorySettingsRequest) -> Result<(), String> { + info!("批量更新目录设置,共 {} 项", request.settings.len()); + + let service = DirectorySettingsService::new(); + let mut settings = Vec::new(); + + // 解析所有设置类型 + for setting in request.settings { + let setting_type_enum = parse_directory_setting_type(&setting.setting_type)?; + settings.push((setting_type_enum, setting.path)); + } + + match service.update_multiple_directory_settings(settings) { + Ok(()) => { + info!("成功批量更新目录设置"); + Ok(()) + } + Err(e) => { + error!("批量更新目录设置失败: {}", e); + Err(format!("批量更新目录设置失败: {}", e)) + } + } +} + +/// 清除目录设置 +#[command] +pub async fn clear_directory_setting(setting_type: String) -> Result<(), String> { + info!("清除目录设置: {}", setting_type); + + let setting_type_enum = parse_directory_setting_type(&setting_type)?; + let service = DirectorySettingsService::new(); + + match service.clear_directory_setting(setting_type_enum) { + Ok(()) => { + info!("成功清除目录设置: {}", setting_type); + Ok(()) + } + Err(e) => { + error!("清除目录设置失败: {}", e); + Err(format!("清除目录设置失败: {}", e)) + } + } +} + +/// 重置所有目录设置 +#[command] +pub async fn reset_all_directory_settings() -> Result<(), String> { + info!("重置所有目录设置"); + + let service = DirectorySettingsService::new(); + + match service.reset_all_directory_settings() { + Ok(()) => { + info!("成功重置所有目录设置"); + Ok(()) + } + Err(e) => { + error!("重置目录设置失败: {}", e); + Err(format!("重置目录设置失败: {}", e)) + } + } +} + +/// 设置自动记忆功能 +#[command] +pub async fn set_auto_remember_directories(enabled: bool) -> Result<(), String> { + info!("设置自动记忆功能: {}", enabled); + + let service = DirectorySettingsService::new(); + + match service.set_auto_remember_directories(enabled) { + Ok(()) => { + info!("成功设置自动记忆功能: {}", enabled); + Ok(()) + } + Err(e) => { + error!("设置自动记忆功能失败: {}", e); + Err(format!("设置自动记忆功能失败: {}", e)) + } + } +} + +/// 检查是否启用自动记忆功能 +#[command] +pub async fn is_auto_remember_enabled() -> Result { + let service = DirectorySettingsService::new(); + + match service.is_auto_remember_enabled() { + Ok(enabled) => { + info!("自动记忆功能状态: {}", enabled); + Ok(enabled) + } + Err(e) => { + error!("获取自动记忆功能状态失败: {}", e); + Err(format!("获取自动记忆功能状态失败: {}", e)) + } + } +} + +/// 验证目录路径 +#[command] +pub async fn validate_directory_path(path: String) -> Result { + let is_valid = DirectorySettingsService::validate_directory_path(&path); + info!("验证目录路径 {}: {}", path, is_valid); + Ok(is_valid) +} + +/// 选择目录并可选择性地保存设置 +#[command] +pub async fn select_and_save_directory( + app: tauri::AppHandle, + setting_type: Option, + title: Option, +) -> Result, String> { + use crate::presentation::commands::system_commands::select_directory; + + // 设置对话框标题 + let dialog_title = title.unwrap_or_else(|| { + if let Some(ref st) = setting_type { + format!("选择{}", get_setting_display_name_by_string(st)) + } else { + "选择目录".to_string() + } + }); + + info!("打开目录选择对话框: {}", dialog_title); + + // 使用现有的目录选择功能 + match select_directory(app) { + Ok(Some(selected_path)) => { + info!("用户选择了目录: {}", selected_path); + + // 如果指定了设置类型,询问是否保存 + if let Some(st) = setting_type { + let service = DirectorySettingsService::new(); + + // 检查是否启用自动记忆 + if service.is_auto_remember_enabled().unwrap_or(false) { + let setting_type_enum = parse_directory_setting_type(&st)?; + if let Err(e) = service.update_directory_setting(setting_type_enum, selected_path.clone()) { + warn!("自动保存目录设置失败: {}", e); + // 不返回错误,只是记录警告 + } else { + info!("自动保存目录设置: {} -> {}", st, selected_path); + } + } + } + + Ok(Some(selected_path)) + } + Ok(None) => { + info!("用户取消了目录选择"); + Ok(None) + } + Err(e) => { + error!("目录选择失败: {}", e); + Err(format!("目录选择失败: {}", e)) + } + } +} + +/// 解析目录设置类型字符串 +fn parse_directory_setting_type(setting_type: &str) -> Result { + match setting_type.to_lowercase().as_str() { + "material_import" | "materialimport" => Ok(DirectorySettingType::MaterialImport), + "template_import" | "templateimport" => Ok(DirectorySettingType::TemplateImport), + "jianying_export" | "jianyingexport" => Ok(DirectorySettingType::JianyingExport), + "project_export" | "projectexport" => Ok(DirectorySettingType::ProjectExport), + "thumbnail_export" | "thumbnailexport" => Ok(DirectorySettingType::ThumbnailExport), + _ => Err(format!("不支持的目录设置类型: {}", setting_type)), + } +} + +/// 根据字符串获取设置显示名称 +fn get_setting_display_name_by_string(setting_type: &str) -> &'static str { + match setting_type.to_lowercase().as_str() { + "material_import" | "materialimport" => "素材导入目录", + "template_import" | "templateimport" => "模板导入目录", + "jianying_export" | "jianyingexport" => "剪影导出目录", + "project_export" | "projectexport" => "项目导出目录", + "thumbnail_export" | "thumbnailexport" => "缩略图导出目录", + _ => "目录", + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_directory_setting_type() { + assert!(matches!( + parse_directory_setting_type("material_import"), + Ok(DirectorySettingType::MaterialImport) + )); + assert!(matches!( + parse_directory_setting_type("MaterialImport"), + Ok(DirectorySettingType::MaterialImport) + )); + assert!(matches!( + parse_directory_setting_type("template_import"), + Ok(DirectorySettingType::TemplateImport) + )); + assert!(matches!( + parse_directory_setting_type("jianying_export"), + Ok(DirectorySettingType::JianyingExport) + )); + assert!(matches!( + parse_directory_setting_type("project_export"), + Ok(DirectorySettingType::ProjectExport) + )); + assert!(matches!( + parse_directory_setting_type("thumbnail_export"), + Ok(DirectorySettingType::ThumbnailExport) + )); + assert!(parse_directory_setting_type("invalid").is_err()); + } + + #[test] + fn test_get_setting_display_name() { + assert_eq!(get_setting_display_name_by_string("material_import"), "素材导入目录"); + assert_eq!(get_setting_display_name_by_string("template_import"), "模板导入目录"); + assert_eq!(get_setting_display_name_by_string("jianying_export"), "剪影导出目录"); + assert_eq!(get_setting_display_name_by_string("project_export"), "项目导出目录"); + assert_eq!(get_setting_display_name_by_string("thumbnail_export"), "缩略图导出目录"); + assert_eq!(get_setting_display_name_by_string("invalid"), "目录"); + } + + #[test] + fn test_directory_settings_response_from_settings() { + let settings = DirectorySettings { + material_import_directory: Some("/path/to/material".to_string()), + template_import_directory: Some("/path/to/template".to_string()), + jianying_export_directory: None, + project_export_directory: None, + thumbnail_export_directory: None, + auto_remember_directories: true, + }; + + let response = DirectorySettingsResponse::from(settings); + + assert_eq!(response.material_import_directory, Some("/path/to/material".to_string())); + assert_eq!(response.template_import_directory, Some("/path/to/template".to_string())); + assert_eq!(response.jianying_export_directory, None); + assert_eq!(response.project_export_directory, None); + assert_eq!(response.thumbnail_export_directory, None); + assert_eq!(response.auto_remember_directories, true); + } + + #[test] + fn test_batch_update_request_parsing() { + let request = BatchUpdateDirectorySettingsRequest { + settings: vec![ + UpdateDirectorySettingRequest { + setting_type: "material_import".to_string(), + path: "/path/to/material".to_string(), + }, + UpdateDirectorySettingRequest { + setting_type: "template_import".to_string(), + path: "/path/to/template".to_string(), + }, + ], + }; + + assert_eq!(request.settings.len(), 2); + assert_eq!(request.settings[0].setting_type, "material_import"); + assert_eq!(request.settings[0].path, "/path/to/material"); + assert_eq!(request.settings[1].setting_type, "template_import"); + assert_eq!(request.settings[1].path, "/path/to/template"); + } + + #[tokio::test] + async fn test_get_directory_settings_command() { + // 这个测试需要实际的服务实例,在集成测试中更合适 + // 这里只测试函数签名和基本逻辑 + let result = get_directory_settings().await; + // 由于依赖文件系统,这里只验证返回类型 + assert!(result.is_ok() || result.is_err()); + } + + #[tokio::test] + async fn test_validate_directory_path_command() { + // 测试有效路径(当前目录应该存在) + let result = validate_directory_path(".".to_string()).await; + assert!(result.is_ok()); + assert!(result.unwrap()); + + // 测试无效路径 + let result = validate_directory_path("/non/existent/path".to_string()).await; + assert!(result.is_ok()); + assert!(!result.unwrap()); + } +} diff --git a/apps/desktop/src-tauri/src/presentation/commands/material_commands.rs b/apps/desktop/src-tauri/src/presentation/commands/material_commands.rs index 38fa8d9..d7c87d5 100644 --- a/apps/desktop/src-tauri/src/presentation/commands/material_commands.rs +++ b/apps/desktop/src-tauri/src/presentation/commands/material_commands.rs @@ -714,12 +714,31 @@ async fn process_single_file_with_full_logic( /// 选择文件夹进行批量导入 #[command] pub async fn select_material_folders(app_handle: tauri::AppHandle) -> Result, String> { - use crate::presentation::commands::system_commands::select_directory; + use crate::presentation::commands::system_commands::select_directory_with_options; + use crate::business::services::directory_settings_service::DirectorySettingsService; + use crate::config::DirectorySettingType; - // 使用现有的目录选择功能,但允许多选 - // 注意:这是一个简化实现,实际可能需要修改系统命令以支持多选 - match select_directory(app_handle) { - Ok(Some(folder_path)) => Ok(vec![folder_path]), + // 获取默认目录设置 + let service = DirectorySettingsService::new(); + let default_dir = service.get_directory_setting(DirectorySettingType::MaterialImport) + .unwrap_or(None); + + // 使用带选项的目录选择功能 + match select_directory_with_options( + app_handle, + Some("选择素材导入目录".to_string()), + default_dir + ) { + Ok(Some(folder_path)) => { + // 如果启用了自动记忆,更新默认目录 + if let Ok(true) = service.is_auto_remember_enabled() { + let _ = service.update_directory_setting( + DirectorySettingType::MaterialImport, + folder_path.clone() + ); + } + Ok(vec![folder_path]) + }, Ok(None) => Ok(Vec::new()), Err(e) => Err(e), } @@ -999,8 +1018,12 @@ pub async fn get_supported_extensions() -> Result, String> { #[command] pub async fn select_material_files(app: tauri::AppHandle) -> Result, String> { use tauri_plugin_dialog::DialogExt; + use crate::business::services::directory_settings_service::DirectorySettingsService; + use crate::config::DirectorySettingType; - let dialog = app.dialog().file() + // 获取默认目录设置 + let service = DirectorySettingsService::new(); + let mut dialog = app.dialog().file() .set_title("选择素材文件") .add_filter("视频文件", &["mp4", "avi", "mov", "mkv", "wmv", "flv", "webm", "m4v"]) .add_filter("音频文件", &["mp3", "wav", "flac", "aac", "ogg", "wma", "m4a"]) @@ -1008,6 +1031,13 @@ pub async fn select_material_files(app: tauri::AppHandle) -> Result, .add_filter("文档文件", &["pdf", "doc", "docx", "txt", "md", "rtf"]) .add_filter("所有支持的文件", &MaterialService::get_supported_extensions()); + // 设置默认目录 + if let Ok(Some(default_dir)) = service.get_directory_setting(DirectorySettingType::MaterialImport) { + if std::path::Path::new(&default_dir).exists() { + dialog = dialog.set_directory(&default_dir); + } + } + let (tx, rx) = std::sync::mpsc::channel(); dialog.pick_files(move |file_paths| { @@ -1020,7 +1050,23 @@ pub async fn select_material_files(app: tauri::AppHandle) -> Result, }); match rx.recv_timeout(std::time::Duration::from_secs(30)) { - Ok(paths) => Ok(paths), + Ok(paths) => { + // 如果选择了文件且启用了自动记忆,更新默认目录 + if !paths.is_empty() { + if let Ok(true) = service.is_auto_remember_enabled() { + if let Some(first_file) = paths.first() { + if let Some(parent_dir) = std::path::Path::new(first_file).parent() { + let parent_path = parent_dir.to_string_lossy().to_string(); + let _ = service.update_directory_setting( + DirectorySettingType::MaterialImport, + parent_path + ); + } + } + } + } + Ok(paths) + }, Err(_) => Err("文件选择操作超时或失败".to_string()), } } diff --git a/apps/desktop/src-tauri/src/presentation/commands/mod.rs b/apps/desktop/src-tauri/src/presentation/commands/mod.rs index ff1c64f..fc4b555 100644 --- a/apps/desktop/src-tauri/src/presentation/commands/mod.rs +++ b/apps/desktop/src-tauri/src/presentation/commands/mod.rs @@ -30,3 +30,4 @@ pub mod image_download_commands; pub mod conversation_commands; pub mod watermark_commands; pub mod template_segment_weight_commands; +pub mod directory_settings_commands; diff --git a/apps/desktop/src-tauri/src/presentation/commands/system_commands.rs b/apps/desktop/src-tauri/src/presentation/commands/system_commands.rs index f251202..b39b798 100644 --- a/apps/desktop/src-tauri/src/presentation/commands/system_commands.rs +++ b/apps/desktop/src-tauri/src/presentation/commands/system_commands.rs @@ -15,10 +15,29 @@ pub struct AppInfo { /// 遵循 Tauri 开发规范的系统集成设计 #[command] pub fn select_directory(app: AppHandle) -> Result, String> { + select_directory_with_options(app, None, None) +} + +/// 选择目录命令(带选项) +/// 支持自定义标题和默认目录 +#[command] +pub fn select_directory_with_options( + app: AppHandle, + title: Option, + default_directory: Option +) -> Result, String> { use tauri_plugin_dialog::DialogExt; // 使用同步方式打开文件夹选择对话框 - let dialog = app.dialog().file().set_title("选择项目目录"); + let mut dialog = app.dialog().file() + .set_title(&title.unwrap_or_else(|| "选择目录".to_string())); + + // 设置默认目录 + if let Some(default_dir) = default_directory { + if std::path::Path::new(&default_dir).exists() { + dialog = dialog.set_directory(&default_dir); + } + } // 创建一个简单的阻塞实现 let (tx, rx) = std::sync::mpsc::channel(); @@ -159,9 +178,22 @@ pub async fn record_performance_metric(name: String, value: f64) -> Result<(), S /// 选择文件命令 #[command] pub fn select_file(app: AppHandle, filters: Option)>>) -> Result, String> { + select_file_with_options(app, filters, None, None) +} + +/// 选择文件命令(带选项) +/// 支持自定义标题和默认目录 +#[command] +pub fn select_file_with_options( + app: AppHandle, + filters: Option)>>, + title: Option, + default_directory: Option +) -> Result, String> { use tauri_plugin_dialog::DialogExt; - let mut dialog = app.dialog().file().set_title("选择文件"); + let mut dialog = app.dialog().file() + .set_title(&title.unwrap_or_else(|| "选择文件".to_string())); // 添加文件过滤器 if let Some(filter_list) = filters { @@ -171,6 +203,13 @@ pub fn select_file(app: AppHandle, filters: Option)>>) } } + // 设置默认目录 + if let Some(default_dir) = default_directory { + if std::path::Path::new(&default_dir).exists() { + dialog = dialog.set_directory(&default_dir); + } + } + // 创建一个简单的阻塞实现 let (tx, rx) = std::sync::mpsc::channel(); @@ -186,6 +225,56 @@ pub fn select_file(app: AppHandle, filters: Option)>>) } } +/// 保存文件命令(带选项) +/// 支持自定义标题、默认文件名和默认目录 +#[command] +pub fn save_file_with_options( + app: AppHandle, + title: Option, + default_filename: Option, + default_directory: Option, + filters: Option)>>, +) -> Result, String> { + use tauri_plugin_dialog::DialogExt; + + let mut dialog = app.dialog().file() + .set_title(&title.unwrap_or_else(|| "保存文件".to_string())); + + // 设置默认文件名 + if let Some(filename) = default_filename { + dialog = dialog.set_file_name(&filename); + } + + // 设置默认目录 + if let Some(default_dir) = default_directory { + if std::path::Path::new(&default_dir).exists() { + dialog = dialog.set_directory(&default_dir); + } + } + + // 添加文件过滤器 + if let Some(filter_list) = filters { + for (name, extensions) in filter_list { + let ext_refs: Vec<&str> = extensions.iter().map(|s| s.as_str()).collect(); + dialog = dialog.add_filter(&name, &ext_refs); + } + } + + // 创建一个简单的阻塞实现 + let (tx, rx) = std::sync::mpsc::channel(); + + dialog.save_file(move |file_path| { + let _ = tx.send(file_path); + }); + + // 等待结果,设置超时 + match rx.recv_timeout(std::time::Duration::from_secs(30)) { + Ok(Some(path)) => Ok(Some(path.to_string())), + Ok(None) => Ok(None), + Err(_) => Err("文件保存超时".to_string()), + } +} + /// 打开文件所在目录命令 #[command] pub async fn open_file_directory(file_path: String) -> Result<(), String> { diff --git a/apps/desktop/src-tauri/src/presentation/commands/template_commands.rs b/apps/desktop/src-tauri/src/presentation/commands/template_commands.rs index 6ce614a..8eb67e3 100644 --- a/apps/desktop/src-tauri/src/presentation/commands/template_commands.rs +++ b/apps/desktop/src-tauri/src/presentation/commands/template_commands.rs @@ -402,4 +402,43 @@ pub async fn get_segment_matching_rule( service.get_segment_matching_rule(&segment_id) .await .map_err(|e| e.to_string()) +} + +/// 选择模板文件命令 +#[tauri::command] +pub async fn select_template_file(app: tauri::AppHandle) -> Result, String> { + use crate::presentation::commands::system_commands::select_file_with_options; + use crate::business::services::directory_settings_service::DirectorySettingsService; + use crate::config::DirectorySettingType; + + // 获取默认目录设置 + let service = DirectorySettingsService::new(); + let default_dir = service.get_directory_setting(DirectorySettingType::TemplateImport) + .unwrap_or(None); + + // 使用带选项的文件选择功能 + let result = select_file_with_options( + app, + Some(vec![("剪映草稿文件".to_string(), vec!["json".to_string()])]), + Some("选择模板文件".to_string()), + default_dir + ); + + match result { + Ok(Some(file_path)) => { + // 如果启用了自动记忆,更新默认目录 + if let Ok(true) = service.is_auto_remember_enabled() { + if let Some(parent_dir) = std::path::Path::new(&file_path).parent() { + let parent_path = parent_dir.to_string_lossy().to_string(); + let _ = service.update_directory_setting( + DirectorySettingType::TemplateImport, + parent_path + ); + } + } + Ok(Some(file_path)) + }, + Ok(None) => Ok(None), + Err(e) => Err(e), + } } \ No newline at end of file diff --git a/apps/desktop/src-tauri/src/presentation/commands/template_matching_result_commands.rs b/apps/desktop/src-tauri/src/presentation/commands/template_matching_result_commands.rs index 2872678..f1713b8 100644 --- a/apps/desktop/src-tauri/src/presentation/commands/template_matching_result_commands.rs +++ b/apps/desktop/src-tauri/src/presentation/commands/template_matching_result_commands.rs @@ -474,3 +474,46 @@ pub async fn export_matching_result_to_jianying_v2( } } } + +/// 选择剪影导出路径命令 +#[command] +pub async fn select_jianying_export_path( + app: tauri::AppHandle, + default_filename: String, +) -> Result, String> { + use crate::presentation::commands::system_commands::save_file_with_options; + use crate::business::services::directory_settings_service::DirectorySettingsService; + use crate::config::DirectorySettingType; + + // 获取默认目录设置 + let service = DirectorySettingsService::new(); + let default_dir = service.get_directory_setting(DirectorySettingType::JianyingExport) + .unwrap_or(None); + + // 使用带选项的文件保存功能 + let result = save_file_with_options( + app, + Some("导出到剪影".to_string()), + Some(default_filename), + default_dir, + Some(vec![("JSON文件".to_string(), vec!["json".to_string()])]) + ); + + match result { + Ok(Some(file_path)) => { + // 如果启用了自动记忆,更新默认目录 + if let Ok(true) = service.is_auto_remember_enabled() { + if let Some(parent_dir) = std::path::Path::new(&file_path).parent() { + let parent_path = parent_dir.to_string_lossy().to_string(); + let _ = service.update_directory_setting( + DirectorySettingType::JianyingExport, + parent_path + ); + } + } + Ok(Some(file_path)) + }, + Ok(None) => Ok(None), + Err(e) => Err(e), + } +} diff --git a/apps/desktop/src/components/DirectorySettingsButton.tsx b/apps/desktop/src/components/DirectorySettingsButton.tsx new file mode 100644 index 0000000..e448a7b --- /dev/null +++ b/apps/desktop/src/components/DirectorySettingsButton.tsx @@ -0,0 +1,60 @@ +import React, { useState } from 'react'; +import { Settings } from 'lucide-react'; +import DirectorySettingsModal from './DirectorySettingsModal'; + +interface DirectorySettingsButtonProps { + className?: string; + size?: 'sm' | 'md' | 'lg'; + variant?: 'primary' | 'secondary' | 'ghost'; +} + +const DirectorySettingsButton: React.FC = ({ + className = '', + size = 'md', + variant = 'secondary', +}) => { + const [isModalOpen, setIsModalOpen] = useState(false); + + const sizeClasses = { + sm: 'px-2 py-1 text-xs', + md: 'px-3 py-2 text-sm', + lg: 'px-4 py-3 text-base', + }; + + const variantClasses = { + primary: 'bg-blue-600 text-white hover:bg-blue-700', + secondary: 'bg-gray-200 text-gray-700 hover:bg-gray-300', + ghost: 'bg-transparent text-gray-600 hover:bg-gray-100', + }; + + const iconSizes = { + sm: 'w-3 h-3', + md: 'w-4 h-4', + lg: 'w-5 h-5', + }; + + return ( + <> + + + setIsModalOpen(false)} + /> + + ); +}; + +export default DirectorySettingsButton; diff --git a/apps/desktop/src/components/DirectorySettingsModal.tsx b/apps/desktop/src/components/DirectorySettingsModal.tsx new file mode 100644 index 0000000..50cfda1 --- /dev/null +++ b/apps/desktop/src/components/DirectorySettingsModal.tsx @@ -0,0 +1,268 @@ +import React, { useState, useEffect } from 'react'; +import { X, Folder, Settings, RotateCcw, Save } from 'lucide-react'; +import { invoke } from '@tauri-apps/api/core'; + +interface DirectorySettings { + material_import_directory: string | null; + template_import_directory: string | null; + jianying_export_directory: string | null; + project_export_directory: string | null; + thumbnail_export_directory: string | null; + auto_remember_directories: boolean; +} + +interface DirectorySettingsModalProps { + isOpen: boolean; + onClose: () => void; +} + +const DirectorySettingsModal: React.FC = ({ + isOpen, + onClose, +}) => { + const [settings, setSettings] = useState({ + material_import_directory: null, + template_import_directory: null, + jianying_export_directory: null, + project_export_directory: null, + thumbnail_export_directory: null, + auto_remember_directories: true, + }); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + + // 加载设置 + useEffect(() => { + if (isOpen) { + loadSettings(); + } + }, [isOpen]); + + const loadSettings = async () => { + setLoading(true); + try { + const result = await invoke('get_directory_settings'); + setSettings(result); + } catch (error) { + console.error('加载目录设置失败:', error); + } finally { + setLoading(false); + } + }; + + // 选择目录 + const selectDirectory = async (settingType: string, displayName: string) => { + try { + const result = await invoke('select_and_save_directory', { + settingType, + title: `选择${displayName}`, + }); + + if (result) { + setSettings(prev => ({ + ...prev, + [`${settingType}_directory`]: result, + })); + } + } catch (error) { + console.error(`选择${displayName}失败:`, error); + } + }; + + // 清除目录设置 + const clearDirectory = async (settingType: string) => { + try { + await invoke('clear_directory_setting', { settingType }); + setSettings(prev => ({ + ...prev, + [`${settingType}_directory`]: null, + })); + } catch (error) { + console.error('清除目录设置失败:', error); + } + }; + + // 重置所有设置 + const resetAllSettings = async () => { + if (!confirm('确定要重置所有目录设置吗?')) { + return; + } + + try { + await invoke('reset_all_directory_settings'); + await loadSettings(); + } catch (error) { + console.error('重置设置失败:', error); + } + }; + + // 保存自动记忆设置 + const saveAutoRememberSetting = async (enabled: boolean) => { + setSaving(true); + try { + await invoke('set_auto_remember_directories', { enabled }); + setSettings(prev => ({ + ...prev, + auto_remember_directories: enabled, + })); + } catch (error) { + console.error('保存自动记忆设置失败:', error); + } finally { + setSaving(false); + } + }; + + // 目录设置项配置 + const directoryItems = [ + { + key: 'material_import', + label: '素材导入目录', + description: '选择素材文件时的默认目录', + value: settings.material_import_directory, + }, + { + key: 'template_import', + label: '模板导入目录', + description: '选择模板文件时的默认目录', + value: settings.template_import_directory, + }, + { + key: 'jianying_export', + label: '剪影导出目录', + description: '导出剪影项目时的默认目录', + value: settings.jianying_export_directory, + }, + { + key: 'project_export', + label: '项目导出目录', + description: '导出项目文件时的默认目录', + value: settings.project_export_directory, + }, + { + key: 'thumbnail_export', + label: '缩略图导出目录', + description: '导出缩略图时的默认目录', + value: settings.thumbnail_export_directory, + }, + ]; + + if (!isOpen) return null; + + return ( +
+
+ {/* 标题栏 */} +
+
+ +

目录设置

+
+ +
+ + {/* 内容区域 */} +
+ {loading ? ( +
+
+ 加载设置中... +
+ ) : ( +
+ {/* 自动记忆设置 */} +
+
+
+

自动记忆目录

+

+ 启用后,选择文件时会自动记住所选目录作为下次的默认位置 +

+
+ +
+
+ + {/* 目录设置列表 */} +
+

默认目录设置

+ {directoryItems.map((item) => ( +
+
+
+

{item.label}

+

{item.description}

+ {item.value && ( +
+ {item.value} +
+ )} +
+
+ + {item.value && ( + + )} +
+
+
+ ))} +
+
+ )} +
+ + {/* 底部操作栏 */} +
+ +
+ +
+
+
+
+ ); +}; + +export default DirectorySettingsModal; diff --git a/apps/desktop/src/components/QuickDirectorySelector.tsx b/apps/desktop/src/components/QuickDirectorySelector.tsx new file mode 100644 index 0000000..e80daff --- /dev/null +++ b/apps/desktop/src/components/QuickDirectorySelector.tsx @@ -0,0 +1,114 @@ +import React, { useState, useEffect } from 'react'; +import { Folder, FolderOpen, Settings } from 'lucide-react'; +import { invoke } from '@tauri-apps/api/core'; + +interface QuickDirectorySelectorProps { + settingType: 'material_import' | 'template_import' | 'jianying_export' | 'project_export' | 'thumbnail_export'; + label: string; + onDirectorySelected?: (directory: string) => void; + className?: string; +} + +const QuickDirectorySelector: React.FC = ({ + settingType, + label, + onDirectorySelected, + className = '', +}) => { + const [currentDirectory, setCurrentDirectory] = useState(null); + const [loading, setLoading] = useState(false); + + // 加载当前设置的目录 + useEffect(() => { + loadCurrentDirectory(); + }, [settingType]); + + const loadCurrentDirectory = async () => { + try { + const directory = await invoke('get_directory_setting', { + settingType, + }); + setCurrentDirectory(directory); + } catch (error) { + console.error('加载目录设置失败:', error); + } + }; + + // 选择目录 + const selectDirectory = async () => { + setLoading(true); + try { + const result = await invoke('select_and_save_directory', { + settingType, + title: `选择${label}`, + }); + + if (result) { + setCurrentDirectory(result); + onDirectorySelected?.(result); + } + } catch (error) { + console.error('选择目录失败:', error); + } finally { + setLoading(false); + } + }; + + // 使用当前目录 + const useCurrentDirectory = () => { + if (currentDirectory) { + onDirectorySelected?.(currentDirectory); + } + }; + + return ( +
+ {/* 当前目录显示 */} + {currentDirectory && ( +
+ +
+ )} + + {/* 选择目录按钮 */} + + + {/* 设置按钮 */} + +
+ ); +}; + +export default QuickDirectorySelector; diff --git a/apps/desktop/src/components/template/ImportTemplateModal.tsx b/apps/desktop/src/components/template/ImportTemplateModal.tsx index 058c06b..9ac4397 100644 --- a/apps/desktop/src/components/template/ImportTemplateModal.tsx +++ b/apps/desktop/src/components/template/ImportTemplateModal.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { X, Upload, AlertCircle, CheckCircle } from 'lucide-react'; import { invoke } from '@tauri-apps/api/core'; import { ImportTemplateRequest } from '../../types/template'; +import DirectorySettingsButton from '../DirectorySettingsButton'; interface ImportTemplateModalProps { onClose: () => void; @@ -28,10 +29,8 @@ export const ImportTemplateModal: React.FC = ({ // 处理文件选择 const handleFileSelect = async () => { try { - // 使用 Tauri 命令选择文件 - const selected = await invoke('select_file', { - filters: [['剪映草稿文件', ['json']]] - }); + // 使用新的模板文件选择命令,支持默认目录 + const selected = await invoke('select_template_file'); if (selected) { setSelectedFile(selected); @@ -174,6 +173,11 @@ export const ImportTemplateModal: React.FC = ({ {errors.file_path} )} + + {/* 目录设置按钮 */} +
+ +
{/* 模板名称 */}