6060 "iterations_conf_msg" : {"en" : "Iterations confirmed." , "zh" : "迭代次数已确认。" },
6161 "iterations_empty_msg" : {"en" : "Iterations is empty!" , "zh" : "迭代次数为空!" },
6262 "iterations_max_restrict" : {"en" : "Iterations cannot exceed 100000000!" , "zh" : "迭代次数不能超过100000000!" },
63+ "use_timestamp" : {"en" : "Timestamp filename" , "zh" : "时间戳文件名" },
64+ "delete_original" : {"en" : "Delete original" , "zh" : "删除原文件" },
65+ "confirm_delete_title" : {"en" : "Confirm Delete" , "zh" : "确认删除" },
66+ "confirm_delete_msg" : {"en" : "Delete the original file?\n {path}" , "zh" : "删除原始文件?\n {path}" },
67+ "batch_select" : {"en" : "Batch Select" , "zh" : "批量选择" },
68+ "batch_files_selected" : {"en" : "{count} files selected" , "zh" : "已选择 {count} 个文件" },
69+ "batch_file_status" : {"en" : "File {done}/{total}" , "zh" : "文件 {done}/{total}" },
70+ "file_delete_failed" : {"en" : "Failed to delete original: {err}" , "zh" : "删除原文件失败:{err}" },
6371}
6472
6573CONFIG_FILE = "settings.json"
@@ -78,7 +86,17 @@ def __init__(self):
7886
7987 self .lang_code = "en"
8088 self .translatable_widgets = [] # List of (widget, key, attribute_name)
81-
89+
90+ # Batch file lists for bulk operations
91+ self .batch_enc_files = []
92+ self .batch_dec_files = []
93+
94+ # Options for file operations
95+ self .use_timestamp_enc = tk .BooleanVar ()
96+ self .use_timestamp_dec = tk .BooleanVar ()
97+ self .delete_original_enc = tk .BooleanVar ()
98+ self .delete_original_dec = tk .BooleanVar ()
99+
82100 # Load settings before UI setup to apply language and paths
83101 self .load_settings ()
84102
@@ -331,13 +349,36 @@ def create_single_file_op(self, parent, row, title_key, path_var, btn_text_key,
331349 btn_frame = ttk .Frame (sub_frame )
332350 btn_frame .pack (fill = "x" )
333351
334- btn_sel = ttk .Button (btn_frame , command = lambda : self .select_file (path_var ))
352+ btn_sel = ttk .Button (btn_frame , command = lambda : self .select_file (path_var , mode ))
335353 self .register_widget (btn_sel , "select_file" )
336354 btn_sel .pack (side = "left" , padx = 2 )
355+
356+ btn_batch = ttk .Button (btn_frame , command = lambda : self .select_batch_files (path_var , mode ))
357+ self .register_widget (btn_batch , "batch_select" )
358+ btn_batch .pack (side = "left" , padx = 2 )
337359
338360 btn_do = ttk .Button (btn_frame , command = btn_command )
339361 self .register_widget (btn_do , btn_text_key )
340362 btn_do .pack (side = "right" , padx = 2 )
363+
364+ # Options: timestamp filename and delete original
365+ options_frame = ttk .Frame (sub_frame )
366+ options_frame .pack (fill = "x" , pady = (2 , 0 ))
367+
368+ if mode == "enc" :
369+ ts_var = self .use_timestamp_enc
370+ del_var = self .delete_original_enc
371+ else :
372+ ts_var = self .use_timestamp_dec
373+ del_var = self .delete_original_dec
374+
375+ cb_ts = ttk .Checkbutton (options_frame , variable = ts_var )
376+ self .register_widget (cb_ts , "use_timestamp" )
377+ cb_ts .pack (side = "left" , padx = 2 )
378+
379+ cb_del = ttk .Checkbutton (options_frame , variable = del_var )
380+ self .register_widget (cb_del , "delete_original" )
381+ cb_del .pack (side = "left" , padx = 2 )
341382
342383 # Progress Bar
343384 pb = ttk .Progressbar (sub_frame , orient = "horizontal" , mode = "determinate" )
@@ -436,9 +477,29 @@ def select_dec_out_dir(self):
436477 self .dec_output_path .set (d )
437478 self .save_settings ()
438479
439- def select_file (self , var ):
480+ def select_file (self , var , mode = None ):
440481 f = filedialog .askopenfilename ()
441- if f : var .set (f )
482+ if f :
483+ var .set (f )
484+ # Clear batch list when single file is selected
485+ if mode == "enc" :
486+ self .batch_enc_files = []
487+ elif mode == "dec" :
488+ self .batch_dec_files = []
489+
490+ def select_batch_files (self , path_var , mode ):
491+ files = filedialog .askopenfilenames ()
492+ if files :
493+ files = list (files )
494+ if mode == "enc" :
495+ self .batch_enc_files = files
496+ else :
497+ self .batch_dec_files = files
498+ count = len (files )
499+ if count == 1 :
500+ path_var .set (files [0 ])
501+ else :
502+ path_var .set (self .tr ("batch_files_selected" ).format (count = count ))
442503
443504 def action_encrypt_text (self ):
444505 pwd = self .get_password ()
@@ -474,68 +535,131 @@ def action_decrypt_text(self):
474535 messagebox .showerror (self .tr ("error" ), self .tr ("dec_fail_msg" ))
475536
476537 def action_encrypt_file (self ):
477- self ._run_file_op (self .selected_enc_file , self .enc_output_path ,
478- self .crypto .encrypt_file , self .enc_pb , self .enc_status , ".enc" , self .tr ("file_enc_title" ))
538+ files = self .batch_enc_files if self .batch_enc_files else [self .selected_enc_file .get ()]
539+ self ._run_file_ops (files , self .enc_output_path ,
540+ self .crypto .encrypt_file , self .enc_pb , self .enc_status , ".enc" ,
541+ self .tr ("file_enc_title" ),
542+ self .use_timestamp_enc .get (), self .delete_original_enc .get ())
479543
480544 def action_decrypt_file (self ):
481- self ._run_file_op (self .selected_dec_file , self .dec_output_path ,
482- self .crypto .decrypt_file , self .dec_pb , self .dec_status , ".dec" , self .tr ("file_dec_title" ))
483-
484- def _run_file_op (self , input_var , output_dir_var , op_func , pb , status_lbl , suffix , op_name ):
545+ files = self .batch_dec_files if self .batch_dec_files else [self .selected_dec_file .get ()]
546+ self ._run_file_ops (files , self .dec_output_path ,
547+ self .crypto .decrypt_file , self .dec_pb , self .dec_status , ".dec" ,
548+ self .tr ("file_dec_title" ),
549+ self .use_timestamp_dec .get (), self .delete_original_dec .get ())
550+
551+ def _run_file_ops (self , input_paths , output_dir_var , op_func , pb , status_lbl , suffix , op_name ,
552+ use_timestamp = False , delete_original = False ):
553+ """Run file operation(s) on one or more files in a background thread."""
485554 pwd = self .get_password ()
486555 iterations = self .get_iterations ()
487556 if not iterations : return
488557 if not pwd : return
489-
490- in_path = input_var .get ()
491- if not in_path or not os .path .exists (in_path ):
558+
559+ # Validate all input paths
560+ valid_paths = [p for p in input_paths if p and os .path .exists (p )]
561+ if not valid_paths :
492562 messagebox .showwarning (self .tr ("file_error_title" ), self .tr ("file_error_msg" ))
493563 return
494564
495565 out_dir = output_dir_var .get ()
496- if out_dir :
497- if not os .path .exists (out_dir ):
498- try :
499- os .makedirs (out_dir )
500- except OSError :
501- # If cannot create, fallback to input directory
502- out_dir = ""
503-
504- if not out_dir :
505- out_dir = os .path .dirname (in_path )
506-
507- base_name = os .path .basename (in_path )
508- out_path = os .path .join (out_dir , base_name + suffix )
509-
566+ if out_dir and not os .path .exists (out_dir ):
567+ try :
568+ os .makedirs (out_dir )
569+ except OSError :
570+ out_dir = ""
571+
510572 algo = self .algo_var .get ()
511-
512- # Prepare translated strings for the thread
573+ total_files = len (valid_paths )
574+
575+ # Translate strings once before entering thread
513576 str_processing = self .tr ("processing" )
514577 str_done = self .tr ("done" )
515578 str_complete = self .tr ("op_complete" )
516579 str_saved = self .tr ("saved_to" )
517580 str_error = self .tr ("error" )
518-
581+ str_file_status = self .tr ("batch_file_status" )
582+ str_confirm_delete_title = self .tr ("confirm_delete_title" )
583+ str_confirm_delete_msg = self .tr ("confirm_delete_msg" )
584+ str_file_delete_failed = self .tr ("file_delete_failed" )
585+
586+ # Shared state for progress polling
587+ progress_info = {"file_value" : 0 , "done" : False }
588+
589+ def poll_progress ():
590+ pb .config (value = progress_info ["file_value" ])
591+ if not progress_info ["done" ]:
592+ self .after (50 , poll_progress )
593+
519594 def worker ():
520595 try :
521- # Update UI
522596 self .after (0 , lambda : status_lbl .config (text = str_processing , foreground = "blue" ))
523597 self .after (0 , lambda : pb .config (value = 0 ))
524-
525- def progress (current , total ):
526- perc = (current / total ) * 100
527- self .after (0 , lambda : pb .config (value = perc ))
528-
598+ self .after (50 , poll_progress )
599+
600+ saved_paths = []
601+ completed_inputs = [] # collect originals for deferred deletion
529602 start_time = datetime .datetime .now ()
530- op_func (in_path , out_path , pwd , algo , progress )
603+
604+ for idx , in_path in enumerate (valid_paths ):
605+ # Build output path
606+ file_out_dir = out_dir if out_dir else os .path .dirname (in_path )
607+ if use_timestamp :
608+ ts = datetime .datetime .now ().strftime ("%Y%m%d_%H%M%S_%f" )
609+ out_name = ts + suffix
610+ else :
611+ base_name = os .path .basename (in_path )
612+ out_name = base_name + suffix
613+ out_path = os .path .join (file_out_dir , out_name )
614+
615+ # Update status for batch
616+ if total_files > 1 :
617+ status_text = str_file_status .format (done = idx + 1 , total = total_files )
618+ self .after (0 , lambda t = status_text : status_lbl .config (text = t , foreground = "blue" ))
619+
620+ # progress_info is written here and read on main thread via poll_progress.
621+ # Simple int assignment is atomic under CPython's GIL, so no lock needed.
622+ progress_info ["file_value" ] = 0
623+
624+ def progress (current , total , pi = progress_info ):
625+ if total > 0 :
626+ pi ["file_value" ] = int ((current / total ) * 100 )
627+
628+ op_func (in_path , out_path , pwd , algo , progress )
629+ progress_info ["file_value" ] = 100
630+ saved_paths .append (out_path )
631+ if delete_original :
632+ completed_inputs .append (in_path )
633+
531634 end_time = datetime .datetime .now ()
532-
533- self .after (0 , lambda : status_lbl .config (text = f"{ str_done } ({ end_time - start_time } )" , foreground = "green" ))
534- self .after (0 , lambda : messagebox .showinfo (f"{ op_name } { str_complete } " , f"{ str_saved } \n { out_path } " ))
535-
635+ elapsed = end_time - start_time
636+ progress_info ["done" ] = True
637+
638+ done_text = f"{ str_done } ({ elapsed } )"
639+ self .after (0 , lambda t = done_text : status_lbl .config (text = t , foreground = "green" ))
640+ saved_msg = f"{ str_saved } \n " + "\n " .join (saved_paths )
641+ self .after (0 , lambda m = saved_msg : messagebox .showinfo (f"{ op_name } { str_complete } " , m ))
642+
643+ # Prompt deletion for each completed original after all operations finish.
644+ # Must run on the main thread since it shows dialogs.
645+ def prompt_deletions (paths = completed_inputs ):
646+ for orig_path in paths :
647+ msg = str_confirm_delete_msg .format (path = orig_path )
648+ if messagebox .askyesno (str_confirm_delete_title , msg ):
649+ try :
650+ os .remove (orig_path )
651+ except Exception as del_err :
652+ err_msg = str_file_delete_failed .format (err = str (del_err ))
653+ messagebox .showwarning (str_error , err_msg )
654+
655+ if completed_inputs :
656+ self .after (0 , prompt_deletions )
657+
536658 except Exception as e :
537- self .after (0 , lambda : status_lbl .config (text = f"{ str_error } : { str (e )} " , foreground = "red" ))
538- self .after (0 , lambda : messagebox .showerror (str_error , str (e )))
659+ progress_info ["done" ] = True
660+ err_text = f"{ str_error } : { str (e )} "
661+ self .after (0 , lambda t = err_text : status_lbl .config (text = t , foreground = "red" ))
662+ self .after (0 , lambda m = str (e ): messagebox .showerror (str_error , m ))
539663
540664 threading .Thread (target = worker , daemon = True ).start ()
541665
0 commit comments