Skip to content

Commit 44dca15

Browse files
Merge remote changes and resolve .gitignore conflict
2 parents 1b193dd + b2c6a68 commit 44dca15

2 files changed

Lines changed: 168 additions & 44 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
venv/
44
env/
55

6-
# Python
6+
# Python cache
77
__pycache__/
88
*.pyc
99
*.pyo

main.py

Lines changed: 167 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@
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

6573
CONFIG_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

Comments
 (0)