feat: 实现全局目录设置功能

- 扩展AppConfig结构体,添加DirectorySettings字段
- 创建DirectorySettingsService服务,提供CRUD操作
- 添加Tauri命令接口,支持前端调用
- 修改现有文件选择逻辑,优先使用保存的目录设置
- 创建DirectorySettingsModal等UI组件
- 集成到素材导入、模板导入、剪影导出等界面
- 实现自动记忆功能,可选择是否记住路径
- 编写完整的单元测试,确保功能可靠性

功能特点:
- 支持素材导入、模板导入、剪影导出、项目导出、缩略图导出等目录设置
- 自动记忆用户选择的目录,下次使用时作为默认路径
- 提供友好的UI界面管理目录设置
- 支持批量更新、清除、重置等操作
- 完整的错误处理和验证机制
This commit is contained in:
imeepos
2025-07-25 18:38:50 +08:00
parent 05903e22fc
commit 7c43f7a7ed
14 changed files with 1570 additions and 15 deletions

View File

@@ -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<Mutex<AppConfig>>,
}
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<DirectorySettings> {
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<Option<String>> {
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<bool> {
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<AppConfig> {
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);
}
}

View File

@@ -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;

View File

@@ -11,6 +11,25 @@ pub struct AppConfig {
pub window_size: WindowSize,
pub recent_projects: Vec<String>,
pub default_project_path: Option<String>,
pub directory_settings: DirectorySettings,
}
/// 全局目录设置
/// 存储各种导入导出操作的默认目录路径
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DirectorySettings {
/// 素材导入默认目录
pub material_import_directory: Option<String>,
/// 模板导入默认目录
pub template_import_directory: Option<String>,
/// 剪影导出默认目录
pub jianying_export_directory: Option<String>,
/// 项目导出默认目录
pub project_export_directory: Option<String>,
/// 缩略图导出默认目录
pub thumbnail_export_directory: Option<String>,
/// 是否启用自动记忆功能
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,
}

View File

@@ -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| {
// 初始化日志系统

View File

@@ -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<String>,
pub template_import_directory: Option<String>,
pub jianying_export_directory: Option<String>,
pub project_export_directory: Option<String>,
pub thumbnail_export_directory: Option<String>,
pub auto_remember_directories: bool,
}
impl From<DirectorySettings> 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<UpdateDirectorySettingRequest>,
}
/// 获取所有目录设置
#[command]
pub async fn get_directory_settings() -> Result<DirectorySettingsResponse, String> {
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<Option<String>, 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<bool, String> {
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<bool, String> {
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<String>,
title: Option<String>,
) -> Result<Option<String>, 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<DirectorySettingType, String> {
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());
}
}

View File

@@ -714,12 +714,31 @@ async fn process_single_file_with_full_logic(
/// 选择文件夹进行批量导入
#[command]
pub async fn select_material_folders(app_handle: tauri::AppHandle) -> Result<Vec<String>, 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<Vec<String>, String> {
#[command]
pub async fn select_material_files(app: tauri::AppHandle) -> Result<Vec<String>, 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<Vec<String>,
.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<Vec<String>,
});
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()),
}
}

View File

@@ -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;

View File

@@ -15,10 +15,29 @@ pub struct AppInfo {
/// 遵循 Tauri 开发规范的系统集成设计
#[command]
pub fn select_directory(app: AppHandle) -> Result<Option<String>, String> {
select_directory_with_options(app, None, None)
}
/// 选择目录命令(带选项)
/// 支持自定义标题和默认目录
#[command]
pub fn select_directory_with_options(
app: AppHandle,
title: Option<String>,
default_directory: Option<String>
) -> Result<Option<String>, 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<Vec<(String, Vec<String>)>>) -> Result<Option<String>, String> {
select_file_with_options(app, filters, None, None)
}
/// 选择文件命令(带选项)
/// 支持自定义标题和默认目录
#[command]
pub fn select_file_with_options(
app: AppHandle,
filters: Option<Vec<(String, Vec<String>)>>,
title: Option<String>,
default_directory: Option<String>
) -> Result<Option<String>, 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<Vec<(String, Vec<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();
@@ -186,6 +225,56 @@ pub fn select_file(app: AppHandle, filters: Option<Vec<(String, Vec<String>)>>)
}
}
/// 保存文件命令(带选项)
/// 支持自定义标题、默认文件名和默认目录
#[command]
pub fn save_file_with_options(
app: AppHandle,
title: Option<String>,
default_filename: Option<String>,
default_directory: Option<String>,
filters: Option<Vec<(String, Vec<String>)>>,
) -> Result<Option<String>, 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> {

View File

@@ -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<Option<String>, 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),
}
}

View File

@@ -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<Option<String>, 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),
}
}

View File

