diff --git a/html/src/app.js b/html/src/app.js index 33dbda05..74b6f991 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -946,7 +946,11 @@ speechSynthesis.getVoices(); if (!this.imageurl) { return; } - $app.showAvatarAuthorDialog(this.userid, this.imageurl); + $app.showAvatarAuthorDialog( + this.userid, + this.ownerId, + this.imageurl + ); } }, watch: { @@ -9120,21 +9124,21 @@ speechSynthesis.getVoices(); var entry = { created_at: new Date().toJSON(), type: 'AvatarChange', - userId: user.id, - displayName: user.displayName, - name: avatar.name, - description: avatar.description, - avatarId: avatar.id, - authorId: avatar.authorId, - releaseStatus: avatar.releaseStatus, - imageUrl: avatar.imageUrl, - thumbnailImageUrl: avatar.thumbnailImageUrl - }; - this.queueGameLogNoty(entry); - this.addGameLog(entry); - this.addEntryPhotonEvent({ - photonId, - displayName: user.displayName, + userId: user.id, + displayName: user.displayName, + name: avatar.name, + description: avatar.description, + avatarId: avatar.id, + authorId: avatar.authorId, + releaseStatus: avatar.releaseStatus, + imageUrl: avatar.imageUrl, + thumbnailImageUrl: avatar.thumbnailImageUrl + }; + this.queueGameLogNoty(entry); + this.addGameLog(entry); + this.addEntryPhotonEvent({ + photonId, + displayName: user.displayName, userId: user.id, text: `ChangeAvatar ${avatar.name}`, created_at: gameLogDate, @@ -9730,10 +9734,14 @@ speechSynthesis.getVoices(); $app.data.searchWorldOption = ''; $app.data.searchWorldParams = {}; $app.data.searchAvatarResults = []; + $app.data.searchAvatarPage = []; + $app.data.searchAvatarPageNum = 0; $app.data.searchAvatarFilter = ''; $app.data.searchAvatarSort = ''; + $app.data.searchAvatarFilterRemote = ''; $app.data.isSearchUserLoading = false; $app.data.isSearchWorldLoading = false; + $app.data.isSearchAvatarLoading = false; API.$on('LOGIN', function () { $app.searchText = ''; @@ -9743,10 +9751,14 @@ speechSynthesis.getVoices(); $app.searchWorldOption = ''; $app.searchWorldParams = {}; $app.searchAvatarResults = []; + $app.searchAvatarPage = []; + $app.searchAvatarPageNum = 0; $app.searchAvatarFilter = ''; $app.searchAvatarSort = ''; + $app.searchAvatarFilterRemote = ''; $app.isSearchUserLoading = false; $app.isSearchWorldLoading = false; + $app.isSearchAvatarLoading = false; }); $app.methods.clearSearch = function () { @@ -9756,6 +9768,8 @@ speechSynthesis.getVoices(); this.searchUserResults = []; this.searchWorldResults = []; this.searchAvatarResults = []; + this.searchAvatarPage = []; + this.searchAvatarPageNum = 0; }; $app.methods.search = function () { @@ -9895,78 +9909,117 @@ speechSynthesis.getVoices(); }); }; - $app.methods.searchAvatar = function () { + $app.methods.searchAvatar = async function () { + this.isSearchAvatarLoading = true; if (!this.searchAvatarFilter) { this.searchAvatarFilter = 'all'; } if (!this.searchAvatarSort) { this.searchAvatarSort = 'name'; } - var avatars = []; + if (!this.searchAvatarFilterRemote) { + this.searchAvatarFilterRemote = 'all'; + } + var avatars = new Map(); var query = this.searchText.toUpperCase(); if (!query) { for (var ref of API.cachedAvatars.values()) { if (ref.authorId === API.currentUser.id) { switch (this.searchAvatarFilter) { case 'all': - avatars.push(ref); + avatars.set(ref.id, ref); break; case 'public': if (ref.releaseStatus === 'public') { - avatars.push(ref); + avatars.set(ref.id, ref); } break; case 'private': if (ref.releaseStatus === 'private') { - avatars.push(ref); + avatars.set(ref.id, ref); } break; } } } + this.isSearchAvatarLoading = false; } else { - for (var ref of API.cachedAvatars.values()) { - var match = ref.name.toUpperCase().includes(query); - if (!match && ref.description) { - match = ref.description.toUpperCase().includes(query); - } - if (!match && ref.authorName) { - match = ref.authorName.toUpperCase().includes(query); - } - if (match) { - switch (this.searchAvatarFilter) { - case 'all': - avatars.push(ref); - break; - case 'public': - if (ref.releaseStatus === 'public') { - avatars.push(ref); - } - break; - case 'private': - if (ref.releaseStatus === 'private') { - avatars.push(ref); - } - break; + if ( + this.searchAvatarFilterRemote === 'all' || + this.searchAvatarFilterRemote === 'local' + ) { + for (var ref of API.cachedAvatars.values()) { + var match = ref.name.toUpperCase().includes(query); + if (!match && ref.description) { + match = ref.description.toUpperCase().includes(query); + } + if (!match && ref.authorName) { + match = ref.authorName.toUpperCase().includes(query); + } + if (match) { + switch (this.searchAvatarFilter) { + 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 (avatars.length >= 1000) { - break; + } + if ( + (this.searchAvatarFilterRemote === 'all' || + this.searchAvatarFilterRemote === 'remote') && + this.avatarRemoteDatabase && + query.length >= 3 + ) { + var data = await this.lookupAvatars('search', query); + if (data && typeof data === 'object') { + data.forEach((avatar) => { + avatars.set(avatar.id, avatar); + }); } } + this.isSearchAvatarLoading = false; } + var avatarsArray = Array.from(avatars.values()); switch (this.searchAvatarSort) { case 'updated': - avatars.sort(compareByUpdatedAt); + avatarsArray.sort(compareByUpdatedAt); break; case 'created': - avatars.sort(compareByCreatedAt); + avatarsArray.sort(compareByCreatedAt); break; case 'name': - avatars.sort(compareByName); + avatarsArray.sort(compareByName); break; } - this.searchAvatarResults = avatars; + this.searchAvatarPageNum = 0; + this.searchAvatarResults = avatarsArray; + this.searchAvatarPage = avatarsArray.slice(0, 10); + }; + + $app.methods.moreSearchAvatar = function (n) { + if (n === -1) { + this.searchAvatarPageNum--; + var offset = this.searchAvatarPageNum * 10; + } + if (n === 1) { + this.searchAvatarPageNum++; + var offset = this.searchAvatarPageNum * 10; + } + this.searchAvatarPage = this.searchAvatarResults.slice( + offset, + offset + 10 + ); }; // App: Favorite @@ -10963,6 +11016,12 @@ speechSynthesis.getVoices(); $app.data.nextClearVRCXCacheCheck = configRepository.getString( 'VRCX_clearVRCXCacheFrequency' ); + $app.data.avatarRemoteDatabase = configRepository.getBool( + 'VRCX_avatarRemoteDatabase' + ); + $app.data.avatarRemoteDatabaseProvider = configRepository.getString( + 'VRCX_avatarRemoteDatabaseProvider' + ); $app.methods.saveOpenVROption = function () { configRepository.setBool('openVR', this.openVR); configRepository.setBool('openVRAlways', this.openVRAlways); @@ -11024,6 +11083,10 @@ speechSynthesis.getVoices(); 'VRCX_vrBackgroundEnabled', this.vrBackgroundEnabled ); + configRepository.setBool( + 'VRCX_avatarRemoteDatabase', + this.avatarRemoteDatabase + ); this.updateSharedFeed(true); this.updateVRConfigVars(); AppApi.ExecuteVrOverlayFunction('notyClear', ''); @@ -12162,6 +12225,30 @@ speechSynthesis.getVoices(); ); }; + $app.methods.promptSetAvatarRemoteDatabase = function () { + this.$prompt( + 'Enter avatar database provider URL', + 'Avatar Database Provider', + { + distinguishCancelAndClose: true, + confirmButtonText: 'OK', + cancelButtonText: 'Cancel', + inputValue: this.avatarRemoteDatabaseProvider, + inputPattern: /\S+/, + inputErrorMessage: 'Valid URL is required', + callback: (action, instance) => { + if (action === 'confirm' && instance.inputValue) { + this.avatarRemoteDatabaseProvider = instance.inputValue; + configRepository.setString( + 'VRCX_avatarRemoteDatabaseProvider', + this.avatarRemoteDatabaseProvider + ); + } + } + } + ); + }; + // App: Dialog var adjustDialogZ = (el) => { @@ -12490,15 +12577,14 @@ speechSynthesis.getVoices(); } else if (this.$refs.userDialogTabs.currentName === '3') { this.userDialogLastActiveTab = 'Avatars'; this.setUserDialogAvatars(userId); - if (this.userDialogLastAvatar !== userId) { - this.userDialogLastAvatar = userId; - if ( - userId === API.currentUser.id && - D.avatars.length === 0 - ) { - this.refreshUserDialogAvatars(); - } + this.userDialogLastAvatar = userId; + if ( + userId === API.currentUser.id && + D.avatars.length === 0 + ) { + this.refreshUserDialogAvatars(); } + this.setUserDialogAvatarsRemote(userId); } else if (this.$refs.userDialogTabs.currentName === '4') { this.userDialogLastActiveTab = 'JSON'; this.refreshUserDialogTreeData(); @@ -12880,13 +12966,85 @@ speechSynthesis.getVoices(); }; $app.methods.setUserDialogAvatars = function (userId) { - var avatars = []; + var avatars = new Set(); + this.userDialogAvatars.forEach((avatar) => { + avatars.add(avatar.id, avatar); + }); for (var ref of API.cachedAvatars.values()) { - if (ref.authorId === userId) { - avatars.push(ref); + if (ref.authorId === userId && !avatars.has(ref.id)) { + this.userDialog.avatars.push(ref); } } - this.sortUserDialogAvatars(avatars); + this.sortUserDialogAvatars(this.userDialog.avatars); + }; + + $app.methods.setUserDialogAvatarsRemote = async function (userId) { + if (this.avatarRemoteDatabase && userId !== API.currentUser.id) { + var data = await this.lookupAvatars('authorId', userId); + var avatars = new Set(); + this.userDialogAvatars.forEach((avatar) => { + avatars.add(avatar.id, avatar); + }); + if (data && typeof data === 'object') { + data.forEach((avatar) => { + if (avatar.id && !avatars.has(avatar.id)) { + this.userDialog.avatars.push(avatar); + } + }); + } + } + this.sortUserDialogAvatars(this.userDialog.avatars); + }; + + $app.methods.lookupAvatars = async function (type, search) { + if (type === 'search') { + var limit = '&limit=5000'; + } else { + var limit = ''; + } + var avatars = new Map(); + try { + var response = await webApiService.execute({ + url: `${ + this.avatarRemoteDatabaseProvider + }?${type}=${encodeURIComponent(search)}${limit}`, + method: 'GET', + headers: { + 'User-Agent': appVersion, + Referer: 'https://vrcx.pypy.moe' + } + }); + var json = JSON.parse(response.data); + if (this.debugWebRequests) { + console.log(json, response); + } + if (response.status === 200 && typeof json === 'object') { + json.forEach((avatar) => { + if (!avatars.has(avatar.Id)) { + var ref1 = { + 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, + thumbnailImageUrl: avatar.imageUrl + }; + avatars.set(ref1.id, ref1); + } + }); + } else { + throw new Error(`Error: ${response.data}`); + } + } catch { + console.error(`Avatar lookup failed for ${search}`); + } + return avatars; }; $app.methods.sortUserDialogAvatars = function (array) { @@ -13154,7 +13312,11 @@ speechSynthesis.getVoices(); }); } else if (command === 'Show Avatar Author') { var {currentAvatarImageUrl} = D.ref; - this.showAvatarAuthorDialog(D.id, currentAvatarImageUrl); + this.showAvatarAuthorDialog( + D.id, + D.$avatarInfo.ownerId, + currentAvatarImageUrl + ); } else if (command === 'Show Fallback Avatar Details') { var {fallbackAvatar} = D.ref; if (fallbackAvatar) { @@ -13900,8 +14062,34 @@ speechSynthesis.getVoices(); } }; - $app.methods.showAvatarAuthorDialog = function ( + $app.methods.checkAvatarCache = function (fileId) { + var avatarId = ''; + for (var ref of API.cachedAvatars.values()) { + if (extractFileId(ref.imageUrl) === fileId) { + avatarId = ref.id; + } + } + return avatarId; + }; + + $app.methods.checkAvatarCacheRemote = async function (fileId, ownerUserId) { + var avatarId = ''; + if (this.avatarRemoteDatabase) { + var data = await this.lookupAvatars('authorId', ownerUserId); + if (data && typeof data === 'object') { + data.forEach((avatar) => { + if (extractFileId(avatar.imageUrl) === fileId) { + avatarId = avatar.id; + } + }); + } + } + return avatarId; + }; + + $app.methods.showAvatarAuthorDialog = async function ( refUserId, + ownerUserId, currentAvatarImageUrl ) { var fileId = extractFileId(currentAvatarImageUrl); @@ -13910,44 +14098,37 @@ speechSynthesis.getVoices(); message: 'Sorry, the author is unknown', type: 'error' }); - return; - } - if (refUserId === API.currentUser.id) { + } else if (refUserId === API.currentUser.id) { this.showAvatarDialog(API.currentUser.currentAvatar); - return; - } - for (var ref of API.cachedAvatars.values()) { - if (extractFileId(ref.imageUrl) === fileId) { - this.showAvatarDialog(ref.id); - return; - } - } - if (API.cachedAvatarNames.has(fileId)) { - let {ownerId} = API.cachedAvatarNames.get(fileId); - if (ownerId === API.currentUser.id) { - this.refreshUserDialogAvatars(fileId); - return; - } - if (ownerId === refUserId) { - this.$message({ - message: "It's personal (own) avatar", - type: 'warning' - }); - return; - } - this.showUserDialog(ownerId); } else { - API.getAvatarImages({fileId}).then((args) => { - let ownerId = args.json.ownerId; - if (ownerId === refUserId) { + var avatarId = await this.checkAvatarCache(fileId); + if (!avatarId) { + var avatarInfo = await this.getAvatarName( + currentAvatarImageUrl + ); + if (avatarInfo.ownerId === API.currentUser.id) { + this.refreshUserDialogAvatars(fileId); + } + } + if (!avatarId) { + avatarId = await this.checkAvatarCacheRemote( + fileId, + ownerUserId + ); + } + if (!avatarId) { + if (avatarInfo.ownerId === refUserId) { this.$message({ message: "It's personal (own) avatar", type: 'warning' }); - return; + } else { + this.showUserDialog(avatarInfo.ownerId); } - this.showUserDialog(ownerId); - }); + } + if (avatarId) { + this.showAvatarDialog(avatarId); + } } }; @@ -16866,6 +17047,8 @@ speechSynthesis.getVoices(); this.userDialog.avatars.length === 0 ) { this.refreshUserDialogAvatars(); + } else { + this.setUserDialogAvatarsRemote(userId); } } } else if (obj.label === 'Worlds') { diff --git a/html/src/index.pug b/html/src/index.pug index ba3c2403..16bdde2d 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -410,30 +410,37 @@ html span.extra(v-else v-text="world.authorName") el-button-group(style="margin-top:15px") el-button(v-if="searchWorldParams.offset" @click="moreSearchWorld(-1)" icon="el-icon-back" size="small") Prev - el-button(v-if="searchWorldResults.length" @click="moreSearchWorld(1)" icon="el-icon-right" size="small") Next - el-tab-pane(label="Avatar" style="min-height:60px") + el-button(v-if="searchWorldResults.length >= 10" @click="moreSearchWorld(1)" icon="el-icon-right" size="small") Next + el-tab-pane(label="Avatar" v-loading="isSearchAvatarLoading" style="min-height:60px") el-tooltip(placement="bottom" content="Refresh own avatars" :disabled="hideTooltips") el-button(type="default" :loading="userDialog.isAvatarsLoading" @click="refreshUserDialogAvatars()" size="mini" icon="el-icon-refresh" circle) span(style="font-size:14px;margin-left:5px") Results {{ searchAvatarResults.length }} - el-radio-group(v-model="searchAvatarSort" size="mini" style="margin-left:30px" @change="searchAvatar") + el-radio-group(v-model="searchAvatarSort" size="mini" style="margin:5px;display:block" @change="searchAvatar") el-radio(label="name") by name el-radio(label="update") by update el-radio(label="created") by created - el-radio-group(v-model="searchAvatarFilter" size="mini" style="margin-left:80px" @change="searchAvatar") + el-radio-group(v-model="searchAvatarFilter" size="mini" style="margin:5px;display:block" @change="searchAvatar") el-radio(label="all") all el-radio(label="public") public el-radio(label="private") private + el-radio-group(v-model="searchAvatarFilterRemote" size="mini" style="margin:5px;display:block" @change="searchAvatar") + el-radio(label="all") all + el-radio(label="local") local + el-radio(label="remote" :disabled="!avatarRemoteDatabase") remote .x-friend-list(style="margin-top:20px") - .x-friend-item(v-for="avatar in searchAvatarResults" :key="avatar.id" @click="showAvatarDialog(avatar.id)") + .x-friend-item(v-for="avatar in searchAvatarPage" :key="avatar.id" @click="showAvatarDialog(avatar.id)") template(v-once) .avatar - img(v-lazy="avatar.thumbnailImageUrl") + img(v-if="avatar.thumbnailImageUrl" v-lazy="avatar.thumbnailImageUrl") .detail span.name(v-text="avatar.name") span.extra(v-text="avatar.releaseStatus" v-if="avatar.releaseStatus === 'public'" style="color: #67c23a;") span.extra(v-text="avatar.releaseStatus" v-else-if="avatar.releaseStatus === 'private'" style="color: #f56c6c;") span.extra(v-text="avatar.releaseStatus" v-else) span.extra(v-text="avatar.authorName") + el-button-group(style="margin-top:15px") + el-button(v-if="searchAvatarPageNum" @click="moreSearchAvatar(-1)" icon="el-icon-back" size="small") Prev + el-button(v-if="searchAvatarResults.length > 10 && (searchAvatarPageNum + 1) * 10 < searchAvatarResults.length" @click="moreSearchAvatar(1)" icon="el-icon-right" size="small") Next //- favorite .x-container(v-show="$refs.menu && $refs.menu.activeIndex === 'favorite'" v-if="$refs.menu && $refs.menu.activeIndex === 'favorite'") @@ -1141,6 +1148,13 @@ html el-button-group el-button(size="small" icon="el-icon-s-operation" @click="showLaunchOptions()") Launch Options el-button(size="small" icon="el-icon-s-operation" @click="showVRChatConfig()") VRChat config.json + div.options-container + span.header Remote Avatar Database + div.options-container-item + span.name Enable + el-switch(v-model="avatarRemoteDatabase" @change="saveOpenVROption") + div.options-container-item + el-button(size="small" icon="el-icon-user-solid" @click="promptSetAvatarRemoteDatabase" :disabled="!avatarRemoteDatabase") Avatar Database Provider div.options-container span.header YouTube API div.options-container-item