@@ -481,6 +481,120 @@ impl EditorPanel for AssetBrowserPanel {
481481 }
482482 }
483483
484+ // --- Batch rename dialog ---
485+ if state. batch_rename_active {
486+ let count = state. batch_rename_assets . len ( ) ;
487+ let preview_ext = state
488+ . batch_rename_assets
489+ . first ( )
490+ . and_then ( |p| p. extension ( ) )
491+ . and_then ( |e| e. to_str ( ) )
492+ . map ( |s| s. to_string ( ) ) ;
493+ let mut open = true ;
494+ egui:: Window :: new ( "Batch Rename" )
495+ . collapsible ( false )
496+ . resizable ( false )
497+ . open ( & mut open)
498+ . anchor ( egui:: Align2 :: CENTER_CENTER , [ 0.0 , 0.0 ] )
499+ . show ( ui. ctx ( ) , |ui| {
500+ ui. horizontal ( |ui| {
501+ ui. label ( "Base name:" ) ;
502+ ui. text_edit_singleline ( & mut state. batch_rename_base ) ;
503+ } ) ;
504+ ui. horizontal ( |ui| {
505+ ui. label ( "Start at:" ) ;
506+ ui. add (
507+ egui:: DragValue :: new ( & mut state. batch_rename_start ) . range ( 0 ..=9999 ) ,
508+ ) ;
509+ } ) ;
510+ ui. add_space ( 4.0 ) ;
511+ let preview_text = match & preview_ext {
512+ Some ( ext) => format ! (
513+ "Preview: {}_{:02}.{}, {}_{:02}.{}, … ({} files)" ,
514+ state. batch_rename_base, state. batch_rename_start, ext,
515+ state. batch_rename_base, state. batch_rename_start + 1 , ext,
516+ count,
517+ ) ,
518+ None => format ! (
519+ "Preview: {}_{:02}, {}_{:02}, … ({} items)" ,
520+ state. batch_rename_base,
521+ state. batch_rename_start,
522+ state. batch_rename_base,
523+ state. batch_rename_start + 1 ,
524+ count,
525+ ) ,
526+ } ;
527+ ui. label (
528+ egui:: RichText :: new ( preview_text)
529+ . size ( 11.0 )
530+ . color ( theme. text . muted . to_color32 ( ) ) ,
531+ ) ;
532+ ui. add_space ( 4.0 ) ;
533+ ui. horizontal ( |ui| {
534+ let base_ok = !state. batch_rename_base . trim ( ) . is_empty ( ) ;
535+ let rename_btn = egui:: Button :: new ( "Rename" ) ;
536+ if ui. add_enabled ( base_ok, rename_btn) . clicked ( ) {
537+ state. pending_batch_rename = Some ( (
538+ state. batch_rename_base . clone ( ) ,
539+ state. batch_rename_start ,
540+ state. batch_rename_assets . clone ( ) ,
541+ ) ) ;
542+ state. batch_rename_active = false ;
543+ }
544+ if ui. button ( "Cancel" ) . clicked ( ) {
545+ state. batch_rename_active = false ;
546+ }
547+ } ) ;
548+ } ) ;
549+ if !open {
550+ state. batch_rename_active = false ;
551+ }
552+ }
553+
554+ // --- Process pending batch rename ---
555+ if let Some ( ( base, start, assets) ) = state. pending_batch_rename . take ( ) {
556+ let mut new_selection: std:: collections:: HashSet < std:: path:: PathBuf > =
557+ std:: collections:: HashSet :: new ( ) ;
558+ for ( i, old_path) in assets. iter ( ) . enumerate ( ) {
559+ let Some ( parent) = old_path. parent ( ) else {
560+ new_selection. insert ( old_path. clone ( ) ) ;
561+ continue ;
562+ } ;
563+ let ext = old_path. extension ( ) . and_then ( |e| e. to_str ( ) ) ;
564+ let new_name = match ext {
565+ Some ( e) => format ! ( "{}_{:02}.{}" , base, start as usize + i, e) ,
566+ None => format ! ( "{}_{:02}" , base, start as usize + i) ,
567+ } ;
568+ let new_path = parent. join ( & new_name) ;
569+ if new_path == * old_path {
570+ new_selection. insert ( old_path. clone ( ) ) ;
571+ continue ;
572+ }
573+ if new_path. exists ( ) {
574+ state. last_error = Some ( format ! ( "Skipped: {} already exists" , new_name) ) ;
575+ state. error_timeout = 3.0 ;
576+ new_selection. insert ( old_path. clone ( ) ) ;
577+ continue ;
578+ }
579+ let is_dir = old_path. is_dir ( ) ;
580+ match std:: fs:: rename ( old_path, & new_path) {
581+ Ok ( _) => {
582+ new_selection. insert ( new_path. clone ( ) ) ;
583+ if state. selected_path . as_ref ( ) == Some ( old_path) {
584+ state. selected_path = Some ( new_path. clone ( ) ) ;
585+ }
586+ emit_asset_path_change ( world, old_path, & new_path, is_dir) ;
587+ }
588+ Err ( e) => {
589+ state. last_error = Some ( format ! ( "Rename failed: {}" , e) ) ;
590+ state. error_timeout = 3.0 ;
591+ new_selection. insert ( old_path. clone ( ) ) ;
592+ }
593+ }
594+ }
595+ state. selected_assets = new_selection;
596+ }
597+
484598 // --- Process pending move (drag-to-folder) ---
485599 if let Some ( ( sources, target) ) = state. pending_move . take ( ) {
486600 let mut moved = 0usize ;
@@ -837,6 +951,22 @@ fn render_context_menu(
837951 }
838952 state. context_menu_pos = None ;
839953 }
954+ } else if state. selected_assets . len ( ) > 1 {
955+ if menu_item ( ui, regular:: TEXT_AA , "Batch Rename…" , "" , text_primary) {
956+ let mut assets: Vec < std:: path:: PathBuf > =
957+ state. selected_assets . iter ( ) . cloned ( ) . collect ( ) ;
958+ assets. sort ( ) ;
959+ state. batch_rename_base = assets
960+ . first ( )
961+ . and_then ( |p| p. file_stem ( ) )
962+ . and_then ( |s| s. to_str ( ) )
963+ . unwrap_or ( "file" )
964+ . to_string ( ) ;
965+ state. batch_rename_start = 1 ;
966+ state. batch_rename_assets = assets;
967+ state. batch_rename_active = true ;
968+ state. context_menu_pos = None ;
969+ }
840970 }
841971
842972 if menu_item ( ui, regular:: COPY , "Duplicate" , "Ctrl+D" , text_primary) {
0 commit comments