fix: content tab fixes (#5543)

* fix: search again

* fix: navigation bug

* fix: switch to stable key for toggle disable

* feat: inline backup slow warning icon

* fix: qa

* feat: fix installation state
This commit is contained in:
Calum H.
2026-03-12 22:52:55 +00:00
committed by GitHub
parent 52d46b8aaa
commit ba06c89a0e
15 changed files with 387 additions and 196 deletions

View File

@@ -1,6 +1,6 @@
<template>
<ProjectCard
:title="project.title"
:title="project.name"
:link="
() => {
emit('open')
@@ -12,7 +12,7 @@
"
:author="{ name: project.author, link: `https://modrinth.com/user/${project.author}` }"
:icon-url="project.icon_url"
:summary="project.description"
:summary="project.summary"
:tags="project.display_categories"
:all-tags="project.categories"
:downloads="project.downloads"
@@ -20,16 +20,6 @@
:date-updated="project.date_modified"
:banner="project.featured_gallery ?? undefined"
:color="project.color ?? undefined"
:environment="
projectType
? ['mod', 'modpack'].includes(projectType)
? {
clientSide: project.client_side,
serverSide: project.server_side,
}
: undefined
: undefined
"
layout="list"
>
<template #actions>
@@ -140,5 +130,5 @@ async function install() {
).catch(handleError)
}
const modpack = computed(() => props.project.project_type === 'modpack')
const modpack = computed(() => props.project.project_types?.includes('modpack'))
</script>

View File

@@ -40,7 +40,7 @@ import type Instance from '@/components/ui/Instance.vue'
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
import NavTabs from '@/components/ui/NavTabs.vue'
import SearchCard from '@/components/ui/SearchCard.vue'
import { get_project_v3, get_search_results, get_search_results_v3 } from '@/helpers/cache.js'
import { get_project_v3, get_search_results_v3 } from '@/helpers/cache.js'
import { process_listener } from '@/helpers/events'
import { get_by_profile_path } from '@/helpers/process'
import {
@@ -186,6 +186,21 @@ const instanceFilters = computed(() => {
option: 'client',
})
}
if (
instanceHideInstalled.value &&
(installedProjectIds.value || newlyInstalled.value.length > 0)
) {
const allInstalled = [...(installedProjectIds.value ?? []), ...newlyInstalled.value]
allInstalled
.map((x) => ({
type: 'project_id',
option: `project_id:${x}`,
negative: true,
}))
.forEach((x) => filters.push(x))
}
}
debugLog('instanceFilters result', filters)
@@ -338,13 +353,13 @@ watch(projectType, () => {
loading.value = true
})
interface SearchResults extends Labrinth.Search.v2.SearchResults {
hits: (Labrinth.Search.v2.ResultSearchProject & { installed?: boolean })[]
interface SearchResults extends Labrinth.Search.v3.SearchResults {
hits: (Labrinth.Search.v3.ResultSearchProject & { installed?: boolean })[]
}
const results: Ref<SearchResults | null> = shallowRef(null)
const pageCount = computed(() =>
results.value ? Math.ceil(results.value.total_hits / results.value.limit) : 1,
results.value ? Math.ceil(results.value.total_hits / results.value.hits_per_page) : 1,
)
const effectiveRequestParams = computed(() => {
@@ -364,10 +379,6 @@ watch(effectiveRequestParams, () => {
}, 200)
})
watch(instanceHideInstalled, () => {
debugLog('instanceHideInstalled changed', instanceHideInstalled.value)
refreshSearch()
})
async function refreshSearch() {
const version = ++searchVersion
@@ -375,23 +386,34 @@ async function refreshSearch() {
try {
const isServer = projectType.value === 'server'
const searchParams = isServer ? serverRequestParams.value : requestParams.value
debugLog('searching v3', searchParams)
let rawResults = (await get_search_results_v3(searchParams)) as {
result: SearchResults
} | null
if (version !== searchVersion) {
debugLog('search version stale, discarding', { version, current: searchVersion })
return
}
if (!rawResults) {
rawResults = {
result: {
hits: [],
total_hits: 0,
hits_per_page: maxResults.value,
page: 1,
},
}
}
if (isServer) {
debugLog('searching v3 (server)', serverRequestParams.value)
const rawResults = (await get_search_results_v3(serverRequestParams.value)) as {
result: Labrinth.Search.v3.SearchResults
} | null
if (version !== searchVersion) {
debugLog('search version stale, discarding', { version, current: searchVersion })
return
}
const searchResults = rawResults?.result ?? { hits: [], total_hits: 0 }
const hits = searchResults.hits ?? []
const hits = rawResults.result.hits ?? []
debugLog('server search results', {
hitCount: hits.length,
totalHits: searchResults.total_hits,
totalHits: rawResults.result.total_hits,
})
serverHits.value = hits
serverPings.value = {}
@@ -399,31 +421,11 @@ async function refreshSearch() {
checkServerRunningStates(hits)
results.value = {
hits: [],
total_hits: searchResults.total_hits ?? 0,
limit: maxResults.value,
offset: 0,
total_hits: rawResults.result.total_hits ?? 0,
hits_per_page: maxResults.value,
page: 1,
}
} else {
debugLog('searching v2', requestParams.value)
let rawResults = (await get_search_results(requestParams.value)) as {
result: SearchResults
} | null
if (version !== searchVersion) {
debugLog('search version stale, discarding', { version, current: searchVersion })
return
}
if (!rawResults) {
rawResults = {
result: {
hits: [],
total_hits: 0,
limit: 1,
offset: 0,
},
}
}
if (instance.value) {
const allInstalledIds = new Set([
...newlyInstalled.value,
@@ -434,12 +436,8 @@ async function refreshSearch() {
...val,
installed: allInstalledIds.has(val.project_id),
}))
if (instanceHideInstalled.value) {
rawResults.result.hits = rawResults.result.hits.filter((val) => !val.installed)
}
}
debugLog('v2 search results', {
debugLog('v3 search results', {
hitCount: rawResults.result.hits.length,
totalHits: rawResults.result.total_hits,
})
@@ -671,11 +669,11 @@ const handleRightClick = (event, result) => {
const handleOptionsClick = (args) => {
switch (args.option) {
case 'open_link':
openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`)
openUrl(`https://modrinth.com/${args.item.project_types?.[0] ?? 'project'}/${args.item.slug}`)
break
case 'copy_link':
navigator.clipboard.writeText(
`https://modrinth.com/${args.item.project_type}/${args.item.slug}`,
`https://modrinth.com/${args.item.project_types?.[0] ?? 'project'}/${args.item.slug}`,
)
break
}
@@ -887,7 +885,7 @@ previousFilterState.value = JSON.stringify({
exclude-loaders
@contextmenu.prevent.stop="
(event: any) =>
handleRightClick(event, { project_type: 'server', slug: project.slug })
handleRightClick(event, { project_types: ['server'], slug: project.slug })
"
>
<template #actions>

View File

@@ -76,6 +76,7 @@ import {
useVIntl,
} from '@modrinth/ui'
import { ContentCardLayout as ContentPageLayout } from '@modrinth/ui'
import { useDebounceFn } from '@vueuse/core'
import { getCurrentWebview } from '@tauri-apps/api/webview'
import { open } from '@tauri-apps/plugin-dialog'
import { openUrl } from '@tauri-apps/plugin-opener'
@@ -174,12 +175,34 @@ const props = defineProps<{
const loading = ref(true)
const projects = ref<ContentItem[]>([])
const installingBuffer = ref<ContentItem[]>([])
watch(
() => installingItems.value.get(props.instance.path),
(items) => {
if (items && items.length > 0) {
installingBuffer.value = [...items]
}
},
{ immediate: true, deep: true },
)
watch(projects, (newProjects) => {
if (installingBuffer.value.length === 0) return
const realProjectIds = new Set(newProjects.map((p) => p.project?.id).filter(Boolean))
if (installingBuffer.value.every((item) => realProjectIds.has(item.project?.id))) {
installingBuffer.value = []
}
})
const mergedProjects = computed<ContentItem[]>(() => {
const pending = installingItems.value.get(props.instance.path) ?? []
const active = installingItems.value.get(props.instance.path)
const pending = active ?? installingBuffer.value
if (pending.length === 0) return projects.value
const realProjectIds = new Set(projects.value.map((p) => p.project?.id).filter(Boolean))
const placeholders = pending.filter((item) => !realProjectIds.has(item.project?.id))
return [...projects.value, ...placeholders]
return placeholders.length > 0 ? [...projects.value, ...placeholders] : projects.value
})
const linkedModpackProject = ref<ContentModpackCardProject | null>(null)
@@ -252,7 +275,7 @@ async function handleUploadFiles() {
}
}
async function toggleDisableMod(mod: ContentItem) {
async function _toggleDisableMod(mod: ContentItem) {
try {
mod.file_path = await toggle_disable_project(props.instance.path, mod.file_path!)
mod.enabled = !mod.enabled
@@ -270,6 +293,8 @@ async function toggleDisableMod(mod: ContentItem) {
}
}
const toggleDisableMod = useDebounceFn(_toggleDisableMod, 20)
async function removeMod(mod: ContentItem) {
await remove_project(props.instance.path, mod.file_path!).catch(handleError)
projects.value = projects.value.filter((x) => mod.file_path !== x.file_path)
@@ -354,9 +379,7 @@ async function handleModpackContentToggle(item: ContentItem) {
}
async function handleModpackContentBulkToggle(items: ContentItem[]) {
for (const item of items) {
await toggleDisableMod(item)
}
await Promise.all(items.map((item) => toggleDisableMod(item)))
}
async function handleModpackContent() {
@@ -676,7 +699,10 @@ provideContentManager({
getItemId: (item) => item.file_name,
contentTypeLabel: ref(formatMessage(messages.contentTypeProject)),
toggleEnabled: toggleDisableMod,
bulkEnableItems: (items) => Promise.all(items.map((item) => toggleDisableMod(item))).then(() => {}),
bulkDisableItems: (items) => Promise.all(items.map((item) => toggleDisableMod(item))).then(() => {}),
deleteItem: removeMod,
bulkDeleteItems: (items) => Promise.all(items.map((item) => removeMod(item))).then(() => {}),
refresh: () => initProjects('must_revalidate'),
browse: handleBrowseContent,
uploadFiles: handleUploadFiles,
@@ -699,27 +725,17 @@ provideContentManager({
title: item.file_name.replace('.disabled', ''),
icon_url: null,
},
projectLink: item.installing
? undefined
: item.project?.id
? `/project/${item.project.id}`
: undefined,
version: item.installing
? {
id: item.file_name,
version_number: formatMessage(messages.installing),
file_name: '',
}
: (item.version ?? {
id: item.file_name,
version_number: formatMessage(messages.unknownVersion),
file_name: item.file_name,
}),
versionLink: item.installing
? undefined
: item.project?.id && item.version?.id
? `/project/${item.project.id}/version/${item.version.id}`
: undefined,
projectLink: item.project?.id
? `/project/${item.project.id}`
: undefined,
version: item.version ?? {
id: item.file_name,
version_number: formatMessage(messages.unknownVersion),
file_name: item.file_name,
},
versionLink: item.project?.id && item.version?.id
? `/project/${item.project.id}/version/${item.version.id}`
: undefined,
owner: item.owner
? {
...item.owner,

View File

@@ -127,7 +127,9 @@ export function createContentInstall(opts: {
icon_url?: string | null
project_type?: string
},
version?: Labrinth.Versions.v2.Version,
) {
const primaryFile = version?.files?.find((f) => f.primary) ?? version?.files?.[0]
const placeholder: ContentItem = {
file_name: `__installing_${project.id}`,
project: {
@@ -136,6 +138,13 @@ export function createContentInstall(opts: {
title: project.title,
icon_url: project.icon_url ?? null,
},
version: version
? {
id: version.id,
version_number: version.version_number,
file_name: primaryFile?.filename ?? '',
}
: undefined,
project_type: project.project_type ?? 'mod',
has_update: false,
update_version_id: null,
@@ -288,7 +297,7 @@ export function createContentInstall(opts: {
const installedProjectIds: string[] = []
if (currentProject) {
addInstallingItem(instance.id, currentProject)
addInstallingItem(instance.id, currentProject, version)
installedProjectIds.push(currentProject.id)
}
@@ -297,8 +306,8 @@ export function createContentInstall(opts: {
await installVersionDependencies(
profile,
version,
(depProject: Labrinth.Projects.v2.Project) => {
addInstallingItem(instance.id, depProject)
(depProject: Labrinth.Projects.v2.Project, depVersion?: Labrinth.Versions.v2.Version) => {
addInstallingItem(instance.id, depProject, depVersion)
installedProjectIds.push(depProject.id)
},
)
@@ -450,14 +459,14 @@ export function createContentInstall(opts: {
}
const installedProjectIds: string[] = [project.id]
addInstallingItem(instancePath, project)
addInstallingItem(instancePath, project, version)
try {
await add_project_from_version(instance.path, version.id)
await installVersionDependencies(
instance,
version,
(depProject: Labrinth.Projects.v2.Project) => {
addInstallingItem(instancePath, depProject)
(depProject: Labrinth.Projects.v2.Project, depVersion?: Labrinth.Versions.v2.Version) => {
addInstallingItem(instancePath, depProject, depVersion)
installedProjectIds.push(depProject.id)
},
)

View File

@@ -173,7 +173,7 @@
data-pyro-navigation
class="isolate flex w-full select-none flex-col justify-between gap-4 overflow-auto md:flex-row md:items-center"
>
<NavTabs :links="navLinks" />
<NavTabs :links="navLinks" replace />
</div>
<div data-pyro-mount class="h-full w-full flex-1">

View File

@@ -586,10 +586,6 @@ impl Typesense {
/// `filters`/`version` fields, translating each from Meilisearch filter
/// syntax to Typesense filter syntax.
fn build_filter(info: &SearchRequest) -> Result<Option<String>, ApiError> {
if let Some(new_filters) = info.new_filters.as_deref() {
return Ok(Some(meili_to_typesense(new_filters)));
}
let facet_part = if let Some(facets_json) = info.facets.as_deref() {
Some(
facets_to_typesense(facets_json)
@@ -599,10 +595,18 @@ impl Typesense {
None
};
let legacy_part =
combined_search_filters(info).map(|f| meili_to_typesense(&f));
let new_filters_part =
info.new_filters.as_deref().map(|f| meili_to_typesense(f));
Ok(match (facet_part, legacy_part) {
let legacy_part = if info.new_filters.is_none() {
combined_search_filters(info).map(|f| meili_to_typesense(&f))
} else {
None
};
let filter_part = new_filters_part.or(legacy_part);
Ok(match (facet_part, filter_part) {
(Some(f), Some(l)) if !f.is_empty() && !l.is_empty() => {
Some(format!("({f}) && ({l})"))
}