From f56f1b8a83aeb58e41a0895282e03ebfe8ea1c14 Mon Sep 17 00:00:00 2001 From: Natsumi Date: Sat, 16 Nov 2024 10:32:43 +1300 Subject: [PATCH] Add support for prints --- Dotnet/ScreenshotMetadata/ScreenshotHelper.cs | 51 ++++++++ .../ScreenshotMetadata/ScreenshotMetadata.cs | 7 + Dotnet/WebApi.cs | 65 +++++++++- html/src/app.js | 122 +++++++++++++++++- html/src/classes/uiComponents.js | 10 +- html/src/classes/websocket.js | 4 + html/src/localization/en/en.json | 1 + html/src/mixins/dialogs/currentUser.pug | 22 +++- .../src/mixins/dialogs/screenshotMetadata.pug | 5 +- 9 files changed, 278 insertions(+), 9 deletions(-) diff --git a/Dotnet/ScreenshotMetadata/ScreenshotHelper.cs b/Dotnet/ScreenshotMetadata/ScreenshotHelper.cs index c2e4117f..7e95ff2d 100644 --- a/Dotnet/ScreenshotMetadata/ScreenshotHelper.cs +++ b/Dotnet/ScreenshotMetadata/ScreenshotHelper.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Numerics; using System.Text; +using System.Xml; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NLog; @@ -172,6 +173,18 @@ namespace VRCX // If not JSON metadata, return early so we're not throwing/catching pointless exceptions if (!metadataString.StartsWith("{")) { + // parse VRC prints + var xmlIndex = metadataString.IndexOf(" /// Writes a text description into a PNG file at the specified path. diff --git a/Dotnet/ScreenshotMetadata/ScreenshotMetadata.cs b/Dotnet/ScreenshotMetadata/ScreenshotMetadata.cs index a3f4f024..edaf8024 100644 --- a/Dotnet/ScreenshotMetadata/ScreenshotMetadata.cs +++ b/Dotnet/ScreenshotMetadata/ScreenshotMetadata.cs @@ -44,6 +44,13 @@ namespace VRCX /// [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public Vector3? Pos { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public DateTime? Timestamp { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string? Note { get; set; } + /// /// Any error that occurred while parsing the file. This being true implies nothing else is set. diff --git a/Dotnet/WebApi.cs b/Dotnet/WebApi.cs index e5987e28..c8ca3f96 100644 --- a/Dotnet/WebApi.cs +++ b/Dotnet/WebApi.cs @@ -256,7 +256,6 @@ namespace VRCX 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) { @@ -294,6 +293,65 @@ namespace VRCX await requestStream.WriteAsync(endBytes, 0, endBytes.Length); requestStream.Close(); } + + private static async Task PrintImageUpload(HttpWebRequest request, IDictionary options) + { + if (ProxySet) + request.Proxy = Proxy; + + request.AutomaticDecompression = DecompressionMethods.All; + request.Method = "POST"; + var boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x"); + request.ContentType = "multipart/form-data; boundary=" + boundary; + var requestStream = request.GetRequestStream(); + // var requestStream = new MemoryStream(); + var imageData = options["imageData"] as string; + var fileToUpload = Convert.FromBase64String(imageData); + const string fileFormKey = "image"; + const string fileName = "image"; + const string fileMimeType = "image/png"; + var fileSize = fileToUpload.Length; + const string headerTemplate = "--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\"\r\nContent-Type: {3}\r\nContent-Length: {4}\r\n"; + var header = string.Format(headerTemplate, boundary, fileFormKey, fileName, fileMimeType, fileSize); + var headerBytes = Encoding.UTF8.GetBytes(header); + await requestStream.WriteAsync(headerBytes); + using (var fileStream = new MemoryStream(fileToUpload)) + { + var buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0) + { + await requestStream.WriteAsync(buffer.AsMemory(0, bytesRead)); + } + fileStream.Close(); + } + var newlineBytes = Encoding.UTF8.GetBytes("\r\n"); + await requestStream.WriteAsync(newlineBytes); + const string textContentType = "text/plain; charset=utf-8"; + const string formDataTemplate = "--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\nContent-Type: {2}\r\nContent-Length: {3}\r\n{4}\r\n"; + if (options.TryGetValue("postData", out var postDataObject)) + { + var jsonPostData = JsonConvert.DeserializeObject>(postDataObject.ToString()); + if (jsonPostData != null) + { + foreach (var (key, value) in jsonPostData) + { + var section = string.Format(formDataTemplate, boundary, key, textContentType, value.Length, value); + var sectionBytes = Encoding.UTF8.GetBytes(section); + await requestStream.WriteAsync(sectionBytes); + } + } + } + var endBytes = Encoding.UTF8.GetBytes("--" + boundary + "--"); + await requestStream.WriteAsync(endBytes); + // test file + // var newFileStream = new FileStream(@"D:\WindowsFiles\Desktop\test", FileMode.Create, FileAccess.Write); + // requestStream.WriteTo(newFileStream); + // newFileStream.Close(); + + requestStream.Close(); + // throw new NotImplementedException(); + } #pragma warning disable CS4014 @@ -364,6 +422,11 @@ namespace VRCX { await LegacyImageUpload(request, options); } + + if (options.TryGetValue("uploadImagePrint", out _)) + { + await PrintImageUpload(request, options); + } try { diff --git a/html/src/app.js b/html/src/app.js index 4caf2613..055e5867 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -7573,6 +7573,7 @@ speechSynthesis.getVoices(); }, layout: 'table' }; + $app.data.printTable = []; $app.data.stickerTable = []; $app.data.emojiTable = []; $app.data.VRCPlusIconsTable = []; @@ -16068,8 +16069,11 @@ speechSynthesis.getVoices(); // D.metadata.resolution = `${regex[18]}x${regex[19]}`; } } + if (metadata.timestamp) { + D.metadata.dateTime = Date.parse(metadata.timestamp); + } if (!D.metadata.dateTime) { - D.metadata.dateTime = Date.parse(json.creationDate); + D.metadata.dateTime = Date.parse(metadata.creationDate); } if (this.fullscreenImageDialog?.visible) { @@ -17120,6 +17124,7 @@ speechSynthesis.getVoices(); $app.data.galleryDialogIconsLoading = false; $app.data.galleryDialogEmojisLoading = false; $app.data.galleryDialogStickersLoading = false; + $app.data.galleryDialogPrintsLoading = false; API.$on('LOGIN', function () { $app.galleryTable = []; @@ -17132,6 +17137,7 @@ speechSynthesis.getVoices(); this.refreshVRCPlusIconsTable(); this.refreshEmojiTable(); this.refreshStickerTable(); + this.refreshPrintTable(); workerTimers.setTimeout(() => this.setGalleryTab(pageNum), 100); }; @@ -17395,6 +17401,120 @@ speechSynthesis.getVoices(); } }); + // #endregion + // #region | Prints + API.$on('LOGIN', function () { + $app.printTable = []; + }); + + $app.methods.refreshPrintTable = function () { + this.galleryDialogPrintsLoading = true; + var params = { + n: 100, + tag: 'print' + }; + API.getFileList(params); + }; + + API.$on('FILES:LIST', function (args) { + if (args.params.tag === 'print') { + $app.printTable = args.json.reverse(); + $app.galleryDialogPrintsLoading = false; + } + }); + + $app.methods.deletePrint = function (fileId) { + API.deleteFile(fileId).then((args) => { + API.$emit('PRINT:DELETE', args); + return args; + }); + }; + + API.$on('PRINT:DELETE', function (args) { + var array = $app.printTable; + var { length } = array; + for (var i = 0; i < length; ++i) { + if (args.fileId === array[i].id) { + array.splice(i, 1); + break; + } + } + }); + + $app.methods.onFileChangePrint = function (e) { + var clearFile = function () { + if (document.querySelector('#PrintUploadButton')) { + document.querySelector('#PrintUploadButton').value = ''; + } + }; + var files = e.target.files || e.dataTransfer.files; + if (!files.length) { + return; + } + if (files[0].size >= 100000000) { + // 100MB + $app.$message({ + message: 'File size too large', + type: 'error' + }); + clearFile(); + return; + } + if (!files[0].type.match(/image.*/)) { + $app.$message({ + message: "File isn't an image", + type: 'error' + }); + clearFile(); + return; + } + var r = new FileReader(); + r.onload = function () { + var date = new Date(); + var timestamp = date.toISOString().slice(0, 19); + var params = { + note: 'test print', + worldId: 'wrld_10e5e467-fc65-42ed-8957-f02cace1398c', + timestamp + }; + var base64Body = btoa(r.result); + API.uploadPrint(base64Body, params).then((args) => { + $app.$message({ + message: 'Print uploaded', + type: 'success' + }); + return args; + }); + }; + r.readAsBinaryString(files[0]); + clearFile(); + }; + + $app.methods.displayPrintUpload = function () { + document.getElementById('PrintUploadButton').click(); + }; + + API.uploadPrint = function (imageData, params) { + return this.call('prints', { + uploadImagePrint: true, + postData: JSON.stringify(params), + imageData + }).then((json) => { + var args = { + json, + params + }; + this.$emit('PRINT:ADD', args); + return args; + }); + }; + + API.$on('PRINT:ADD', function (args) { + if (Object.keys($app.printTable).length !== 0) { + $app.printTable.unshift(args.json); + } + }); + // #endregion // #region | Emoji diff --git a/html/src/classes/uiComponents.js b/html/src/classes/uiComponents.js index 73373f04..336e00e7 100644 --- a/html/src/classes/uiComponents.js +++ b/html/src/classes/uiComponents.js @@ -559,7 +559,11 @@ export default class extends baseClass { props: { userid: String, location: String, - key: Number + key: Number, + hint: { + type: String, + default: '' + } }, data() { return { @@ -569,7 +573,9 @@ export default class extends baseClass { methods: { async parse() { this.username = this.userid; - if (this.userid) { + if (this.hint) { + this.username = this.hint; + } else if (this.userid) { var args = await API.getCachedUser({ userId: this.userid }); diff --git a/html/src/classes/websocket.js b/html/src/classes/websocket.js index 3bf7eb26..d2e2d460 100644 --- a/html/src/classes/websocket.js +++ b/html/src/classes/websocket.js @@ -525,6 +525,10 @@ export default class extends baseClass { if ($app.galleryDialogVisible) { $app.refreshEmojiTable(); } + } else if (contentType === 'print') { + if ($app.galleryDialogVisible) { + $app.refreshPrintTable(); + } } else if (contentType === 'avatar') { // hmm, utilizing this might be too spamy and cause UI to move around } else if (contentType === 'world') { diff --git a/html/src/localization/en/en.json b/html/src/localization/en/en.json index 4dc3642d..5f84ed9a 100644 --- a/html/src/localization/en/en.json +++ b/html/src/localization/en/en.json @@ -1266,6 +1266,7 @@ "icons": "Icons", "emojis": "Emojis", "stickers": "Stickers", + "prints": "Prints", "refresh": "Refresh", "upload": "Upload", "clear": "Clear", diff --git a/html/src/mixins/dialogs/currentUser.pug b/html/src/mixins/dialogs/currentUser.pug index a6b83f97..02265d4d 100644 --- a/html/src/mixins/dialogs/currentUser.pug +++ b/html/src/mixins/dialogs/currentUser.pug @@ -78,7 +78,7 @@ mixin currentUser() .vrcplus-icon(v-if="image.versions[image.versions.length - 1].file.url" @click="setProfilePicOverride(image.id)" :class="{ 'current-vrcplus-icon': compareCurrentProfilePic(image.id) }") img.avatar(v-lazy="image.versions[image.versions.length - 1].file.url") div(style="float:right;margin-top:5px") - el-button(type="default" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)" size="mini" icon="el-icon-download" circle) + el-button(type="default" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)" size="mini" icon="el-icon-picture-outline" circle) el-button(type="default" @click="deleteGalleryImage(image.id)" size="mini" icon="el-icon-delete" circle style="margin-left:5px") el-tab-pane(v-if="galleryDialogVisible" v-loading="galleryDialogIconsLoading") span(slot="label") {{ $t('dialog.gallery_icons.icons') }} @@ -93,7 +93,7 @@ mixin currentUser() .vrcplus-icon(v-if="image.versions[image.versions.length - 1].file.url" @click="setVRCPlusIcon(image.id)" :class="{ 'current-vrcplus-icon': compareCurrentVRCPlusIcon(image.id) }") img.avatar(v-lazy="image.versions[image.versions.length - 1].file.url") div(style="float:right;margin-top:5px") - el-button(type="default" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)" size="mini" icon="el-icon-download" circle) + el-button(type="default" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)" size="mini" icon="el-icon-picture-outline" circle) el-button(type="default" @click="deleteVRCPlusIcon(image.id)" size="mini" icon="el-icon-delete" circle style="margin-left:5px") el-tab-pane(v-if="galleryDialogVisible" v-loading="galleryDialogEmojisLoading") span(slot="label") {{ $t('dialog.gallery_icons.emojis') }} @@ -134,7 +134,7 @@ mixin currentUser() span(v-if="image.frames" style="margin-right:5px") {{ image.frames }}frames br div(style="float:right;margin-top:5px") - el-button(type="default" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)" size="mini" icon="el-icon-download" circle) + el-button(type="default" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)" size="mini" icon="el-icon-picture-outline" circle) el-button(type="default" @click="deleteEmoji(image.id)" size="mini" icon="el-icon-delete" circle style="margin-left:5px") el-tab-pane(v-if="galleryDialogVisible" v-loading="galleryDialogStickersLoading") span(slot="label") {{ $t('dialog.gallery_icons.stickers') }} @@ -148,5 +148,19 @@ mixin currentUser() .vrcplus-icon(v-if="image.versions[image.versions.length - 1].file.url" style="overflow:hidden" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)") img.avatar(v-lazy="image.versions[image.versions.length - 1].file.url") div(style="float:right;margin-top:5px") - el-button(type="default" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)" size="mini" icon="el-icon-download" circle) + el-button(type="default" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)" size="mini" icon="el-icon-picture-outline" circle) el-button(type="default" @click="deleteSticker(image.id)" size="mini" icon="el-icon-delete" circle style="margin-left:5px") + el-tab-pane(v-if="galleryDialogVisible" v-loading="galleryDialogPrintsLoading") + span(slot="label") {{ $t('dialog.gallery_icons.prints') }} + span(style="color:#909399;font-size:12px;margin-left:5px") {{ printTable.length }}/64 + input(type="file" accept="image/png,image/jpg,image/jpeg,image/webp,image/bmp,image/gif" @change="onFileChangePrint" id="PrintUploadButton" style="display:none") + el-button-group + el-button(type="default" size="small" @click="refreshPrintTable" icon="el-icon-refresh") {{ $t('dialog.gallery_icons.refresh') }} + //- el-button(type="default" size="small" @click="displayPrintUpload" icon="el-icon-upload2" :disabled="!API.currentUser.$isVRCPlus") {{ $t('dialog.gallery_icons.upload') }} + br + .x-friend-item(v-if="image.versions && image.versions.length > 0" v-for="image in printTable" :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" style="overflow:hidden" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)") + img.avatar(v-lazy="image.versions[image.versions.length - 1].file.url") + div(style="float:right;margin-top:5px") + el-button(type="default" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)" size="mini" icon="el-icon-picture-outline" circle) + el-button(type="default" @click="deletePrint(image.id)" size="mini" icon="el-icon-delete" circle style="margin-left:5px") diff --git a/html/src/mixins/dialogs/screenshotMetadata.pug b/html/src/mixins/dialogs/screenshotMetadata.pug index 37ca68d8..298c5985 100644 --- a/html/src/mixins/dialogs/screenshotMetadata.pug +++ b/html/src/mixins/dialogs/screenshotMetadata.pug @@ -23,13 +23,16 @@ mixin screenshotMetadata() br span(v-text="screenshotMetadataDialog.metadata.fileName") br + template(v-if="screenshotMetadataDialog.metadata.note") + span(v-text="screenshotMetadataDialog.metadata.note") + br span(v-if="screenshotMetadataDialog.metadata.dateTime" style="margin-right:5px") {{ screenshotMetadataDialog.metadata.dateTime | formatDate('long') }} span(v-if="screenshotMetadataDialog.metadata.fileResolution" v-text="screenshotMetadataDialog.metadata.fileResolution" style="margin-right:5px") el-tag(v-if="screenshotMetadataDialog.metadata.fileSize" type="info" effect="plain" size="mini" v-text="screenshotMetadataDialog.metadata.fileSize") br location(v-if="screenshotMetadataDialog.metadata.world" :location="screenshotMetadataDialog.metadata.world.instanceId" :hint="screenshotMetadataDialog.metadata.world.name") br - span.x-link(v-if="screenshotMetadataDialog.metadata.author" v-text="screenshotMetadataDialog.metadata.author.displayName" @click="showUserDialog(screenshotMetadataDialog.metadata.author.id)" style="color:#909399;font-family:monospace") + display-name(v-if="screenshotMetadataDialog.metadata.author" :userid="screenshotMetadataDialog.metadata.author.id" :hind="screenshotMetadataDialog.metadata.author.displayName" style="color:#909399;font-family:monospace") br el-carousel(ref="screenshotMetadataCarousel" :interval="0" initial-index="1" indicator-position="none" arrow="always" height="600px" style="margin-top:10px" @change="screenshotMetadataCarouselChange") el-carousel-item