diff --git a/WebApi.cs b/WebApi.cs index 4e3a5613..220b0bdb 100644 --- a/WebApi.cs +++ b/WebApi.cs @@ -6,6 +6,9 @@ using System.Net; using System.Runtime.Serialization.Formatters.Binary; using System.Text; using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace VRCX { @@ -128,6 +131,97 @@ namespace VRCX _cookieDirty = true; // force cookies to be saved for lastUserLoggedIn } + private static async Task LegacyImageUpload(HttpWebRequest request, IDictionary options) + { + request.Method = "POST"; + string boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x"); + request.ContentType = "multipart/form-data; boundary=" + boundary; + Stream requestStream = request.GetRequestStream(); + if (options.TryGetValue("postData", out object postDataObject) == true) + { + Dictionary postData = new Dictionary(); + postData.Add("data", (string)postDataObject); + string FormDataTemplate = "--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}\r\n"; + foreach (string key in postData.Keys) + { + string item = string.Format(FormDataTemplate, boundary, key, postData[key]); + byte[] itemBytes = Encoding.UTF8.GetBytes(item); + await requestStream.WriteAsync(itemBytes, 0, itemBytes.Length); + } + } + var imageData = options["imageData"] as string; + byte[] fileToUpload = Convert.FromBase64CharArray(imageData.ToCharArray(), 0, imageData.Length); + string fileFormKey = "image"; + string fileName = "image.png"; + string fileMimeType = "image/png"; + string HeaderTemplate = "--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\"\r\nContent-Type: {3}\r\n\r\n"; + string header = string.Format(HeaderTemplate, boundary, fileFormKey, fileName, fileMimeType); + byte[] headerbytes = Encoding.UTF8.GetBytes(header); + await requestStream.WriteAsync(headerbytes, 0, headerbytes.Length); + using (MemoryStream fileStream = new MemoryStream(fileToUpload)) + { + byte[] buffer = new byte[1024]; + int bytesRead = 0; + while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0) + { + await requestStream.WriteAsync(buffer, 0, bytesRead); + } + fileStream.Close(); + } + byte[] newlineBytes = Encoding.UTF8.GetBytes("\r\n"); + await requestStream.WriteAsync(newlineBytes, 0, newlineBytes.Length); + byte[] endBytes = Encoding.UTF8.GetBytes("--" + boundary + "--"); + await requestStream.WriteAsync(endBytes, 0, endBytes.Length); + requestStream.Close(); + } + + private static async Task ImageUpload(HttpWebRequest request, IDictionary options) + { + request.Method = "POST"; + string boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x"); + request.ContentType = "multipart/form-data; boundary=" + boundary; + Stream requestStream = request.GetRequestStream(); + if (options.TryGetValue("postData", out object postDataObject)) + { + var jsonPostData = (JObject)JsonConvert.DeserializeObject((string)postDataObject); + Dictionary postData = new Dictionary(); + string formDataTemplate = "--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}\r\n"; + if (jsonPostData != null) + { + foreach (var data in jsonPostData) + { + string item = string.Format(formDataTemplate, boundary, data.Key, data.Value); + byte[] itemBytes = Encoding.UTF8.GetBytes(item); + await requestStream.WriteAsync(itemBytes, 0, itemBytes.Length); + } + } + } + var imageData = options["imageData"] as string; + byte[] fileToUpload = Convert.FromBase64CharArray(imageData.ToCharArray(), 0, imageData.Length); + string fileFormKey = "file"; + string fileName = "blob"; + string fileMimeType = "image/png"; + string HeaderTemplate = "--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\"\r\nContent-Type: {3}\r\n\r\n"; + string header = string.Format(HeaderTemplate, boundary, fileFormKey, fileName, fileMimeType); + byte[] headerbytes = Encoding.UTF8.GetBytes(header); + await requestStream.WriteAsync(headerbytes, 0, headerbytes.Length); + using (MemoryStream fileStream = new MemoryStream(fileToUpload)) + { + byte[] buffer = new byte[1024]; + int bytesRead = 0; + while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0) + { + await requestStream.WriteAsync(buffer, 0, bytesRead); + } + fileStream.Close(); + } + byte[] newlineBytes = Encoding.UTF8.GetBytes("\r\n"); + await requestStream.WriteAsync(newlineBytes, 0, newlineBytes.Length); + byte[] endBytes = Encoding.UTF8.GetBytes("--" + boundary + "--"); + await requestStream.WriteAsync(endBytes, 0, endBytes.Length); + requestStream.Close(); + } + #pragma warning disable CS4014 public async void Execute(IDictionary options, IJavascriptCallback callback) @@ -139,7 +233,7 @@ namespace VRCX request.KeepAlive = true; request.UserAgent = Program.Version; - if (options.TryGetValue("headers", out object headers) == true) + if (options.TryGetValue("headers", out object headers)) { foreach (var header in (IEnumerable>)headers) { @@ -161,7 +255,7 @@ namespace VRCX } } - if (options.TryGetValue("method", out object method) == true) + if (options.TryGetValue("method", out object method)) { var _method = (string)method; request.Method = _method; @@ -176,63 +270,15 @@ namespace VRCX } } } - - if (options.TryGetValue("uploadFilePUT", out object uploadImagePUT) == true) + + if (options.TryGetValue("uploadImage", out _)) { - request.Method = "PUT"; - request.ContentType = options["fileMIME"] as string; - var imageData = options["fileData"] as string; - byte[] sentData = Convert.FromBase64CharArray(imageData.ToCharArray(), 0, imageData.Length); - request.ContentLength = sentData.Length; - using (System.IO.Stream sendStream = request.GetRequestStream()) - { - await sendStream.WriteAsync(sentData, 0, sentData.Length); - sendStream.Close(); - } + await ImageUpload(request, options); } - if (options.TryGetValue("uploadImage", out object uploadImage) == true) + if (options.TryGetValue("uploadImageLegacy", out _)) { - request.Method = "POST"; - string boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x"); - request.ContentType = "multipart/form-data; boundary=" + boundary; - Stream requestStream = request.GetRequestStream(); - if (options.TryGetValue("postData", out object postDataObject) == true) - { - Dictionary postData = new Dictionary(); - postData.Add("data", (string)postDataObject); - string FormDataTemplate = "--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}\r\n"; - foreach (string key in postData.Keys) - { - string item = string.Format(FormDataTemplate, boundary, key, postData[key]); - byte[] itemBytes = System.Text.Encoding.UTF8.GetBytes(item); - await requestStream.WriteAsync(itemBytes, 0, itemBytes.Length); - } - } - var imageData = options["imageData"] as string; - byte[] fileToUpload = Convert.FromBase64CharArray(imageData.ToCharArray(), 0, imageData.Length); - string fileFormKey = "image"; - string fileName = "image.png"; - string fileMimeType = "image/png"; - string HeaderTemplate = "--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\"\r\nContent-Type: {3}\r\n\r\n"; - string header = string.Format(HeaderTemplate, boundary, fileFormKey, fileName, fileMimeType); - byte[] headerbytes = Encoding.UTF8.GetBytes(header); - await requestStream.WriteAsync(headerbytes, 0, headerbytes.Length); - using (MemoryStream fileStream = new MemoryStream(fileToUpload)) - { - byte[] buffer = new byte[1024]; - int bytesRead = 0; - while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0) - { - await requestStream.WriteAsync(buffer, 0, bytesRead); - } - fileStream.Close(); - } - byte[] newlineBytes = Encoding.UTF8.GetBytes("\r\n"); - await requestStream.WriteAsync(newlineBytes, 0, newlineBytes.Length); - byte[] endBytes = System.Text.Encoding.UTF8.GetBytes("--" + boundary + "--"); - await requestStream.WriteAsync(endBytes, 0, endBytes.Length); - requestStream.Close(); + await LegacyImageUpload(request, options); } try diff --git a/html/src/app.js b/html/src/app.js index 70af784e..e95484b0 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -3423,7 +3423,7 @@ speechSynthesis.getVoices(); API.sendInvitePhoto = function (params, receiverUserId) { return this.call(`invite/${receiverUserId}/photo`, { - uploadImage: true, + uploadImageLegacy: true, postData: JSON.stringify(params), imageData: $app.uploadImage }).then((json) => { @@ -3454,7 +3454,7 @@ speechSynthesis.getVoices(); API.sendRequestInvitePhoto = function (params, receiverUserId) { return this.call(`requestInvite/${receiverUserId}/photo`, { - uploadImage: true, + uploadImageLegacy: true, postData: JSON.stringify(params), imageData: $app.uploadImage }).then((json) => { @@ -3486,7 +3486,7 @@ speechSynthesis.getVoices(); API.sendInviteResponsePhoto = function (params, inviteId) { return this.call(`invite/${inviteId}/response/photo`, { - uploadImage: true, + uploadImageLegacy: true, postData: JSON.stringify(params), imageData: $app.uploadImage, inviteId @@ -19367,10 +19367,14 @@ speechSynthesis.getVoices(); document.getElementById('VRCPlusIconUploadButton').click(); }; - API.uploadVRCPlusIcon = function (params) { - return this.call('icon', { + API.uploadVRCPlusIcon = function (imageData) { + var params = { + tag: 'icon' + }; + return this.call('file/image', { uploadImage: true, - imageData: params + postData: JSON.stringify(params), + imageData }).then((json) => { var args = { json, @@ -22634,10 +22638,14 @@ speechSynthesis.getVoices(); document.getElementById('GalleryUploadButton').click(); }; - API.uploadGalleryImage = function (params) { - return this.call('gallery', { + API.uploadGalleryImage = function (imageData) { + var params = { + tag: 'gallery' + }; + return this.call('file/image', { uploadImage: true, - imageData: params + postData: JSON.stringify(params), + imageData }).then((json) => { var args = { json, @@ -22724,8 +22732,13 @@ speechSynthesis.getVoices(); } var r = new FileReader(); r.onload = function () { + var params = { + tag: 'emoji', + animationStyle: $app.emojiAnimationStyle.toLowerCase(), + maskTag: 'square' + }; var base64Body = btoa(r.result); - API.uploadEmoji(base64Body).then((args) => { + API.uploadEmoji(base64Body, params).then((args) => { $app.$message({ message: 'Emoji uploaded', type: 'success' @@ -22741,10 +22754,11 @@ speechSynthesis.getVoices(); document.getElementById('EmojiUploadButton').click(); }; - API.uploadEmoji = function (params) { - return this.call('emoji', { + API.uploadEmoji = function (imageData, params) { + return this.call('file/image', { uploadImage: true, - imageData: params + postData: JSON.stringify(params), + imageData }).then((json) => { var args = { json, @@ -22761,6 +22775,39 @@ speechSynthesis.getVoices(); } }); + $app.data.emojiAnimationStyle = 'Aura'; + $app.data.emojiAnimationStyleUrl = + 'https://assets.vrchat.com/www/images/emoji-previews/'; + $app.data.emojiAnimationStyleList = { + Aura: 'Preview_B2-Aura.gif', + Bats: 'Preview_B2-Fall_Bats.gif', + Bees: 'Preview_B2-Bees.gif', + Bounce: 'Preview_B2-Bounce.gif', + Cloud: 'Preview_B2-Cloud.gif', + Confetti: 'Preview_B2-Winter_Confetti.gif', + Crying: 'Preview_B2-Crying.gif', + Dislike: 'Preview_B2-Dislike.gif', + Fire: 'Preview_B2-Fire.gif', + Idea: 'Preview_B2-Idea.gif', + Lasers: 'Preview_B2-Lasers.gif', + Like: 'Preview_B2-Like.gif', + Magnet: 'Preview_B2-Magnet.gif', + Mistletoe: 'Preview_B2-Winter_Mistletoe.gif', + Money: 'Preview_B2-Money.gif', + Noise: 'Preview_B2-Noise.gif', + Orbit: 'Preview_B2-Orbit.gif', + Pizza: 'Preview_B2-Pizza.gif', + Rain: 'Preview_B2-Rain.gif', + Rotate: 'Preview_B2-Rotate.gif', + Shake: 'Preview_B2-Shake.gif', + Snow: 'Preview_B2-Spin.gif', + Snowball: 'Preview_B2-Winter_Snowball.gif', + Spin: 'Preview_B2-Spin.gif', + Splash: 'Preview_B2-SummerSplash.gif', + Stop: 'Preview_B2-Stop.gif', + ZZZ: 'Preview_B2-ZZZ.gif' + }; + // #endregion // #region Misc diff --git a/html/src/index.pug b/html/src/index.pug index adc9d677..99322c69 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -2121,14 +2121,23 @@ html el-tab-pane(v-if="galleryDialogVisible" v-loading="galleryDialogIconsLoading") span(slot="label") {{ $t('dialog.gallery_icons.emojis') }} span(style="color:#909399;font-size:12px;margin-left:5px") {{ emojiTable.length }}/5 - //- input(type="file" accept="image/*" @change="onFileChangeEmoji" id="EmojiUploadButton" style="display:none") - el-button-group + input(type="file" accept="image/*" @change="onFileChangeEmoji" id="EmojiUploadButton" style="display:none") + el-button-group(style="margin-right:10px") el-button(type="default" size="small" @click="refreshEmojiTable" icon="el-icon-refresh") {{ $t('dialog.gallery_icons.refresh') }} el-button(type="default" size="small" @click="displayEmojiUpload" icon="el-icon-upload2" :disabled="!API.currentUser.$isVRCPlus") {{ $t('dialog.gallery_icons.upload') }} + el-select(v-model="emojiAnimationStyle") + el-option-group {{ $t('dialog.gallery_icons.emoji_animation_styles') }} + el-option.x-friend-item(v-for="(fileName, styleName) in emojiAnimationStyleList" :key="fileName" :label="styleName" :value="styleName" style="height:auto") + .avatar(style="width:200px;height:200px") + img(v-lazy="`${emojiAnimationStyleUrl}${fileName}`") + .detail + span.name(v-text="styleName" style="margin-right:100px") br .x-friend-item(v-if="image.versions && image.versions.length > 0" v-for="image in emojiTable" :key="image.id" style="display:inline-block;margin-top:10px;width:unset;cursor:default") .vrcplus-icon(v-if="image.versions[image.versions.length - 1].file.url") img.avatar(v-lazy="image.versions[image.versions.length - 1].file.url") + div(style="display:inline-block;margin:5px") + span {{ image.animationStyle }} div(style="float:right;margin-top:5px") el-button(type="default" @click="downloadAndSaveImage(image.versions[image.versions.length - 1].file.url)" size="mini" icon="el-icon-download" circle) el-button(type="default" @click="deleteEmoji(image.id)" size="mini" icon="el-icon-delete" circle style="margin-left:5px") diff --git a/html/src/localization/strings/en.json b/html/src/localization/strings/en.json index 7ce1767a..86492c80 100644 --- a/html/src/localization/strings/en.json +++ b/html/src/localization/strings/en.json @@ -1111,7 +1111,8 @@ "emojis": "Emojis", "refresh": "Refresh", "upload": "Upload", - "clear": "Clear" + "clear": "Clear", + "emoji_animation_styles": "Animation Styles" }, "change_content_image": { "avatar": "Change Avatar Image",