diff --git a/apps/app-frontend/src/composables/browse/use-app-server-browse.ts b/apps/app-frontend/src/composables/browse/use-app-server-browse.ts index e96dd59595..14bdfecf05 100644 --- a/apps/app-frontend/src/composables/browse/use-app-server-browse.ts +++ b/apps/app-frontend/src/composables/browse/use-app-server-browse.ts @@ -1,5 +1,5 @@ import type { Labrinth } from '@modrinth/api-client' -import { CheckIcon, PlayIcon, PlusIcon, StopCircleIcon } from '@modrinth/assets' +import { CheckIcon, PlayIcon, PlusIcon, SpinnerIcon, StopCircleIcon } from '@modrinth/assets' import type { CardAction } from '@modrinth/ui' import { commonMessages, defineMessages, useDebugLogger, useVIntl } from '@modrinth/ui' import { useQueryClient } from '@tanstack/vue-query' @@ -75,6 +75,7 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) { const debugLog = useDebugLogger('BrowseServer') const serverPings = shallowRef>({}) const runningServerProjects = ref>({}) + const preparingServerProjects = ref([]) const lastServerHits = shallowRef([]) const contextMenuRef = ref(null) let serverPingsActive = true @@ -109,9 +110,16 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) { } async function handlePlayServerProject(projectId: string) { + if (preparingServerProjects.value.includes(projectId)) return + debugLog('handlePlayServerProject', projectId) - await options.playServerProject(projectId) - checkServerRunningStates(lastServerHits.value) + preparingServerProjects.value.push(projectId) + try { + await options.playServerProject(projectId) + checkServerRunningStates(lastServerHits.value) + } finally { + preparingServerProjects.value = preparingServerProjects.value.filter((id) => id !== projectId) + } } async function handleAddServerToInstance(project: Labrinth.Search.v3.ResultSearchProject) { @@ -247,13 +255,16 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) { }) } else { const isInstalling = options.installingServerProjects.value.includes(serverResult.project_id) + const isPreparing = preparingServerProjects.value.includes(serverResult.project_id) + const isBusy = isInstalling || isPreparing actions.push({ key: 'play', label: formatMessage( isInstalling ? commonMessages.installingLabel : commonMessages.playButton, ), - icon: PlayIcon, - disabled: isInstalling, + icon: isBusy ? SpinnerIcon : PlayIcon, + iconClass: isBusy ? 'animate-spin' : undefined, + disabled: isBusy, color: 'brand', type: 'outlined', onClick: () => handlePlayServerProject(serverResult.project_id), diff --git a/apps/app-frontend/src/pages/instance/components/page-header/index.vue b/apps/app-frontend/src/pages/instance/components/page-header/index.vue index 1107d8a22d..3cfd2832db 100644 --- a/apps/app-frontend/src/pages/instance/components/page-header/index.vue +++ b/apps/app-frontend/src/pages/instance/components/page-header/index.vue @@ -33,7 +33,7 @@ :ping="ping" :minecraft-server="minecraftServer" :show-instance-play-time="showInstancePlayTime" - :playtime-label="playtimeLabel" + :playtime-label="playtimeLabel ?? formatMessage(messages.neverPlayed)" /> void, - ) => void + ) => Promise >, ) => void setUpdateToPlayModal: ( @@ -84,7 +84,7 @@ export function createServerInstall(opts: { project: Labrinth.Projects.v3.Project, modpackVersionId: string | null, callback?: () => void, - ) => void + ) => Promise > | null = null let updateToPlayModalRef: ModalRef< (instance: GameInstance, activeVersionId: string | null, callback?: () => void) => void @@ -246,10 +246,10 @@ export function createServerInstall(opts: { project_id: contentProjectId, version_id: contentVersionId, title: project.title, + icon_url: project.icon_url, }, { name: project.title, - iconPath: project.icon_url ?? null, link: { type: 'server_project_modpack', server_project_id: serverProjectId, @@ -314,7 +314,7 @@ export function createServerInstall(opts: { return } if (isModpack && !instance) { - installToPlayModalRef?.show(projectV3, modpackVersionId, async () => { + await installToPlayModalRef?.show(projectV3, modpackVersionId, async () => { const newInstance = await findInstalledInstance(project.id) if (!newInstance) return showModpackInstallSuccess(newInstance, serverAddress) diff --git a/packages/app-lib/src/api/pack/install_from.rs b/packages/app-lib/src/api/pack/install_from.rs index d2b7af8702..13d3758cd6 100644 --- a/packages/app-lib/src/api/pack/install_from.rs +++ b/packages/app-lib/src/api/pack/install_from.rs @@ -309,7 +309,6 @@ pub(crate) async fn generate_pack_from_version_id_with_reporter( reporter: InstallProgressReporter, ) -> crate::Result { let state = State::get().await?; - let has_icon_url = icon_url.is_some(); let version = CachedEntry::get_version( &version_id, @@ -443,48 +442,28 @@ pub(crate) async fn generate_pack_from_version_id_with_reporter( .update(InstallPhaseId::ResolvingPack, None, details.clone()) .await?; - let project = CachedEntry::get_project( - &version.project_id, - None, - &state.pool, - &state.api_semaphore, - ) - .await? - .ok_or_else(|| { - crate::ErrorKind::InputError( - "Invalid project ID specified!".to_string(), - ) - })?; - - // Only fetch the pack icon when icon_url is provided (new profile). - // When installing to an existing profile (e.g. server projects), - // icon_url is None and we preserve the profile's existing icon. - let icon = if has_icon_url { - if let Some(icon_url) = project.icon_url { - let state = State::get().await?; - reporter - .set_context( - InstallErrorContext::new("download modpack icon") - .urls(vec![icon_url.clone()]) - .project_id(project_id.clone()) - .version_id(version_id.clone()) - .build(), - ) - .await?; - let icon_bytes = fetch( - &icon_url, - None, - None, - None, - &state.fetch_semaphore, - &state.pool, + // When no icon URL is supplied, preserve the instance's existing icon. + let icon = if let Some(icon_url) = icon_url { + reporter + .set_context( + InstallErrorContext::new("download modpack icon") + .urls(vec![icon_url.clone()]) + .project_id(project_id.clone()) + .version_id(version_id.clone()) + .build(), ) .await?; + let icon_bytes = fetch( + &icon_url, + None, + None, + None, + &state.fetch_semaphore, + &state.pool, + ) + .await?; - Some(crate::api::instance::cache_icon(icon_bytes, &state).await?) - } else { - None - } + Some(crate::api::instance::cache_icon(icon_bytes, &state).await?) } else { None }; @@ -587,7 +566,7 @@ pub async fn set_instance_information( } else { None }; - let link = match (&description.project_id, &description.version_id) { + let pack_link = match (&description.project_id, &description.version_id) { (Some(project_id), Some(version_id)) => { Some(InstanceLink::ModrinthModpack { project_id: project_id.clone(), @@ -605,13 +584,35 @@ pub async fn set_instance_information( } _ => None, }; + let existing_link = crate::api::instance::get(&instance_id) + .await? + .map(|metadata| metadata.link); + let link = match existing_link { + Some( + link @ (InstanceLink::ServerProject { .. } + | InstanceLink::ServerProjectModpack { .. } + | InstanceLink::ModrinthHosting { .. } + | InstanceLink::SharedInstance { .. }), + ) => Some(link), + _ => pack_link, + }; let source_kind = match &link { Some(InstanceLink::ModrinthModpack { .. }) => { Some(ContentSourceKind::ModrinthModpack) } + Some( + InstanceLink::ServerProject { .. } + | InstanceLink::ServerProjectModpack { .. }, + ) => Some(ContentSourceKind::ServerProject), + Some(InstanceLink::ModrinthHosting { .. }) => { + Some(ContentSourceKind::ModrinthHosting) + } Some(InstanceLink::ImportedModpack { .. }) => { Some(ContentSourceKind::ImportedModpack) } + Some(InstanceLink::SharedInstance { .. }) => { + Some(ContentSourceKind::SharedInstance) + } _ => None, }; crate::api::instance::edit( diff --git a/packages/ui/src/layouts/shared/browse-tab/layout.vue b/packages/ui/src/layouts/shared/browse-tab/layout.vue index b326b436b2..021e496843 100644 --- a/packages/ui/src/layouts/shared/browse-tab/layout.vue +++ b/packages/ui/src/layouts/shared/browse-tab/layout.vue @@ -130,7 +130,7 @@ function getProjectCardTags(result: Labrinth.Search.v3.ResultSearchProject, disp