mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 21:26:40 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c63558b678 | ||
|
|
3145759e88 | ||
|
|
b1f2017b07 | ||
|
|
4b3e9e785b | ||
|
|
f7ca96a1a4 | ||
|
|
ad4912d64d | ||
|
|
0002ae2479 | ||
|
|
e4568330a7 | ||
|
|
b0108e0624 | ||
|
|
ac63abadb7 |
@@ -180,7 +180,7 @@
|
||||
"message": "Discover servers"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Hide already added servers"
|
||||
"message": "Hide servers already added"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Modpacks"
|
||||
@@ -410,12 +410,27 @@
|
||||
"app.project.install-button.already-installed": {
|
||||
"message": "This project is already installed"
|
||||
},
|
||||
"app.project.install-button.switch-version": {
|
||||
"message": "Switch version"
|
||||
},
|
||||
"app.project.install-context.back-to-browse": {
|
||||
"message": "Back to discover"
|
||||
},
|
||||
"app.project.install-context.install-content-to-instance": {
|
||||
"message": "Install content to instance"
|
||||
},
|
||||
"app.project.version.all-versions": {
|
||||
"message": "All versions"
|
||||
},
|
||||
"app.project.version.download-in-browser": {
|
||||
"message": "Download in browser"
|
||||
},
|
||||
"app.project.version.installing": {
|
||||
"message": "Installing"
|
||||
},
|
||||
"app.project.versions.already-installed": {
|
||||
"message": "Already installed"
|
||||
},
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Developer mode enabled."
|
||||
},
|
||||
|
||||
@@ -403,7 +403,7 @@ const messages = defineMessages({
|
||||
},
|
||||
hideAddedServers: {
|
||||
id: 'app.browse.hide-added-servers',
|
||||
defaultMessage: 'Hide already added servers',
|
||||
defaultMessage: 'Hide servers already added',
|
||||
},
|
||||
installingToServer: {
|
||||
id: 'app.browse.server.installing',
|
||||
|
||||
@@ -1351,7 +1351,9 @@ provideContentManager({
|
||||
title: item.file_name.replace('.disabled', ''),
|
||||
icon_url: null,
|
||||
},
|
||||
projectLink: item.project?.id ? { path: `/project/${item.project.id}` } : undefined,
|
||||
projectLink: item.project?.id
|
||||
? { path: `/project/${item.project.id}`, query: { i: props.instance.id } }
|
||||
: undefined,
|
||||
version: item.version ?? {
|
||||
id: item.file_name,
|
||||
version_number: formatMessage(commonMessages.unknownLabel),
|
||||
@@ -1361,6 +1363,7 @@ provideContentManager({
|
||||
item.project?.id && item.version?.id
|
||||
? {
|
||||
path: `/project/${item.project.id}/version/${item.version.id}`,
|
||||
query: { i: props.instance.id },
|
||||
}
|
||||
: undefined,
|
||||
owner: item.owner
|
||||
|
||||
@@ -116,13 +116,27 @@
|
||||
aria-label="More options"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #open-in-browser> <ExternalIcon /> Open in browser </template>
|
||||
<template #open-in-browser>
|
||||
<ExternalIcon /> {{ formatMessage(commonMessages.openInBrowserButton) }}
|
||||
</template>
|
||||
<template #report> <ReportIcon /> Report </template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<template v-else #actions>
|
||||
<ButtonStyled size="large" color="brand">
|
||||
<ButtonStyled v-if="showSwitchVersion && onVersionsPage" size="large">
|
||||
<button v-tooltip="installButtonTooltip" disabled>
|
||||
<CheckIcon />
|
||||
{{ formatMessage(commonMessages.installedLabel) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else-if="showSwitchVersion" size="large">
|
||||
<button @click="goToVersions">
|
||||
<SwapIcon />
|
||||
{{ formatMessage(messages.switchVersion) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else size="large" color="brand">
|
||||
<button
|
||||
v-tooltip="installButtonTooltip"
|
||||
:disabled="installButtonDisabled"
|
||||
@@ -171,7 +185,9 @@
|
||||
aria-label="More options"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #open-in-browser> <ExternalIcon /> Open in browser </template>
|
||||
<template #open-in-browser>
|
||||
<ExternalIcon /> {{ formatMessage(commonMessages.openInBrowserButton) }}
|
||||
</template>
|
||||
<template #follow> <HeartIcon /> Follow </template>
|
||||
<template #save> <BookmarkIcon /> Save </template>
|
||||
<template #report> <ReportIcon /> Report </template>
|
||||
@@ -293,6 +309,7 @@ import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { computed, onUnmounted, ref, shallowRef, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { SwapIcon } from '@/assets/icons/index.js'
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
|
||||
import {
|
||||
@@ -343,6 +360,10 @@ const messages = defineMessages({
|
||||
id: 'app.project.install-button.already-installed',
|
||||
defaultMessage: 'This project is already installed',
|
||||
},
|
||||
switchVersion: {
|
||||
id: 'app.project.install-button.switch-version',
|
||||
defaultMessage: 'Switch version',
|
||||
},
|
||||
})
|
||||
|
||||
const { installingServerProjects, playServerProject, showAddServerToInstanceModal } =
|
||||
@@ -514,6 +535,13 @@ const installButtonTooltip = computed(() => {
|
||||
return null
|
||||
})
|
||||
|
||||
const showSwitchVersion = computed(() => !!instance.value && installed.value)
|
||||
const onVersionsPage = computed(() => route.name === 'Versions')
|
||||
|
||||
function goToVersions() {
|
||||
router.push(versionsHref.value)
|
||||
}
|
||||
|
||||
const [allLoaders, allGameVersions] = await Promise.all([
|
||||
get_loaders().catch(handleError).then(ref),
|
||||
get_game_versions().catch(handleError).then(ref),
|
||||
|
||||
@@ -1,259 +1,152 @@
|
||||
<template>
|
||||
<div>
|
||||
<Card>
|
||||
<Breadcrumbs
|
||||
:current-title="version.name"
|
||||
:link-stack="[
|
||||
{
|
||||
href: buildProjectHref(`/project/${route.params.id}/versions`),
|
||||
label: 'Versions',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<div class="version-title">
|
||||
<h2>{{ version.name }}</h2>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<div class="flex flex-col">
|
||||
<router-link
|
||||
class="mb-4 flex w-fit items-center gap-2 rounded-lg px-2 py-0.5 pl-0 text-link"
|
||||
:to="buildProjectHref(`/project/${route.params.id}/versions`)"
|
||||
>
|
||||
<ChevronLeftIcon class="shrink-0" /> {{ formatMessage(messages.allVersions) }}
|
||||
</router-link>
|
||||
<VersionPage
|
||||
v-if="version"
|
||||
:version="version"
|
||||
:enrichment="enrichment"
|
||||
:enrichment-loading="enrichmentLoading"
|
||||
:members="members"
|
||||
:dependency-link-creator="createDependencyLink"
|
||||
>
|
||||
<template #headerActions>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
:disabled="installing || (installed && installedVersion === version.id)"
|
||||
@click="() => install(version.id)"
|
||||
@click="() => version && install(version.id)"
|
||||
>
|
||||
<DownloadIcon v-if="!installed" />
|
||||
<SwapIcon v-else-if="installedVersion !== version.id" />
|
||||
<CheckIcon v-else />
|
||||
{{
|
||||
installing
|
||||
? 'Installing...'
|
||||
? formatMessage(messages.installing)
|
||||
: installed && installedVersion === version.id
|
||||
? 'Installed'
|
||||
: 'Install'
|
||||
? formatMessage(commonMessages.installedLabel)
|
||||
: installed
|
||||
? formatMessage(commonMessages.switchToVersionButton)
|
||||
: formatMessage(commonMessages.installButton)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button>
|
||||
<ReportIcon />
|
||||
Report
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a
|
||||
:href="`https://modrinth.com/mod/${route.params.id}/version/${route.params.version}`"
|
||||
rel="external"
|
||||
<ButtonStyled type="outlined" circular>
|
||||
<OverflowMenu
|
||||
v-tooltip="formatMessage(commonMessages.moreOptionsButton)"
|
||||
:options="[
|
||||
{
|
||||
id: 'open-in-browser',
|
||||
link: `https://modrinth.com/${project.project_type}/${project.slug}/version/${version.id}`,
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
color: 'red',
|
||||
hoverFilled: true,
|
||||
link: `https://modrinth.com/report?item=version&itemID=${version.id}`,
|
||||
external: true,
|
||||
},
|
||||
]"
|
||||
aria-label="More options"
|
||||
>
|
||||
Modrinth website
|
||||
<ExternalIcon />
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #open-in-browser>
|
||||
<ExternalIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.openInBrowserButton) }}
|
||||
</template>
|
||||
<template #report>
|
||||
<ReportIcon aria-hidden="true" /> {{ formatMessage(commonMessages.reportButton) }}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<template #supplementaryResourceActions="{ file }">
|
||||
<ButtonStyled>
|
||||
<a :href="file.url" :download="file.filename" target="_blank">
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.downloadInBrowser) }}
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</Card>
|
||||
<div class="version-container">
|
||||
<div class="description-cards">
|
||||
<Card>
|
||||
<h3 class="card-title">Changelog</h3>
|
||||
<div class="markdown-body" v-html="renderString(version.changelog ?? '')" />
|
||||
</Card>
|
||||
<Card>
|
||||
<h3 class="card-title">Files</h3>
|
||||
<Card
|
||||
v-for="file in version.files"
|
||||
:key="file.id"
|
||||
:class="{ primary: file.primary }"
|
||||
class="file"
|
||||
>
|
||||
<span class="label">
|
||||
<FileIcon />
|
||||
<span>
|
||||
<span class="title">
|
||||
{{ file.filename }}
|
||||
</span>
|
||||
({{ formatBytes(file.size) }})
|
||||
<span v-if="file.primary" class="primary-label"> Primary </span>
|
||||
</span>
|
||||
</span>
|
||||
<ButtonStyled v-if="project.project_type !== 'modpack' || file.primary" color="brand">
|
||||
<button class="download" :disabled="installed" @click="() => install(version.id)">
|
||||
<DownloadIcon v-if="!installed" />
|
||||
<CheckIcon v-else />
|
||||
{{ installed ? 'Installed' : 'Install' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</Card>
|
||||
</Card>
|
||||
<Card v-if="displayDependencies.length > 0">
|
||||
<h2>Dependencies</h2>
|
||||
<div v-for="dependency in displayDependencies" :key="dependency.title">
|
||||
<router-link v-if="dependency.link" class="btn dependency" :to="dependency.link">
|
||||
<Avatar size="sm" :src="dependency.icon" />
|
||||
<div>
|
||||
<span class="title"> {{ dependency.title }} </span> <br />
|
||||
<span> {{ dependency.subtitle }} </span>
|
||||
</div>
|
||||
</router-link>
|
||||
<div v-else class="dependency disabled" disabled="">
|
||||
<Avatar size="sm" :src="dependency.icon" />
|
||||
<div class="text">
|
||||
<div class="title">{{ dependency.title }}</div>
|
||||
<div>{{ dependency.subtitle }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<Card class="metadata-card">
|
||||
<h3 class="card-title">Metadata</h3>
|
||||
<div class="metadata">
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Release Channel</span>
|
||||
<span class="metadata-value"
|
||||
><Badge
|
||||
:color="releaseColor(version.version_type)"
|
||||
:type="
|
||||
version.version_type.charAt(0).toUpperCase() + version.version_type.slice(1)
|
||||
"
|
||||
/></span>
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Version Number</span>
|
||||
<span class="metadata-value">{{ version.version_number }}</span>
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Loaders</span>
|
||||
<span class="metadata-value">{{
|
||||
version.loaders
|
||||
.map((loader) => loader.charAt(0).toUpperCase() + loader.slice(1))
|
||||
.join(', ')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Game Versions</span>
|
||||
<span class="metadata-value"> {{ version.game_versions.join(', ') }} </span>
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Downloads</span>
|
||||
<span class="metadata-value">{{ version.downloads }}</span>
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Publication Date</span>
|
||||
<span class="metadata-value">
|
||||
{{ formatDateTime(version.date_published) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="author" class="metadata-item">
|
||||
<span class="metadata-label">Author</span>
|
||||
<a
|
||||
:href="`https://modrinth.com/user/${author.user.username}`"
|
||||
rel="external"
|
||||
class="metadata-value btn author"
|
||||
>
|
||||
<Avatar size="sm" :src="author.user.avatar_url" circle />
|
||||
<span>
|
||||
<strong>
|
||||
{{ author.user.username }}
|
||||
</strong>
|
||||
<br />
|
||||
{{ author.role }}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Version ID</span>
|
||||
<span class="metadata-value"><CopyCode class="copycode" :text="version.id" /></span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
</VersionPage>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { CheckIcon, DownloadIcon, ExternalIcon, FileIcon, ReportIcon } from '@modrinth/assets'
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronLeftIcon,
|
||||
DownloadIcon,
|
||||
ExternalIcon,
|
||||
MoreVerticalIcon,
|
||||
ReportIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Breadcrumbs,
|
||||
ButtonStyled,
|
||||
Card,
|
||||
CopyCode,
|
||||
useFormatBytes,
|
||||
useFormatDateTime,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
type DependencyContext,
|
||||
OverflowMenu,
|
||||
useVIntl,
|
||||
VersionPage,
|
||||
} from '@modrinth/ui'
|
||||
import { renderString } from '@modrinth/utils'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { SwapIcon } from '@/assets/icons'
|
||||
import { get_project_many, get_version_many } from '@/helpers/cache.js'
|
||||
import { releaseColor } from '@/helpers/utils'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
|
||||
const formatDateTime = useFormatDateTime({
|
||||
timeStyle: 'short',
|
||||
dateStyle: 'long',
|
||||
})
|
||||
const formatBytes = useFormatBytes()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
members: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
install: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
installed: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
const messages = defineMessages({
|
||||
allVersions: {
|
||||
id: 'app.project.version.all-versions',
|
||||
defaultMessage: 'All versions',
|
||||
},
|
||||
installing: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
id: 'app.project.version.installing',
|
||||
defaultMessage: 'Installing',
|
||||
},
|
||||
installedVersion: {
|
||||
type: String,
|
||||
required: true,
|
||||
downloadInBrowser: {
|
||||
id: 'app.project.version.download-in-browser',
|
||||
defaultMessage: 'Download in browser',
|
||||
},
|
||||
})
|
||||
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
const route = useRoute()
|
||||
|
||||
const props = defineProps<{
|
||||
project: Labrinth.Projects.v2.Project
|
||||
versions: Labrinth.Versions.v3.Version[]
|
||||
members: Labrinth.Projects.v3.TeamMember[]
|
||||
install: (version: string | null) => void
|
||||
installed: boolean
|
||||
installing: boolean
|
||||
installedVersion: string
|
||||
}>()
|
||||
|
||||
const version = ref(props.versions.find((version) => version.id === route.params.version))
|
||||
breadcrumbs.setName('Version', version.value.name)
|
||||
if (version.value) {
|
||||
breadcrumbs.setName('Version', version.value.name)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.versions,
|
||||
async () => {
|
||||
if (route.params.version) {
|
||||
version.value = props.versions.find((version) => version.id === route.params.version)
|
||||
await refreshDisplayDependencies()
|
||||
breadcrumbs.setName('Version', version.value.name)
|
||||
}
|
||||
},
|
||||
)
|
||||
const enrichment = ref<Labrinth.Projects.v2.DependencyInfo | undefined>(undefined)
|
||||
const enrichmentLoading = ref(false)
|
||||
|
||||
const author = computed(() =>
|
||||
props.members ? props.members.find((member) => member.user.id === version.value.author_id) : null,
|
||||
)
|
||||
|
||||
const displayDependencies = ref({})
|
||||
|
||||
function buildProjectHref(path) {
|
||||
function buildProjectHref(path: string): string {
|
||||
const params = new URLSearchParams()
|
||||
for (const [key, val] of Object.entries(route.query)) {
|
||||
if (Array.isArray(val)) {
|
||||
for (const v of val) params.append(key, v)
|
||||
for (const v of val) {
|
||||
if (v != null) params.append(key, v)
|
||||
}
|
||||
} else if (val) {
|
||||
params.append(key, String(val))
|
||||
}
|
||||
@@ -262,222 +155,65 @@ function buildProjectHref(path) {
|
||||
return qs ? `${path}?${qs}` : path
|
||||
}
|
||||
|
||||
async function refreshDisplayDependencies() {
|
||||
const projectIds = new Set()
|
||||
const versionIds = new Set()
|
||||
if (version.value.dependencies) {
|
||||
for (const dependency of version.value.dependencies) {
|
||||
if (dependency.project_id) {
|
||||
projectIds.add(dependency.project_id)
|
||||
}
|
||||
if (dependency.version_id) {
|
||||
versionIds.add(dependency.version_id)
|
||||
}
|
||||
}
|
||||
function createDependencyLink(context: DependencyContext): string | undefined {
|
||||
if (context.version) {
|
||||
return buildProjectHref(`/project/${context.version.project_id}/version/${context.version.id}`)
|
||||
}
|
||||
const [projectDeps, versionDeps] = await Promise.all([
|
||||
get_project_many([...projectIds]),
|
||||
get_version_many([...versionIds]),
|
||||
])
|
||||
|
||||
const dependencies = {
|
||||
projects: projectDeps,
|
||||
versions: versionDeps,
|
||||
if (context.project) {
|
||||
return buildProjectHref(`/project/${context.project.id}`)
|
||||
}
|
||||
|
||||
displayDependencies.value = version.value.dependencies.map((dependency) => {
|
||||
const version = dependencies.versions.find((obj) => obj.id === dependency.version_id)
|
||||
if (version) {
|
||||
const project = dependencies.projects.find(
|
||||
(obj) => obj.id === version.project_id || obj.id === dependency.project_id,
|
||||
)
|
||||
return {
|
||||
icon: project?.icon_url,
|
||||
title: project?.title || project?.name,
|
||||
subtitle: `Version ${version.version_number} is ${dependency.dependency_type}`,
|
||||
link: buildProjectHref(`/project/${project.slug}/version/${version.id}`),
|
||||
}
|
||||
} else {
|
||||
const project = dependencies.projects.find((obj) => obj.id === dependency.project_id)
|
||||
|
||||
if (project) {
|
||||
return {
|
||||
icon: project?.icon_url,
|
||||
title: project?.title || project?.name,
|
||||
subtitle: `${dependency.dependency_type}`,
|
||||
link: buildProjectHref(`/project/${project.slug}`),
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
icon: null,
|
||||
title: dependency.file_name,
|
||||
subtitle: `Added via overrides`,
|
||||
link: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
await refreshDisplayDependencies()
|
||||
|
||||
async function refreshEnrichment() {
|
||||
if (!version.value) return
|
||||
|
||||
const projectIds = new Set<string>()
|
||||
const versionIds = new Set<string>()
|
||||
for (const dependency of version.value.dependencies ?? []) {
|
||||
if (dependency.project_id) {
|
||||
projectIds.add(dependency.project_id)
|
||||
}
|
||||
if (dependency.version_id) {
|
||||
versionIds.add(dependency.version_id)
|
||||
}
|
||||
}
|
||||
|
||||
if (projectIds.size === 0 && versionIds.size === 0) {
|
||||
enrichment.value = { projects: [], versions: [] }
|
||||
return
|
||||
}
|
||||
|
||||
enrichmentLoading.value = true
|
||||
try {
|
||||
const versionResults = versionIds.size > 0 ? await get_version_many([...versionIds]) : []
|
||||
for (const dependencyVersion of versionResults ?? []) {
|
||||
if (dependencyVersion.project_id) {
|
||||
projectIds.add(dependencyVersion.project_id)
|
||||
}
|
||||
}
|
||||
const projectResults = projectIds.size > 0 ? await get_project_many([...projectIds]) : []
|
||||
enrichment.value = {
|
||||
projects: projectResults ?? [],
|
||||
versions: versionResults ?? [],
|
||||
}
|
||||
} finally {
|
||||
enrichmentLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.versions,
|
||||
async () => {
|
||||
if (route.params.version) {
|
||||
version.value = props.versions.find((v) => v.id === route.params.version)
|
||||
if (version.value) {
|
||||
breadcrumbs.setName('Version', version.value.name)
|
||||
}
|
||||
await refreshEnrichment()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
await refreshEnrichment()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.version-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.version-title {
|
||||
margin-bottom: 1rem;
|
||||
h2 {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: 700;
|
||||
color: var(--color-contrast);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dependency {
|
||||
display: flex;
|
||||
padding: 0.5rem 1rem 0.5rem 0.5rem;
|
||||
gap: 0.5rem;
|
||||
background: var(--color-raised-bg);
|
||||
color: var(--color-base);
|
||||
width: 100%;
|
||||
|
||||
.title {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
:deep(svg) {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.file {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
background: var(--color-button-bg);
|
||||
color: var(--color-base);
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
.download {
|
||||
margin-left: auto;
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
margin: auto 0 auto;
|
||||
gap: 0.5rem;
|
||||
|
||||
.title {
|
||||
font-weight: bolder;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
svg {
|
||||
min-width: 1.1rem;
|
||||
min-height: 1.1rem;
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
.primary-label {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.primary {
|
||||
background: var(--color-brand-highlight);
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--color-contrast);
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.description-cards {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.metadata-card {
|
||||
width: 20rem;
|
||||
height: min-content;
|
||||
}
|
||||
|
||||
.metadata {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
|
||||
.metadata-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
.metadata-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: var(--color-base);
|
||||
background: var(--color-raised-bg);
|
||||
padding: 0.5rem;
|
||||
width: 100%;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
:deep(hr),
|
||||
:deep(h1),
|
||||
:deep(h2),
|
||||
img {
|
||||
max-width: max(60rem, 90%) !important;
|
||||
}
|
||||
|
||||
:deep(ul),
|
||||
:deep(ol) {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.copycode {
|
||||
border: 0;
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
|
||||
.disabled {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
vertical-align: center;
|
||||
align-items: center;
|
||||
cursor: not-allowed;
|
||||
border-radius: var(--radius-lg);
|
||||
|
||||
.text {
|
||||
filter: brightness(0.5);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,10 +9,18 @@
|
||||
:version-link="(version) => buildProjectHref(`/project/${project.id}/version/${version.id}`)"
|
||||
>
|
||||
<template #actions="{ version }">
|
||||
<ButtonStyled circular type="transparent" color="green">
|
||||
<ButtonStyled
|
||||
circular
|
||||
type="transparent"
|
||||
:color="installed && version.id === installedVersion ? 'standard' : 'green'"
|
||||
>
|
||||
<button
|
||||
v-tooltip="
|
||||
!installed ? 'Install' : version.id !== installedVersion ? 'Swap version' : ''
|
||||
!installed
|
||||
? formatMessage(commonMessages.installButton)
|
||||
: version.id !== installedVersion
|
||||
? formatMessage(commonMessages.switchToVersionButton)
|
||||
: formatMessage(messages.alreadyInstalled)
|
||||
"
|
||||
:disabled="installing || (installed && version.id === installedVersion)"
|
||||
@click.stop="() => install(version.id)"
|
||||
@@ -45,11 +53,13 @@
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
Add to another instance
|
||||
</template>
|
||||
<template #open-in-browser> <ExternalIcon /> Open in browser </template>
|
||||
<template #open-in-browser>
|
||||
<ExternalIcon /> {{ formatMessage(commonMessages.openInBrowserButton) }}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
<a
|
||||
v-else
|
||||
v-tooltip="`Open in browser`"
|
||||
v-tooltip="formatMessage(commonMessages.openInBrowserButton)"
|
||||
:href="`https://modrinth.com/${project.project_type}/${project.slug}/version/${version.id}`"
|
||||
target="_blank"
|
||||
>
|
||||
@@ -65,9 +75,12 @@
|
||||
import { CheckIcon, DownloadIcon, ExternalIcon, MoreVerticalIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
OverflowMenu,
|
||||
ProjectPageVersions,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
@@ -76,8 +89,16 @@ import { SwapIcon } from '@/assets/icons/index.js'
|
||||
import { get_game_versions, get_loaders } from '@/helpers/tags.js'
|
||||
import { useTheming } from '@/store/theme.ts'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const themeStore = useTheming()
|
||||
|
||||
const messages = defineMessages({
|
||||
alreadyInstalled: {
|
||||
id: 'app.project.versions.already-installed',
|
||||
defaultMessage: 'Already installed',
|
||||
},
|
||||
})
|
||||
|
||||
defineProps({
|
||||
project: {
|
||||
type: Object,
|
||||
|
||||
@@ -257,7 +257,7 @@ const messages = defineMessages({
|
||||
},
|
||||
managePasskeyModalLoading: {
|
||||
id: 'settings.account.security.passkey.modal.loading',
|
||||
defaultMessage: 'Loading passkeys…',
|
||||
defaultMessage: 'Loading passkeys...',
|
||||
},
|
||||
managePasskeyModalNoPasskeys: {
|
||||
id: 'settings.account.security.passkey.modal.no-passkeys',
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
BrowseInstallPlan,
|
||||
BrowseSearchState,
|
||||
CreationFlowContextValue,
|
||||
EnvironmentSearchOverride,
|
||||
FilterValue,
|
||||
PendingServerContentInstall,
|
||||
PendingServerContentInstallType,
|
||||
@@ -160,6 +161,7 @@ export function useServerInstallContent({
|
||||
})
|
||||
|
||||
const serverHideInstalled = ref(false)
|
||||
const serverContentServerOnly = ref(false)
|
||||
const hideSelectedServerInstalls = ref(false)
|
||||
const installingProjectIds = ref<Set<string>>(new Set())
|
||||
const optimisticallyInstalledProjectIds = ref<Set<string>>(new Set())
|
||||
@@ -348,10 +350,6 @@ export function useServerInstallContent({
|
||||
filters.push({ type: 'plugin_loader', option: platform })
|
||||
}
|
||||
|
||||
if (projectType.value?.id === 'mod') {
|
||||
filters.push({ type: 'environment', option: 'server' })
|
||||
}
|
||||
|
||||
if (serverHideInstalled.value && hiddenInstalledProjectIds.value.size > 0) {
|
||||
for (const x of hiddenInstalledProjectIds.value) {
|
||||
filters.push({
|
||||
@@ -383,6 +381,28 @@ export function useServerInstallContent({
|
||||
return filters
|
||||
})
|
||||
|
||||
const showServerOnlyToggle = computed(() => !!serverData.value && projectType.value?.id === 'mod')
|
||||
|
||||
const serverEnvironmentOverride = computed<EnvironmentSearchOverride | undefined>(() => {
|
||||
if (!showServerOnlyToggle.value) return undefined
|
||||
if (serverContentServerOnly.value) {
|
||||
return {
|
||||
mode: 'include',
|
||||
values: [
|
||||
'server_only',
|
||||
'dedicated_server_only',
|
||||
'server_only_client_optional',
|
||||
'client_or_server_prefers_both',
|
||||
'client_or_server',
|
||||
],
|
||||
}
|
||||
}
|
||||
return {
|
||||
mode: 'exclude',
|
||||
values: ['client_only', 'singleplayer_only'],
|
||||
}
|
||||
})
|
||||
|
||||
function getCurrentServerInstallType(): BrowseInstallContentType {
|
||||
const type = projectType.value?.id
|
||||
if (type === 'modpack' || type === 'mod' || type === 'plugin' || type === 'datapack') {
|
||||
@@ -737,6 +757,10 @@ export function useServerInstallContent({
|
||||
serverHideInstalled.value = route.query.shi === 'true'
|
||||
}
|
||||
|
||||
if (route.query.so && projectType.value?.id === 'mod') {
|
||||
serverContentServerOnly.value = route.query.so === 'true'
|
||||
}
|
||||
|
||||
watch(serverHideInstalled, (hideInstalled) => {
|
||||
if (hideInstalled) {
|
||||
syncHiddenInstalledProjectIds()
|
||||
@@ -763,6 +787,9 @@ export function useServerInstallContent({
|
||||
serverContentData,
|
||||
serverFilters,
|
||||
serverHideInstalled,
|
||||
serverContentServerOnly,
|
||||
showServerOnlyToggle,
|
||||
serverEnvironmentOverride,
|
||||
hideSelectedServerInstalls,
|
||||
installingProjectIds,
|
||||
optimisticallyInstalledProjectIds,
|
||||
|
||||
@@ -4164,7 +4164,7 @@
|
||||
"message": "Last used {ago}"
|
||||
},
|
||||
"settings.account.security.passkey.modal.loading": {
|
||||
"message": "Loading passkeys…"
|
||||
"message": "Loading passkeys..."
|
||||
},
|
||||
"settings.account.security.passkey.modal.never-used": {
|
||||
"message": "Never used"
|
||||
|
||||
@@ -143,7 +143,7 @@ onMounted(() => {
|
||||
{{ previewError }}
|
||||
</div>
|
||||
|
||||
<div v-if="previewLoading" class="my-4 text-sm text-secondary">Loading preview…</div>
|
||||
<div v-if="previewLoading" class="my-4 text-sm text-secondary">Loading preview...</div>
|
||||
<div v-else>
|
||||
<div v-if="previewVariables.length" class="mt-2 grid gap-3 md:grid-cols-2">
|
||||
<label
|
||||
|
||||
@@ -158,6 +158,9 @@ const {
|
||||
serverContentData,
|
||||
serverFilters,
|
||||
serverHideInstalled,
|
||||
serverContentServerOnly,
|
||||
showServerOnlyToggle,
|
||||
serverEnvironmentOverride,
|
||||
hideSelectedServerInstalls,
|
||||
installingProjectIds,
|
||||
optimisticallyInstalledProjectIds,
|
||||
@@ -378,10 +381,12 @@ const searchState = useBrowseSearch({
|
||||
projectType: projectTypeId,
|
||||
tags,
|
||||
providedFilters: serverFilters,
|
||||
environmentOverride: serverEnvironmentOverride,
|
||||
search,
|
||||
persistentQueryParams: ['sid', 'wid', 'shi', 'from'],
|
||||
persistentQueryParams: ['sid', 'wid', 'shi', 'so', 'from'],
|
||||
getExtraQueryParams: () => ({
|
||||
shi: serverHideInstalled.value ? 'true' : undefined,
|
||||
so: showServerOnlyToggle.value && serverContentServerOnly.value ? 'true' : undefined,
|
||||
}),
|
||||
maxResultsOptions: currentMaxResultsOptions,
|
||||
displayMode: resultsDisplayMode,
|
||||
@@ -469,6 +474,10 @@ provideBrowseManager({
|
||||
queuedServerInstallCount.value > 0,
|
||||
),
|
||||
hideSelectedLabel: computed(() => formatMessage(commonMessages.hideSelectedContentLabel)),
|
||||
serverOnly: serverContentServerOnly,
|
||||
showServerOnly: showServerOnlyToggle,
|
||||
serverOnlyLabel: computed(() => formatMessage(commonMessages.serverOnlyLabel)),
|
||||
hiddenFilterTypes: computed(() => (showServerOnlyToggle.value ? ['environment'] : [])),
|
||||
displayMode: resultsDisplayMode,
|
||||
cycleDisplayMode: cycleSearchDisplayMode,
|
||||
maxResultsOptions: currentMaxResultsOptions,
|
||||
|
||||
@@ -10,6 +10,62 @@ export type VersionEntry = {
|
||||
}
|
||||
|
||||
const VERSIONS: VersionEntry[] = [
|
||||
{
|
||||
date: `2026-07-05T01:48:48+00:00`,
|
||||
product: 'app',
|
||||
version: '0.15.7',
|
||||
body: `## Fixed
|
||||
- Fixed environment filters showing in server panel's browse page.`,
|
||||
},
|
||||
{
|
||||
date: `2026-07-05T01:48:48+00:00`,
|
||||
product: 'hosting',
|
||||
body: `## Fixed
|
||||
- Fixed environment filters showing in server panel's browse page.
|
||||
- Fixed server-specific filter options not being removed from the URL when toggled off.`,
|
||||
},
|
||||
{
|
||||
date: `2026-07-04T23:15:47+00:00`,
|
||||
product: 'app',
|
||||
version: '0.15.6',
|
||||
body: `## Fixed
|
||||
- Fixed the functionality of the server environment filter in Discover.`,
|
||||
},
|
||||
{
|
||||
date: `2026-07-04T23:15:47+00:00`,
|
||||
product: 'web',
|
||||
body: `## Fixed
|
||||
- Fixed the functionality of the server environment filter in Discover.`,
|
||||
},
|
||||
{
|
||||
date: `2026-07-04T23:15:47+00:00`,
|
||||
product: 'hosting',
|
||||
body: `## Changed
|
||||
- Replaced the environment filters in the panel with a toggle for server-side only content that appears at the top of the filters.`,
|
||||
},
|
||||
{
|
||||
date: `2026-07-04T22:59:54+00:00`,
|
||||
product: 'app',
|
||||
version: '0.15.5',
|
||||
body: `## Changed
|
||||
- Added cdn.serilum.com and workflow.serilum.com as domains that can bypass the image proxy.
|
||||
- Allow modfolio.creeperkatze.dev to bypass image proxy.
|
||||
|
||||
## Fixed
|
||||
- Fixed the update app button width being fixed width, causing issues with non-English translations.`,
|
||||
},
|
||||
{
|
||||
date: `2026-07-04T22:59:54+00:00`,
|
||||
product: 'web',
|
||||
body: `## Changed
|
||||
- Added cdn.serilum.com and workflow.serilum.com as domains that can bypass the image proxy.
|
||||
- Refreshed project download modal UI.
|
||||
- Allow modfolio.creeperkatze.dev to bypass image proxy.
|
||||
- Added a guard to prevent \`.mrpacks\` to be uploaded as supplementary files.
|
||||
|
||||
## Fixed
|
||||
- Fixed version page table's platform tag overflow issues.`,
|
||||
},
|
||||
{
|
||||
date: `2026-07-03T15:16:21+00:00`,
|
||||
product: 'web',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div v-bind="$attrs">
|
||||
<div v-if="divider && !!slots.title" class="flex items-center gap-4 mb-4">
|
||||
<div v-if="divider && (!!slots.title || !!slots.button)" class="flex items-center gap-4 mb-4">
|
||||
<button
|
||||
:class="
|
||||
buttonClass ??
|
||||
@@ -24,7 +24,7 @@
|
||||
<hr class="h-px w-full border-none bg-divider" aria-hidden="true" />
|
||||
</div>
|
||||
<button
|
||||
v-else-if="!!slots.title"
|
||||
v-else-if="!!slots.title || !!slots.button"
|
||||
:class="buttonClass ?? 'flex flex-col gap-2 bg-transparent m-0 p-0 border-none'"
|
||||
@click="() => (forceOpen ? undefined : toggledOpen ? close() : open())"
|
||||
>
|
||||
|
||||
@@ -38,7 +38,7 @@ const messages = defineMessages({
|
||||
},
|
||||
addFilesModalSearchPlaceholder: {
|
||||
id: 'external-files.permissions-card.add-files-modal.search-placeholder',
|
||||
defaultMessage: 'Search files…',
|
||||
defaultMessage: 'Search files...',
|
||||
},
|
||||
addFilesModalNoSearchResults: {
|
||||
id: 'external-files.permissions-card.add-files-modal.no-search-results',
|
||||
|
||||
@@ -216,7 +216,7 @@ defineExpose({ show, hide })
|
||||
:icon="SearchIcon"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
placeholder="Search external projects…"
|
||||
placeholder="Search external projects..."
|
||||
clearable
|
||||
wrapper-class="flex-1 min-w-[12rem]"
|
||||
:disabled="addFilesMutation.isPending.value"
|
||||
@@ -237,14 +237,14 @@ defineExpose({ show, hide })
|
||||
>
|
||||
<span class="text-contrast font-semibold">
|
||||
<template v-if="searchNeedsInput"> Enter a search term to get started </template>
|
||||
<template v-else-if="isLoading"> Loading external projects… </template>
|
||||
<template v-else-if="isLoading"> Loading external projects...</template>
|
||||
<template v-else> No projects matched that search </template>
|
||||
</span>
|
||||
<span class="text-secondary text-sm">
|
||||
<template v-if="searchNeedsInput">
|
||||
Type at least 3 characters of a project's title to begin browsing.
|
||||
</template>
|
||||
<template v-else-if="isLoading"> Loading external projects… </template>
|
||||
<template v-else-if="isLoading"> Loading external projects...</template>
|
||||
<template v-else> No projects matched that search </template>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -7,10 +7,16 @@
|
||||
title-wrapper-class="flex flex-col gap-2 justify-start items-start"
|
||||
:open-by-default="openByDefault !== undefined ? openByDefault : true"
|
||||
>
|
||||
<template #title>
|
||||
<slot name="header" :filter="filterType">
|
||||
<h2 class="font-semibold">{{ filterType.formatted_name }}</h2>
|
||||
</slot>
|
||||
<template #button="{ open }">
|
||||
<div class="flex items-center gap-1 w-full text-contrast">
|
||||
<slot name="header" :filter="filterType">
|
||||
<h2 class="text-base font-semibold text-red">{{ filterType.formatted_name }}</h2>
|
||||
</slot>
|
||||
<DropdownIcon
|
||||
class="ml-auto size-5 transition-transform duration-300 shrink-0 text-primary group-hover:text-contrast"
|
||||
:class="{ 'rotate-180': open }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
v-if="
|
||||
@@ -39,7 +45,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="locked" #default>
|
||||
<div class="flex flex-col gap-2 p-3 border-dashed border-2 rounded-2xl border-divider mx-2">
|
||||
<div class="flex flex-col gap-2 p-3 border-dashed border-2 rounded-2xl border-divider">
|
||||
<p class="m-0 font-bold items-center">
|
||||
<slot :name="`locked-${filterType.id}`">
|
||||
{{ formatMessage(messages.lockedTitle, { type: filterType.formatted_name }) }}
|
||||
|
||||
@@ -4,7 +4,13 @@ import { computed, nextTick, ref, shallowRef, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { useDebugLogger } from '#ui/composables/debug-logger'
|
||||
import type { FilterType, FilterValue, ProjectType, SortType } from '#ui/utils/search'
|
||||
import type {
|
||||
EnvironmentSearchOverride,
|
||||
FilterType,
|
||||
FilterValue,
|
||||
ProjectType,
|
||||
SortType,
|
||||
} from '#ui/utils/search'
|
||||
import { LOADER_FILTER_TYPES, useSearch } from '#ui/utils/search'
|
||||
import { useServerSearch } from '#ui/utils/server-search'
|
||||
|
||||
@@ -18,6 +24,7 @@ export interface UseBrowseSearchOptions {
|
||||
categories: Labrinth.Tags.v2.Category[]
|
||||
}>
|
||||
providedFilters?: ComputedRef<FilterValue[]>
|
||||
environmentOverride?: ComputedRef<EnvironmentSearchOverride | undefined>
|
||||
search: (params: string) => Promise<BrowseSearchResponse>
|
||||
persistentQueryParams: string[]
|
||||
getExtraQueryParams?: () => Record<string, string | undefined>
|
||||
@@ -82,7 +89,12 @@ export function useBrowseSearch(options: UseBrowseSearchOptions): BrowseSearchSt
|
||||
sortTypes,
|
||||
requestParams,
|
||||
createPageParams,
|
||||
} = useSearch(projectTypes, options.tags, options.providedFilters ?? computed(() => []))
|
||||
} = useSearch(
|
||||
projectTypes,
|
||||
options.tags,
|
||||
options.providedFilters ?? computed(() => []),
|
||||
options.environmentOverride ?? computed(() => undefined),
|
||||
)
|
||||
|
||||
const {
|
||||
serverCurrentSortType,
|
||||
@@ -262,9 +274,7 @@ export function useBrowseSearch(options: UseBrowseSearchOptions): BrowseSearchSt
|
||||
|
||||
const extraParams = options.getExtraQueryParams?.() ?? {}
|
||||
for (const [key, value] of Object.entries(extraParams)) {
|
||||
if (value !== undefined) {
|
||||
persistentParams[key] = value
|
||||
}
|
||||
persistentParams[key] = value
|
||||
}
|
||||
|
||||
const params = {
|
||||
|
||||
@@ -164,7 +164,11 @@ const messages = defineMessages({
|
||||
<SearchFilterControl
|
||||
v-else
|
||||
v-model:selected-filters="ctx.currentFilters.value"
|
||||
:filters="ctx.filters.value.filter((f) => f.display !== 'none')"
|
||||
:filters="
|
||||
ctx.filters.value.filter(
|
||||
(f) => f.display !== 'none' && !(ctx.hiddenFilterTypes?.value ?? []).includes(f.id),
|
||||
)
|
||||
"
|
||||
:provided-filters="ctx.providedFilters?.value ?? []"
|
||||
:overridden-provided-filter-types="ctx.overriddenProvidedFilterTypes.value"
|
||||
:provided-message="lockedMessages?.providedBy"
|
||||
|
||||
@@ -69,6 +69,10 @@ export interface BrowseManagerContext {
|
||||
hideSelected?: Ref<boolean>
|
||||
showHideSelected?: ComputedRef<boolean>
|
||||
hideSelectedLabel?: ComputedRef<string>
|
||||
serverOnly?: Ref<boolean>
|
||||
showServerOnly?: ComputedRef<boolean>
|
||||
serverOnlyLabel?: ComputedRef<string>
|
||||
hiddenFilterTypes?: ComputedRef<string[]>
|
||||
onInstalled?: (projectId: string) => void
|
||||
|
||||
displayMode?: Ref<'list' | 'grid' | 'gallery'> | ComputedRef<'list' | 'grid' | 'gallery'>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { InfoIcon, XIcon } from '@modrinth/assets'
|
||||
import { computed, toValue } from 'vue'
|
||||
|
||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||
import Checkbox from '#ui/components/base/Checkbox.vue'
|
||||
import Toggle from '#ui/components/base/Toggle.vue'
|
||||
import SearchSidebarFilter from '#ui/components/search/SearchSidebarFilter.vue'
|
||||
import { useVIntl } from '#ui/composables/i18n'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
@@ -15,6 +15,7 @@ const { formatMessage } = useVIntl()
|
||||
|
||||
const isApp = computed(() => ctx.variant === 'app')
|
||||
const lockedMessages = computed(() => toValue(ctx.lockedFilterMessages))
|
||||
const hiddenFilterTypes = computed(() => ctx.hiddenFilterTypes?.value ?? [])
|
||||
|
||||
function closeFiltersMenu() {
|
||||
if (ctx.filtersMenuOpen) {
|
||||
@@ -37,7 +38,7 @@ const buttonClass = computed(() => {
|
||||
if (isApp.value) {
|
||||
return 'button-animation flex flex-col gap-1 px-3 py-3 w-full bg-transparent cursor-pointer border-none hover:bg-button-bg'
|
||||
}
|
||||
return 'button-animation flex flex-col gap-1 px-6 py-3 w-full bg-transparent cursor-pointer border-none'
|
||||
return 'button-animation flex flex-col gap-1 px-4 py-3 w-full bg-transparent cursor-pointer border-none'
|
||||
})
|
||||
|
||||
const contentClass = computed(() => (isApp.value ? 'mt-2 mb-3' : 'mb-4 mx-3'))
|
||||
@@ -99,38 +100,60 @@ function getFilterOpenByDefault(filterId: string): boolean {
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="ctx.showHideInstalled?.value || ctx.showHideSelected?.value"
|
||||
v-if="
|
||||
ctx.showHideInstalled?.value || ctx.showHideSelected?.value || ctx.showServerOnly?.value
|
||||
"
|
||||
:class="
|
||||
isApp
|
||||
? 'flex flex-col gap-3 border-0 border-b-[1px] p-4 last:border-b-0 border-[--brand-gradient-border] border-solid'
|
||||
: 'card-shadow flex flex-col gap-3 rounded-2xl bg-bg-raised p-4'
|
||||
: 'card-shadow flex flex-col gap-3 rounded-2xl bg-bg-raised border-solid border-surface-4 border p-4'
|
||||
"
|
||||
>
|
||||
<Checkbox
|
||||
<label
|
||||
v-if="ctx.showServerOnly?.value"
|
||||
class="flex cursor-pointer items-center justify-between gap-3 text-contrast font-medium"
|
||||
>
|
||||
{{ ctx.serverOnlyLabel?.value ?? formatMessage(commonMessages.serverOnlyLabel) }}
|
||||
<Toggle
|
||||
v-model="ctx.serverOnly!.value"
|
||||
small
|
||||
class="shrink-0"
|
||||
@update:model-value="ctx.onFilterChange()"
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
v-if="ctx.showHideInstalled?.value"
|
||||
v-model="ctx.hideInstalled!.value"
|
||||
:label="
|
||||
class="flex cursor-pointer items-center justify-between gap-3 text-contrast font-medium"
|
||||
>
|
||||
{{
|
||||
ctx.hideInstalledLabel?.value ?? formatMessage(commonMessages.hideInstalledContentLabel)
|
||||
"
|
||||
class="filter-checkbox"
|
||||
@update:model-value="ctx.onFilterChange()"
|
||||
@click.prevent.stop
|
||||
/>
|
||||
<Checkbox
|
||||
}}
|
||||
<Toggle
|
||||
v-model="ctx.hideInstalled!.value"
|
||||
small
|
||||
class="shrink-0"
|
||||
@update:model-value="ctx.onFilterChange()"
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
v-if="ctx.showHideSelected?.value"
|
||||
v-model="ctx.hideSelected!.value"
|
||||
:label="
|
||||
ctx.hideSelectedLabel?.value ?? formatMessage(commonMessages.hideSelectedContentLabel)
|
||||
"
|
||||
class="filter-checkbox"
|
||||
@update:model-value="ctx.onFilterChange()"
|
||||
@click.prevent.stop
|
||||
/>
|
||||
class="flex cursor-pointer items-center justify-between gap-3 text-contrast font-medium"
|
||||
>
|
||||
{{ ctx.hideSelectedLabel?.value ?? formatMessage(commonMessages.hideSelectedContentLabel) }}
|
||||
<Toggle
|
||||
v-model="ctx.hideSelected!.value"
|
||||
small
|
||||
class="shrink-0"
|
||||
@update:model-value="ctx.onFilterChange()"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<template v-if="ctx.isServerType.value">
|
||||
<SearchSidebarFilter
|
||||
v-for="filterType in ctx.serverFilterTypes.value.filter((f) => f.options.length > 0)"
|
||||
v-for="filterType in ctx.serverFilterTypes.value.filter(
|
||||
(f) => f.options.length > 0 && !hiddenFilterTypes.includes(f.id),
|
||||
)"
|
||||
:key="`server-filter-${filterType.id}`"
|
||||
v-model:selected-filters="ctx.serverCurrentFilters.value"
|
||||
v-model:toggled-groups="ctx.serverToggledGroups.value"
|
||||
@@ -143,7 +166,7 @@ function getFilterOpenByDefault(filterId: string): boolean {
|
||||
:open-by-default="getFilterOpenByDefault(filterType.id)"
|
||||
>
|
||||
<template #header>
|
||||
<h3 :class="isApp ? 'text-base m-0' : 'm-0 text-lg font-semibold'">
|
||||
<h3 :class="isApp ? 'text-base m-0' : 'm-0 text-base font-semibold'">
|
||||
{{ filterType.formatted_name }}
|
||||
</h3>
|
||||
</template>
|
||||
@@ -151,7 +174,9 @@ function getFilterOpenByDefault(filterId: string): boolean {
|
||||
</template>
|
||||
<template v-else>
|
||||
<SearchSidebarFilter
|
||||
v-for="filter in ctx.filters.value.filter((f) => f.display !== 'none')"
|
||||
v-for="filter in ctx.filters.value.filter(
|
||||
(f) => f.display !== 'none' && !hiddenFilterTypes.includes(f.id),
|
||||
)"
|
||||
:key="`filter-${filter.id}`"
|
||||
v-model:selected-filters="ctx.currentFilters.value"
|
||||
v-model:toggled-groups="ctx.toggledGroups.value"
|
||||
|
||||
@@ -221,6 +221,9 @@
|
||||
"button.open-folder": {
|
||||
"defaultMessage": "Open folder"
|
||||
},
|
||||
"button.open-in-browser": {
|
||||
"defaultMessage": "Open in browser"
|
||||
},
|
||||
"button.open-in-folder": {
|
||||
"defaultMessage": "Open in folder"
|
||||
},
|
||||
@@ -290,6 +293,9 @@
|
||||
"button.stop": {
|
||||
"defaultMessage": "Stop"
|
||||
},
|
||||
"button.switch-to-version": {
|
||||
"defaultMessage": "Switch to version"
|
||||
},
|
||||
"button.switch-version": {
|
||||
"defaultMessage": "Switch version"
|
||||
},
|
||||
@@ -912,7 +918,7 @@
|
||||
"defaultMessage": "No files match your search."
|
||||
},
|
||||
"external-files.permissions-card.add-files-modal.search-placeholder": {
|
||||
"defaultMessage": "Search files…"
|
||||
"defaultMessage": "Search files..."
|
||||
},
|
||||
"external-files.permissions-card.add-files-modal.selected-count": {
|
||||
"defaultMessage": "{count, plural, one {# file selected} other {# files selected}}"
|
||||
@@ -2190,7 +2196,7 @@
|
||||
"defaultMessage": "Game version"
|
||||
},
|
||||
"label.hide-installed-content": {
|
||||
"defaultMessage": "Hide already installed content"
|
||||
"defaultMessage": "Hide content already installed"
|
||||
},
|
||||
"label.hide-selected-content": {
|
||||
"defaultMessage": "Hide selected content"
|
||||
@@ -2294,6 +2300,9 @@
|
||||
"label.server": {
|
||||
"defaultMessage": "Server"
|
||||
},
|
||||
"label.server-only": {
|
||||
"defaultMessage": "Server only"
|
||||
},
|
||||
"label.servers": {
|
||||
"defaultMessage": "Servers"
|
||||
},
|
||||
|
||||
@@ -207,6 +207,10 @@ export const commonMessages = defineMessages({
|
||||
id: 'button.open-folder',
|
||||
defaultMessage: 'Open folder',
|
||||
},
|
||||
openInBrowserButton: {
|
||||
id: 'button.open-in-browser',
|
||||
defaultMessage: 'Open in browser',
|
||||
},
|
||||
openInModrinthButton: {
|
||||
id: 'button.open-in-modrinth',
|
||||
defaultMessage: 'Open in Modrinth',
|
||||
@@ -429,12 +433,16 @@ export const commonMessages = defineMessages({
|
||||
},
|
||||
hideInstalledContentLabel: {
|
||||
id: 'label.hide-installed-content',
|
||||
defaultMessage: 'Hide already installed content',
|
||||
defaultMessage: 'Hide content already installed',
|
||||
},
|
||||
hideSelectedContentLabel: {
|
||||
id: 'label.hide-selected-content',
|
||||
defaultMessage: 'Hide selected content',
|
||||
},
|
||||
serverOnlyLabel: {
|
||||
id: 'label.server-only',
|
||||
defaultMessage: 'Server only',
|
||||
},
|
||||
installedModpackTitle: {
|
||||
id: 'label.installed-modpack',
|
||||
defaultMessage: 'Installed modpack',
|
||||
@@ -541,6 +549,10 @@ export const commonMessages = defineMessages({
|
||||
id: 'button.copy-link',
|
||||
defaultMessage: 'Copy link',
|
||||
},
|
||||
switchToVersionButton: {
|
||||
id: 'button.switch-to-version',
|
||||
defaultMessage: 'Switch to version',
|
||||
},
|
||||
switchVersionButton: {
|
||||
id: 'button.switch-version',
|
||||
defaultMessage: 'Switch version',
|
||||
|
||||
@@ -60,6 +60,10 @@ export type FilterValue = {
|
||||
negative?: boolean
|
||||
}
|
||||
|
||||
export type EnvironmentSearchOverride =
|
||||
| { mode: 'include'; values: string[] }
|
||||
| { mode: 'exclude'; values: string[] }
|
||||
|
||||
export const LOADER_FILTER_TYPES = [
|
||||
'mod_loader',
|
||||
'plugin_loader',
|
||||
@@ -112,6 +116,7 @@ export function useSearch(
|
||||
projectTypes: Ref<ProjectType[]>,
|
||||
tags: Ref<Tags>,
|
||||
providedFilters: Ref<FilterValue[]>,
|
||||
environmentOverride: Ref<EnvironmentSearchOverride | undefined> = ref(undefined),
|
||||
) {
|
||||
const query = ref('')
|
||||
const maxResults = ref(20)
|
||||
@@ -505,22 +510,34 @@ export function useSearch(
|
||||
}
|
||||
|
||||
// Environment facets
|
||||
const client = filterValues.some(
|
||||
(filter) => filter.type === 'environment' && filter.option === 'client',
|
||||
)
|
||||
const server = filterValues.some(
|
||||
(filter) => filter.type === 'environment' && filter.option === 'server',
|
||||
)
|
||||
for (const envGroup of getEnvironmentFilterGroups(client, server)) {
|
||||
if (envGroup.length === 1) {
|
||||
const [field, val] = envGroup[0].split(':')
|
||||
parts.push(`${field} = ${formatSearchFilterValue(val)}`)
|
||||
} else if (envGroup.length > 1) {
|
||||
const conditions = envGroup.map((f) => {
|
||||
const [field, val] = f.split(':')
|
||||
return `${field} = ${formatSearchFilterValue(val)}`
|
||||
})
|
||||
parts.push(`(${conditions.join(' OR ')})`)
|
||||
const override = environmentOverride.value
|
||||
if (override) {
|
||||
if (override.values.length === 1) {
|
||||
const operator = override.mode === 'include' ? '=' : '!='
|
||||
parts.push(`environment ${operator} ${formatSearchFilterValue(override.values[0])}`)
|
||||
} else if (override.values.length > 1) {
|
||||
const operator = override.mode === 'include' ? 'IN' : 'NOT IN'
|
||||
const quoted = override.values.map(formatSearchFilterValue).join(', ')
|
||||
parts.push(`environment ${operator} [${quoted}]`)
|
||||
}
|
||||
} else {
|
||||
const client = filterValues.some(
|
||||
(filter) => filter.type === 'environment' && filter.option === 'client',
|
||||
)
|
||||
const server = filterValues.some(
|
||||
(filter) => filter.type === 'environment' && filter.option === 'server',
|
||||
)
|
||||
for (const envGroup of getEnvironmentFilterGroups(client, server)) {
|
||||
if (envGroup.length === 1) {
|
||||
const [field, val] = envGroup[0].split(':')
|
||||
parts.push(`${field} = ${formatSearchFilterValue(val)}`)
|
||||
} else if (envGroup.length > 1) {
|
||||
const conditions = envGroup.map((f) => {
|
||||
const [field, val] = f.split(':')
|
||||
return `${field} = ${formatSearchFilterValue(val)}`
|
||||
})
|
||||
parts.push(`(${conditions.join(' OR ')})`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -777,17 +794,28 @@ function mapProjectTypeToSearch(projectType: ProjectType): string {
|
||||
function getEnvironmentFilterGroups(client: boolean, server: boolean): string[][] {
|
||||
const groups: string[][] = []
|
||||
if (client && server) {
|
||||
groups.push(
|
||||
['client_side:required', 'client_side:optional', 'client_side:unsupported'],
|
||||
['server_side:required', 'server_side:optional'],
|
||||
)
|
||||
groups.push([
|
||||
'environment:client_only_server_optional',
|
||||
'environment:server_only_client_optional',
|
||||
'environment:client_and_server',
|
||||
'environment:client_or_server',
|
||||
'environment:client_or_server_prefers_both',
|
||||
])
|
||||
} else if (client) {
|
||||
groups.push(
|
||||
['client_side:optional', 'client_side:required'],
|
||||
['server_side:optional', 'server_side:unsupported'],
|
||||
)
|
||||
groups.push([
|
||||
'environment:client_only',
|
||||
'environment:client_only_server_optional',
|
||||
'environment:client_or_server_prefers_both',
|
||||
'environment:client_or_server',
|
||||
])
|
||||
} else if (server) {
|
||||
groups.push(['server_side:optional', 'server_side:required'])
|
||||
groups.push([
|
||||
'environment:server_only',
|
||||
'environment:dedicated_server_only',
|
||||
'environment:server_only_client_optional',
|
||||
'environment:client_or_server_prefers_both',
|
||||
'environment:client_or_server',
|
||||
])
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user