feat: 一键安装支持自选游戏目录
Some checks failed
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
Some checks failed
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
StarLight 实例一键安装此前固定落到启动器默认目录,无法选择游戏数据位置。 现复用创建流程的外部游戏目录能力:点击一键安装后弹出目录选择框, 只允许修改游戏目录,其余参数仍由服务器整合包决定。 - hosted::create 接受 game_dir_root,拼为 <root>/<包名> 作为 game_dir_override (刻意避开 versions/ 布局,该结构被外部直链实例检测占用) - 新增顶层命令 get_launcher_root_dir,作为弹窗默认值(可执行文件所在目录) - 新增 HostedGameDirModal 弹窗:只读路径框 + 浏览 + 默认位置 + 安装预览 - 补齐 en-US / zh-CN / zh-TW 三语文案 hosted_create 增加 game_dir_root 参数,前端 hostedCreate/install 同步透传。
This commit is contained in:
161
apps/app-frontend/src/components/instance/HostedGameDirModal.vue
Normal file
161
apps/app-frontend/src/components/instance/HostedGameDirModal.vue
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
<template>
|
||||||
|
<NewModal
|
||||||
|
ref="modal"
|
||||||
|
:header="formatMessage(messages.header)"
|
||||||
|
max-width="560px"
|
||||||
|
:on-hide="handleHide"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<p class="m-0 text-secondary">
|
||||||
|
{{ formatMessage(messages.description) }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span class="font-semibold text-contrast">
|
||||||
|
{{ formatMessage(messages.gameDirLabel) }}
|
||||||
|
</span>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<StyledInput
|
||||||
|
class="flex-1"
|
||||||
|
:model-value="selectedPath"
|
||||||
|
readonly
|
||||||
|
:placeholder="defaultPath || formatMessage(messages.noSelection)"
|
||||||
|
/>
|
||||||
|
<ButtonStyled>
|
||||||
|
<button type="button" @click="browse">
|
||||||
|
<FolderOpenIcon />
|
||||||
|
{{ formatMessage(messages.browse) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="defaultPath && selectedPath !== defaultPath"
|
||||||
|
type="button"
|
||||||
|
class="self-start text-sm text-brand underline decoration-transparent underline-offset-2 transition-colors hover:decoration-current"
|
||||||
|
@click="selectedPath = defaultPath"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.resetDefault) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="previewPath" class="m-0 text-sm text-secondary break-all">
|
||||||
|
{{ formatMessage(messages.preview, { path: previewPath }) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<ButtonStyled type="outlined">
|
||||||
|
<button type="button" @click="handleCancel">
|
||||||
|
<XIcon />
|
||||||
|
{{ formatMessage(commonMessages.cancelButton) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button type="button" :disabled="!selectedPath" @click="handleConfirm">
|
||||||
|
<CheckIcon />
|
||||||
|
{{ formatMessage(messages.confirm) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</NewModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { CheckIcon, FolderOpenIcon, XIcon } from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
ButtonStyled,
|
||||||
|
commonMessages,
|
||||||
|
defineMessages,
|
||||||
|
injectFilePicker,
|
||||||
|
NewModal,
|
||||||
|
StyledInput,
|
||||||
|
useVIntl,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'confirm', gameDirRoot: string): void
|
||||||
|
(e: 'cancel'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
const filePicker = injectFilePicker()
|
||||||
|
const modal = ref<InstanceType<typeof NewModal>>()
|
||||||
|
const defaultPath = ref('')
|
||||||
|
const selectedPath = ref('')
|
||||||
|
const accepted = ref(false)
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
header: {
|
||||||
|
id: 'app.hosted-install.game-dir.header',
|
||||||
|
defaultMessage: 'Choose game directory',
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
id: 'app.hosted-install.game-dir.description',
|
||||||
|
defaultMessage:
|
||||||
|
'StarLight instance data (mods, saves, configs, resource packs) is stored in an external game directory. Pick a root folder — the modpack gets its own subfolder inside it.',
|
||||||
|
},
|
||||||
|
gameDirLabel: {
|
||||||
|
id: 'app.hosted-install.game-dir.label',
|
||||||
|
defaultMessage: 'Game directory root',
|
||||||
|
},
|
||||||
|
browse: {
|
||||||
|
id: 'app.hosted-install.game-dir.browse',
|
||||||
|
defaultMessage: 'Browse',
|
||||||
|
},
|
||||||
|
noSelection: {
|
||||||
|
id: 'app.hosted-install.game-dir.no-selection',
|
||||||
|
defaultMessage: 'No folder selected',
|
||||||
|
},
|
||||||
|
resetDefault: {
|
||||||
|
id: 'app.hosted-install.game-dir.reset-default',
|
||||||
|
defaultMessage: 'Use default location',
|
||||||
|
},
|
||||||
|
preview: {
|
||||||
|
id: 'app.hosted-install.game-dir.preview',
|
||||||
|
defaultMessage: 'Game files will be installed to: {path}',
|
||||||
|
},
|
||||||
|
confirm: {
|
||||||
|
id: 'app.hosted-install.game-dir.confirm',
|
||||||
|
defaultMessage: 'Install',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const previewPath = computed(() => {
|
||||||
|
const base = (selectedPath.value ?? '').replace(/[\\/]+$/, '')
|
||||||
|
return base ? `${base}/<pack name>` : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
async function show() {
|
||||||
|
if (!defaultPath.value) {
|
||||||
|
defaultPath.value = await invoke<string>('get_launcher_root_dir').catch(() => '')
|
||||||
|
}
|
||||||
|
if (!selectedPath.value) selectedPath.value = defaultPath.value
|
||||||
|
accepted.value = false
|
||||||
|
modal.value?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function browse() {
|
||||||
|
const picked = await filePicker.pickFolder?.()
|
||||||
|
if (picked?.path) selectedPath.value = picked.path
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
modal.value?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConfirm() {
|
||||||
|
accepted.value = true
|
||||||
|
modal.value?.hide()
|
||||||
|
emit('confirm', selectedPath.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleHide() {
|
||||||
|
if (!accepted.value) emit('cancel')
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ show })
|
||||||
|
</script>
|
||||||
@ -23,7 +23,7 @@ export function useHostedCreation() {
|
|||||||
completed.value = false
|
completed.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function install() {
|
async function install(gameDirRoot?: string | null) {
|
||||||
if (installing.value) return
|
if (installing.value) return
|
||||||
installing.value = true
|
installing.value = true
|
||||||
installError.value = ''
|
installError.value = ''
|
||||||
@ -40,7 +40,7 @@ export function useHostedCreation() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (completed.value) return createdInstance.value
|
if (completed.value) return createdInstance.value
|
||||||
createdInstance.value ??= await hostedCreate()
|
createdInstance.value ??= await hostedCreate(gameDirRoot)
|
||||||
const instanceId = createdInstance.value
|
const instanceId = createdInstance.value
|
||||||
await hostedSync(instanceId)
|
await hostedSync(instanceId)
|
||||||
if (attempt !== generation) return
|
if (attempt !== generation) return
|
||||||
|
|||||||
@ -99,7 +99,10 @@ export interface HostedSyncResult {
|
|||||||
}
|
}
|
||||||
export const hostedDefault = () =>
|
export const hostedDefault = () =>
|
||||||
invokeWithSession<HostedPublication>('plugin:install|hosted_default')
|
invokeWithSession<HostedPublication>('plugin:install|hosted_default')
|
||||||
export const hostedCreate = () => invokeWithSession<string>('plugin:install|hosted_create')
|
export const hostedCreate = (gameDirRoot?: string | null) =>
|
||||||
|
invokeWithSession<string>('plugin:install|hosted_create', {
|
||||||
|
gameDirRoot: gameDirRoot ?? null,
|
||||||
|
})
|
||||||
export const hostedBinding = (instanceId: string) =>
|
export const hostedBinding = (instanceId: string) =>
|
||||||
invokeHosted<HostedBinding | null>('plugin:install|hosted_binding', { instanceId })
|
invokeHosted<HostedBinding | null>('plugin:install|hosted_binding', { instanceId })
|
||||||
export const hostedSync = (instanceId: string) => {
|
export const hostedSync = (instanceId: string) => {
|
||||||
|
|||||||
@ -29,6 +29,14 @@
|
|||||||
"app.hosted-packs.auto-installing": { "message": "Downloading and installing the server modpack…" },
|
"app.hosted-packs.auto-installing": { "message": "Downloading and installing the server modpack…" },
|
||||||
"app.hosted-packs.auto-description": { "message": "Download and automatically install the modpack from StarLight to play on the StarLight server with one click." },
|
"app.hosted-packs.auto-description": { "message": "Download and automatically install the modpack from StarLight to play on the StarLight server with one click." },
|
||||||
"app.hosted-packs.auto-install": { "message": "Install the StarLight modpack" },
|
"app.hosted-packs.auto-install": { "message": "Install the StarLight modpack" },
|
||||||
|
"app.hosted-install.game-dir.header": { "message": "Choose game directory" },
|
||||||
|
"app.hosted-install.game-dir.description": { "message": "StarLight instance data (mods, saves, configs, resource packs) is stored in an external game directory. Pick a root folder — the modpack gets its own subfolder inside it." },
|
||||||
|
"app.hosted-install.game-dir.label": { "message": "Game directory root" },
|
||||||
|
"app.hosted-install.game-dir.browse": { "message": "Browse" },
|
||||||
|
"app.hosted-install.game-dir.no-selection": { "message": "No folder selected" },
|
||||||
|
"app.hosted-install.game-dir.reset-default": { "message": "Use default location" },
|
||||||
|
"app.hosted-install.game-dir.preview": { "message": "Game files will be installed to: {path}" },
|
||||||
|
"app.hosted-install.game-dir.confirm": { "message": "Install" },
|
||||||
"app.onboarding.instance-mode.title": { "message": "Choose your instance type" },
|
"app.onboarding.instance-mode.title": { "message": "Choose your instance type" },
|
||||||
"app.onboarding.instance-mode.description": { "message": "StarLight automatically installs the administrator-selected modpack and versions, then checks and completes updates before every launch. A StarLight login is required. Choose Local to select your own versions and modpacks for other servers or single-player." },
|
"app.onboarding.instance-mode.description": { "message": "StarLight automatically installs the administrator-selected modpack and versions, then checks and completes updates before every launch. A StarLight login is required. Choose Local to select your own versions and modpacks for other servers or single-player." },
|
||||||
"app.instance-mode.title": { "message": "Instance type" },
|
"app.instance-mode.title": { "message": "Instance type" },
|
||||||
|
|||||||
@ -29,6 +29,14 @@
|
|||||||
"app.hosted-packs.auto-installing": { "message": "正在下载并安装服务器整合包…" },
|
"app.hosted-packs.auto-installing": { "message": "正在下载并安装服务器整合包…" },
|
||||||
"app.hosted-packs.auto-description": { "message": "从StarLight服务器获取整合包并自动安装,可一键游玩StarLight服务器" },
|
"app.hosted-packs.auto-description": { "message": "从StarLight服务器获取整合包并自动安装,可一键游玩StarLight服务器" },
|
||||||
"app.hosted-packs.auto-install": { "message": "安装 StarLight 官方整合包" },
|
"app.hosted-packs.auto-install": { "message": "安装 StarLight 官方整合包" },
|
||||||
|
"app.hosted-install.game-dir.header": { "message": "选择游戏目录" },
|
||||||
|
"app.hosted-install.game-dir.description": { "message": "StarLight 实例的游戏数据(mods、存档、配置、资源包)会存放在外部游戏目录中。请选择一个根目录,整合包会在其中单独建一个子文件夹。" },
|
||||||
|
"app.hosted-install.game-dir.label": { "message": "游戏目录根路径" },
|
||||||
|
"app.hosted-install.game-dir.browse": { "message": "浏览" },
|
||||||
|
"app.hosted-install.game-dir.no-selection": { "message": "尚未选择文件夹" },
|
||||||
|
"app.hosted-install.game-dir.reset-default": { "message": "使用默认位置" },
|
||||||
|
"app.hosted-install.game-dir.preview": { "message": "游戏文件将安装到:{path}" },
|
||||||
|
"app.hosted-install.game-dir.confirm": { "message": "安装" },
|
||||||
"app.onboarding.instance-mode.title": { "message": "选择实例类型" },
|
"app.onboarding.instance-mode.title": { "message": "选择实例类型" },
|
||||||
"app.onboarding.instance-mode.description": { "message": "StarLight 实例自动安装管理员指定的整合包和版本,每次启动先检查并完成更新,需要登录 StarLight 账号。本地实例可自选版本和整合包,适合第三方服务器与单人游玩。" },
|
"app.onboarding.instance-mode.description": { "message": "StarLight 实例自动安装管理员指定的整合包和版本,每次启动先检查并完成更新,需要登录 StarLight 账号。本地实例可自选版本和整合包,适合第三方服务器与单人游玩。" },
|
||||||
"app.instance-mode.title": { "message": "实例类型" },
|
"app.instance-mode.title": { "message": "实例类型" },
|
||||||
|
|||||||
@ -12,6 +12,14 @@
|
|||||||
"app.hosted-packs.auto-installing": { "message": "正在下載並安裝伺服器整合包…" },
|
"app.hosted-packs.auto-installing": { "message": "正在下載並安裝伺服器整合包…" },
|
||||||
"app.hosted-packs.auto-description": { "message": "從StarLight伺服器取得整合包並自動安裝,可一鍵遊玩StarLight伺服器" },
|
"app.hosted-packs.auto-description": { "message": "從StarLight伺服器取得整合包並自動安裝,可一鍵遊玩StarLight伺服器" },
|
||||||
"app.hosted-packs.auto-install": { "message": "安裝 StarLight 官方整合包" },
|
"app.hosted-packs.auto-install": { "message": "安裝 StarLight 官方整合包" },
|
||||||
|
"app.hosted-install.game-dir.header": { "message": "選擇遊戲目錄" },
|
||||||
|
"app.hosted-install.game-dir.description": { "message": "StarLight 實例的遊戲資料(mods、存檔、設定、資源包)會存放在外部遊戲目錄中。請選擇一個根目錄,整合包會在其中另外建立一個子資料夾。" },
|
||||||
|
"app.hosted-install.game-dir.label": { "message": "遊戲目錄根路徑" },
|
||||||
|
"app.hosted-install.game-dir.browse": { "message": "瀏覽" },
|
||||||
|
"app.hosted-install.game-dir.no-selection": { "message": "尚未選擇資料夾" },
|
||||||
|
"app.hosted-install.game-dir.reset-default": { "message": "使用預設位置" },
|
||||||
|
"app.hosted-install.game-dir.preview": { "message": "遊戲檔案將安裝到:{path}" },
|
||||||
|
"app.hosted-install.game-dir.confirm": { "message": "安裝" },
|
||||||
"app.onboarding.instance-mode.title": { "message": "選擇實例類型" },
|
"app.onboarding.instance-mode.title": { "message": "選擇實例類型" },
|
||||||
"app.onboarding.instance-mode.description": { "message": "StarLight 實例自動安裝管理員指定的整合包和版本,每次啟動先檢查並完成更新,需要登入 StarLight 帳號。本地實例可自行選擇版本和整合包,適合第三方伺服器與單人遊玩。" },
|
"app.onboarding.instance-mode.description": { "message": "StarLight 實例自動安裝管理員指定的整合包和版本,每次啟動先檢查並完成更新,需要登入 StarLight 帳號。本地實例可自行選擇版本和整合包,適合第三方伺服器與單人遊玩。" },
|
||||||
"app.instance-mode.title": { "message": "實例類型" },
|
"app.instance-mode.title": { "message": "實例類型" },
|
||||||
|
|||||||
@ -4,6 +4,7 @@ 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 InstanceModeOptions from '@/components/instance/InstanceModeOptions.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 type { InstanceMode } from '@/helpers/hosted-packs'
|
||||||
import { useHostedCreation } from '@/composables/useHostedCreation'
|
import { useHostedCreation } from '@/composables/useHostedCreation'
|
||||||
@ -15,6 +16,18 @@ const { installing, installError, createdInstance, completed, acknowledge, insta
|
|||||||
const instanceMode = ref<InstanceMode>(
|
const instanceMode = ref<InstanceMode>(
|
||||||
installing.value || createdInstance.value ? 'starlight' : 'local',
|
installing.value || createdInstance.value ? 'starlight' : 'local',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const hostedGameDirModal = ref<InstanceType<typeof HostedGameDirModal>>()
|
||||||
|
|
||||||
|
// One-click StarLight installs reuse the same external game-directory choice
|
||||||
|
// as the custom creation flow: the pack gets its own folder under `<root>`.
|
||||||
|
function promptHostedGameDir() {
|
||||||
|
hostedGameDirModal.value?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installHostedWithGameDir(gameDirRoot: string) {
|
||||||
|
await install(gameDirRoot)
|
||||||
|
}
|
||||||
async function openCompleted(instanceId: string) {
|
async function openCompleted(instanceId: string) {
|
||||||
try {
|
try {
|
||||||
const failure = await router.push(`/instance/${encodeURIComponent(instanceId)}/`)
|
const failure = await router.push(`/instance/${encodeURIComponent(instanceId)}/`)
|
||||||
@ -103,9 +116,14 @@ async function handleStartFresh() {
|
|||||||
await openCompleted(createdInstance.value)
|
await openCompleted(createdInstance.value)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (createdInstance.value) {
|
||||||
|
// A previous attempt already created the instance; retry in place.
|
||||||
await install()
|
await install()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
promptHostedGameDir()
|
||||||
|
return
|
||||||
|
}
|
||||||
showModal?.({
|
showModal?.({
|
||||||
skipSetupType: true,
|
skipSetupType: true,
|
||||||
initialMode: 'custom',
|
initialMode: 'custom',
|
||||||
@ -179,6 +197,10 @@ function handleImportExisting() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<HostedGameDirModal
|
||||||
|
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">
|
||||||
|
|||||||
@ -66,8 +66,10 @@ pub async fn hosted_default() -> Result<theseus::pack::hosted::Publication> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn hosted_create() -> Result<String> {
|
pub async fn hosted_create(
|
||||||
Ok(theseus::pack::hosted::create().await?)
|
game_dir_root: Option<String>,
|
||||||
|
) -> Result<String> {
|
||||||
|
Ok(theseus::pack::hosted::create(game_dir_root).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@ -232,6 +232,21 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Directory that contains the launcher executable. Used as the default
|
||||||
|
/// external game-directory root for the one-click StarLight install flow.
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_launcher_root_dir() -> api::Result<String> {
|
||||||
|
let exe = std::env::current_exe().map_err(|error| {
|
||||||
|
theseus::Error::from(theseus::ErrorKind::FSError(error.to_string()))
|
||||||
|
})?;
|
||||||
|
let dir = exe.parent().ok_or_else(|| {
|
||||||
|
theseus::Error::from(theseus::ErrorKind::FSError(
|
||||||
|
"Launcher executable has no parent directory".to_string(),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
Ok(dir.to_string_lossy().into_owned())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn get_update_channel(app: tauri::AppHandle) -> api::Result<String> {
|
fn get_update_channel(app: tauri::AppHandle) -> api::Result<String> {
|
||||||
let channel = read_update_channel_state(&app)?
|
let channel = read_update_channel_state(&app)?
|
||||||
@ -838,6 +853,7 @@ fn main() {
|
|||||||
.manage(PendingUpdateData::default())
|
.manage(PendingUpdateData::default())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
initialize_state,
|
initialize_state,
|
||||||
|
get_launcher_root_dir,
|
||||||
get_update_channel,
|
get_update_channel,
|
||||||
get_current_app_database_path,
|
get_current_app_database_path,
|
||||||
set_update_channel,
|
set_update_channel,
|
||||||
|
|||||||
@ -426,10 +426,26 @@ pub async fn default_publication() -> crate::Result<Publication> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create() -> crate::Result<String> {
|
pub async fn create(
|
||||||
|
game_dir_root: Option<String>,
|
||||||
|
) -> crate::Result<String> {
|
||||||
let publication = default_publication().await?;
|
let publication = default_publication().await?;
|
||||||
let runtime = &publication.manifest.runtime;
|
let runtime = &publication.manifest.runtime;
|
||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
|
// The pack's game files live in their own folder under the chosen root,
|
||||||
|
// e.g. `<root>/<pack name>`. Avoid a `versions/<name>` layout: that shape
|
||||||
|
// is reserved for externally linked launcher instances and would make the
|
||||||
|
// launcher expect a Minecraft version JSON beside the pack.
|
||||||
|
let game_dir_override = game_dir_root
|
||||||
|
.as_deref()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|root| !root.is_empty())
|
||||||
|
.map(|root| {
|
||||||
|
Path::new(root)
|
||||||
|
.join(&publication.manifest.name)
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
});
|
||||||
let instance = crate::state::create_instance(
|
let instance = crate::state::create_instance(
|
||||||
crate::state::CreateInstance {
|
crate::state::CreateInstance {
|
||||||
name: publication.manifest.name.clone(),
|
name: publication.manifest.name.clone(),
|
||||||
@ -440,7 +456,7 @@ pub async fn create() -> crate::Result<String> {
|
|||||||
icon_path: None,
|
icon_path: None,
|
||||||
link: crate::state::InstanceLink::Unmanaged,
|
link: crate::state::InstanceLink::Unmanaged,
|
||||||
symlink_target: None,
|
symlink_target: None,
|
||||||
game_dir_override: None,
|
game_dir_override,
|
||||||
},
|
},
|
||||||
&state,
|
&state,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user