Небольшие изменения
All checks were successful
Build and Publish Release / Build Linux (release) Successful in 2m20s
Build and Publish Release / Build Windows (release) Successful in 4m52s
Build and Publish Release / Upload Assets to Release (release) Successful in 13s

This commit is contained in:
borderban 2026-05-03 22:25:02 +05:00
parent 401cc652a2
commit d17ea24fa5

110
main.py
View file

@ -6,11 +6,22 @@ import tkinter as tk
from tkinter import filedialog, messagebox, ttk from tkinter import filedialog, messagebox, ttk
from pathlib import Path from pathlib import Path
import threading import threading
import subprocess
import re
# --- КОНФИГУРАЦИЯ --- # --- КОНФИГУРАЦИЯ ---
SERVER_URL = "https://server1.borderban.ru/mods/" SERVER_URL = "https://server1.borderban.ru/mods/"
CONFIG_FILE = Path.home() / ".factorio_sync_config.json" 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): def get_file_hash(filepath):
sha256_hash = hashlib.sha256() sha256_hash = hashlib.sha256()
with open(filepath, "rb") as f: with open(filepath, "rb") as f:
@ -22,28 +33,45 @@ class SyncApp:
def __init__(self, root): def __init__(self, root):
self.root = root self.root = root
self.root.title("Factorio Mod Sync") self.root.title("Factorio Mod Sync")
self.root.geometry("500x250") self.root.geometry("500x350")
self.config = self.load_config() self.config = self.load_config()
self.setup_ui() self.setup_ui()
def load_config(self): def load_config(self):
if CONFIG_FILE.exists(): if CONFIG_FILE.exists():
return json.loads(CONFIG_FILE.read_text()) data = json.loads(CONFIG_FILE.read_text())
return {"mods_path": ""} return {
"mods_path": data.get("mods_path", ""),
"game_path": data.get("game_path", "")
}
return {"mods_path": "", "game_path": ""}
def save_config(self, mods_path): def save_config(self, mods_path=None, game_path=None):
CONFIG_FILE.write_text(json.dumps({"mods_path": mods_path})) 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): def setup_ui(self):
# Выбор пути # Выбор пути
tk.Label(self.root, text="Путь к папке mods:").pack(pady=(10, 0)) tk.Label(self.root, text="Путь к папке mods:").pack(pady=(10, 0))
self.path_var = tk.StringVar(value=self.config["mods_path"]) self.path_var = tk.StringVar(value=self.config["mods_path"])
entry_frame = tk.Frame(self.root) 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.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.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 = tk.Label(self.root, text="Готов к работе", fg="blue")
self.status_label.pack(pady=(10, 0)) 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 = ttk.Progressbar(self.root, orient="horizontal", length=400, mode="determinate")
self.progress.pack(pady=10) 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) btn_frame = tk.Frame(self.root)
self.sync_btn.pack(pady=10) 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): def select_path(self):
path = filedialog.askdirectory() path = filedialog.askdirectory(title="Выберите папку mods")
if path: if path:
self.path_var.set(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): def update_status(self, text, value=None):
self.status_label.config(text=text) self.status_label.config(text=text)
@ -79,12 +132,14 @@ class SyncApp:
return return
self.sync_btn.config(state="disabled") self.sync_btn.config(state="disabled")
self.launch_btn.config(state="disabled")
try: try:
# 1. Получение манифеста # 1. Получение манифеста
self.update_status("Получение списка модов с сервера...", 5) self.update_status("Получение списка модов с сервера...", 5)
response = requests.get(f"{SERVER_URL}mods_list.json", timeout=15) response = requests.get(f"{SERVER_URL}mods_list.json", timeout=15)
response.raise_for_status() response.raise_for_status()
server_manifest = response.json() server_manifest = response.json()
server_base_names = {get_mod_base_name(f) for f in server_manifest.keys()}
local_mods = Path(mods_path) local_mods = Path(mods_path)
if not local_mods.exists(): if not local_mods.exists():
@ -92,14 +147,17 @@ class SyncApp:
mod_status = {} mod_status = {}
# --- ЭТАП ОЧИСТКИ (Удаление лишнего) --- # --- ЭТАП ОЧИСТКИ (Удаление старых версий) ---
self.update_status("Очистка старых модов...", 10) self.update_status("Очистка старых версий модов...", 10)
local_files = list(local_mods.glob("*.zip")) local_files = list(local_mods.glob("*.zip"))
for local_file in local_files: for local_file in local_files:
if local_file.name not in server_manifest: if local_file.name not in server_manifest:
print(f"Удаление лишнего файла: {local_file.name}") local_base = get_mod_base_name(local_file.name)
local_file.unlink() # Удаляем только если это старая версия мода из сборки (базовое имя совпадает)
mod_status[local_file.name] = "удалено" if local_base in server_base_names:
print(f"Удаление старой версии: {local_file.name}")
local_file.unlink()
mod_status[local_file.name] = "удалено"
# --------------------------------------- # ---------------------------------------
# 2. Синхронизация (Загрузка нужного) # 2. Синхронизация (Загрузка нужного)
@ -135,13 +193,8 @@ class SyncApp:
self.update_status("Синхронизация завершена!", 100) self.update_status("Синхронизация завершена!", 100)
# --- СОРТИРОВКА И ВЫВОД ОТЧЕТА --- # --- СОРТИРОВКА И ВЫВОД ОТЧЕТА ---
sort_order = { # Сортируем просто по алфавиту для удобного git-diff стиля
"без изменений": 1, sorted_mods = sorted(mod_status.items(), key=lambda item: item[0])
"обновлено": 2,
"добавлено": 3,
"удалено": 4
}
sorted_mods = sorted(mod_status.items(), key=lambda item: sort_order.get(item[1], 5))
# Безопасный вызов отрисовки UI из фонового потока # Безопасный вызов отрисовки UI из фонового потока
self.root.after(0, self.show_report, sorted_mods) self.root.after(0, self.show_report, sorted_mods)
@ -151,6 +204,7 @@ class SyncApp:
self.root.after(0, lambda: messagebox.showerror("Ошибка", str(e))) self.root.after(0, lambda: messagebox.showerror("Ошибка", str(e)))
finally: finally:
self.root.after(0, lambda: self.sync_btn.config(state="normal")) 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): def show_report(self, sorted_mods):
report_win = tk.Toplevel(self.root) report_win = tk.Toplevel(self.root)
@ -173,8 +227,14 @@ class SyncApp:
# Заполняем поле # Заполняем поле
for mod, status in sorted_mods: for mod, status in sorted_mods:
text_widget.insert(tk.END, f"{mod}: ") if status == "добавлено":
text_widget.insert(tk.END, f"{status}\n", 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") # Только чтение text_widget.config(state="disabled") # Только чтение