Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15c375bca5 | ||
|
|
d2a319a4d5 | ||
|
|
2359b80250 | ||
|
|
c273734aec | ||
|
|
f89cbff37e |
No files matched your search
@@ -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 签名校验机制,自动在后台完成更新,无需手动下载安装更新。
|
||||||
|
|
||||||
| 系统平台 | 推荐下载文件 |
|
| 系统平台 | 推荐下载文件 |
|
||||||
|
|||||||
@@ -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,8 @@ 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 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 +138,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,6 +489,12 @@ 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()
|
||||||
@@ -1701,6 +1706,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][]
|
||||||
@@ -2423,9 +2429,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 +2499,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 +2587,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"
|
||||||
|
|||||||
@@ -32,6 +32,50 @@ export const ANNOUNCEMENT_CHANGE_TYPES: readonly AnnouncementChangeType[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export const launcherAnnouncements: readonly LauncherAnnouncement[] = [
|
export const launcherAnnouncements: readonly LauncherAnnouncement[] = [
|
||||||
|
{
|
||||||
|
id: 'launcher-1.0.0',
|
||||||
|
version: '1.0.0',
|
||||||
|
publishedAt: '2026-09-26',
|
||||||
|
title: {
|
||||||
|
'en-US': 'Starlight Launcher 1.0.0',
|
||||||
|
'zh-CN': 'Starlight Launcher 1.0.0',
|
||||||
|
},
|
||||||
|
changes: {
|
||||||
|
added: [
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'Enabled the built-in automatic updater. The launcher can now check for and install new versions from the update channel.',
|
||||||
|
'zh-CN': '启用内置自动更新,启动器现在可以从更新通道检查并安装新版本。',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fixed: [
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'Fixed a crash on startup caused by the updater running before launcher state finished initializing.',
|
||||||
|
'zh-CN': '修复了更新器在启动器状态初始化完成前运行导致的启动崩溃。',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'launcher-1.0.0-beta15',
|
||||||
|
version: '1.0.0-beta15',
|
||||||
|
publishedAt: '2026-09-26',
|
||||||
|
title: {
|
||||||
|
'en-US': 'Starlight Launcher 1.0.0-beta15',
|
||||||
|
'zh-CN': 'Starlight Launcher 1.0.0-beta15',
|
||||||
|
},
|
||||||
|
changes: {
|
||||||
|
fixed: [
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'Fixed the launcher crashing on startup in 1.0.0-beta14 by temporarily reverting the built-in updater. The updater will be re-enabled after the crash is resolved.',
|
||||||
|
'zh-CN':
|
||||||
|
'修复 1.0.0-beta14 中启动器一打开就崩溃的问题:暂时回退了内置自动更新。待崩溃问题解决后会重新启用。',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'launcher-1.0.0-beta14',
|
id: 'launcher-1.0.0-beta14',
|
||||||
version: '1.0.0-beta14',
|
version: '1.0.0-beta14',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
`${
|
||||||
? formatMessage(messages.databaseOperationActiveTarget)
|
targetChannel === activeChannel
|
||||||
: formatMessage(messages.databaseOperationFailed),
|
? formatMessage(messages.databaseOperationActiveTarget)
|
||||||
|
: 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 () => {
|
||||||
|
|||||||
@@ -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 = '') => ({
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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 },
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -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'
|
||||||
@@ -567,7 +567,7 @@ const messages = defineMessages({
|
|||||||
favoritesLoading: {
|
favoritesLoading: {
|
||||||
id: 'app.content-favorites.loading',
|
id: 'app.content-favorites.loading',
|
||||||
defaultMessage: 'Updating favorites…',
|
defaultMessage: 'Updating favorites…',
|
||||||
},
|
},
|
||||||
viewDependents: {
|
viewDependents: {
|
||||||
id: 'project.actions.view-dependents',
|
id: 'project.actions.view-dependents',
|
||||||
defaultMessage: 'View dependents',
|
defaultMessage: 'View dependents',
|
||||||
|
|||||||
@@ -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:/, ''))
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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) =
|
||||||
|
|||||||
@@ -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,8 +571,8 @@ fn is_dev() -> bool {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn are_updates_enabled() -> bool {
|
fn are_updates_enabled() -> bool {
|
||||||
// StarLight launcher enables the built-in updater; the endpoint and
|
// The frontend waits for launcher state initialization before starting
|
||||||
// signing public key are configured in tauri-release.conf.json.
|
// the built-in updater.
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -909,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())
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -1,6 +1,46 @@
|
|||||||
{
|
{
|
||||||
"updated_at": "2026-09-25T23:51:11.379Z",
|
"updated_at": "2026-09-26T06:23:01.194Z",
|
||||||
"announcements": [
|
"announcements": [
|
||||||
|
{
|
||||||
|
"id": "launcher-1.0.0",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"publishedAt": "2026-09-26",
|
||||||
|
"title": {
|
||||||
|
"en-US": "Starlight Launcher 1.0.0",
|
||||||
|
"zh-CN": "Starlight Launcher 1.0.0"
|
||||||
|
},
|
||||||
|
"changes": {
|
||||||
|
"added": [
|
||||||
|
{
|
||||||
|
"en-US": "Enabled the built-in automatic updater. The launcher can now check for and install new versions from the update channel.",
|
||||||
|
"zh-CN": "启用内置自动更新,启动器现在可以从更新通道检查并安装新版本。"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fixed": [
|
||||||
|
{
|
||||||
|
"en-US": "Fixed a crash on startup caused by the updater running before launcher state finished initializing.",
|
||||||
|
"zh-CN": "修复了更新器在启动器状态初始化完成前运行导致的启动崩溃。"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "launcher-1.0.0-beta15",
|
||||||
|
"version": "1.0.0-beta15",
|
||||||
|
"publishedAt": "2026-09-26",
|
||||||
|
"title": {
|
||||||
|
"en-US": "Starlight Launcher 1.0.0-beta15",
|
||||||
|
"zh-CN": "Starlight Launcher 1.0.0-beta15"
|
||||||
|
},
|
||||||
|
"changes": {
|
||||||
|
"fixed": [
|
||||||
|
{
|
||||||
|
"en-US": "Fixed the launcher crashing on startup in 1.0.0-beta14 by temporarily reverting the built-in updater. The updater will be re-enabled after the crash is resolved.",
|
||||||
|
"zh-CN": "修复 1.0.0-beta14 中启动器一打开就崩溃的问题:暂时回退了内置自动更新。待崩溃问题解决后会重新启用。"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "launcher-1.0.0-beta14",
|
"id": "launcher-1.0.0-beta14",
|
||||||
"version": "1.0.0-beta14",
|
"version": "1.0.0-beta14",
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user