From d17ea24fa560bc338bbd972713b192df7a9ead10 Mon Sep 17 00:00:00 2001 From: borderban Date: Sun, 3 May 2026 22:25:02 +0500 Subject: [PATCH] =?UTF-8?q?=D0=9D=D0=B5=D0=B1=D0=BE=D0=BB=D1=8C=D1=88?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 110 +++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 85 insertions(+), 25 deletions(-) diff --git a/main.py b/main.py index e45919b..41edd68 100644 --- a/main.py +++ b/main.py @@ -6,11 +6,22 @@ import tkinter as tk from tkinter import filedialog, messagebox, ttk from pathlib import Path import threading +import subprocess +import re # --- КОНФИГУРАЦИЯ --- SERVER_URL = "https://server1.borderban.ru/mods/" CONFIG_FILE = Path.home() / ".factorio_sync_config.json" +def get_mod_base_name(filename): + # Пытаемся отсечь версию мода (обычно ИмяМода_1.0.0.zip) + match = re.match(r"^(.+)_(\d+\.\d+\.\d+)\.zip$", filename) + if match: + return match.group(1) + if "_" in filename and filename.endswith(".zip"): + return filename.rsplit("_", 1)[0] + return filename + def get_file_hash(filepath): sha256_hash = hashlib.sha256() with open(filepath, "rb") as f: @@ -22,28 +33,45 @@ class SyncApp: def __init__(self, root): self.root = root self.root.title("Factorio Mod Sync") - self.root.geometry("500x250") + self.root.geometry("500x350") self.config = self.load_config() self.setup_ui() def load_config(self): if CONFIG_FILE.exists(): - return json.loads(CONFIG_FILE.read_text()) - return {"mods_path": ""} + data = json.loads(CONFIG_FILE.read_text()) + return { + "mods_path": data.get("mods_path", ""), + "game_path": data.get("game_path", "") + } + return {"mods_path": "", "game_path": ""} - def save_config(self, mods_path): - CONFIG_FILE.write_text(json.dumps({"mods_path": mods_path})) + def save_config(self, mods_path=None, game_path=None): + config = self.load_config() + if mods_path is not None: + config["mods_path"] = mods_path + if game_path is not None: + config["game_path"] = game_path + CONFIG_FILE.write_text(json.dumps(config)) def setup_ui(self): # Выбор пути tk.Label(self.root, text="Путь к папке mods:").pack(pady=(10, 0)) self.path_var = tk.StringVar(value=self.config["mods_path"]) entry_frame = tk.Frame(self.root) - entry_frame.pack(fill="x", padx=20, pady=5) + entry_frame.pack(fill="x", padx=20, pady=2) tk.Entry(entry_frame, textvariable=self.path_var).pack(side="left", fill="x", expand=True) tk.Button(entry_frame, text="Обзор", command=self.select_path).pack(side="right", padx=5) + # Выбор пути к игре + tk.Label(self.root, text="Путь к игре (exe):").pack(pady=(5, 0)) + self.game_path_var = tk.StringVar(value=self.config["game_path"]) + game_entry_frame = tk.Frame(self.root) + game_entry_frame.pack(fill="x", padx=20, pady=2) + tk.Entry(game_entry_frame, textvariable=self.game_path_var).pack(side="left", fill="x", expand=True) + tk.Button(game_entry_frame, text="Обзор", command=self.select_game_path).pack(side="right", padx=5) + # Статус и Прогресс self.status_label = tk.Label(self.root, text="Готов к работе", fg="blue") self.status_label.pack(pady=(10, 0)) @@ -51,15 +79,40 @@ class SyncApp: self.progress = ttk.Progressbar(self.root, orient="horizontal", length=400, mode="determinate") self.progress.pack(pady=10) - self.sync_btn = tk.Button(self.root, text="СИНХРОНИЗИРОВАТЬ", command=self.start_sync_thread, - bg="#2c3e50", fg="white", font=("Arial", 10, "bold"), height=2) - self.sync_btn.pack(pady=10) + # Кнопки + btn_frame = tk.Frame(self.root) + btn_frame.pack(pady=10) + + self.sync_btn = tk.Button(btn_frame, text="СИНХРОНИЗИРОВАТЬ", command=self.start_sync_thread, + bg="#2c3e50", fg="white", font=("Arial", 10, "bold"), height=2, width=18) + self.sync_btn.pack(side="left", padx=10) + + self.launch_btn = tk.Button(btn_frame, text="ЗАПУСТИТЬ ИГРУ", command=self.launch_game, + bg="#27ae60", fg="white", font=("Arial", 10, "bold"), height=2, width=18) + self.launch_btn.pack(side="right", padx=10) def select_path(self): - path = filedialog.askdirectory() + path = filedialog.askdirectory(title="Выберите папку mods") if path: self.path_var.set(path) - self.save_config(path) + self.save_config(mods_path=path) + + def select_game_path(self): + path = filedialog.askopenfilename(title="Выберите исполняемый файл игры") + if path: + self.game_path_var.set(path) + self.save_config(game_path=path) + + def launch_game(self): + game_path = self.game_path_var.get() + if not game_path or not Path(game_path).exists(): + messagebox.showwarning("Внимание", "Сначала выберите правильный путь к исполняемому файлу игры!") + return + try: + subprocess.Popen([game_path], cwd=os.path.dirname(game_path)) + self.update_status("Игра запущена!", 100) + except Exception as e: + messagebox.showerror("Ошибка", f"Не удалось запустить игру:\n{e}") def update_status(self, text, value=None): self.status_label.config(text=text) @@ -79,12 +132,14 @@ class SyncApp: return self.sync_btn.config(state="disabled") + self.launch_btn.config(state="disabled") try: # 1. Получение манифеста self.update_status("Получение списка модов с сервера...", 5) response = requests.get(f"{SERVER_URL}mods_list.json", timeout=15) response.raise_for_status() server_manifest = response.json() + server_base_names = {get_mod_base_name(f) for f in server_manifest.keys()} local_mods = Path(mods_path) if not local_mods.exists(): @@ -92,14 +147,17 @@ class SyncApp: mod_status = {} - # --- ЭТАП ОЧИСТКИ (Удаление лишнего) --- - self.update_status("Очистка старых модов...", 10) + # --- ЭТАП ОЧИСТКИ (Удаление старых версий) --- + self.update_status("Очистка старых версий модов...", 10) local_files = list(local_mods.glob("*.zip")) for local_file in local_files: if local_file.name not in server_manifest: - print(f"Удаление лишнего файла: {local_file.name}") - local_file.unlink() - mod_status[local_file.name] = "удалено" + local_base = get_mod_base_name(local_file.name) + # Удаляем только если это старая версия мода из сборки (базовое имя совпадает) + if local_base in server_base_names: + print(f"Удаление старой версии: {local_file.name}") + local_file.unlink() + mod_status[local_file.name] = "удалено" # --------------------------------------- # 2. Синхронизация (Загрузка нужного) @@ -135,13 +193,8 @@ class SyncApp: self.update_status("Синхронизация завершена!", 100) # --- СОРТИРОВКА И ВЫВОД ОТЧЕТА --- - sort_order = { - "без изменений": 1, - "обновлено": 2, - "добавлено": 3, - "удалено": 4 - } - sorted_mods = sorted(mod_status.items(), key=lambda item: sort_order.get(item[1], 5)) + # Сортируем просто по алфавиту для удобного git-diff стиля + sorted_mods = sorted(mod_status.items(), key=lambda item: item[0]) # Безопасный вызов отрисовки UI из фонового потока self.root.after(0, self.show_report, sorted_mods) @@ -151,6 +204,7 @@ class SyncApp: self.root.after(0, lambda: messagebox.showerror("Ошибка", str(e))) finally: self.root.after(0, lambda: self.sync_btn.config(state="normal")) + self.root.after(0, lambda: self.launch_btn.config(state="normal")) def show_report(self, sorted_mods): report_win = tk.Toplevel(self.root) @@ -173,8 +227,14 @@ class SyncApp: # Заполняем поле for mod, status in sorted_mods: - text_widget.insert(tk.END, f"{mod}: ") - text_widget.insert(tk.END, f"{status}\n", status) + if status == "добавлено": + text_widget.insert(tk.END, f"+ {mod}\n", status) + elif status == "удалено": + text_widget.insert(tk.END, f"- {mod}\n", status) + elif status == "обновлено": + text_widget.insert(tk.END, f"~ {mod}\n", status) + elif status == "без изменений": + text_widget.insert(tk.END, f" {mod}\n", status) text_widget.config(state="disabled") # Только чтение