diff --git a/CefCustomDownloadHandler.cs b/CefCustomDownloadHandler.cs new file mode 100644 index 00000000..c58ce9a8 --- /dev/null +++ b/CefCustomDownloadHandler.cs @@ -0,0 +1,36 @@ +// Copyright(c) 2019-2022 pypy, Natsumi and individual contributors. +// All rights reserved. +// +// This work is licensed under the terms of the MIT license. +// For a copy, see . + +using CefSharp; + +namespace VRCX +{ + public class CustomDownloadHandler : IDownloadHandler + { + public bool CanDownload(IWebBrowser chromiumWebBrowser, IBrowser browser, string url, string requestMethod) + { + return true; + } + + public void OnBeforeDownload(IWebBrowser chromiumWebBrowser, IBrowser browser, DownloadItem downloadItem, IBeforeDownloadCallback callback) + { + if (callback.IsDisposed) + return; + + using (callback) + { + callback.Continue( + downloadItem.SuggestedFileName, + showDialog: true + ); + } + } + + public void OnDownloadUpdated(IWebBrowser chromiumWebBrowser, IBrowser browser, DownloadItem downloadItem, IDownloadItemCallback callback) + { + } + } +} \ No newline at end of file diff --git a/MainForm.cs b/MainForm.cs index 6e26c381..1694a6e6 100644 --- a/MainForm.cs +++ b/MainForm.cs @@ -45,6 +45,7 @@ namespace VRCX { DragHandler = new NoopDragHandler(), MenuHandler = new CustomMenuHandler(), + DownloadHandler = new CustomDownloadHandler(), BrowserSettings = { DefaultEncoding = "UTF-8", diff --git a/VRCX.csproj b/VRCX.csproj index 9aa1ace1..312ef141 100644 --- a/VRCX.csproj +++ b/VRCX.csproj @@ -1,4 +1,4 @@ - + @@ -84,6 +84,7 @@ + diff --git a/WebApi.cs b/WebApi.cs index 1c2cd61b..ab59e8f5 100644 --- a/WebApi.cs +++ b/WebApi.cs @@ -124,6 +124,7 @@ namespace VRCX } #pragma warning disable CS4014 + public async void Execute(IDictionary options, IJavascriptCallback callback) { try @@ -180,7 +181,7 @@ namespace VRCX request.ContentLength = sentData.Length; using (System.IO.Stream sendStream = request.GetRequestStream()) { - sendStream.Write(sentData, 0, sentData.Length); + await sendStream.WriteAsync(sentData, 0, sentData.Length); sendStream.Close(); } } @@ -200,7 +201,7 @@ namespace VRCX { string item = String.Format(FormDataTemplate, boundary, key, postData[key]); byte[] itemBytes = System.Text.Encoding.UTF8.GetBytes(item); - requestStream.Write(itemBytes, 0, itemBytes.Length); + await requestStream.WriteAsync(itemBytes, 0, itemBytes.Length); } } var imageData = options["imageData"] as string; @@ -211,21 +212,21 @@ namespace VRCX 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); - requestStream.Write(headerbytes, 0, headerbytes.Length); + 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) { - requestStream.Write(buffer, 0, bytesRead); + await requestStream.WriteAsync(buffer, 0, bytesRead); } fileStream.Close(); } byte[] newlineBytes = Encoding.UTF8.GetBytes("\r\n"); - requestStream.Write(newlineBytes, 0, newlineBytes.Length); + await requestStream.WriteAsync(newlineBytes, 0, newlineBytes.Length); byte[] endBytes = System.Text.Encoding.UTF8.GetBytes("--" + boundary + "--"); - requestStream.Write(endBytes, 0, endBytes.Length); + await requestStream.WriteAsync(endBytes, 0, endBytes.Length); requestStream.Close(); } @@ -242,11 +243,27 @@ namespace VRCX { if (callback.CanExecute == true) { - callback.ExecuteAsync(null, new + if (response.ContentType.Contains("image/") || response.ContentType.Contains("application/octet-stream")) { - data = await streamReader.ReadToEndAsync(), - status = response.StatusCode - }); + // base64 response data for image + using (var memoryStream = new MemoryStream()) + { + await stream.CopyToAsync(memoryStream); + callback.ExecuteAsync(null, new + { + data = $"data:image/png;base64,{Convert.ToBase64String(memoryStream.ToArray())}", + status = response.StatusCode + }); + } + } + else + { + callback.ExecuteAsync(null, new + { + data = await streamReader.ReadToEndAsync(), + status = response.StatusCode + }); + } } } } @@ -289,6 +306,7 @@ namespace VRCX callback.Dispose(); } + #pragma warning restore CS4014 } -} +} \ No newline at end of file diff --git a/html/src/app.js b/html/src/app.js index 99ae0b32..77b7e885 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -23807,6 +23807,42 @@ speechSynthesis.getVoices(); ); }; + $app.methods.downloadAndSaveImage = async function (url) { + if (!url) { + return; + } + try { + var response = await webApiService.execute({ + url, + method: 'GET' + }); + if ( + response.status !== 200 || + !response.data.startsWith('data:image/png') + ) { + throw new Error(`Error: ${response.data}`); + } + var link = document.createElement('a'); + link.href = response.data; + var fileName = `${extractFileId(url)}.png`; + if (!fileName) { + fileName = `${url.split('/').pop()}.png`; + } + if (!fileName) { + fileName = 'image.png'; + } + link.setAttribute('download', fileName); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } catch { + new Noty({ + type: 'error', + text: escapeTag(`Failed to download image. ${url}`) + }).show(); + } + }; + $app = new Vue($app); window.$app = $app; })(); diff --git a/html/src/index.pug b/html/src/index.pug index 897150f0..590bdad8 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -82,7 +82,7 @@ html div(v-if="currentInstanceWorld.ref.id" style="display:flex") el-popover(placement="right" width="500px" trigger="click" style="height:120px") img.x-link(slot="reference" v-lazy="currentInstanceWorld.ref.thumbnailImageUrl" style="flex:none;width:160px;height:120px;border-radius:4px") - img.x-link(v-lazy="currentInstanceWorld.ref.imageUrl" style="width:500px;height:375px" @click="openExternalLink(currentInstanceWorld.ref.imageUrl)") + img.x-link(v-lazy="currentInstanceWorld.ref.imageUrl" style="width:500px;height:375px" @click="downloadAndSaveImage(currentInstanceWorld.ref.imageUrl)") div(style="margin-left:10px;display:flex;flex-direction:column;min-width:320px;width:100%") div span.x-link(@click="showWorldDialog(currentInstanceWorld.ref.id)" style="font-weight:bold;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1") @@ -247,7 +247,7 @@ html template(v-if="userImage(scope.row.ref)") el-popover(placement="right" height="500px" trigger="hover") img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row.ref)") - img.friends-list-avatar(v-lazy="userImageFull(scope.row.ref)" style="height:500px;cursor:pointer" @click="openExternalLink(userImageFull(scope.row.ref))") + img.friends-list-avatar(v-lazy="userImageFull(scope.row.ref)" style="height:500px;cursor:pointer" @click="downloadAndSaveImage(userImageFull(scope.row.ref))") el-table-column(label="Timer" width="90" prop="timer" sortable) template(v-once #default="scope") timer(:epoch="scope.row.timer") @@ -778,11 +778,11 @@ html template(v-if="scope.row.details && scope.row.details.imageUrl") el-popover(placement="right" width="500px" trigger="click") img.x-link(slot="reference" v-lazy="scope.row.details.imageUrl" style="flex:none;width:90px;border-radius:4px") - img.x-link(v-lazy="scope.row.details.imageUrl" style="width:500px" @click="openExternalLink(scope.row.details.imageUrl)") + img.x-link(v-lazy="scope.row.details.imageUrl" style="width:500px" @click="downloadAndSaveImage(scope.row.details.imageUrl)") template(v-else-if="scope.row.imageUrl") el-popover(placement="right" width="500px" trigger="click") img.x-link(slot="reference" v-lazy="scope.row.imageUrl" style="flex:none;width:90px;border-radius:4px") - img.x-link(v-lazy="scope.row.imageUrl" style="width:500px" @click="openExternalLink(scope.row.imageUrl)") + img.x-link(v-lazy="scope.row.imageUrl" style="width:500px" @click="downloadAndSaveImage(scope.row.imageUrl)") el-table-column(label="Message" prop="message") template(v-once #default="scope") span(v-if="scope.row.message" v-text="scope.row.message") @@ -1015,7 +1015,7 @@ html template(v-once #default="scope") el-popover(placement="right" height="500px" trigger="hover") img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row)") - img.friends-list-avatar(v-lazy="userImageFull(scope.row)" style="height:500px;cursor:pointer" @click="openExternalLink(userImageFull(scope.row))") + img.friends-list-avatar(v-lazy="userImageFull(scope.row)" style="height:500px;cursor:pointer" @click="downloadAndSaveImage(userImageFull(scope.row))") el-table-column(label="Display Name" min-width="140" prop="displayName" sortable :sort-method="(a, b) => sortAlphabetically(a, b, 'displayName')") template(v-once #default="scope") span.name(v-if="randomUserColours" v-text="scope.row.displayName" :style="{'color':scope.row.$userColour}") @@ -1582,10 +1582,10 @@ html div(style="display:flex") el-popover(v-if="userDialog.ref.profilePicOverride" placement="right" width="500px" trigger="click") img.x-link(slot="reference" v-lazy="userDialog.ref.profilePicOverride" style="flex:none;height:120px;width:213.33px;border-radius:4px;object-fit:cover") - img.x-link(v-lazy="userDialog.ref.profilePicOverride" style="height:400px" @click="openExternalLink(userDialog.ref.profilePicOverride)") + img.x-link(v-lazy="userDialog.ref.profilePicOverride" style="height:400px" @click="downloadAndSaveImage(userDialog.ref.profilePicOverride)") el-popover(v-else placement="right" width="500px" trigger="click") img.x-link(slot="reference" v-lazy="userDialog.ref.currentAvatarThumbnailImageUrl" style="flex:none;height:120px;width:160px;border-radius:4px;object-fit:cover") - img.x-link(v-lazy="userDialog.ref.currentAvatarImageUrl" style="height:500px" @click="openExternalLink(userDialog.ref.currentAvatarImageUrl)") + img.x-link(v-lazy="userDialog.ref.currentAvatarImageUrl" style="height:500px" @click="downloadAndSaveImage(userDialog.ref.currentAvatarImageUrl)") div(style="flex:1;display:flex;align-items:center;margin-left:15px") div(style="flex:1") div @@ -1628,7 +1628,7 @@ html div(v-if="userDialog.ref.userIcon" style="flex:none;margin-right:10px") el-popover(placement="right" width="500px" trigger="click") img.x-link(slot="reference" v-lazy="userDialog.ref.userIcon" style="flex:none;width:120px;height:120px;border-radius:4px;object-fit:cover") - img.x-link(v-lazy="userDialog.ref.userIcon" style="height:500px" @click="openExternalLink(userDialog.ref.userIcon)") + img.x-link(v-lazy="userDialog.ref.userIcon" style="height:500px" @click="downloadAndSaveImage(userDialog.ref.userIcon)") div(style="flex:none") template(v-if="API.currentUser.id !== userDialog.ref.id") el-tooltip(v-if="userDialog.isFavorite" placement="top" content="Remove from favorites" :disabled="hideTooltips") @@ -1734,7 +1734,7 @@ html div(style="display:inline-block;flex:none;margin-right:5px") el-popover(placement="right" width="500px" trigger="click") img.x-link(slot="reference" v-lazy="userDialog.representedGroup.iconUrl" style="flex:none;width:60px;height:60px;border-radius:4px;object-fit:cover") - img.x-link(v-lazy="userDialog.representedGroup.iconUrl" style="height:500px" @click="openExternalLink(userDialog.representedGroup.iconUrl)") + img.x-link(v-lazy="userDialog.representedGroup.iconUrl" style="height:500px" @click="downloadAndSaveImage(userDialog.representedGroup.iconUrl)") span(style="vertical-align:top;cursor:pointer" @click="showGroupDialog(userDialog.representedGroup.groupId)") span(v-if="userDialog.representedGroup.ownerId === userDialog.id" style="margin-right:5px") 👑 span(v-text="userDialog.representedGroup.name" style="margin-right:5px") @@ -1919,7 +1919,7 @@ html div(style="display:flex") el-popover(placement="right" width="500px" trigger="click") img.x-link(slot="reference" v-lazy="worldDialog.ref.thumbnailImageUrl" style="flex:none;width:160px;height:120px;border-radius:4px") - img.x-link(v-lazy="worldDialog.ref.imageUrl" style="width:500px;height:375px" @click="openExternalLink(worldDialog.ref.imageUrl)") + img.x-link(v-lazy="worldDialog.ref.imageUrl" style="width:500px;height:375px" @click="downloadAndSaveImage(worldDialog.ref.imageUrl)") div(style="flex:1;display:flex;align-items:center;margin-left:15px") div(style="flex:1") div @@ -2091,7 +2091,7 @@ html div(style="display:flex") el-popover(placement="right" width="500px" trigger="click") img.x-link(slot="reference" v-lazy="avatarDialog.ref.thumbnailImageUrl" style="flex:none;width:160px;height:120px;border-radius:4px") - img.x-link(v-lazy="avatarDialog.ref.imageUrl" style="width:500px;height:375px" @click="openExternalLink(avatarDialog.ref.imageUrl)") + img.x-link(v-lazy="avatarDialog.ref.imageUrl" style="width:500px;height:375px" @click="downloadAndSaveImage(avatarDialog.ref.imageUrl)") div(style="flex:1;display:flex;align-items:center;margin-left:15px") div(style="flex:1") div @@ -2176,12 +2176,12 @@ html .group-banner-image el-popover(placement="right" width="500px" trigger="click") img.x-link(slot="reference" v-lazy="groupDialog.ref.bannerUrl" style="flex:none;width:100%;aspect-ratio:6/1;object-fit:cover;border-radius:4px") - img.x-link(v-lazy="groupDialog.ref.bannerUrl" style="width:854px;height:480px" @click="openExternalLink(groupDialog.ref.bannerUrl)") + img.x-link(v-lazy="groupDialog.ref.bannerUrl" style="width:854px;height:480px" @click="downloadAndSaveImage(groupDialog.ref.bannerUrl)") .group-body(v-loading="groupDialog.loading") div(style="display:flex") el-popover(placement="right" width="500px" trigger="click") img.x-link(slot="reference" v-lazy="groupDialog.ref.iconUrl" style="flex:none;width:120px;height:120px;border-radius:4px") - img.x-link(v-lazy="groupDialog.ref.iconUrl" style="width:500px;height:500px" @click="openExternalLink(groupDialog.ref.iconUrl)") + img.x-link(v-lazy="groupDialog.ref.iconUrl" style="width:500px;height:500px" @click="downloadAndSaveImage(groupDialog.ref.iconUrl)") div(style="flex:1;display:flex;align-items:center;margin-left:15px") .group-header(style="flex:1") span(v-if="groupDialog.ref.ownerId === API.currentUser.id" style="margin-right:5px") 👑 @@ -2257,7 +2257,7 @@ html .group-banner-image-info el-popover(placement="right" width="500px" trigger="click") img.x-link(slot="reference" v-lazy="groupDialog.ref.bannerUrl" style="flex:none;width:100%;aspect-ratio:6/1;object-fit:cover;border-radius:4px") - img.x-link(v-lazy="groupDialog.ref.bannerUrl" style="width:854px;height:480px" @click="openExternalLink(groupDialog.ref.bannerUrl)") + img.x-link(v-lazy="groupDialog.ref.bannerUrl" style="width:854px;height:480px" @click="downloadAndSaveImage(groupDialog.ref.bannerUrl)") .x-friend-list(style="max-height:none") .x-friend-item(v-if="groupDialog.ref.membershipStatus === 'member'" style="width:100%;cursor:default") .detail @@ -2266,7 +2266,7 @@ html div(v-if="groupDialog.announcement.imageUrl" style="display:inline-block;margin-right:5px") el-popover(placement="right" width="500px" trigger="click") img.x-link(slot="reference" v-lazy="groupDialog.announcement.imageUrl" style="flex:none;width:60px;height:60px;border-radius:4px;object-fit:cover") - img.x-link(v-lazy="groupDialog.announcement.imageUrl" style="height:500px" @click="openExternalLink(groupDialog.announcement.imageUrl)") + img.x-link(v-lazy="groupDialog.announcement.imageUrl" style="height:500px" @click="downloadAndSaveImage(groupDialog.announcement.imageUrl)") pre.extra(style="display:inline-block;vertical-align:top;font-family:inherit;font-size:12px;white-space:pre-wrap;margin:0") {{ groupDialog.announcement.text || '-' }} br .extra(v-if="groupDialog.announcement.id" style="float:right;margin-left:5px") @@ -3292,7 +3292,7 @@ html div(style="display:inline-block" v-for="image in previousImagesTable" :key="image.version" v-if="image.file") el-popover.x-change-image-item(placement="right" width="500px" trigger="click") img.x-link(slot="reference" v-lazy="image.file.url") - img.x-link(v-lazy="image.file.url" style="width:500px;height:375px" @click="openExternalLink(image.file.url)") + img.x-link(v-lazy="image.file.url" style="width:500px;height:375px" @click="downloadAndSaveImage(image.file.url)") //- dialog: Gallery/VRCPlusIcons el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="galleryDialog" :visible.sync="galleryDialogVisible" title="Gallery and Icons" width="100%") @@ -3311,7 +3311,7 @@ html .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="openExternalLink(image.versions[image.versions.length - 1].file.url)" size="mini" icon="el-icon-paperclip" circle) + el-button(type="default" @click="downloadAndSaveImage(image.versions[image.versions.length - 1].file.url)" size="mini" icon="el-icon-paperclip" 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") Icons @@ -3326,7 +3326,7 @@ html .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="openExternalLink(image.versions[image.versions.length - 1].file.url)" size="mini" icon="el-icon-paperclip" circle) + el-button(type="default" @click="downloadAndSaveImage(image.versions[image.versions.length - 1].file.url)" size="mini" icon="el-icon-paperclip" circle) el-button(type="default" @click="deleteVRCPlusIcon(image.id)" size="mini" icon="el-icon-delete" circle style="margin-left:5px") //- dialog Table: Previous Instances User @@ -3459,7 +3459,7 @@ html template(v-once #default="scope") el-popover(placement="right" height="500px" trigger="hover") img.friends-list-avatar(slot="reference" v-lazy="scope.row.thumbnailImageUrl") - img.friends-list-avatar(v-lazy="scope.row.imageUrl" style="height:500px;cursor:pointer" @click="openExternalLink(scope.row.imageUrl)") + img.friends-list-avatar(v-lazy="scope.row.imageUrl" style="height:500px;cursor:pointer" @click="downloadAndSaveImage(scope.row.imageUrl)") el-table-column(label="Name" prop="name") template(v-once #default="scope") span.x-link(v-text="scope.row.name" @click="showWorldDialog(scope.row.id)") @@ -3518,7 +3518,7 @@ html template(v-once #default="scope") el-popover(placement="right" height="500px" trigger="hover") img.friends-list-avatar(slot="reference" v-lazy="scope.row.thumbnailImageUrl") - img.friends-list-avatar(v-lazy="scope.row.imageUrl" style="height:500px;cursor:pointer" @click="openExternalLink(scope.row.imageUrl)") + img.friends-list-avatar(v-lazy="scope.row.imageUrl" style="height:500px;cursor:pointer" @click="downloadAndSaveImage(scope.row.imageUrl)") el-table-column(label="Name" prop="name") template(v-once #default="scope") span.x-link(v-text="scope.row.name" @click="showAvatarDialog(scope.row.id)") @@ -3577,7 +3577,7 @@ html template(v-once #default="scope") el-popover(placement="right" height="500px" trigger="hover") img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row)") - img.friends-list-avatar(v-lazy="userImageFull(scope.row)" style="height:500px;cursor:pointer" @click="openExternalLink(userImageFull(scope.row))") + img.friends-list-avatar(v-lazy="userImageFull(scope.row)" style="height:500px;cursor:pointer" @click="downloadAndSaveImage(userImageFull(scope.row))") el-table-column(label="Name" prop="displayName") template(v-once #default="scope") span.x-link(v-text="scope.row.displayName" @click="showUserDialog(scope.row.id)") @@ -3609,7 +3609,7 @@ html template(v-once #default="scope") el-popover(placement="right" height="500px" trigger="hover") img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row.ref)") - img.friends-list-avatar(v-lazy="userImageFull(scope.row.ref)" style="height:500px;cursor:pointer" @click="openExternalLink(userImageFull(scope.row.ref))") + img.friends-list-avatar(v-lazy="userImageFull(scope.row.ref)" style="height:500px;cursor:pointer" @click="downloadAndSaveImage(userImageFull(scope.row.ref))") el-table-column(label="Name" width="170" prop="name") template(v-once #default="scope") span.x-link(v-text="scope.row.name" @click="showUserDialog(scope.row.id)")