import { nextTick, ref, watch } from 'vue'; import { ElMessageBox } from 'element-plus'; import { defineStore } from 'pinia'; import { toast } from 'vue-sonner'; import { checkVRChatCache, extractFileId, getAvailablePlatforms, getBundleDateSize, getPlatformInfo, replaceBioSymbols, storeAvatarImage } from '../shared/utils'; import { avatarRequest, miscRequest } from '../api'; import { AppDebug } from '../service/appConfig'; import { database } from '../service/database'; import { useAdvancedSettingsStore } from './settings/advanced'; import { useAvatarProviderStore } from './avatarProvider'; import { useFavoriteStore } from './favorite'; import { useUserStore } from './user'; import { useVRCXUpdaterStore } from './vrcxUpdater'; import { watchState } from '../service/watchState'; import webApiService from '../service/webapi'; export const useAvatarStore = defineStore('Avatar', () => { const favoriteStore = useFavoriteStore(); const avatarProviderStore = useAvatarProviderStore(); const vrcxUpdaterStore = useVRCXUpdaterStore(); const advancedSettingsStore = useAdvancedSettingsStore(); const userStore = useUserStore(); let cachedAvatarModerations = new Map(); let cachedAvatars = new Map(); let cachedAvatarNames = new Map(); const avatarDialog = ref({ visible: false, loading: false, id: '', memo: '', ref: {}, isFavorite: false, isBlocked: false, isQuestFallback: false, hasImposter: false, imposterVersion: '', isPC: false, isQuest: false, isIos: false, bundleSizes: [], platformInfo: {}, galleryImages: [], galleryLoading: false, lastUpdated: '', inCache: false, cacheSize: '', cacheLocked: false, cachePath: '', fileAnalysis: [] }); const avatarHistory = ref([]); watch( () => watchState.isLoggedIn, (isLoggedIn) => { avatarDialog.value.visible = false; cachedAvatars.clear(); cachedAvatarNames.clear(); cachedAvatarModerations.clear(); avatarHistory.value = []; if (isLoggedIn) { getAvatarHistory(); } }, { flush: 'sync' } ); /** / * @param {object} json / * @returns {object} ref */ function applyAvatar(json) { json.name = replaceBioSymbols(json.name); json.description = replaceBioSymbols(json.description); let ref = cachedAvatars.get(json.id); if (typeof ref === 'undefined') { ref = { acknowledgements: '', authorId: '', authorName: '', created_at: '', description: '', featured: false, highestPrice: null, id: '', imageUrl: '', listingDate: null, lock: false, lowestPrice: null, name: '', pendingUpload: false, performance: {}, productId: null, publishedListings: [], releaseStatus: '', searchable: false, styles: [], tags: [], thumbnailImageUrl: '', unityPackageUrl: '', unityPackageUrlObject: {}, unityPackages: [], updated_at: '', version: 0, ...json }; cachedAvatars.set(ref.id, ref); } else { const { unityPackages } = ref; Object.assign(ref, json); if ( json.unityPackages?.length > 0 && unityPackages.length > 0 && !json.unityPackages[0].assetUrl ) { ref.unityPackages = unityPackages; } } for (const listing of ref.publishedListings) { listing.displayName = replaceBioSymbols(listing.displayName); listing.description = replaceBioSymbols(listing.description); } favoriteStore.applyFavorite('avatar', ref.id); if (favoriteStore.localAvatarFavoritesList.includes(ref.id)) { for ( let i = 0; i < favoriteStore.localAvatarFavoriteGroups.length; ++i ) { const groupName = favoriteStore.localAvatarFavoriteGroups[i]; if (!favoriteStore.localAvatarFavorites[groupName]) { continue; } for ( let j = 0; j < favoriteStore.localAvatarFavorites[groupName].length; ++j ) { const ref = favoriteStore.localAvatarFavorites[groupName][j]; if (ref.id === ref.id) { favoriteStore.localAvatarFavorites[groupName][j] = ref; } } } // update db cache database.addAvatarToCache(ref); } return ref; } /** * * @param {string} avatarId * @returns */ function showAvatarDialog(avatarId) { const D = avatarDialog.value; D.visible = true; D.loading = true; D.id = avatarId; D.inCache = false; D.cacheSize = ''; D.cacheLocked = false; D.cachePath = ''; D.fileAnalysis = []; D.isQuestFallback = false; D.isPC = false; D.isQuest = false; D.isIos = false; D.hasImposter = false; D.imposterVersion = ''; D.lastUpdated = ''; D.bundleSizes = []; D.platformInfo = {}; D.galleryImages = []; D.galleryLoading = true; D.isFavorite = favoriteStore.getCachedFavoritesByObjectId(avatarId) || (userStore.isLocalUserVrcPlusSupporter && favoriteStore.localAvatarFavoritesList.includes(avatarId)); D.isBlocked = cachedAvatarModerations.has(avatarId); const ref2 = cachedAvatars.get(avatarId); if (typeof ref2 !== 'undefined') { D.ref = ref2; updateVRChatAvatarCache(); if ( ref2.releaseStatus !== 'public' && ref2.authorId !== userStore.currentUser.id ) { D.loading = false; return; } } avatarRequest .getAvatar({ avatarId }) .then((args) => { const ref = applyAvatar(args.json); D.ref = ref; getAvatarGallery(avatarId); updateVRChatAvatarCache(); if (/quest/.test(ref.tags)) { D.isQuestFallback = true; } const { isPC, isQuest, isIos } = getAvailablePlatforms( ref.unityPackages ); D.isPC = isPC; D.isQuest = isQuest; D.isIos = isIos; D.platformInfo = getPlatformInfo(ref.unityPackages); for (let i = ref.unityPackages.length - 1; i > -1; i--) { const unityPackage = ref.unityPackages[i]; if (unityPackage.variant === 'impostor') { D.hasImposter = true; D.imposterVersion = unityPackage.impostorizerVersion; break; } } if (D.bundleSizes.length === 0) { getBundleDateSize(ref).then((bundleSizes) => { D.bundleSizes = bundleSizes; }); } }) .catch((err) => { D.visible = false; throw err; }) .finally(() => { nextTick(() => (D.loading = false)); }); } /** * * @param {string} avatarId * @returns {Promise} */ async function getAvatarGallery(avatarId) { const D = avatarDialog.value; const args = await avatarRequest .getAvatarGallery(avatarId) .finally(() => { D.galleryLoading = false; }); if (args.params.galleryId !== D.id) { return; } D.galleryImages = []; // wtf is this? why is order sorting only needed if it's your own avatar? const sortedGallery = args.json.sort((a, b) => { if (!a.order && !b.order) { return 0; } return a.order - b.order; }); for (const file of sortedGallery) { const url = file.versions[file.versions.length - 1].file.url; D.galleryImages.push(url); } // for JSON tab treeData D.ref.gallery = args.json; return D.galleryImages; } /** * * @param {object} json * @returns {object} ref */ function applyAvatarModeration(json) { // fix inconsistent Unix time response if (typeof json.created === 'number') { json.created = new Date(json.created).toJSON(); } let ref = cachedAvatarModerations.get(json.targetAvatarId); if (typeof ref === 'undefined') { ref = { avatarModerationType: '', created: '', targetAvatarId: '', ...json }; cachedAvatarModerations.set(ref.targetAvatarId, ref); } else { Object.assign(ref, json); } // update avatar dialog const D = avatarDialog.value; if ( D.visible && ref.avatarModerationType === 'block' && D.id === ref.targetAvatarId ) { D.isBlocked = true; } return ref; } function updateVRChatAvatarCache() { const D = avatarDialog.value; if (D.visible) { D.inCache = false; D.cacheSize = ''; D.cacheLocked = false; D.cachePath = ''; checkVRChatCache(D.ref).then((cacheInfo) => { if (cacheInfo.Item1 > 0) { D.inCache = true; D.cacheSize = `${(cacheInfo.Item1 / 1048576).toFixed(2)} MB`; D.cachePath = cacheInfo.Item3; } D.cacheLocked = cacheInfo.Item2; }); } } /** * * @returns {Promise} */ async function getAvatarHistory() { const historyArray = await database.getAvatarHistory( userStore.currentUser.id ); for (let i = 0; i < historyArray.length; i++) { const avatar = historyArray[i]; if (avatar.authorId === userStore.currentUser.id) { continue; } applyAvatar(avatar); } avatarHistory.value = historyArray; } /** * @param {string} avatarId */ function addAvatarToHistory(avatarId) { avatarRequest .getAvatar({ avatarId }) .then((args) => { const ref = applyAvatar(args.json); database.addAvatarToCache(ref); database.addAvatarToHistory(ref.id); if (ref.authorId === userStore.currentUser.id) { return; } const historyArray = avatarHistory.value; for (let i = 0; i < historyArray.length; ++i) { if (historyArray[i].id === ref.id) { historyArray.splice(i, 1); } } avatarHistory.value.unshift(ref); }) .catch((err) => { console.error('Failed to add avatar to history:', err); }); } function clearAvatarHistory() { avatarHistory.value = []; database.clearAvatarHistory(); } function promptClearAvatarHistory() { ElMessageBox.confirm('Continue? Clear Avatar History', 'Confirm', { confirmButtonText: 'Confirm', cancelButtonText: 'Cancel', type: 'info' }) .then(() => { clearAvatarHistory(); }) .catch(() => {}); } /** * * @param {string} imageUrl * @returns {Promise} */ async function getAvatarName(imageUrl) { const fileId = extractFileId(imageUrl); if (!fileId) { return { ownerId: '', avatarName: '-' }; } if (cachedAvatarNames.has(fileId)) { return cachedAvatarNames.get(fileId); } try { const args = await miscRequest.getFile({ fileId }); return storeAvatarImage(args, cachedAvatarNames); } catch (error) { console.error('Failed to get avatar images:', error); return { ownerId: '', vatarName: '-' }; } } async function lookupAvatars(type, search) { const avatars = new Map(); if (type === 'search') { try { const response = await webApiService.execute({ url: `${ avatarProviderStore.avatarRemoteDatabaseProvider }?${type}=${encodeURIComponent(search)}&n=5000`, method: 'GET', headers: { Referer: 'https://vrcx.app', 'VRCX-ID': vrcxUpdaterStore.vrcxId } }); const json = JSON.parse(response.data); if (AppDebug.debugWebRequests) { console.log(json, response); } if (response.status === 200 && typeof json === 'object') { json.forEach((avatar) => { if (!avatars.has(avatar.Id)) { const ref = { authorId: '', authorName: '', name: '', description: '', id: '', imageUrl: '', thumbnailImageUrl: '', created_at: '0001-01-01T00:00:00.0000000Z', updated_at: '0001-01-01T00:00:00.0000000Z', releaseStatus: 'public', ...avatar }; avatars.set(ref.id, ref); } }); } else { throw new Error(`Error: ${response.data}`); } } catch (err) { const msg = `Avatar search failed for ${search} with ${avatarProviderStore.avatarRemoteDatabaseProvider}\n${err}`; console.error(msg); toast.error(msg); } } else if (type === 'authorId') { const length = avatarProviderStore.avatarRemoteDatabaseProviderList.length; for (let i = 0; i < length; ++i) { const url = avatarProviderStore.avatarRemoteDatabaseProviderList[i]; const avatarArray = await lookupAvatarsByAuthor(url, search); avatarArray.forEach((avatar) => { if (!avatars.has(avatar.id)) { avatars.set(avatar.id, avatar); } }); } } return avatars; } async function lookupAvatarByImageFileId(authorId, fileId) { const length = avatarProviderStore.avatarRemoteDatabaseProviderList.length; for (let i = 0; i < length; ++i) { const url = avatarProviderStore.avatarRemoteDatabaseProviderList[i]; const avatarArray = await lookupAvatarsByAuthor(url, authorId); for (const avatar of avatarArray) { if (extractFileId(avatar.imageUrl) === fileId) { return avatar.id; } } } return null; } async function lookupAvatarsByAuthor(url, authorId) { const avatars = []; if (!url) { return avatars; } try { const response = await webApiService.execute({ url: `${url}?authorId=${encodeURIComponent(authorId)}`, method: 'GET', headers: { Referer: 'https://vrcx.app', 'VRCX-ID': vrcxUpdaterStore.vrcxId } }); const json = JSON.parse(response.data); if (AppDebug.debugWebRequests) { console.log(json, response); } if (response.status === 200 && typeof json === 'object') { json.forEach((avatar) => { const ref = { authorId: '', authorName: '', name: '', description: '', id: '', imageUrl: '', thumbnailImageUrl: '', created_at: '0001-01-01T00:00:00.0000000Z', updated_at: '0001-01-01T00:00:00.0000000Z', releaseStatus: 'public', ...avatar }; avatars.push(ref); }); } else { throw new Error(`Error: ${response.data}`); } } catch (err) { const msg = `Avatar lookup failed for ${authorId} with ${url}\n${err}`; console.error(msg); toast.error(msg); } return avatars; } function selectAvatarWithConfirmation(id) { ElMessageBox.confirm(`Continue? Select Avatar`, 'Confirm', { confirmButtonText: 'Confirm', cancelButtonText: 'Cancel', type: 'info' }) .then(() => { selectAvatarWithoutConfirmation(id); }) .catch(() => {}); } async function selectAvatarWithoutConfirmation(id) { if (userStore.currentUser.currentAvatar === id) { toast.info('Avatar already selected'); return; } return avatarRequest .selectAvatar({ avatarId: id }) .then(() => { toast.success('Avatar changed'); }); } function checkAvatarCache(fileId) { let avatarId = ''; for (let ref of cachedAvatars.values()) { if (extractFileId(ref.imageUrl) === fileId) { avatarId = ref.id; } } return avatarId; } async function checkAvatarCacheRemote(fileId, ownerUserId) { if (advancedSettingsStore.avatarRemoteDatabase) { const avatarId = await lookupAvatarByImageFileId( ownerUserId, fileId ); return avatarId; } return null; } async function showAvatarAuthorDialog( refUserId, ownerUserId, currentAvatarImageUrl ) { const fileId = extractFileId(currentAvatarImageUrl); if (!fileId) { toast.error('Sorry, the author is unknown'); } else if (refUserId === userStore.currentUser.id) { showAvatarDialog(userStore.currentUser.currentAvatar); } else { let avatarId = checkAvatarCache(fileId); let avatarInfo; if (!avatarId) { avatarInfo = await getAvatarName(currentAvatarImageUrl); if (avatarInfo.ownerId === userStore.currentUser.id) { userStore.refreshUserDialogAvatars(fileId); } } if (!avatarId) { avatarId = await checkAvatarCacheRemote( fileId, avatarInfo.ownerId ); } if (!avatarId) { if (avatarInfo.ownerId === refUserId) { toast.warning( "It's personal (own) avatar or not found in avatar database" ); } else { toast.warning('Avatar not found in avatar database'); userStore.showUserDialog(avatarInfo.ownerId); } } if (avatarId) { showAvatarDialog(avatarId); } } } function addAvatarWearTime(avatarId) { if (!userStore.currentUser.$previousAvatarSwapTime || !avatarId) { return; } const timeSpent = Date.now() - userStore.currentUser.$previousAvatarSwapTime; database.addAvatarTimeSpent(avatarId, timeSpent); } return { avatarDialog, avatarHistory, cachedAvatarModerations, cachedAvatars, cachedAvatarNames, showAvatarDialog, applyAvatarModeration, getAvatarGallery, updateVRChatAvatarCache, getAvatarHistory, addAvatarToHistory, applyAvatar, promptClearAvatarHistory, getAvatarName, lookupAvatars, selectAvatarWithConfirmation, selectAvatarWithoutConfirmation, showAvatarAuthorDialog, addAvatarWearTime }; });