from __future__ import annotations from pathlib import Path import threading import traceback import tkinter as tk from tkinter import filedialog, messagebox import customtkinter as ctk import sys from tkinterdnd2 import TkinterDnD, DND_FILES # type: ignore DND_AVAILABLE = True from main import copy_pdf_pages # type: ignore from main import main as dummy_main # to avoid linter error APP_TITLE = "PDF 解锁(拖入即可)" SUFFIX = "_decrypt" def parse_dropped_paths(tcl_list: str, tk_root: tk.Misc) -> list[Path]: """将 DND 的 raw 字符串解析为 Path 列表(兼容空格/花括号)。""" return [Path(p) for p in tk_root.tk.splitlist(tcl_list)] def gather_pdfs(paths: list[Path]) -> list[Path]: """根据传入路径自动识别:单文件 / 多文件 / 目录(递归),提取所有 PDF。""" out: list[Path] = [] for p in paths: if p.is_dir(): out.extend([f for f in p.rglob("*.pdf") if f.is_file()]) elif p.is_file() and p.suffix.lower() == ".pdf": out.append(p) # 去重并按路径排序 uniq = sorted({f.resolve() for f in out}) return list(uniq) def output_path_for(in_path: Path) -> Path: return in_path.with_name(f"{in_path.stem}{SUFFIX}.pdf") class App: def __init__(self, root: tk.Tk): self.root = root self.root.title(APP_TITLE) self.root.geometry("720x360") ctk.set_appearance_mode("System") ctk.set_default_color_theme("blue") # ---- 单一可点击/可拖拽区域 ---- self.drop = ctk.CTkFrame(self.root, height=240, corner_radius=12) self.drop.pack(fill="both", expand=True, padx=20, pady=20) self.label = ctk.CTkLabel( self.drop, text=( "将 PDF 文件或文件夹拖入此区域即可开始解锁\n" "输出在原文件同目录,文件名加上 _decrypt 后缀" + ("(拖拽可用)") ), font=("微软雅黑", 16), justify="center", ) self.label.place(relx=0.5, rely=0.5, anchor="center") # 点击同样可选择(依然只有这一个控件) self.drop.bind("", self._on_click_select) self.label.bind("", self._on_click_select) if DND_AVAILABLE: self.drop.drop_target_register(DND_FILES) # type: ignore self.drop.dnd_bind("<>", self._on_drop) # type: ignore # ---- 事件 ---- def _on_click_select(self, _evt=None): # 仅一个简单文件选择器;若想选目录,可直接把目录拖进来 files = filedialog.askopenfilenames( title="选择 PDF 文件(可多选)", filetypes=[("PDF 文件", "*.pdf"), ("所有文件", "*.*")], ) if files: self._start_process([Path(f) for f in files]) def _on_drop(self, event): try: paths = parse_dropped_paths(event.data, self.root) except Exception: return self._start_process(paths) # ---- 核心处理 ---- def _start_process(self, raw_paths: list[Path]): if copy_pdf_pages is None: messagebox.showerror( "错误", "未能从 main.py 导入 copy_pdf_pages(input_path: Path, output_path: Path) -> bool", ) return pdfs = gather_pdfs(raw_paths) if not pdfs: messagebox.showwarning("提示", "未找到任何 PDF 文件。") return # 后台线程,避免 UI 卡死 self.label.configure(text=f"发现 {len(pdfs)} 个 PDF,开始处理…") t = threading.Thread(target=self._worker, args=(pdfs,), daemon=True) t.start() def _worker(self, pdfs: list[Path]): ok, fail = 0, 0 errors: list[str] = [] for f in pdfs: try: out_path = output_path_for(f) # 简化:若已存在,直接覆盖 out_path.parent.mkdir(parents=True, exist_ok=True) success = bool(copy_pdf_pages(f, out_path)) # type: ignore if success: ok += 1 else: fail += 1 except Exception as e: fail += 1 errors.append(f"{f}: {e}{traceback.format_exc()}") summary = f"完成:成功 {ok},失败 {fail}。输出文件位于各自原目录。" self._set_status(summary) if errors: # 仅在有错误时弹出详情 messagebox.showerror("部分失败", summary + "\n" + "\n".join(errors[:3])) else: messagebox.showinfo("完成", summary) def _set_status(self, text: str): self.label.configure(text=text) def main(): root: tk.Misc if DND_AVAILABLE: root = TkinterDnD.Tk() # type: ignore else: root = tk.Tk() App(root) root.mainloop() if __name__ == "__main__": if len(sys.argv)>=2: dummy_main() else: main()