|
| 1 | +import os |
| 2 | +import json |
| 3 | +import shutil |
| 4 | +import tkinter as tk |
| 5 | +from tkinter import ttk, filedialog, messagebox |
| 6 | +from tkinterdnd2 import DND_FILES, TkinterDnD |
| 7 | +import sv_ttk |
| 8 | +from threading import Thread |
| 9 | +from watchdog.observers import Observer |
| 10 | +from watchdog.events import FileSystemEventHandler |
| 11 | + |
| 12 | +# ========================= |
| 13 | +# Helpers |
| 14 | +# ========================= |
| 15 | +CONFIG_FILE = "folders_data.json" |
| 16 | + |
| 17 | +def load_folders(): |
| 18 | + if os.path.exists(CONFIG_FILE): |
| 19 | + with open(CONFIG_FILE, "r", encoding="utf-8") as f: |
| 20 | + return json.load(f) |
| 21 | + return [] |
| 22 | + |
| 23 | +def save_folders(): |
| 24 | + with open(CONFIG_FILE, "w", encoding="utf-8") as f: |
| 25 | + json.dump(folders_data, f, ensure_ascii=False, indent=4) |
| 26 | + |
| 27 | +def set_status(msg): |
| 28 | + status_var.set(msg) |
| 29 | + root.update_idletasks() |
| 30 | + |
| 31 | +def sanitize_folder_name(name): |
| 32 | + return "".join(c for c in name if c not in r'<>:"/\|?*') |
| 33 | + |
| 34 | +def format_size(size_bytes): |
| 35 | + if size_bytes < 1024: |
| 36 | + return f"{size_bytes} B" |
| 37 | + elif size_bytes < 1024**2: |
| 38 | + return f"{size_bytes/1024:.2f} KB" |
| 39 | + elif size_bytes < 1024**3: |
| 40 | + return f"{size_bytes/1024**2:.2f} MB" |
| 41 | + else: |
| 42 | + return f"{size_bytes/1024**3:.2f} GB" |
| 43 | + |
| 44 | +def categorize_file(size): |
| 45 | + if size < 1024*1024: |
| 46 | + return "Small_1MB" |
| 47 | + elif size < 10*1024*1024: |
| 48 | + return "Medium_1-10MB" |
| 49 | + else: |
| 50 | + return "Large_10MB_plus" |
| 51 | + |
| 52 | +# ========================= |
| 53 | +# App Setup |
| 54 | +# ========================= |
| 55 | +root = TkinterDnD.Tk() |
| 56 | +root.title("📂 File Size Organizer Pro - Filter & Sort") |
| 57 | +root.geometry("1300x620") |
| 58 | + |
| 59 | +# ========================= |
| 60 | +# Globals |
| 61 | +# ========================= |
| 62 | +folders_data = load_folders() |
| 63 | +last_operation = [] |
| 64 | +combined_files = [] |
| 65 | +filtered_files = [] |
| 66 | +observer = None |
| 67 | +current_filter = tk.StringVar(value="All") |
| 68 | +current_sort = tk.StringVar(value="Name") |
| 69 | + |
| 70 | +# ========================= |
| 71 | +# Folder & File Functions |
| 72 | +# ========================= |
| 73 | +def add_folder(path): |
| 74 | + path = path.strip() |
| 75 | + |
| 76 | + # 1️⃣ Empty validation |
| 77 | + if not path: |
| 78 | + messagebox.showwarning("Invalid Folder", "Please select or enter a folder path.") |
| 79 | + return |
| 80 | + |
| 81 | + path = os.path.abspath(path) |
| 82 | + |
| 83 | + # 2️⃣ Directory validation |
| 84 | + if not os.path.isdir(path): |
| 85 | + messagebox.showwarning("Invalid Folder", "The selected path is not a valid folder.") |
| 86 | + return |
| 87 | + |
| 88 | + # 3️⃣ Duplicate validation |
| 89 | + for folder in folders_data: |
| 90 | + if os.path.abspath(folder) == path: |
| 91 | + messagebox.showinfo("Already Added", "This folder is already being monitored.") |
| 92 | + return |
| 93 | + |
| 94 | + # 4️⃣ Add folder |
| 95 | + folders_data.append(path) |
| 96 | + save_folders() |
| 97 | + start_watcher(path) |
| 98 | + refresh_combined_preview() |
| 99 | + set_status(f"Added folder: {path}") |
| 100 | + |
| 101 | +def remove_folder(path): |
| 102 | + path = os.path.abspath(path.strip()) |
| 103 | + |
| 104 | + for folder in folders_data: |
| 105 | + if os.path.abspath(folder) == path: |
| 106 | + folders_data.remove(folder) |
| 107 | + save_folders() |
| 108 | + refresh_combined_preview() |
| 109 | + set_status(f"Removed folder: {path}") |
| 110 | + return |
| 111 | + |
| 112 | + messagebox.showwarning("Not Found", "Folder not found in the list.") |
| 113 | + |
| 114 | +def scan_folder(folder_path): |
| 115 | + folder_path = os.path.abspath(folder_path) |
| 116 | + files = [] |
| 117 | + if not os.path.exists(folder_path): |
| 118 | + return files |
| 119 | + |
| 120 | + for f in os.listdir(folder_path): |
| 121 | + path = os.path.join(folder_path, f) |
| 122 | + if os.path.isfile(path): |
| 123 | + try: |
| 124 | + size = os.path.getsize(path) |
| 125 | + except (FileNotFoundError, PermissionError): |
| 126 | + continue # file moved or locked, safely skip |
| 127 | + |
| 128 | + category = sanitize_folder_name(categorize_file(size)) |
| 129 | + files.append((folder_path, f, size, category)) |
| 130 | + |
| 131 | + return files |
| 132 | + |
| 133 | +def refresh_combined_preview(): |
| 134 | + global combined_files |
| 135 | + combined_files = [] |
| 136 | + for folder in folders_data: |
| 137 | + combined_files.extend(scan_folder(folder)) |
| 138 | + apply_filter_sort() |
| 139 | + |
| 140 | +# ========================= |
| 141 | +# Filter & Sort Functions |
| 142 | +# ========================= |
| 143 | +def apply_filter_sort(): |
| 144 | + global filtered_files |
| 145 | + # Filter |
| 146 | + if current_filter.get() == "All": |
| 147 | + filtered_files = combined_files.copy() |
| 148 | + else: |
| 149 | + filtered_files = [f for f in combined_files if f[3] == current_filter.get()] |
| 150 | + # Sort |
| 151 | + sort_key = current_sort.get() |
| 152 | + if sort_key == "Name": |
| 153 | + filtered_files.sort(key=lambda x: x[1].lower()) |
| 154 | + elif sort_key == "Size": |
| 155 | + filtered_files.sort(key=lambda x: x[2]) |
| 156 | + elif sort_key == "Folder": |
| 157 | + filtered_files.sort(key=lambda x: x[0].lower()) |
| 158 | + elif sort_key == "Category": |
| 159 | + filtered_files.sort(key=lambda x: x[3].lower()) |
| 160 | + update_file_tree() |
| 161 | + |
| 162 | +def update_file_tree(): |
| 163 | + file_tree.delete(*file_tree.get_children()) |
| 164 | + counts = {"Small_1MB":0, "Medium_1-10MB":0, "Large_10MB_plus":0} |
| 165 | + for folder, f, size, category in filtered_files: |
| 166 | + file_tree.insert("", "end", values=(folder, f, format_size(size), category)) |
| 167 | + if category in counts: |
| 168 | + counts[category] +=1 |
| 169 | + total_files_var.set(f"Total Files: {len(filtered_files)} | Small: {counts['Small_1MB']} | Medium: {counts['Medium_1-10MB']} | Large: {counts['Large_10MB_plus']}") |
| 170 | + |
| 171 | +# ========================= |
| 172 | +# File Operations |
| 173 | +# ========================= |
| 174 | +def organize_files_thread(): |
| 175 | + global watcher_paused |
| 176 | + watcher_paused = True |
| 177 | + progress_var.set(0) |
| 178 | + |
| 179 | + if not combined_files: |
| 180 | + watcher_paused = False |
| 181 | + return |
| 182 | + |
| 183 | + global last_operation |
| 184 | + last_operation = [] |
| 185 | + total = len(combined_files) |
| 186 | + |
| 187 | + for idx, (folder, f, size, category) in enumerate(combined_files, start=1): |
| 188 | + src = os.path.join(folder, f) |
| 189 | + dst_folder = os.path.join(folder, category) |
| 190 | + os.makedirs(dst_folder, exist_ok=True) |
| 191 | + dst = os.path.join(dst_folder, f) |
| 192 | + |
| 193 | + try: |
| 194 | + shutil.move(src, dst) |
| 195 | + last_operation.append((dst, src)) |
| 196 | + except Exception: |
| 197 | + pass |
| 198 | + |
| 199 | + progress_var.set(int(idx / total * 100)) |
| 200 | + root.update_idletasks() |
| 201 | + |
| 202 | + watcher_paused = False |
| 203 | + refresh_combined_preview() |
| 204 | + set_status("Files organized successfully!") |
| 205 | + |
| 206 | +def organize_files(): |
| 207 | + Thread(target=organize_files_thread, daemon=True).start() |
| 208 | + |
| 209 | +def undo_last_operation(): |
| 210 | + if not last_operation: |
| 211 | + messagebox.showinfo("Nothing to undo", "No operation to undo.") |
| 212 | + return |
| 213 | + for dst, src in reversed(last_operation): |
| 214 | + if os.path.exists(dst): |
| 215 | + shutil.move(dst, src) |
| 216 | + last_operation.clear() |
| 217 | + refresh_combined_preview() |
| 218 | + set_status("Undo completed!") |
| 219 | + |
| 220 | +# ========================= |
| 221 | +# Drag & Drop |
| 222 | +# ========================= |
| 223 | +def drop(event): |
| 224 | + paths = root.tk.splitlist(event.data) |
| 225 | + for path in paths: |
| 226 | + if os.path.isdir(path): |
| 227 | + add_folder(path) |
| 228 | + |
| 229 | +# ========================= |
| 230 | +# Watchdog Handler |
| 231 | +# ========================= |
| 232 | +class FolderEventHandler(FileSystemEventHandler): |
| 233 | + def on_any_event(self, event): |
| 234 | + if watcher_paused: |
| 235 | + return |
| 236 | + root.after(200, refresh_combined_preview) |
| 237 | + |
| 238 | +def start_watcher(folder_path): |
| 239 | + global observer |
| 240 | + if observer is None: |
| 241 | + observer = Observer() |
| 242 | + observer.start() |
| 243 | + handler = FolderEventHandler() |
| 244 | + observer.schedule(handler, folder_path, recursive=True) |
| 245 | + |
| 246 | +# ========================= |
| 247 | +# GUI Setup |
| 248 | +# ========================= |
| 249 | +main_frame = ttk.Frame(root, padding=20) |
| 250 | +main_frame.pack(expand=True, fill="both") |
| 251 | + |
| 252 | +ttk.Label(main_frame, text="📂 File Size Organizer Pro - Filter & Sort", font=("Segoe UI", 22, "bold")).pack(pady=(0,5)) |
| 253 | +ttk.Label(main_frame, text="Drag folders or add manually. Files auto-update in real-time.", font=("Segoe UI", 12)).pack(pady=(0,10)) |
| 254 | + |
| 255 | +# Folder selection and management |
| 256 | +folder_frame = ttk.LabelFrame(main_frame, text="Folders", padding=10) |
| 257 | +folder_frame.pack(fill="x", pady=5) |
| 258 | + |
| 259 | +folder_entry = tk.Entry(folder_frame, width=80) |
| 260 | +folder_entry.grid(row=0, column=0, padx=5) |
| 261 | + |
| 262 | +def browse_folder(): |
| 263 | + path = filedialog.askdirectory() |
| 264 | + if path: |
| 265 | + folder_entry.delete(0, tk.END) |
| 266 | + folder_entry.insert(0, path) |
| 267 | + |
| 268 | +ttk.Button(folder_frame, text="Browse", command=browse_folder).grid(row=0, column=1, padx=5) |
| 269 | +ttk.Button(folder_frame, text="Add Folder", style="Manage.TButton", command=lambda:add_folder(folder_entry.get().strip())).grid(row=0, column=2, padx=5) |
| 270 | +ttk.Button(folder_frame, text="Remove Folder", style="Undo.TButton", command=lambda: remove_folder(folder_entry.get().strip())).grid(row=0, column=3, padx=5) |
| 271 | + |
| 272 | +# Buttons styles |
| 273 | +ttk.Style().configure("Organize.TButton", foreground="black", background="#4CAF50") |
| 274 | +ttk.Style().configure("Undo.TButton", foreground="black", background="#FF9800") |
| 275 | +ttk.Style().configure("Manage.TButton", foreground="black", background="#2196F3") |
| 276 | + |
| 277 | +ttk.Button(folder_frame, text="📂 Organize Files", style="Organize.TButton", command=organize_files).grid(row=0, column=4, padx=5) |
| 278 | +ttk.Button(folder_frame, text="↩️ Undo Last", style="Undo.TButton", command=undo_last_operation).grid(row=0, column=5, padx=5) |
| 279 | + |
| 280 | +# Filter & Sort Panel |
| 281 | +filter_sort_frame = ttk.LabelFrame(main_frame, text="Filter & Sort", padding=10) |
| 282 | +filter_sort_frame.pack(fill="x", pady=5) |
| 283 | + |
| 284 | +ttk.Label(filter_sort_frame, text="Filter by Category:").grid(row=0, column=0, padx=5) |
| 285 | +filter_combo = ttk.Combobox(filter_sort_frame, state="readonly", width=20, textvariable=current_filter) |
| 286 | +filter_combo['values'] = ["All", "Small_1MB", "Medium_1-10MB", "Large_10MB_plus"] |
| 287 | +filter_combo.current(0) |
| 288 | +filter_combo.grid(row=0, column=1, padx=5) |
| 289 | +filter_combo.bind("<<ComboboxSelected>>", lambda e: apply_filter_sort()) |
| 290 | + |
| 291 | +ttk.Label(filter_sort_frame, text="Sort by:").grid(row=0, column=2, padx=5) |
| 292 | +sort_combo = ttk.Combobox(filter_sort_frame, state="readonly", width=20, textvariable=current_sort) |
| 293 | +sort_combo['values'] = ["Name", "Size", "Folder", "Category"] |
| 294 | +sort_combo.current(0) |
| 295 | +sort_combo.grid(row=0, column=3, padx=5) |
| 296 | +sort_combo.bind("<<ComboboxSelected>>", lambda e: apply_filter_sort()) |
| 297 | + |
| 298 | +# File preview Treeview |
| 299 | +tree_frame = ttk.Frame(main_frame) |
| 300 | +tree_frame.pack(expand=True, fill="both", pady=10) |
| 301 | +columns = ("folder", "name", "size", "category") |
| 302 | +file_tree = ttk.Treeview(tree_frame, columns=columns, show="headings") |
| 303 | +file_tree.heading("folder", text="Folder") |
| 304 | +file_tree.heading("name", text="File Name") |
| 305 | +file_tree.heading("size", text="Size") |
| 306 | +file_tree.heading("category", text="Category") |
| 307 | +file_tree.column("folder", width=300) |
| 308 | +file_tree.column("name", width=400) |
| 309 | +file_tree.column("size", width=100) |
| 310 | +file_tree.column("category", width=150) |
| 311 | +file_tree.pack(expand=True, fill="both", side="left") |
| 312 | + |
| 313 | +scrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=file_tree.yview) |
| 314 | +scrollbar.pack(side="right", fill="y") |
| 315 | +file_tree.configure(yscrollcommand=scrollbar.set) |
| 316 | + |
| 317 | +# File counts |
| 318 | +total_files_var = tk.StringVar(value="Total Files: 0 | Small: 0 | Medium: 0 | Large: 0") |
| 319 | +ttk.Label(main_frame, textvariable=total_files_var, font=("Segoe UI", 12)).pack() |
| 320 | + |
| 321 | +# Progress bar |
| 322 | +progress_var = tk.IntVar() |
| 323 | +progress_bar = ttk.Progressbar(main_frame, orient="horizontal", mode="determinate", maximum=100, variable=progress_var) |
| 324 | +progress_bar.pack(fill="x", pady=5) |
| 325 | + |
| 326 | +# Drag & Drop binding |
| 327 | +root.drop_target_register(DND_FILES) |
| 328 | +root.dnd_bind('<<Drop>>', drop) |
| 329 | + |
| 330 | +# Status Bar |
| 331 | +status_var = tk.StringVar(value="Ready") |
| 332 | +ttk.Label(root, textvariable=status_var, anchor="w").pack(side="bottom", fill="x") |
| 333 | + |
| 334 | +# Initial load and start watchers |
| 335 | +for folder in folders_data: |
| 336 | + start_watcher(folder) |
| 337 | +refresh_combined_preview() |
| 338 | + |
| 339 | +# Run App |
| 340 | +try: |
| 341 | + root.mainloop() |
| 342 | +finally: |
| 343 | + if observer: |
| 344 | + observer.stop() |
| 345 | + observer.join() |
0 commit comments