diff --git a/AppApi.cs b/AppApi.cs index ecfda3d4..c6a85798 100644 --- a/AppApi.cs +++ b/AppApi.cs @@ -297,6 +297,30 @@ namespace VRCX broadcastSocket.SendTo(byteBuffer, endPoint); } + public void DownloadVRCXUpdate(string url, string AppVersion) + { + var Location = Path.Combine(Program.BaseDirectory, "update.zip"); + WebClient client = new WebClient(); + client.Headers.Add("user-agent", AppVersion); + client.DownloadFile(new System.Uri(url), Location); + } + + public void RestartApplication() + { + System.Diagnostics.Process VRCXProcess = new System.Diagnostics.Process(); + VRCXProcess.StartInfo.FileName = Path.Combine(Program.BaseDirectory, "VRCX.exe"); + VRCXProcess.StartInfo.UseShellExecute = false; + VRCXProcess.Start(); + System.Environment.Exit(0); + } + + public bool checkForUpdateZip() + { + if (File.Exists(Path.Combine(Program.BaseDirectory, "update.zip"))) + return true; + return false; + } + public void SetStartup(bool enabled) { try diff --git a/AssetBundleCacher.cs b/AssetBundleCacher.cs index 3d641835..e9ecf216 100644 --- a/AssetBundleCacher.cs +++ b/AssetBundleCacher.cs @@ -33,6 +33,7 @@ namespace VRCX public static string DownloadTempLocation; public static int DownloadProgress; public static bool DownloadCanceled; + public static bool IsUpdate; public static string AssetId; public static string AssetVersion; public static int AssetSize; @@ -98,7 +99,7 @@ namespace VRCX return -1; } - public void DownloadCacheFile(string cacheDir, string url, string id, int version, int sizeInBytes, string md5, string AppVersion) + public void DownloadCacheFile(string cacheDir, string url, string id, int version, int sizeInBytes, string md5, string AppVersion, bool IsUpdate) { if (!File.Exists(Path.Combine(Program.BaseDirectory, "AssetBundleCacher\\AssetBundleCacher.exe"))) { @@ -160,6 +161,11 @@ namespace VRCX Directory.CreateDirectory(AssetBundleCacherTemp); AssetBundleCacherArgs = $@" -url ""file:\\{DownloadTempLocation}"" -id ""{id}"" -ver {version} -batchmode -path ""{AssetBundleCacherTemp}"""; DownloadCanceled = false; + if (IsUpdate) + { + AssetBundleCacher.IsUpdate = true; + DownloadTempLocation = Path.Combine(Program.BaseDirectory, "update.zip"); + } client = new WebClient(); client.Headers.Add("user-agent", AppVersion); client.DownloadProgressChanged += new DownloadProgressChangedEventHandler(DownloadProgressCallback); @@ -216,6 +222,11 @@ namespace VRCX DownloadProgress = -15; return; } + if (IsUpdate) + { + DownloadProgress = -16; + return; + } FileInfo data = new FileInfo(DownloadTempLocation); if (data.Length != AssetSize) { @@ -242,7 +253,6 @@ namespace VRCX DownloadProgress = -13; return; } - if (DownloadCanceled) { if (File.Exists(DownloadTempLocation)) diff --git a/Program.cs b/Program.cs index 2ed55c6c..954d72af 100644 --- a/Program.cs +++ b/Program.cs @@ -1,4 +1,4 @@ -// Copyright(c) 2019 pypy. All rights reserved. +// Copyright(c) 2019 pypy. All rights reserved. // // This work is licensed under the terms of the MIT license. // For a copy, see . @@ -33,6 +33,8 @@ namespace VRCX private static void Run() { + Update.Check(); + Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); diff --git a/Update.cs b/Update.cs new file mode 100644 index 00000000..e7960055 --- /dev/null +++ b/Update.cs @@ -0,0 +1,79 @@ +// Copyright(c) 2019-2021 pypy. All rights reserved. +// +// This work is licensed under the terms of the MIT license. +// For a copy, see . + +using System; +using System.IO; +using System.IO.Compression; +using System.Windows.Forms; + +namespace VRCX +{ + class Update + { + public static void Check() + { + try + { + var CurrentDirectory = new DirectoryInfo(Program.BaseDirectory); + FileInfo[] Files = CurrentDirectory.GetFiles(); + foreach (FileInfo FileDetails in Files) + { + var FilePath = Path.Combine(Program.BaseDirectory, FileDetails.Name); + if (FileDetails.Extension == ".old") + File.Delete(FilePath); + } + var Location = Path.Combine(Program.BaseDirectory, "update.zip"); + if (File.Exists(Location)) + Install(); + } + catch (Exception e) + { + MessageBox.Show(e.ToString(), "Update failed", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + public static void Install() + { + var Location = Path.Combine(Program.BaseDirectory, "update.zip"); + if (!File.Exists(Location)) + return; + if (File.Exists(Path.Combine(Program.BaseDirectory, "VRCX.exe.old"))) + File.Delete(Path.Combine(Program.BaseDirectory, "VRCX.exe.old")); + if (File.Exists(Path.Combine(Program.BaseDirectory, "VRCX.exe"))) + File.Move(Path.Combine(Program.BaseDirectory, "VRCX.exe"), Path.Combine(Program.BaseDirectory, "VRCX.exe.old")); + var CurrentDirectory = new DirectoryInfo(Program.BaseDirectory); + FileInfo[] Files = CurrentDirectory.GetFiles(); + foreach (FileInfo FileDetails in Files) + { + var FilePath = Path.Combine(Program.BaseDirectory, $"{FileDetails.Name}.old"); + if (FileDetails.Extension == ".dll") + { + if (File.Exists(FilePath)) + File.Delete(FilePath); + File.Move(Path.Combine(Program.BaseDirectory, FileDetails.Name), FilePath); + } + } + using (ZipArchive archive = ZipFile.OpenRead(Location)) + { + foreach (ZipArchiveEntry entry in archive.Entries) + { + var path = Path.Combine(Program.BaseDirectory, entry.FullName); + if (entry.Name == "") + { + Directory.CreateDirectory(Path.GetDirectoryName(path)); + continue; + } + entry.ExtractToFile(path, true); + } + } + File.Delete(Location); + System.Diagnostics.Process VRCXProcess = new System.Diagnostics.Process(); + VRCXProcess.StartInfo.FileName = Path.Combine(Program.BaseDirectory, "VRCX.exe"); + VRCXProcess.StartInfo.UseShellExecute = false; + VRCXProcess.Start(); + System.Environment.Exit(0); + } + } +} diff --git a/VRCX.csproj b/VRCX.csproj index ce0d19ad..05c1fe78 100644 --- a/VRCX.csproj +++ b/VRCX.csproj @@ -66,6 +66,8 @@ + + @@ -79,6 +81,7 @@ + diff --git a/html/src/app.js b/html/src/app.js index 50de537a..83795c7e 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -3749,6 +3749,7 @@ speechSynthesis.getVoices(); API, nextCurrentUserRefresh: 0, nextFriendsRefresh: 0, + nextAppUpdateCheck: 0, isGameRunning: false, isGameNoVR: false, appVersion, @@ -3762,7 +3763,6 @@ speechSynthesis.getVoices(); watch: {}, el: '#x-app', mounted() { - this.checkAppVersion(); API.$on('SHOW_WORLD_DIALOG', (tag) => this.showWorldDialog(tag)); API.$on('SHOW_LAUNCH_DIALOG', (tag) => this.showLaunchDialog(tag)); this.updateLoop(); @@ -3809,35 +3809,6 @@ speechSynthesis.getVoices(); return style; }; - $app.methods.checkAppVersion = async function () { - var response = await webApiService.execute({ - url: 'https://api.github.com/repos/pypy-vrc/VRCX/releases/latest', - method: 'GET', - headers: { - 'User-Agent': 'VRCX' - } - }); - var json = JSON.parse(response.data); - if (json === Object(json) && - json.name && - json.published_at) { - this.latestAppVersion = `${json.name} (${formatDate(json.published_at, 'YYYY-MM-DD HH24:MI:SS')})`; - if (json.name > this.appVersion) { - new Noty({ - type: 'info', - text: `Update available!!
${this.latestAppVersion}`, - timeout: 60000, - callbacks: { - onClick: () => AppApi.OpenLink('https://github.com/pypy-vrc/VRCX/releases') - } - }).show(); - this.notifyMenu('settings'); - } - } else { - this.latestAppVersion = 'Error occured'; - } - }; - $app.methods.updateLoop = function () { try { if (API.isLoggedIn === true) { @@ -3854,6 +3825,12 @@ speechSynthesis.getVoices(); API.refreshPlayerModerations(); } } + if (--this.nextAppUpdateCheck <= 0) { + this.nextAppUpdateCheck = 43200; // 6hours + if (this.autoUpdateVRCX !== 'Off') { + this.checkForVRCXUpdate(); + } + } AppApi.CheckGameRunning().then(([isGameRunning, isGameNoVR]) => { if (isGameRunning !== this.isGameRunning) { this.isGameRunning = isGameRunning; @@ -7652,6 +7629,8 @@ speechSynthesis.getVoices(); $app.data.autoSweepVRChatCache = configRepository.getBool('VRCX_autoSweepVRChatCache'); $app.data.vrBackgroundEnabled = configRepository.getBool('VRCX_vrBackgroundEnabled'); $app.data.asideWidth = configRepository.getInt('VRCX_asidewidth'); + $app.data.autoUpdateVRCX = configRepository.getString('VRCX_autoUpdateVRCX'); + $app.data.branch = configRepository.getString('VRCX_branch'); var saveOpenVROption = function () { configRepository.setBool('openVR', this.openVR); configRepository.setBool('openVRAlways', this.openVRAlways); @@ -7776,6 +7755,20 @@ speechSynthesis.getVoices(); $app.data.asideWidth = 236; configRepository.setInt('VRCX_asidewidth', $app.data.asideWidth); } + if (!configRepository.getString('VRCX_autoUpdateVRCX')) { + $app.data.autoUpdateVRCX = 'Notify'; + configRepository.setString('VRCX_autoUpdateVRCX', $app.data.autoUpdateVRCX); + } + if (!configRepository.getString('VRCX_branch')) { + $app.data.branch = 'Stable'; + if (appVersion.substring(0, 24) === 'VRCX.PyPyDance.Companion') { + $app.data.branch = 'Beta'; + } + configRepository.setString('VRCX_branch', $app.data.branch); + } + if (!configRepository.getString('VRCX_lastVRCXVersion')) { + configRepository.setString('VRCX_lastVRCXVersion', appVersion); + } if (!configRepository.getString('sharedFeedFilters')) { var sharedFeedFilters = { noty: { @@ -12461,6 +12454,12 @@ speechSynthesis.getVoices(); var { ref, type } = this.downloadCurrent; this.downloadQueue.delete(ref.id); this.downloadQueueTable.data = Array.from(this.downloadQueue.values()); + if (this.downloadCurrent.id === 'VRCXUpdate') { + var url = this.downloadCurrent.updateZipUrl; + await AssetBundleCacher.DownloadCacheFile('', url, '', 0, 0, '', appVersion, true); + this.downloadVRChatCacheProgress(); + return; + } var assetUrl = ''; for (var i = ref.unityPackages.length - 1; i > -1; i--) { var unityPackage = ref.unityPackages[i]; @@ -12512,7 +12511,7 @@ speechSynthesis.getVoices(); } var { url, md5, sizeInBytes } = file; var cacheDir = await this.getVRChatCacheDir(); - await AssetBundleCacher.DownloadCacheFile(cacheDir, url, ref.id, ref.version, sizeInBytes, md5, appVersion); + await AssetBundleCacher.DownloadCacheFile(cacheDir, url, ref.id, ref.version, sizeInBytes, md5, appVersion, false); this.downloadVRChatCacheProgress(); }; @@ -12628,7 +12627,7 @@ speechSynthesis.getVoices(); if (this.worldDialog.id === this.downloadCurrent.id) { this.updateVRChatCache(); } - if (this.downloadCurrent.type === 'manual') { + if (this.downloadCurrent.type === 'Manual') { this.$message({ message: 'World cache complete', type: 'success' @@ -12681,7 +12680,7 @@ speechSynthesis.getVoices(); if (this.worldDialog.id === this.downloadCurrent.id) { this.updateVRChatCache(); } - if (this.downloadCurrent.type === 'manual') { + if (this.downloadCurrent.type === 'Manual') { this.$message({ message: 'File already in cache', type: 'warning' @@ -12734,6 +12733,21 @@ speechSynthesis.getVoices(); this.downloadInProgress = false; this.downloadVRChatCache(); return; + case -16: + this.downloadCurrent.status = 'Success'; + this.downloadCurrent.date = Date.now(); + this.downloadHistoryTable.data.unshift(this.downloadCurrent); + if (this.downloadCurrent.autoInstall) { + this.restartVRCX(); + } else { + this.downloadDialog.visible = false; + this.showVRCXUpdateDialog(); + } + this.downloadCurrent = {}; + this.downloadProgress = 0; + this.downloadInProgress = false; + this.downloadVRChatCache(); + return; default: this.downloadProgress = downloadProgress; } @@ -13146,6 +13160,130 @@ speechSynthesis.getVoices(); configRepository.setInt('VRCX_asidewidth', this.asideWidth); }; + // VRCX auto update + + $app.data.VRCXUpdateDialog = { + visible: false, + updatePending: false, + release: '', + releases: [] + }; + + $app.data.checkingForVRCXUpdate = false; + + $app.data.branches = { + Stable: { name: 'Stable', urlReleases: 'https://api.github.com/repos/pypy-vrc/VRCX/releases', urlLatest: 'https://api.github.com/repos/pypy-vrc/VRCX/releases/latest' }, + Beta: { name: 'Beta', urlReleases: 'https://api.github.com/repos/natsumi-sama/VRCX/releases', urlLatest: 'https://api.github.com/repos/natsumi-sama/VRCX/releases/latest' } + }; + + $app.methods.showVRCXUpdateDialog = async function () { + this.$nextTick(() => adjustDialogZ(this.$refs.VRCXUpdateDialog.$el)); + var D = this.VRCXUpdateDialog; + D.visible = true; + D.updatePending = await AppApi.checkForUpdateZip(); + this.loadBranchVersions(); + }; + + $app.methods.downloadVRCXUpdate = function (updateZipUrl, name, type, autoInstall) { + var ref = { + id: 'VRCXUpdate', + name + }; + this.downloadQueue.set('VRCXUpdate', { ref, type, updateZipUrl, autoInstall }); + this.downloadQueueTable.data = Array.from(this.downloadQueue.values()); + if (!this.downloadInProgress) { + this.downloadVRChatCache(); + } + }; + + $app.methods.installVRCXUpdate = function () { + for (var release of this.VRCXUpdateDialog.releases) { + if (release.name === this.VRCXUpdateDialog.release) { + var downloadUrl = release.assets[0].browser_download_url; + var name = release.name; + var type = 'Manual'; + var autoInstall = false; + this.downloadVRCXUpdate(downloadUrl, name, type, autoInstall); + this.VRCXUpdateDialog.visible = false; + this.showDownloadDialog(); + } + } + }; + + $app.methods.restartVRCX = function () { + AppApi.RestartApplication(); + }; + + $app.methods.loadBranchVersions = async function () { + var D = this.VRCXUpdateDialog; + var url = this.branches[this.branch].urlReleases; + this.checkingForVRCXUpdate = true; + var response = await webApiService.execute({ + url, + method: 'GET', + headers: { + 'User-Agent': appVersion + } + }); + this.checkingForVRCXUpdate = false; + var json = JSON.parse(response.data); + D.releases = json; + D.release = json[0].name; + if (configRepository.getString('VRCX_branch') !== this.branch) { + configRepository.setString('VRCX_branch', this.branch); + } + }; + + $app.methods.saveAutoUpdateVRCX = function () { + configRepository.setString('VRCX_autoUpdateVRCX', this.autoUpdateVRCX); + }; + + $app.methods.checkForVRCXUpdate = async function () { + if (await AppApi.checkForUpdateZip()) { + return; + } + var url = this.branches[this.branch].urlLatest; + this.checkingForVRCXUpdate = true; + var response = await webApiService.execute({ + url, + method: 'GET', + headers: { + 'User-Agent': appVersion + } + }); + this.checkingForVRCXUpdate = false; + var json = JSON.parse(response.data); + if (json === Object(json) && + json.name && + json.published_at) { + this.latestAppVersion = `${json.name} (${formatDate(json.published_at, 'YYYY-MM-DD HH24:MI:SS')})`; + if (json.name > this.appVersion) { + if ((json.assets[0].content_type !== 'application/x-zip-compressed') || (json.assets[0].state !== 'uploaded')) { + return; + } + this.notifyMenu('settings'); + var downloadUrl = json.assets[0].browser_download_url; + var name = json.name; + var type = 'Auto'; + if (this.autoUpdateVRCX === 'Notify') { + this.showVRCXUpdateDialog(); + } else if (this.autoUpdateVRCX === 'Auto Download') { + if (downloadUrl) { + var autoInstall = false; + this.downloadVRCXUpdate(downloadUrl, name, type, autoInstall); + } + } else if (this.autoUpdateVRCX === 'Auto Install') { + if (downloadUrl) { + var autoInstall = true; + this.downloadVRCXUpdate(downloadUrl, name, type, autoInstall); + } + } + } + } else { + this.latestAppVersion = 'Error occured'; + } + }; + $app = new Vue($app); window.$app = $app; }()); diff --git a/html/src/index.pug b/html/src/index.pug index 55bec5cc..3e0c9fd9 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -652,7 +652,7 @@ html .detail span.name Version span.extra(v-text="appVersion") - .x-friend-item(@click="checkAppVersion()") + .x-friend-item(style="cursor:default") .detail span.name Latest Version span.extra(v-if="latestAppVersion" v-text="latestAppVersion") @@ -661,6 +661,18 @@ html .detail span.name Repository URL span.extra https://github.com/pypy-vrc/VRCX + div.options-container + span.sub-header VRCX Updater + div.options-container-item + el-button(size="small" icon="el-icon-upload" @click="showVRCXUpdateDialog()") Check for update + div.options-container-item + span.name Auto update: + br + el-radio-group(v-model="autoUpdateVRCX" @change="saveAutoUpdateVRCX" size="mini") + el-radio-button(label="Off") + el-radio-button(label="Notify") + el-radio-button(label="Auto Download") + el-radio-button(label="Auto Install") div.options-container span.header Appearance div.options-container-item @@ -1678,7 +1690,7 @@ html template(v-if="downloadQueueTable.data.length >= 1") span(style="margin-top:15px") Queue: data-tables(v-bind="downloadQueueTable" style="margin-top:10px") - el-table-column(label="World Name" prop="name") + el-table-column(label="Name" prop="name") template(v-once #default="scope") span.x-link(v-text="scope.row.ref.name" @click="showWorldDialog(scope.row.location)") el-table-column(label="User Name" prop="name" width="150") @@ -1693,9 +1705,12 @@ html el-table-column(label="Time" prop="date" width="90") template(v-once #default="scope") timer(:epoch="scope.row.date") - el-table-column(label="World Name" prop="name") + el-table-column(label="Name" prop="name") template(v-once #default="scope") - span.x-link(v-text="scope.row.ref.name" @click="showWorldDialog(scope.row.location)") + template(v-if="scope.row.ref.id === 'VRCXUpdate'") + el-button(size="small" @click="showVRCXUpdateDialog") VRCX Update + template(v-else) + span.x-link(v-text="scope.row.ref.name" @click="showWorldDialog(scope.row.location)") el-table-column(label="User Name" prop="name" width="150") template(v-once #default="scope") span.x-link(v-text="getDisplayName(scope.row.userId)" @click="showUserDialog(scope.row.userId)") @@ -1704,6 +1719,20 @@ html template(#footer) el-button(v-if="downloadQueue.size >= 1" size="small" @click="cancelAllVRChatCacheDownload") Cancel All el-button(size="small" @click="downloadDialog.visible = false") Close + + //- dialog: update VRCX + el-dialog.x-dialog(ref="VRCXUpdateDialog" :visible.sync="VRCXUpdateDialog.visible" title="VRCX Updater" width="400px") + div(v-loading="checkingForVRCXUpdate" style="margin-top:15px") + template(v-if="VRCXUpdateDialog.updatePending") + span Update ready for install, restart VRCX to apply. + template(v-else) + el-select(v-model="branch" @change="loadBranchVersions" style="display:inline-block;width:150px;margin-right:15px") + el-option(v-once v-for="branch in branches" :key="branch.name" :label="branch.name" :value="branch.name") + el-select(v-model="VRCXUpdateDialog.release" style="display:inline-block;width:150px") + el-option(v-for="item in VRCXUpdateDialog.releases" :key="item.name" :label="item.tag_name" :value="item.name") + template(#footer) + el-button(v-if="!VRCXUpdateDialog.updatePending && VRCXUpdateDialog.release !== appVersion" type="primary" size="small" @click="installVRCXUpdate") Download + el-button(v-if="VRCXUpdateDialog.updatePending" type="primary" size="small" @click="restartVRCX") Install //- dialog: launch el-dialog.x-dialog(ref="launchDialog" :visible.sync="launchDialog.visible" title="Launch" width="400px")