feat: add quick search

This commit is contained in:
pa
2026-03-05 22:20:07 +09:00
parent b570de6d4a
commit fb6358b3be
13 changed files with 1411 additions and 106 deletions
+87 -4
View File
@@ -15,6 +15,7 @@ import {
import { avatarRequest, miscRequest } from '../api';
import { AppDebug } from '../service/appConfig';
import { database } from '../service/database';
import { processBulk } from '../service/request';
import { useAdvancedSettingsStore } from './settings/advanced';
import { useAvatarProviderStore } from './avatarProvider';
import { useFavoriteStore } from './favorite';
@@ -79,15 +80,16 @@ export const useAvatarStore = defineStore('Avatar', () => {
avatarHistory.value = [];
if (isLoggedIn) {
getAvatarHistory();
preloadOwnAvatars();
}
},
{ flush: 'sync' }
);
/**
/ * @param {object} json
/ * @returns {object} ref
*/
* @param {object} json
* @returns {object} ref
*/
function applyAvatar(json) {
json.name = replaceBioSymbols(json.name);
json.description = replaceBioSymbols(json.description);
@@ -332,6 +334,9 @@ export const useAvatarStore = defineStore('Avatar', () => {
return ref;
}
/**
*
*/
function updateVRChatAvatarCache() {
const D = avatarDialog.value;
if (D.visible) {
@@ -398,11 +403,17 @@ export const useAvatarStore = defineStore('Avatar', () => {
});
}
/**
*
*/
function clearAvatarHistory() {
avatarHistory.value = [];
database.clearAvatarHistory();
}
/**
*
*/
function promptClearAvatarHistory() {
modalStore
.confirm({
@@ -444,6 +455,11 @@ export const useAvatarStore = defineStore('Avatar', () => {
}
}
/**
*
* @param type
* @param search
*/
async function lookupAvatars(type, search) {
const avatars = new Map();
if (type === 'search') {
@@ -507,6 +523,11 @@ export const useAvatarStore = defineStore('Avatar', () => {
return avatars;
}
/**
*
* @param authorId
* @param fileId
*/
async function lookupAvatarByImageFileId(authorId, fileId) {
for (const providerUrl of avatarProviderStore.avatarRemoteDatabaseProviderList) {
const avatar = await lookupAvatarByFileId(providerUrl, fileId);
@@ -529,6 +550,11 @@ export const useAvatarStore = defineStore('Avatar', () => {
return null;
}
/**
*
* @param providerUrl
* @param fileId
*/
async function lookupAvatarByFileId(providerUrl, fileId) {
try {
const url = `${providerUrl}?fileId=${encodeURIComponent(fileId)}`;
@@ -568,6 +594,11 @@ export const useAvatarStore = defineStore('Avatar', () => {
}
}
/**
*
* @param providerUrl
* @param authorId
*/
async function lookupAvatarsByAuthor(providerUrl, authorId) {
const avatars = [];
if (!providerUrl || !authorId) {
@@ -615,6 +646,10 @@ export const useAvatarStore = defineStore('Avatar', () => {
return avatars;
}
/**
*
* @param id
*/
function selectAvatarWithConfirmation(id) {
modalStore
.confirm({
@@ -628,6 +663,10 @@ export const useAvatarStore = defineStore('Avatar', () => {
.catch(() => {});
}
/**
*
* @param id
*/
async function selectAvatarWithoutConfirmation(id) {
if (userStore.currentUser.currentAvatar === id) {
toast.info('Avatar already selected');
@@ -642,6 +681,10 @@ export const useAvatarStore = defineStore('Avatar', () => {
});
}
/**
*
* @param fileId
*/
function checkAvatarCache(fileId) {
let avatarId = '';
for (let ref of cachedAvatars.values()) {
@@ -652,6 +695,11 @@ export const useAvatarStore = defineStore('Avatar', () => {
return avatarId;
}
/**
*
* @param fileId
* @param ownerUserId
*/
async function checkAvatarCacheRemote(fileId, ownerUserId) {
if (advancedSettingsStore.avatarRemoteDatabase) {
try {
@@ -673,6 +721,12 @@ export const useAvatarStore = defineStore('Avatar', () => {
return null;
}
/**
*
* @param refUserId
* @param ownerUserId
* @param currentAvatarImageUrl
*/
async function showAvatarAuthorDialog(
refUserId,
ownerUserId,
@@ -712,6 +766,10 @@ export const useAvatarStore = defineStore('Avatar', () => {
}
}
/**
*
* @param avatarId
*/
function addAvatarWearTime(avatarId) {
if (!userStore.currentUser.$previousAvatarSwapTime || !avatarId) {
return;
@@ -721,6 +779,30 @@ export const useAvatarStore = defineStore('Avatar', () => {
database.addAvatarTimeSpent(avatarId, timeSpent);
}
/**
* Preload all own avatars into cache at startup for global search.
*/
async function preloadOwnAvatars() {
const params = {
n: 50,
offset: 0,
sort: 'updated',
order: 'descending',
releaseStatus: 'all',
user: 'me'
};
await processBulk({
fn: avatarRequest.getAvatars,
N: -1,
params,
handle: (args) => {
for (const json of args.json) {
applyAvatar(json);
}
}
});
}
return {
avatarDialog,
avatarHistory,
@@ -741,6 +823,7 @@ export const useAvatarStore = defineStore('Avatar', () => {
selectAvatarWithConfirmation,
selectAvatarWithoutConfirmation,
showAvatarAuthorDialog,
addAvatarWearTime
addAvatarWearTime,
preloadOwnAvatars
};
});
+192
View File
@@ -0,0 +1,192 @@
import { computed, ref, watch } from 'vue';
import { defineStore } from 'pinia';
import {
searchAvatars,
searchFavoriteAvatars,
searchFavoriteWorlds,
searchFriends,
searchGroups,
searchWorlds
} from '../shared/utils/globalSearchUtils';
import { useAvatarStore } from './avatar';
import { useFavoriteStore } from './favorite';
import { useFriendStore } from './friend';
import { useGroupStore } from './group';
import { useUserStore } from './user';
import { useWorldStore } from './world';
export const useGlobalSearchStore = defineStore('GlobalSearch', () => {
const friendStore = useFriendStore();
const favoriteStore = useFavoriteStore();
const avatarStore = useAvatarStore();
const worldStore = useWorldStore();
const groupStore = useGroupStore();
const userStore = useUserStore();
const isOpen = ref(false);
const query = ref('');
const stringComparer = computed(
() =>
new Intl.Collator(undefined, {
usage: 'search',
sensitivity: 'base'
})
);
// Reset query when dialog closes
watch(isOpen, (open) => {
if (!open) {
query.value = '';
}
});
const currentUserId = computed(() => userStore.currentUser?.id);
const friendResults = computed(() => {
if (!query.value || query.value.length < 2) return [];
return searchFriends(
query.value,
friendStore.friends,
stringComparer.value
);
});
// Own avatars (filter cachedAvatars by authorId)
const ownAvatarResults = computed(() => {
if (!query.value || query.value.length < 2) return [];
return searchAvatars(
query.value,
avatarStore.cachedAvatars,
stringComparer.value,
currentUserId.value
);
});
// Favorite avatars (from favoriteStore, deduplicated against own)
const favoriteAvatarResults = computed(() => {
if (!query.value || query.value.length < 2) return [];
const favResults = searchFavoriteAvatars(
query.value,
favoriteStore.favoriteAvatars,
stringComparer.value
);
// Deduplicate: remove items already in ownAvatarResults
const ownIds = new Set(ownAvatarResults.value.map((r) => r.id));
return favResults.filter((r) => !ownIds.has(r.id));
});
// Own worlds (filter cachedWorlds by authorId)
const ownWorldResults = computed(() => {
if (!query.value || query.value.length < 2) return [];
return searchWorlds(
query.value,
worldStore.cachedWorlds,
stringComparer.value,
currentUserId.value
);
});
// Favorite worlds (from favoriteStore, deduplicated against own)
const favoriteWorldResults = computed(() => {
if (!query.value || query.value.length < 2) return [];
const favResults = searchFavoriteWorlds(
query.value,
favoriteStore.favoriteWorlds,
stringComparer.value
);
// Deduplicate: remove items already in ownWorldResults
const ownIds = new Set(ownWorldResults.value.map((r) => r.id));
return favResults.filter((r) => !ownIds.has(r.id));
});
// Own groups (filter by ownerId === currentUser)
const ownGroupResults = computed(() => {
if (!query.value || query.value.length < 2) return [];
return searchGroups(
query.value,
groupStore.currentUserGroups,
stringComparer.value,
currentUserId.value
);
});
// Joined groups (all matching groups, deduplicated against own)
const joinedGroupResults = computed(() => {
if (!query.value || query.value.length < 2) return [];
const allResults = searchGroups(
query.value,
groupStore.currentUserGroups,
stringComparer.value
);
const ownIds = new Set(ownGroupResults.value.map((r) => r.id));
return allResults.filter((r) => !ownIds.has(r.id));
});
const hasResults = computed(
() =>
friendResults.value.length > 0 ||
ownAvatarResults.value.length > 0 ||
favoriteAvatarResults.value.length > 0 ||
ownWorldResults.value.length > 0 ||
favoriteWorldResults.value.length > 0 ||
ownGroupResults.value.length > 0 ||
joinedGroupResults.value.length > 0
);
/**
*
*/
function open() {
isOpen.value = true;
}
/**
*
*/
function close() {
isOpen.value = false;
}
/**
* @param {{id: string, type: string}} item
*/
function selectResult(item) {
if (!item) return;
close();
switch (item.type) {
case 'friend':
userStore.showUserDialog(item.id);
break;
case 'avatar':
avatarStore.showAvatarDialog(item.id);
break;
case 'world':
worldStore.showWorldDialog(item.id);
break;
case 'group':
groupStore.showGroupDialog(item.id);
break;
}
}
return {
isOpen,
query,
friendResults,
ownAvatarResults,
favoriteAvatarResults,
ownWorldResults,
favoriteWorldResults,
ownGroupResults,
joinedGroupResults,
hasResults,
open,
close,
selectResult
};
});
+5 -2
View File
@@ -16,6 +16,7 @@ import { useGalleryStore } from './gallery';
import { useGameLogStore } from './gameLog';
import { useGameStore } from './game';
import { useGeneralSettingsStore } from './settings/general';
import { useGlobalSearchStore } from './globalSearch';
import { useGroupStore } from './group';
import { useInstanceStore } from './instance';
import { useInviteStore } from './invite';
@@ -163,7 +164,8 @@ export function createGlobalStores() {
auth: useAuthStore(),
vrcStatus: useVrcStatusStore(),
charts: useChartsStore(),
modal: useModalStore()
modal: useModalStore(),
globalSearch: useGlobalSearchStore()
};
}
@@ -202,5 +204,6 @@ export {
useSharedFeedStore,
useUpdateLoopStore,
useVrcStatusStore,
useModalStore
useModalStore,
useGlobalSearchStore
};
+38 -6
View File
@@ -14,9 +14,8 @@ import {
} from '../shared/utils';
import { instanceRequest, miscRequest, worldRequest } from '../api';
import { database } from '../service/database';
import { useAvatarStore } from './avatar';
import { processBulk } from '../service/request';
import { useFavoriteStore } from './favorite';
import { useGroupStore } from './group';
import { useInstanceStore } from './instance';
import { useLocationStore } from './location';
import { useUiStore } from './ui';
@@ -28,8 +27,6 @@ export const useWorldStore = defineStore('World', () => {
const favoriteStore = useFavoriteStore();
const instanceStore = useInstanceStore();
const userStore = useUserStore();
const avatarStore = useAvatarStore();
const groupStore = useGroupStore();
const uiStore = useUiStore();
const { t } = useI18n();
@@ -64,9 +61,12 @@ export const useWorldStore = defineStore('World', () => {
watch(
() => watchState.isLoggedIn,
() => {
(isLoggedIn) => {
worldDialog.visible = false;
cachedWorlds.clear();
if (isLoggedIn) {
preloadOwnWorlds();
}
},
{ flush: 'sync' }
);
@@ -210,6 +210,9 @@ export const useWorldStore = defineStore('World', () => {
});
}
/**
*
*/
function updateVRChatWorldCache() {
const D = worldDialog;
if (D.visible) {
@@ -228,6 +231,10 @@ export const useWorldStore = defineStore('World', () => {
}
}
/**
*
* @param WorldCache
*/
function cleanupWorldCache(WorldCache) {
const maxCacheSize = 10000;
@@ -339,11 +346,36 @@ export const useWorldStore = defineStore('World', () => {
return ref;
}
/**
* Preload all own worlds into cache at startup for global search.
*/
async function preloadOwnWorlds() {
const params = {
n: 50,
offset: 0,
sort: 'updated',
order: 'descending',
releaseStatus: 'all',
user: 'me'
};
await processBulk({
fn: (p) => worldRequest.getWorlds(p),
N: -1,
params,
handle: (args) => {
for (const json of args.json) {
applyWorld(json);
}
}
});
}
return {
worldDialog,
cachedWorlds,
showWorldDialog,
updateVRChatWorldCache,
applyWorld
applyWorld,
preloadOwnWorlds
};
});