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