#!/usr/bin/env python3 """ ZIPName — macOS Drag-and-Drop utility to fix mojibake names in folders and ZIP archives. """ import os import sys import threading import tkinter as tk from tkinter import ttk, messagebox, filedialog try: from tkinterdnd2 import TkinterDnD, DND_FILES HAS_DND = True except ImportError: HAS_DND = False from fixer_core import process_path, fix_filename_string class ZIPNameApp: def __init__(self, root): self.root = root self.root.title("ZIPName") self.root.geometry("580x420") self.root.minsize(500, 360) # Apple styling colors self.bg_color = "#1e1e24" self.card_color = "#2b2b36" self.accent_color = "#ff2d20" self.text_color = "#ffffff" self.sub_text_color = "#9e9ea7" self.border_color = "#3d3d4e" self.root.configure(bg=self.bg_color) self.setup_ui() def setup_ui(self): # Header header_frame = tk.Frame(self.root, bg=self.bg_color) header_frame.pack(fill="x", padx=20, pady=(20, 10)) title_lbl = tk.Label( header_frame, text="ZIPName", font=("SF Pro Display", 20, "bold"), fg=self.text_color, bg=self.bg_color ) title_lbl.pack(anchor="w") sub_lbl = tk.Label( header_frame, text="Восстановление имён файлов и распаковка архивов с битой кодировкой", font=("SF Pro Text", 12), fg=self.sub_text_color, bg=self.bg_color ) sub_lbl.pack(anchor="w", pady=(2, 0)) # Drop Zone Frame self.drop_frame = tk.Frame( self.root, bg=self.card_color, highlightbackground=self.border_color, highlightthickness=2, bd=0, relief="flat" ) self.drop_frame.pack(fill="both", expand=True, padx=20, pady=10) # Drop zone contents self.drop_label_icon = tk.Label( self.drop_frame, text="📦 ➔ ✨", font=("SF Pro Display", 32), fg=self.accent_color, bg=self.card_color ) self.drop_label_icon.pack(expand=True, pady=(20, 5)) self.drop_label_main = tk.Label( self.drop_frame, text="Перетащи сюда папку, файлы или ZIP-архив", font=("SF Pro Text", 14, "bold"), fg=self.text_color, bg=self.card_color ) self.drop_label_main.pack(expand=True, pady=(0, 2)) self.drop_label_sub = tk.Label( self.drop_frame, text="или выбери вручную кнопкой ниже", font=("SF Pro Text", 11), fg=self.sub_text_color, bg=self.card_color ) self.drop_label_sub.pack(expand=True, pady=(0, 10)) # Buttons in drop frame btn_frame = tk.Frame(self.drop_frame, bg=self.card_color) btn_frame.pack(pady=(0, 20)) btn_style = { "font": ("SF Pro Text", 11, "bold"), "fg": "#ffffff", "bg": "#3e3e50", "activebackground": self.accent_color, "activeforeground": "#ffffff", "relief": "flat", "padx": 14, "pady": 6, "bd": 0, "cursor": "pointinghand" } self.btn_browse_folder = tk.Button(btn_frame, text="Выбрать папку", command=self.browse_folder, **btn_style) self.btn_browse_folder.pack(side="left", padx=6) self.btn_browse_zip = tk.Button(btn_frame, text="Выбрать ZIP", command=self.browse_zip, **btn_style) self.btn_browse_zip.pack(side="left", padx=6) # Log & Status area self.status_lbl = tk.Label( self.root, text="Готов к работе", font=("SF Pro Text", 11), fg=self.sub_text_color, bg=self.bg_color ) self.status_lbl.pack(fill="x", padx=25, pady=(4, 10)) # Register Drag and Drop if HAS_DND: self.root.drop_target_register(DND_FILES) self.root.dnd_bind('<>', self.handle_drop) self.drop_frame.drop_target_register(DND_FILES) self.drop_frame.dnd_bind('<>', self.handle_drop) for widget in (self.drop_label_icon, self.drop_label_main, self.drop_label_sub): widget.drop_target_register(DND_FILES) widget.dnd_bind('<>', self.handle_drop) def handle_drop(self, event): raw_data = event.data paths = self.parse_dnd_paths(raw_data) if paths: self.start_processing(paths) def parse_dnd_paths(self, dnd_data): """Parse macOS/TkinterDnD paths, handling curly braces and spaces.""" paths = [] cur = [] in_brace = False for char in dnd_data: if char == '{': in_brace = True elif char == '}': in_brace = False elif char == ' ' and not in_brace: if cur: paths.append("".join(cur).strip()) cur = [] else: cur.append(char) if cur: paths.append("".join(cur).strip()) return [p for p in paths if p] def browse_folder(self): selected = filedialog.askdirectory(title="Выберите папку с кракозябрами") if selected: self.start_processing([selected]) def browse_zip(self): selected = filedialog.askopenfilename( title="Выберите ZIP архив", filetypes=[("ZIP Archives", "*.zip"), ("All Files", "*.*")] ) if selected: self.start_processing([selected]) def start_processing(self, paths): # Disable buttons while running self.btn_browse_folder.config(state="disabled") self.btn_browse_zip.config(state="disabled") self.status_lbl.config(text="Обработка...", fg="#e1a100") thread = threading.Thread(target=self._worker, args=(paths,), daemon=True) thread.start() def _worker(self, paths): total_renamed = 0 total_extracted = 0 total_deleted = 0 all_results = [] for p in paths: def on_progress(old, new): self.root.after(0, lambda o=old, n=new: self.status_lbl.config( text=f"Исправлено: {n[:35]}...", fg="#4cd964" )) res = process_path(p, progress_callback=on_progress) all_results.extend(res) for action, old, new in all_results: if action in ("rename_file", "rename_dir"): total_renamed += 1 elif action == "extract_file": total_extracted += 1 elif action == "delete": total_deleted += 1 self.root.after(0, lambda: self._on_complete(total_renamed, total_extracted, total_deleted)) def _on_complete(self, renamed, extracted, deleted): self.btn_browse_folder.config(state="normal") self.btn_browse_zip.config(state="normal") msg_parts = [] if renamed > 0: msg_parts.append(f"Переименовано элементов: {renamed}") if extracted > 0: msg_parts.append(f"Распаковано файлов: {extracted}") if deleted > 0: msg_parts.append(f"Удалено мусорных temp-файлов: {deleted}") if not msg_parts: summary = "Ничего менять не потребовалось — все имена уже в порядке!" self.status_lbl.config(text=summary, fg=self.sub_text_color) messagebox.showinfo("ZIPName", summary) else: summary = "\n".join(msg_parts) self.status_lbl.config(text="Готово! " + " · ".join(msg_parts), fg="#4cd964") messagebox.showinfo("ZIPName — Готово", f"Операция успешно завершена:\n\n{summary}") def main(): if HAS_DND: root = TkinterDnD.Tk() else: root = tk.Tk() app = ZIPNameApp(root) root.mainloop() if __name__ == "__main__": main()