PixelLauncher 2.1.0 — стабильная сборка
Офлайн-лаунчер Minecraft: аккаунты без Microsoft, установка версий и лоадеров (Fabric, Quilt, Forge, NeoForge), каталог модпаков Modrinth, поиск и обновление модов, разбор вылетов, системный трей, автозагрузка, плагины, прокси и зеркала.
This commit is contained in:
commit
ec6f13ddcc
33 files changed
+10445
No files matched your search
@@ -0,0 +1,305 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Мастер первоначальной настройки — показывается при первом запуске.
|
||||
|
||||
Окно живёт до построения главного окна (см. ui.run): пользователь выбирает
|
||||
язык, ник и объём ОЗУ. Результат пишется в settings, флаг onboarding_done
|
||||
не даёт мастеру появиться снова; апгрейд со старой версии считается уже
|
||||
настроенным (см. config.load_settings).
|
||||
"""
|
||||
|
||||
import getpass
|
||||
import re
|
||||
import tkinter as tk
|
||||
|
||||
from . import i18n
|
||||
from .i18n import T
|
||||
|
||||
NICK_RE = re.compile(r"[A-Za-z0-9_]{3,16}")
|
||||
|
||||
|
||||
def total_ram_mb():
|
||||
"""ОЗУ системы в МБ (Windows — GlobalMemoryStatusEx, иначе 4096)."""
|
||||
try:
|
||||
import ctypes
|
||||
|
||||
class _MemStatus(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("dwLength", ctypes.c_ulong),
|
||||
("dwMemoryLoad", ctypes.c_ulong),
|
||||
("ullTotalPhys", ctypes.c_ulonglong),
|
||||
("ullAvailPhys", ctypes.c_ulonglong),
|
||||
("ullTotalPageFile", ctypes.c_ulonglong),
|
||||
("ullAvailPageFile", ctypes.c_ulonglong),
|
||||
("ullTotalVirtual", ctypes.c_ulonglong),
|
||||
("ullAvailVirtual", ctypes.c_ulonglong),
|
||||
("ullAvailExtendedVirtual", ctypes.c_ulonglong),
|
||||
]
|
||||
|
||||
st = _MemStatus()
|
||||
st.dwLength = ctypes.sizeof(_MemStatus)
|
||||
if ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(st)):
|
||||
return max(1024, st.ullTotalPhys // (1024 * 1024))
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
return 4096
|
||||
|
||||
|
||||
def default_ram_mb():
|
||||
"""Разумный дефолт: половина ОЗУ, в пределах 2-8 ГБ, кратно 512 МБ."""
|
||||
mb = (total_ram_mb() // 2) // 512 * 512
|
||||
return max(2048, min(8192, mb))
|
||||
|
||||
|
||||
def _suggest_nick():
|
||||
"""Ник по умолчанию: имя пользователя Windows, если оно допустимо."""
|
||||
try:
|
||||
user = getpass.getuser()
|
||||
except Exception: # noqa: BLE001
|
||||
user = ""
|
||||
return user if NICK_RE.fullmatch(user) else "Player"
|
||||
|
||||
|
||||
class Wizard:
|
||||
"""Окно мастера: язык -> ник -> ОЗУ -> готово."""
|
||||
|
||||
def __init__(self, root, settings):
|
||||
# палитру берём из ui (лениво: ui импортирует этот модуль в run())
|
||||
from .ui import (BG, DIM, ERR, FAINT, GREEN, GREEN_HI, INPUT,
|
||||
INPUT_BD, PANEL, TEXT)
|
||||
|
||||
self.pal = dict(BG=BG, DIM=DIM, ERR=ERR, FAINT=FAINT, GREEN=GREEN,
|
||||
GREEN_HI=GREEN_HI, INPUT=INPUT, INPUT_BD=INPUT_BD,
|
||||
PANEL=PANEL, TEXT=TEXT)
|
||||
self.root = root
|
||||
self.settings = settings
|
||||
self.step = 0
|
||||
self.result = None # "done" | "skip"
|
||||
|
||||
root.title("PixelLauncher")
|
||||
root.configure(bg=BG)
|
||||
root.geometry("560x430")
|
||||
root.resizable(False, False)
|
||||
root.protocol("WM_DELETE_WINDOW", self.skip)
|
||||
|
||||
self.var_lang = tk.StringVar(
|
||||
value="en" if settings.get("lang") == "en" else "ru")
|
||||
self.var_nick = tk.StringVar(value=_suggest_nick())
|
||||
self.var_auto = tk.BooleanVar(value=True)
|
||||
self.var_ram = tk.IntVar(value=default_ram_mb())
|
||||
self.var_err = tk.StringVar(value="")
|
||||
|
||||
self.lbl_ram = None
|
||||
self.scale_ram = None
|
||||
self._goto(0)
|
||||
self._center()
|
||||
|
||||
# ------------------------------------------------------------ каркас
|
||||
def _goto(self, i):
|
||||
"""Показать шаг i (окно перерисовывается целиком — тексты живые)."""
|
||||
self.step = i
|
||||
for w in self.root.winfo_children():
|
||||
w.destroy()
|
||||
p = self.pal
|
||||
|
||||
head = tk.Frame(self.root, bg=p["BG"])
|
||||
head.pack(fill="x", padx=36, pady=(26, 0))
|
||||
tk.Label(head, text="PixelLauncher", bg=p["BG"], fg=p["GREEN"],
|
||||
font=("Consolas", 13, "bold")).pack(side="left")
|
||||
tk.Label(head, text=T("Шаг {n} из {m}").format(n=i + 1, m=4),
|
||||
bg=p["BG"], fg=p["DIM"],
|
||||
font=("Segoe UI", 9)).pack(side="right")
|
||||
|
||||
body = tk.Frame(self.root, bg=p["BG"])
|
||||
body.pack(fill="both", expand=True, padx=36, pady=18)
|
||||
(self._step0 if i == 0 else self._step1 if i == 1
|
||||
else self._step2 if i == 2 else self._step3)(body)
|
||||
|
||||
foot = tk.Frame(self.root, bg=p["BG"])
|
||||
foot.pack(fill="x", padx=36, pady=(0, 24))
|
||||
if i > 0:
|
||||
self._btn(foot, T("Назад"), lambda: self._goto(i - 1))
|
||||
self._btn(foot, T("Пропустить настройку"), self.skip, dim=True)
|
||||
label = T("Начать играть") if i == 3 else T("Далее")
|
||||
self._btn(foot, label, self._next, accent=True, right=True)
|
||||
|
||||
def _btn(self, parent, text, cmd, accent=False, dim=False, right=False):
|
||||
p = self.pal
|
||||
bg = p["GREEN"] if accent else (p["BG"] if dim else p["PANEL"])
|
||||
fg = "#0e1a08" if accent else (p["DIM"] if dim else p["TEXT"])
|
||||
b = tk.Button(parent, text=text, command=cmd, bg=bg, fg=fg,
|
||||
activebackground=p["GREEN_HI"] if accent else bg,
|
||||
activeforeground=fg, relief="flat", cursor="hand2",
|
||||
font=("Segoe UI", 10, "bold" if accent else "normal"),
|
||||
padx=16, pady=6)
|
||||
b.pack(side="right" if right else "left",
|
||||
padx=(10, 0) if right else (0, 10))
|
||||
return b
|
||||
|
||||
def _center(self):
|
||||
self.root.update_idletasks()
|
||||
w, h = 560, 430
|
||||
x = (self.root.winfo_screenwidth() - w) // 2
|
||||
y = (self.root.winfo_screenheight() - h) // 2 - 30
|
||||
self.root.geometry("%dx%d+%d+%d" % (w, h, max(0, x), max(0, y)))
|
||||
|
||||
def _title(self, parent, text):
|
||||
p = self.pal
|
||||
tk.Label(parent, text=text, bg=p["BG"], fg=p["TEXT"],
|
||||
font=("Segoe UI", 15, "bold")).pack(anchor="w")
|
||||
|
||||
# ------------------------------------------------------------ шаги
|
||||
def _step0(self, body):
|
||||
"""Язык."""
|
||||
p = self.pal
|
||||
self._title(body, T("Добро пожаловать в PixelLauncher!"))
|
||||
tk.Label(body, wraplength=470, justify="left", bg=p["BG"],
|
||||
fg=p["DIM"], font=("Segoe UI", 10),
|
||||
text=T("Первая настройка займёт минуту: язык, ник и "
|
||||
"память для игры.")).pack(anchor="w", pady=(10, 18))
|
||||
row = tk.Frame(body, bg=p["BG"])
|
||||
row.pack(anchor="w")
|
||||
for code, caption in (("ru", "🇷🇺 Русский"), ("en", "🇬🇧 English")):
|
||||
chosen = self.var_lang.get() == code
|
||||
b = tk.Button(row, text=caption, bg=p["GREEN"] if chosen
|
||||
else p["PANEL"], fg="#0e1a08" if chosen else p["TEXT"],
|
||||
activebackground=p["GREEN_HI"], relief="flat",
|
||||
cursor="hand2", font=("Segoe UI", 11, "bold"),
|
||||
padx=22, pady=10,
|
||||
command=lambda c=code: self._set_lang(c))
|
||||
b.pack(side="left", padx=(0, 12))
|
||||
tk.Label(body, wraplength=470, justify="left", bg=p["BG"],
|
||||
fg=p["FAINT"], font=("Segoe UI", 9),
|
||||
text=T("Язык можно поменять позже в настройках.")
|
||||
).pack(anchor="w", pady=(18, 0))
|
||||
|
||||
def _set_lang(self, code):
|
||||
self.var_lang.set(code)
|
||||
i18n.set_language(code)
|
||||
self.settings["lang"] = code # сохранится даже при пропуске
|
||||
self._goto(self.step)
|
||||
|
||||
def _step1(self, body):
|
||||
"""Ник."""
|
||||
p = self.pal
|
||||
self._title(body, T("Как тебя зовут?"))
|
||||
tk.Label(body, bg=p["BG"], fg=p["DIM"], font=("Segoe UI", 10),
|
||||
text=T("Ник игрока (виден в игре):")
|
||||
).pack(anchor="w", pady=(12, 6))
|
||||
e = tk.Entry(body, textvariable=self.var_nick, bg=p["INPUT"],
|
||||
fg=p["TEXT"], insertbackground=p["TEXT"],
|
||||
relief="flat", font=("Segoe UI", 13), width=24)
|
||||
e.pack(anchor="w", ipady=7)
|
||||
e.focus_set()
|
||||
e.bind("<Return>", lambda ev: self._next())
|
||||
tk.Label(body, textvariable=self.var_err, bg=p["BG"], fg=p["ERR"],
|
||||
font=("Segoe UI", 9)).pack(anchor="w", pady=(8, 0))
|
||||
tk.Label(body, wraplength=470, justify="left", bg=p["BG"],
|
||||
fg=p["DIM"], font=("Segoe UI", 9),
|
||||
text=T("Ник: 3-16 символов, латиница, цифры и _")
|
||||
).pack(anchor="w", pady=(4, 0))
|
||||
|
||||
def _step2(self, body):
|
||||
"""ОЗУ."""
|
||||
p = self.pal
|
||||
self._title(body, T("Сколько памяти выделить игре?"))
|
||||
auto = tk.Checkbutton(body, text=T("Подобрать автоматически"),
|
||||
variable=self.var_auto, bg=p["BG"], fg=p["TEXT"],
|
||||
selectcolor=p["INPUT"], activebackground=p["BG"],
|
||||
activeforeground=p["TEXT"], relief="flat",
|
||||
font=("Segoe UI", 10), cursor="hand2",
|
||||
command=self._sync_ram)
|
||||
auto.pack(anchor="w", pady=(14, 8))
|
||||
self.scale_ram = tk.Scale(body, from_=1024, to=16384, resolution=512,
|
||||
orient="horizontal", showvalue=0,
|
||||
bg=p["BG"], fg=p["TEXT"], troughcolor=p["INPUT"],
|
||||
highlightthickness=0, length=440,
|
||||
sliderrelief="flat", sliderlength=18,
|
||||
command=self._on_ram)
|
||||
self.scale_ram.set(self.var_ram.get())
|
||||
self.scale_ram.pack(anchor="w")
|
||||
self.lbl_ram = tk.Label(body, text=self._ram_text(), bg=p["BG"],
|
||||
fg=p["GREEN"], font=("Segoe UI", 11, "bold"))
|
||||
self.lbl_ram.pack(anchor="w", pady=(4, 6))
|
||||
tk.Label(body, wraplength=470, justify="left", bg=p["BG"],
|
||||
fg=p["DIM"], font=("Segoe UI", 9),
|
||||
text=T("Можно оставить авто — лаунчер подберёт значение "
|
||||
"при запуске.")).pack(anchor="w")
|
||||
self._sync_ram()
|
||||
|
||||
def _on_ram(self, value):
|
||||
try:
|
||||
self.var_ram.set(int(float(value)))
|
||||
except (TypeError, ValueError):
|
||||
return
|
||||
if self.lbl_ram is not None:
|
||||
self.lbl_ram.config(text=self._ram_text())
|
||||
|
||||
def _ram_text(self):
|
||||
mb = int(self.var_ram.get())
|
||||
return T("{mb} МБ ({gb} ГБ)").format(mb=mb, gb=int(round(mb / 1024)))
|
||||
|
||||
def _sync_ram(self):
|
||||
"""Галка «авто» включает/выключает ползунок."""
|
||||
if self.scale_ram is not None:
|
||||
self.scale_ram.config(
|
||||
state="disabled" if self.var_auto.get() else "normal")
|
||||
|
||||
def _step3(self, body):
|
||||
"""Готово."""
|
||||
p = self.pal
|
||||
self._title(body, T("Всё готово!"))
|
||||
lang = T("English") if self.var_lang.get() == "en" else "Русский"
|
||||
ram = T("авто") if self.var_auto.get() else self._ram_text()
|
||||
info = "\n".join([T("Ник: {nick}").format(
|
||||
nick=self.var_nick.get().strip() or "Player"),
|
||||
T("Язык: {lang}").format(lang=lang),
|
||||
T("Память: {ram}").format(ram=ram)])
|
||||
tk.Label(body, justify="left", anchor="w", bg=p["PANEL"], fg=p["TEXT"],
|
||||
font=("Segoe UI", 11), padx=18, pady=14, text=info
|
||||
).pack(anchor="w", pady=(14, 12))
|
||||
tk.Label(body, wraplength=470, justify="left", bg=p["BG"],
|
||||
fg=p["DIM"], font=("Segoe UI", 9),
|
||||
text=T("Чтобы пройти настройку заново, удалите файл "
|
||||
"settings.json.")).pack(anchor="w")
|
||||
|
||||
# ------------------------------------------------------------ действия
|
||||
def _next(self):
|
||||
if self.step == 1 and not NICK_RE.fullmatch(
|
||||
self.var_nick.get().strip()):
|
||||
self.var_err.set(T("Недопустимый ник: латиница, цифры и _, "
|
||||
"3-16 символов"))
|
||||
return
|
||||
if self.step == 3:
|
||||
self.finish()
|
||||
return
|
||||
self._goto(self.step + 1)
|
||||
|
||||
def finish(self):
|
||||
"""Применить выбор: язык, ник, ОЗУ — и закрыть мастер."""
|
||||
nick = self.var_nick.get().strip()
|
||||
s = self.settings
|
||||
s["lang"] = "en" if self.var_lang.get() == "en" else "ru"
|
||||
i18n.set_language(s["lang"])
|
||||
s["username"] = nick
|
||||
s["accounts"] = [nick]
|
||||
s["active_account"] = nick
|
||||
s["ram"] = int(self.var_ram.get())
|
||||
s["auto_ram"] = bool(self.var_auto.get())
|
||||
s["onboarding_done"] = True
|
||||
self.result = "done"
|
||||
self.root.destroy()
|
||||
|
||||
def skip(self):
|
||||
"""Настроить позже: только зафиксировать, что мастер показан."""
|
||||
self.settings["onboarding_done"] = True
|
||||
self.result = "skip"
|
||||
self.root.destroy()
|
||||
|
||||
|
||||
def run(settings, _auto_close_ms=None):
|
||||
"""Показать мастер и дождаться закрытия. settings меняется на месте."""
|
||||
root = tk.Tk()
|
||||
Wizard(root, settings)
|
||||
if _auto_close_ms: # тестовый режим: просто мигнуть окном
|
||||
root.after(_auto_close_ms, root.destroy)
|
||||
root.mainloop()
|
||||
Reference in new issue
Block a user