pdf_unlock: add ui

This commit is contained in:
2025-11-01 00:12:29 +08:00
parent d454f8c8f4
commit 1e550961d2
4 changed files with 753 additions and 3 deletions

148
pdf_unlock/ui.py Normal file
View File

@ -0,0 +1,148 @@
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
from tkinterdnd2 import TkinterDnD, DND_FILES # type: ignore
DND_AVAILABLE = True
from main import copy_pdf_pages # type: ignore
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("<Button-1>", self._on_click_select)
self.label.bind("<Button-1>", self._on_click_select)
if DND_AVAILABLE:
self.drop.drop_target_register(DND_FILES) # type: ignore
self.drop.dnd_bind("<<Drop>>", 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__":
main()