diff --git a/AppApi.cs b/AppApi.cs index a8a64669..c0dea53d 100644 --- a/AppApi.cs +++ b/AppApi.cs @@ -689,11 +689,8 @@ namespace VRCX public void GetScreenshotMetadata(string path) { var fileName = Path.GetFileNameWithoutExtension(path); - - const string fileNamePrefix = "VRChat_"; var metadata = new JObject(); - - if (File.Exists(path) && path.EndsWith(".png") && fileName.StartsWith(fileNamePrefix)) + if (File.Exists(path) && path.EndsWith(".png")) { string metadataString = null; var readPNGFailed = false; @@ -756,9 +753,14 @@ namespace VRCX metadata.Add("nextFilePath", files[index + 1]); } + metadata.Add("fileResolution", ScreenshotHelper.ReadPNGResolution(path)); + var creationDate = File.GetCreationTime(path); + metadata.Add("creationDate", creationDate.ToString("yyyy-MM-dd HH:mm:ss")); metadata.Add("fileName", fileName); metadata.Add("filePath", path); - metadata.Add("fileSize", $"{(new FileInfo(path).Length / 1024f / 1024f).ToString("0.00")} MB"); + var fileSizeBytes = new FileInfo(path).Length; + metadata.Add("fileSizeBytes", fileSizeBytes.ToString()); + metadata.Add("fileSize", $"{(fileSizeBytes / 1024f / 1024f).ToString("0.00")} MB"); ExecuteAppFunction("displayScreenshotMetadata", metadata.ToString(Formatting.Indented)); } @@ -775,6 +777,16 @@ namespace VRCX } } + public string GetFileBase64(string path) + { + if (File.Exists(path)) + { + return Convert.ToBase64String(File.ReadAllBytes(path)); + } + + return null; + } + private struct XSOMessage { public int messageType { get; set; } diff --git a/ScreenshotHelper.cs b/ScreenshotHelper.cs index 0435a4e0..379a17aa 100644 --- a/ScreenshotHelper.cs +++ b/ScreenshotHelper.cs @@ -66,6 +66,19 @@ namespace VRCX return text; } + public static string ReadPNGResolution(string path) + { + if (!File.Exists(path) || !IsPNGFile(path)) return null; + + var png = File.ReadAllBytes(path); + var existingpHYs = FindChunk(png, "IHDR"); + if (existingpHYs == null) return null; + + var text = existingpHYs.GetResolution(); + + return text; + } + /// /// Determines whether the specified file is a PNG file. We do this by checking if the first 8 bytes in the file path match the PNG signature. /// @@ -358,10 +371,16 @@ namespace VRCX public string GetText(string keyword) { var offset = keywordEncoding.GetByteCount(keyword) + 5; - // Read string from PNG chunk return Encoding.UTF8.GetString(ChunkDataBytes.ToArray(), offset, ChunkDataBytes.Count - offset); } + public string GetResolution() + { + var x = BitConverter.ToInt32(ChunkDataBytes.Take(4).Reverse().ToArray(), 0); + var y = BitConverter.ToInt32(ChunkDataBytes.Skip(4).Take(4).Reverse().ToArray(), 0); + return $"{x}x{y}"; + } + // Crc32 implementation from // https://web.archive.org/web/20150825201508/http://upokecenter.dreamhosters.com/articles/png-image-encoder-in-c/ private static uint Crc32(byte[] stream, int offset, int length, uint crc) diff --git a/html/src/app.js b/html/src/app.js index 7eb9d855..09785042 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -20212,33 +20212,40 @@ speechSynthesis.getVoices(); var json = JSON.parse(metadata); D.metadata = json; - // VRChat_3840x2160_2022-02-02_03-21-39.771 - // VRChat_2023-02-16_10-39-25.274_3840x2160 var regex = json.fileName.match( /VRChat_((\d{3,})x(\d{3,})_(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})\.(\d{1,})|(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})\.(\d{3})_(\d{3,})x(\d{3,}))/ ); if (regex) { - if (typeof regex[2] !== 'undefined') { + if (typeof regex[2] !== 'undefined' && regex[4].length === 4) { // old format + // VRChat_3840x2160_2022-02-02_03-21-39.771 var date = `${regex[4]}-${regex[5]}-${regex[6]}`; var time = `${regex[7]}:${regex[8]}:${regex[9]}`; D.metadata.dateTime = Date.parse(`${date} ${time}`); - D.metadata.resolution = `${regex[2]}x${regex[3]}`; - } else if (typeof regex[11] !== 'undefined') { + // D.metadata.resolution = `${regex[2]}x${regex[3]}`; + } else if ( + typeof regex[11] !== 'undefined' && + regex[11].length === 4 + ) { // new format + // VRChat_2023-02-16_10-39-25.274_3840x2160 var date = `${regex[11]}-${regex[12]}-${regex[13]}`; var time = `${regex[14]}:${regex[15]}:${regex[16]}`; D.metadata.dateTime = Date.parse(`${date} ${time}`); - D.metadata.resolution = `${regex[18]}x${regex[19]}`; + // D.metadata.resolution = `${regex[18]}x${regex[19]}`; } } + if (!D.metadata.dateTime) { + D.metadata.dateTime = Date.parse(json.creationDate); + } this.showScreenshotMetadataDialog(); }; $app.data.screenshotMetadataDialog = { visible: false, - metadata: {} + metadata: {}, + isUploading: false }; $app.methods.showScreenshotMetadataDialog = function () { @@ -20270,6 +20277,40 @@ speechSynthesis.getVoices(); } }; + $app.methods.uploadScreenshotToGallery = function () { + var D = this.screenshotMetadataDialog; + if (D.metadata.fileSizeBytes > 10000000) { + $app.$message({ + message: 'File size too large', + type: 'error' + }); + return; + } + D.isUploading = true; + AppApi.GetFileBase64(D.metadata.filePath) + .then((base64Body) => { + API.uploadGalleryImage(base64Body) + .then((args) => { + $app.$message({ + message: 'Gallery image uploaded', + type: 'success' + }); + return args; + }) + .finally(() => { + D.isUploading = false; + }); + }) + .catch((err) => { + $app.$message({ + message: 'Failed to upload gallery image', + type: 'error' + }); + console.error(err); + D.isUploading = false; + }); + }; + // YouTube API $app.data.youTubeApiKey = ''; @@ -21475,6 +21516,9 @@ speechSynthesis.getVoices(); } this.photonLastEvent7List = Date.parse(data.dt); break; + case 'VrcxMessage': + this.eventVrcxMessage(data); + break; case 'Ping': if (!this.photonLoggingEnabled) { this.photonLoggingEnabled = true; @@ -21512,6 +21556,18 @@ speechSynthesis.getVoices(); $app.data.photonEventCount = 0; $app.data.photonEventIcon = false; + $app.methods.eventVrcxMessage = function (data) { + console.log(data); + var entry = { + created_at: new Date().toJSON(), + type: 'Event', + data: data.Data + }; + database.addGamelogEventToDatabase(entry); + this.queueGameLogNoty(entry); + this.addGameLog(entry); + }; + $app.methods.photonEventPulse = function () { this.photonEventCount++; this.photonEventIcon = true; diff --git a/html/src/index.pug b/html/src/index.pug index 222be7fa..0d2d8066 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -3800,9 +3800,11 @@ html el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="screenshotMetadataDialog" :visible.sync="screenshotMetadataDialog.visible" :title="$t('dialog.screenshot_metadata.header')" width="1050px") div(v-if="screenshotMetadataDialog.visible") el-button(size="small" icon="el-icon-folder-opened" @click="AppApi.OpenScreenshotFileDialog()") {{ $t('dialog.screenshot_metadata.browse') }} - span(v-if="!screenshotMetadataDialog.metadata.resolution" v-text="screenshotMetadataDialog.metadata.fileName" style="margin-left:5px") + el-button(v-if="API.currentUser.$isVRCPlus && screenshotMetadataDialog.metadata.filePath" size="small" icon="el-icon-upload2" @click="uploadScreenshotToGallery") {{ $t('dialog.screenshot_metadata.upload') }} + span(v-text="screenshotMetadataDialog.metadata.fileName" style="margin-left:5px") + br span(v-if="screenshotMetadataDialog.metadata.dateTime" style="margin-left:5px") {{ screenshotMetadataDialog.metadata.dateTime | formatDate('long') }} - span(v-if="screenshotMetadataDialog.metadata.resolution" v-text="screenshotMetadataDialog.metadata.resolution" style="margin-left:5px") + span(v-if="screenshotMetadataDialog.metadata.fileResolution" v-text="screenshotMetadataDialog.metadata.fileResolution" style="margin-left:5px") el-tag(v-if="screenshotMetadataDialog.metadata.fileSize" type="info" effect="plain" size="mini" style="margin-left:5px" v-text="screenshotMetadataDialog.metadata.fileSize") br location(v-if="screenshotMetadataDialog.metadata.world" :location="screenshotMetadataDialog.metadata.world.instanceId" :hint="screenshotMetadataDialog.metadata.world.name") diff --git a/html/src/localization/strings/en.json b/html/src/localization/strings/en.json index 90d5fa4d..ccfc6e5a 100644 --- a/html/src/localization/strings/en.json +++ b/html/src/localization/strings/en.json @@ -1055,7 +1055,8 @@ }, "screenshot_metadata": { "header": "Screenshot Metadata", - "browse": "Browse" + "browse": "Browse", + "upload": "Upload" } }, "prompt": {