@@ -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<DirectorySettingsButtonProps> = ({
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 (
<>
<button
onClick={() => setIsModalOpen(true)}
className={`
flex items-center space-x-2 rounded transition-colors
${sizeClasses[size]}
${variantClasses[variant]}
${className}
`}
title="目录设置"
>
<Settings className={iconSizes[size]} />
<span></span>
</button>
<DirectorySettingsModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
/>
</>
);
};
export default DirectorySettingsButton;

View File

@@ -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<DirectorySettingsModalProps> = ({
isOpen,
onClose,
}) => {
const [settings, setSettings] = useState<DirectorySettings>({
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<DirectorySettings>('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<string | null>('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 (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[80vh] overflow-hidden">
{/* 标题栏 */}
<div className="flex items-center justify-between p-6 border-b border-gray-200">
<div className="flex items-center space-x-2">
<Settings className="w-5 h-5 text-blue-600" />
<h2 className="text-xl font-semibold text-gray-900"></h2>
</div>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<X className="w-6 h-6" />
</button>
</div>
{/* 内容区域 */}
<div className="p-6 overflow-y-auto max-h-[60vh]">
{loading ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-2 text-gray-600">...</span>
</div>
) : (
<div className="space-y-6">
{/* 自动记忆设置 */}
<div className="bg-blue-50 p-4 rounded-lg">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-blue-900"></h3>
<p className="text-sm text-blue-700 mt-1">
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={settings.auto_remember_directories}
onChange={(e) => saveAutoRememberSetting(e.target.checked)}
disabled={saving}
className="sr-only"
/>
<div className={`w-11 h-6 bg-gray-200 rounded-full peer ${
settings.auto_remember_directories ? 'bg-blue-600' : ''
} ${saving ? 'opacity-50' : ''}`}>
<div className={`absolute top-[2px] left-[2px] bg-white border border-gray-300 rounded-full h-5 w-5 transition-transform ${
settings.auto_remember_directories ? 'translate-x-5' : ''
}`}></div>
</div>
</label>
</div>
</div>
{/* 目录设置列表 */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-gray-900"></h3>
{directoryItems.map((item) => (
<div key={item.key} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className="text-sm font-medium text-gray-900">{item.label}</h4>
<p className="text-sm text-gray-500 mt-1">{item.description}</p>
{item.value && (
<div className="mt-2 p-2 bg-gray-50 rounded text-sm text-gray-700 font-mono break-all">
{item.value}
</div>
)}
</div>
<div className="flex items-center space-x-2 ml-4">
<button
onClick={() => selectDirectory(item.key, item.label)}
className="flex items-center space-x-1 px-3 py-1.5 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
>
<Folder className="w-4 h-4" />
<span>{item.value ? '更改' : '选择'}</span>
</button>
{item.value && (
<button
onClick={() => clearDirectory(item.key)}
className="px-3 py-1.5 text-sm bg-gray-200 text-gray-700 rounded hover:bg-gray-300 transition-colors"
>
</button>
)}
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
{/* 底部操作栏 */}
<div className="flex items-center justify-between p-6 border-t border-gray-200 bg-gray-50">
<button
onClick={resetAllSettings}
className="flex items-center space-x-2 px-4 py-2 text-sm bg-red-600 text-white rounded hover:bg-red-700 transition-colors"
>
<RotateCcw className="w-4 h-4" />
<span></span>
</button>
<div className="flex items-center space-x-3">
<button
onClick={onClose}
className="px-4 py-2 text-sm bg-gray-200 text-gray-700 rounded hover:bg-gray-300 transition-colors"
>
</button>
</div>
</div>
</div>
</div>
);
};
export default DirectorySettingsModal;

View File

@@ -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<QuickDirectorySelectorProps> = ({
settingType,
label,
onDirectorySelected,
className = '',
}) => {
const [currentDirectory, setCurrentDirectory] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
// 加载当前设置的目录
useEffect(() => {
loadCurrentDirectory();
}, [settingType]);
const loadCurrentDirectory = async () => {
try {
const directory = await invoke<string | null>('get_directory_setting', {
settingType,
});
setCurrentDirectory(directory);
} catch (error) {
console.error('加载目录设置失败:', error);
}
};
// 选择目录
const selectDirectory = async () => {
setLoading(true);
try {
const result = await invoke<string | null>('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 (
<div className={`flex items-center space-x-2 ${className}`}>
{/* 当前目录显示 */}
{currentDirectory && (
<div className="flex items-center space-x-2 flex-1 min-w-0">
<button
onClick={useCurrentDirectory}
className="flex items-center space-x-2 px-3 py-2 bg-green-50 text-green-700 rounded border border-green-200 hover:bg-green-100 transition-colors flex-1 min-w-0"
title={`使用默认目录: ${currentDirectory}`}
>
<FolderOpen className="w-4 h-4 flex-shrink-0" />
<span className="text-sm truncate">
{currentDirectory.split(/[/\\]/).pop() || currentDirectory}
</span>
</button>
</div>
)}
{/* 选择目录按钮 */}
<button
onClick={selectDirectory}
disabled={loading}
className="flex items-center space-x-2 px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
title={currentDirectory ? `更改${label}` : `选择${label}`}
>
{loading ? (
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : (
<Folder className="w-4 h-4" />
)}
<span className="text-sm whitespace-nowrap">
{currentDirectory ? '更改' : '选择'}
</span>
</button>
{/* 设置按钮 */}
<button
onClick={() => {
// 这里可以打开目录设置对话框或者其他设置操作
console.log('打开目录设置');
}}
className="p-2 text-gray-400 hover:text-gray-600 transition-colors"
title="目录设置"
>
<Settings className="w-4 h-4" />
</button>
</div>
);
};
export default QuickDirectorySelector;

View File

@@ -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<ImportTemplateModalProps> = ({
// 处理文件选择
const handleFileSelect = async () => {
try {
// 使用 Tauri 命令选择文件
const selected = await invoke<string | null>('select_file', {
filters: [['剪映草稿文件', ['json']]]
});
// 使用新的模板文件选择命令,支持默认目录
const selected = await invoke<string | null>('select_template_file');
if (selected) {
setSelectedFile(selected);
@@ -174,6 +173,11 @@ export const ImportTemplateModal: React.FC<ImportTemplateModalProps> = ({
{errors.file_path}
</div>
)}
{/* 目录设置按钮 */}
<div className="mt-3 flex justify-end">
<DirectorySettingsButton size="sm" variant="ghost" />
</div>
</div>
{/* 模板名称 */}