You've already forked AstralRinth
forked from didirus/AstralRinth
refactor: migrate to common eslint+prettier configs (#4168)
* refactor: migrate to common eslint+prettier configs * fix: prettier frontend * feat: config changes * fix: lint issues * fix: lint * fix: type imports * fix: cyclical import issue * fix: lockfile * fix: missing dep * fix: switch to tabs * fix: continue switch to tabs * fix: rustfmt parity * fix: moderation lint issue * fix: lint issues * fix: ui intl * fix: lint issues * Revert "fix: rustfmt parity" This reverts commit cb99d2376c321d813d4b7fc7e2a213bb30a54711. * feat: revert last rs
This commit is contained in:
@@ -1,4 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { ClipboardCopyIcon, ExternalIcon, GlobeIcon, SearchIcon, XIcon } from '@modrinth/assets'
|
||||
import type { Category, GameVersion, Platform, ProjectType, SortType, Tags } from '@modrinth/ui'
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
DropdownSelect,
|
||||
injectNotificationManager,
|
||||
LoadingIndicator,
|
||||
Pagination,
|
||||
SearchFilterControl,
|
||||
SearchSidebarFilter,
|
||||
useSearch,
|
||||
} from '@modrinth/ui'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, nextTick, ref, shallowRef, watch } from 'vue'
|
||||
import type { LocationQuery } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import type Instance from '@/components/ui/Instance.vue'
|
||||
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
|
||||
@@ -8,25 +28,6 @@ import { get_search_results } from '@/helpers/cache.js'
|
||||
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile.js'
|
||||
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { ClipboardCopyIcon, ExternalIcon, GlobeIcon, SearchIcon, XIcon } from '@modrinth/assets'
|
||||
import type { Category, GameVersion, Platform, ProjectType, SortType, Tags } from '@modrinth/ui'
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
DropdownSelect,
|
||||
injectNotificationManager,
|
||||
LoadingIndicator,
|
||||
Pagination,
|
||||
SearchFilterControl,
|
||||
SearchSidebarFilter,
|
||||
useSearch,
|
||||
} from '@modrinth/ui'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, nextTick, ref, shallowRef, watch } from 'vue'
|
||||
import type { LocationQuery } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
@@ -35,34 +36,34 @@ const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const projectTypes = computed(() => {
|
||||
return [route.params.projectType as ProjectType]
|
||||
return [route.params.projectType as ProjectType]
|
||||
})
|
||||
|
||||
const [categories, loaders, availableGameVersions] = await Promise.all([
|
||||
get_categories().catch(handleError).then(ref),
|
||||
get_loaders().catch(handleError).then(ref),
|
||||
get_game_versions().catch(handleError).then(ref),
|
||||
get_categories().catch(handleError).then(ref),
|
||||
get_loaders().catch(handleError).then(ref),
|
||||
get_game_versions().catch(handleError).then(ref),
|
||||
])
|
||||
|
||||
const tags: Ref<Tags> = computed(() => ({
|
||||
gameVersions: availableGameVersions.value as GameVersion[],
|
||||
loaders: loaders.value as Platform[],
|
||||
categories: categories.value as Category[],
|
||||
gameVersions: availableGameVersions.value as GameVersion[],
|
||||
loaders: loaders.value as Platform[],
|
||||
categories: categories.value as Category[],
|
||||
}))
|
||||
|
||||
type Instance = {
|
||||
game_version: string
|
||||
loader: string
|
||||
path: string
|
||||
install_stage: string
|
||||
icon_path?: string
|
||||
name: string
|
||||
game_version: string
|
||||
loader: string
|
||||
path: string
|
||||
install_stage: string
|
||||
icon_path?: string
|
||||
name: string
|
||||
}
|
||||
|
||||
type InstanceProject = {
|
||||
metadata: {
|
||||
project_id: string
|
||||
}
|
||||
metadata: {
|
||||
project_id: string
|
||||
}
|
||||
}
|
||||
|
||||
const instance: Ref<Instance | null> = ref(null)
|
||||
@@ -75,98 +76,98 @@ const PERSISTENT_QUERY_PARAMS = ['i', 'ai']
|
||||
await updateInstanceContext()
|
||||
|
||||
watch(route, () => {
|
||||
updateInstanceContext()
|
||||
updateInstanceContext()
|
||||
})
|
||||
|
||||
async function updateInstanceContext() {
|
||||
if (route.query.i) {
|
||||
;[instance.value, instanceProjects.value] = await Promise.all([
|
||||
getInstance(route.query.i).catch(handleError),
|
||||
getInstanceProjects(route.query.i).catch(handleError),
|
||||
])
|
||||
newlyInstalled.value = []
|
||||
}
|
||||
if (route.query.i) {
|
||||
;[instance.value, instanceProjects.value] = await Promise.all([
|
||||
getInstance(route.query.i).catch(handleError),
|
||||
getInstanceProjects(route.query.i).catch(handleError),
|
||||
])
|
||||
newlyInstalled.value = []
|
||||
}
|
||||
|
||||
if (route.query.ai && !(projectTypes.value.length === 1 && projectTypes.value[0] === 'modpack')) {
|
||||
instanceHideInstalled.value = route.query.ai === 'true'
|
||||
}
|
||||
if (route.query.ai && !(projectTypes.value.length === 1 && projectTypes.value[0] === 'modpack')) {
|
||||
instanceHideInstalled.value = route.query.ai === 'true'
|
||||
}
|
||||
|
||||
if (instance.value && instance.value.path !== route.query.i && route.path.startsWith('/browse')) {
|
||||
instance.value = null
|
||||
instanceHideInstalled.value = false
|
||||
}
|
||||
if (instance.value && instance.value.path !== route.query.i && route.path.startsWith('/browse')) {
|
||||
instance.value = null
|
||||
instanceHideInstalled.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const instanceFilters = computed(() => {
|
||||
const filters = []
|
||||
const filters = []
|
||||
|
||||
if (instance.value) {
|
||||
const gameVersion = instance.value.game_version
|
||||
if (gameVersion) {
|
||||
filters.push({
|
||||
type: 'game_version',
|
||||
option: gameVersion,
|
||||
})
|
||||
}
|
||||
if (instance.value) {
|
||||
const gameVersion = instance.value.game_version
|
||||
if (gameVersion) {
|
||||
filters.push({
|
||||
type: 'game_version',
|
||||
option: gameVersion,
|
||||
})
|
||||
}
|
||||
|
||||
const platform = instance.value.loader
|
||||
const platform = instance.value.loader
|
||||
|
||||
const supportedModLoaders = ['fabric', 'forge', 'quilt', 'neoforge']
|
||||
const supportedModLoaders = ['fabric', 'forge', 'quilt', 'neoforge']
|
||||
|
||||
if (platform && projectTypes.value.includes('mod') && supportedModLoaders.includes(platform)) {
|
||||
filters.push({
|
||||
type: 'mod_loader',
|
||||
option: platform,
|
||||
})
|
||||
}
|
||||
if (platform && projectTypes.value.includes('mod') && supportedModLoaders.includes(platform)) {
|
||||
filters.push({
|
||||
type: 'mod_loader',
|
||||
option: platform,
|
||||
})
|
||||
}
|
||||
|
||||
if (instanceHideInstalled.value && instanceProjects.value) {
|
||||
const installedMods = Object.values(instanceProjects.value)
|
||||
.filter((x) => x.metadata)
|
||||
.map((x) => x.metadata.project_id)
|
||||
if (instanceHideInstalled.value && instanceProjects.value) {
|
||||
const installedMods = Object.values(instanceProjects.value)
|
||||
.filter((x) => x.metadata)
|
||||
.map((x) => x.metadata.project_id)
|
||||
|
||||
installedMods.push(...newlyInstalled.value)
|
||||
installedMods.push(...newlyInstalled.value)
|
||||
|
||||
installedMods
|
||||
?.map((x) => ({
|
||||
type: 'project_id',
|
||||
option: `project_id:${x}`,
|
||||
negative: true,
|
||||
}))
|
||||
.forEach((x) => filters.push(x))
|
||||
}
|
||||
}
|
||||
installedMods
|
||||
?.map((x) => ({
|
||||
type: 'project_id',
|
||||
option: `project_id:${x}`,
|
||||
negative: true,
|
||||
}))
|
||||
.forEach((x) => filters.push(x))
|
||||
}
|
||||
}
|
||||
|
||||
return filters
|
||||
return filters
|
||||
})
|
||||
|
||||
const {
|
||||
// Selections
|
||||
query,
|
||||
currentSortType,
|
||||
currentFilters,
|
||||
toggledGroups,
|
||||
maxResults,
|
||||
currentPage,
|
||||
overriddenProvidedFilterTypes,
|
||||
// Selections
|
||||
query,
|
||||
currentSortType,
|
||||
currentFilters,
|
||||
toggledGroups,
|
||||
maxResults,
|
||||
currentPage,
|
||||
overriddenProvidedFilterTypes,
|
||||
|
||||
// Lists
|
||||
filters,
|
||||
sortTypes,
|
||||
// Lists
|
||||
filters,
|
||||
sortTypes,
|
||||
|
||||
// Computed
|
||||
requestParams,
|
||||
// Computed
|
||||
requestParams,
|
||||
|
||||
// Functions
|
||||
createPageParams,
|
||||
// Functions
|
||||
createPageParams,
|
||||
} = useSearch(projectTypes, tags, instanceFilters)
|
||||
|
||||
const offline = ref(!navigator.onLine)
|
||||
window.addEventListener('offline', () => {
|
||||
offline.value = true
|
||||
offline.value = true
|
||||
})
|
||||
window.addEventListener('online', () => {
|
||||
offline.value = false
|
||||
offline.value = false
|
||||
})
|
||||
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
@@ -177,350 +178,350 @@ const loading = ref(true)
|
||||
const projectType = ref(route.params.projectType)
|
||||
|
||||
watch(projectType, () => {
|
||||
loading.value = true
|
||||
loading.value = true
|
||||
})
|
||||
|
||||
type SearchResult = {
|
||||
project_id: string
|
||||
project_id: string
|
||||
}
|
||||
|
||||
type SearchResults = {
|
||||
total_hits: number
|
||||
limit: number
|
||||
hits: SearchResult[]
|
||||
total_hits: number
|
||||
limit: number
|
||||
hits: SearchResult[]
|
||||
}
|
||||
|
||||
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.limit) : 1,
|
||||
)
|
||||
|
||||
watch(requestParams, () => {
|
||||
if (!route.params.projectType) return
|
||||
refreshSearch()
|
||||
if (!route.params.projectType) return
|
||||
refreshSearch()
|
||||
})
|
||||
|
||||
async function refreshSearch() {
|
||||
let rawResults = await get_search_results(requestParams.value)
|
||||
if (!rawResults) {
|
||||
rawResults = {
|
||||
result: {
|
||||
hits: [],
|
||||
total_hits: 0,
|
||||
limit: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
if (instance.value) {
|
||||
for (const val of rawResults.result.hits) {
|
||||
val.installed =
|
||||
newlyInstalled.value.includes(val.project_id) ||
|
||||
Object.values(instanceProjects.value).some(
|
||||
(x) => x.metadata && x.metadata.project_id === val.project_id,
|
||||
)
|
||||
}
|
||||
}
|
||||
results.value = rawResults.result
|
||||
currentPage.value = 1
|
||||
let rawResults = await get_search_results(requestParams.value)
|
||||
if (!rawResults) {
|
||||
rawResults = {
|
||||
result: {
|
||||
hits: [],
|
||||
total_hits: 0,
|
||||
limit: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
if (instance.value) {
|
||||
for (const val of rawResults.result.hits) {
|
||||
val.installed =
|
||||
newlyInstalled.value.includes(val.project_id) ||
|
||||
Object.values(instanceProjects.value).some(
|
||||
(x) => x.metadata && x.metadata.project_id === val.project_id,
|
||||
)
|
||||
}
|
||||
}
|
||||
results.value = rawResults.result
|
||||
currentPage.value = 1
|
||||
|
||||
const persistentParams: LocationQuery = {}
|
||||
const persistentParams: LocationQuery = {}
|
||||
|
||||
for (const [key, value] of Object.entries(route.query)) {
|
||||
if (PERSISTENT_QUERY_PARAMS.includes(key)) {
|
||||
persistentParams[key] = value
|
||||
}
|
||||
}
|
||||
for (const [key, value] of Object.entries(route.query)) {
|
||||
if (PERSISTENT_QUERY_PARAMS.includes(key)) {
|
||||
persistentParams[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if (instanceHideInstalled.value) {
|
||||
persistentParams.ai = 'true'
|
||||
} else {
|
||||
delete persistentParams.ai
|
||||
}
|
||||
if (instanceHideInstalled.value) {
|
||||
persistentParams.ai = 'true'
|
||||
} else {
|
||||
delete persistentParams.ai
|
||||
}
|
||||
|
||||
const params = {
|
||||
...persistentParams,
|
||||
...createPageParams(),
|
||||
}
|
||||
const params = {
|
||||
...persistentParams,
|
||||
...createPageParams(),
|
||||
}
|
||||
|
||||
breadcrumbs.setContext({
|
||||
name: 'Discover content',
|
||||
link: `/browse/${projectType.value}`,
|
||||
query: params,
|
||||
})
|
||||
await router.replace({ path: route.path, query: params })
|
||||
loading.value = false
|
||||
breadcrumbs.setContext({
|
||||
name: 'Discover content',
|
||||
link: `/browse/${projectType.value}`,
|
||||
query: params,
|
||||
})
|
||||
await router.replace({ path: route.path, query: params })
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
async function setPage(newPageNumber: number) {
|
||||
currentPage.value = newPageNumber
|
||||
currentPage.value = newPageNumber
|
||||
|
||||
await onSearchChangeToTop()
|
||||
await onSearchChangeToTop()
|
||||
}
|
||||
|
||||
const searchWrapper: Ref<HTMLElement | null> = ref(null)
|
||||
|
||||
async function onSearchChangeToTop() {
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
query.value = ''
|
||||
currentPage.value = 1
|
||||
query.value = ''
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.params.projectType,
|
||||
async (newType) => {
|
||||
// Check if the newType is not the same as the current value
|
||||
if (!newType || newType === projectType.value) return
|
||||
() => route.params.projectType,
|
||||
async (newType) => {
|
||||
// Check if the newType is not the same as the current value
|
||||
if (!newType || newType === projectType.value) return
|
||||
|
||||
projectType.value = newType
|
||||
projectType.value = newType
|
||||
|
||||
currentSortType.value = { display: 'Relevance', name: 'relevance' }
|
||||
query.value = ''
|
||||
},
|
||||
currentSortType.value = { display: 'Relevance', name: 'relevance' }
|
||||
query.value = ''
|
||||
},
|
||||
)
|
||||
|
||||
const selectableProjectTypes = computed(() => {
|
||||
let dataPacks = false,
|
||||
mods = false,
|
||||
modpacks = false
|
||||
let dataPacks = false,
|
||||
mods = false,
|
||||
modpacks = false
|
||||
|
||||
if (instance.value) {
|
||||
if (
|
||||
availableGameVersions.value.findIndex((x) => x.version === instance.value.game_version) <=
|
||||
availableGameVersions.value.findIndex((x) => x.version === '1.13')
|
||||
) {
|
||||
dataPacks = true
|
||||
}
|
||||
if (instance.value) {
|
||||
if (
|
||||
availableGameVersions.value.findIndex((x) => x.version === instance.value.game_version) <=
|
||||
availableGameVersions.value.findIndex((x) => x.version === '1.13')
|
||||
) {
|
||||
dataPacks = true
|
||||
}
|
||||
|
||||
if (instance.value.loader !== 'vanilla') {
|
||||
mods = true
|
||||
}
|
||||
} else {
|
||||
dataPacks = true
|
||||
mods = true
|
||||
modpacks = true
|
||||
}
|
||||
if (instance.value.loader !== 'vanilla') {
|
||||
mods = true
|
||||
}
|
||||
} else {
|
||||
dataPacks = true
|
||||
mods = true
|
||||
modpacks = true
|
||||
}
|
||||
|
||||
const params: LocationQuery = {}
|
||||
const params: LocationQuery = {}
|
||||
|
||||
if (route.query.i) {
|
||||
params.i = route.query.i
|
||||
}
|
||||
if (route.query.ai) {
|
||||
params.ai = route.query.ai
|
||||
}
|
||||
if (route.query.i) {
|
||||
params.i = route.query.i
|
||||
}
|
||||
if (route.query.ai) {
|
||||
params.ai = route.query.ai
|
||||
}
|
||||
|
||||
const links = [
|
||||
{ label: 'Modpacks', href: `/browse/modpack`, shown: modpacks },
|
||||
{ label: 'Mods', href: `/browse/mod`, shown: mods },
|
||||
{ label: 'Resource Packs', href: `/browse/resourcepack` },
|
||||
{ label: 'Data Packs', href: `/browse/datapack`, shown: dataPacks },
|
||||
{ label: 'Shaders', href: `/browse/shader` },
|
||||
]
|
||||
const links = [
|
||||
{ label: 'Modpacks', href: `/browse/modpack`, shown: modpacks },
|
||||
{ label: 'Mods', href: `/browse/mod`, shown: mods },
|
||||
{ label: 'Resource Packs', href: `/browse/resourcepack` },
|
||||
{ label: 'Data Packs', href: `/browse/datapack`, shown: dataPacks },
|
||||
{ label: 'Shaders', href: `/browse/shader` },
|
||||
]
|
||||
|
||||
if (params) {
|
||||
return links.map((link) => {
|
||||
return {
|
||||
...link,
|
||||
href: {
|
||||
path: link.href,
|
||||
query: params,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
if (params) {
|
||||
return links.map((link) => {
|
||||
return {
|
||||
...link,
|
||||
href: {
|
||||
path: link.href,
|
||||
query: params,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return links
|
||||
return links
|
||||
})
|
||||
|
||||
const messages = defineMessages({
|
||||
gameVersionProvidedByInstance: {
|
||||
id: 'search.filter.locked.instance-game-version.title',
|
||||
defaultMessage: 'Game version is provided by the instance',
|
||||
},
|
||||
modLoaderProvidedByInstance: {
|
||||
id: 'search.filter.locked.instance-loader.title',
|
||||
defaultMessage: 'Loader is provided by the instance',
|
||||
},
|
||||
providedByInstance: {
|
||||
id: 'search.filter.locked.instance',
|
||||
defaultMessage: 'Provided by the instance',
|
||||
},
|
||||
syncFilterButton: {
|
||||
id: 'search.filter.locked.instance.sync',
|
||||
defaultMessage: 'Sync with instance',
|
||||
},
|
||||
gameVersionProvidedByInstance: {
|
||||
id: 'search.filter.locked.instance-game-version.title',
|
||||
defaultMessage: 'Game version is provided by the instance',
|
||||
},
|
||||
modLoaderProvidedByInstance: {
|
||||
id: 'search.filter.locked.instance-loader.title',
|
||||
defaultMessage: 'Loader is provided by the instance',
|
||||
},
|
||||
providedByInstance: {
|
||||
id: 'search.filter.locked.instance',
|
||||
defaultMessage: 'Provided by the instance',
|
||||
},
|
||||
syncFilterButton: {
|
||||
id: 'search.filter.locked.instance.sync',
|
||||
defaultMessage: 'Sync with instance',
|
||||
},
|
||||
})
|
||||
|
||||
const options = ref(null)
|
||||
const handleRightClick = (event, result) => {
|
||||
options.value.showMenu(event, result, [
|
||||
{
|
||||
name: 'open_link',
|
||||
},
|
||||
{
|
||||
name: 'copy_link',
|
||||
},
|
||||
])
|
||||
options.value.showMenu(event, result, [
|
||||
{
|
||||
name: 'open_link',
|
||||
},
|
||||
{
|
||||
name: 'copy_link',
|
||||
},
|
||||
])
|
||||
}
|
||||
const handleOptionsClick = (args) => {
|
||||
switch (args.option) {
|
||||
case 'open_link':
|
||||
openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`)
|
||||
break
|
||||
case 'copy_link':
|
||||
navigator.clipboard.writeText(
|
||||
`https://modrinth.com/${args.item.project_type}/${args.item.slug}`,
|
||||
)
|
||||
break
|
||||
}
|
||||
switch (args.option) {
|
||||
case 'open_link':
|
||||
openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`)
|
||||
break
|
||||
case 'copy_link':
|
||||
navigator.clipboard.writeText(
|
||||
`https://modrinth.com/${args.item.project_type}/${args.item.slug}`,
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
await refreshSearch()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport v-if="filters" to="#sidebar-teleport-target">
|
||||
<div
|
||||
v-if="instance"
|
||||
class="border-0 border-b-[1px] p-4 last:border-b-0 border-[--brand-gradient-border] border-solid"
|
||||
>
|
||||
<Checkbox
|
||||
v-model="instanceHideInstalled"
|
||||
label="Hide installed content"
|
||||
class="filter-checkbox"
|
||||
@update:model-value="onSearchChangeToTop()"
|
||||
@click.prevent.stop
|
||||
/>
|
||||
</div>
|
||||
<SearchSidebarFilter
|
||||
v-for="filter in filters.filter((f) => f.display !== 'none')"
|
||||
:key="`filter-${filter.id}`"
|
||||
v-model:selected-filters="currentFilters"
|
||||
v-model:toggled-groups="toggledGroups"
|
||||
v-model:overridden-provided-filter-types="overriddenProvidedFilterTypes"
|
||||
:provided-filters="instanceFilters"
|
||||
:filter-type="filter"
|
||||
class="border-0 border-b-[1px] [&:first-child>button]:pt-4 last:border-b-0 border-[--brand-gradient-border] border-solid"
|
||||
button-class="button-animation flex flex-col gap-1 px-4 py-3 w-full bg-transparent cursor-pointer border-none hover:bg-button-bg"
|
||||
content-class="mb-3"
|
||||
inner-panel-class="ml-2 mr-3"
|
||||
:open-by-default="
|
||||
filter.id.startsWith('category') || filter.id === 'environment' || filter.id === 'license'
|
||||
"
|
||||
>
|
||||
<template #header>
|
||||
<h3 class="text-base m-0">{{ filter.formatted_name }}</h3>
|
||||
</template>
|
||||
<template #locked-game_version>
|
||||
{{ formatMessage(messages.gameVersionProvidedByInstance) }}
|
||||
</template>
|
||||
<template #locked-mod_loader>
|
||||
{{ formatMessage(messages.modLoaderProvidedByInstance) }}
|
||||
</template>
|
||||
<template #sync-button> {{ formatMessage(messages.syncFilterButton) }} </template>
|
||||
</SearchSidebarFilter>
|
||||
</Teleport>
|
||||
<div ref="searchWrapper" class="flex flex-col gap-3 p-6">
|
||||
<template v-if="instance">
|
||||
<InstanceIndicator :instance="instance" />
|
||||
<h1 class="m-0 mb-1 text-xl">Install content to instance</h1>
|
||||
</template>
|
||||
<NavTabs :links="selectableProjectTypes" />
|
||||
<div class="iconified-input">
|
||||
<SearchIcon aria-hidden="true" class="text-lg" />
|
||||
<input
|
||||
v-model="query"
|
||||
class="h-12 card-shadow"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
type="text"
|
||||
:placeholder="`Search ${projectType}s...`"
|
||||
/>
|
||||
<Button v-if="query" class="r-btn" @click="() => clearSearch()">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<DropdownSelect
|
||||
v-slot="{ selected }"
|
||||
v-model="currentSortType"
|
||||
class="max-w-[16rem]"
|
||||
name="Sort by"
|
||||
:options="sortTypes as any"
|
||||
:display-name="(option: SortType | undefined) => option?.display"
|
||||
>
|
||||
<span class="font-semibold text-primary">Sort by: </span>
|
||||
<span class="font-semibold text-secondary">{{ selected }}</span>
|
||||
</DropdownSelect>
|
||||
<DropdownSelect
|
||||
v-slot="{ selected }"
|
||||
v-model="maxResults"
|
||||
name="Max results"
|
||||
:options="[5, 10, 15, 20, 50, 100]"
|
||||
class="max-w-[9rem]"
|
||||
>
|
||||
<span class="font-semibold text-primary">View: </span>
|
||||
<span class="font-semibold text-secondary">{{ selected }}</span>
|
||||
</DropdownSelect>
|
||||
<Pagination :page="currentPage" :count="pageCount" class="ml-auto" @switch-page="setPage" />
|
||||
</div>
|
||||
<SearchFilterControl
|
||||
v-model:selected-filters="currentFilters"
|
||||
:filters="filters.filter((f) => f.display !== 'none')"
|
||||
:provided-filters="instanceFilters"
|
||||
:overridden-provided-filter-types="overriddenProvidedFilterTypes"
|
||||
:provided-message="messages.providedByInstance"
|
||||
/>
|
||||
<div class="search">
|
||||
<section v-if="loading" class="offline">
|
||||
<LoadingIndicator />
|
||||
</section>
|
||||
<section v-else-if="offline && results.total_hits === 0" class="offline">
|
||||
You are currently offline. Connect to the internet to browse Modrinth!
|
||||
</section>
|
||||
<section v-else class="project-list display-mode--list instance-results" role="list">
|
||||
<SearchCard
|
||||
v-for="result in results.hits"
|
||||
:key="result?.project_id"
|
||||
:project="result"
|
||||
:instance="instance"
|
||||
:categories="[
|
||||
...categories.filter(
|
||||
(cat) =>
|
||||
result?.display_categories.includes(cat.name) && cat.project_type === projectType,
|
||||
),
|
||||
...loaders.filter(
|
||||
(loader) =>
|
||||
result?.display_categories.includes(loader.name) &&
|
||||
loader.supported_project_types?.includes(projectType),
|
||||
),
|
||||
]"
|
||||
:installed="result.installed || newlyInstalled.includes(result.project_id)"
|
||||
@install="
|
||||
(id) => {
|
||||
newlyInstalled.push(id)
|
||||
}
|
||||
"
|
||||
@contextmenu.prevent.stop="(event) => handleRightClick(event, result)"
|
||||
/>
|
||||
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
||||
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
||||
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
||||
</ContextMenu>
|
||||
</section>
|
||||
<div class="flex justify-end">
|
||||
<pagination
|
||||
:page="currentPage"
|
||||
:count="pageCount"
|
||||
class="pagination-after"
|
||||
@switch-page="setPage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Teleport v-if="filters" to="#sidebar-teleport-target">
|
||||
<div
|
||||
v-if="instance"
|
||||
class="border-0 border-b-[1px] p-4 last:border-b-0 border-[--brand-gradient-border] border-solid"
|
||||
>
|
||||
<Checkbox
|
||||
v-model="instanceHideInstalled"
|
||||
label="Hide installed content"
|
||||
class="filter-checkbox"
|
||||
@update:model-value="onSearchChangeToTop()"
|
||||
@click.prevent.stop
|
||||
/>
|
||||
</div>
|
||||
<SearchSidebarFilter
|
||||
v-for="filter in filters.filter((f) => f.display !== 'none')"
|
||||
:key="`filter-${filter.id}`"
|
||||
v-model:selected-filters="currentFilters"
|
||||
v-model:toggled-groups="toggledGroups"
|
||||
v-model:overridden-provided-filter-types="overriddenProvidedFilterTypes"
|
||||
:provided-filters="instanceFilters"
|
||||
:filter-type="filter"
|
||||
class="border-0 border-b-[1px] [&:first-child>button]:pt-4 last:border-b-0 border-[--brand-gradient-border] border-solid"
|
||||
button-class="button-animation flex flex-col gap-1 px-4 py-3 w-full bg-transparent cursor-pointer border-none hover:bg-button-bg"
|
||||
content-class="mb-3"
|
||||
inner-panel-class="ml-2 mr-3"
|
||||
:open-by-default="
|
||||
filter.id.startsWith('category') || filter.id === 'environment' || filter.id === 'license'
|
||||
"
|
||||
>
|
||||
<template #header>
|
||||
<h3 class="text-base m-0">{{ filter.formatted_name }}</h3>
|
||||
</template>
|
||||
<template #locked-game_version>
|
||||
{{ formatMessage(messages.gameVersionProvidedByInstance) }}
|
||||
</template>
|
||||
<template #locked-mod_loader>
|
||||
{{ formatMessage(messages.modLoaderProvidedByInstance) }}
|
||||
</template>
|
||||
<template #sync-button> {{ formatMessage(messages.syncFilterButton) }} </template>
|
||||
</SearchSidebarFilter>
|
||||
</Teleport>
|
||||
<div ref="searchWrapper" class="flex flex-col gap-3 p-6">
|
||||
<template v-if="instance">
|
||||
<InstanceIndicator :instance="instance" />
|
||||
<h1 class="m-0 mb-1 text-xl">Install content to instance</h1>
|
||||
</template>
|
||||
<NavTabs :links="selectableProjectTypes" />
|
||||
<div class="iconified-input">
|
||||
<SearchIcon aria-hidden="true" class="text-lg" />
|
||||
<input
|
||||
v-model="query"
|
||||
class="h-12 card-shadow"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
type="text"
|
||||
:placeholder="`Search ${projectType}s...`"
|
||||
/>
|
||||
<Button v-if="query" class="r-btn" @click="() => clearSearch()">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<DropdownSelect
|
||||
v-slot="{ selected }"
|
||||
v-model="currentSortType"
|
||||
class="max-w-[16rem]"
|
||||
name="Sort by"
|
||||
:options="sortTypes as any"
|
||||
:display-name="(option: SortType | undefined) => option?.display"
|
||||
>
|
||||
<span class="font-semibold text-primary">Sort by: </span>
|
||||
<span class="font-semibold text-secondary">{{ selected }}</span>
|
||||
</DropdownSelect>
|
||||
<DropdownSelect
|
||||
v-slot="{ selected }"
|
||||
v-model="maxResults"
|
||||
name="Max results"
|
||||
:options="[5, 10, 15, 20, 50, 100]"
|
||||
class="max-w-[9rem]"
|
||||
>
|
||||
<span class="font-semibold text-primary">View: </span>
|
||||
<span class="font-semibold text-secondary">{{ selected }}</span>
|
||||
</DropdownSelect>
|
||||
<Pagination :page="currentPage" :count="pageCount" class="ml-auto" @switch-page="setPage" />
|
||||
</div>
|
||||
<SearchFilterControl
|
||||
v-model:selected-filters="currentFilters"
|
||||
:filters="filters.filter((f) => f.display !== 'none')"
|
||||
:provided-filters="instanceFilters"
|
||||
:overridden-provided-filter-types="overriddenProvidedFilterTypes"
|
||||
:provided-message="messages.providedByInstance"
|
||||
/>
|
||||
<div class="search">
|
||||
<section v-if="loading" class="offline">
|
||||
<LoadingIndicator />
|
||||
</section>
|
||||
<section v-else-if="offline && results.total_hits === 0" class="offline">
|
||||
You are currently offline. Connect to the internet to browse Modrinth!
|
||||
</section>
|
||||
<section v-else class="project-list display-mode--list instance-results" role="list">
|
||||
<SearchCard
|
||||
v-for="result in results.hits"
|
||||
:key="result?.project_id"
|
||||
:project="result"
|
||||
:instance="instance"
|
||||
:categories="[
|
||||
...categories.filter(
|
||||
(cat) =>
|
||||
result?.display_categories.includes(cat.name) && cat.project_type === projectType,
|
||||
),
|
||||
...loaders.filter(
|
||||
(loader) =>
|
||||
result?.display_categories.includes(loader.name) &&
|
||||
loader.supported_project_types?.includes(projectType),
|
||||
),
|
||||
]"
|
||||
:installed="result.installed || newlyInstalled.includes(result.project_id)"
|
||||
@install="
|
||||
(id) => {
|
||||
newlyInstalled.push(id)
|
||||
}
|
||||
"
|
||||
@contextmenu.prevent.stop="(event) => handleRightClick(event, result)"
|
||||
/>
|
||||
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
||||
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
||||
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
||||
</ContextMenu>
|
||||
</section>
|
||||
<div class="flex justify-end">
|
||||
<pagination
|
||||
:page="currentPage"
|
||||
:count="pageCount"
|
||||
class="pagination-after"
|
||||
@switch-page="setPage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user