PixelLauncher 2.1.2 — секции настроек, панель быстрых настроек, сборки Modrinth/CurseForge
This commit is contained in:
1 parent
80dc1826a1
commit
39cf752b7b
13 files changed
+972
-49
No files matched your search
+381
-1
@@ -24,12 +24,14 @@ from ctypes import wintypes
|
||||
from tkinter import filedialog, messagebox, ttk
|
||||
|
||||
from . import config, crash as crash_mod, downloader, forge as forge_mod, http
|
||||
from . import curseforge as curseforge_mod
|
||||
from . import java as java_mod, java_fetch
|
||||
from . import launch as launch_mod
|
||||
from . import manifest as manifest_mod
|
||||
from . import mods as mods_mod
|
||||
from . import modrinth as modrinth_mod
|
||||
from . import mrpack as mrpack_mod
|
||||
from . import packs as packs_mod
|
||||
from . import plugins as plugins_mod
|
||||
from . import protocol as protocol_mod
|
||||
from . import selfupdate
|
||||
@@ -952,6 +954,12 @@ class App(tk.Tk):
|
||||
self.news_rows.pack()
|
||||
self._fill_news(self._news)
|
||||
|
||||
# --- Быстрые настройки прямо на главном экране: частые переключатели
|
||||
# не требуют ни меню ⚙, ни диалога «Настройки».
|
||||
self.quick_bar = tk.Frame(main, bg=BG)
|
||||
self.quick_bar.pack(pady=(10, 0))
|
||||
self._build_quick_bar(self.quick_bar)
|
||||
|
||||
self.lbl_status = tk.Label(main, text=T("Готово к запуску"), bg=BG, fg=DIM,
|
||||
font=("Segoe UI", 8))
|
||||
self.lbl_status.pack(pady=(12, 0))
|
||||
@@ -1657,6 +1665,8 @@ class App(tk.Tk):
|
||||
m.add_command(label=T("Моды…"), command=self._open_mods)
|
||||
m.add_command(label=T("Импорт модпака…"), command=self._import_pack)
|
||||
m.add_command(label=T("Каталог модпаков…"), command=self._open_packs)
|
||||
m.add_command(label=T("Сборки Modrinth…"), command=self._open_modrinth_packs)
|
||||
m.add_command(label=T("Сборки CurseForge…"), command=self._open_curseforge_packs)
|
||||
m.add_command(label=T("Плагины…"), command=self._open_plugins)
|
||||
m.add_command(label=T("Магазин плагинов…"), command=self._open_store)
|
||||
m.add_command(label=T("Скин-превью…"), command=self._open_skin)
|
||||
@@ -2469,6 +2479,117 @@ class App(tk.Tk):
|
||||
self._finish_dialog(d)
|
||||
return d
|
||||
|
||||
def _build_quick_bar(self, parent):
|
||||
"""Кликабельные чипы быстрых настроек на главном экране.
|
||||
|
||||
Дублируют подменю ⚙ → «Быстрые настройки»: язык, масштаб,
|
||||
автозагрузка, трей и авто-бэкап меняются одним кликом.
|
||||
"""
|
||||
def chip(text, command, active=False):
|
||||
bg = GREEN_BD if active else PILL
|
||||
fg = TEXT if active else DIM
|
||||
lbl = tk.Label(parent, text=text, bg=bg, fg=fg,
|
||||
font=("Segoe UI", 8), padx=9, pady=3,
|
||||
cursor="hand2")
|
||||
lbl.pack(side="left", padx=3)
|
||||
lbl.bind("<Button-1>", lambda e: command())
|
||||
lbl.bind("<Enter>", lambda e: lbl.configure(bg=HOVER, fg=TEXT))
|
||||
lbl.bind("<Leave>", lambda e: lbl.configure(bg=bg, fg=fg))
|
||||
return lbl
|
||||
|
||||
s = self.settings
|
||||
on, off = T("вкл"), T("выкл")
|
||||
cur_lang = s.get("lang", "ru")
|
||||
|
||||
ram_txt = (T("авто") if s.get("auto_ram")
|
||||
else self._fmt_ram(s.get("ram", 2048)))
|
||||
chip(T("ОЗУ") + ": " + ram_txt, self._cycle_ram,
|
||||
active=bool(s.get("auto_ram")))
|
||||
|
||||
self.quick_ver_chip = chip(self._ver_chip_text(),
|
||||
self._open_versions)
|
||||
|
||||
chip("RU" if cur_lang == "ru" else "EN",
|
||||
lambda: self._quick_lang("en" if cur_lang == "ru" else "ru"))
|
||||
|
||||
chip("%d%%" % int(round(float(s.get("ui_scale", 1.0)) * 100)),
|
||||
self._cycle_scale)
|
||||
|
||||
chip(T("Автозагрузка") + ": " + (on if s.get("autostart") else off),
|
||||
self._toggle_autostart, active=bool(s.get("autostart")))
|
||||
|
||||
chip(T("Трей") + ": " + (on if s.get("minimize_to_tray_on_launch")
|
||||
else off),
|
||||
self._toggle_tray,
|
||||
active=bool(s.get("minimize_to_tray_on_launch")))
|
||||
|
||||
chip(T("Бэкап") + ": " + (on if s.get("auto_backup") else off),
|
||||
self._toggle_backup, active=bool(s.get("auto_backup")))
|
||||
|
||||
@staticmethod
|
||||
def _fmt_ram(mb):
|
||||
try:
|
||||
gb = float(mb) / 1024
|
||||
except (TypeError, ValueError):
|
||||
gb = 2.0
|
||||
return ("%.1f" % gb).rstrip("0").rstrip(".") + " " + T("ГБ")
|
||||
|
||||
def _ver_chip_text(self):
|
||||
vid = self._selected_version_id()
|
||||
if len(vid) > 14:
|
||||
vid = vid[:13] + "…"
|
||||
return T("Версия") + ": " + vid
|
||||
|
||||
def _cycle_ram(self):
|
||||
steps = [None, 2048, 4096, 6144, 8192]
|
||||
if self.settings.get("auto_ram"):
|
||||
cur = None
|
||||
else:
|
||||
try:
|
||||
cur = int(self.settings.get("ram", 2048))
|
||||
except (TypeError, ValueError):
|
||||
cur = 2048
|
||||
try:
|
||||
nxt = steps[(steps.index(cur) + 1) % len(steps)]
|
||||
except ValueError:
|
||||
nxt = steps[0]
|
||||
if nxt is None:
|
||||
self.settings["auto_ram"] = True
|
||||
else:
|
||||
self.settings["auto_ram"] = False
|
||||
self.settings["ram"] = nxt
|
||||
config.save_settings(self.settings)
|
||||
self.after_idle(self._rebuild_ui)
|
||||
|
||||
def _cycle_scale(self):
|
||||
"""Масштаб по кругу: 100 → 125 → 150 → 100 %."""
|
||||
steps = (100, 125, 150)
|
||||
cur = int(round(float(self.settings.get("ui_scale", 1.0)) * 100))
|
||||
try:
|
||||
nxt = steps[(steps.index(cur) + 1) % len(steps)]
|
||||
except ValueError:
|
||||
nxt = 100
|
||||
self._quick_scale(nxt)
|
||||
|
||||
def _toggle_autostart(self):
|
||||
on = not bool(self.settings.get("autostart"))
|
||||
if not self._set_autostart(on):
|
||||
return
|
||||
self.settings["autostart"] = on
|
||||
config.save_settings(self.settings)
|
||||
self.after_idle(self._rebuild_ui)
|
||||
|
||||
def _toggle_tray(self):
|
||||
self.settings["minimize_to_tray_on_launch"] = not bool(
|
||||
self.settings.get("minimize_to_tray_on_launch"))
|
||||
config.save_settings(self.settings)
|
||||
self.after_idle(self._rebuild_ui)
|
||||
|
||||
def _toggle_backup(self):
|
||||
self.settings["auto_backup"] = not bool(self.settings.get("auto_backup"))
|
||||
config.save_settings(self.settings)
|
||||
self.after_idle(self._rebuild_ui)
|
||||
|
||||
def _quick_settings(self, parent):
|
||||
"""Подменю быстрых настроек в главном меню.
|
||||
|
||||
@@ -2810,6 +2931,253 @@ class App(tk.Tk):
|
||||
self._btn(btns, T("Отмена"), d.destroy).pack(side="right", padx=(0, 6))
|
||||
self._finish_dialog(d)
|
||||
|
||||
def _open_modrinth_packs(self):
|
||||
"""Сборки Modrinth: поиск, выбор версии, установка."""
|
||||
self._open_pack_browser("modrinth")
|
||||
|
||||
def _open_curseforge_packs(self):
|
||||
"""Сборки CurseForge: поиск, выбор файла, установка."""
|
||||
self._open_pack_browser("curseforge")
|
||||
|
||||
def _open_pack_browser(self, source):
|
||||
"""Диалог сборок: Modrinth (source='modrinth') или CurseForge."""
|
||||
is_mr = source == "modrinth"
|
||||
d = self._dialog(T("Modrinth: сборки") if is_mr
|
||||
else T("CurseForge: сборки"), width=700)
|
||||
body = tk.Frame(d, bg=BG)
|
||||
body.pack(fill="both", expand=True, padx=12, pady=10)
|
||||
|
||||
top = tk.Frame(body, bg=BG)
|
||||
top.pack(fill="x")
|
||||
var_q = tk.StringVar()
|
||||
ent = tk.Entry(top, textvariable=var_q, width=42, font=("Segoe UI", 9),
|
||||
bg=INPUT, fg=TEXT, insertbackground=TEXT, relief="solid",
|
||||
bd=1, highlightbackground=INPUT_BD, highlightthickness=1)
|
||||
ent.pack(side="left", ipady=2)
|
||||
ent.bind("<FocusIn>", lambda e: ent.configure(highlightbackground=GREEN))
|
||||
ent.bind("<FocusOut>",
|
||||
lambda e: ent.configure(highlightbackground=INPUT_BD))
|
||||
tk.Label(top, text=T("Поиск сборок"), bg=BG, fg=DIM,
|
||||
font=("Segoe UI", 9)).pack(side="left", padx=(8, 0))
|
||||
|
||||
status = tk.Label(body, text=T("Загрузка…"), bg=BG, fg=DIM, anchor="w",
|
||||
font=("Segoe UI", 8))
|
||||
status.pack(fill="x", pady=(8, 0))
|
||||
|
||||
lb = tk.Listbox(body, height=9, bg=INPUT, fg=TEXT, selectbackground=GREEN,
|
||||
selectforeground="#0d1a0d", relief="solid", bd=1,
|
||||
highlightthickness=0, font=("Segoe UI", 9),
|
||||
exportselection=False)
|
||||
lb.pack(fill="both", expand=True, pady=(4, 0))
|
||||
|
||||
lbl_files = tk.Label(body, text=T("Файлы сборки"), bg=BG, fg=DIM,
|
||||
anchor="w", font=("Segoe UI", 8))
|
||||
lbl_files.pack(fill="x", pady=(8, 0))
|
||||
lb_f = tk.Listbox(body, height=6, bg=INPUT, fg=TEXT,
|
||||
selectbackground=GREEN, selectforeground="#0d1a0d",
|
||||
relief="solid", bd=1, highlightthickness=0,
|
||||
font=("Segoe UI", 9), exportselection=False)
|
||||
lb_f.pack(fill="both", expand=True, pady=(4, 0))
|
||||
|
||||
packs = []
|
||||
files = []
|
||||
timer = {"id": None}
|
||||
|
||||
def run(fn, ok, err):
|
||||
"""Выполняет fn в фоне, результат отдаёт в GUI-поток."""
|
||||
state = {"done": False, "data": None, "error": None}
|
||||
|
||||
def go():
|
||||
try:
|
||||
state["data"] = fn()
|
||||
except Exception as e: # noqa: BLE001
|
||||
state["error"] = e
|
||||
state["done"] = True
|
||||
|
||||
threading.Thread(target=go, daemon=True).start()
|
||||
|
||||
def wait():
|
||||
try:
|
||||
if not d.winfo_exists():
|
||||
return
|
||||
except tk.TclError:
|
||||
return
|
||||
if not state["done"]:
|
||||
d.after(120, wait)
|
||||
return
|
||||
if state["error"] is not None:
|
||||
err(state["error"])
|
||||
else:
|
||||
ok(state["data"])
|
||||
wait()
|
||||
|
||||
def fmt_size(n):
|
||||
n = int(n or 0)
|
||||
if n <= 0:
|
||||
return ""
|
||||
if n >= 1024 * 1024:
|
||||
return "%.1f %s" % (n / (1024.0 * 1024.0), T("МБ"))
|
||||
if n >= 1024:
|
||||
return "%d %s" % (n // 1024, T("КБ"))
|
||||
return "%d %s" % (n, T("Б"))
|
||||
|
||||
def search_packs():
|
||||
q = var_q.get().strip()
|
||||
status.configure(text=T("Загрузка…"), fg=DIM)
|
||||
|
||||
def ok(res):
|
||||
nonlocal packs
|
||||
out, total = res
|
||||
packs = out
|
||||
lb.delete(0, "end")
|
||||
for it in out:
|
||||
title = it.get("title") or it.get("name") or "?"
|
||||
desc = it.get("desc") or it.get("summary") or ""
|
||||
bits = []
|
||||
if it.get("downloads"):
|
||||
bits.append(str(it["downloads"]))
|
||||
if it.get("loaders"):
|
||||
bits.append(", ".join(it["loaders"][:2]))
|
||||
tail = (" [" + " · ".join(bits) + "]") if bits else ""
|
||||
lb.insert("end", "%s — %s%s" % (title, desc, tail))
|
||||
status.configure(text=T("Найдено: ") + str(total), fg=DIM)
|
||||
|
||||
def err(e):
|
||||
status.configure(text=T("Ошибка: ") + str(e), fg=ERR)
|
||||
|
||||
if is_mr:
|
||||
api = lambda: modrinth_mod.search_modpacks(q, limit=30)
|
||||
else:
|
||||
api = lambda: curseforge_mod.search_modpacks(q, page_size=30)
|
||||
run(api, ok, err)
|
||||
|
||||
def schedule_search(*_a):
|
||||
if timer["id"] is not None:
|
||||
try:
|
||||
d.after_cancel(timer["id"])
|
||||
except tk.TclError:
|
||||
pass
|
||||
timer["id"] = d.after(400, search_packs)
|
||||
|
||||
def load_files(*_a):
|
||||
sel = lb.curselection()
|
||||
lb_f.delete(0, "end")
|
||||
files.clear()
|
||||
if not sel or sel[0] >= len(packs):
|
||||
lbl_files.configure(text=T("Файлы сборки"), fg=DIM)
|
||||
return
|
||||
p = packs[sel[0]]
|
||||
status.configure(text=T("Загрузка…"), fg=DIM)
|
||||
|
||||
def ok(res):
|
||||
files.clear()
|
||||
files.extend(res)
|
||||
lb_f.delete(0, "end")
|
||||
for f in res:
|
||||
bits = []
|
||||
gv = f.get("game_versions") or []
|
||||
ld = f.get("loaders") or []
|
||||
if gv:
|
||||
bits.append(", ".join(str(x) for x in gv[:3]))
|
||||
if ld:
|
||||
bits.append(", ".join(str(x) for x in ld[:2]))
|
||||
if f.get("size"):
|
||||
bits.append(fmt_size(f["size"]))
|
||||
if f.get("date"):
|
||||
bits.append(str(f["date"]))
|
||||
name = f.get("name") or f.get("file_name") or "?"
|
||||
tail = (" [" + " · ".join(bits) + "]") if bits else ""
|
||||
lb_f.insert("end", "%s%s" % (name, tail))
|
||||
lbl_files.configure(
|
||||
text=T("Файлы сборки") + ": " + str(len(files)), fg=DIM)
|
||||
status.configure(text=T("Найдено: ") + str(len(files)), fg=DIM)
|
||||
|
||||
def err(e):
|
||||
status.configure(text=T("Ошибка: ") + str(e), fg=ERR)
|
||||
|
||||
if is_mr:
|
||||
pid = p.get("slug") or p.get("id")
|
||||
api = lambda: modrinth_mod.get_modpack_versions(pid)
|
||||
else:
|
||||
api = lambda: curseforge_mod.get_files(p.get("id"))
|
||||
run(api, ok, err)
|
||||
|
||||
def install_sel():
|
||||
sel = lb.curselection()
|
||||
if not sel or sel[0] >= len(packs):
|
||||
status.configure(text=T("Выберите сборку"), fg=ERR)
|
||||
return
|
||||
p = packs[sel[0]]
|
||||
fsel = lb_f.curselection()
|
||||
f = files[fsel[0]] if (fsel and fsel[0] < len(files)) else None
|
||||
title = p.get("title") or p.get("name") or "?"
|
||||
d.destroy()
|
||||
if is_mr:
|
||||
pid = p.get("slug") or p.get("id")
|
||||
ver = (f or {}).get("id")
|
||||
|
||||
def worker(log, prog, cancel):
|
||||
return packs_mod.install_pack_modrinth(
|
||||
pid, game_dir=self._game_dir(), version=ver, log=log,
|
||||
progress=prog, cancel=cancel)
|
||||
else:
|
||||
mid = p.get("id")
|
||||
fid = (f or {}).get("id") or p.get("file_id")
|
||||
|
||||
def worker(log, prog, cancel):
|
||||
return packs_mod.install_pack_curseforge(
|
||||
mid, fid, game_dir=self._game_dir(), log=log,
|
||||
progress=prog, cancel=cancel)
|
||||
self._install_pack_async(
|
||||
worker, T("Установка сборки: ") + str(title))
|
||||
|
||||
var_q.trace_add("write", schedule_search)
|
||||
lb.bind("<<ListboxSelect>>", load_files)
|
||||
lb_f.bind("<Double-Button-1>", lambda e: install_sel())
|
||||
lb.bind("<Double-Button-1>", lambda e: install_sel())
|
||||
|
||||
btns = tk.Frame(body, bg=BG)
|
||||
btns.pack(fill="x", pady=(10, 0))
|
||||
self._btn(btns, T("Установить"), install_sel, primary=True).pack(
|
||||
side="left")
|
||||
self._btn(btns, T("Обновить список"), search_packs).pack(
|
||||
side="left", padx=(6, 0))
|
||||
self._btn(btns, T("Закрыть"), d.destroy).pack(side="right")
|
||||
search_packs()
|
||||
self._finish_dialog(d)
|
||||
|
||||
def _install_pack_async(self, worker, label):
|
||||
"""Ставит сборку в фоне: прогресс и лог — в главном окне."""
|
||||
if self.busy:
|
||||
return
|
||||
self.busy = True
|
||||
self._cancel = threading.Event()
|
||||
cancel = self._cancel
|
||||
self.btn_play.set_state("disabled")
|
||||
self.btn_cancel.configure(state="normal")
|
||||
self.btn_cancel.pack()
|
||||
self.progress.set(0)
|
||||
|
||||
def bg():
|
||||
log = lambda m, t=None: self.ui_q.put(("log", m, t)) # noqa: E731
|
||||
|
||||
def prog(f):
|
||||
self.ui_q.put(("progress", f * 100, 100, T("Сборка")))
|
||||
try:
|
||||
log(label)
|
||||
vid, got = worker(log, prog, cancel)
|
||||
if vid:
|
||||
self.ui_q.put(("use_version", vid))
|
||||
self.ui_q.put(("reload_versions",))
|
||||
log(T("Модпак установлен") + (f" ({got})" if got else ""), "ok")
|
||||
except http.Cancelled:
|
||||
log(T("Загрузка отменена"), "warn")
|
||||
except Exception as e: # noqa: BLE001
|
||||
log(T("Ошибка: ") + str(e), "err")
|
||||
finally:
|
||||
self.ui_q.put(("done",))
|
||||
threading.Thread(target=bg, daemon=True).start()
|
||||
|
||||
def _rebuild_ui(self):
|
||||
"""Пересобирает главное окно (после смены языка/настроек)."""
|
||||
for c in self.winfo_children():
|
||||
@@ -4854,9 +5222,21 @@ class App(tk.Tk):
|
||||
if prof:
|
||||
text = f"{prof} · {text}"
|
||||
self.pill.set_text(text)
|
||||
if hasattr(self, "quick_ver_chip") and self.quick_ver_chip.winfo_exists():
|
||||
try:
|
||||
self.quick_ver_chip.configure(text=self._ver_chip_text())
|
||||
except tk.TclError:
|
||||
pass
|
||||
|
||||
|
||||
def _selected_version_id(self):
|
||||
vsel = self.var_ver.get().strip()
|
||||
if hasattr(self, "var_ver") and self.var_ver is not None:
|
||||
try:
|
||||
vsel = self.var_ver.get().strip()
|
||||
except Exception:
|
||||
vsel = ""
|
||||
else:
|
||||
vsel = str(self.settings.get("version", "latest-release") or "")
|
||||
latest = self._latest or {}
|
||||
if vsel.startswith("latest-release"):
|
||||
return latest.get("release", "latest-release")
|
||||
|
||||
Reference in new issue
Block a user