249 lines
8.4 KiB
Python
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()
|