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>
|
||||
Reference in New Issue
Block a user