feat: v1.2.926 资源中心
- 启动器内一键下载游戏版本 (官方源) - Mod & 插件下载 (Modrinth API, 支持MC版本/加载器筛选) - GUI改为标签页布局: 启动器 + 资源中心 - 版本号升级至 1.2.926
This commit is contained in:
1 parent
8823b9218d
commit
e4cc14cb9e
2 files changed
+194
-18
No files matched your search
@@ -5,6 +5,7 @@
|
||||
## 功能特性
|
||||
|
||||
- 🖥️ 图形界面(Tkinter),Windows / Linux / macOS 三端界面一致
|
||||
- 📥 资源中心:启动器内一键下载游戏版本、Mod 与服务器插件(Modrinth)
|
||||
- 🔑 第三方登录:支持 LittleSkin、Blessing Skin 等 authlib-injector 皮肤站(Yggdrasil 协议)
|
||||
- 🎭 离线模式:自动生成与官方一致的离线 UUID
|
||||
- 📦 自动获取官方版本清单,支持任意版本(`1.20.4`、`latest-release`、`latest-snapshot`)
|
||||
|
||||
+193
-18
@@ -13,6 +13,7 @@ import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import uuid as uuid_mod
|
||||
import zipfile
|
||||
@@ -24,7 +25,8 @@ from pathlib import Path
|
||||
|
||||
VERSION_MANIFEST_URL = "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json"
|
||||
AUTHLIB_INJECTOR_API = "https://api.github.com/repos/yushijinhun/authlib-injector/releases/latest"
|
||||
USER_AGENT = "Mozilla/5.0 (MeowLauncher/1.2)"
|
||||
VERSION = "1.2.926"
|
||||
USER_AGENT = "Mozilla/5.0 (MeowLauncher/%s)" % VERSION
|
||||
|
||||
OS_NAME = {"Windows": "windows", "Linux": "linux", "Darwin": "osx"}.get(platform.system(), "linux")
|
||||
|
||||
@@ -135,6 +137,43 @@ def library_allowed(lib: dict) -> bool:
|
||||
|
||||
|
||||
|
||||
|
||||
class ModrinthClient:
|
||||
BASE = "https://api.modrinth.com/v2"
|
||||
|
||||
def search(self, query="", game_versions=None, loaders=None, project_type=None, limit=20):
|
||||
facets = []
|
||||
if game_versions:
|
||||
facets.append(["versions:%s" % game_versions])
|
||||
if loaders:
|
||||
facets.append(["categories:%s" % loaders])
|
||||
if project_type:
|
||||
facets.append(["project_type:%s" % project_type])
|
||||
url = "%s/search?query=%s&limit=%d" % (self.BASE, urllib.parse.quote(query), limit)
|
||||
if facets:
|
||||
url += "&facets=" + urllib.parse.quote(json.dumps(facets))
|
||||
data = json.loads(http_get(url).decode("utf-8"))
|
||||
return data.get("hits", [])
|
||||
|
||||
def project_versions(self, project_id, game_version=None, loader=None):
|
||||
url = "%s/project/%s/version" % (self.BASE, project_id)
|
||||
params = []
|
||||
if game_version:
|
||||
params.append("game_versions=%s" % urllib.parse.quote(json.dumps([game_version])))
|
||||
if loader:
|
||||
params.append("loaders=%s" % urllib.parse.quote(json.dumps([loader])))
|
||||
if params:
|
||||
url += "?" + "&".join(params)
|
||||
return json.loads(http_get(url).decode("utf-8"))
|
||||
|
||||
def first_file(self, project_id, game_version, loader):
|
||||
versions = self.project_versions(project_id, game_version, loader)
|
||||
for v in versions:
|
||||
for f in v.get("files", []):
|
||||
if f.get("url"):
|
||||
return f
|
||||
return None
|
||||
|
||||
class YggdrasilAuth:
|
||||
|
||||
PRESETS = {
|
||||
@@ -470,15 +509,17 @@ class MeowLauncherApp:
|
||||
|
||||
def __init__(self):
|
||||
self.root = tk.Tk()
|
||||
self.root.title("MeowLauncher - Minecraft 启动器")
|
||||
self.root.geometry("520x720")
|
||||
self.root.minsize(480, 640)
|
||||
self.root.title("MeowLauncher %s - Minecraft 启动器" % VERSION)
|
||||
self.root.geometry("600x780")
|
||||
self.root.minsize(540, 680)
|
||||
self.root.configure(bg=self.BG)
|
||||
|
||||
self.msg_queue = queue.Queue()
|
||||
self.auth = None
|
||||
self.yggdrasil = None
|
||||
self.versions_cache = []
|
||||
self.modrinth = ModrinthClient()
|
||||
self.res_hits = []
|
||||
|
||||
self._setup_style()
|
||||
self._build_ui()
|
||||
@@ -512,6 +553,9 @@ class MeowLauncherApp:
|
||||
style.configure("TCheckbutton", background=self.BG, foreground=self.TEXT)
|
||||
style.configure("Horizontal.TProgressbar", background=self.ACCENT, troughcolor=self.CARD)
|
||||
style.configure("TRadiobutton", background=self.BG, foreground=self.TEXT)
|
||||
style.configure("Treeview", background=self.CARD, fieldbackground=self.CARD,
|
||||
foreground=self.TEXT)
|
||||
style.configure("Treeview.Heading", background=self.CARD, foreground=self.TEXT)
|
||||
|
||||
|
||||
|
||||
@@ -523,12 +567,21 @@ class MeowLauncherApp:
|
||||
return frame
|
||||
|
||||
def _build_ui(self):
|
||||
|
||||
ttk.Label(self.root, text="MeowLauncher", style="Title.TLabel").pack(pady=(18, 0))
|
||||
ttk.Label(self.root, text="跨平台 Minecraft 启动器", style="Dim.TLabel").pack()
|
||||
ttk.Label(self.root, text="MeowLauncher", style="Title.TLabel").pack(pady=(14, 0))
|
||||
ttk.Label(self.root, text="跨平台 Minecraft 启动器 v%s" % VERSION, style="Dim.TLabel").pack()
|
||||
|
||||
|
||||
login = self._card(self.root, "账号登录")
|
||||
self.notebook = ttk.Notebook(self.root)
|
||||
self.notebook.pack(fill="both", expand=True, padx=10, pady=8)
|
||||
|
||||
self.tab_launch = ttk.Frame(self.notebook)
|
||||
self.tab_res = ttk.Frame(self.notebook)
|
||||
self.notebook.add(self.tab_launch, text=" 启动器 ")
|
||||
self.notebook.add(self.tab_res, text=" 资源中心 ")
|
||||
self._build_launch_tab()
|
||||
self._build_res_tab()
|
||||
|
||||
def _build_launch_tab(self):
|
||||
login = self._card(self.tab_launch, "账号登录")
|
||||
self.login_mode = tk.StringVar(value="offline")
|
||||
row = ttk.Frame(login, style="Card.TFrame")
|
||||
row.pack(fill="x", pady=4)
|
||||
@@ -565,8 +618,7 @@ class MeowLauncherApp:
|
||||
ttk.Label(self.ygg_frame, text="支持 LittleSkin 等 authlib-injector 皮肤站",
|
||||
style="Card.TLabel").grid(row=3, column=0, columnspan=2, sticky="w", pady=2)
|
||||
|
||||
|
||||
ver = self._card(self.root, "游戏版本")
|
||||
ver = self._card(self.tab_launch, "游戏版本")
|
||||
row = ttk.Frame(ver, style="Card.TFrame")
|
||||
row.pack(fill="x")
|
||||
self.version_var = tk.StringVar()
|
||||
@@ -577,8 +629,7 @@ class MeowLauncherApp:
|
||||
ttk.Checkbutton(ver, text="显示快照版", variable=self.show_snapshot,
|
||||
command=self.refresh_versions).pack(anchor="w", pady=(6, 0))
|
||||
|
||||
|
||||
cfg = self._card(self.root, "启动设置")
|
||||
cfg = self._card(self.tab_launch, "启动设置")
|
||||
ttk.Label(cfg, text="最大内存 (MB):", style="Card.TLabel").grid(row=0, column=0, sticky="w", pady=3)
|
||||
self.mem_var = tk.StringVar(value="2048")
|
||||
ttk.Entry(cfg, textvariable=self.mem_var, width=10).grid(row=0, column=1, sticky="w", pady=3)
|
||||
@@ -595,8 +646,7 @@ class MeowLauncherApp:
|
||||
ttk.Entry(row, textvariable=self.dir_var, width=16).pack(side="left")
|
||||
ttk.Button(row, text="浏览", command=self._pick_dir).pack(side="left", padx=4)
|
||||
|
||||
|
||||
log_card = self._card(self.root, "日志")
|
||||
log_card = self._card(self.tab_launch, "日志")
|
||||
self.log_box = scrolledtext.ScrolledText(log_card, height=8, bg="#151521",
|
||||
fg=self.TEXT, font=("Consolas", 9),
|
||||
relief="flat", state="disabled")
|
||||
@@ -604,12 +654,124 @@ class MeowLauncherApp:
|
||||
self.progress = ttk.Progressbar(log_card, mode="determinate", maximum=100)
|
||||
self.progress.pack(fill="x", pady=(8, 0))
|
||||
|
||||
|
||||
self.launch_btn = ttk.Button(self.root, text="启动游戏", style="Accent.TButton",
|
||||
self.launch_btn = ttk.Button(self.tab_launch, text="启动游戏", style="Accent.TButton",
|
||||
command=self._on_launch)
|
||||
self.launch_btn.pack(fill="x", padx=14, pady=14)
|
||||
|
||||
|
||||
def _build_res_tab(self):
|
||||
dver = self._card(self.tab_res, "游戏版本下载")
|
||||
row = ttk.Frame(dver, style="Card.TFrame")
|
||||
row.pack(fill="x")
|
||||
self.res_version_var = tk.StringVar()
|
||||
self.res_version_box = ttk.Combobox(row, textvariable=self.res_version_var, state="readonly", width=22)
|
||||
self.res_version_box.pack(side="left")
|
||||
ttk.Button(row, text="刷新列表", command=self.refresh_versions).pack(side="left", padx=6)
|
||||
ttk.Button(row, text="一键下载", command=self._download_version_only).pack(side="left", padx=6)
|
||||
|
||||
dmod = self._card(self.tab_res, "Mod / 插件下载 (Modrinth)")
|
||||
row = ttk.Frame(dmod, style="Card.TFrame")
|
||||
row.pack(fill="x")
|
||||
self.res_query = ttk.Entry(row, width=14)
|
||||
self.res_query.pack(side="left")
|
||||
self.res_type_var = tk.StringVar(value="mod")
|
||||
ttk.Combobox(row, textvariable=self.res_type_var, values=["mod", "plugin"],
|
||||
state="readonly", width=8).pack(side="left", padx=4)
|
||||
self.res_mcver_var = tk.StringVar(value="")
|
||||
ttk.Combobox(row, textvariable=self.res_mcver_var,
|
||||
values=["", "1.21", "1.20.4", "1.20.1", "1.19.4", "1.18.2", "1.16.5"],
|
||||
state="readonly", width=8).pack(side="left", padx=4)
|
||||
self.res_loader_var = tk.StringVar(value="fabric")
|
||||
ttk.Combobox(row, textvariable=self.res_loader_var,
|
||||
values=["fabric", "forge", "neoforge", "quilt", "paper", "spigot", "bukkit"],
|
||||
state="readonly", width=9).pack(side="left", padx=4)
|
||||
ttk.Button(row, text="搜索", command=self._search_res).pack(side="left", padx=4)
|
||||
|
||||
self.res_tree = ttk.Treeview(dmod, columns=("title", "author", "downloads"),
|
||||
show="headings", height=6)
|
||||
self.res_tree.heading("title", text="名称")
|
||||
self.res_tree.heading("author", text="作者")
|
||||
self.res_tree.heading("downloads", text="下载量")
|
||||
self.res_tree.column("title", width=190)
|
||||
self.res_tree.column("author", width=90)
|
||||
self.res_tree.column("downloads", width=80)
|
||||
self.res_tree.pack(fill="x", pady=6)
|
||||
ttk.Button(dmod, text="下载选中项", command=self._download_selected).pack(anchor="w")
|
||||
|
||||
self.res_log = scrolledtext.ScrolledText(self.tab_res, height=5, bg="#151521",
|
||||
fg=self.TEXT, font=("Consolas", 9),
|
||||
relief="flat", state="disabled")
|
||||
self.res_log.pack(fill="x", padx=14, pady=(4, 6))
|
||||
self.res_progress = ttk.Progressbar(self.tab_res, mode="determinate", maximum=100)
|
||||
self.res_progress.pack(fill="x", padx=14, pady=(0, 10))
|
||||
|
||||
def _res_append(self, msg):
|
||||
self.res_log.config(state="normal")
|
||||
self.res_log.insert("end", msg + "\n")
|
||||
self.res_log.see("end")
|
||||
self.res_log.config(state="disabled")
|
||||
|
||||
def _search_res(self):
|
||||
def worker():
|
||||
try:
|
||||
q = self.res_query.get().strip()
|
||||
pt = self.res_type_var.get()
|
||||
mc = self.res_mcver_var.get() or None
|
||||
ld = self.res_loader_var.get() or None
|
||||
hits = self.modrinth.search(query=q, game_versions=mc, loaders=ld,
|
||||
project_type=pt, limit=20)
|
||||
self.msg_queue.put(("res_hits", hits))
|
||||
except Exception as e:
|
||||
self.msg_queue.put(("reslog", "[资源] 搜索失败: %s" % e))
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def _download_version_only(self):
|
||||
def worker():
|
||||
try:
|
||||
version = self.res_version_var.get()
|
||||
if not version:
|
||||
raise RuntimeError("请先选择版本")
|
||||
self.msg_queue.put(("reslog", "[版本] 开始下载 %s ..." % version))
|
||||
l = MinecraftLauncher(game_dir=self.dir_var.get(), version_id=version,
|
||||
username="x",
|
||||
progress_cb=lambda m: self.msg_queue.put(("reslog", m)))
|
||||
l.resolve_version()
|
||||
l.download_client()
|
||||
l.download_libraries()
|
||||
l.download_assets()
|
||||
self.msg_queue.put(("reslog", "[版本] %s 下载完成!" % version))
|
||||
except Exception as e:
|
||||
self.msg_queue.put(("reslog", "[错误] %s" % e))
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def _download_selected(self):
|
||||
sel = self.res_tree.selection()
|
||||
if not sel:
|
||||
return
|
||||
idx = int(sel[0])
|
||||
if idx >= len(self.res_hits):
|
||||
return
|
||||
hit = self.res_hits[idx]
|
||||
pt = self.res_type_var.get()
|
||||
|
||||
def worker():
|
||||
try:
|
||||
mc = self.res_mcver_var.get() or None
|
||||
ld = self.res_loader_var.get() or None
|
||||
self.msg_queue.put(("reslog", "[下载] 获取 %s 的版本信息 ..." % hit.get("title", "")))
|
||||
f = self.modrinth.first_file(hit["project_id"], mc, ld)
|
||||
if not f:
|
||||
raise RuntimeError("没有匹配该MC版本/加载器的文件")
|
||||
base = Path(self.dir_var.get())
|
||||
target_dir = base / ("mods" if pt == "mod" else "plugins")
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
dest = target_dir / f["filename"]
|
||||
size = f.get("size", 0) // 1024
|
||||
self.msg_queue.put(("reslog", "[下载] %s (%d KB)" % (f["filename"], size)))
|
||||
download_file(f["url"], dest)
|
||||
self.msg_queue.put(("reslog", "[下载] 完成! 已保存到 %s" % dest))
|
||||
except Exception as e:
|
||||
self.msg_queue.put(("reslog", "[错误] %s" % e))
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def _toggle_login(self):
|
||||
if self.login_mode.get() == "offline":
|
||||
@@ -649,6 +811,17 @@ class MeowLauncherApp:
|
||||
elif kind == "versions":
|
||||
self.versions_cache = payload["versions"]
|
||||
self._fill_versions(payload["latest"])
|
||||
elif kind == "res_hits":
|
||||
hits = payload
|
||||
self.res_hits = hits
|
||||
self.res_tree.delete(*self.res_tree.get_children())
|
||||
for i, h in enumerate(hits):
|
||||
self.res_tree.insert("", "end", iid=str(i),
|
||||
values=(h.get("title", ""), h.get("author", ""),
|
||||
h.get("downloads", 0)))
|
||||
self._res_append("[资源] 找到 %d 个结果" % len(hits))
|
||||
elif kind == "reslog":
|
||||
self._res_append(payload)
|
||||
elif kind == "done":
|
||||
self.launch_btn.config(state="normal")
|
||||
elif kind == "err":
|
||||
@@ -665,6 +838,8 @@ class MeowLauncherApp:
|
||||
items = list(dict.fromkeys(items))
|
||||
self.version_box["values"] = items
|
||||
self.version_var.set(latest.get("release") or (items[0] if items else ""))
|
||||
self.res_version_box["values"] = items
|
||||
self.res_version_var.set(latest.get("release") or (items[0] if items else ""))
|
||||
|
||||
def _append_log(self, msg):
|
||||
self.log_box.config(state="normal")
|
||||
|
||||
Reference in new issue
Block a user