You've already forked AstralRinth
forked from didirus/AstralRinth
155f4091a6
* Tweak search sorting * Tweak search sorting * fix ping impl * remove port field, add server regions * fix compile * fix tests * update frontend banner upload size limit * feat: use server project region instead of country * remove java and bedrock port in frontend * add helper text * allow filtering by if server is online * add server status online offline filter * use region in instance * pre-collapse status in app discovery * pnpm prepr * remove server discovery flag * add servers into mobile nav tabs * parse port from address if present --------- Co-authored-by: tdgao <mr.trumgao@gmail.com>
379 lines
10 KiB
TypeScript
379 lines
10 KiB
TypeScript
import { getCategoryIcon } from '@modrinth/assets'
|
|
import { computed, type Ref, ref } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
|
|
import { useVIntl } from '../composables/i18n'
|
|
import type { FilterType, FilterValue, SortType, Tags } from './search'
|
|
import { formatCategory, formatCategoryHeader } from './tag-messages'
|
|
|
|
const SERVER_CATEGORY_ICON_MAP: Record<string, string> = {
|
|
'adventure-mode': 'compass',
|
|
anarchy: 'skull',
|
|
'battle-royale': 'target',
|
|
bedwars: 'bed-double',
|
|
bosses: 'crown',
|
|
classes: 'badge',
|
|
competitive: 'trophy',
|
|
'creative-mode': 'palette',
|
|
'creator-community': 'clapperboard',
|
|
crossplay: 'gamepad-2',
|
|
'custom-content': 'blocks',
|
|
dungeons: 'castle',
|
|
factions: 'flag',
|
|
gens: 'pickaxe',
|
|
'hardcore-mode': 'heart-crack',
|
|
'keep-inventory': 'backpack',
|
|
kitpvp: 'sword',
|
|
lifesteal: 'heart-pulse',
|
|
media: 'film',
|
|
microgames: 'grid-3x3',
|
|
minigames: 'dices',
|
|
mmo: 'globe',
|
|
network: 'network',
|
|
'offline-mode': 'wifi-off',
|
|
oneblock: 'square',
|
|
op: 'zap',
|
|
parkour: 'footprints',
|
|
'personal-worlds': 'house',
|
|
plots: 'map-pinned',
|
|
pokemon: 'paw-print',
|
|
prison: 'lock',
|
|
pve: 'shield',
|
|
pvp: 'swords',
|
|
questing: 'scroll-text',
|
|
racing: 'gauge',
|
|
'recording-smp': 'camera',
|
|
roleplay: 'theater',
|
|
rpg: 'wand-sparkles',
|
|
skyblock: 'cloud',
|
|
smp: 'users',
|
|
'survival-mode': 'tree-pine',
|
|
teams: 'handshake',
|
|
technical: 'terminal',
|
|
towns: 'building-2',
|
|
whitelisted: 'badge-check',
|
|
'world-resets': 'refresh-ccw',
|
|
}
|
|
|
|
export const SERVER_REGIONS = [
|
|
{ code: 'us_east', name: 'US East' },
|
|
{ code: 'us_west', name: 'US West' },
|
|
{ code: 'europe', name: 'Europe' },
|
|
{ code: 'asia', name: 'Asia' },
|
|
{ code: 'australia', name: 'Australia' },
|
|
{ code: 'south_america', name: 'South America' },
|
|
{ code: 'middle_east', name: 'Middle East' },
|
|
{ code: 'russia', name: 'Russia' },
|
|
]
|
|
|
|
export const SERVER_LANGUAGES = [
|
|
{ code: 'en', name: 'English' },
|
|
{ code: 'de', name: 'German' },
|
|
{ code: 'fr', name: 'French' },
|
|
{ code: 'es', name: 'Spanish' },
|
|
{ code: 'pt', name: 'Portuguese' },
|
|
{ code: 'ru', name: 'Russian' },
|
|
{ code: 'zh', name: 'Chinese' },
|
|
{ code: 'ja', name: 'Japanese' },
|
|
{ code: 'ko', name: 'Korean' },
|
|
{ code: 'nl', name: 'Dutch' },
|
|
{ code: 'pl', name: 'Polish' },
|
|
{ code: 'it', name: 'Italian' },
|
|
{ code: 'tr', name: 'Turkish' },
|
|
{ code: 'sv', name: 'Swedish' },
|
|
{ code: 'fi', name: 'Finnish' },
|
|
]
|
|
|
|
export const SERVER_SORT_TYPES: SortType[] = [
|
|
{ display: 'Relevance', name: 'relevance' },
|
|
{ display: 'Verified Plays', name: 'minecraft_java_server.verified_plays_2w' },
|
|
{ display: 'Players', name: 'minecraft_java_server.ping.data.players_online' },
|
|
{ display: 'Followers', name: 'follows' },
|
|
{ display: 'Date Published', name: 'date_created' },
|
|
{ display: 'Date Updated', name: 'date_modified' },
|
|
]
|
|
|
|
const FILTER_FIELD_MAP: Record<string, string> = {
|
|
server_content_type: 'minecraft_java_server.content.kind',
|
|
server_game_version: 'minecraft_java_server.content.supported_game_versions',
|
|
server_status: 'minecraft_java_server.ping.data',
|
|
server_region: 'minecraft_server.region',
|
|
server_language: 'minecraft_server.languages',
|
|
}
|
|
|
|
function getFilterField(filterId: string): string | undefined {
|
|
if (filterId.startsWith('server_category_')) return 'categories'
|
|
return FILTER_FIELD_MAP[filterId]
|
|
}
|
|
|
|
export function useServerSearch(opts: {
|
|
tags: Ref<Tags>
|
|
query: Ref<string>
|
|
maxResults: Ref<number>
|
|
currentPage: Ref<number>
|
|
}) {
|
|
const { tags, query, maxResults, currentPage } = opts
|
|
|
|
const { formatMessage } = useVIntl()
|
|
|
|
const route = useRoute()
|
|
|
|
const serverCurrentSortType = ref<SortType>(SERVER_SORT_TYPES[0])
|
|
const serverCurrentFilters = ref<FilterValue[]>([{ type: 'server_status', option: 'online' }])
|
|
const serverToggledGroups = ref<string[]>([])
|
|
|
|
const serverFilterTypes = computed<FilterType[]>(() => {
|
|
const categoryFilters: Record<string, FilterType> = {}
|
|
for (const c of (tags.value?.categories ?? []).filter(
|
|
(c) => c.project_type === 'minecraft_java_server',
|
|
)) {
|
|
const filterTypeId = `server_category_${c.header}`
|
|
if (!categoryFilters[filterTypeId]) {
|
|
categoryFilters[filterTypeId] = {
|
|
id: filterTypeId,
|
|
formatted_name: formatCategoryHeader(formatMessage, c.header),
|
|
supported_project_types: ['server'],
|
|
display: 'all',
|
|
query_param: 'sc',
|
|
supports_negative_filter: true,
|
|
searchable: false,
|
|
options: [],
|
|
}
|
|
}
|
|
categoryFilters[filterTypeId].options.push({
|
|
id: c.name,
|
|
formatted_name: formatCategory(formatMessage, c.name),
|
|
icon: getCategoryIcon(SERVER_CATEGORY_ICON_MAP[c.name] ?? c.name),
|
|
method: 'or' as const,
|
|
value: c.name,
|
|
})
|
|
}
|
|
|
|
const featuresFilter = categoryFilters['server_category_minecraft_server_features']
|
|
if (featuresFilter) {
|
|
featuresFilter.options.sort((a, b) => {
|
|
if (a.id === 'pokemon') return -1
|
|
if (b.id === 'pokemon') return 1
|
|
return 0
|
|
})
|
|
}
|
|
|
|
return [
|
|
{
|
|
id: 'server_content_type',
|
|
formatted_name: 'Type',
|
|
supported_project_types: ['server'],
|
|
display: 'all',
|
|
query_param: 'sct',
|
|
supports_negative_filter: false,
|
|
searchable: false,
|
|
options: [
|
|
{ id: 'vanilla', formatted_name: 'Vanilla', method: 'or', value: 'vanilla' },
|
|
{ id: 'modpack', formatted_name: 'Modded', method: 'or', value: 'modpack' },
|
|
],
|
|
},
|
|
...[
|
|
'minecraft_server_features',
|
|
'minecraft_server_gameplay',
|
|
'minecraft_server_meta',
|
|
'minecraft_server_community',
|
|
]
|
|
.map((h) => categoryFilters[`server_category_${h}`])
|
|
.filter(Boolean),
|
|
{
|
|
id: 'server_game_version',
|
|
formatted_name: 'Game Version',
|
|
supported_project_types: ['server'],
|
|
display: 'scrollable',
|
|
query_param: 'sgv',
|
|
supports_negative_filter: false,
|
|
searchable: true,
|
|
options: (tags.value?.gameVersions ?? []).map((gv) => ({
|
|
id: gv.version,
|
|
toggle_group: gv.version_type !== 'release' ? 'all_versions' : undefined,
|
|
method: 'or' as const,
|
|
value: gv.version,
|
|
query_value: gv.version,
|
|
})),
|
|
},
|
|
{
|
|
id: 'server_region',
|
|
formatted_name: 'Region',
|
|
supported_project_types: ['server'],
|
|
display: 'all',
|
|
query_param: 'sr',
|
|
supports_negative_filter: true,
|
|
searchable: false,
|
|
options: SERVER_REGIONS.map((r) => ({
|
|
id: r.code,
|
|
formatted_name: r.name,
|
|
method: 'or' as const,
|
|
value: r.code,
|
|
})),
|
|
},
|
|
{
|
|
id: 'server_language',
|
|
formatted_name: 'Language',
|
|
supported_project_types: ['server'],
|
|
display: 'scrollable',
|
|
query_param: 'sl',
|
|
supports_negative_filter: false,
|
|
searchable: true,
|
|
options: SERVER_LANGUAGES.map((l) => ({
|
|
id: l.code,
|
|
formatted_name: l.name,
|
|
method: 'or' as const,
|
|
value: l.code,
|
|
})),
|
|
},
|
|
{
|
|
id: 'server_status',
|
|
formatted_name: 'Status',
|
|
supported_project_types: ['server'],
|
|
display: 'all',
|
|
query_param: 'sst',
|
|
supports_negative_filter: false,
|
|
searchable: false,
|
|
options: [
|
|
{ id: 'online', formatted_name: 'Online', method: 'or', value: 'online' },
|
|
{ id: 'offline', formatted_name: 'Offline', method: 'or', value: 'offline' },
|
|
],
|
|
},
|
|
]
|
|
})
|
|
|
|
const newFilters = computed(() => {
|
|
const parts = ['project_types = minecraft_java_server']
|
|
|
|
for (const filterType of serverFilterTypes.value) {
|
|
const field = getFilterField(filterType.id)
|
|
if (!field) continue
|
|
const matched = serverCurrentFilters.value.filter((f) => f.type === filterType.id)
|
|
if (matched.length === 0) continue
|
|
|
|
if (filterType.id === 'server_status') {
|
|
const selected = matched[0]?.option
|
|
if (selected === 'online') {
|
|
parts.push(`${field} EXISTS`)
|
|
} else if (selected === 'offline') {
|
|
parts.push(`${field} NOT EXISTS`)
|
|
}
|
|
continue
|
|
}
|
|
|
|
const included = matched.filter((f) => !f.negative)
|
|
const excluded = matched.filter((f) => f.negative)
|
|
if (included.length > 0) {
|
|
const values = included.map((f) => `"${f.option}"`).join(', ')
|
|
parts.push(`${field} IN [${values}]`)
|
|
}
|
|
if (excluded.length > 0) {
|
|
const values = excluded.map((f) => `"${f.option}"`).join(', ')
|
|
parts.push(`${field} NOT IN [${values}]`)
|
|
}
|
|
}
|
|
|
|
return parts.join(' AND ')
|
|
})
|
|
|
|
const serverRequestParams = computed(() => {
|
|
const params = [`limit=${maxResults.value}`, `index=${serverCurrentSortType.value.name}`]
|
|
if (query.value) params.push(`query=${encodeURIComponent(query.value)}`)
|
|
const offset = (currentPage.value - 1) * maxResults.value
|
|
if (offset > 0) params.push(`offset=${offset}`)
|
|
params.push(`new_filters=${encodeURIComponent(newFilters.value)}`)
|
|
return `?${params.join('&')}`
|
|
})
|
|
|
|
function readServerQueryParams() {
|
|
const q = route.query
|
|
|
|
if (q.q) {
|
|
query.value = String(q.q)
|
|
}
|
|
|
|
if (q.ss) {
|
|
serverCurrentSortType.value =
|
|
SERVER_SORT_TYPES.find((s) => s.name === String(q.ss)) ?? SERVER_SORT_TYPES[0]
|
|
}
|
|
|
|
if (q.m) {
|
|
maxResults.value = Number(q.m)
|
|
}
|
|
|
|
if (q.page) {
|
|
currentPage.value = Number(q.page)
|
|
}
|
|
|
|
for (const filterType of serverFilterTypes.value) {
|
|
const paramValue = q[filterType.query_param]
|
|
if (!paramValue) continue
|
|
|
|
const values =
|
|
typeof paramValue === 'string'
|
|
? [paramValue]
|
|
: paramValue.filter((v): v is string => v !== null)
|
|
|
|
for (const value of values) {
|
|
const isNegative = value.startsWith('!')
|
|
const cleanValue = isNegative ? value.slice(1) : value
|
|
const option = filterType.options.find((o) => o.id === cleanValue)
|
|
if (option) {
|
|
serverCurrentFilters.value.push({
|
|
type: filterType.id,
|
|
option: option.id,
|
|
negative: isNegative,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function createServerPageParams(): Record<string, string | string[]> {
|
|
const items: Record<string, string[]> = {}
|
|
|
|
if (query.value) {
|
|
items.q = [query.value]
|
|
}
|
|
|
|
for (const filterValue of serverCurrentFilters.value) {
|
|
const type = serverFilterTypes.value.find((t) => t.id === filterValue.type)
|
|
if (type) {
|
|
const value = filterValue.negative ? `!${filterValue.option}` : filterValue.option
|
|
if (items[type.query_param]) {
|
|
items[type.query_param].push(value)
|
|
} else {
|
|
items[type.query_param] = [value]
|
|
}
|
|
}
|
|
}
|
|
|
|
if (serverCurrentSortType.value.name !== 'relevance') {
|
|
items.ss = [serverCurrentSortType.value.name]
|
|
}
|
|
|
|
if (maxResults.value !== 20) {
|
|
items.m = [String(maxResults.value)]
|
|
}
|
|
|
|
if (currentPage.value > 1) {
|
|
items.page = [String(currentPage.value)]
|
|
}
|
|
|
|
return items
|
|
}
|
|
|
|
readServerQueryParams()
|
|
|
|
return {
|
|
serverCurrentSortType,
|
|
serverCurrentFilters,
|
|
serverToggledGroups,
|
|
serverSortTypes: SERVER_SORT_TYPES,
|
|
serverFilterTypes,
|
|
newFilters,
|
|
serverRequestParams,
|
|
createServerPageParams,
|
|
}
|
|
}
|