diff --git a/AppApi.cs b/AppApi.cs index e24dd577..503c7b03 100644 --- a/AppApi.cs +++ b/AppApi.cs @@ -426,6 +426,7 @@ namespace VRCX var byteBuffer = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(msg); broadcastSocket.SendTo(byteBuffer, endPoint); + broadcastSocket.Close(); } /// diff --git a/ImageCache.cs b/ImageCache.cs index 0795752f..d84fd071 100644 --- a/ImageCache.cs +++ b/ImageCache.cs @@ -8,6 +8,7 @@ namespace VRCX class ImageCache { private static readonly string cacheLocation = Path.Combine(Program.AppDataDirectory, "ImageCache"); + private static readonly WebClient webClient = new WebClient(); private const string IMAGE_HOST1 = "api.vrchat.cloud"; private const string IMAGE_HOST2 = "files.vrchat.cloud"; @@ -31,22 +32,19 @@ namespace VRCX Uri uri = new Uri(url); if (uri.Host != IMAGE_HOST1 && uri.Host != IMAGE_HOST2 && uri.Host != IMAGE_HOST3) throw new ArgumentException("Invalid image host", url); - - using (var client = new WebClient()) + + string cookieString = string.Empty; + if (WebApi.Instance != null && WebApi.Instance._cookieContainer != null) { - string cookieString = string.Empty; - if (WebApi.Instance != null && WebApi.Instance._cookieContainer != null) - { - CookieCollection cookies = WebApi.Instance._cookieContainer.GetCookies(new Uri($"https://{IMAGE_HOST1}")); - foreach (Cookie cookie in cookies) - cookieString += $"{cookie.Name}={cookie.Value};"; - } - - client.Headers.Add(HttpRequestHeader.Cookie, cookieString); - client.Headers.Add("user-agent", Program.Version); - client.DownloadFile(url, fileLocation); + CookieCollection cookies = WebApi.Instance._cookieContainer.GetCookies(new Uri($"https://{IMAGE_HOST1}")); + foreach (Cookie cookie in cookies) + cookieString += $"{cookie.Name}={cookie.Value};"; } + webClient.Headers.Add(HttpRequestHeader.Cookie, cookieString); + webClient.Headers.Add("user-agent", Program.Version); + webClient.DownloadFile(url, fileLocation); + int cacheSize = Directory.GetDirectories(cacheLocation).Length; if (cacheSize > 1100) CleanImageCache(); diff --git a/WebApi.cs b/WebApi.cs index 220b0bdb..94308506 100644 --- a/WebApi.cs +++ b/WebApi.cs @@ -174,6 +174,20 @@ namespace VRCX await requestStream.WriteAsync(endBytes, 0, endBytes.Length); requestStream.Close(); } + + private static async Task UploadFilePut(HttpWebRequest request, IDictionary options) + { + request.Method = "PUT"; + request.ContentType = options["fileMIME"] as string; + var fileData = options["fileData"] as string; + var sentData = Convert.FromBase64CharArray(fileData.ToCharArray(), 0, fileData.Length); + request.ContentLength = sentData.Length; + using (var sendStream = request.GetRequestStream()) + { + await sendStream.WriteAsync(sentData, 0, sentData.Length); + sendStream.Close(); + } + } private static async Task ImageUpload(HttpWebRequest request, IDictionary options) { @@ -275,6 +289,11 @@ namespace VRCX { await ImageUpload(request, options); } + + if (options.TryGetValue("uploadFilePUT", out _)) + { + await UploadFilePut(request, options); + } if (options.TryGetValue("uploadImageLegacy", out _)) { diff --git a/html/src/app.js b/html/src/app.js index 3af058e6..a971dc16 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -108,9 +108,12 @@ speechSynthesis.getVoices(); this.Set(key, JSON.stringify(value)); }; - workerTimers.setInterval(function () { - VRCXStorage.Flush(); - }, 5 * 60 * 1000); + workerTimers.setInterval( + function () { + VRCXStorage.Flush(); + }, + 5 * 60 * 1000 + ); // #endregion // #region | Init: Noty, Vue, Vue-Markdown, ElementUI, VueI18n, VueLazyLoad, Vue filters, dark stylesheet @@ -380,7 +383,11 @@ speechSynthesis.getVoices(); } this.pendingGetRequests.delete(init.url); } - } else if (init.uploadImage || init.uploadFilePUT) { + } else if ( + init.uploadImage || + init.uploadFilePUT || + init.uploadImageLegacy + ) { // nothing } else { init.headers = { @@ -1073,7 +1080,7 @@ speechSynthesis.getVoices(); '{{ $t("dialog.user.info.instance_game_version") }} {{ gameServerVersion }}
' + '{{ $t("dialog.user.info.instance_queuing_enabled") }}
' + '{{ $t("dialog.user.info.instance_users") }}
' + - '' + + '
' + '' + '' + '' + @@ -11936,7 +11943,7 @@ speechSynthesis.getVoices(); var videoPos = Number(data[1]); var videoLength = Number(data[2]); var displayName = data[3]; - var videoName = data[4]; + var videoName = this.replaceBioSymbols(data[4]); var videoUrl = videoName; var videoId = 'LSMedia'; if (videoUrl === this.nowPlaying.url) { @@ -13037,6 +13044,15 @@ speechSynthesis.getVoices(); }); }; + $app.methods.deleteFavoriteNoConfirm = function (objectId) { + if (!objectId) { + return; + } + API.deleteFavorite({ + objectId + }); + }; + $app.methods.changeFavoriteGroupName = function (ctx) { this.$prompt( $t('prompt.change_favorite_group_name.description'), @@ -18208,6 +18224,19 @@ speechSynthesis.getVoices(); for (var group of API.favoriteWorldGroups) { if (favorite.groupKey === group.key) { D.currentGroup = group; + return; + } + } + for (var group of API.favoriteAvatarGroups) { + if (favorite.groupKey === group.key) { + D.currentGroup = group; + return; + } + } + for (var group of API.favoriteFriendGroups) { + if (favorite.groupKey === group.key) { + D.currentGroup = group; + return; } } } @@ -22211,10 +22240,14 @@ speechSynthesis.getVoices(); if (result || !this.isRealInstance(location)) { return; } + if (this.isGameNoVR) { + this.restartCrashedGame(location); + return; + } // wait a bit for SteamVR to potentially close before deciding to relaunch workerTimers.setTimeout( () => this.restartCrashedGame(location), - 1000 + 3000 ); }); }; @@ -27336,6 +27369,20 @@ speechSynthesis.getVoices(); if (!$app.avatarDialog.visible) { return; } + var ref = args.json; + if (typeof ref.fileSize !== 'undefined') { + ref._fileSize = `${(ref.fileSize / 1048576).toFixed(2)} MiB`; + } + if (typeof ref.uncompressedSize !== 'undefined') { + ref._uncompressedSize = `${(ref.uncompressedSize / 1048576).toFixed( + 2 + )} MiB`; + } + if (typeof ref.avatarStats?.totalTextureUsage !== 'undefined') { + ref._totalTextureUsage = `${( + ref.avatarStats.totalTextureUsage / 1048576 + ).toFixed(2)} MiB`; + } $app.avatarDialog.fileAnalysis = buildTreeData(args.json); }); diff --git a/html/src/index.pug b/html/src/index.pug index 7cf5be65..9a076349 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -262,7 +262,7 @@ html div(style="flex:none") template(v-if="(API.currentUser.id !== userDialog.ref.id && userDialog.isFriend) || userDialog.isFavorite") el-tooltip(v-if="userDialog.isFavorite" placement="top" :content="$t('dialog.user.actions.unfavorite_tooltip')" :disabled="hideTooltips") - el-button(@click="userDialogCommand('Delete Favorite')" type="warning" icon="el-icon-star-on" circle) + el-button(@click="userDialogCommand('Add Favorite')" type="warning" icon="el-icon-star-on" circle) el-tooltip(v-else placement="top" :content="$t('dialog.user.actions.favorite_tooltip')" :disabled="hideTooltips") el-button(type="default" @click="userDialogCommand('Add Favorite')" icon="el-icon-star-off" circle) el-dropdown(trigger="click" @command="userDialogCommand" size="small") @@ -756,7 +756,7 @@ html el-tooltip(v-if="avatarDialog.inCache" placement="top" :content="$t('dialog.avatar.actions.delete_cache_tooltip')" :disabled="hideTooltips") el-button(icon="el-icon-delete" circle @click="deleteVRChatCache(avatarDialog.ref)" :disabled="isGameRunning && avatarDialog.cacheLocked") el-tooltip(v-if="avatarDialog.isFavorite" placement="top" :content="$t('dialog.avatar.actions.unfavorite_tooltip')" :disabled="hideTooltips") - el-button(type="warning" icon="el-icon-star-on" circle @click="avatarDialogCommand('Delete Favorite')" style="margin-left:5px") + el-button(type="warning" icon="el-icon-star-on" circle @click="avatarDialogCommand('Add Favorite')" style="margin-left:5px") el-tooltip(v-else placement="top" :content="$t('dialog.avatar.actions.favorite_tooltip')" :disabled="hideTooltips") el-button(type="default" icon="el-icon-star-off" circle @click="avatarDialogCommand('Add Favorite')" style="margin-left:5px") el-dropdown(trigger="click" @command="avatarDialogCommand" size="small" style="margin-left:5px") @@ -810,7 +810,7 @@ html el-tab-pane(:label="$t('dialog.avatar.json.header')") el-button(type="default" @click="refreshAvatarDialogTreeData()" size="mini" icon="el-icon-refresh" circle) el-tooltip(placement="top" :content="$t('dialog.avatar.json.file_analysis')" :disabled="hideTooltips") - el-button(type="default" @click="getAvatarFileAnalysis" size="mini" icon="el-icon-question" circle style="margin-left:5px") + el-button(type="default" @click="getAvatarFileAnalysis" size="mini" icon="el-icon-s-data" circle style="margin-left:5px") el-button(type="default" @click="downloadAndSaveJson(avatarDialog.id, avatarDialog.ref)" size="mini" icon="el-icon-download" circle style="margin-left:5px") el-tree(v-if="Object.keys(avatarDialog.fileAnalysis).length > 0" :data="avatarDialog.fileAnalysis" style="margin-top:5px;font-size:12px") template(#default="scope") @@ -1080,7 +1080,7 @@ html div(v-if="favoriteDialog.visible" v-loading="favoriteDialog.loading") span(style="display:block;text-align:center") {{ $t('dialog.favorite.vrchat_favorites') }} template(v-if="favoriteDialog.currentGroup && favoriteDialog.currentGroup.key") - el-button(style="display:block;width:100%;margin:10px 0" @click="deleteFavorite(favoriteDialog.objectId)") #[i.el-icon-check] {{ favoriteDialog.currentGroup.displayName }} ({{ favoriteDialog.currentGroup.count }} / {{ favoriteDialog.currentGroup.capacity }}) + el-button(style="display:block;width:100%;margin:10px 0" @click="deleteFavoriteNoConfirm(favoriteDialog.objectId)") #[i.el-icon-check] {{ favoriteDialog.currentGroup.displayName }} ({{ favoriteDialog.currentGroup.count }} / {{ favoriteDialog.currentGroup.capacity }}) template(v-else) el-button(v-for="group in favoriteDialog.groups" :key="group" style="display:block;width:100%;margin:10px 0" @click="addFavorite(group)") {{ group.displayName }} ({{ group.count }} / {{ group.capacity }}) div(v-if="favoriteDialog.visible && favoriteDialog.type === 'world'" style="margin-top:20px") @@ -2045,7 +2045,7 @@ html el-button(type="primary" size="small" @click="saveEditAndSendInvite") {{ $t('dialog.edit_send_invite_message.send') }} //- dialog: Change avatar image - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="changeAvatarImageDialog" :visible.sync="changeAvatarImageDialogVisible" :title="$t('dialog.change_content_image.avatar')" width="800px") + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="changeAvatarImageDialog" :visible.sync="changeAvatarImageDialogVisible" :title="$t('dialog.change_content_image.avatar')" width="850px") div(v-if="changeAvatarImageDialogVisible" v-loading="changeAvatarImageDialogLoading") input(type="file" accept="image/*" @change="onFileChangeAvatarImage" id="AvatarImageUploadButton" style="display:none") span {{ $t('dialog.change_content_image.description') }} @@ -2060,7 +2060,7 @@ html img.image(v-lazy="image.file.url") //- dialog: Change world image - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="changeWorldImageDialog" :visible.sync="changeWorldImageDialogVisible" :title="$t('dialog.change_content_image.world')" width="800px") + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="changeWorldImageDialog" :visible.sync="changeWorldImageDialogVisible" :title="$t('dialog.change_content_image.world')" width="850px") div(v-if="changeWorldImageDialogVisible" v-loading="changeWorldImageDialogLoading") input(type="file" accept="image/*" @change="onFileChangeWorldImage" id="WorldImageUploadButton" style="display:none") span {{ $t('dialog.change_content_image.description') }} diff --git a/html/src/localization/strings/en.json b/html/src/localization/strings/en.json index 86492c80..f266e84e 100644 --- a/html/src/localization/strings/en.json +++ b/html/src/localization/strings/en.json @@ -123,7 +123,6 @@ "bulk_unfriend": "Bulk Unfriend Mode", "bulk_unfriend_selection": "Bulk Unfriend Selection", "load": "Load missing entries", - "load_notice": "This takes a lot of API requests so use it sparingly", "load_tooltip": "Load", "favorites_only_tooltip": "Filter favorites only", "search_placeholder": "Search", @@ -140,6 +139,7 @@ "two_factor_enabled": "Enabled", "two_factor_disabled": "Disabled", "logout": "Logout", + "manage_gallery_icon": "Manage Photos/Icons/Emojis", "export_friend_list": "Export Friends List", "export_own_avatars": "Export Own Avatars", "discord_names": "Discord Names", diff --git a/html/src/localization/strings/fr.json b/html/src/localization/strings/fr.json index f9f1fc0f..9bfc4d8f 100644 --- a/html/src/localization/strings/fr.json +++ b/html/src/localization/strings/fr.json @@ -1477,4 +1477,4 @@ "online": "En ligne :" } } -} \ No newline at end of file +} diff --git a/html/src/mixins/tabs/friendsList.pug b/html/src/mixins/tabs/friendsList.pug index baea35d2..adf74205 100644 --- a/html/src/mixins/tabs/friendsList.pug +++ b/html/src/mixins/tabs/friendsList.pug @@ -10,8 +10,6 @@ mixin friendsListTab() span.name {{ $t('view.friend_list.bulk_unfriend') }} el-switch(v-model="friendsListBulkUnfriendMode" style="margin-left:5px") span {{ $t('view.friend_list.load') }} - el-tooltip(placement="top" style="margin-left:5px" :content="$t('view.friend_list.load_notice')") - i.el-icon-warning template(v-if="friendsListLoading") span(v-text="friendsListLoadingProgress" style="margin-left:5px") el-tooltip(placement="top" :content="$t('view.friend_list.cancel_tooltip')" :disabled="hideTooltips") @@ -40,7 +38,7 @@ mixin friendsListTab() 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="downloadAndSaveImage(userImageFull(scope.row))") + img.friends-list-avatar(v-lazy="userImageFull(scope.row)" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(userImageFull(scope.row))") el-table-column(:label="$t('table.friendList.displayName')" 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}") diff --git a/html/src/mixins/tabs/profile.pug b/html/src/mixins/tabs/profile.pug index fbccb235..0426d95e 100644 --- a/html/src/mixins/tabs/profile.pug +++ b/html/src/mixins/tabs/profile.pug @@ -19,6 +19,7 @@ mixin profileTab() span.extra {{ API.currentUser.twoFactorAuthEnabled ? $t('view.profile.profile.two_factor_enabled') : $t('view.profile.profile.two_factor_disabled') }} div el-button(size="small" icon="el-icon-switch-button" @click="logout()" style="margin-left:0;margin-right:5px;margin-top:10px") {{ $t('view.profile.profile.logout') }} + el-button(size="small" icon="el-icon-picture-outline" @click="showGalleryDialog()" style="margin-left:0;margin-right:5px;margin-top:10px") {{ $t('view.profile.profile.manage_gallery_icon') }} el-button(size="small" icon="el-icon-printer" @click="showExportFriendsListDialog()" style="margin-left:0;margin-right:5px;margin-top:10px") {{ $t('view.profile.profile.export_friend_list') }} el-button(size="small" icon="el-icon-user" @click="showExportAvatarsListDialog()" style="margin-left:0;margin-right:5px;margin-top:10px") {{ $t('view.profile.profile.export_own_avatars') }} el-button(size="small" icon="el-icon-chat-dot-round" @click="showDiscordNamesDialog()" style="margin-left:0;margin-right:5px;margin-top:10px") {{ $t('view.profile.profile.discord_names') }}