mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-18 14:23:51 +02:00
756 lines
33 KiB
Vue
756 lines
33 KiB
Vue
<template>
|
|
<div class="x-container">
|
|
<div style="margin: 0 0 10px; display: flex; align-items: center">
|
|
<InputGroupField
|
|
:model-value="searchText"
|
|
:placeholder="t('view.search.search_placeholder')"
|
|
style="flex: 1"
|
|
clearable
|
|
@input="updateSearchText"
|
|
@keyup.enter="search" />
|
|
<TooltipWrapper side="bottom" :content="t('view.search.clear_results_tooltip')">
|
|
<Button class="rounded-full ml-2" size="icon" variant="ghost" @click="handleClearSearch"
|
|
><Trash2
|
|
/></Button>
|
|
</TooltipWrapper>
|
|
</div>
|
|
<TabsUnderline
|
|
v-model="activeSearchTab"
|
|
:items="searchTabs"
|
|
aria-label="Search tabs"
|
|
:unmount-on-hide="false"
|
|
style="margin-top: 15px">
|
|
<template #user>
|
|
<div v-loading="isSearchUserLoading" style="min-height: 60px">
|
|
<label class="inline-flex items-center gap-2" style="margin-left: 10px">
|
|
<Checkbox v-model="searchUserByBio" />
|
|
<span>{{ t('view.search.user.search_by_bio') }}</span>
|
|
</label>
|
|
<label class="inline-flex items-center gap-2" style="margin-left: 10px">
|
|
<Checkbox v-model="searchUserSortByLastLoggedIn" />
|
|
<span>{{ t('view.search.user.sort_by_last_logged_in') }}</span>
|
|
</label>
|
|
<div class="x-friend-list" style="min-height: 500px">
|
|
<div
|
|
v-for="user in searchUserResults"
|
|
:key="user.id"
|
|
class="x-friend-item"
|
|
@click="showUserDialog(user.id)">
|
|
<div class="avatar">
|
|
<img :src="userImage(user, true)" loading="lazy" />
|
|
</div>
|
|
<div class="detail">
|
|
<span class="name" v-text="user.displayName"></span>
|
|
<span
|
|
v-if="randomUserColours"
|
|
class="extra"
|
|
:class="user.$trustClass"
|
|
v-text="user.$trustLevel"></span>
|
|
<span
|
|
v-else
|
|
class="extra"
|
|
:style="{ color: user.$userColour }"
|
|
v-text="user.$trustLevel"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<ButtonGroup v-if="searchUserResults.length" style="margin-top: 15px">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
:disabled="!searchUserParams.offset"
|
|
@click="handleMoreSearchUser(-1)">
|
|
<ArrowLeft />
|
|
{{ t('view.search.prev_page') }}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
:disabled="searchUserResults.length < 10"
|
|
@click="handleMoreSearchUser(1)">
|
|
<ArrowRight />
|
|
{{ t('view.search.next_page') }}
|
|
</Button>
|
|
</ButtonGroup>
|
|
</div>
|
|
</template>
|
|
<template #world>
|
|
<div v-loading="isSearchWorldLoading" style="min-height: 60px">
|
|
<div class="inline-flex justify-between mb-4 w-full">
|
|
<Select
|
|
:model-value="searchWorldCategoryIndex"
|
|
@update:modelValue="handleSearchWorldCategorySelect"
|
|
style="margin-bottom: 15px">
|
|
<SelectTrigger size="sm">
|
|
<SelectValue :placeholder="t('view.search.world.category')" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectGroup>
|
|
<SelectItem
|
|
v-for="row in cachedConfig.dynamicWorldRows"
|
|
:key="row.index"
|
|
:value="row.index">
|
|
{{ row.name }}
|
|
</SelectItem>
|
|
</SelectGroup>
|
|
</SelectContent>
|
|
</Select>
|
|
<label class="inline-flex items-center gap-2" style="margin-left: 10px">
|
|
<Checkbox v-model="searchWorldLabs" />
|
|
<span>{{ t('view.search.world.community_lab') }}</span>
|
|
</label>
|
|
</div>
|
|
<div class="x-friend-list" style="min-height: 500px">
|
|
<div
|
|
v-for="world in searchWorldResults"
|
|
:key="world.id"
|
|
class="x-friend-item"
|
|
@click="showWorldDialog(world.id)">
|
|
<div class="avatar">
|
|
<img :src="world.thumbnailImageUrl" loading="lazy" />
|
|
</div>
|
|
<div class="detail">
|
|
<span class="name" v-text="world.name"></span>
|
|
<span v-if="world.occupants" class="extra"
|
|
>{{ world.authorName }} ({{ world.occupants }})</span
|
|
>
|
|
<span v-else class="extra" v-text="world.authorName"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<ButtonGroup v-if="searchWorldResults.length" style="margin-top: 15px">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
:disabled="!searchWorldParams.offset"
|
|
@click="moreSearchWorld(-1)">
|
|
<ArrowLeft />
|
|
{{ t('view.search.prev_page') }}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
:disabled="searchWorldResults.length < 10"
|
|
@click="moreSearchWorld(1)">
|
|
<ArrowRight />
|
|
{{ t('view.search.next_page') }}
|
|
</Button>
|
|
</ButtonGroup>
|
|
</div>
|
|
</template>
|
|
<template #avatar>
|
|
<div v-loading="isSearchAvatarLoading" style="min-height: 60px">
|
|
<div style="display: flex; align-items: center; justify-content: space-between">
|
|
<div style="display: flex; align-items: center">
|
|
<Select
|
|
v-if="avatarRemoteDatabaseProviderList.length > 1"
|
|
:model-value="avatarRemoteDatabaseProvider"
|
|
@update:modelValue="setAvatarProvider"
|
|
style="margin-right: 5px">
|
|
<SelectTrigger size="sm">
|
|
<SelectValue :placeholder="t('view.search.avatar.search_provider')" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectGroup>
|
|
<SelectItem
|
|
v-for="provider in avatarRemoteDatabaseProviderList"
|
|
:key="provider"
|
|
:value="provider">
|
|
{{ provider }}
|
|
</SelectItem>
|
|
</SelectGroup>
|
|
</SelectContent>
|
|
</Select>
|
|
<TooltipWrapper side="bottom" :content="t('view.search.avatar.refresh_tooltip')">
|
|
<Button
|
|
class="rounded-full ml-1"
|
|
variant="outline"
|
|
size="icon-sm"
|
|
:disabled="userDialog.isAvatarsLoading"
|
|
@click="refreshUserDialogAvatars">
|
|
<Spinner v-if="userDialog.isAvatarsLoading" />
|
|
<RefreshCw v-else />
|
|
</Button>
|
|
</TooltipWrapper>
|
|
<span style="font-size: 14px; margin-left: 5px; margin-right: 5px">{{
|
|
t('view.search.avatar.result_count', {
|
|
count: searchAvatarResults.length
|
|
})
|
|
}}</span>
|
|
</div>
|
|
<div style="display: flex; align-items: center">
|
|
<RadioGroup
|
|
:model-value="searchAvatarFilter"
|
|
class="flex items-center gap-4"
|
|
style="margin: 5px"
|
|
@update:modelValue="handleSearchAvatarFilterChange">
|
|
<div class="flex items-center space-x-2">
|
|
<RadioGroupItem id="searchAvatarFilter-all" value="all" />
|
|
<label for="searchAvatarFilter-all">{{ t('view.search.avatar.all') }}</label>
|
|
</div>
|
|
<div class="flex items-center space-x-2">
|
|
<RadioGroupItem id="searchAvatarFilter-public" value="public" />
|
|
<label for="searchAvatarFilter-public">{{ t('view.search.avatar.public') }}</label>
|
|
</div>
|
|
<div class="flex items-center space-x-2">
|
|
<RadioGroupItem id="searchAvatarFilter-private" value="private" />
|
|
<label for="searchAvatarFilter-private">{{
|
|
t('view.search.avatar.private')
|
|
}}</label>
|
|
</div>
|
|
</RadioGroup>
|
|
<Separator orientation="vertical" class="mx-2 h-5" />
|
|
<RadioGroup
|
|
:model-value="searchAvatarFilterRemote"
|
|
class="flex items-center gap-4"
|
|
style="margin: 5px"
|
|
@update:modelValue="handleSearchAvatarFilterRemoteChange">
|
|
<div class="flex items-center space-x-2">
|
|
<RadioGroupItem id="searchAvatarFilterRemote-all" value="all" />
|
|
<label for="searchAvatarFilterRemote-all">{{ t('view.search.avatar.all') }}</label>
|
|
</div>
|
|
<div class="flex items-center space-x-2">
|
|
<RadioGroupItem id="searchAvatarFilterRemote-local" value="local" />
|
|
<label for="searchAvatarFilterRemote-local">{{
|
|
t('view.search.avatar.local')
|
|
}}</label>
|
|
</div>
|
|
<div class="flex items-center space-x-2">
|
|
<RadioGroupItem
|
|
id="searchAvatarFilterRemote-remote"
|
|
value="remote"
|
|
:disabled="!avatarRemoteDatabase" />
|
|
<label for="searchAvatarFilterRemote-remote">{{
|
|
t('view.search.avatar.remote')
|
|
}}</label>
|
|
</div>
|
|
</RadioGroup>
|
|
</div>
|
|
</div>
|
|
<div style="display: flex; justify-content: end">
|
|
<RadioGroup
|
|
:model-value="searchAvatarSort"
|
|
:disabled="searchAvatarFilterRemote !== 'local'"
|
|
class="flex items-center gap-4"
|
|
style="margin: 5px"
|
|
@update:modelValue="handleSearchAvatarSortChange">
|
|
<div class="flex items-center space-x-2">
|
|
<RadioGroupItem id="searchAvatarSort-name" value="name" />
|
|
<label for="searchAvatarSort-name">{{ t('view.search.avatar.sort_name') }}</label>
|
|
</div>
|
|
<div class="flex items-center space-x-2">
|
|
<RadioGroupItem id="searchAvatarSort-update" value="update" />
|
|
<label for="searchAvatarSort-update">{{ t('view.search.avatar.sort_update') }}</label>
|
|
</div>
|
|
<div class="flex items-center space-x-2">
|
|
<RadioGroupItem id="searchAvatarSort-created" value="created" />
|
|
<label for="searchAvatarSort-created">{{ t('view.search.avatar.sort_created') }}</label>
|
|
</div>
|
|
</RadioGroup>
|
|
</div>
|
|
<div class="x-friend-list" style="margin-top: 20px; min-height: 500px">
|
|
<div
|
|
v-for="avatar in searchAvatarPage"
|
|
:key="avatar.id"
|
|
class="x-friend-item"
|
|
@click="showAvatarDialog(avatar.id)">
|
|
<div class="avatar">
|
|
<img v-if="avatar.thumbnailImageUrl" :src="avatar.thumbnailImageUrl" loading="lazy" />
|
|
<img v-else-if="avatar.imageUrl" :src="avatar.imageUrl" loading="lazy" />
|
|
</div>
|
|
<div class="detail">
|
|
<span class="name" v-text="avatar.name"></span>
|
|
<span
|
|
v-if="avatar.releaseStatus === 'public'"
|
|
class="extra"
|
|
v-text="avatar.releaseStatus"></span>
|
|
<span
|
|
v-else-if="avatar.releaseStatus === 'private'"
|
|
class="extra"
|
|
v-text="avatar.releaseStatus"></span>
|
|
<span v-else class="extra" v-text="avatar.releaseStatus"></span>
|
|
<span class="extra" v-text="avatar.authorName"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<ButtonGroup v-if="searchAvatarPage.length" style="margin-top: 15px">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
:disabled="!searchAvatarPageNum"
|
|
@click="moreSearchAvatar(-1)">
|
|
<ArrowLeft />
|
|
{{ t('view.search.prev_page') }}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
:disabled="
|
|
searchAvatarResults.length < 10 ||
|
|
(searchAvatarPageNum + 1) * 10 >= searchAvatarResults.length
|
|
"
|
|
@click="moreSearchAvatar(1)">
|
|
<ArrowRight />
|
|
{{ t('view.search.next_page') }}
|
|
</Button>
|
|
</ButtonGroup>
|
|
</div>
|
|
</template>
|
|
<template #group>
|
|
<div v-loading="isSearchGroupLoading" style="min-height: 60px">
|
|
<div class="x-friend-list" style="min-height: 500px">
|
|
<div
|
|
v-for="group in searchGroupResults"
|
|
:key="group.id"
|
|
class="x-friend-item"
|
|
@click="showGroupDialog(group.id)">
|
|
<div class="avatar">
|
|
<img :src="getSmallThumbnailUrl(group.iconUrl)" loading="lazy" />
|
|
</div>
|
|
<div class="detail">
|
|
<span class="name">
|
|
<span v-text="group.name"></span>
|
|
<span style="margin-left: 5px; font-weight: normal">({{ group.memberCount }})</span>
|
|
<span
|
|
style="
|
|
margin-left: 5px;
|
|
color: #909399;
|
|
font-weight: normal;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
"
|
|
>{{ group.shortCode }}.{{ group.discriminator }}</span
|
|
>
|
|
</span>
|
|
<span class="extra" v-text="group.description"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<ButtonGroup v-if="searchGroupResults.length" style="margin-top: 15px">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
:disabled="!searchGroupParams.offset"
|
|
@click="moreSearchGroup(-1)">
|
|
<ArrowLeft />
|
|
{{ t('view.search.prev_page') }}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
:disabled="searchGroupResults.length < 10"
|
|
@click="moreSearchGroup(1)">
|
|
<ArrowRight />
|
|
{{ t('view.search.next_page') }}
|
|
</Button>
|
|
</ButtonGroup>
|
|
</div>
|
|
</template>
|
|
</TabsUnderline>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { ArrowLeft, ArrowRight, RefreshCw, Trash2 } from 'lucide-vue-next';
|
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
|
import { computed, ref } from 'vue';
|
|
import { Button } from '@/components/ui/button';
|
|
import { ButtonGroup } from '@/components/ui/button-group';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import { InputGroupField } from '@/components/ui/input-group';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { Spinner } from '@/components/ui/spinner';
|
|
import { TabsUnderline } from '@/components/ui/tabs';
|
|
import { storeToRefs } from 'pinia';
|
|
import { useI18n } from 'vue-i18n';
|
|
|
|
import {
|
|
useAdvancedSettingsStore,
|
|
useAppearanceSettingsStore,
|
|
useAuthStore,
|
|
useAvatarProviderStore,
|
|
useAvatarStore,
|
|
useGroupStore,
|
|
useSearchStore,
|
|
useUserStore,
|
|
useWorldStore
|
|
} from '../../stores';
|
|
import {
|
|
compareByCreatedAt,
|
|
compareByName,
|
|
compareByUpdatedAt,
|
|
convertFileUrlToImageUrl,
|
|
replaceBioSymbols,
|
|
userImage
|
|
} from '../../shared/utils';
|
|
import { groupRequest, worldRequest } from '../../api';
|
|
|
|
const { randomUserColours } = storeToRefs(useAppearanceSettingsStore());
|
|
const { avatarRemoteDatabase } = storeToRefs(useAdvancedSettingsStore());
|
|
const { avatarRemoteDatabaseProviderList, avatarRemoteDatabaseProvider } = storeToRefs(useAvatarProviderStore());
|
|
const { setAvatarProvider } = useAvatarProviderStore();
|
|
const { userDialog } = storeToRefs(useUserStore());
|
|
const { showUserDialog, refreshUserDialogAvatars } = useUserStore();
|
|
const { showAvatarDialog, lookupAvatars, cachedAvatars } = useAvatarStore();
|
|
const { cachedWorlds, showWorldDialog } = useWorldStore();
|
|
const { showGroupDialog } = useGroupStore();
|
|
const { searchText, searchUserResults } = storeToRefs(useSearchStore());
|
|
const { clearSearch, moreSearchUser } = useSearchStore();
|
|
const { cachedConfig } = storeToRefs(useAuthStore());
|
|
|
|
const { t } = useI18n();
|
|
|
|
const activeSearchTab = ref('user');
|
|
const searchTabs = computed(() => [
|
|
{ value: 'user', label: t('view.search.user.header') },
|
|
{ value: 'world', label: t('view.search.world.header') },
|
|
{ value: 'avatar', label: t('view.search.avatar.header') },
|
|
{ value: 'group', label: t('view.search.group.header') }
|
|
]);
|
|
|
|
const searchUserParams = ref({});
|
|
const searchUserByBio = ref(false);
|
|
const searchUserSortByLastLoggedIn = ref(false);
|
|
|
|
const isSearchUserLoading = ref(false);
|
|
const isSearchWorldLoading = ref(false);
|
|
const isSearchAvatarLoading = ref(false);
|
|
const isSearchGroupLoading = ref(false);
|
|
|
|
const searchWorldOption = ref('');
|
|
const searchWorldLabs = ref(false);
|
|
const searchWorldParams = ref({});
|
|
|
|
const searchWorldCategoryIndex = ref(null);
|
|
const searchWorldResults = ref([]);
|
|
|
|
function handleSearchAvatarFilterChange(value) {
|
|
searchAvatarFilter.value = value;
|
|
searchAvatar();
|
|
}
|
|
|
|
function handleSearchAvatarFilterRemoteChange(value) {
|
|
searchAvatarFilterRemote.value = value;
|
|
searchAvatar();
|
|
}
|
|
|
|
function handleSearchAvatarSortChange(value) {
|
|
searchAvatarSort.value = value;
|
|
searchAvatar();
|
|
}
|
|
|
|
const searchAvatarFilter = ref('');
|
|
const searchAvatarSort = ref('');
|
|
const searchAvatarFilterRemote = ref('');
|
|
const searchAvatarPageNum = ref(0);
|
|
const searchAvatarResults = ref([]);
|
|
const searchAvatarPage = ref([]);
|
|
|
|
const searchGroupParams = ref({});
|
|
const searchGroupResults = ref([]);
|
|
|
|
function getSmallThumbnailUrl(url) {
|
|
return convertFileUrlToImageUrl(url);
|
|
}
|
|
|
|
function handleClearSearch() {
|
|
searchUserParams.value = {};
|
|
searchWorldParams.value = {};
|
|
searchWorldResults.value = [];
|
|
searchAvatarResults.value = [];
|
|
searchAvatarPage.value = [];
|
|
searchAvatarPageNum.value = 0;
|
|
searchGroupParams.value = {};
|
|
searchGroupResults.value = [];
|
|
clearSearch();
|
|
}
|
|
|
|
function updateSearchText(text) {
|
|
searchText.value = text;
|
|
}
|
|
|
|
function handleSearchTabChange(tabName) {
|
|
searchText.value = '';
|
|
activeSearchTab.value = tabName;
|
|
}
|
|
|
|
function search() {
|
|
switch (activeSearchTab.value) {
|
|
case 'user':
|
|
searchUser();
|
|
break;
|
|
case 'world':
|
|
searchWorld({});
|
|
break;
|
|
case 'avatar':
|
|
searchAvatar();
|
|
break;
|
|
case 'group':
|
|
searchGroup();
|
|
break;
|
|
}
|
|
}
|
|
|
|
async function searchUser() {
|
|
searchUserParams.value = {
|
|
n: 10,
|
|
offset: 0,
|
|
search: searchText.value,
|
|
customFields: searchUserByBio.value ? 'bio' : 'displayName',
|
|
sort: searchUserSortByLastLoggedIn.value ? 'last_login' : 'relevance'
|
|
};
|
|
await handleMoreSearchUser();
|
|
}
|
|
|
|
async function handleMoreSearchUser(go = null) {
|
|
isSearchUserLoading.value = true;
|
|
await moreSearchUser(go, searchUserParams.value);
|
|
isSearchUserLoading.value = false;
|
|
}
|
|
|
|
function searchWorld(ref) {
|
|
searchWorldOption.value = '';
|
|
searchWorldCategoryIndex.value = ref?.index ?? null;
|
|
const params = {
|
|
n: 10,
|
|
offset: 0
|
|
};
|
|
switch (ref.sortHeading) {
|
|
case 'featured':
|
|
params.sort = 'order';
|
|
params.featured = 'true';
|
|
break;
|
|
case 'trending':
|
|
params.sort = 'popularity';
|
|
params.featured = 'false';
|
|
break;
|
|
case 'updated':
|
|
params.sort = 'updated';
|
|
break;
|
|
case 'created':
|
|
params.sort = 'created';
|
|
break;
|
|
case 'publication':
|
|
params.sort = 'publicationDate';
|
|
break;
|
|
case 'shuffle':
|
|
params.sort = 'shuffle';
|
|
break;
|
|
case 'active':
|
|
searchWorldOption.value = 'active';
|
|
break;
|
|
case 'recent':
|
|
searchWorldOption.value = 'recent';
|
|
break;
|
|
case 'favorite':
|
|
searchWorldOption.value = 'favorites';
|
|
break;
|
|
case 'labs':
|
|
params.sort = 'labsPublicationDate';
|
|
break;
|
|
case 'heat':
|
|
params.sort = 'heat';
|
|
params.featured = 'false';
|
|
break;
|
|
default:
|
|
params.sort = 'relevance';
|
|
params.search = replaceBioSymbols(searchText.value);
|
|
break;
|
|
}
|
|
params.order = ref.sortOrder || 'descending';
|
|
if (ref.sortOwnership === 'mine') {
|
|
params.user = 'me';
|
|
params.releaseStatus = 'all';
|
|
}
|
|
if (ref.tag) {
|
|
params.tag = ref.tag;
|
|
}
|
|
if (!searchWorldLabs.value) {
|
|
if (params.tag) {
|
|
params.tag += ',system_approved';
|
|
} else {
|
|
params.tag = 'system_approved';
|
|
}
|
|
}
|
|
// TODO: option.platform
|
|
searchWorldParams.value = params;
|
|
moreSearchWorld();
|
|
}
|
|
|
|
function handleSearchWorldCategorySelect(index) {
|
|
searchWorldCategoryIndex.value = index;
|
|
const row = cachedConfig.value?.dynamicWorldRows?.find((r) => r.index === index);
|
|
searchWorld(row || {});
|
|
}
|
|
|
|
function moreSearchWorld(go) {
|
|
const params = searchWorldParams.value;
|
|
if (go) {
|
|
params.offset += params.n * go;
|
|
if (params.offset < 0) {
|
|
params.offset = 0;
|
|
}
|
|
}
|
|
isSearchWorldLoading.value = true;
|
|
worldRequest
|
|
.getWorlds(params, searchWorldOption.value)
|
|
.finally(() => {
|
|
isSearchWorldLoading.value = false;
|
|
})
|
|
.then((args) => {
|
|
const map = new Map();
|
|
for (const json of args.json) {
|
|
const ref = cachedWorlds.get(json.id);
|
|
if (typeof ref !== 'undefined') {
|
|
map.set(ref.id, ref);
|
|
}
|
|
}
|
|
searchWorldResults.value = Array.from(map.values());
|
|
return args;
|
|
});
|
|
}
|
|
|
|
async function searchAvatar() {
|
|
let ref;
|
|
isSearchAvatarLoading.value = true;
|
|
if (!searchAvatarFilter.value) {
|
|
searchAvatarFilter.value = 'all';
|
|
}
|
|
if (!searchAvatarSort.value) {
|
|
searchAvatarSort.value = 'name';
|
|
}
|
|
if (!searchAvatarFilterRemote.value) {
|
|
searchAvatarFilterRemote.value = 'all';
|
|
}
|
|
if (searchAvatarFilterRemote.value !== 'local') {
|
|
searchAvatarSort.value = 'name';
|
|
}
|
|
const avatars = new Map();
|
|
const query = searchText.value;
|
|
const queryUpper = query.toUpperCase();
|
|
if (!query) {
|
|
for (ref of cachedAvatars.values()) {
|
|
switch (searchAvatarFilter.value) {
|
|
case 'all':
|
|
avatars.set(ref.id, ref);
|
|
break;
|
|
case 'public':
|
|
if (ref.releaseStatus === 'public') {
|
|
avatars.set(ref.id, ref);
|
|
}
|
|
break;
|
|
case 'private':
|
|
if (ref.releaseStatus === 'private') {
|
|
avatars.set(ref.id, ref);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
isSearchAvatarLoading.value = false;
|
|
} else {
|
|
if (searchAvatarFilterRemote.value === 'all' || searchAvatarFilterRemote.value === 'local') {
|
|
for (ref of cachedAvatars.values()) {
|
|
let match = ref.name.toUpperCase().includes(queryUpper);
|
|
if (!match && ref.description) {
|
|
match = ref.description.toUpperCase().includes(queryUpper);
|
|
}
|
|
if (!match && ref.authorName) {
|
|
match = ref.authorName.toUpperCase().includes(queryUpper);
|
|
}
|
|
if (match) {
|
|
switch (searchAvatarFilter.value) {
|
|
case 'all':
|
|
avatars.set(ref.id, ref);
|
|
break;
|
|
case 'public':
|
|
if (ref.releaseStatus === 'public') {
|
|
avatars.set(ref.id, ref);
|
|
}
|
|
break;
|
|
case 'private':
|
|
if (ref.releaseStatus === 'private') {
|
|
avatars.set(ref.id, ref);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (
|
|
(searchAvatarFilterRemote.value === 'all' || searchAvatarFilterRemote.value === 'remote') &&
|
|
avatarRemoteDatabase.value &&
|
|
query.length >= 3
|
|
) {
|
|
const data = await lookupAvatars('search', query);
|
|
if (data && typeof data === 'object') {
|
|
data.forEach((avatar) => {
|
|
avatars.set(avatar.id, avatar);
|
|
});
|
|
}
|
|
}
|
|
isSearchAvatarLoading.value = false;
|
|
}
|
|
const avatarsArray = Array.from(avatars.values());
|
|
if (searchAvatarFilterRemote.value === 'local') {
|
|
switch (searchAvatarSort.value) {
|
|
case 'updated':
|
|
avatarsArray.sort(compareByUpdatedAt);
|
|
break;
|
|
case 'created':
|
|
avatarsArray.sort(compareByCreatedAt);
|
|
break;
|
|
case 'name':
|
|
avatarsArray.sort(compareByName);
|
|
break;
|
|
}
|
|
}
|
|
searchAvatarPageNum.value = 0;
|
|
searchAvatarResults.value = avatarsArray;
|
|
searchAvatarPage.value = avatarsArray.slice(0, 10);
|
|
}
|
|
function moreSearchAvatar(n) {
|
|
let offset;
|
|
if (n === -1) {
|
|
searchAvatarPageNum.value--;
|
|
offset = searchAvatarPageNum.value * 10;
|
|
}
|
|
if (n === 1) {
|
|
searchAvatarPageNum.value++;
|
|
offset = searchAvatarPageNum.value * 10;
|
|
}
|
|
searchAvatarPage.value = searchAvatarResults.value.slice(offset, offset + 10);
|
|
}
|
|
async function searchGroup() {
|
|
searchGroupParams.value = {
|
|
n: 10,
|
|
offset: 0,
|
|
query: replaceBioSymbols(searchText.value)
|
|
};
|
|
await moreSearchGroup();
|
|
}
|
|
async function moreSearchGroup(go) {
|
|
const params = searchGroupParams.value;
|
|
if (go) {
|
|
params.offset += params.n * go;
|
|
if (params.offset < 0) {
|
|
params.offset = 0;
|
|
}
|
|
}
|
|
isSearchGroupLoading.value = true;
|
|
await groupRequest
|
|
.groupSearch(params)
|
|
.finally(() => {
|
|
isSearchGroupLoading.value = false;
|
|
})
|
|
.then((args) => {
|
|
const map = new Map();
|
|
for (const json of args.json) {
|
|
map.set(json.id, json);
|
|
}
|
|
searchGroupResults.value = Array.from(map.values());
|
|
return args;
|
|
});
|
|
}
|
|
</script>
|