Compare commits

...
Author SHA1 Message Date
AxTps 2efaa4fb35 chore: 发布 1.0.1-beta1 公告
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
Sync LobeHub models / sync (push) Has been cancelled
Starlight release / Release preflight (push) Has been cancelled
Prepare pnpm cache / prepare (push) Has been cancelled
Starlight release / Build (Windows x64) (push) Has been cancelled
Starlight release / Build (Linux arm64) (push) Has been cancelled
Starlight release / Build (Linux x64) (push) Has been cancelled
Starlight release / Build (macOS universal) (push) Has been cancelled
Starlight release / Verify and publish (push) Has been cancelled
- catalog.ts 增量新增 1.0.1-beta1 公告(首次启动欢迎弹窗);
- 网站 catalog.json 重新生成。
2026-09-27 08:39:49 +08:00
AxTps 2d52d16933 feat: 添加了首次启动器时弹出的公告
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
2026-09-27 08:24:13 +08:00
Axolotl Release Bot bbb9e5c054 chore(website): update download metadata to v1.0.0 [skip ci] 2026-09-26 07:26:43 +00:00
AxTps 15c375bca5 chore: 发布 1.0.0 正式版公告
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
Starlight release / Release preflight (push) Has been cancelled
Prepare pnpm cache / prepare (push) Has been cancelled
Starlight release / Build (Windows x64) (push) Has been cancelled
Starlight release / Build (Linux arm64) (push) Has been cancelled
Starlight release / Build (Linux x64) (push) Has been cancelled
Starlight release / Build (macOS universal) (push) Has been cancelled
Starlight release / Verify and publish (push) Has been cancelled
- catalog.ts 增量新增 1.0.0 公告(启用内置自动更新、修复启动崩溃);
- 网站 catalog.json 重新生成。
2026-09-26 14:25:28 +08:00
Disy d2a319a4d5 fix: 启用启动器更新并修复初始化竞态
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
2026-09-26 13:06:51 +08:00
Disy 2359b80250 fix: 修复自选目录实例的模组同步路径
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
Sync LobeHub models / sync (push) Has been cancelled
2026-09-26 11:00:01 +08:00
AxTps c273734aec fix: 临时回退内置自动更新以修复启动崩溃
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
Starlight release / Release preflight (push) Has been cancelled
Prepare pnpm cache / prepare (push) Has been cancelled
Starlight release / Build (Windows x64) (push) Has been cancelled
Starlight release / Build (Linux arm64) (push) Has been cancelled
Starlight release / Build (Linux x64) (push) Has been cancelled
Starlight release / Build (macOS universal) (push) Has been cancelled
Starlight release / Verify and publish (push) Has been cancelled
1.0.0-beta14 启用内置更新后,Windows 上一打开即崩溃(c0000409 栈
溢出)。暂时将 are_updates_enabled 改回 false,待定位并修复崩溃后
再重新启用。

- main.rs: are_updates_enabled 恢复返回 false;
- catalog.ts: 增量新增 1.0.0-beta15 公告;
- 网站 catalog.json 重新生成。
2026-09-26 09:56:52 +08:00
AxTps f89cbff37e docs: README 链接改指向 GitHub
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
将仓库、Releases、Stars 及最新版下载等链接从自建 git.starlight.cool
改为 GitHub (github.com/ApTx2012/Starlight_Lancher)。
2026-09-26 08:19:01 +08:00
AxTps f92aee9b4c feat: 启用内置自动更新
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
Starlight release / Release preflight (push) Has been cancelled
Prepare pnpm cache / prepare (push) Has been cancelled
Starlight release / Build (Windows x64) (push) Has been cancelled
Starlight release / Build (Linux arm64) (push) Has been cancelled
Starlight release / Build (Linux x64) (push) Has been cancelled
Starlight release / Build (macOS universal) (push) Has been cancelled
Starlight release / Verify and publish (push) Has been cancelled
- are_updates_enabled 由硬编码 false 改为 true,正式启用内置更新器
  (更新端点与签名公钥由 tauri-release.conf.json 配置)。
- catalog.ts 新增 1.0.0-beta14 公告(增量追加,保留历史版本)。
- 网站 catalog.json 由 catalog.ts 重新生成。
2026-09-26 07:52:06 +08:00
AxTps 2192a19d50 chore: 移除 1.0.0-beta1 之前的版本公告
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
清理上游遗留的旧版本公告(1.9.6-beta.3 ~ 1.4.0),只保留本项目
自己的版本记录(1.0.0-beta1 及之后的 1.0.0-beta13)。

- catalog.ts 删除 49 条旧公告;
- 网站 catalog.json 由 create-website-release-catalog.mjs 从
  catalog.ts 重新生成,现仅含 1.0.0-beta13 与 1.0.0-beta1。
2026-09-26 07:35:49 +08:00
51 changed files with 730 additions and 6294 deletions

No files matched your search

