Files
macZIPfixer/app.py
T

249 lines
8.4 KiB
Python

#!/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('<<Drop>>', self.handle_drop)
self.drop_frame.drop_target_register(DND_FILES)
self.drop_frame.dnd_bind('<<Drop>>', 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('<<Drop>>', 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()