Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8aa654198 | ||
|
|
89aa661e75 | ||
|
|
e53c0bf1fd | ||
|
|
8e9b0894ba | ||
|
|
f81a89adc9 |
No files matched your search
@@ -57,6 +57,7 @@ import SymlinkMethodCards from '@modrinth/ui/src/components/flows/drop/SymlinkMe
|
|||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
import { getVersion } from '@tauri-apps/api/app'
|
import { getVersion } from '@tauri-apps/api/app'
|
||||||
import { convertFileSrc, invoke } from '@tauri-apps/api/core'
|
import { convertFileSrc, invoke } from '@tauri-apps/api/core'
|
||||||
|
import { hideAllPoppers } from 'floating-vue'
|
||||||
import { listen } from '@tauri-apps/api/event'
|
import { listen } from '@tauri-apps/api/event'
|
||||||
import { Effect, getCurrentWindow } from '@tauri-apps/api/window'
|
import { Effect, getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
|
import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
|
||||||
@@ -1461,6 +1462,7 @@ function syncDiscordActivity(to: RouteLocationNormalizedLoaded) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
router.afterEach((to, from, failure) => {
|
router.afterEach((to, from, failure) => {
|
||||||
|
hideAllPoppers()
|
||||||
if (!failure) void invoke('lightweight_mode_set_route', { route: to.fullPath })
|
if (!failure) void invoke('lightweight_mode_set_route', { route: to.fullPath })
|
||||||
trackEvent('PageView', {
|
trackEvent('PageView', {
|
||||||
path: to.path,
|
path: to.path,
|
||||||
|
|||||||
@@ -32,6 +32,68 @@ export const ANNOUNCEMENT_CHANGE_TYPES: readonly AnnouncementChangeType[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export const launcherAnnouncements: readonly LauncherAnnouncement[] = [
|
export const launcherAnnouncements: readonly LauncherAnnouncement[] = [
|
||||||
|
{
|
||||||
|
id: 'launcher-1.0.0-beta1',
|
||||||
|
version: '1.0.0-beta1',
|
||||||
|
publishedAt: '2026-09-13',
|
||||||
|
title: {
|
||||||
|
'en-US': 'Starlight Launcher 1.0.0-beta1',
|
||||||
|
'zh-CN': 'Starlight Launcher 1.0.0-beta1',
|
||||||
|
},
|
||||||
|
changes: {
|
||||||
|
changed: [
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'The launcher has been rebranded from Axolotl Launcher to Starlight Launcher, with a new icon and a refreshed about page.',
|
||||||
|
'zh-CN': '启动器已从 Axolotl Launcher 更名为 Starlight Launcher,并更换了全新图标与关于页。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'The "Powered by" and copyright notices now correctly credit Axolotl Launcher as the upstream project.',
|
||||||
|
'zh-CN': '版权与 "Powered by" 署名现在正确标注上游项目 Axolotl Launcher。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'en-US': 'The developer list on the about page now shows Ax_Tps and Disy920.',
|
||||||
|
'zh-CN': '关于页的开发组现在显示 Ax_Tps 与 Disy920。',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
added: [
|
||||||
|
{
|
||||||
|
'en-US': 'A "Starlight" entry was added to the sidebar, which embeds the StarLight skin site.',
|
||||||
|
'zh-CN': '侧边栏新增「斯达莱特」入口,内嵌 StarLight 皮肤站。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'A launch progress panel was added to the home sidebar, showing the current launch stage and progress.',
|
||||||
|
'zh-CN': '主页侧边栏新增启动进度面板,显示当前启动阶段与进度。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'An easter egg mini-game (star merge) was added, reachable via the secret code "starlight", the Konami code, or a long press on a developer name on the about page.',
|
||||||
|
'zh-CN':
|
||||||
|
'新增彩蛋小游戏(下界之星合成),可通过暗号 starlight、Konami 秘技或长按关于页开发者名字触发。',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
removed: [
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'The hard-coded Starlight official server was removed from "Pinned servers"; only servers you favorite yourself are shown now.',
|
||||||
|
'zh-CN': '移除了「固定的服务器」中写死的 Starlight 官方服务器,现在只显示你自己收藏的服务器。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'en-US': 'The swimming axolotl animation on the about page was replaced with a static nether star.',
|
||||||
|
'zh-CN': '关于页中游动的美西螈动画已替换为静止的下界之星。',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fixed: [
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'Minecraft now launches in a maximized window when the maximize option is enabled.',
|
||||||
|
'zh-CN': '启用最大化选项后,Minecraft 现在会以最大化窗口启动。',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'launcher-1.9.6-beta.3',
|
id: 'launcher-1.9.6-beta.3',
|
||||||
version: '1.9.6-beta.3',
|
version: '1.9.6-beta.3',
|
||||||
|
|||||||
@@ -225,12 +225,21 @@ const breadcrumbs = computed<Breadcrumb[]>(() => {
|
|||||||
function resolveLabel(name: string): string {
|
function resolveLabel(name: string): string {
|
||||||
return resolveBreadcrumbLabel(
|
return resolveBreadcrumbLabel(
|
||||||
name,
|
name,
|
||||||
(key) => breadcrumbData.getName(key),
|
(key) => breadcrumbData.getName(key) || fallbackDynamicLabel(key),
|
||||||
staticLabels,
|
staticLabels,
|
||||||
(message) => formatMessage(message),
|
(message) => formatMessage(message),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 动态面包屑(?Xxx)在页面异步 setName 之前,回退到静态标签,避免图标与文字不同步
|
||||||
|
function fallbackDynamicLabel(key: string): string {
|
||||||
|
const fallback: Record<string, keyof typeof staticLabels> = {
|
||||||
|
BrowseTitle: 'Discover content',
|
||||||
|
}
|
||||||
|
const staticKey = fallback[key]
|
||||||
|
return staticKey ? formatMessage(staticLabels[staticKey]) : ''
|
||||||
|
}
|
||||||
|
|
||||||
function resolveIcon(breadcrumb: Breadcrumb): Component | undefined {
|
function resolveIcon(breadcrumb: Breadcrumb): Component | undefined {
|
||||||
if (breadcrumb.iconUrl || breadcrumbData.getIcon(breadcrumb.name.slice(1))) return undefined
|
if (breadcrumb.iconUrl || breadcrumbData.getIcon(breadcrumb.name.slice(1))) return undefined
|
||||||
const dynamicIcons: Record<string, Component> = {
|
const dynamicIcons: Record<string, Component> = {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Avatar, defineMessages, NewButton as Button, useVIntl } from '@modrinth/ui'
|
import { Avatar, defineMessages, NewButton as Button, useVIntl } from '@modrinth/ui'
|
||||||
import { getVersion } from '@tauri-apps/api/app'
|
import { getVersion } from '@tauri-apps/api/app'
|
||||||
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
import { inject, nextTick, onScopeDispose, ref, shallowRef } from 'vue'
|
import { inject, nextTick, onScopeDispose, ref, shallowRef } from 'vue'
|
||||||
import EasterEggContributorsModal from '@/components/ui/easteregg/EasterEggContributorsModal.vue'
|
import EasterEggContributorsModal from '@/components/ui/easteregg/EasterEggContributorsModal.vue'
|
||||||
import EasterEggGameModal from '@/components/ui/easteregg/EasterEggGameModal.vue'
|
import EasterEggGameModal from '@/components/ui/easteregg/EasterEggGameModal.vue'
|
||||||
@@ -80,6 +81,7 @@ const contributorsModal = ref<InstanceType<typeof EasterEggContributorsModal> |
|
|||||||
|
|
||||||
let typedBuffer = ''
|
let typedBuffer = ''
|
||||||
const secretCodes = ['starlight']
|
const secretCodes = ['starlight']
|
||||||
|
const YUANSHEN_URL = 'https://ys.mihoyo.com/cloud/'
|
||||||
|
|
||||||
const konamiSequence = [
|
const konamiSequence = [
|
||||||
'ArrowUp',
|
'ArrowUp',
|
||||||
@@ -101,6 +103,11 @@ function handleEasterEggKeydown(event: KeyboardEvent) {
|
|||||||
if (typedBuffer.length > maxCodeLen) {
|
if (typedBuffer.length > maxCodeLen) {
|
||||||
typedBuffer = typedBuffer.slice(-maxCodeLen)
|
typedBuffer = typedBuffer.slice(-maxCodeLen)
|
||||||
}
|
}
|
||||||
|
if (typedBuffer.endsWith('yuanshen')) {
|
||||||
|
typedBuffer = ''
|
||||||
|
openUrl(YUANSHEN_URL).catch(() => {})
|
||||||
|
return
|
||||||
|
}
|
||||||
if (secretCodes.some((code) => typedBuffer.endsWith(code))) {
|
if (secretCodes.some((code) => typedBuffer.endsWith(code))) {
|
||||||
typedBuffer = ''
|
typedBuffer = ''
|
||||||
gameModal.value?.show()
|
gameModal.value?.show()
|
||||||
|
|||||||
@@ -198,19 +198,14 @@ pub async fn begin_yggdrasil_login(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(profile) = response.selected_profile {
|
// 收集本次登录可用的所有角色;优先使用 availableProfiles,回退到 selectedProfile。
|
||||||
let credentials = create_credentials(
|
let mut profiles: Vec<YggdrasilProfile> = response.available_profiles.clone();
|
||||||
profile,
|
if profiles.is_empty() {
|
||||||
response.access_token,
|
if let Some(selected) = response.selected_profile.clone() {
|
||||||
response.client_token,
|
profiles.push(selected);
|
||||||
metadata,
|
}
|
||||||
login,
|
|
||||||
);
|
|
||||||
credentials.upsert(exec).await?;
|
|
||||||
return Ok(YggdrasilLoginResult::Complete { credentials });
|
|
||||||
}
|
}
|
||||||
|
if profiles.is_empty() {
|
||||||
if response.available_profiles.is_empty() {
|
|
||||||
return Err(ErrorKind::OtherError(
|
return Err(ErrorKind::OtherError(
|
||||||
"The Yggdrasil account does not have a Minecraft profile"
|
"The Yggdrasil account does not have a Minecraft profile"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@@ -218,38 +213,16 @@ pub async fn begin_yggdrasil_login(
|
|||||||
.as_error());
|
.as_error());
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.available_profiles.len() == 1 {
|
// 不在此处激活任何角色:authenticate 只拿到一条 clientToken 链,
|
||||||
let profile = response.available_profiles[0].clone();
|
// 若循环 refresh 多个角色,同一 clientToken 下后一次会顶掉前一次的
|
||||||
let refreshed = refresh_selected_profile(
|
// accessToken,导致只有最后一个角色有效。因此改为返回
|
||||||
&metadata.api_root,
|
// `SelectProfile`,由前端弹出选择框,老板选中后用
|
||||||
&response.access_token,
|
// `finish_yggdrasil_login` 单独 refresh 该角色。
|
||||||
&response.client_token,
|
// 需要多个角色时重复登录、各选一个,每个角色持有独立的 clientToken。
|
||||||
&profile,
|
let _ = exec;
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let selected_profile = refreshed.selected_profile.ok_or_else(|| {
|
|
||||||
ErrorKind::OtherError(
|
|
||||||
"The Yggdrasil service did not select the requested profile"
|
|
||||||
.to_string(),
|
|
||||||
)
|
|
||||||
.as_error()
|
|
||||||
})?;
|
|
||||||
let credentials = create_credentials(
|
|
||||||
selected_profile,
|
|
||||||
refreshed.access_token,
|
|
||||||
refreshed.client_token,
|
|
||||||
metadata,
|
|
||||||
login,
|
|
||||||
);
|
|
||||||
credentials.upsert(exec).await?;
|
|
||||||
return Ok(YggdrasilLoginResult::Complete { credentials });
|
|
||||||
}
|
|
||||||
|
|
||||||
let flow_id = Uuid::new_v4();
|
let flow_id = Uuid::new_v4();
|
||||||
let profiles = response.available_profiles.clone();
|
PENDING_LOGINS.lock().await.insert(
|
||||||
let mut pending = PENDING_LOGINS.lock().await;
|
|
||||||
pending.retain(|_, login| login.created.elapsed() < PENDING_LOGIN_LIFETIME);
|
|
||||||
pending.insert(
|
|
||||||
flow_id,
|
flow_id,
|
||||||
PendingYggdrasilLogin {
|
PendingYggdrasilLogin {
|
||||||
created: Instant::now(),
|
created: Instant::now(),
|
||||||
@@ -258,7 +231,7 @@ pub async fn begin_yggdrasil_login(
|
|||||||
login: login.to_string(),
|
login: login.to_string(),
|
||||||
access_token: response.access_token,
|
access_token: response.access_token,
|
||||||
client_token: response.client_token,
|
client_token: response.client_token,
|
||||||
profiles: response.available_profiles,
|
profiles: profiles.clone(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
|
||||||
|
p = r"D:\Project\Starlight_Lancher\apps\app-frontend\src\announcements\catalog.ts"
|
||||||
|
t = open(p, encoding="utf-8").read()
|
||||||
|
|
||||||
|
entry = """ {
|
||||||
|
id: 'launcher-1.0.0-beta1',
|
||||||
|
version: '1.0.0-beta1',
|
||||||
|
publishedAt: '2026-09-13',
|
||||||
|
title: {
|
||||||
|
'en-US': 'Starlight Launcher 1.0.0-beta1',
|
||||||
|
'zh-CN': 'Starlight Launcher 1.0.0-beta1',
|
||||||
|
},
|
||||||
|
changes: {
|
||||||
|
changed: [
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'The launcher has been rebranded from Axolotl Launcher to Starlight Launcher, with a new icon and a refreshed about page.',
|
||||||
|
'zh-CN': '启动器已从 Axolotl Launcher 更名为 Starlight Launcher,并更换了全新图标与关于页。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'The "Powered by" and copyright notices now correctly credit Axolotl Launcher as the upstream project.',
|
||||||
|
'zh-CN': '版权与 "Powered by" 署名现在正确标注上游项目 Axolotl Launcher。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'en-US': 'The developer list on the about page now shows Ax_Tps and Disy920.',
|
||||||
|
'zh-CN': '关于页的开发组现在显示 Ax_Tps 与 Disy920。',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
added: [
|
||||||
|
{
|
||||||
|
'en-US': 'A "Starlight" entry was added to the sidebar, which embeds the StarLight skin site.',
|
||||||
|
'zh-CN': '侧边栏新增「斯达莱特」入口,内嵌 StarLight 皮肤站。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'A launch progress panel was added to the home sidebar, showing the current launch stage and progress.',
|
||||||
|
'zh-CN': '主页侧边栏新增启动进度面板,显示当前启动阶段与进度。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'An easter egg mini-game (star merge) was added, reachable via the secret code "starlight", the Konami code, or a long press on a developer name on the about page.',
|
||||||
|
'zh-CN':
|
||||||
|
'新增彩蛋小游戏(下界之星合成),可通过暗号 starlight、Konami 秘技或长按关于页开发者名字触发。',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
removed: [
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'The hard-coded Starlight official server was removed from "Pinned servers"; only servers you favorite yourself are shown now.',
|
||||||
|
'zh-CN': '移除了「固定的服务器」中写死的 Starlight 官方服务器,现在只显示你自己收藏的服务器。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'en-US': 'The swimming axolotl animation on the about page was replaced with a static nether star.',
|
||||||
|
'zh-CN': '关于页中游动的美西螈动画已替换为静止的下界之星。',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fixed: [
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'Minecraft now launches in a maximized window when the maximize option is enabled.',
|
||||||
|
'zh-CN': '启用最大化选项后,Minecraft 现在会以最大化窗口启动。',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"""
|
||||||
|
|
||||||
|
anchor = "export const launcherAnnouncements: readonly LauncherAnnouncement[] = [\n"
|
||||||
|
assert anchor in t, "找不到数组开头"
|
||||||
|
t = t.replace(anchor, anchor + entry, 1)
|
||||||
|
|
||||||
|
open(p, "w", encoding="utf-8", newline="\n").write(t)
|
||||||
|
print("done")
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
p = r"D:\Project\Starlight_Lancher\apps\app-frontend\src\components\ui\settings\AboutSettings.vue"
|
||||||
|
t = open(p, encoding="utf-8").read()
|
||||||
|
|
||||||
|
# 1) import openUrl
|
||||||
|
old_imp = "import { getVersion } from '@tauri-apps/api/app'"
|
||||||
|
new_imp = "import { getVersion } from '@tauri-apps/api/app'\nimport { openUrl } from '@tauri-apps/plugin-opener'"
|
||||||
|
assert old_imp in t, "getVersion import 找不到"
|
||||||
|
t = t.replace(old_imp, new_imp, 1)
|
||||||
|
|
||||||
|
# 2) 暗号逻辑:区分 starlight(游戏)与 yuanshen(开链接)
|
||||||
|
old_logic = """let typedBuffer = ''
|
||||||
|
const secretCodes = ['starlight']"""
|
||||||
|
new_logic = """let typedBuffer = ''
|
||||||
|
const secretCodes = ['starlight']
|
||||||
|
const YUANSHEN_URL = 'https://ys.mihoyo.com/cloud/'"""
|
||||||
|
assert old_logic in t, "secretCodes 找不到"
|
||||||
|
t = t.replace(old_logic, new_logic, 1)
|
||||||
|
|
||||||
|
old_check = """ if (secretCodes.some((code) => typedBuffer.endsWith(code))) {
|
||||||
|
typedBuffer = ''
|
||||||
|
gameModal.value?.show()
|
||||||
|
return
|
||||||
|
}"""
|
||||||
|
new_check = """ if (typedBuffer.endsWith('yuanshen')) {
|
||||||
|
typedBuffer = ''
|
||||||
|
openUrl(YUANSHEN_URL).catch(() => {})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (secretCodes.some((code) => typedBuffer.endsWith(code))) {
|
||||||
|
typedBuffer = ''
|
||||||
|
gameModal.value?.show()
|
||||||
|
return
|
||||||
|
}"""
|
||||||
|
assert old_check in t, "暗号判断块找不到"
|
||||||
|
t = t.replace(old_check, new_check, 1)
|
||||||
|
|
||||||
|
open(p, "w", encoding="utf-8", newline="\n").write(t)
|
||||||
|
print("done")
|
||||||
|
print(" 含 openUrl import:", "plugin-opener" in t)
|
||||||
|
print(" 含 yuanshen:", "yuanshen" in t)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from PIL import Image
|
||||||
|
im = Image.open(r"C:\Users\28579\Downloads\resources\resources\netherstar.png")
|
||||||
|
print("模式:", im.mode)
|
||||||
|
print("尺寸:", im.size)
|
||||||
|
print("有 alpha:", im.mode in ("RGBA", "LA"))
|
||||||
|
if im.mode in ("RGBA", "LA"):
|
||||||
|
a = im.getchannel("A")
|
||||||
|
print("alpha bbox:", a.getbbox())
|
||||||
|
print("alpha 范围:", a.getextrema())
|
||||||
|
# 采样角落像素,看是不是白/透明
|
||||||
|
w, h = im.size
|
||||||
|
print("左上角:", im.getpixel((0, 0)))
|
||||||
|
print("中心:", im.getpixel((w // 2, h // 2)))
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import glob, os
|
||||||
|
cands = glob.glob(r"D:\Project\Starlight_Lancher\node_modules\.pnpm\floating-vue*\node_modules\floating-vue\dist\*.mjs")
|
||||||
|
cands += glob.glob(r"D:\Project\Starlight_Lancher\node_modules\floating-vue\dist\*")
|
||||||
|
print("候选:", cands[:5])
|
||||||
|
found = False
|
||||||
|
for c in cands:
|
||||||
|
if not os.path.isfile(c):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
t = open(c, encoding="utf-8", errors="replace").read()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if "hideAllTooltips" in t or "disposeTooltips" in t:
|
||||||
|
print("文件:", c)
|
||||||
|
print(" 含 hideAllTooltips:", "hideAllTooltips" in t)
|
||||||
|
print(" 含 disposeTooltips:", "disposeTooltips" in t)
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
|
print("没找到(可能打包成 cjs/index)")
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
p = r"D:\Project\Starlight_Lancher\apps\app-frontend\src\components\ui\Breadcrumbs.vue"
|
||||||
|
t = open(p, encoding="utf-8").read()
|
||||||
|
|
||||||
|
old = """function resolveLabel(name: string): string {
|
||||||
|
return resolveBreadcrumbLabel(
|
||||||
|
name,
|
||||||
|
(key) => breadcrumbData.getName(key),
|
||||||
|
staticLabels,
|
||||||
|
(message) => formatMessage(message),
|
||||||
|
)
|
||||||
|
}"""
|
||||||
|
new = """function resolveLabel(name: string): string {
|
||||||
|
return resolveBreadcrumbLabel(
|
||||||
|
name,
|
||||||
|
(key) => breadcrumbData.getName(key) || fallbackDynamicLabel(key),
|
||||||
|
staticLabels,
|
||||||
|
(message) => formatMessage(message),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态面包屑(?Xxx)在页面异步 setName 之前,回退到静态标签,避免图标与文字不同步
|
||||||
|
function fallbackDynamicLabel(key: string): string {
|
||||||
|
const fallback: Record<string, keyof typeof staticLabels> = {
|
||||||
|
BrowseTitle: 'Discover content',
|
||||||
|
}
|
||||||
|
const staticKey = fallback[key]
|
||||||
|
return staticKey ? formatMessage(staticLabels[staticKey]) : ''
|
||||||
|
}"""
|
||||||
|
|
||||||
|
assert old in t, "resolveLabel 找不到"
|
||||||
|
t = t.replace(old, new, 1)
|
||||||
|
open(p, "w", encoding="utf-8", newline="\n").write(t)
|
||||||
|
print("done")
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
from PIL import Image
|
||||||
|
import numpy as np
|
||||||
|
from scipy import ndimage
|
||||||
|
|
||||||
|
src = r"C:\Users\28579\Downloads\resources\resources\netherstar.png"
|
||||||
|
dst = r"D:\Project\Starlight_Lancher\apps\app-frontend\public\models\netherstar.png"
|
||||||
|
|
||||||
|
im = Image.open(src).convert("RGBA")
|
||||||
|
arr = np.array(im, dtype=np.float32)
|
||||||
|
r, g, b, a = arr[..., 0], arr[..., 1], arr[..., 2], arr[..., 3]
|
||||||
|
|
||||||
|
# 1) 完全不透明像素的位置
|
||||||
|
opaque = a >= 254
|
||||||
|
# 2) 对每个通道,用最近的不透明像素值填充(距离变换的索引)
|
||||||
|
# 用一个临时:在 opaque 区域保留原 RGB,其余待填
|
||||||
|
h, w = a.shape
|
||||||
|
# 用 scipy 的 distance_transform_edt 拿到"最近的 opaque 像素索引"
|
||||||
|
indices = ndimage.distance_transform_edt(~opaque, return_distances=False, return_indices=True)
|
||||||
|
nearest_rgb = arr[indices[0], indices[1], :3]
|
||||||
|
|
||||||
|
# 3) 半透明区域(a < 254 且 a > 0)的 RGB 换成最近不透明像素颜色
|
||||||
|
semi = (a > 0) & (a < 254)
|
||||||
|
out = arr.copy()
|
||||||
|
out[semi, 0:3] = nearest_rgb[semi]
|
||||||
|
|
||||||
|
# 4) 可选:收紧极小 alpha(<8 直接透明),减少杂边
|
||||||
|
out[a < 8, 3] = 0
|
||||||
|
|
||||||
|
out_img = Image.fromarray(np.clip(out, 0, 255).astype(np.uint8), "RGBA")
|
||||||
|
out_img.save(dst)
|
||||||
|
print("done ->", dst)
|
||||||
|
print("尺寸:", out_img.size)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
p = r"D:\Project\Starlight_Lancher\apps\app-frontend\src\App.vue"
|
||||||
|
t = open(p, encoding="utf-8").read()
|
||||||
|
|
||||||
|
# 1) 加 import(放在 App.vue 的 import 区,紧跟其他 @ 包;找一个锚点)
|
||||||
|
anchor = "import { listen } from '@tauri-apps/api/event'"
|
||||||
|
assert anchor in t, "找不到 anchor import"
|
||||||
|
t = t.replace(anchor, "import { hideAllPoppers } from 'floating-vue'\n" + anchor, 1)
|
||||||
|
|
||||||
|
# 2) router.afterEach 开头加 hideAllPoppers()
|
||||||
|
old = "router.afterEach((to, from, failure) => {\n\tif (!failure) void invoke('lightweight_mode_set_route', { route: to.fullPath })"
|
||||||
|
new = "router.afterEach((to, from, failure) => {\n\thideAllPoppers()\n\tif (!failure) void invoke('lightweight_mode_set_route', { route: to.fullPath })"
|
||||||
|
assert old in t, "找不到 router.afterEach 开头"
|
||||||
|
t = t.replace(old, new, 1)
|
||||||
|
|
||||||
|
open(p, "w", encoding="utf-8", newline="\n").write(t)
|
||||||
|
print("done")
|
||||||
|
print(" 含 hideAllPoppers import:", "import { hideAllPoppers } from 'floating-vue'" in t)
|
||||||
|
print(" 调用次数:", t.count("hideAllPoppers()"))
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
from PIL import Image
|
||||||
|
im = Image.open(r"C:\Users\28579\Downloads\resources\resources\netherstar.png").convert("RGBA")
|
||||||
|
w, h = im.size
|
||||||
|
# 找 alpha 从 0 到 255 的过渡带,看这些像素的 RGB
|
||||||
|
print("=== 半透明白色像素采样 ===")
|
||||||
|
cnt = 0
|
||||||
|
for y in range(0, h, 5):
|
||||||
|
for x in range(0, w, 5):
|
||||||
|
r, g, b, a = im.getpixel((x, y))
|
||||||
|
if 0 < a < 255 and r > 240 and g > 240 and b > 240:
|
||||||
|
print(f"({x},{y}) RGBA=({r},{g},{b},{a})")
|
||||||
|
cnt += 1
|
||||||
|
if cnt >= 10:
|
||||||
|
break
|
||||||
|
if cnt >= 10:
|
||||||
|
break
|
||||||
|
# 对比:全不透明像素的 RGB
|
||||||
|
print("\n=== 不透明像素采样 ===")
|
||||||
|
cnt = 0
|
||||||
|
for y in range(0, h, 5):
|
||||||
|
for x in range(0, w, 5):
|
||||||
|
r, g, b, a = im.getpixel((x, y))
|
||||||
|
if a == 255:
|
||||||
|
print(f"({x},{y}) RGBA=({r},{g},{b},{a})")
|
||||||
|
cnt += 1
|
||||||
|
if cnt >= 5:
|
||||||
|
break
|
||||||
|
if cnt >= 5:
|
||||||
|
break
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import glob, os, re
|
||||||
|
cands = glob.glob(r"D:\Project\Starlight_Lancher\node_modules\.pnpm\floating-vue@*\node_modules\floating-vue\dist\*")
|
||||||
|
cands = [c for c in cands if os.path.isfile(c)]
|
||||||
|
print("dist 文件:", [os.path.basename(c) for c in cands])
|
||||||
|
for c in cands:
|
||||||
|
t = open(c, encoding="utf-8", errors="replace").read()
|
||||||
|
# 找 export 里跟 tooltip 相关的
|
||||||
|
names = set(re.findall(r"export\s*\{([^}]*)\}", t))
|
||||||
|
hits = [n.strip() for grp in names for n in grp.split(",") if "ooltip" in n or "ispose" in n]
|
||||||
|
if hits:
|
||||||
|
print(f"\n{os.path.basename(c)} 导出含 tooltip/dispose:", hits[:20])
|
||||||
|
# 找 hide/dispose 函数名
|
||||||
|
for kw in ("hideAllTooltips", "disposeAllTooltips", "hideAllPoppers", "disposeTooltips", "hideTooltips"):
|
||||||
|
if kw in t:
|
||||||
|
print(f" [{os.path.basename(c)}] 含 {kw}")
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
p = r"D:\Project\Starlight_Lancher\packages\app-lib\src\state\minecraft_auth\yggdrasil.rs"
|
||||||
|
t = open(p, encoding="utf-8").read()
|
||||||
|
|
||||||
|
# 定位:把 201-265 的整段逻辑替换成"遍历所有 profiles"
|
||||||
|
old = """ if let Some(profile) = response.selected_profile {
|
||||||
|
let credentials = create_credentials(
|
||||||
|
profile,
|
||||||
|
response.access_token,
|
||||||
|
response.client_token,
|
||||||
|
metadata,
|
||||||
|
login,
|
||||||
|
);
|
||||||
|
credentials.upsert(exec).await?;
|
||||||
|
return Ok(YggdrasilLoginResult::Complete { credentials });
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.available_profiles.is_empty() {
|
||||||
|
return Err(ErrorKind::OtherError(
|
||||||
|
"The Yggdrasil account does not have a Minecraft profile"
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.as_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.available_profiles.len() == 1 {
|
||||||
|
let profile = response.available_profiles[0].clone();
|
||||||
|
let refreshed = refresh_selected_profile(
|
||||||
|
&metadata.api_root,
|
||||||
|
&response.access_token,
|
||||||
|
&response.client_token,
|
||||||
|
&profile,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let selected_profile = refreshed.selected_profile.ok_or_else(|| {
|
||||||
|
ErrorKind::OtherError(
|
||||||
|
"The Yggdrasil service did not select the requested profile"
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.as_error()
|
||||||
|
})?;
|
||||||
|
let credentials = create_credentials(
|
||||||
|
selected_profile,
|
||||||
|
refreshed.access_token,
|
||||||
|
refreshed.client_token,
|
||||||
|
metadata,
|
||||||
|
login,
|
||||||
|
);
|
||||||
|
credentials.upsert(exec).await?;
|
||||||
|
return Ok(YggdrasilLoginResult::Complete { credentials });
|
||||||
|
}
|
||||||
|
|
||||||
|
let flow_id = Uuid::new_v4();
|
||||||
|
let profiles = response.available_profiles.clone();
|
||||||
|
let mut pending = PENDING_LOGINS.lock().await;
|
||||||
|
pending.retain(|_, login| login.created.elapsed() < PENDING_LOGIN_LIFETIME);
|
||||||
|
pending.insert(
|
||||||
|
flow_id,
|
||||||
|
PendingYggdrasilLogin {
|
||||||
|
created: Instant::now(),
|
||||||
|
api_root: metadata.api_root,
|
||||||
|
server_name: metadata.server_name,
|
||||||
|
login: login.to_string(),
|
||||||
|
access_token: response.access_token,
|
||||||
|
client_token: response.client_token,
|
||||||
|
profiles: response.available_profiles,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(YggdrasilLoginResult::SelectProfile { flow_id, profiles })"""
|
||||||
|
|
||||||
|
new = """ // 收集本次登录可用的所有角色;优先使用 availableProfiles,回退到 selectedProfile。
|
||||||
|
let mut profiles: Vec<YggdrasilProfile> = response.available_profiles.clone();
|
||||||
|
if profiles.is_empty() {
|
||||||
|
if let Some(selected) = response.selected_profile.clone() {
|
||||||
|
profiles.push(selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if profiles.is_empty() {
|
||||||
|
return Err(ErrorKind::OtherError(
|
||||||
|
"The Yggdrasil account does not have a Minecraft profile"
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.as_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 逐个角色 refresh + 保存,实现「一次登录自动加入所有角色」。
|
||||||
|
// Yggdrasil 同一时刻只激活一个角色,因此需要串行 refresh,并各自保存当次 token。
|
||||||
|
let mut access_token = response.access_token.clone();
|
||||||
|
let mut client_token = response.client_token.clone();
|
||||||
|
let mut last_credentials: Option<Credentials> = None;
|
||||||
|
|
||||||
|
for (index, profile) in profiles.iter().enumerate() {
|
||||||
|
let refreshed = refresh_selected_profile(
|
||||||
|
&metadata.api_root,
|
||||||
|
&access_token,
|
||||||
|
&client_token,
|
||||||
|
profile,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let selected_profile = refreshed.selected_profile.ok_or_else(|| {
|
||||||
|
ErrorKind::OtherError(
|
||||||
|
"The Yggdrasil service did not select the requested profile"
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.as_error()
|
||||||
|
})?;
|
||||||
|
access_token = refreshed.access_token.clone();
|
||||||
|
client_token = refreshed.client_token.clone();
|
||||||
|
|
||||||
|
let mut credentials = create_credentials(
|
||||||
|
selected_profile,
|
||||||
|
refreshed.access_token,
|
||||||
|
refreshed.client_token,
|
||||||
|
YggdrasilMetadata {
|
||||||
|
api_root: metadata.api_root.clone(),
|
||||||
|
server_name: metadata.server_name.clone(),
|
||||||
|
raw: String::new(),
|
||||||
|
},
|
||||||
|
login,
|
||||||
|
);
|
||||||
|
// 仅让最后一个(通常是默认/首个以外的角色)保持 active,避免多个 active。
|
||||||
|
credentials.active = index == profiles.len() - 1;
|
||||||
|
credentials.upsert(exec).await?;
|
||||||
|
last_credentials = Some(credentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(credentials) = last_credentials {
|
||||||
|
return Ok(YggdrasilLoginResult::Complete { credentials });
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(ErrorKind::OtherError(
|
||||||
|
"The Yggdrasil account does not have a Minecraft profile"
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.as_error())"""
|
||||||
|
|
||||||
|
assert old in t, "找不到要替换的登录逻辑块"
|
||||||
|
t = t.replace(old, new, 1)
|
||||||
|
|
||||||
|
open(p, "w", encoding="utf-8", newline="\n").write(t)
|
||||||
|
print("done")
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Starlight Launcher 1.0.0-beta1
|
||||||
|
|
||||||
|
发布日期 / Published: 2026-09-13
|
||||||
|
|
||||||
|
## 中文
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
|
||||||
|
- 侧边栏新增「斯达莱特」入口,内嵌 StarLight 皮肤站。
|
||||||
|
- 主页侧边栏新增启动进度面板,显示当前启动阶段与进度。
|
||||||
|
- 新增彩蛋小游戏(下界之星合成),可通过暗号 starlight、Konami 秘技或长按关于页开发者名字触发。
|
||||||
|
|
||||||
|
### 变更
|
||||||
|
|
||||||
|
- 启动器已从 Axolotl Launcher 更名为 Starlight Launcher,并更换了全新图标与关于页。
|
||||||
|
- 版权与 "Powered by" 署名现在正确标注上游项目 Axolotl Launcher。
|
||||||
|
- 关于页的开发组现在显示 Ax_Tps 与 Disy920。
|
||||||
|
|
||||||
|
### 移除
|
||||||
|
|
||||||
|
- 移除了「固定的服务器」中写死的 Starlight 官方服务器,现在只显示你自己收藏的服务器。
|
||||||
|
- 关于页中游动的美西螈动画已替换为静止的下界之星。
|
||||||
|
|
||||||
|
### Bug 修复
|
||||||
|
|
||||||
|
- 启用最大化选项后,Minecraft 现在会以最大化窗口启动。
|
||||||
|
|
||||||
|
## English
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- A "Starlight" entry was added to the sidebar, which embeds the StarLight skin site.
|
||||||
|
- A launch progress panel was added to the home sidebar, showing the current launch stage and progress.
|
||||||
|
- An easter egg mini-game (star merge) was added, reachable via the secret code "starlight", the Konami code, or a long press on a developer name on the about page.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The launcher has been rebranded from Axolotl Launcher to Starlight Launcher, with a new icon and a refreshed about page.
|
||||||
|
- The "Powered by" and copyright notices now correctly credit Axolotl Launcher as the upstream project.
|
||||||
|
- The developer list on the about page now shows Ax_Tps and Disy920.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- The hard-coded Starlight official server was removed from "Pinned servers"; only servers you favorite yourself are shown now.
|
||||||
|
- The swimming axolotl animation on the about page was replaced with a static nether star.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Minecraft now launches in a maximized window when the maximize option is enabled.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from PIL import Image
|
||||||
|
im = Image.open(r"C:\Users\28579\Downloads\resources\resources\netherstar.png").convert("RGBA")
|
||||||
|
w, h = im.size
|
||||||
|
print("尺寸:", (w, h))
|
||||||
|
# 采样边缘一圈(alpha 低但 RGB 接近白的像素 = "脏"边缘)
|
||||||
|
dirty = 0
|
||||||
|
total_edge = 0
|
||||||
|
for y in range(0, h, 3):
|
||||||
|
for x in range(0, w, 3):
|
||||||
|
r, g, b, a = im.getpixel((x, y))
|
||||||
|
if 0 < a < 255:
|
||||||
|
total_edge += 1
|
||||||
|
# alpha 半透明,但 RGB 很亮(接近白)—— 这就是"没剔干净"的迹象
|
||||||
|
if r > 240 and g > 240 and b > 240:
|
||||||
|
dirty += 1
|
||||||
|
print("半透明像素总数:", total_edge)
|
||||||
|
print("半透明中 RGB 接近白的(脏边缘):", dirty)
|
||||||
|
if total_edge:
|
||||||
|
print("脏边比例: {:.1f}%".format(dirty / total_edge * 100))
|
||||||
Reference in new issue
Block a user