Files
PixelLauncher/launcher/onboarding.py
T
kosta2014p ec6f13ddcc PixelLauncher 2.1.0 — стабильная сборка
Офлайн-лаунчер Minecraft: аккаунты без Microsoft, установка версий и лоадеров
(Fabric, Quilt, Forge, NeoForge), каталог модпаков Modrinth, поиск и обновление
модов, разбор вылетов, системный трей, автозагрузка, плагины, прокси и зеркала.
2026-10-18 22:44:19 +07:00

306 lines
14 KiB
Python

# -*- 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()