+6 -6
View File
@@ -5,13 +5,13 @@
<p><strong>次世代 Minecraft 桌面客户端,全能、美观、全平台覆盖。</strong></p> <p><strong>次世代 Minecraft 桌面客户端,全能、美观、全平台覆盖。</strong></p>
<p> <p>
<a href="https://git.starlight.cool/AxTps/Starlight_Lancher/actions"> <a href="https://github.com/ApTx2012/Starlight_Lancher/actions">
<img src="https://img.shields.io/badge/Repo-git.starlight.cool-blue?style=for-the-badge&logo=git" alt="Repository" /> <img src="https://img.shields.io/badge/Repo-GitHub-black?style=for-the-badge&logo=github" alt="Repository" />
</a> </a>
<a href="https://git.starlight.cool/AxTps/Starlight_Lancher/releases"> <a href="https://github.com/ApTx2012/Starlight_Lancher/releases">
<img src="https://img.shields.io/badge/Releases-Download-green?style=for-the-badge&logo=github" alt="Releases" /> <img src="https://img.shields.io/badge/Releases-Download-green?style=for-the-badge&logo=github" alt="Releases" />
</a> </a>
<a href="https://git.starlight.cool/AxTps/Starlight_Lancher/stargazers"> <a href="https://github.com/ApTx2012/Starlight_Lancher/stargazers">
<img src="https://img.shields.io/badge/Stars-★-ffb800?style=for-the-badge&logo=github" alt="Stars" /> <img src="https://img.shields.io/badge/Stars-★-ffb800?style=for-the-badge&logo=github" alt="Stars" />
</a> </a>
<a href="COPYING.md"> <a href="COPYING.md">
@@ -21,7 +21,7 @@
<p> <p>
<a href="https://skin.starlight.cool/">官方网站</a> | <a href="https://skin.starlight.cool/">官方网站</a> |
<a href="https://git.starlight.cool/AxTps/Starlight_Lancher/releases/latest">下载最新版</a> | <a href="https://github.com/ApTx2012/Starlight_Lancher/releases/latest">下载最新版</a> |
<a href="CONTRIBUTING.md">参与贡献</a> <a href="CONTRIBUTING.md">参与贡献</a>
| <a href="CODE_OF_CONDUCT.md">行为准则</a> | <a href="CODE_OF_CONDUCT.md">行为准则</a>
</p> </p>
@@ -57,7 +57,7 @@ _(注:本项目是调用 Modrinth 公开 API 的独立客户端,与 Rinth, I
## 下载与安装 ## 下载与安装
请前往 [Releases](https://git.starlight.cool/AxTps/Starlight_Lancher/releases/latest) 下载适合你操作系统的最新安装包。 请前往 [Releases](https://github.com/ApTx2012/Starlight_Lancher/releases/latest) 下载适合你操作系统的最新安装包。
已安装的用户每次均可通过内置的 Tauri 签名校验机制,自动在后台完成更新,无需手动下载安装更新。 已安装的用户每次均可通过内置的 Tauri 签名校验机制,自动在后台完成更新,无需手动下载安装更新。
| 系统平台 | 推荐下载文件 | | 系统平台 | 推荐下载文件 |
+35 -9
View File
@@ -63,9 +63,11 @@ import { openUrl } from '@tauri-apps/plugin-opener'
import { type as getOsType } from '@tauri-apps/plugin-os' import { type as getOsType } from '@tauri-apps/plugin-os'
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state' import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
import { hideAllPoppers } from 'floating-vue' import { hideAllPoppers } from 'floating-vue'
import { computed, nextTick, onMounted, onUnmounted, provide, ref, watch } from 'vue' import { computed, onMounted, onUnmounted, provide, ref, watch } from 'vue'
import { type RouteLocationNormalizedLoaded, RouterView, useRoute, useRouter } from 'vue-router' import { type RouteLocationNormalizedLoaded, RouterView, useRoute, useRouter } from 'vue-router'
import InstancePlayerModal from '@/components/instance/InstancePlayerModal.vue'
import TaggedModDownloadsModal from '@/components/instance/TaggedModDownloadsModal.vue'
import InstanceExportModal from '@/components/lab/recipe-generator/InstanceExportModal.vue' import InstanceExportModal from '@/components/lab/recipe-generator/InstanceExportModal.vue'
import AccountsCard from '@/components/ui/AccountsCard.vue' import AccountsCard from '@/components/ui/AccountsCard.vue'
import AppActionBar from '@/components/ui/AppActionBar.vue' import AppActionBar from '@/components/ui/AppActionBar.vue'
@@ -79,7 +81,6 @@ import MinecraftAuthErrorModal from '@/components/ui/minecraft-auth-error-modal/
import MinecraftCrashModal from '@/components/ui/MinecraftCrashModal.vue' import MinecraftCrashModal from '@/components/ui/MinecraftCrashModal.vue'
import AuthGrantFlowWaitModal from '@/components/ui/modal/AuthGrantFlowWaitModal.vue' import AuthGrantFlowWaitModal from '@/components/ui/modal/AuthGrantFlowWaitModal.vue'
import CurseForgeManualDownloadsModal from '@/components/ui/modal/CurseForgeManualDownloadsModal.vue' import CurseForgeManualDownloadsModal from '@/components/ui/modal/CurseForgeManualDownloadsModal.vue'
import TaggedModDownloadsModal from '@/components/instance/TaggedModDownloadsModal.vue'
import InstallToPlayModal from '@/components/ui/modal/InstallToPlayModal.vue' import InstallToPlayModal from '@/components/ui/modal/InstallToPlayModal.vue'
import InstanceIconPickerModal from '@/components/ui/modal/InstanceIconPickerModal.vue' import InstanceIconPickerModal from '@/components/ui/modal/InstanceIconPickerModal.vue'
import JavaDownloadConfirmationModal from '@/components/ui/modal/JavaDownloadConfirmationModal.vue' import JavaDownloadConfirmationModal from '@/components/ui/modal/JavaDownloadConfirmationModal.vue'
@@ -90,9 +91,9 @@ import NavButton from '@/components/ui/NavButton.vue'
import NavRail from '@/components/ui/NavRail.vue' import NavRail from '@/components/ui/NavRail.vue'
import OnboardingOverlay from '@/components/ui/onboarding/OnboardingOverlay.vue' import OnboardingOverlay from '@/components/ui/onboarding/OnboardingOverlay.vue'
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue' import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
import SkinSiteSessionFrame from '@/components/ui/SkinSiteSessionFrame.vue' import SkinSiteSessionFrame from '@/components/ui/SkinSiteSessionFrame.vue'
import InstancePlayerModal from '@/components/instance/InstancePlayerModal.vue' import StartupAnnouncementModal from '@/components/ui/StartupAnnouncementModal.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
import WindowControls from '@/components/ui/WindowControls.vue' import WindowControls from '@/components/ui/WindowControls.vue'
import { useCheckDisableMouseover } from '@/composables/macCssFix.js' import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
import { useDropImport } from '@/composables/useDropImport' import { useDropImport } from '@/composables/useDropImport'
@@ -138,7 +139,6 @@ import {
isDev, isDev,
isElevated, isElevated,
isNetworkMetered, isNetworkMetered,
restartApp,
setRestartAfterPendingUpdate, setRestartAfterPendingUpdate,
} from '@/helpers/utils.js' } from '@/helpers/utils.js'
import { start_join_server, start_join_singleplayer_world } from '@/helpers/worlds.ts' import { start_join_server, start_join_singleplayer_world } from '@/helpers/worlds.ts'
@@ -490,9 +490,16 @@ onMounted(async () => {
document.querySelector('body').addEventListener('auxclick', handleAuxClick) document.querySelector('body').addEventListener('auxclick', handleAuxClick)
window.addEventListener(DIRECT_LINKS_SYNCED_EVENT, handleDirectLinkSyncReport) window.addEventListener(DIRECT_LINKS_SYNCED_EVENT, handleDirectLinkSyncReport)
try {
await stateInitialization
} catch {
return
}
checkUpdates() checkUpdates()
void warnIfRunningElevated() void warnIfRunningElevated()
startDirectLinkSync() startDirectLinkSync()
maybeShowStartupAnnouncement()
}) })
let directLinkSync: (() => Promise<void>) | undefined let directLinkSync: (() => Promise<void>) | undefined
@@ -1533,6 +1540,19 @@ const updateToPlayModal = ref()
const modrinthLoginFlowWaitModal = ref() const modrinthLoginFlowWaitModal = ref()
const startupAnnouncementModal = ref<InstanceType<typeof StartupAnnouncementModal> | null>(null)
const STARTUP_ANNOUNCEMENT_KEY = 'starlight-startup-announcement-seen-v1'
function maybeShowStartupAnnouncement() {
try {
if (localStorage.getItem(STARTUP_ANNOUNCEMENT_KEY)) return
localStorage.setItem(STARTUP_ANNOUNCEMENT_KEY, '1')
} catch (error) {
console.warn('Failed to read/write startup announcement flag', error)
}
startupAnnouncementModal.value?.show()
}
// ── Drop import system ────────────────────────────────────────────────── // ── Drop import system ──────────────────────────────────────────────────
const dropImport = useDropImport({ const dropImport = useDropImport({
notificationManager, notificationManager,
@@ -1701,6 +1721,7 @@ onMounted(() => {
setServerUpdateToPlayModal(updateToPlayModal.value) setServerUpdateToPlayModal(updateToPlayModal.value)
void (async () => { void (async () => {
try { try {
await stateInitialization
const ready = await invoke<{ const ready = await invoke<{
pending_crashes: { instance_id: string; uuid: string }[] pending_crashes: { instance_id: string; uuid: string }[]
pending_commands: Parameters<typeof handleCommand>[0][] pending_commands: Parameters<typeof handleCommand>[0][]
@@ -2161,6 +2182,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
<SplashScreen v-if="!stateFailed" ref="splashScreen" data-tauri-drag-region /> <SplashScreen v-if="!stateFailed" ref="splashScreen" data-tauri-drag-region />
<SkinSiteSessionFrame v-if="stateInitialized" /> <SkinSiteSessionFrame v-if="stateInitialized" />
<InstancePlayerModal v-if="stateInitialized" /> <InstancePlayerModal v-if="stateInitialized" />
<StartupAnnouncementModal ref="startupAnnouncementModal" />
<div id="teleports"></div> <div id="teleports"></div>
<div <div
v-if="stateInitialized && themeStore.customBackgroundPath && !themeStore.transparentBackground" v-if="stateInitialized && themeStore.customBackgroundPath && !themeStore.transparentBackground"
@@ -2423,9 +2445,13 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
{{ formatMessage(messages.authUnreachableBody) }} {{ formatMessage(messages.authUnreachableBody) }}
</Admonition> </Admonition>
<div class="page-transition-grid grid min-h-full"> <div class="page-transition-grid grid min-h-full">
<RouterView v-slot="{ Component, route }"> <RouterView v-slot="{ Component, route: currentRoute }">
<Transition name="page-slide" :css="themeStore.getFeatureFlag('page_transitions')"> <Transition name="page-slide" :css="themeStore.getFeatureFlag('page_transitions')">
<div v-if="Component" :key="getPageTransitionKey(route)" class="page-transition-layer"> <div
v-if="Component"
:key="getPageTransitionKey(currentRoute)"
class="page-transition-layer"
>
<Suspense @pending="onSuspensePending" @resolve="onSuspenseResolve"> <Suspense @pending="onSuspensePending" @resolve="onSuspenseResolve">
<component :is="Component"></component> <component :is="Component"></component>
</Suspense> </Suspense>
@@ -2489,7 +2515,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
:on-error-action="exportNotificationErrorLogs" :on-error-action="exportNotificationErrorLogs"
:error-action-label="formatMessage(messages.exportErrorLogs)" :error-action-label="formatMessage(messages.exportErrorLogs)"
/> />
<MinecraftCrashModal ref="minecraftCrashModal" @error="handleError" /> <MinecraftCrashModal v-if="stateInitialized" ref="minecraftCrashModal" @error="handleError" />
<JavaDownloadConfirmationModal ref="javaDownloadConfirmationModal" /> <JavaDownloadConfirmationModal ref="javaDownloadConfirmationModal" />
<NewModal <NewModal
ref="closeChoiceModal" ref="closeChoiceModal"
@@ -2577,7 +2603,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
@install="handleContentInstallModpackInstall" @install="handleContentInstallModpackInstall"
@cancel="handleContentInstallModpackInstallCancel" @cancel="handleContentInstallModpackInstallCancel"
/> />
<TaggedModDownloadsModal /> <TaggedModDownloadsModal v-if="stateInitialized" />
<CurseForgeManualDownloadsModal <CurseForgeManualDownloadsModal
ref="contentInstallCurseForgeManualDownloadsModal" ref="contentInstallCurseForgeManualDownloadsModal"
@view-instance="handleContentInstallModpackDuplicateGoToInstance" @view-instance="handleContentInstallModpackDuplicateGoToInstance"
File diff suppressed because it is too large. Load diff
@@ -28,8 +28,8 @@ import { useNetworkStatus } from '@/composables/useNetworkStatus'
import { process_listener } from '@/helpers/events' import { process_listener } from '@/helpers/events'
import { install_existing_instance, install_pack_to_existing_instance } from '@/helpers/install' import { install_existing_instance, install_pack_to_existing_instance } from '@/helpers/install'
import { kill, run } from '@/helpers/instance' import { kill, run } from '@/helpers/instance'
import { get_by_instance_id } from '@/helpers/process'
import { isInstanceLaunching } from '@/helpers/instance-launch-state' import { isInstanceLaunching } from '@/helpers/instance-launch-state'
import { get_by_instance_id } from '@/helpers/process'
import type { GameInstance } from '@/helpers/types' import type { GameInstance } from '@/helpers/types'
import { handleSevereError } from '@/store/error' import { handleSevereError } from '@/store/error'
@@ -36,6 +36,7 @@ const props = withDefaults(
limit?: HomeRecentLimit limit?: HomeRecentLimit
}>(), }>(),
{ {
dashboardSize: null,
limit: HOME_RECENT_DEFAULT_LIMIT, limit: HOME_RECENT_DEFAULT_LIMIT,
}, },
) )
@@ -146,11 +147,17 @@ async function stopInstance(instance: GameInstance) {
</script> </script>
<template> <template>
<section class="home-recent-worlds flex min-w-0 min-h-0 h-full flex-col gap-3" :data-size="dashboardSize"> <section
class="home-recent-worlds flex min-w-0 min-h-0 h-full flex-col gap-3"
:data-size="dashboardSize"
>
<div class="home-widget-heading flex min-w-0 h-8 flex-none items-center gap-2"> <div class="home-widget-heading flex min-w-0 h-8 flex-none items-center gap-2">
<h2>{{ formatMessage(messages.recentTitle) }}</h2> <h2>{{ formatMessage(messages.recentTitle) }}</h2>
</div> </div>
<div v-if="recentItems.length > 0" class="home-recent-list flex min-w-0 min-h-0 flex-1 flex-col gap-1 overflow-x-hidden overflow-y-auto pr-1"> <div
v-if="recentItems.length > 0"
class="home-recent-list flex min-w-0 min-h-0 flex-1 flex-col gap-1 overflow-x-hidden overflow-y-auto pr-1"
>
<template <template
v-for="item in recentItems" v-for="item in recentItems"
:key="item.type === 'world' ? worldKey(item.world) : `${item.instance.id}:instance`" :key="item.type === 'world' ? worldKey(item.world) : `${item.instance.id}:instance`"
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ProgressBar, defineMessages, useVIntl } from '@modrinth/ui' import { defineMessages, ProgressBar, useVIntl } from '@modrinth/ui'
import { computed } from 'vue' import { computed } from 'vue'
import { injectDownloadManager } from '@/providers/download-manager' import { injectDownloadManager } from '@/providers/download-manager'
const props = defineProps<{ instanceId?: string; active?: boolean }>() const props = defineProps<{ instanceId?: string; active?: boolean }>()
@@ -14,13 +14,14 @@ import {
import { users } from '@/helpers/auth' import { users } from '@/helpers/auth'
import { getInstanceMode } from '@/helpers/hosted-packs' import { getInstanceMode } from '@/helpers/hosted-packs'
import { import {
type InstancePlayer,
type PlayerChoice,
PlayerSelectionNavigatedAwayError, PlayerSelectionNavigatedAwayError,
registerInstancePlayerPicker, registerInstancePlayerPicker,
saveInstancePlayer, saveInstancePlayer,
waitForSkinSiteSession, waitForSkinSiteSession,
type InstancePlayer,
type PlayerChoice,
} from '@/helpers/instance-player' } from '@/helpers/instance-player'
import { playerMessages as messages } from './instance-player-messages' import { playerMessages as messages } from './instance-player-messages'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
@@ -1,12 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { ButtonStyled, useVIntl } from '@modrinth/ui' import { ButtonStyled, useVIntl } from '@modrinth/ui'
import { onUnmounted, ref, watch } from 'vue' import { onUnmounted, ref, watch } from 'vue'
import { import {
chooseInstancePlayer, chooseInstancePlayer,
getInstancePlayer, getInstancePlayer,
onInstancePlayerChanged,
type InstancePlayer, type InstancePlayer,
onInstancePlayerChanged,
} from '@/helpers/instance-player' } from '@/helpers/instance-player'
import { playerMessages as messages } from './instance-player-messages' import { playerMessages as messages } from './instance-player-messages'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
@@ -1,10 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { NewModal, ProgressBar, ButtonStyled, defineMessages, useVIntl } from '@modrinth/ui' import { ButtonStyled, defineMessages, NewModal, ProgressBar, useVIntl } from '@modrinth/ui'
import { onMounted, onUnmounted, nextTick, ref, shallowRef } from 'vue' import { nextTick, onMounted, onUnmounted, ref, shallowRef } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { loading_listener } from '@/helpers/events' import { loading_listener } from '@/helpers/events'
import { progress_bars_list } from '@/helpers/state'
import { onHostedPackAttemptStarted } from '@/helpers/hosted-packs' import { onHostedPackAttemptStarted } from '@/helpers/hosted-packs'
import { progress_bars_list } from '@/helpers/state'
import { import {
createTaggedModProgress, createTaggedModProgress,
type TaggedProgressEvent, type TaggedProgressEvent,
@@ -12,10 +12,14 @@
> >
{{ formatMessage(messages.score, { score }) }} {{ formatMessage(messages.score, { score }) }}
</span> </span>
<span class="shrink-0 whitespace-nowrap rounded-full bg-surface-3 px-2 py-0.5 tabular-nums text-secondary"> <span
class="shrink-0 whitespace-nowrap rounded-full bg-surface-3 px-2 py-0.5 tabular-nums text-secondary"
>
{{ formatMessage(messages.highestLevel, { level: highestLevel + 1 }) }} {{ formatMessage(messages.highestLevel, { level: highestLevel + 1 }) }}
</span> </span>
<span class="shrink-0 whitespace-nowrap rounded-full bg-surface-3 px-2 py-0.5 tabular-nums text-secondary"> <span
class="shrink-0 whitespace-nowrap rounded-full bg-surface-3 px-2 py-0.5 tabular-nums text-secondary"
>
{{ formatMessage(messages.best, { score: bestScore }) }} {{ formatMessage(messages.best, { score: bestScore }) }}
</span> </span>
<NewButton type="base" size="sm" class="shrink-0" @click="settleGame"> <NewButton type="base" size="sm" class="shrink-0" @click="settleGame">
@@ -80,7 +84,10 @@
</div> </div>
</div> </div>
</Transition> </Transition>
<div v-if="gameOver && !endedManually" class="tide-drain pointer-events-none absolute inset-0 z-20" /> <div
v-if="gameOver && !endedManually"
class="tide-drain pointer-events-none absolute inset-0 z-20"
/>
<Transition name="panel"> <Transition name="panel">
<div <div
v-if="gameOver" v-if="gameOver"
@@ -115,16 +122,26 @@
</div> </div>
<div class="grid w-full min-w-0 grid-cols-3 gap-1.5"> <div class="grid w-full min-w-0 grid-cols-3 gap-1.5">
<div class="flex min-w-0 flex-col items-center rounded-lg bg-surface-3 px-1 py-1"> <div class="flex min-w-0 flex-col items-center rounded-lg bg-surface-3 px-1 py-1">
<span class="text-[9px] text-secondary">{{ formatMessage(messages.levelLabel) }}</span> <span class="text-[9px] text-secondary">{{
<span class="text-sm font-bold tabular-nums text-contrast">{{ highestLevel + 1 }}</span> formatMessage(messages.levelLabel)
}}</span>
<span class="text-sm font-bold tabular-nums text-contrast">{{
highestLevel + 1
}}</span>
</div> </div>
<div class="flex min-w-0 flex-col items-center rounded-lg bg-surface-3 px-1 py-1"> <div class="flex min-w-0 flex-col items-center rounded-lg bg-surface-3 px-1 py-1">
<span class="text-[9px] text-secondary">{{ formatMessage(messages.bestLabel) }}</span> <span class="text-[9px] text-secondary">{{
formatMessage(messages.bestLabel)
}}</span>
<span class="text-sm font-bold tabular-nums text-contrast">{{ bestScore }}</span> <span class="text-sm font-bold tabular-nums text-contrast">{{ bestScore }}</span>
</div> </div>
<div class="flex min-w-0 flex-col items-center rounded-lg bg-surface-3 px-1 py-1"> <div class="flex min-w-0 flex-col items-center rounded-lg bg-surface-3 px-1 py-1">
<span class="text-[9px] text-secondary">{{ formatMessage(messages.overtimesLabel) }}</span> <span class="text-[9px] text-secondary">{{
<span class="text-sm font-bold tabular-nums text-contrast">{{ overtimeCount }}</span> formatMessage(messages.overtimesLabel)
}}</span>
<span class="text-sm font-bold tabular-nums text-contrast">{{
overtimeCount
}}</span>
</div> </div>
</div> </div>
<div class="flex flex-wrap justify-center gap-2"> <div class="flex flex-wrap justify-center gap-2">
@@ -150,8 +167,8 @@ import blueStar from '@/assets/stars/blue.png'
import greenStar from '@/assets/stars/green.png' import greenStar from '@/assets/stars/green.png'
import pinkStar from '@/assets/stars/pink.png' import pinkStar from '@/assets/stars/pink.png'
import redStar from '@/assets/stars/red.png' import redStar from '@/assets/stars/red.png'
import yellowStar from '@/assets/stars/yellow.png'
import superStar from '@/assets/stars/super.png' import superStar from '@/assets/stars/super.png'
import yellowStar from '@/assets/stars/yellow.png'
interface Piece { interface Piece {
id: number id: number
@@ -396,7 +413,14 @@ function mergePieces(a: Piece, b: Piece) {
score.value += points[level] score.value += points[level]
highestLevel.value = Math.max(highestLevel.value, level) highestLevel.value = Math.max(highestLevel.value, level)
const isRainbow = level === colors.length - 1 const isRainbow = level === colors.length - 1
spawnBurst(x, y, isRainbow ? rainbowColors : [colors[level]], 10 + level * 3, 130 + level * 20, 3.4) spawnBurst(
x,
y,
isRainbow ? rainbowColors : [colors[level]],
10 + level * 3,
130 + level * 20,
3.4,
)
spawnShockwave(x, y, 46 + level * 26, 'rgba(255, 255, 255, 0.9)', Math.min(3, 1.4 + level * 0.3)) spawnShockwave(x, y, 46 + level * 26, 'rgba(255, 255, 255, 0.9)', Math.min(3, 1.4 + level * 0.3))
shakePower = Math.max(shakePower, Math.min(2 + level * 1.2, 7)) shakePower = Math.max(shakePower, Math.min(2 + level * 1.2, 7))
spawnFloater(x, y, `+${points[level]}`, 15 + level * 3, colors[level]) spawnFloater(x, y, `+${points[level]}`, 15 + level * 3, colors[level])
@@ -423,7 +447,14 @@ function easeOutBack(t: number) {
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2) return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2)
} }
function spawnBurst(x: number, y: number, palette: string[], count: number, speed: number, size: number) { function spawnBurst(
x: number,
y: number,
palette: string[],
count: number,
speed: number,
size: number,
) {
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
if (particles.length >= 260) return if (particles.length >= 260) return
const angle = Math.random() * Math.PI * 2 const angle = Math.random() * Math.PI * 2
@@ -508,7 +539,8 @@ function update(delta: number) {
if (overtimeStartAt >= 0) { if (overtimeStartAt >= 0) {
const overtimeElapsed = elapsed - overtimeStartAt const overtimeElapsed = elapsed - overtimeStartAt
if (overtimeElapsed < 0.8) { if (overtimeElapsed < 0.8) {
surfaceRatio = overtimeFromRatio + (0.05 - overtimeFromRatio) * easeInOutCubic(overtimeElapsed / 0.8) surfaceRatio =
overtimeFromRatio + (0.05 - overtimeFromRatio) * easeInOutCubic(overtimeElapsed / 0.8)
} else { } else {
overtimeStartAt = -1 overtimeStartAt = -1
} }
@@ -670,17 +702,7 @@ function prepareBallSprite(src: string, level: number) {
sprite.height = Math.max(1, Math.round(bounds.h * scale)) sprite.height = Math.max(1, Math.round(bounds.h * scale))
const ctx = sprite.getContext('2d') const ctx = sprite.getContext('2d')
if (!ctx) return if (!ctx) return
ctx.drawImage( ctx.drawImage(image, bounds.x, bounds.y, bounds.w, bounds.h, 0, 0, sprite.width, sprite.height)
image,
bounds.x,
bounds.y,
bounds.w,
bounds.h,
0,
0,
sprite.width,
sprite.height,
)
ballSprites[level] = sprite ballSprites[level] = sprite
} }
image.src = src image.src = src
@@ -692,9 +714,7 @@ function drawMergePiece(piece: Piece) {
const spawnProgress = Math.min(1, (elapsed - piece.spawnAt) / 0.22) const spawnProgress = Math.min(1, (elapsed - piece.spawnAt) / 0.22)
const spawnScale = spawnProgress >= 1 ? 1 : easeOutBack(spawnProgress) const spawnScale = spawnProgress >= 1 ? 1 : easeOutBack(spawnProgress)
const radius = const radius =
piece.radius * piece.radius * spawnScale * (isRainbow ? 1 + 0.05 * Math.sin(elapsed * 3.2 + piece.id) : 1)
spawnScale *
(isRainbow ? 1 + 0.05 * Math.sin(elapsed * 3.2 + piece.id) : 1)
context.save() context.save()
context.translate(piece.x, piece.y) context.translate(piece.x, piece.y)
const hue = (elapsed * 55) % 360 const hue = (elapsed * 55) % 360
@@ -792,9 +812,7 @@ function draw() {
context.beginPath() context.beginPath()
for (let x = 0; x <= width; x += 8) { for (let x = 0; x <= width; x += 8) {
const waveY = const waveY =
surfaceY + surfaceY + Math.sin(x * 0.045 + elapsed * 1.8) * 2.5 + Math.sin(x * 0.013 + elapsed * 0.9) * 2
Math.sin(x * 0.045 + elapsed * 1.8) * 2.5 +
Math.sin(x * 0.013 + elapsed * 0.9) * 2
if (x === 0) context.moveTo(x, waveY) if (x === 0) context.moveTo(x, waveY)
else context.lineTo(x, waveY) else context.lineTo(x, waveY)
} }
@@ -807,7 +825,12 @@ function draw() {
const progress = Math.min(1, (elapsed - waveStart) / 0.8) const progress = Math.min(1, (elapsed - waveStart) / 0.8)
const waveY = height * (1 - progress) const waveY = height * (1 - progress)
const bandHeight = Math.max(22, height * 0.1) const bandHeight = Math.max(22, height * 0.1)
const gradient = context.createLinearGradient(0, waveY - bandHeight, 0, waveY + bandHeight * 0.5) const gradient = context.createLinearGradient(
0,
waveY - bandHeight,
0,
waveY + bandHeight * 0.5,
)
gradient.addColorStop(0, 'rgba(255, 255, 255, 0)') gradient.addColorStop(0, 'rgba(255, 255, 255, 0)')
gradient.addColorStop(0.55, 'rgba(96, 178, 255, 0.5)') gradient.addColorStop(0.55, 'rgba(96, 178, 255, 0.5)')
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)') gradient.addColorStop(1, 'rgba(255, 255, 255, 0)')
@@ -11,8 +11,8 @@ import {
StyledInput, StyledInput,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import { save } from '@tauri-apps/plugin-dialog'
import { join } from '@tauri-apps/api/path' import { join } from '@tauri-apps/api/path'
import { save } from '@tauri-apps/plugin-dialog'
import { readDir, stat } from '@tauri-apps/plugin-fs' import { readDir, stat } from '@tauri-apps/plugin-fs'
import { ref } from 'vue' import { ref } from 'vue'
@@ -20,8 +20,8 @@ import { process_listener } from '@/helpers/events'
import { install_existing_instance, install_pack_to_existing_instance } from '@/helpers/install' import { install_existing_instance, install_pack_to_existing_instance } from '@/helpers/install'
import { kill, run } from '@/helpers/instance' import { kill, run } from '@/helpers/instance'
import { getDisplayInstanceIcon } from '@/helpers/instance-icons' import { getDisplayInstanceIcon } from '@/helpers/instance-icons'
import { get_by_instance_id } from '@/helpers/process'
import { isInstanceLaunching } from '@/helpers/instance-launch-state' import { isInstanceLaunching } from '@/helpers/instance-launch-state'
import { get_by_instance_id } from '@/helpers/process'
import { showInstanceInFolder } from '@/helpers/utils.js' import { showInstanceInFolder } from '@/helpers/utils.js'
import { handleSevereError } from '@/store/error.js' import { handleSevereError } from '@/store/error.js'
@@ -98,7 +98,7 @@ const checkProcess = async () => {
if (Array.isArray(runningProcesses)) internalPlaying.value = runningProcesses.length > 0 if (Array.isArray(runningProcesses)) internalPlaying.value = runningProcesses.length > 0
} }
const play = async (e, context) => { const play = async (e) => {
e?.stopPropagation() e?.stopPropagation()
if (loading.value) return if (loading.value) return
await run(props.instance.id).catch(async (err) => { await run(props.instance.id).catch(async (err) => {
@@ -111,7 +111,7 @@ const play = async (e, context) => {
await checkProcess() await checkProcess()
} }
const stop = async (e, context) => { const stop = async (e) => {
e?.stopPropagation() e?.stopPropagation()
await kill(props.instance.id).catch(handleError) await kill(props.instance.id).catch(handleError)
await checkProcess() await checkProcess()
@@ -205,7 +205,7 @@ onUnmounted(() => unlisten())
: formatMessage(isPlaying ? messages.launchAnother : commonMessages.playButton) : formatMessage(isPlaying ? messages.launchAnother : commonMessages.playButton)
" "
:disabled="offline && !installed" :disabled="offline && !installed"
@click="(e) => play(e, 'InstanceCard')" @click="play"
@mousehover="checkProcess" @mousehover="checkProcess"
> >
<!-- Translate for optical centering --> <!-- Translate for optical centering -->
@@ -295,7 +295,7 @@ onUnmounted(() => unlisten())
'scale-75 opacity-0 transition-all group-hover:scale-100 group-hover:opacity-100': 'scale-75 opacity-0 transition-all group-hover:scale-100 group-hover:opacity-100':
!disabled, !disabled,
}" }"
@click="(e) => play(e, 'InstanceCard')" @click="play"
@mousehover="checkProcess" @mousehover="checkProcess"
> >
<PlayIcon class="translate-x-[1px]" /> <PlayIcon class="translate-x-[1px]" />
@@ -522,7 +522,7 @@ async function copyShareUrl(): Promise<void> {
} }
} }
function _openAIAnalysis(): void { function openAIAnalysis(): void {
if (!lastAnalysis?.combined_log) { if (!lastAnalysis?.combined_log) {
notifyNoLogContent() notifyNoLogContent()
return return
@@ -603,7 +603,7 @@ onUnmounted(() => {
unlistenProcess?.() unlistenProcess?.()
}) })
defineExpose({ handleLaunchError, handleWarning, isLaunchFailure, showPreview, openAIAnalysis: _openAIAnalysis }) defineExpose({ handleLaunchError, handleWarning, isLaunchFailure, showPreview, openAIAnalysis })
</script> </script>
<template> <template>
@@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue' import { onMounted, onUnmounted, ref, watch } from 'vue'
import { clearHostedSession } from '@/helpers/hosted-packs'
import { import {
receiveSkinSiteMessage, receiveSkinSiteMessage,
@@ -10,6 +9,7 @@ import {
skinSiteStatus, skinSiteStatus,
skinSiteUser, skinSiteUser,
} from '@/composables/skin-site-session' } from '@/composables/skin-site-session'
import { clearHostedSession } from '@/helpers/hosted-packs'
const frame = ref<HTMLIFrameElement>() const frame = ref<HTMLIFrameElement>()
let lastMessage = 0 let lastMessage = 0
@@ -0,0 +1,132 @@
<script setup lang="ts">
import { ExternalIcon, UsersIcon, XIcon } from '@modrinth/assets'
import { Avatar, ButtonStyled, NewModal } from '@modrinth/ui'
import { openUrl } from '@tauri-apps/plugin-opener'
import { ref } from 'vue'
import { teamMembers } from '@/data/about'
const modal = ref<InstanceType<typeof NewModal>>()
const SERVER_IPS: { label: string; host: string }[] = [
{ label: '直连 IPv4', host: 'slv4.starlight.cool' },
{ label: '直连 IPv6', host: 'slv6.starlight.cool' },
{ label: '北方代理 IPv4', host: 'north.starlight.cool' },
{ label: '南方代理 IPv4', host: 'south.starlight.cool' },
{ label: '美国代理 IPv4', host: 'usa.starlight.cool' },
]
const RULES_URL = 'https://kdocs.cn/l/cmT6lVdryHJu'
const QQ_GROUP = '535392227'
function show() {
modal.value?.show()
}
function hide() {
modal.value?.hide()
}
function openLink(url: string) {
void openUrl(url)
}
</script>
<template>
<NewModal ref="modal" header="欢迎使用 Starlight Launcher" width="640px" max-width="640px">
<div class="flex min-w-0 flex-col gap-5">
<!-- 1. 开发组 -->
<section class="flex flex-col gap-2">
<h3 class="m-0 flex items-center gap-2 text-sm font-semibold text-contrast">
<UsersIcon class="size-4 text-secondary" />
开发团队
</h3>
<p class="m-0 text-sm text-secondary">
本启动器由
<span class="font-semibold text-contrast">Ax_Tps</span>
和
<span class="font-semibold text-contrast">Disy920</span>
基于 Axolotl 开发。
</p>
<div class="grid gap-3 sm:grid-cols-2">
<a
v-for="member in teamMembers"
:key="member.name"
:href="member.url ?? undefined"
target="_blank"
rel="noopener noreferrer"
class="flex min-w-0 items-center gap-3 rounded-xl bg-surface-4 p-3 transition-colors hover:bg-surface-5"
@click.prevent="member.url && openLink(member.url)"
>
<Avatar :src="member.avatarUrl" :alt="member.name" size="2.5rem" circle no-shadow />
<span class="min-w-0 flex-1 truncate font-semibold text-contrast">{{ member.name }}</span>
<ExternalIcon v-if="member.url" class="size-4 shrink-0 text-secondary" />
</a>
</div>
</section>
<!-- 2. SLS 专属 -->
<section class="flex flex-col gap-2">
<h3 class="m-0 text-sm font-semibold text-contrast">Starlight 专用启动器</h3>
<p class="m-0 text-sm text-secondary">
该启动器为 Starlight 专用。如果您还未加入 StarLight,请前往我们的 QQ 审核群
<button
type="button"
class="cursor-pointer border-0 bg-transparent p-0 font-semibold text-brand underline"
@click="openLink(`https://qm.qq.com/q/${QQ_GROUP}`)"
>
{{ QQ_GROUP }}
</button>
。
</p>
</section>
<!-- 3. 服规更新 -->
<section class="flex flex-col gap-2">
<h3 class="m-0 text-sm font-semibold text-contrast">StarLight 总则章程更新</h3>
<p class="m-0 text-sm text-secondary">
StarLight 总则章程(即“服规”)更新至第 15 版。该版服规进一步完善与严格了聚落建立、改组、招新与运营的全链条要求,请大家注意阅读并遵守!本版服规自
2026 年 9 月 27 日 0 点起开始试行。
</p>
<button
type="button"
class="flex w-fit cursor-pointer items-center gap-1 border-0 bg-transparent p-0 text-sm font-semibold text-brand underline"
@click="openLink(RULES_URL)"
>
查看服规链接
<ExternalIcon class="size-3.5" />
</button>
</section>
<!-- 4. 服务器连接 -->
<section class="flex flex-col gap-2">
<h3 class="m-0 text-sm font-semibold text-contrast">服务器连接</h3>
<p class="m-0 text-sm text-secondary">服务器目前下列几项 IP 可用:</p>
<ul class="m-0 flex flex-col gap-1 pl-5 text-sm text-secondary">
<li v-for="ip in SERVER_IPS" :key="ip.host">
<span class="text-contrast">{{ ip.label }}</span>
:<code class="text-brand">{{ ip.host }}</code>
</li>
</ul>
<p class="m-0 text-sm text-secondary">
请注意,我们优先推荐您尝试直连 IP(包括 v4 和 v6,哪个可用用哪个)。若没有出现高延迟或延迟波动严重的问题,尽量避免使用代理
IP(代理 IP 是流量计费的,使用会给服务器带来更大的经济压力)。
</p>
<p class="m-0 text-sm text-secondary">
若必须使用代理 IP,按以往经验,河北(含)以北的我国省份,在选用代理 IP 时建议优先选用北方代理,否则请使用南方代理。
</p>
</section>
</div>
<template #actions>
<div class="flex justify-end">
<ButtonStyled color="brand">
<button @click="hide">
<XIcon />
我知道了
</button>
</ButtonStyled>
</div>
</template>
</NewModal>
</template>
@@ -67,6 +67,7 @@ withDefaults(
}>(), }>(),
{ {
count: 1, count: 1,
symlinkTarget: null,
}, },
) )
@@ -127,7 +127,7 @@ interface IconBackground {
style: Record<string, string> style: Record<string, string>
} }
function background( function makeBackground(
id: string, id: string,
name: MessageDescriptor, name: MessageDescriptor,
top: string, top: string,
@@ -171,16 +171,16 @@ const backgrounds = [
backgroundSize: '16px 16px', backgroundSize: '16px 16px',
}, },
} satisfies IconBackground, } satisfies IconBackground,
background('grass', backgroundNames.grass, '#7fc95b', '#2f7d32'), makeBackground('grass', backgroundNames.grass, '#7fc95b', '#2f7d32'),
background('ocean', backgroundNames.ocean, '#55b7ff', '#3157c8'), makeBackground('ocean', backgroundNames.ocean, '#55b7ff', '#3157c8'),
background('amethyst', backgroundNames.amethyst, '#c084fc', '#6d28d9'), makeBackground('amethyst', backgroundNames.amethyst, '#c084fc', '#6d28d9'),
background('sunset', backgroundNames.sunset, '#ffba52', '#e94b64'), makeBackground('sunset', backgroundNames.sunset, '#ffba52', '#e94b64'),
background('cherry', backgroundNames.cherry, '#ff9ec4', '#b93670'), makeBackground('cherry', backgroundNames.cherry, '#ff9ec4', '#b93670'),
background('nether', backgroundNames.nether, '#ef5a46', '#6d1717'), makeBackground('nether', backgroundNames.nether, '#ef5a46', '#6d1717'),
background('slime', backgroundNames.slime, '#b6ee55', '#44972f'), makeBackground('slime', backgroundNames.slime, '#b6ee55', '#44972f'),
background('deep-dark', backgroundNames.deepDark, '#245369', '#0c1f2b'), makeBackground('deep-dark', backgroundNames.deepDark, '#245369', '#0c1f2b'),
background('stone', backgroundNames.stone, '#aeb5bd', '#525b66'), makeBackground('stone', backgroundNames.stone, '#aeb5bd', '#525b66'),
background('midnight', backgroundNames.midnight, '#4d5f8f', '#171b2d'), makeBackground('midnight', backgroundNames.midnight, '#4d5f8f', '#171b2d'),
] ]
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
@@ -12,13 +12,13 @@ import {
import { getVersion } from '@tauri-apps/api/app' import { getVersion } from '@tauri-apps/api/app'
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/core'
import { fetch as tauriFetch } from '@tauri-apps/plugin-http' import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
import { inject, nextTick, ref, watch } from 'vue' import { nextTick, ref, watch } from 'vue'
import UpdateAnnouncementHistory from '@/components/ui/announcement/UpdateAnnouncementHistory.vue' import UpdateAnnouncementHistory from '@/components/ui/announcement/UpdateAnnouncementHistory.vue'
import { import {
betaDatabaseExists, betaDatabaseExists,
copyReleaseDatabaseToBeta,
copyDatabaseBetweenChannels, copyDatabaseBetweenChannels,
copyReleaseDatabaseToBeta,
getCurrentAppDatabasePath, getCurrentAppDatabasePath,
getUpdateChannel, getUpdateChannel,
getUpdatePreferences, getUpdatePreferences,
@@ -446,11 +446,14 @@ async function confirmDatabaseOperation() {
}), }),
}) })
} catch (error) { } catch (error) {
const detail = error instanceof Error ? error.message : String(error)
handleError( handleError(
new Error( new Error(
`${
targetChannel === activeChannel targetChannel === activeChannel
? formatMessage(messages.databaseOperationActiveTarget) ? formatMessage(messages.databaseOperationActiveTarget)
: formatMessage(messages.databaseOperationFailed), : formatMessage(messages.databaseOperationFailed)
}: ${detail}`,
), ),
) )
} finally { } finally {
@@ -468,7 +471,7 @@ function cancelDatabaseOperation() {
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<SettingsSection :title="formatMessage(messages.title)"> <SettingsSection :title="formatMessage(messages.title)">
<div class="update-channel-panel"> <div class="update-channel-panel">
<p id="settings-target-updates-channel" class="m-0 text-sm leading-[1.45] text-secondary"> <p id="settings-target-updates-source" class="m-0 text-sm leading-[1.45] text-secondary">
{{ formatMessage(messages.description) }} {{ formatMessage(messages.description) }}
</p> </p>
<div <div
@@ -1,4 +1,4 @@
import { defineMessage, type MessageDescriptor } from '@modrinth/ui' import type { MessageDescriptor } from '@modrinth/ui'
export type SettingsCategoryId = export type SettingsCategoryId =
| 'interface' | 'interface'
@@ -28,100 +28,100 @@ export interface SettingsCategoryDefinition {
export const settingsCategoryDefinitions: SettingsCategoryDefinition[] = [ export const settingsCategoryDefinitions: SettingsCategoryDefinition[] = [
{ {
id: 'interface', id: 'interface',
name: defineMessage({ name: {
id: 'app.settings.tabs.interface', id: 'app.settings.tabs.interface',
defaultMessage: 'Interface & appearance', defaultMessage: 'Interface & appearance',
}), },
group: 'launcher', group: 'launcher',
onboardingId: 'settings-tab-interface', onboardingId: 'settings-tab-interface',
}, },
{ {
id: 'home-navigation', id: 'home-navigation',
name: defineMessage({ name: {
id: 'app.settings.tabs.home-navigation', id: 'app.settings.tabs.home-navigation',
defaultMessage: 'Home & navigation', defaultMessage: 'Home & navigation',
}), },
group: 'launcher', group: 'launcher',
onboardingId: 'settings-tab-home-navigation', onboardingId: 'settings-tab-home-navigation',
}, },
{ {
id: 'language-translation', id: 'language-translation',
name: defineMessage({ name: {
id: 'app.settings.tabs.language-translation', id: 'app.settings.tabs.language-translation',
defaultMessage: 'Language & translation', defaultMessage: 'Language & translation',
}), },
group: 'launcher', group: 'launcher',
onboardingId: 'settings-tab-language-translation', onboardingId: 'settings-tab-language-translation',
}, },
{ {
id: 'ai', id: 'ai',
name: defineMessage({ id: 'app.settings.tabs.ai', defaultMessage: 'AI' }), name: { id: 'app.settings.tabs.ai', defaultMessage: 'AI' },
group: 'launcher', group: 'launcher',
flushContent: true, flushContent: true,
onboardingId: 'settings-tab-ai', onboardingId: 'settings-tab-ai',
}, },
{ {
id: 'java-performance', id: 'java-performance',
name: defineMessage({ name: {
id: 'app.settings.tabs.java-performance', id: 'app.settings.tabs.java-performance',
defaultMessage: 'Java & performance', defaultMessage: 'Java & performance',
}), },
group: 'game', group: 'game',
onboardingId: 'settings-tab-java-performance', onboardingId: 'settings-tab-java-performance',
}, },
{ {
id: 'launch-defaults', id: 'launch-defaults',
name: defineMessage({ name: {
id: 'app.settings.tabs.launch-defaults', id: 'app.settings.tabs.launch-defaults',
defaultMessage: 'Launch & instance defaults', defaultMessage: 'Launch & instance defaults',
}), },
group: 'game', group: 'game',
onboardingId: 'settings-tab-launch-defaults', onboardingId: 'settings-tab-launch-defaults',
}, },
{ {
id: 'content-downloads', id: 'content-downloads',
name: defineMessage({ name: {
id: 'app.settings.tabs.content-downloads', id: 'app.settings.tabs.content-downloads',
defaultMessage: 'Content & downloads', defaultMessage: 'Content & downloads',
}), },
group: 'game', group: 'game',
onboardingId: 'settings-tab-content-downloads', onboardingId: 'settings-tab-content-downloads',
}, },
{ {
id: 'network-multiplayer', id: 'network-multiplayer',
name: defineMessage({ name: {
id: 'app.settings.tabs.network-multiplayer', id: 'app.settings.tabs.network-multiplayer',
defaultMessage: 'Network & multiplayer', defaultMessage: 'Network & multiplayer',
}), },
group: 'game', group: 'game',
onboardingId: 'settings-tab-network-multiplayer', onboardingId: 'settings-tab-network-multiplayer',
}, },
{ {
id: 'storage-backups', id: 'storage-backups',
name: defineMessage({ name: {
id: 'app.settings.tabs.storage-backups', id: 'app.settings.tabs.storage-backups',
defaultMessage: 'Storage & backups', defaultMessage: 'Storage & backups',
}), },
group: 'data-privacy', group: 'data-privacy',
onboardingId: 'settings-tab-storage-backups', onboardingId: 'settings-tab-storage-backups',
}, },
{ {
id: 'updates', id: 'updates',
name: defineMessage({ id: 'app.settings.tabs.updates', defaultMessage: 'Updates' }), name: { id: 'app.settings.tabs.updates', defaultMessage: 'Updates' },
group: 'support', group: 'support',
onboardingId: 'settings-tab-updates', onboardingId: 'settings-tab-updates',
}, },
{ {
id: 'about', id: 'about',
name: defineMessage({ id: 'app.settings.tabs.about', defaultMessage: 'About' }), name: { id: 'app.settings.tabs.about', defaultMessage: 'About' },
group: 'support', group: 'support',
}, },
{ {
id: 'feature-flags', id: 'feature-flags',
name: defineMessage({ name: {
id: 'settings.feature-flags.title', id: 'settings.feature-flags.title',
defaultMessage: 'Feature flags', defaultMessage: 'Feature flags',
}), },
group: 'developer', group: 'developer',
developerOnly: true, developerOnly: true,
}, },
@@ -15,7 +15,8 @@ import {
UsersIcon, UsersIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { commonMessages, defineMessages, type MessageDescriptor } from '@modrinth/ui' import { commonMessages, defineMessages, type MessageDescriptor } from '@modrinth/ui'
import { defineAsyncComponent, type Component } from 'vue' import { type Component, defineAsyncComponent } from 'vue'
import { import {
getVisibleSettingsCategoryDefinitions, getVisibleSettingsCategoryDefinitions,
type SettingsCategoryDefinition, type SettingsCategoryDefinition,
@@ -29,8 +29,8 @@ import { useMinecraftLaunchError } from '@/composables/useMinecraftLaunchError'
import { get_project } from '@/helpers/cache' import { get_project } from '@/helpers/cache'
import { process_listener } from '@/helpers/events' import { process_listener } from '@/helpers/events'
import { kill, run } from '@/helpers/instance' import { kill, run } from '@/helpers/instance'
import { get_by_instance_id } from '@/helpers/process'
import { isInstanceLaunching } from '@/helpers/instance-launch-state' import { isInstanceLaunching } from '@/helpers/instance-launch-state'
import { get_by_instance_id } from '@/helpers/process'
import type { GameInstance } from '@/helpers/types' import type { GameInstance } from '@/helpers/types'
import { showInstanceInFolder } from '@/helpers/utils' import { showInstanceInFolder } from '@/helpers/utils'
import { handleSevereError } from '@/store/error' import { handleSevereError } from '@/store/error'
@@ -1,7 +1,6 @@
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
import test from 'node:test' import test from 'node:test'
import { useHostedSync } from './useHostedSync.ts'
import { useHostedCreation, forgetHostedCreation } from './useHostedCreation.ts'
import { import {
clearHostedSession, clearHostedSession,
hostedCreate, hostedCreate,
@@ -10,12 +9,11 @@ import {
onHostedPackAttemptStarted, onHostedPackAttemptStarted,
setInstanceMode, setInstanceMode,
} from '../helpers/hosted-packs.ts' } from '../helpers/hosted-packs.ts'
import { import {
openSkinSiteLogin, openSkinSiteLogin,
receiveSkinSiteMessage, receiveSkinSiteMessage,
requestSkinSiteLuck,
requestSkinSiteDownloadToken, requestSkinSiteDownloadToken,
requestSkinSiteLuck,
requestSkinSitePlayers, requestSkinSitePlayers,
requestSkinSiteSkinUpdate, requestSkinSiteSkinUpdate,
resetSkinSiteSession, resetSkinSiteSession,
@@ -27,6 +25,8 @@ import {
skinSiteStatus, skinSiteStatus,
skinSiteUser, skinSiteUser,
} from './skin-site-session.ts' } from './skin-site-session.ts'
import { forgetHostedCreation, useHostedCreation } from './useHostedCreation.ts'
import { useHostedSync } from './useHostedSync.ts'
test('reconnecting the same skin site frame waits for verification instead of reporting signed out', () => { test('reconnecting the same skin site frame waits for verification instead of reporting signed out', () => {
const frame = { postMessage() {} } as unknown as Window const frame = { postMessage() {} } as unknown as Window
@@ -13,9 +13,9 @@ import type {
SymlinkMethodChoice, SymlinkMethodChoice,
} from '@modrinth/ui' } from '@modrinth/ui'
import { useDebugLogger, useGlobalDrop, useInstanceContext, useVIntl } from '@modrinth/ui' import { useDebugLogger, useGlobalDrop, useInstanceContext, useVIntl } from '@modrinth/ui'
import { join } from '@tauri-apps/api/path'
import { computed, type ComputedRef, nextTick, ref } from 'vue' import { computed, type ComputedRef, nextTick, ref } from 'vue'
import type { Router } from 'vue-router' import type { Router } from 'vue-router'
import { join } from '@tauri-apps/api/path'
import { import {
classifyDroppedItem, classifyDroppedItem,
@@ -1412,10 +1412,7 @@ export function useDropImport(options: DropImportOptions) {
cleanupLauncherZipTemp() cleanupLauncherZipTemp()
} }
function resolvedInstancePath( function resolvedInstancePath(inst: SelectedInstance): string | undefined {
inst: SelectedInstance,
ctx: ImportContext | null,
): string | undefined {
if (inst.compatibleMode) return inst.versionPath if (inst.compatibleMode) return inst.versionPath
return inst.path return inst.path
} }
@@ -1,5 +1,6 @@
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
import test from 'node:test' import test from 'node:test'
import { createHostedDownloadFailures } from './hosted-download-failures.ts' import { createHostedDownloadFailures } from './hosted-download-failures.ts'
test('retry clears only its own failure before progress arrives and ignores late old events', () => { test('retry clears only its own failure before progress arrives and ignores late old events', () => {
@@ -1,4 +1,5 @@
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/core'
import { import {
requestSkinSiteDownloadToken, requestSkinSiteDownloadToken,
skinSiteStatus, skinSiteStatus,
@@ -1,5 +1,6 @@
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
import { test } from 'node:test' import { test } from 'node:test'
import { isInstanceLaunching, launchOnce } from './instance-launch-state.ts' import { isInstanceLaunching, launchOnce } from './instance-launch-state.ts'
test('launches different instances concurrently and coalesces repeated pending clicks', async () => { test('launches different instances concurrently and coalesces repeated pending clicks', async () => {
@@ -1,18 +1,19 @@
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
import test from 'node:test' import test from 'node:test'
import { useNetworkStatus } from '../composables/useNetworkStatus.ts'
import { import {
setSkinSiteFrame,
resetSkinSiteSession,
receiveSkinSiteMessage, receiveSkinSiteMessage,
resetSkinSiteSession,
setSkinSiteFrame,
SKIN_SITE_ORIGIN, SKIN_SITE_ORIGIN,
} from '../composables/skin-site-session.ts' } from '../composables/skin-site-session.ts'
import { useNetworkStatus } from '../composables/useNetworkStatus.ts'
import { import {
type InstancePlayer,
onInstancePlayerChanged,
prepareInstancePlayer, prepareInstancePlayer,
registerInstancePlayerPicker, registerInstancePlayerPicker,
saveInstancePlayer, saveInstancePlayer,
onInstancePlayerChanged,
type InstancePlayer,
} from './instance-player.ts' } from './instance-player.ts'
test('instance player selection persists independently from the globally selected account', async () => { test('instance player selection persists independently from the globally selected account', async () => {
+1 -1
View File
@@ -18,8 +18,8 @@ import { detectGcStrategy, GC_STRATEGY_DEFINITIONS } from '@/helpers/gc/strategi
import type { GcContext, ResolvedGcStrategyId } from '@/helpers/gc/types' import type { GcContext, ResolvedGcStrategyId } from '@/helpers/gc/types'
import { setLastGcLaunchReport } from '@/helpers/gc-notice' import { setLastGcLaunchReport } from '@/helpers/gc-notice'
import { getInstanceMode, prepareHostedSession } from '@/helpers/hosted-packs' import { getInstanceMode, prepareHostedSession } from '@/helpers/hosted-packs'
import { prepareInstancePlayer } from '@/helpers/instance-player'
import { launchOnce } from '@/helpers/instance-launch-state' import { launchOnce } from '@/helpers/instance-launch-state'
import { prepareInstancePlayer } from '@/helpers/instance-player'
import { AUTO_GC_PRESET_ARG } from '@/helpers/java-arguments' import { AUTO_GC_PRESET_ARG } from '@/helpers/java-arguments'
import { get_jre, get_memory_status } from '@/helpers/jre.js' import { get_jre, get_memory_status } from '@/helpers/jre.js'
import { get as getSettings } from '@/helpers/settings' import { get as getSettings } from '@/helpers/settings'
@@ -82,20 +82,20 @@ test('notice query uses the target instance value and registered Tauri command',
assert.match(tauriSource, /tauri::generate_handler!\[[\s\S]*instance_get_post_upgrade_notice/) assert.match(tauriSource, /tauri::generate_handler!\[[\s\S]*instance_get_post_upgrade_notice/)
}) })
test('result uses Modrinth Card and Accordion for collapsed compatibility warnings', () => { test('result collections use Modrinth Card and Accordion for collapsed compatibility warnings', () => {
const source = readFileSync( const source = readFileSync(
new URL('../pages/instance/upgrade/UpgradeResultDetails.vue', import.meta.url), new URL('../pages/instance/upgrade/UpgradeResultCollections.vue', import.meta.url),
'utf8', 'utf8',
) )
assert.match(source, /<Card v-if="warningRows\.length"/) assert.match(source, /<Card v-if="warnings\.length"/)
assert.match(source, /<Accordion[\s\S]*:open-by-default="warningsExpandedByDefault"/) assert.match(source, /<Accordion[\s\S]*:open-by-default="warningsDefaultOpen"/)
assert.match(source, /<TriangleAlertIcon/) assert.match(source, /<TriangleAlertIcon/)
}) })
test('runtime icon references use names exported by the assets package', () => { test('runtime icon references use names exported by the assets package', () => {
const downloadsSource = readFileSync(new URL('../pages/Downloads.vue', import.meta.url), 'utf8') const downloadsSource = readFileSync(new URL('../pages/Downloads.vue', import.meta.url), 'utf8')
const resultSource = readFileSync( const resultSource = readFileSync(
new URL('../pages/instance/upgrade/UpgradeResultDetails.vue', import.meta.url), new URL('../pages/instance/upgrade/UpgradeResultCollections.vue', import.meta.url),
'utf8', 'utf8',
) )
const assetsSource = readFileSync( const assetsSource = readFileSync(
@@ -1,5 +1,6 @@
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
import test from 'node:test' import test from 'node:test'
import { createTaggedModProgress } from './tagged-mod-progress.ts' import { createTaggedModProgress } from './tagged-mod-progress.ts'
const parent = (id = 'parent', fraction: number | null = 0, error = '') => ({ const parent = (id = 'parent', fraction: number | null = 0, error = '') => ({
+3 -4
View File
@@ -4,9 +4,9 @@ import { invoke } from '@tauri-apps/api/core'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { isOfflineMode } from '@/composables/useNetworkStatus' import { isOfflineMode } from '@/composables/useNetworkStatus'
import { prepareInstancePlayer } from '@/helpers/instance-player'
import { getInstanceMode, prepareHostedSession } from '@/helpers/hosted-packs' import { getInstanceMode, prepareHostedSession } from '@/helpers/hosted-packs'
import { get_full_path, resolveGcLaunchIntent } from '@/helpers/instance' import { get_full_path, resolveGcLaunchIntent } from '@/helpers/instance'
import { prepareInstancePlayer } from '@/helpers/instance-player'
import { openPath } from '@/helpers/utils' import { openPath } from '@/helpers/utils'
type BaseWorld = { type BaseWorld = {
@@ -20,7 +20,6 @@ type BaseWorld = {
export type WorldType = 'singleplayer' | 'server' export type WorldType = 'singleplayer' | 'server'
export type DisplayStatus = 'normal' | 'hidden' | 'favorite' export type DisplayStatus = 'normal' | 'hidden' | 'favorite'
export type SingleplayerWorld = BaseWorld & { export type SingleplayerWorld = BaseWorld & {
type: 'singleplayer' type: 'singleplayer'
path: string path: string
@@ -251,7 +250,7 @@ export async function start_join_singleplayer_world(
world: string, world: string,
): Promise<unknown> { ): Promise<unknown> {
await prepareInstancePlayer(instanceId) await prepareInstancePlayer(instanceId)
if (await getInstanceMode(instanceId) === 'starlight') await prepareHostedSession() if ((await getInstanceMode(instanceId)) === 'starlight') await prepareHostedSession()
const { args, gcIntent } = await resolveGcLaunchIntent(instanceId) const { args, gcIntent } = await resolveGcLaunchIntent(instanceId)
return await invoke('plugin:worlds|start_join_singleplayer_world', { return await invoke('plugin:worlds|start_join_singleplayer_world', {
instanceId, instanceId,
@@ -264,7 +263,7 @@ export async function start_join_singleplayer_world(
export async function start_join_server(instanceId: string, address: string): Promise<unknown> { export async function start_join_server(instanceId: string, address: string): Promise<unknown> {
await prepareInstancePlayer(instanceId) await prepareInstancePlayer(instanceId)
if (await getInstanceMode(instanceId) === 'starlight') await prepareHostedSession() if ((await getInstanceMode(instanceId)) === 'starlight') await prepareHostedSession()
const { args, gcIntent } = await resolveGcLaunchIntent(instanceId) const { args, gcIntent } = await resolveGcLaunchIntent(instanceId)
return await invoke('plugin:worlds|start_join_server', { return await invoke('plugin:worlds|start_join_server', {
instanceId, instanceId,
+5 -7
View File
@@ -3,11 +3,12 @@ import { FolderOpenIcon, LeftArrowIcon, SparklesIcon } from '@modrinth/assets'
import { BigOptionButton, Button, defineMessages, useVIntl } from '@modrinth/ui' import { BigOptionButton, Button, defineMessages, useVIntl } from '@modrinth/ui'
import { inject, ref, watch } from 'vue' import { inject, ref, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import InstanceModeOptions from '@/components/instance/InstanceModeOptions.vue'
import HostedGameDirModal from '@/components/instance/HostedGameDirModal.vue' import HostedGameDirModal from '@/components/instance/HostedGameDirModal.vue'
import HostedPackProgress from '@/components/instance/HostedPackProgress.vue' import HostedPackProgress from '@/components/instance/HostedPackProgress.vue'
import type { InstanceMode } from '@/helpers/hosted-packs' import InstanceModeOptions from '@/components/instance/InstanceModeOptions.vue'
import { useHostedCreation } from '@/composables/useHostedCreation' import { useHostedCreation } from '@/composables/useHostedCreation'
import type { InstanceMode } from '@/helpers/hosted-packs'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const router = useRouter() const router = useRouter()
@@ -187,8 +188,8 @@ function handleImportExisting() {
/> />
<BigOptionButton <BigOptionButton
data-onboarding-id="creation-method-import"
v-if="instanceMode === 'local'" v-if="instanceMode === 'local'"
data-onboarding-id="creation-method-import"
:icon="FolderOpenIcon" :icon="FolderOpenIcon"
:title="formatMessage(messages.importTitle)" :title="formatMessage(messages.importTitle)"
:description="formatMessage(messages.importDescription)" :description="formatMessage(messages.importDescription)"
@@ -197,10 +198,7 @@ function handleImportExisting() {
/> />
</div> </div>
<HostedGameDirModal <HostedGameDirModal ref="hostedGameDirModal" @confirm="installHostedWithGameDir" />
ref="hostedGameDirModal"
@confirm="installHostedWithGameDir"
/>
<HostedPackProgress :instance-id="createdInstance" :active="installing" /> <HostedPackProgress :instance-id="createdInstance" :active="installing" />
<p v-if="installError" class="m-0 text-red" role="alert">{{ installError }}</p> <p v-if="installError" class="m-0 text-red" role="alert">{{ installError }}</p>
<p v-if="instanceMode === 'local'" class="m-0 text-sm text-secondary"> <p v-if="instanceMode === 'local'" class="m-0 text-sm text-secondary">
@@ -259,7 +259,7 @@ onMounted(() => {
/> />
<div class="flex flex-col gap-1.5"> <div class="flex flex-col gap-1.5">
<ButtonStyled color="brand"> <ButtonStyled color="brand">
<button class="start-button" @click="startTranslation" :disabled="!canStart"> <button class="start-button" :disabled="!canStart" @click="startTranslation">
<PlayIcon />{{ formatMessage(messages.start) }} <PlayIcon />{{ formatMessage(messages.start) }}
</button> </button>
</ButtonStyled> </ButtonStyled>
+1 -1
View File
@@ -33,11 +33,11 @@ import {
defineMessages, defineMessages,
DropdownSelect, DropdownSelect,
injectNotificationManager, injectNotificationManager,
type MessageDescriptor,
PopoutMenu, PopoutMenu,
Slider, Slider,
StyledInput, StyledInput,
Toggle, Toggle,
type MessageDescriptor,
useRelativeTime, useRelativeTime,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
+1 -3
View File
@@ -2,10 +2,9 @@
import { UsersIcon } from '@modrinth/assets' import { UsersIcon } from '@modrinth/assets'
import { defineMessages, NavTabs, useVIntl } from '@modrinth/ui' import { defineMessages, NavTabs, useVIntl } from '@modrinth/ui'
import { computed } from 'vue' import { computed } from 'vue'
import { RouterView, useRoute, useRouter } from 'vue-router' import { RouterView, useRouter } from 'vue-router'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const route = useRoute()
const router = useRouter() const router = useRouter()
const messages = defineMessages({ const messages = defineMessages({
@@ -13,7 +12,6 @@ const messages = defineMessages({
roomsTab: { id: 'app.multiplayer.tab.rooms', defaultMessage: 'Rooms' }, roomsTab: { id: 'app.multiplayer.tab.rooms', defaultMessage: 'Rooms' },
}) })
const activeTab = computed(() => (route.path.startsWith('/multiplayer/rooms') ? 'rooms' : 'rooms'))
const tabLinks = computed(() => [ const tabLinks = computed(() => [
{ label: formatMessage(messages.roomsTab), href: '/multiplayer/rooms', icon: UsersIcon }, { label: formatMessage(messages.roomsTab), href: '/multiplayer/rooms', icon: UsersIcon },
]) ])
+10 -10
View File
@@ -163,7 +163,7 @@
</button> </button>
</ButtonStyled> </ButtonStyled>
<ButtonStyled v-else-if="!isServerInstance" color="brand" size="large"> <ButtonStyled v-else-if="!isServerInstance" color="brand" size="large">
<button @click="startInstance('InstancePage')"> <button @click="startInstance">
<PlayIcon /> <PlayIcon />
{{ formatMessage(playing ? messages.launchAnother : commonMessages.playButton) }} {{ formatMessage(playing ? messages.launchAnother : commonMessages.playButton) }}
</button> </button>
@@ -184,7 +184,7 @@
}, },
{ {
id: 'launch_instance', id: 'launch_instance',
action: () => startInstance('InstancePage'), action: startInstance,
}, },
]" ]"
> >
@@ -204,7 +204,7 @@
</ButtonStyled> </ButtonStyled>
</div> </div>
<ButtonStyled v-if="playing" color="red" size="large"> <ButtonStyled v-if="playing" color="red" size="large">
<button :disabled="stopping || loading" @click="stopInstance('InstancePage')"> <button :disabled="stopping || loading" @click="stopInstance">
<StopCircleIcon /> <StopCircleIcon />
{{ formatMessage(stopping ? messages.stopping : messages.stopAll) }} {{ formatMessage(stopping ? messages.stopping : messages.stopAll) }}
</button> </button>
@@ -294,7 +294,7 @@
:is-server-instance="isServerInstance" :is-server-instance="isServerInstance"
:open-settings="() => settingsModal?.show(1)" :open-settings="() => settingsModal?.show(1)"
@play="updatePlayState" @play="updatePlayState"
@stop="() => stopInstance('InstanceSubpage')" @stop="stopInstance"
></component> ></component>
</Suspense> </Suspense>
</template> </template>
@@ -400,9 +400,9 @@ import {
getFreshCachedServerStatus, getFreshCachedServerStatus,
} from '@/composables/instances/use-server-status-query' } from '@/composables/instances/use-server-status-query'
import { useInstanceConsole } from '@/composables/useInstanceConsole' import { useInstanceConsole } from '@/composables/useInstanceConsole'
import { useInstanceMode } from '@/composables/useInstanceMode'
import { useMinecraftLaunchError } from '@/composables/useMinecraftLaunchError' import { useMinecraftLaunchError } from '@/composables/useMinecraftLaunchError'
import { useNetworkStatus } from '@/composables/useNetworkStatus' import { useNetworkStatus } from '@/composables/useNetworkStatus'
import { useInstanceMode } from '@/composables/useInstanceMode'
import { postUpgradeNoticeQueryKey, usePostUpgradeNotice } from '@/composables/usePostUpgradeNotice' import { postUpgradeNoticeQueryKey, usePostUpgradeNotice } from '@/composables/usePostUpgradeNotice'
import { useSymlinkWarningDismiss } from '@/composables/useSymlinkWarningDismiss' import { useSymlinkWarningDismiss } from '@/composables/useSymlinkWarningDismiss'
import { get_project_v3 } from '@/helpers/cache.js' import { get_project_v3 } from '@/helpers/cache.js'
@@ -421,8 +421,8 @@ import {
run, run,
} from '@/helpers/instance' } from '@/helpers/instance'
import { getDisplayInstanceIcon } from '@/helpers/instance-icons' import { getDisplayInstanceIcon } from '@/helpers/instance-icons'
import { get_by_instance_id } from '@/helpers/process'
import { isInstanceLaunching } from '@/helpers/instance-launch-state' import { isInstanceLaunching } from '@/helpers/instance-launch-state'
import { get_by_instance_id } from '@/helpers/process'
import type { GameInstance } from '@/helpers/types' import type { GameInstance } from '@/helpers/types'
import { createInstanceShortcut, showInstanceInFolder } from '@/helpers/utils.js' import { createInstanceShortcut, showInstanceInFolder } from '@/helpers/utils.js'
import { refreshWorlds, type ServerStatus } from '@/helpers/worlds' import { refreshWorlds, type ServerStatus } from '@/helpers/worlds'
@@ -733,7 +733,7 @@ watch(
const options = ref<InstanceType<typeof ContextMenu> | null>(null) const options = ref<InstanceType<typeof ContextMenu> | null>(null)
const startInstance = async (context: string) => { const startInstance = async () => {
if (!instance.value || loading.value) return if (!instance.value || loading.value) return
const id = props.id const id = props.id
const name = instance.value.name const name = instance.value.name
@@ -780,7 +780,7 @@ watch(
{ immediate: true }, { immediate: true },
) )
const stopInstance = async (context: string) => { const stopInstance = async () => {
stopping.value = true stopping.value = true
await kill(props.id).catch(handleError) await kill(props.id).catch(handleError)
stopping.value = false stopping.value = false
@@ -882,10 +882,10 @@ const handleRightClick = (event: MouseEvent) => {
const handleOptionsClick = async (args: { option: string; item: unknown }) => { const handleOptionsClick = async (args: { option: string; item: unknown }) => {
switch (args.option) { switch (args.option) {
case 'play': case 'play':
await startInstance('InstancePageContextMenu') await startInstance()
break break
case 'stop': case 'stop':
await stopInstance('InstancePageContextMenu') await stopInstance()
break break
case 'add_content': case 'add_content':
await router.push({ await router.push({
@@ -1408,7 +1408,8 @@ async function getUpdaterProjectVersions(
if (!versions) { if (!versions) {
versions = (await get_project_versions(projectId).catch(() => null)) as versions = (await get_project_versions(projectId).catch(() => null)) as
Labrinth.Versions.v2.Version[] | null | Labrinth.Versions.v2.Version[]
| null
} }
if (!versions && fetchError) { if (!versions && fetchError) {
@@ -1719,9 +1720,6 @@ async function applyToggleDisableBatch(items: ContentItem[], enabled: boolean) {
resultById, resultById,
false, false,
) )
for (const operation of operations) {
}
} catch (error) { } catch (error) {
for (const operation of operations) { for (const operation of operations) {
operation.optimisticPath = operation.originalFilePath operation.optimisticPath = operation.originalFilePath
@@ -845,7 +845,7 @@ async function installSelected(fileId: string | null) {
} }
if (isWorldMap.value) { if (isWorldMap.value) {
installing.value = true installing.value = true
await installCurseForgeWorld(project.value.id, fileId, instanceId.value, 'ProjectPage', () => { await installCurseForgeWorld(project.value.id, fileId, instanceId.value, () => {
installing.value = false installing.value = false
}).catch((error) => { }).catch((error) => {
installing.value = false installing.value = false
@@ -414,8 +414,8 @@ import {
ProjectSidebarServerInfo, ProjectSidebarServerInfo,
ProjectSidebarTags, ProjectSidebarTags,
requestInstall, requestInstall,
SelectedProjectsFloatingBar,
ScrollToTopButton, ScrollToTopButton,
SelectedProjectsFloatingBar,
usesTargetGameVersion, usesTargetGameVersion,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
@@ -72,12 +72,7 @@
</template> </template>
<script setup> <script setup>
import { import { CheckIcon, DownloadIcon, ExternalIcon, MoreVerticalIcon } from '@modrinth/assets'
CheckIcon,
DownloadIcon,
ExternalIcon,
MoreVerticalIcon,
} from '@modrinth/assets'
import { import {
ButtonStyled, ButtonStyled,
commonMessages, commonMessages,
@@ -108,7 +103,7 @@ const messages = defineMessages({
}, },
}) })
const props = defineProps({ defineProps({
project: { project: {
type: Object, type: Object,
default: () => {}, default: () => {},
@@ -517,7 +517,6 @@ export interface ContentInstallContext {
projectId: string | number, projectId: string | number,
fileId?: string | number | null, fileId?: string | number | null,
instanceId?: string | null, instanceId?: string | null,
source?: string,
callback?: ContentInstallCallback, callback?: ContentInstallCallback,
) => Promise<void> ) => Promise<void>
installingItems: Ref<Map<string, ContentItem[]>> installingItems: Ref<Map<string, ContentItem[]>>
@@ -1884,7 +1883,6 @@ export function createContentInstall(opts: {
markInstanceContentChanged(instance.id) markInstanceContentChanged(instance.id)
incompatibilityWarningModalRef?.hide() incompatibilityWarningModalRef?.hide()
removeInstallingItems(instance.id, [project.id]) removeInstallingItems(instance.id, [project.id])
} }
function handleIncompatibilityWarningCancel() { function handleIncompatibilityWarningCancel() {
@@ -2056,7 +2054,6 @@ export function createContentInstall(opts: {
: `/instance/${encodeURIComponent(id)}`, : `/instance/${encodeURIComponent(id)}`,
) )
settleCurrentCallback(version.id, installedProjectIds) settleCurrentCallback(version.id, installedProjectIds)
modalRef?.hide() modalRef?.hide()
} catch (err) { } catch (err) {
@@ -2116,10 +2113,14 @@ export function createContentInstall(opts: {
if (project.project_type === 'modpack') { if (project.project_type === 'modpack') {
if (shouldShowInstallTargetModal) hideContentInstallModal() if (shouldShowInstallTargetModal) hideContentInstallModal()
const [versions, packs] = await Promise.all([ const [versions, packs] = await Promise.all([
get_version_many(project.versions, 'must_revalidate') as Promise<Labrinth.Versions.v2.Version[]>, get_version_many(project.versions, 'must_revalidate') as Promise<
Labrinth.Versions.v2.Version[]
>,
list(), list(),
]) ])
const sortedVersions = versions.sort((a, b) => dayjs(b.date_published).valueOf() - dayjs(a.date_published).valueOf()) const sortedVersions = versions.sort(
(a, b) => dayjs(b.date_published).valueOf() - dayjs(a.date_published).valueOf(),
)
pendingModpackInstall = { pendingModpackInstall = {
project, project,
version: versionId ?? sortedVersions[0]?.id ?? '', version: versionId ?? sortedVersions[0]?.id ?? '',
@@ -2132,7 +2133,10 @@ export function createContentInstall(opts: {
sortedVersions.map((version) => [ sortedVersions.map((version) => [
version.id, version.id,
packs packs
.filter((pack) => pack.link?.project_id === project.id && pack.link?.version_id === version.id) .filter(
(pack) =>
pack.link?.project_id === project.id && pack.link?.version_id === version.id,
)
.map((pack) => ({ id: pack.id, name: pack.name })), .map((pack) => ({ id: pack.id, name: pack.name })),
]), ]),
) )
@@ -2391,7 +2395,6 @@ export function createContentInstall(opts: {
projectId: string | number, projectId: string | number,
fileId?: string | number | null, fileId?: string | number | null,
instanceId?: string | null, instanceId?: string | null,
source: string = 'unknown',
callback: ContentInstallCallback = () => {}, callback: ContentInstallCallback = () => {},
) { ) {
const modalSessionId = instanceId ? null : beginInstallSession(callback) const modalSessionId = instanceId ? null : beginInstallSession(callback)
@@ -2486,11 +2489,10 @@ export function createContentInstall(opts: {
projectId: string | number, projectId: string | number,
fileId?: string | number | null, fileId?: string | number | null,
instanceId?: string | null, instanceId?: string | null,
source: string = 'unknown',
callback: ContentInstallCallback = () => {}, callback: ContentInstallCallback = () => {},
) { ) {
return guardInstallRequest( return guardInstallRequest(
() => installCurseForgeWorldInternal(projectId, fileId, instanceId, source, callback), () => installCurseForgeWorldInternal(projectId, fileId, instanceId, callback),
callback, callback,
) )
} }
@@ -2532,8 +2534,7 @@ export function createContentInstall(opts: {
handleCurseForgeManualDownloadsImported, handleCurseForgeManualDownloadsImported,
async handleModpackInstall(versionId: string, name: string) { async handleModpackInstall(versionId: string, name: string) {
if (!pendingModpackInstall) return if (!pendingModpackInstall) return
const { project, source, callback, createInstanceCallback, provider } = const { project, source, callback, createInstanceCallback, provider } = pendingModpackInstall
pendingModpackInstall
pendingModpackInstall = null pendingModpackInstall = null
if (provider === 'curseforge') { if (provider === 'curseforge') {
const numericProjectId = Number(project.id.replace(/^curseforge:/, '')) const numericProjectId = Number(project.id.replace(/^curseforge:/, ''))
+1
View File
@@ -547,6 +547,7 @@ fn keyring_error(
.into() .into()
} }
#[cfg(test)]
fn parse_custom_uuid(uuid: Option<String>) -> Result<Option<uuid::Uuid>> { fn parse_custom_uuid(uuid: Option<String>) -> Result<Option<uuid::Uuid>> {
let Some(uuid) = uuid else { let Some(uuid) = uuid else {
return Ok(None); return Ok(None);
+4 -1
View File
@@ -548,6 +548,10 @@ fn schedule_tray_menu_update(app: &AppHandle) {
}); });
} }
pub fn state_initialized(app: &AppHandle) {
schedule_tray_menu_update(app);
}
async fn rebuild_tray_menu( async fn rebuild_tray_menu(
app: &AppHandle, app: &AppHandle,
active: bool, active: bool,
@@ -708,7 +712,6 @@ pub fn init(app: &AppHandle) {
.build(app) .build(app)
.expect("failed to create system tray"); .expect("failed to create system tray");
let _tray = tray; let _tray = tray;
schedule_tray_menu_update(app);
let app_handle = app.clone(); let app_handle = app.clone();
app.listen("process", move |event| { app.listen("process", move |event| {
let Ok(payload) = let Ok(payload) =
+10 -4
View File
@@ -315,6 +315,8 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
app.fs_scope() app.fs_scope()
.allow_directory(state.directories.servers_dir(), true)?; .allow_directory(state.directories.servers_dir(), true)?;
lightweight_mode::state_initialized(&app);
Ok(()) Ok(())
} }
@@ -569,10 +571,9 @@ fn is_dev() -> bool {
#[tauri::command] #[tauri::command]
fn are_updates_enabled() -> bool { fn are_updates_enabled() -> bool {
// StarLight launcher ships without the built-in update checker. // The frontend waits for launcher state initialization before starting
// Keep the backend interface in place for a future user-supplied // the built-in updater.
// update URL to be wired in by replacing this no-op gate later. true
false
} }
#[cfg(feature = "updater")] #[cfg(feature = "updater")]
@@ -910,6 +911,11 @@ fn main() {
Ok(()) Ok(())
}); });
#[cfg(feature = "updater")]
{
builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
}
builder = builder builder = builder
.plugin(api::ai::init()) .plugin(api::ai::init())
.plugin(api::auth::init()) .plugin(api::auth::init())
+7
View File
@@ -71,6 +71,13 @@
"schemes": ["starlight"] "schemes": ["starlight"]
}, },
"mobile": [] "mobile": []
},
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQ0NDhERTUxN0E3MERBNDYKUldSRzJuQjZVZDVJMUpKSUJ6blVpVzJQSTgvSjlyeVRGK0ozRzcwVWtpbTJjbUl0V0NRcC9OQVoK",
"endpoints": ["https://skin.starlight.cool/starlight/launcher/latest"],
"windows": {
"installMode": "quiet"
}
} }
}, },
"app": { "app": {
File diff suppressed because it is too large. Load diff
+29 -30
View File
@@ -1,34 +1,33 @@
{ {
"tag_name": "v1.9.5", "tag_name": "v1.0.0",
"assets": [ "assets": [
"Axolotl.Launcher-1.9.5-1.aarch64.rpm", "Axolotl_Launcher_1.0.0_x64_modern-setup.exe",
"Axolotl.Launcher-1.9.5-1.aarch64.rpm.sig", "Axolotl_Launcher_1.0.0_x64_modern-setup.exe.sig",
"Axolotl.Launcher-1.9.5-1.x86_64.rpm", "Axolotl_Launcher_1.0.0_x64_nsis-setup.exe",
"Axolotl.Launcher-1.9.5-1.x86_64.rpm.sig", "Axolotl_Launcher_1.0.0_x64_nsis-setup.exe.sig",
"Axolotl.Launcher_1.9.5_aarch64.AppImage", "Axolotl_Launcher_1.0.0_x64_portable.zip",
"Axolotl.Launcher_1.9.5_aarch64.AppImage.sig", "Starlight.Launcher-1.0.0-1.aarch64.rpm",
"Axolotl.Launcher_1.9.5_aarch64.AppImage.tar.gz", "Starlight.Launcher-1.0.0-1.aarch64.rpm.sig",
"Axolotl.Launcher_1.9.5_aarch64.AppImage.tar.gz.sig", "Starlight.Launcher-1.0.0-1.x86_64.rpm",
"Axolotl.Launcher_1.9.5_amd64.AppImage", "Starlight.Launcher-1.0.0-1.x86_64.rpm.sig",
"Axolotl.Launcher_1.9.5_amd64.AppImage.sig", "Starlight.Launcher_1.0.0_aarch64.AppImage",
"Axolotl.Launcher_1.9.5_amd64.AppImage.tar.gz", "Starlight.Launcher_1.0.0_aarch64.AppImage.sig",
"Axolotl.Launcher_1.9.5_amd64.AppImage.tar.gz.sig", "Starlight.Launcher_1.0.0_aarch64.AppImage.tar.gz",
"Axolotl.Launcher_1.9.5_amd64.deb", "Starlight.Launcher_1.0.0_aarch64.AppImage.tar.gz.sig",
"Axolotl.Launcher_1.9.5_amd64.deb.sig", "Starlight.Launcher_1.0.0_amd64.AppImage",
"Axolotl.Launcher_1.9.5_arm64.deb", "Starlight.Launcher_1.0.0_amd64.AppImage.sig",
"Axolotl.Launcher_1.9.5_arm64.deb.sig", "Starlight.Launcher_1.0.0_amd64.AppImage.tar.gz",
"Axolotl.Launcher_1.9.5_universal.dmg", "Starlight.Launcher_1.0.0_amd64.AppImage.tar.gz.sig",
"Axolotl.Launcher_1.9.5_x64-setup.exe", "Starlight.Launcher_1.0.0_amd64.deb",
"Axolotl.Launcher_1.9.5_x64-setup.exe.sig", "Starlight.Launcher_1.0.0_amd64.deb.sig",
"Axolotl.Launcher_1.9.5_x64-setup.nsis.zip", "Starlight.Launcher_1.0.0_arm64.deb",
"Axolotl.Launcher_1.9.5_x64-setup.nsis.zip.sig", "Starlight.Launcher_1.0.0_arm64.deb.sig",
"Axolotl.Launcher_universal.app.tar.gz", "Starlight.Launcher_1.0.0_universal.dmg",
"Axolotl.Launcher_universal.app.tar.gz.sig", "Starlight.Launcher_1.0.0_x64-setup.exe",
"Axolotl_Launcher_1.9.5_x64_modern-setup.exe", "Starlight.Launcher_1.0.0_x64-setup.exe.sig",
"Axolotl_Launcher_1.9.5_x64_modern-setup.exe.sig", "Starlight.Launcher_1.0.0_x64-setup.nsis.zip",
"Axolotl_Launcher_1.9.5_x64_nsis-setup.exe", "Starlight.Launcher_1.0.0_x64-setup.nsis.zip.sig",
"Axolotl_Launcher_1.9.5_x64_nsis-setup.exe.sig", "Starlight.Launcher_universal.app.tar.gz",
"Axolotl_Launcher_1.9.5_x64_portable.zip", "Starlight.Launcher_universal.app.tar.gz.sig"
"latest.json"
] ]
} }
@@ -7,7 +7,6 @@ use crate::state::{
}; };
use crate::util::{fetch::write_cached_icon, io}; use crate::util::{fetch::write_cached_icon, io};
use std::path::Path; use std::path::Path;
use std::path::PathBuf;
#[tracing::instrument] #[tracing::instrument]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@@ -87,11 +87,6 @@ pub(crate) async fn sync_instance_content_files(
.as_deref() .as_deref()
.map(str::trim) .map(str::trim)
.is_some_and(|linked| !linked.is_empty()); .is_some_and(|linked| !linked.is_empty());
let instance_files_root = if is_direct_linked {
content_root.clone()
} else {
state.directories.instances_dir().join(&instance.path)
};
let cache_key_path = if is_direct_linked { let cache_key_path = if is_direct_linked {
content_root.to_string_lossy().into_owned() content_root.to_string_lossy().into_owned()
} else { } else {
@@ -168,8 +163,9 @@ pub(crate) async fn sync_instance_content_files(
let hash_key = file.hash_cache_key.trim_end_matches(".disabled"); let hash_key = file.hash_cache_key.trim_end_matches(".disabled");
let existing_file = existing_files_by_path.get(&file.relative_path); let existing_file = existing_files_by_path.get(&file.relative_path);
let (scanned_sha1, scanned_size) = if existing_file.is_some() { let (scanned_sha1, scanned_size) = if existing_file.is_some() {
let path = // Read from the resolved game directory. Managed StarLight
join_content_path(&instance_files_root, &file.relative_path); // instances may keep their files outside the launcher data root.
let path = join_content_path(&content_root, &file.relative_path);
let (_, sha1) = fetch::sha1_file_async(&path).await?; let (_, sha1) = fetch::sha1_file_async(&path).await?;
(sha1, file.size) (sha1, file.size)
} else { } else {
@@ -748,7 +744,9 @@ mod tests {
base_path: minecraft.path().to_path_buf(), base_path: minecraft.path().to_path_buf(),
instance_folder: "versions/1.12.2-linked".to_string(), instance_folder: "versions/1.12.2-linked".to_string(),
instance_path: None, instance_path: None,
game_dir_mode: None, game_dir_mode: Some(
crate::launcher::ExternalGameDirMode::Shared,
),
}, },
&state, &state,
) )
@@ -875,6 +873,59 @@ mod tests {
); );
} }
#[tokio::test]
async fn refresh_reconciles_externally_replaced_profile_mods() {
let state = global_state().await;
let metadata = crate::api::instance::create(
format!("sync-external {}", uuid::Uuid::new_v4()),
"1.20.1".to_string(),
crate::state::ModLoader::Vanilla,
None,
None,
crate::state::InstanceLink::Unmanaged,
None,
None,
)
.await
.unwrap();
let instance = &metadata.instance;
let mods = state.directories.instance_game_dir(instance).join("mods");
fs::create_dir_all(&mods).unwrap();
fs::write(mods.join("old-version.jar"), "old").unwrap();
fs::write(mods.join("same-name.jar"), "old").unwrap();
let initial =
sync_instance_content_files(instance, &state).await.unwrap();
assert_eq!(initial.len(), 2);
fs::remove_file(mods.join("old-version.jar")).unwrap();
fs::write(mods.join("new-version.jar"), "new version").unwrap();
fs::write(mods.join("same-name.jar"), "new").unwrap();
let refreshed =
sync_instance_content_files(instance, &state).await.unwrap();
assert_eq!(refreshed.len(), 2);
assert!(
refreshed
.iter()
.any(|file| file.relative_path == "mods/new-version.jar")
);
assert!(
!refreshed
.iter()
.any(|file| file.relative_path == "mods/old-version.jar")
);
let before = initial
.iter()
.find(|file| file.relative_path == "mods/same-name.jar")
.unwrap();
let after = refreshed
.iter()
.find(|file| file.relative_path == "mods/same-name.jar")
.unwrap();
assert_eq!(before.id, after.id);
assert_ne!(before.sha1, after.sha1);
assert_eq!(before.size, after.size);
}
/// Regression probe: an instance whose `game_dir_override` points to an /// Regression probe: an instance whose `game_dir_override` points to an
/// external `.minecraft` root must have its content scanned from that root, /// external `.minecraft` root must have its content scanned from that root,
/// not from the (empty) managed instance folder. This is the split-brain /// not from the (empty) managed instance folder. This is the split-brain
@@ -947,6 +998,48 @@ mod tests {
"expected the override-root mod to be scanned" "expected the override-root mod to be scanned"
); );
assert!(files[0].relative_path.ends_with("mods/my-mod.jar")); assert!(files[0].relative_path.ends_with("mods/my-mod.jar"));
let refreshed =
sync_instance_content_files(&instance, &state).await.expect(
"repeat refresh must hash files in the actual game directory",
);
assert_eq!(refreshed[0].sha1, files[0].sha1);
fs::write(mods_dir.join("my-mod.jar"), "new").unwrap();
fs::write(mods_dir.join("added.jar"), "added mod").unwrap();
let replaced = sync_instance_content_files(&instance, &state)
.await
.unwrap();
assert_eq!(replaced.len(), 2);
let updated = replaced
.iter()
.find(|file| file.relative_path == "mods/my-mod.jar")
.unwrap();
assert_eq!(updated.id, files[0].id);
assert_ne!(updated.sha1, files[0].sha1);
assert_eq!(updated.size, files[0].size);
fs::remove_file(mods_dir.join("my-mod.jar")).unwrap();
fs::rename(
mods_dir.join("added.jar"),
mods_dir.join("added.jar.disabled"),
)
.unwrap();
let removed = sync_instance_content_files(&instance, &state)
.await
.unwrap();
assert_eq!(removed.len(), 1);
assert_eq!(removed[0].relative_path, "mods/added.jar.disabled");
assert!(!removed[0].enabled);
let stored =
sqlite::content_rows::get_instance_files(&instance.id, &state.pool)
.await
.unwrap();
assert!(
stored
.iter()
.any(|file| file.id == files[0].id && file.missing)
);
} }
#[test] #[test]
+1 -1
View File
@@ -5855,7 +5855,7 @@ async fn download_to_path_inner(
can_switch_route, can_switch_route,
retry_with_single_thread, retry_with_single_thread,
); );
while attempts < file_attempt_budget { if attempts < file_attempt_budget {
attempts += 1; attempts += 1;
tracing::debug!( tracing::debug!(
path = %destination.display(), path = %destination.display(),
+1 -1
View File
@@ -3,7 +3,7 @@ use crate::minecraft::{
}; };
use chrono::{DateTime, NaiveDateTime, Utc}; use chrono::{DateTime, NaiveDateTime, Utc};
use serde::{Deserialize, Deserializer, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
use std::collections::{HashMap, HashSet}; use std::collections::HashMap;
/// The latest version of the format the fabric model structs deserialize to /// The latest version of the format the fabric model structs deserialize to
pub const CURRENT_FABRIC_FORMAT_VERSION: usize = 1; pub const CURRENT_FABRIC_FORMAT_VERSION: usize = 1;