Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions apps/app-frontend/src/composables/browse/use-app-server-browse.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -75,6 +75,7 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
const debugLog = useDebugLogger('BrowseServer')
const serverPings = shallowRef<Record<string, number | undefined>>({})
const runningServerProjects = ref<Record<string, string>>({})
const preparingServerProjects = ref<string[]>([])
const lastServerHits = shallowRef<Labrinth.Search.v3.ResultSearchProject[]>([])
const contextMenuRef = ref<ContextMenuHandle | null>(null)
let serverPingsActive = true
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
:ping="ping"
:minecraft-server="minecraftServer"
:show-instance-play-time="showInstancePlayTime"
:playtime-label="playtimeLabel"
:playtime-label="playtimeLabel ?? formatMessage(messages.neverPlayed)"
/>
<PageHeaderMetadata v-else>
<PageHeaderMetadataItem
Expand Down
8 changes: 4 additions & 4 deletions apps/app-frontend/src/providers/server-install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export interface ServerInstallContext {
project: Labrinth.Projects.v3.Project,
modpackVersionId: string | null,
callback?: () => void,
) => void
) => Promise<void>
>,
) => void
setUpdateToPlayModal: (
Expand Down Expand Up @@ -84,7 +84,7 @@ export function createServerInstall(opts: {
project: Labrinth.Projects.v3.Project,
modpackVersionId: string | null,
callback?: () => void,
) => void
) => Promise<void>
> | null = null
let updateToPlayModalRef: ModalRef<
(instance: GameInstance, activeVersionId: string | null, callback?: () => void) => void
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
83 changes: 42 additions & 41 deletions packages/app-lib/src/api/pack/install_from.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,6 @@ pub(crate) async fn generate_pack_from_version_id_with_reporter(
reporter: InstallProgressReporter,
) -> crate::Result<CreatePack> {
let state = State::get().await?;
let has_icon_url = icon_url.is_some();

let version = CachedEntry::get_version(
&version_id,
Expand Down Expand Up @@ -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
};
Expand Down Expand Up @@ -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(),
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/layouts/shared/browse-tab/layout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ function getProjectCardTags(result: Labrinth.Search.v3.ResultSearchProject, disp
<template v-if="ctx.installContext?.value && ctx.variant !== 'web'">
<div
ref="stickyInstallHeaderRef"
class="sticky top-0 z-20 -mx-6 -mt-6 rounded-tl-[--radius-xl] border-0 border-b border-solid bg-surface-1 px-3 py-4 border-surface-5"
class="sticky top-0 z-20 -mx-6 -mt-6 rounded-tl-[--radius-xl] border-0 border-b border-solid bg-surface-1 px-6 py-4 border-surface-5"
:class="[isInstallHeaderStuck ? 'border-t' : '']"
>
<BrowseInstallHeader />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import { EyeIcon, WrenchIcon } from '@modrinth/assets'
import { computed } from 'vue'

import AutoLink from '#ui/components/base/AutoLink.vue'
import Avatar from '#ui/components/base/Avatar.vue'
import { Button, IconButton } from '#ui/components/base/buttons'
import { defineMessages, useVIntl } from '#ui/composables/i18n'

Expand Down Expand Up @@ -67,9 +69,20 @@ const title = computed(() =>
<div
class="flex min-h-[94px] flex-col items-stretch justify-between gap-4 bg-surface-3 px-[18px] py-4 @[700px]:flex-row @[700px]:items-center"
>
<div class="flex min-w-0 flex-1 flex-col gap-1.5 pl-0.5">
<h2 class="m-0 text-2xl font-semibold leading-8 text-contrast">{{ title }}</h2>
<ManagedContentCardSummary :summary="data.summary" :installing="data.installing" />
<div class="flex min-w-0 flex-1 items-center gap-4 pl-0.5">
<AutoLink v-if="data.kind === 'modpack'" :to="data.manager.link" class="shrink-0">
<Avatar
:src="data.manager.iconUrl"
:alt="data.manager.name"
size="4rem"
:tint-by="data.manager.name"
no-shadow
/>
</AutoLink>
<div class="flex min-w-0 flex-1 flex-col gap-1.5">
<h2 class="m-0 text-2xl font-semibold leading-8 text-contrast">{{ title }}</h2>
<ManagedContentCardSummary :summary="data.summary" :installing="data.installing" />
</div>
</div>

<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ const formatDateTime = useFormatDateTime({
})

const messages = defineMessages({
managedBy: {
id: 'content.managed-card.managed-by',
defaultMessage: 'Managed by',
providedBy: {
id: 'content.managed-card.provided-by',
defaultMessage: 'Provided by',
},
server: {
id: 'content.managed-card.server-suffix',
Expand Down Expand Up @@ -110,7 +110,7 @@ const showAction = computed(
<LockIcon aria-hidden="true" class="size-5 shrink-0" />
<div class="flex min-w-0 flex-wrap items-center gap-[5px] @[700px]:flex-nowrap">
<span class="whitespace-nowrap">
{{ formatMessage(messages.managedBy) }}
{{ formatMessage(messages.providedBy) }}
</span>
<AutoLink
:to="data.manager.link"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ const messages = defineMessages({
defaultMessage: 'Loading managed content summary',
},
empty: {
id: 'content.managed-card.summary.empty',
defaultMessage: 'No managed content',
id: 'content.managed-card.summary.no-provided-content',
defaultMessage: 'No provided content',
},
installing: {
id: 'content.managed-card.summary.installing',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,6 @@ const messages = defineMessages({
id: 'content.metadata-filter.state',
defaultMessage: 'State',
},
updates: {
id: 'content.metadata-filter.updates',
defaultMessage: 'Updates',
},
warnings: {
id: 'content.metadata-filter.warnings',
defaultMessage: 'Warnings',
Expand All @@ -135,10 +131,6 @@ const messages = defineMessages({
id: 'content.metadata-filter.update.available',
defaultMessage: 'Update available',
},
upToDate: {
id: 'content.metadata-filter.update.up-to-date',
defaultMessage: 'Up to date',
},
clientRetained: {
id: 'content.metadata-filter.warning.client-retained',
defaultMessage: 'Client file retained',
Expand Down Expand Up @@ -237,15 +229,6 @@ export function useContentMetadataFilters(
: option('disabled', formatMessage(messages.disabled)),
],
},
{
key: 'updates',
label: formatMessage(messages.updates),
values: (item) => [
item.has_update
? option('available', formatMessage(messages.updateAvailable))
: option('current', formatMessage(messages.upToDate)),
],
},
{
key: 'warnings',
label: formatMessage(messages.warnings),
Expand All @@ -263,6 +246,13 @@ export function useContentMetadataFilters(
}
},
},
{
key: 'updates',
label: formatMessage(messages.updateAvailable),
direct: true,
values: (item) =>
item.has_update ? [option('available', formatMessage(messages.updateAvailable))] : [],
},
{
key: 'open_source',
label: formatMessage(messages.openSource),
Expand Down
7 changes: 2 additions & 5 deletions packages/ui/src/layouts/shared/content-tab/layout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1024,11 +1024,8 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
class="flex flex-wrap items-center gap-1.5 [&>div:last-of-type]:!h-[34px] [&>div:last-of-type]:!gap-1.5 [&_[data-button]]:!h-[34px]"
>
<div
class="h-6 w-px shrink-0 bg-surface-5"
:class="{
hidden: metadataFiltersWrapped,
'mr-0.5': !metadataFiltersWrapped,
}"
class="mr-0.5 h-6 w-px shrink-0 bg-surface-5"
:class="{ invisible: metadataFiltersWrapped }"
/>
<DropdownFilterBar
v-model="selectedMetadataFilters"
Expand Down
Loading
Loading