Небольшие изменения
This commit is contained in:
parent
401cc652a2
commit
d17ea24fa5
1 changed files with 85 additions and 25 deletions
110
main.py
110
main.py
|
|
@ -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") # Только чтение
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue