From 5006f432dcefd5797d54de780d353eff64d1ea2c Mon Sep 17 00:00:00 2001 From: Xiao-no-love <140927721+Xiao-no-love@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:04:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=80=E9=94=AE=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=87=AA=E9=80=89=E6=B8=B8=E6=88=8F=E7=9B=AE?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StarLight 实例一键安装此前固定落到启动器默认目录,无法选择游戏数据位置。 现复用创建流程的外部游戏目录能力:点击一键安装后弹出目录选择框, 只允许修改游戏目录,其余参数仍由服务器整合包决定。 - hosted::create 接受 game_dir_root,拼为 /<包名> 作为 game_dir_override (刻意避开 versions/ 布局,该结构被外部直链实例检测占用) - 新增顶层命令 get_launcher_root_dir,作为弹窗默认值(可执行文件所在目录) - 新增 HostedGameDirModal 弹窗:只读路径框 + 浏览 + 默认位置 + 安装预览 - 补齐 en-US / zh-CN / zh-TW 三语文案 hosted_create 增加 game_dir_root 参数,前端 hostedCreate/install 同步透传。 --- .../instance/HostedGameDirModal.vue | 161 ++++++++++++++++++ .../src/composables/useHostedCreation.ts | 4 +- apps/app-frontend/src/helpers/hosted-packs.ts | 5 +- .../app-frontend/src/locales/en-US/index.json | 8 + .../app-frontend/src/locales/zh-CN/index.json | 8 + .../app-frontend/src/locales/zh-TW/index.json | 8 + apps/app-frontend/src/pages/Create.vue | 24 ++- apps/app/src/api/install.rs | 6 +- apps/app/src/main.rs | 16 ++ packages/app-lib/src/api/pack/hosted.rs | 20 ++- 10 files changed, 252 insertions(+), 8 deletions(-) create mode 100644 apps/app-frontend/src/components/instance/HostedGameDirModal.vue diff --git a/apps/app-frontend/src/components/instance/HostedGameDirModal.vue b/apps/app-frontend/src/components/instance/HostedGameDirModal.vue new file mode 100644 index 0000000..2c9586a --- /dev/null +++ b/apps/app-frontend/src/components/instance/HostedGameDirModal.vue @@ -0,0 +1,161 @@ + + + \ No newline at end of file diff --git a/apps/app-frontend/src/composables/useHostedCreation.ts b/apps/app-frontend/src/composables/useHostedCreation.ts index 2798f03..df2534e 100644 --- a/apps/app-frontend/src/composables/useHostedCreation.ts +++ b/apps/app-frontend/src/composables/useHostedCreation.ts @@ -23,7 +23,7 @@ export function useHostedCreation() { completed.value = false } } - async function install() { + async function install(gameDirRoot?: string | null) { if (installing.value) return installing.value = true installError.value = '' @@ -40,7 +40,7 @@ export function useHostedCreation() { } } if (completed.value) return createdInstance.value - createdInstance.value ??= await hostedCreate() + createdInstance.value ??= await hostedCreate(gameDirRoot) const instanceId = createdInstance.value await hostedSync(instanceId) if (attempt !== generation) return diff --git a/apps/app-frontend/src/helpers/hosted-packs.ts b/apps/app-frontend/src/helpers/hosted-packs.ts index ebc40c7..5589cd4 100644 --- a/apps/app-frontend/src/helpers/hosted-packs.ts +++ b/apps/app-frontend/src/helpers/hosted-packs.ts @@ -99,7 +99,10 @@ export interface HostedSyncResult { } export const hostedDefault = () => invokeWithSession('plugin:install|hosted_default') -export const hostedCreate = () => invokeWithSession('plugin:install|hosted_create') +export const hostedCreate = (gameDirRoot?: string | null) => + invokeWithSession('plugin:install|hosted_create', { + gameDirRoot: gameDirRoot ?? null, + }) export const hostedBinding = (instanceId: string) => invokeHosted('plugin:install|hosted_binding', { instanceId }) export const hostedSync = (instanceId: string) => { diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json index adf8c8d..a9b6178 100644 --- a/apps/app-frontend/src/locales/en-US/index.json +++ b/apps/app-frontend/src/locales/en-US/index.json @@ -29,6 +29,14 @@ "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-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.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" }, diff --git a/apps/app-frontend/src/locales/zh-CN/index.json b/apps/app-frontend/src/locales/zh-CN/index.json index a7125cfd..d5797f1 100644 --- a/apps/app-frontend/src/locales/zh-CN/index.json +++ b/apps/app-frontend/src/locales/zh-CN/index.json @@ -29,6 +29,14 @@ "app.hosted-packs.auto-installing": { "message": "正在下载并安装服务器整合包…" }, "app.hosted-packs.auto-description": { "message": "从StarLight服务器获取整合包并自动安装,可一键游玩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.description": { "message": "StarLight 实例自动安装管理员指定的整合包和版本,每次启动先检查并完成更新,需要登录 StarLight 账号。本地实例可自选版本和整合包,适合第三方服务器与单人游玩。" }, "app.instance-mode.title": { "message": "实例类型" }, diff --git a/apps/app-frontend/src/locales/zh-TW/index.json b/apps/app-frontend/src/locales/zh-TW/index.json index 9586816..63ec9d3 100644 --- a/apps/app-frontend/src/locales/zh-TW/index.json +++ b/apps/app-frontend/src/locales/zh-TW/index.json @@ -12,6 +12,14 @@ "app.hosted-packs.auto-installing": { "message": "正在下載並安裝伺服器整合包…" }, "app.hosted-packs.auto-description": { "message": "從StarLight伺服器取得整合包並自動安裝,可一鍵遊玩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.description": { "message": "StarLight 實例自動安裝管理員指定的整合包和版本,每次啟動先檢查並完成更新,需要登入 StarLight 帳號。本地實例可自行選擇版本和整合包,適合第三方伺服器與單人遊玩。" }, "app.instance-mode.title": { "message": "實例類型" }, diff --git a/apps/app-frontend/src/pages/Create.vue b/apps/app-frontend/src/pages/Create.vue index 0cce8f6..4a0c03e 100644 --- a/apps/app-frontend/src/pages/Create.vue +++ b/apps/app-frontend/src/pages/Create.vue @@ -4,6 +4,7 @@ import { BigOptionButton, Button, defineMessages, useVIntl } from '@modrinth/ui' import { inject, ref, watch } from 'vue' import { useRouter } from 'vue-router' import InstanceModeOptions from '@/components/instance/InstanceModeOptions.vue' +import HostedGameDirModal from '@/components/instance/HostedGameDirModal.vue' import HostedPackProgress from '@/components/instance/HostedPackProgress.vue' import type { InstanceMode } from '@/helpers/hosted-packs' import { useHostedCreation } from '@/composables/useHostedCreation' @@ -15,6 +16,18 @@ const { installing, installError, createdInstance, completed, acknowledge, insta const instanceMode = ref( installing.value || createdInstance.value ? 'starlight' : 'local', ) + +const hostedGameDirModal = ref>() + +// One-click StarLight installs reuse the same external game-directory choice +// as the custom creation flow: the pack gets its own folder under ``. +function promptHostedGameDir() { + hostedGameDirModal.value?.show() +} + +async function installHostedWithGameDir(gameDirRoot: string) { + await install(gameDirRoot) +} async function openCompleted(instanceId: string) { try { const failure = await router.push(`/instance/${encodeURIComponent(instanceId)}/`) @@ -103,7 +116,12 @@ async function handleStartFresh() { await openCompleted(createdInstance.value) return } - await install() + if (createdInstance.value) { + // A previous attempt already created the instance; retry in place. + await install() + return + } + promptHostedGameDir() return } showModal?.({ @@ -179,6 +197,10 @@ function handleImportExisting() { /> +

diff --git a/apps/app/src/api/install.rs b/apps/app/src/api/install.rs index 56a97fb..919a30d 100644 --- a/apps/app/src/api/install.rs +++ b/apps/app/src/api/install.rs @@ -66,8 +66,10 @@ pub async fn hosted_default() -> Result { } #[tauri::command] -pub async fn hosted_create() -> Result { - Ok(theseus::pack::hosted::create().await?) +pub async fn hosted_create( + game_dir_root: Option, +) -> Result { + Ok(theseus::pack::hosted::create(game_dir_root).await?) } #[tauri::command] diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs index 6c0254a..9d11770 100644 --- a/apps/app/src/main.rs +++ b/apps/app/src/main.rs @@ -232,6 +232,21 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> { 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 { + 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] fn get_update_channel(app: tauri::AppHandle) -> api::Result { let channel = read_update_channel_state(&app)? @@ -838,6 +853,7 @@ fn main() { .manage(PendingUpdateData::default()) .invoke_handler(tauri::generate_handler![ initialize_state, + get_launcher_root_dir, get_update_channel, get_current_app_database_path, set_update_channel, diff --git a/packages/app-lib/src/api/pack/hosted.rs b/packages/app-lib/src/api/pack/hosted.rs index c03d019..ad1cb13 100644 --- a/packages/app-lib/src/api/pack/hosted.rs +++ b/packages/app-lib/src/api/pack/hosted.rs @@ -426,10 +426,26 @@ pub async fn default_publication() -> crate::Result { }) } -pub async fn create() -> crate::Result { +pub async fn create( + game_dir_root: Option, +) -> crate::Result { let publication = default_publication().await?; let runtime = &publication.manifest.runtime; let state = State::get().await?; + // The pack's game files live in their own folder under the chosen root, + // e.g. `/`. Avoid a `versions/` 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( crate::state::CreateInstance { name: publication.manifest.name.clone(), @@ -440,7 +456,7 @@ pub async fn create() -> crate::Result { icon_path: None, link: crate::state::InstanceLink::Unmanaged, symlink_target: None, - game_dir_override: None, + game_dir_override, }, &state, )