|
| 1 | +# ========================================================== |
| 2 | +# Document Classifier PRO |
| 3 | +# Professional Desktop Tool |
| 4 | +# ========================================================== |
| 5 | + |
| 6 | +import os |
| 7 | +import sys |
| 8 | +import threading |
| 9 | +import time |
| 10 | +import traceback |
| 11 | +from queue import Queue, Empty |
| 12 | +from tkinter import filedialog, messagebox |
| 13 | +import tkinter as tk |
| 14 | + |
| 15 | +import ttkbootstrap as tb |
| 16 | +from ttkbootstrap.constants import * |
| 17 | +from tkinterdnd2 import DND_FILES, TkinterDnD |
| 18 | + |
| 19 | + |
| 20 | +# =================== APP CONFIG =================== |
| 21 | + |
| 22 | +APP_NAME = "Document Classifier" |
| 23 | +APP_VERSION = "1.0.0" |
| 24 | + |
| 25 | + |
| 26 | +# =================== APP =================== |
| 27 | + |
| 28 | +app = TkinterDnD.Tk() |
| 29 | +app.title(f"{APP_NAME} {APP_VERSION}") |
| 30 | +app.geometry("1120x650") |
| 31 | + |
| 32 | +tb.Style("darkly") |
| 33 | + |
| 34 | + |
| 35 | +# =================== UTILITY =================== |
| 36 | + |
| 37 | +def resource_path(file_name): |
| 38 | + base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))) |
| 39 | + return os.path.join(base_path, file_name) |
| 40 | + |
| 41 | + |
| 42 | +def log_error(): |
| 43 | + with open("error.log", "a", encoding="utf-8") as f: |
| 44 | + f.write(traceback.format_exc() + "\n") |
| 45 | + |
| 46 | + |
| 47 | +def show_about(): |
| 48 | + messagebox.showinfo( |
| 49 | + f"About {APP_NAME}", |
| 50 | + f"{APP_NAME} v{APP_VERSION}\n\n" |
| 51 | + "Professional Document Classification Tool\n\n" |
| 52 | + "Features:\n" |
| 53 | + "• Drag & Drop files or folders\n" |
| 54 | + "• Folder scanning\n" |
| 55 | + "• Classify documents by content\n" |
| 56 | + "• Output results to folder\n" |
| 57 | + "• Pause / Stop processing\n" |
| 58 | + "• Live progress tracking\n" |
| 59 | + "• Detailed logging\n\n" |
| 60 | + "Built with Python + Tkinter + ttkbootstrap\n" |
| 61 | + "© 2026 Mate Technologies\n" |
| 62 | + "https://matetools.gumroad.com" |
| 63 | + ) |
| 64 | + |
| 65 | + |
| 66 | +try: |
| 67 | + app.iconbitmap(resource_path("logo.ico")) |
| 68 | +except: |
| 69 | + pass |
| 70 | + |
| 71 | + |
| 72 | +# =================== MENU =================== |
| 73 | + |
| 74 | +menubar = tb.Menu(app) |
| 75 | +help_menu = tb.Menu(menubar, tearoff=0) |
| 76 | +help_menu.add_command(label="About", command=show_about) |
| 77 | +menubar.add_cascade(label="Help", menu=help_menu) |
| 78 | +app.config(menu=menubar) |
| 79 | + |
| 80 | + |
| 81 | +# =================== FLAGS =================== |
| 82 | + |
| 83 | +stop_flag = False |
| 84 | +pause_flag = False |
| 85 | +ui_queue = Queue() |
| 86 | + |
| 87 | +file_list = [] |
| 88 | + |
| 89 | +output_path = tb.StringVar() |
| 90 | + |
| 91 | + |
| 92 | +# =================== TITLE =================== |
| 93 | + |
| 94 | +tb.Label( |
| 95 | + app, |
| 96 | + text=APP_NAME, |
| 97 | + font=("Segoe UI", 24, "bold") |
| 98 | +).pack(pady=(10, 2)) |
| 99 | + |
| 100 | +tb.Label( |
| 101 | + app, |
| 102 | + text="Professional Document Classification – AI & Keyword Based", |
| 103 | + font=("Segoe UI", 10, "italic"), |
| 104 | + foreground="#9ca3af" |
| 105 | +).pack(pady=(0, 10)) |
| 106 | + |
| 107 | + |
| 108 | +# =================== FRAME: FILE SELECTION =================== |
| 109 | + |
| 110 | +frame1 = tb.Labelframe(app, text="Files & Folders", padding=10) |
| 111 | +frame1.pack(fill="x", padx=10, pady=6) |
| 112 | + |
| 113 | +file_frame = tb.Frame(frame1) |
| 114 | +file_frame.pack(fill="x", pady=6) |
| 115 | + |
| 116 | +file_listbox = tk.Listbox(file_frame, height=7, selectmode="extended") |
| 117 | +file_listbox.pack(side="left", fill="x", expand=True) |
| 118 | + |
| 119 | +scroll = tb.Scrollbar(file_frame, command=file_listbox.yview) |
| 120 | +scroll.pack(side="right", fill="y") |
| 121 | + |
| 122 | +file_listbox.config(yscrollcommand=scroll.set) |
| 123 | + |
| 124 | + |
| 125 | +# =================== FILE FUNCTIONS =================== |
| 126 | + |
| 127 | +def add_files(): |
| 128 | + files = filedialog.askopenfilenames(title="Select Documents") |
| 129 | + for f in files: |
| 130 | + if f not in file_list: |
| 131 | + file_list.append(f) |
| 132 | + ui_queue.put(("add", f)) |
| 133 | + |
| 134 | + |
| 135 | +def add_folder(): |
| 136 | + folder = filedialog.askdirectory(title="Select Folder") |
| 137 | + if not folder: |
| 138 | + return |
| 139 | + for root, dirs, files in os.walk(folder): |
| 140 | + for name in files: |
| 141 | + path = os.path.join(root, name) |
| 142 | + if path not in file_list: |
| 143 | + file_list.append(path) |
| 144 | + ui_queue.put(("add", path)) |
| 145 | + |
| 146 | + |
| 147 | +def clear_list(): |
| 148 | + file_list.clear() |
| 149 | + ui_queue.put(("clear", None)) |
| 150 | + |
| 151 | + |
| 152 | +def set_output_folder(): |
| 153 | + folder = filedialog.askdirectory() |
| 154 | + if folder: |
| 155 | + output_path.set(folder) |
| 156 | + |
| 157 | + |
| 158 | +# =================== DOCUMENT CLASSIFIER LOGIC =================== |
| 159 | + |
| 160 | +def classify_document(file_path): |
| 161 | + """Simple keyword-based classification placeholder""" |
| 162 | + try: |
| 163 | + with open(file_path, "r", encoding="utf-8", errors="ignore") as f: |
| 164 | + text = f.read().lower() |
| 165 | + if "invoice" in text: |
| 166 | + return "Invoices" |
| 167 | + elif "report" in text: |
| 168 | + return "Reports" |
| 169 | + elif "resume" in text or "cv" in text: |
| 170 | + return "Resumes" |
| 171 | + else: |
| 172 | + return "Others" |
| 173 | + except Exception: |
| 174 | + return "Unknown" |
| 175 | + |
| 176 | + |
| 177 | +# =================== PROCESSING =================== |
| 178 | + |
| 179 | +def process_documents(): |
| 180 | + global stop_flag, pause_flag |
| 181 | + stop_flag = False |
| 182 | + pause_flag = False |
| 183 | + |
| 184 | + classify_btn.config(state="disabled") |
| 185 | + pause_btn.config(state="normal") |
| 186 | + stop_btn.config(state="normal") |
| 187 | + |
| 188 | + total = len(file_list) |
| 189 | + if total == 0: |
| 190 | + messagebox.showerror("Error", "No files selected.") |
| 191 | + classify_btn.config(state="normal") |
| 192 | + pause_btn.config(state="disabled") |
| 193 | + stop_btn.config(state="disabled") |
| 194 | + return |
| 195 | + |
| 196 | + out_dir = output_path.get() or os.path.dirname(file_list[0]) |
| 197 | + ui_queue.put(("log", f"Starting classification of {total} files...")) |
| 198 | + |
| 199 | + for idx, file in enumerate(file_list, 1): |
| 200 | + if stop_flag: |
| 201 | + ui_queue.put(("log", "Process stopped by user.")) |
| 202 | + break |
| 203 | + |
| 204 | + while pause_flag: |
| 205 | + time.sleep(0.2) |
| 206 | + |
| 207 | + try: |
| 208 | + category = classify_document(file) |
| 209 | + dest_dir = os.path.join(out_dir, category) |
| 210 | + os.makedirs(dest_dir, exist_ok=True) |
| 211 | + dest = os.path.join(dest_dir, os.path.basename(file)) |
| 212 | + # Copy file to classified folder |
| 213 | + import shutil |
| 214 | + shutil.copy2(file, dest) |
| 215 | + ui_queue.put(("log", f"✔ {os.path.basename(file)} -> {category}")) |
| 216 | + except Exception: |
| 217 | + log_error() |
| 218 | + ui_queue.put(("log", f"❌ Failed: {file}")) |
| 219 | + |
| 220 | + percent = int((idx / total) * 100) |
| 221 | + ui_queue.put(("progress", percent)) |
| 222 | + |
| 223 | + ui_queue.put(("complete", "Classification finished.")) |
| 224 | + |
| 225 | + |
| 226 | +# =================== CONTROL BUTTONS =================== |
| 227 | + |
| 228 | +tb.Button(frame1, text="Add Files", command=add_files, bootstyle="success").pack(side="left", padx=4) |
| 229 | +tb.Button(frame1, text="Add Folder", command=add_folder, bootstyle="info").pack(side="left", padx=4) |
| 230 | +tb.Button(frame1, text="Clear List", command=clear_list, bootstyle="danger-outline").pack(side="left", padx=4) |
| 231 | + |
| 232 | +tb.Label(frame1, text="Output Folder:", width=13).pack(side="left", padx=(12, 0)) |
| 233 | +tb.Entry(frame1, textvariable=output_path, width=40).pack(side="left", padx=6) |
| 234 | +tb.Button(frame1, text="Browse", command=set_output_folder).pack(side="left", padx=4) |
| 235 | + |
| 236 | +classify_btn = tb.Button(frame1, text="📂 Classify", bootstyle="success") |
| 237 | +pause_btn = tb.Button(frame1, text="⏸ Pause", bootstyle="warning-outline", state="disabled") |
| 238 | +stop_btn = tb.Button(frame1, text="🛑 Stop", bootstyle="danger-outline", state="disabled") |
| 239 | + |
| 240 | +classify_btn.pack(side="left", padx=6) |
| 241 | +pause_btn.pack(side="left", padx=4) |
| 242 | +stop_btn.pack(side="left", padx=4) |
| 243 | + |
| 244 | + |
| 245 | +# =================== PROGRESS =================== |
| 246 | + |
| 247 | +frame2 = tb.Labelframe(app, text="Progress", padding=8) |
| 248 | +frame2.pack(fill="x", padx=10) |
| 249 | + |
| 250 | +progress_var = tb.IntVar() |
| 251 | + |
| 252 | +tb.Progressbar( |
| 253 | + frame2, |
| 254 | + variable=progress_var, |
| 255 | + maximum=100, |
| 256 | + length=500 |
| 257 | +).pack(side="left", padx=10) |
| 258 | + |
| 259 | +status_lbl = tb.Label(frame2, text="Status: Ready") |
| 260 | +status_lbl.pack(side="left", padx=10) |
| 261 | + |
| 262 | + |
| 263 | +# =================== LOG =================== |
| 264 | + |
| 265 | +frame3 = tb.Labelframe(app, text="Processing Log", padding=8) |
| 266 | +frame3.pack(fill="both", expand=True, padx=10, pady=6) |
| 267 | + |
| 268 | +log_text = tk.Text(frame3, height=10) |
| 269 | +log_text.pack(side="left", fill="both", expand=True) |
| 270 | + |
| 271 | +log_scroll = tk.Scrollbar(frame3, command=log_text.yview) |
| 272 | +log_scroll.pack(side="right", fill="y") |
| 273 | + |
| 274 | +log_text.config(yscrollcommand=log_scroll.set, state="disabled") |
| 275 | + |
| 276 | + |
| 277 | +# =================== UI QUEUE =================== |
| 278 | + |
| 279 | +def process_ui_queue(): |
| 280 | + try: |
| 281 | + while True: |
| 282 | + cmd, data = ui_queue.get_nowait() |
| 283 | + if cmd == "add": |
| 284 | + file_listbox.insert("end", data) |
| 285 | + elif cmd == "clear": |
| 286 | + file_listbox.delete(0, "end") |
| 287 | + elif cmd == "progress": |
| 288 | + progress_var.set(data) |
| 289 | + elif cmd == "log": |
| 290 | + log_text.config(state="normal") |
| 291 | + log_text.insert("end", data + "\n") |
| 292 | + log_text.see("end") |
| 293 | + log_text.config(state="disabled") |
| 294 | + elif cmd == "complete": |
| 295 | + progress_var.set(100) |
| 296 | + status_lbl.config(text=f"Status: {data}") |
| 297 | + classify_btn.config(state="normal") |
| 298 | + pause_btn.config(state="disabled") |
| 299 | + stop_btn.config(state="disabled") |
| 300 | + except Empty: |
| 301 | + pass |
| 302 | + app.after(100, process_ui_queue) |
| 303 | + |
| 304 | + |
| 305 | +# =================== BUTTON COMMANDS =================== |
| 306 | + |
| 307 | +def toggle_pause(): |
| 308 | + global pause_flag |
| 309 | + pause_flag = not pause_flag |
| 310 | + pause_btn.config(text="▶ Resume" if pause_flag else "⏸ Pause") |
| 311 | + |
| 312 | + |
| 313 | +def stop_process(): |
| 314 | + global stop_flag |
| 315 | + stop_flag = True |
| 316 | + status_lbl.config(text="Status: Stopping...") |
| 317 | + |
| 318 | + |
| 319 | +classify_btn.config( |
| 320 | + command=lambda: threading.Thread(target=process_documents, daemon=True).start() |
| 321 | +) |
| 322 | +pause_btn.config(command=toggle_pause) |
| 323 | +stop_btn.config(command=stop_process) |
| 324 | + |
| 325 | + |
| 326 | +# =================== DRAG & DROP =================== |
| 327 | + |
| 328 | +def drop(event): |
| 329 | + files = app.tk.splitlist(event.data) |
| 330 | + for f in files: |
| 331 | + if os.path.isfile(f): |
| 332 | + if f not in file_list: |
| 333 | + file_list.append(f) |
| 334 | + ui_queue.put(("add", f)) |
| 335 | + elif os.path.isdir(f): |
| 336 | + for root, dirs, names in os.walk(f): |
| 337 | + for name in names: |
| 338 | + path = os.path.join(root, name) |
| 339 | + if path not in file_list: |
| 340 | + file_list.append(path) |
| 341 | + ui_queue.put(("add", path)) |
| 342 | + |
| 343 | + |
| 344 | +file_listbox.drop_target_register(DND_FILES) |
| 345 | +file_listbox.dnd_bind("<<Drop>>", drop) |
| 346 | + |
| 347 | + |
| 348 | +# =================== START UI =================== |
| 349 | + |
| 350 | +app.after(100, process_ui_queue) |
| 351 | +app.mainloop() |
0 commit comments