Skip to content

Commit 42d5169

Browse files
MarkShawn2020claude
andcommitted
feat(workspace): 新增 Logo 生成与管理面板
- 创建 LogoManager 组件支持 AI 生成 Logo(并发生成 2 个变体) - 自动生成 Lovstudio 品牌风格的 prompt - 支持本地上传和版本历史管理 - 添加 Rust 后端命令:list_project_logos, save_project_logo, exec_shell_command 等 - 使用 tokio 异步执行避免 UI 阻塞 - 在 ProjectDashboard 右侧边栏集成 Logo 管理卡片 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 931c009 commit 42d5169

3 files changed

Lines changed: 639 additions & 7 deletions

File tree

src-tauri/src/lib.rs

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4981,6 +4981,212 @@ fn get_project_logo(project_path: String) -> Option<String> {
49814981
None
49824982
}
49834983

4984+
/// List all logo versions in project assets directory
4985+
#[tauri::command]
4986+
fn list_project_logos(project_path: String) -> Vec<LogoVersion> {
4987+
let project = PathBuf::from(&project_path);
4988+
let assets_dir = project.join("assets");
4989+
4990+
let mut versions = Vec::new();
4991+
4992+
// Get current logo path for comparison
4993+
let current_logo = get_current_logo_path(&project);
4994+
4995+
// Scan assets directory for logo files
4996+
if let Ok(entries) = fs::read_dir(&assets_dir) {
4997+
for entry in entries.flatten() {
4998+
let path = entry.path();
4999+
if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
5000+
// Match logo-*.png, logo.png, logo.svg patterns
5001+
if (filename.starts_with("logo") || filename.starts_with("icon"))
5002+
&& (filename.ends_with(".png") || filename.ends_with(".svg") || filename.ends_with(".jpg"))
5003+
{
5004+
let created_at = entry.metadata()
5005+
.ok()
5006+
.and_then(|m| m.modified().ok())
5007+
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
5008+
.map(|d| d.as_secs())
5009+
.unwrap_or(0);
5010+
5011+
let path_str = path.to_string_lossy().to_string();
5012+
let is_current = current_logo.as_ref().map(|c| c == &path_str).unwrap_or(false);
5013+
5014+
versions.push(LogoVersion {
5015+
path: path_str,
5016+
filename: filename.to_string(),
5017+
created_at,
5018+
is_current,
5019+
});
5020+
}
5021+
}
5022+
}
5023+
}
5024+
5025+
// Sort by created_at descending (newest first)
5026+
versions.sort_by(|a, b| b.created_at.cmp(&a.created_at));
5027+
versions
5028+
}
5029+
5030+
#[derive(Debug, Clone, serde::Serialize)]
5031+
struct LogoVersion {
5032+
path: String,
5033+
filename: String,
5034+
created_at: u64,
5035+
is_current: bool,
5036+
}
5037+
5038+
/// Helper to get current logo path
5039+
fn get_current_logo_path(project: &PathBuf) -> Option<String> {
5040+
let logo_paths = [
5041+
"assets/logo.svg",
5042+
"assets/logo.png",
5043+
"assets/icon.svg",
5044+
"assets/icon.png",
5045+
];
5046+
5047+
for rel_path in logo_paths {
5048+
let full_path = project.join(rel_path);
5049+
if full_path.exists() {
5050+
return Some(full_path.to_string_lossy().to_string());
5051+
}
5052+
}
5053+
None
5054+
}
5055+
5056+
/// Save base64 logo data to project assets
5057+
#[tauri::command]
5058+
fn save_project_logo(project_path: String, base64_data: String, filename: String) -> Result<String, String> {
5059+
use base64::{Engine as _, engine::general_purpose::STANDARD};
5060+
5061+
let project = PathBuf::from(&project_path);
5062+
let assets_dir = project.join("assets");
5063+
5064+
// Ensure assets directory exists
5065+
fs::create_dir_all(&assets_dir)
5066+
.map_err(|e| format!("Failed to create assets directory: {}", e))?;
5067+
5068+
// Decode base64
5069+
let data = STANDARD.decode(&base64_data)
5070+
.map_err(|e| format!("Failed to decode base64: {}", e))?;
5071+
5072+
// Save versioned file
5073+
let versioned_path = assets_dir.join(&filename);
5074+
fs::write(&versioned_path, &data)
5075+
.map_err(|e| format!("Failed to write logo: {}", e))?;
5076+
5077+
// Also save as logo.png (current)
5078+
let ext = filename.rsplit('.').next().unwrap_or("png");
5079+
let current_path = assets_dir.join(format!("logo.{}", ext));
5080+
fs::write(&current_path, &data)
5081+
.map_err(|e| format!("Failed to write current logo: {}", e))?;
5082+
5083+
Ok(versioned_path.to_string_lossy().to_string())
5084+
}
5085+
5086+
/// Copy external file to project assets as logo
5087+
#[tauri::command]
5088+
fn copy_file_to_project_assets(source_path: String, project_path: String, target_filename: String) -> Result<String, String> {
5089+
let source = PathBuf::from(&source_path);
5090+
let project = PathBuf::from(&project_path);
5091+
let assets_dir = project.join("assets");
5092+
5093+
// Ensure assets directory exists
5094+
fs::create_dir_all(&assets_dir)
5095+
.map_err(|e| format!("Failed to create assets directory: {}", e))?;
5096+
5097+
// Copy to target filename
5098+
let target_path = assets_dir.join(&target_filename);
5099+
fs::copy(&source, &target_path)
5100+
.map_err(|e| format!("Failed to copy file: {}", e))?;
5101+
5102+
Ok(target_path.to_string_lossy().to_string())
5103+
}
5104+
5105+
/// Set a specific logo version as current
5106+
#[tauri::command]
5107+
fn set_current_project_logo(project_path: String, logo_path: String) -> Result<(), String> {
5108+
let project = PathBuf::from(&project_path);
5109+
let assets_dir = project.join("assets");
5110+
let source = PathBuf::from(&logo_path);
5111+
5112+
if !source.exists() {
5113+
return Err("Logo file does not exist".to_string());
5114+
}
5115+
5116+
let ext = source.extension()
5117+
.and_then(|e| e.to_str())
5118+
.unwrap_or("png");
5119+
5120+
// Copy as current logo
5121+
let current_path = assets_dir.join(format!("logo.{}", ext));
5122+
fs::copy(&source, &current_path)
5123+
.map_err(|e| format!("Failed to set current logo: {}", e))?;
5124+
5125+
Ok(())
5126+
}
5127+
5128+
/// Delete a logo version
5129+
#[tauri::command]
5130+
fn delete_project_logo(project_path: String, logo_path: String) -> Result<(), String> {
5131+
let path = PathBuf::from(&logo_path);
5132+
5133+
if !path.exists() {
5134+
return Ok(());
5135+
}
5136+
5137+
// Don't allow deleting the current logo (logo.png/logo.svg)
5138+
if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
5139+
if filename == "logo.png" || filename == "logo.svg" {
5140+
return Err("Cannot delete current logo. Set another version as current first.".to_string());
5141+
}
5142+
}
5143+
5144+
fs::remove_file(&path)
5145+
.map_err(|e| format!("Failed to delete logo: {}", e))?;
5146+
5147+
Ok(())
5148+
}
5149+
5150+
/// Read file as base64
5151+
#[tauri::command]
5152+
fn read_file_base64(path: String) -> Result<String, String> {
5153+
use base64::{Engine as _, engine::general_purpose::STANDARD};
5154+
5155+
let file_path = PathBuf::from(&path);
5156+
5157+
if !file_path.exists() {
5158+
return Err(format!("File does not exist: {}", path));
5159+
}
5160+
5161+
let data = fs::read(&file_path)
5162+
.map_err(|e| format!("Failed to read file: {}", e))?;
5163+
5164+
Ok(STANDARD.encode(&data))
5165+
}
5166+
5167+
/// Run a shell command in specified directory using login shell (async, non-blocking)
5168+
#[tauri::command]
5169+
async fn exec_shell_command(command: String, cwd: String) -> Result<String, String> {
5170+
use tokio::process::Command;
5171+
5172+
// Use user's default shell with login mode to get proper environment (API keys, etc.)
5173+
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string());
5174+
5175+
let output = Command::new(&shell)
5176+
.args(["-ilc", &format!("cd '{}' && {}", cwd, command)])
5177+
.output()
5178+
.await
5179+
.map_err(|e| format!("Failed to run command: {}", e))?;
5180+
5181+
if output.status.success() {
5182+
Ok(String::from_utf8_lossy(&output.stdout).to_string())
5183+
} else {
5184+
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
5185+
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
5186+
Err(if stderr.is_empty() { stdout } else { stderr })
5187+
}
5188+
}
5189+
49845190
// ============================================================================
49855191
// Directory Listing
49865192
// ============================================================================
@@ -5715,6 +5921,13 @@ pub fn run() {
57155921
hook_is_monitoring,
57165922
// Project logo
57175923
get_project_logo,
5924+
list_project_logos,
5925+
save_project_logo,
5926+
copy_file_to_project_assets,
5927+
set_current_project_logo,
5928+
delete_project_logo,
5929+
read_file_base64,
5930+
exec_shell_command,
57185931
hook_get_monitored,
57195932
hook_notify_complete,
57205933
// File system

0 commit comments

Comments
 (0)