feat: smaller qa points

This commit is contained in:
Calum H. (IMB11)
2026-07-27 18:09:46 +01:00
parent 5b5468020d
commit f1f306d7ea
12 changed files with 109 additions and 38 deletions
@@ -15,12 +15,12 @@
</PageHeaderBadgeItem> </PageHeaderBadgeItem>
<PageHeaderBadgeItem <PageHeaderBadgeItem
v-else v-else
:icon="UnknownIcon"
:tooltip="sharedInstanceTooltip" :tooltip="sharedInstanceTooltip"
aria-label="Shared instance information" aria-label="Shared instance information"
class="!border-blue !bg-highlight-blue !text-blue" class="!border-blue !bg-highlight-blue !text-blue"
> >
Shared Shared
<UnknownIcon class="block size-4 shrink-0 text-current" aria-hidden="true" />
</PageHeaderBadgeItem> </PageHeaderBadgeItem>
</template> </template>
@@ -19,7 +19,7 @@
<template #cell-id="{ row }"> <template #cell-id="{ row }">
<CopyCode <CopyCode
:text="`${config.siteUrl}/share/${encodeURIComponent(row.id)}`" :text="`${config.siteUrl}/share/${encodeURIComponent(row.id)}`"
:display-text="row.id" :display-text="`/${row.id}`"
/> />
</template> </template>
<template #cell-uses="{ row }"> <template #cell-uses="{ row }">
@@ -172,11 +172,11 @@ const messages = defineMessages({
}, },
activeInvitesDescription: { activeInvitesDescription: {
id: 'instance.settings.sharing.active-invites.description', id: 'instance.settings.sharing.active-invites.description',
defaultMessage: 'Anyone with one of these invite codes can join while it remains active.', defaultMessage: 'Anyone with one of these invite links can join while it remains active.',
}, },
inviteCodeLabel: { inviteCodeLabel: {
id: 'instance.settings.sharing.active-invites.code', id: 'instance.settings.sharing.active-invites.code',
defaultMessage: 'Invite code', defaultMessage: 'Invite link',
}, },
usesLabel: { usesLabel: {
id: 'instance.settings.sharing.active-invites.uses', id: 'instance.settings.sharing.active-invites.uses',
@@ -70,7 +70,7 @@ const messages = defineMessages({
admonitionBody: { admonitionBody: {
id: 'instance.settings.sharing.revoke-invite.admonition-body', id: 'instance.settings.sharing.revoke-invite.admonition-body',
defaultMessage: defaultMessage:
'The invite code <monospace>{code}</monospace> will stop working immediately. People who already joined will keep access.', 'The invite link <monospace>{code}</monospace> will stop working immediately. People who already joined will keep access.',
}, },
revokeButton: { revokeButton: {
id: 'instance.settings.sharing.revoke-invite.confirm', id: 'instance.settings.sharing.revoke-invite.confirm',
@@ -70,10 +70,7 @@
:values="{ username: creator.username }" :values="{ username: creator.username }"
> >
<template #creator="{ children }"> <template #creator="{ children }">
<AutoLink <AutoLink :to="creatorProfileLink" class="font-medium text-contrast hover:underline">
:to="creatorProfileLink"
class="inline-flex items-center gap-1 align-middle font-medium text-contrast hover:underline"
>
<Avatar <Avatar
:src="creator.avatarUrl" :src="creator.avatarUrl"
:alt="creator.username" :alt="creator.username"
@@ -81,8 +78,9 @@
size="24px" size="24px"
circle circle
no-shadow no-shadow
class="mr-1 inline-block align-middle"
/> />
<component :is="() => children" /> <span><component :is="() => children" /></span>
</AutoLink> </AutoLink>
</template> </template>
</IntlFormatted> </IntlFormatted>
@@ -1,7 +1,7 @@
import type { Labrinth } from '@modrinth/api-client' import type { Labrinth } from '@modrinth/api-client'
import type { ContentItem } from '@modrinth/ui' import type { ContentItem } from '@modrinth/ui'
import { get_project_many, get_version, get_version_many } from '@/helpers/cache.js' import { get_project, get_project_many, get_version, get_version_many } from '@/helpers/cache.js'
import type { SharedInstanceInstallPreview } from '@/helpers/install' import type { SharedInstanceInstallPreview } from '@/helpers/install'
type VersionDependency = Labrinth.Versions.v2.Dependency & { version_id?: string } type VersionDependency = Labrinth.Versions.v2.Dependency & { version_id?: string }
@@ -20,7 +20,18 @@ export function useSharedInstancePreviewContent() {
async function modpackContentItems(preview: SharedInstanceInstallPreview) { async function modpackContentItems(preview: SharedInstanceInstallPreview) {
if (!preview.modpackVersionId) return [] if (!preview.modpackVersionId) return []
const version = await get_version(preview.modpackVersionId, 'must_revalidate') const version = await get_version(preview.modpackVersionId, 'must_revalidate')
return await contentItemsFromDependencies(version?.dependencies ?? []) if (!version) return []
const [project, contentItems] = await Promise.all([
get_project(version.project_id, 'must_revalidate'),
contentItemsFromDependencies(version.dependencies ?? []),
])
if (!project) return contentItems
return contentItems.map((item) => ({
...item,
source: { project },
}))
} }
async function contentItemsFromDependencies(dependencies: Labrinth.Versions.v2.Dependency[]) { async function contentItemsFromDependencies(dependencies: Labrinth.Versions.v2.Dependency[]) {
@@ -623,6 +623,9 @@
"app.modal.install-to-play.invite-warning": { "app.modal.install-to-play.invite-warning": {
"message": "This invite was created by another Modrinth user, not Modrinth. Only accept invites from people you trust." "message": "This invite was created by another Modrinth user, not Modrinth. Only accept invites from people you trust."
}, },
"app.modal.install-to-play.invite-warning-with-creator": {
"message": "This invite was created by <creator>{username}</creator>, not Modrinth. Only accept invites from people you trust."
},
"app.modal.install-to-play.mod-count": { "app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# mod} other {# mods}}" "message": "{count, plural, one {# mod} other {# mods}}"
}, },
@@ -1194,10 +1197,10 @@
"message": "Actions" "message": "Actions"
}, },
"instance.settings.sharing.active-invites.code": { "instance.settings.sharing.active-invites.code": {
"message": "Invite code" "message": "Invite link"
}, },
"instance.settings.sharing.active-invites.description": { "instance.settings.sharing.active-invites.description": {
"message": "Anyone with one of these invite codes can join while it remains active." "message": "Anyone with one of these invite links can join while it remains active."
}, },
"instance.settings.sharing.active-invites.empty": { "instance.settings.sharing.active-invites.empty": {
"message": "There are no active invites." "message": "There are no active invites."
@@ -1218,7 +1221,7 @@
"message": "Uses" "message": "Uses"
}, },
"instance.settings.sharing.revoke-invite.admonition-body": { "instance.settings.sharing.revoke-invite.admonition-body": {
"message": "The invite code <monospace>{code}</monospace> will stop working immediately. People who already joined will keep access." "message": "The invite link <monospace>{code}</monospace> will stop working immediately. People who already joined will keep access."
}, },
"instance.settings.sharing.revoke-invite.admonition-header": { "instance.settings.sharing.revoke-invite.admonition-header": {
"message": "This action cannot be undone" "message": "This action cannot be undone"
@@ -604,13 +604,18 @@ async function loadSharedInstanceVersionContent(
dependency.project_id ? [dependency.project_id] : [], dependency.project_id ? [dependency.project_id] : [],
) )
const projectIds = [ const projectIds = [
...new Set([...versions.map((version) => version.project_id), ...dependencyProjectIds]), ...new Set([
...versions.map((version) => version.project_id),
...dependencyProjectIds,
...(modpackVersion ? [modpackVersion.project_id] : []),
]),
] ]
const projects = projectIds.length const projects = projectIds.length
? await client.labrinth.projects_v2.getMultiple(projectIds) ? await client.labrinth.projects_v2.getMultiple(projectIds)
: [] : []
const versionsById = new Map(versions.map((version) => [version.id, version])) const versionsById = new Map(versions.map((version) => [version.id, version]))
const projectsById = new Map(projects.map((project) => [project.id, project])) const projectsById = new Map(projects.map((project) => [project.id, project]))
const modpackProject = modpackVersion ? projectsById.get(modpackVersion.project_id) : undefined
const directContent: ContentItem[] = [...new Set(directVersionIds)].flatMap((versionId) => { const directContent: ContentItem[] = [...new Set(directVersionIds)].flatMap((versionId) => {
const version = versionsById.get(versionId) const version = versionsById.get(versionId)
@@ -633,13 +638,19 @@ async function loadSharedInstanceVersionContent(
const fileName = const fileName =
primaryFile?.filename ?? dependency.file_name ?? project?.title ?? version?.name ?? 'Unknown' primaryFile?.filename ?? dependency.file_name ?? project?.title ?? version?.name ?? 'Unknown'
return sharedInstanceContentItem( const item = sharedInstanceContentItem(
version, version,
project, project,
fileName, fileName,
dependency.project_id ?? fileName, dependency.project_id ?? fileName,
!project && !version, !project && !version,
) )
return modpackProject
? {
...item,
source: { project: modpackProject },
}
: item
}) })
const externalContent: ContentItem[] = instanceVersion.external_files.map((file, index) => ({ const externalContent: ContentItem[] = instanceVersion.external_files.map((file, index) => ({
+17 -18
View File
@@ -224,12 +224,11 @@ fn normalize_svg_from_path(icon_path: &Path) -> crate::Result<Bytes> {
)) ))
})?; })?;
if metadata.len() > INSTANCE_ICON_MAX_SVG_SOURCE_BYTES { if metadata.len() > INSTANCE_ICON_MAX_SVG_SOURCE_BYTES {
return Err(crate::ErrorKind::InputError(format!( return Err(crate::ErrorKind::InputError(format!(
"SVG instance icons cannot exceed {} bytes before normalization", "SVG instance icons cannot exceed {INSTANCE_ICON_MAX_SVG_SOURCE_BYTES} bytes before normalization"
INSTANCE_ICON_MAX_SVG_SOURCE_BYTES ))
)) .into());
.into()); }
}
let bytes = std::fs::read(icon_path).map_err(|error| { let bytes = std::fs::read(icon_path).map_err(|error| {
crate::ErrorKind::InputError(format!( crate::ErrorKind::InputError(format!(
@@ -245,15 +244,16 @@ fn normalize_svg(
resources_dir: Option<&Path>, resources_dir: Option<&Path>,
) -> crate::Result<Bytes> { ) -> crate::Result<Bytes> {
if bytes.len() as u64 > INSTANCE_ICON_MAX_SVG_SOURCE_BYTES { if bytes.len() as u64 > INSTANCE_ICON_MAX_SVG_SOURCE_BYTES {
return Err(crate::ErrorKind::InputError(format!( return Err(crate::ErrorKind::InputError(format!(
"SVG instance icons cannot exceed {} bytes before normalization", "SVG instance icons cannot exceed {INSTANCE_ICON_MAX_SVG_SOURCE_BYTES} bytes before normalization"
INSTANCE_ICON_MAX_SVG_SOURCE_BYTES ))
)) .into());
.into()); }
}
let mut options = resvg::usvg::Options::default(); let mut options = resvg::usvg::Options {
options.resources_dir = resources_dir.map(Path::to_path_buf); resources_dir: resources_dir.map(Path::to_path_buf),
..Default::default()
};
options.fontdb_mut().load_system_fonts(); options.fontdb_mut().load_system_fonts();
let tree = let tree =
resvg::usvg::Tree::from_data(bytes, &options).map_err(|error| { resvg::usvg::Tree::from_data(bytes, &options).map_err(|error| {
@@ -308,9 +308,8 @@ fn has_svg_extension(path: &Path) -> bool {
} }
fn icon_too_large_error() -> crate::Error { fn icon_too_large_error() -> crate::Error {
crate::ErrorKind::InputError(format!( crate::ErrorKind::InputError(format!(
"Instance icons cannot exceed {} bytes", "Instance icons cannot exceed {INSTANCE_ICON_MAX_BYTES} bytes"
INSTANCE_ICON_MAX_BYTES ))
))
.into() .into()
} }
@@ -29,6 +29,7 @@ import type {
ContentCardProject, ContentCardProject,
ContentCardVersion, ContentCardVersion,
ContentOwner, ContentOwner,
ContentSource,
} from '../types' } from '../types'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
@@ -46,6 +47,7 @@ interface Props {
version?: ContentCardVersion version?: ContentCardVersion
versionLink?: string | RouteLocationRaw versionLink?: string | RouteLocationRaw
owner?: ContentOwner owner?: ContentOwner
source?: ContentSource
enabled?: boolean enabled?: boolean
installing?: boolean installing?: boolean
hasUpdate?: boolean hasUpdate?: boolean
@@ -68,6 +70,7 @@ const props = withDefaults(defineProps<Props>(), {
version: undefined, version: undefined,
versionLink: undefined, versionLink: undefined,
owner: undefined, owner: undefined,
source: undefined,
enabled: undefined, enabled: undefined,
installing: false, installing: false,
hasUpdate: false, hasUpdate: false,
@@ -196,8 +199,32 @@ const deleteHovered = ref(false)
</div> </div>
<div class="flex min-w-0 items-center gap-1"> <div class="flex min-w-0 items-center gap-1">
<template v-if="source">
<AutoLink
:target="
typeof source.link === 'string' && source.link.startsWith('http')
? '_blank'
: undefined
"
:to="source.link"
class="flex min-w-0 items-center gap-1 !decoration-secondary"
:class="{ 'hover:underline': source.link }"
>
<Avatar
:src="source.project.icon_url"
:alt="source.project.title"
:tint-by="source.project.id"
size="1.25rem"
no-shadow
class="shrink-0 rounded-md"
/>
<span class="truncate text-sm leading-5 text-secondary">
{{ source.project.title }}
</span>
</AutoLink>
</template>
<AutoLink <AutoLink
v-if="owner" v-else-if="owner"
:target=" :target="
typeof owner.link === 'string' && owner.link.startsWith('http') typeof owner.link === 'string' && owner.link.startsWith('http')
? '_blank' ? '_blank'
@@ -264,6 +264,7 @@ function handleSort(column: ContentCardTableSortColumn) {
:version="item.version" :version="item.version"
:version-link="item.versionLink" :version-link="item.versionLink"
:owner="item.owner" :owner="item.owner"
:source="item.source"
:enabled="item.enabled" :enabled="item.enabled"
:installing="item.installing" :installing="item.installing"
:has-update="item.hasUpdate" :has-update="item.hasUpdate"
@@ -327,6 +328,7 @@ function handleSort(column: ContentCardTableSortColumn) {
:version="item.version" :version="item.version"
:version-link="item.versionLink" :version-link="item.versionLink"
:owner="item.owner" :owner="item.owner"
:source="item.source"
:enabled="item.enabled" :enabled="item.enabled"
:installing="item.installing" :installing="item.installing"
:has-update="item.hasUpdate" :has-update="item.hasUpdate"
@@ -20,6 +20,7 @@ import type { Option as OverflowMenuOption } from '#ui/components/base/OverflowM
import StyledInput from '#ui/components/base/StyledInput.vue' import StyledInput from '#ui/components/base/StyledInput.vue'
import NewModal from '#ui/components/modal/NewModal.vue' import NewModal from '#ui/components/modal/NewModal.vue'
import { defineMessages, useVIntl } from '#ui/composables/i18n' import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { injectPageContext } from '#ui/providers/page-context'
import { import {
commonMessages, commonMessages,
commonProjectTypeCategoryMessages, commonProjectTypeCategoryMessages,
@@ -28,11 +29,12 @@ import {
} from '#ui/utils/common-messages' } from '#ui/utils/common-messages'
import { getClientWarningType, isClientOnlyEnvironment } from '../../composables/content-filtering' import { getClientWarningType, isClientOnlyEnvironment } from '../../composables/content-filtering'
import type { ContentCardTableItem, ContentItem } from '../../types' import type { ContentCardProject, ContentCardTableItem, ContentItem } from '../../types'
import ContentCardTable from '../ContentCardTable.vue' import ContentCardTable from '../ContentCardTable.vue'
import ContentSelectionBar from '../ContentSelectionBar.vue' import ContentSelectionBar from '../ContentSelectionBar.vue'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const pageContext = injectPageContext(null)
interface Props { interface Props {
header?: string header?: string
@@ -263,6 +265,12 @@ const tableItems = computed<ContentCardTableItem[]>(() =>
link: `https://modrinth.com/${item.owner.type}/${item.owner.id}`, link: `https://modrinth.com/${item.owner.type}/${item.owner.id}`,
} }
: undefined, : undefined,
source: item.source
? {
...item.source,
link: item.source.link ?? sourceProjectLink(item.source.project),
}
: undefined,
...(props.enableToggle ? { enabled: item.enabled } : {}), ...(props.enableToggle ? { enabled: item.enabled } : {}),
installing: item.installing === true, installing: item.installing === true,
toggleDisabled: props.actionDisabled, toggleDisabled: props.actionDisabled,
@@ -290,7 +298,7 @@ const tableItems = computed<ContentCardTableItem[]>(() =>
})), })),
) )
const externalItemIds = computed( const externalItemIds = computed(
() => new Set(items.value.filter((item) => item.external).map((item) => item.id)), () => new Set(items.value.filter((item) => item.external && !item.source).map((item) => item.id)),
) )
const externalSlicerUrls = computed(() => { const externalSlicerUrls = computed(() => {
const urls: Record<string, string> = {} const urls: Record<string, string> = {}
@@ -332,6 +340,12 @@ function itemDisplayName(item: ContentItem) {
return item.project?.title ?? item.file_name return item.project?.title ?? item.file_name
} }
function sourceProjectLink(project: ContentCardProject) {
const projectId = project.slug ?? project.id
const url = `https://modrinth.com/modpack/${encodeURIComponent(projectId)}`
return pageContext ? () => pageContext.openExternalUrl(url) : url
}
function handleEnabledChange(id: string, value: boolean) { function handleEnabledChange(id: string, value: boolean) {
if (props.actionDisabled) return if (props.actionDisabled) return
const item = items.value.find((item) => item.id === id) const item = items.value.find((item) => item.id === id)
@@ -21,6 +21,11 @@ export interface ContentOwner {
link?: string | RouteLocationRaw | (() => void) link?: string | RouteLocationRaw | (() => void)
} }
export interface ContentSource {
project: ContentCardProject
link?: string | RouteLocationRaw | (() => void)
}
export type ClientWarningType = 'retained' | 'depends' | 'environment' export type ClientWarningType = 'retained' | 'depends' | 'environment'
export type ContentSourceKind = export type ContentSourceKind =
@@ -44,6 +49,7 @@ export interface ContentCardTableItem {
version?: ContentCardVersion version?: ContentCardVersion
versionLink?: string | RouteLocationRaw versionLink?: string | RouteLocationRaw
owner?: ContentOwner owner?: ContentOwner
source?: ContentSource
enabled?: boolean enabled?: boolean
disabled?: boolean disabled?: boolean
disabledTooltip?: string | null disabledTooltip?: string | null