Небольшие изменения
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 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") # Только чтение