diff --git a/html/src/app.dark.scss b/html/src/app.dark.scss index 0db4be9c..40848b44 100644 --- a/html/src/app.dark.scss +++ b/html/src/app.dark.scss @@ -308,7 +308,8 @@ button { color: #fff; } -.x-friend-item:hover { +.x-friend-item:hover, +.x-change-avatar-item:hover { background: #3e3e3e; } diff --git a/html/src/app.js b/html/src/app.js index 45f01a31..6de260a9 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -7265,7 +7265,11 @@ speechSynthesis.getVoices(); avatarReleaseStatus: 'all', treeData: [], - memo: '' + memo: '', + $avatarInfo: { + id: '', + name: '', + } }; $app.watch['userDialog.memo'] = function () { @@ -7508,6 +7512,7 @@ speechSynthesis.getVoices(); if (args.cache) { API.getUser(args.params); } + this.getAvatarName(args); } return args; }); @@ -8364,6 +8369,9 @@ speechSynthesis.getVoices(); case 'Upload Image': document.getElementById('AvatarImageUploadButton').click(); break; + case 'Change Image': + this.displayAvatarImages(); + break; case 'Change Description': this.promptChangeDescription(D); break; @@ -9980,6 +9988,8 @@ speechSynthesis.getVoices(); return response; }; + // Upload avatar image + $app.methods.onFileChangeAvatarImage = function (e) { var clearFile = function () { if (document.querySelector('#AvatarImageUploadButton')) { @@ -10041,19 +10051,6 @@ speechSynthesis.getVoices(); clearFile(); }; - API.getAvatarImages = function (params) { - return this.call(`file/${params.fileId}`, { - method: 'GET', - params - }).then((json) => { - var args = { - json, - params - }; - return args; - }); - }; - API.uploadAvatarImage = async function (params, fileId) { try { return await this.call(`file/${fileId}`, { @@ -10064,7 +10061,7 @@ speechSynthesis.getVoices(); json, params }; - this.$emit('AVATARIMAGE:STAGE1', args); + this.$emit('AVATARIMAGE:INIT', args); return args; }); } catch (err) { @@ -10074,7 +10071,7 @@ speechSynthesis.getVoices(); }; API.uploadAvatarFailCleanup = async function (fileId) { - var args = await this.call(`file/${fileId}`, { + var json = await this.call(`file/${fileId}`, { method: 'GET' }).then((json) => { return json; @@ -10090,17 +10087,17 @@ speechSynthesis.getVoices(); $app.avatarDialog.loading = false; }; - API.$on('AVATARIMAGE:STAGE1', function (args) { + API.$on('AVATARIMAGE:INIT', function (args) { var fileId = args.json.id; var fileVersion = args.json.versions[args.json.versions.length - 1].version; var params = { fileId, fileVersion }; - this.uploadAvatarImageStage2(params); + this.uploadAvatarImageFileStart(params); }); - API.uploadAvatarImageStage2 = async function (params) { + API.uploadAvatarImageFileStart = async function (params) { try { return await this.call(`file/${params.fileId}/${params.fileVersion}/file/start`, { method: 'PUT' @@ -10109,7 +10106,7 @@ speechSynthesis.getVoices(); json, params }; - this.$emit('AVATARIMAGE:STAGE2', args); + this.$emit('AVATARIMAGE:FILESTART', args); return args; }); } catch (err) { @@ -10118,7 +10115,7 @@ speechSynthesis.getVoices(); } }; - API.$on('AVATARIMAGE:STAGE2', function (args) { + API.$on('AVATARIMAGE:FILESTART', function (args) { var { url } = args.json; var { fileId, fileVersion } = args.params; var params = { @@ -10126,10 +10123,10 @@ speechSynthesis.getVoices(); fileId, fileVersion }; - this.uploadAvatarImageStage3(params); + this.uploadAvatarImageFileAWS(params); }); - API.uploadAvatarImageStage3 = function (params) { + API.uploadAvatarImageFileAWS = function (params) { return webApiService.execute({ url: params.url, uploadFilePUT: true, @@ -10147,21 +10144,21 @@ speechSynthesis.getVoices(); json, params }; - this.$emit('AVATARIMAGE:STAGE3', args); + this.$emit('AVATARIMAGE:FILEAWS', args); return args; }); }; - API.$on('AVATARIMAGE:STAGE3', function (args) { + API.$on('AVATARIMAGE:FILEAWS', function (args) { var { fileId, fileVersion } = args.params; var params = { fileId, fileVersion }; - this.uploadAvatarImageStage4(params); + this.uploadAvatarImageFileFinish(params); }); - API.uploadAvatarImageStage4 = function (params) { + API.uploadAvatarImageFileFinish = function (params) { return this.call(`file/${params.fileId}/${params.fileVersion}/file/finish`, { method: 'PUT', params: { @@ -10173,21 +10170,21 @@ speechSynthesis.getVoices(); json, params }; - this.$emit('AVATARIMAGE:STAGE4', args); + this.$emit('AVATARIMAGE:FILEFINISH', args); return args; }); }; - API.$on('AVATARIMAGE:STAGE4', function (args) { + API.$on('AVATARIMAGE:FILEFINISH', function (args) { var { fileId, fileVersion } = args.params; var params = { fileId, fileVersion }; - this.uploadAvatarImageStage5(params); + this.uploadAvatarImageSigStart(params); }); - API.uploadAvatarImageStage5 = async function (params) { + API.uploadAvatarImageSigStart = async function (params) { try { return await this.call(`file/${params.fileId}/${params.fileVersion}/signature/start`, { method: 'PUT' @@ -10196,7 +10193,7 @@ speechSynthesis.getVoices(); json, params }; - this.$emit('AVATARIMAGE:STAGE5', args); + this.$emit('AVATARIMAGE:SIGSTART', args); return args; }); } catch (err) { @@ -10205,7 +10202,7 @@ speechSynthesis.getVoices(); } }; - API.$on('AVATARIMAGE:STAGE5', function (args) { + API.$on('AVATARIMAGE:SIGSTART', function (args) { var { url } = args.json; var { fileId, fileVersion } = args.params; var params = { @@ -10213,10 +10210,10 @@ speechSynthesis.getVoices(); fileId, fileVersion }; - this.uploadAvatarImageStage6(params); + this.uploadAvatarImageSigAWS(params); }); - API.uploadAvatarImageStage6 = function (params) { + API.uploadAvatarImageSigAWS = function (params) { return webApiService.execute({ url: params.url, uploadFilePUT: true, @@ -10234,21 +10231,21 @@ speechSynthesis.getVoices(); json, params }; - this.$emit('AVATARIMAGE:STAGE6', args); + this.$emit('AVATARIMAGE:SIGAWS', args); return args; }); }; - API.$on('AVATARIMAGE:STAGE6', function (args) { + API.$on('AVATARIMAGE:SIGAWS', function (args) { var { fileId, fileVersion } = args.params; var params = { fileId, fileVersion }; - this.uploadAvatarImageStage7(params); + this.uploadAvatarImageSigFinish(params); }); - API.uploadAvatarImageStage7 = function (params) { + API.uploadAvatarImageSigFinish = function (params) { return this.call(`file/${params.fileId}/${params.fileVersion}/signature/finish`, { method: 'PUT', params: { @@ -10260,21 +10257,21 @@ speechSynthesis.getVoices(); json, params }; - this.$emit('AVATARIMAGE:STAGE7', args); + this.$emit('AVATARIMAGE:SIGFINISH', args); return args; }); }; - API.$on('AVATARIMAGE:STAGE7', function (args) { + API.$on('AVATARIMAGE:SIGFINISH', function (args) { var { fileId, fileVersion } = args.params; var parmas = { id: $app.avatarImage.avatarId, imageUrl: `https://api.vrchat.cloud/api/1/file/${fileId}/${fileVersion}/file` }; - this.uploadAvatarImageStage8(parmas); + this.setAvatarImage(parmas); }); - API.uploadAvatarImageStage8 = function (params) { + API.setAvatarImage = function (params) { return this.call(`avatars/${params.id}`, { method: 'PUT', params @@ -10283,17 +10280,17 @@ speechSynthesis.getVoices(); json, params }; - this.$emit('AVATARIMAGE:STAGE8', args); + this.$emit('AVATARIMAGE:SET', args); this.$emit('AVATAR', args); return args; }); }; - API.$on('AVATARIMAGE:STAGE8', function (args) { + API.$on('AVATARIMAGE:SET', function (args) { $app.avatarDialog.loading = false; if (args.json.imageUrl === args.params.imageUrl) { $app.$message({ - message: 'Avatar image uploaded', + message: 'Avatar image changed', type: 'success' }); } else { @@ -10301,6 +10298,122 @@ speechSynthesis.getVoices(); } }); + // Set avatar image + + $app.methods.displayAvatarImages = function () { + this.changeAvatarImageTableFileId = ''; + this.changeAvatarImageTable = ''; + if (this.avatarDialog.visible) { + var { imageUrl } = this.avatarDialog.ref; + } else if (this.userDialog.visible) { + var imageUrl = this.userDialog.ref.currentAvatarImageUrl; + } else { + return; + } + var url = new URL(imageUrl); + var pathArray = url.pathname.split('/'); + var fileId = pathArray[4]; + var params = { + fileId + }; + this.changeAvatarImageDialogVisible = true; + this.$nextTick(() => adjustDialogZ(this.$refs.changeAvatarImageDialog.$el)); + API.getAvatarImages(params).then((args) => { + this.changeAvatarImageTableFileId = args.json.id; + var images = args.json.versions; + var imageArray = []; + images.forEach((image) => { + if (image.file) { + imageArray.push(image.file.url); + } + }); + this.changeAvatarImageTable = images; + }); + }; + + API.getAvatarImages = function (params) { + return this.call(`file/${params.fileId}`, { + method: 'GET', + params + }).then((json) => { + var args = { + json, + params + }; + return args; + }); + }; + + $app.methods.setAvatarImage = function (image) { + this.changeAvatarImageDialogLoading = true; + var parmas = { + id: $app.avatarDialog.id, + imageUrl: `https://api.vrchat.cloud/api/1/file/${$app.changeAvatarImageTableFileId}/${image.version}/file` + }; + API.setAvatarImage(parmas).finally(() => { + this.changeAvatarImageDialogLoading = false; + $app.changeAvatarImageDialogVisible = false; + }); + }; + + $app.methods.compareCurrentAvatarImage = function (image) { + if (`https://api.vrchat.cloud/api/1/file/${$app.changeAvatarImageTableFileId}/${image.version}/file` === this.avatarDialog.ref.imageUrl) { + return true; + } + return false; + } + + $app.data.changeAvatarImageDialogVisible = false; + $app.data.changeAvatarImageDialogLoading = false; + $app.data.changeAvatarImageTable = {}; + $app.data.changeAvatarImageTableFileId = ''; + + API.$on('LOGIN', function () { + $app.changeAvatarImageTable = {}; + $app.changeAvatarImageDialogVisible = false; + }); + + // Avatar names + + API.cachedAvatarNames = new Map(); + + $app.methods.getAvatarName = function (args) { + var D = this.userDialog; + if (!D.visible) { + return; + } + var imageUrl = D.ref.currentAvatarImageUrl; + var url = new URL(imageUrl); + var pathArray = url.pathname.split('/'); + var fileId = pathArray[4]; + if (API.cachedAvatarNames.has(fileId)) { + D.$avatarInfo = API.cachedAvatarNames.get(fileId); + return; + } + D.$avatarInfo = { + id: '', + name: '' + }; + var params = { + fileId + }; + API.getAvatarImages(params).then((args) => { + var name = ''; + var imageName = args.json.name; + var avatarNameRegex = /Avatar - (.*) - Image -/g.exec(imageName); + if (avatarNameRegex[1]) { + name = avatarNameRegex[1]; + } + var id = args.json.ownerId; + var avatarInfo = { + id, + name + }; + API.cachedAvatarNames.set(fileId, avatarInfo); + D.$avatarInfo = avatarInfo; + }); + }; + $app = new Vue($app); window.$app = $app; }()); diff --git a/html/src/app.scss b/html/src/app.scss index 4f5f0901..933fed76 100644 --- a/html/src/app.scss +++ b/html/src/app.scss @@ -385,6 +385,27 @@ img.friends-list-avatar { object-fit: cover; } +.x-change-avatar-item { + cursor: pointer; + display: inline-block; + padding: 4px 4px 0 4px; +} + +.x-change-avatar-item:hover { + background: #f0f0f0; + border-radius: 2px; +} + +.x-change-avatar-item > img { + width: 240px; + height: 180px; +} + +.current-avatar-image { + border: 2px solid #67c23a; + padding: 2px 2px 0 2px; +} + .x-dialog > .el-dialog { max-width: 100%; margin-bottom: 10px; diff --git a/html/src/index.pug b/html/src/index.pug index 28facf02..f2ab6737 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -1012,7 +1012,14 @@ html .x-friend-item(style="width:100%;cursor:default") .detail span.name Note - el-input.extra(v-model="userDialog.memo" type="textarea" :rows="2" :autosize="{ minRows: 2, maxRows: 20 }" placeholder="Click to add a note" size="mini" resize="none") + el-input.extra(v-model="userDialog.memo" type="textarea" :rows="2" :autosize="{ minRows: 1, maxRows: 20 }" placeholder="Click to add a note" size="mini" resize="none") + .x-friend-item(style="width:100%;cursor:default") + .detail + span.name Avatar Name + div(@click="userDialogCommand('Show Avatar Details')" style="cursor:pointer;width:fit-content") + span.extra(v-text="userDialog.$avatarInfo.name" style="display:inline-block;margin-right:5px") + span.extra(v-if="userDialog.$avatarInfo.id === userDialog.id" style="display:inline-block;color:#E6A23C" ) (own) + span.extra(v-else-if="userDialog.$avatarInfo.name" style="display:inline-block;color:#67C23A") ‎(public) .x-friend-item(style="width:100%;cursor:default") .detail span.name Bio @@ -1229,6 +1236,7 @@ html el-dropdown-item(v-else icon="el-icon-user" command="Make Public" divided) Make Public el-dropdown-item(icon="el-icon-edit" command="Rename") Rename el-dropdown-item(icon="el-icon-edit" command="Change Description") Change Description + el-dropdown-item(icon="el-icon-picture-outline" command="Change Image") Change Image el-dropdown-item(icon="el-icon-upload2" command="Upload Image") Upload Image input(type="file" multiple accept="image/*" @change="onFileChangeAvatarImage" id="AvatarImageUploadButton" style="display:none") el-dropdown-item(icon="el-icon-user" command="Delete" style="color:#F56C6C" divided) Delete @@ -1674,6 +1682,13 @@ html template(#footer) el-button(type="small" @click="cancelEditAndSendInvite") Cancel el-button(type="primary" size="small" @click="saveEditAndSendInvite") Send + + //- dialog: Change avatar image + el-dialog.x-dialog(ref="changeAvatarImageDialog" :visible.sync="changeAvatarImageDialogVisible" title="Change Avatar Image" width="800px") + div(v-loading="changeAvatarImageDialogLoading") + div(style="display:inline-block" v-for="image in changeAvatarImageTable" :key="image.version" v-if="image.file") + .x-change-avatar-item(@click="setAvatarImage(image)" :class="{ 'current-avatar-image': compareCurrentAvatarImage(image) }") + img.avatar(v-lazy="image.file.url") //- dialog: open source software notice el-dialog.x-dialog(:visible.sync="ossDialog" title="Open Source Software Notice" width="650px")