import { computed, ref } from 'vue'; import { defineStore } from 'pinia'; import { toast } from 'vue-sonner'; import { useI18n } from 'vue-i18n'; import { logWebRequest } from '../services/appConfig'; import { branches } from '../shared/constants'; import { getLatestWhatsNewRelease, getWhatsNewRelease, normalizeReleaseVersion } from '../shared/constants/whatsNewReleases'; import { changeLogRemoveLinks } from '../shared/utils'; import configRepository from '../services/config'; import * as workerTimers from 'worker-timers'; const emptyWhatsNewDialog = () => ({ visible: false, titleKey: '', items: [] }); export const useVRCXUpdaterStore = defineStore('VRCXUpdater', () => { const { t } = useI18n(); const arch = ref('x64'); const noUpdater = ref(false); const isMacOS = computed(() => navigator.platform.includes('Mac')); const appVersion = ref(''); const autoUpdateVRCX = ref('Auto Download'); const latestAppVersion = ref(''); const branch = ref('Stable'); const vrcxId = ref(''); const checkingForVRCXUpdate = ref(false); const VRCXUpdateDialog = ref({ visible: false, updatePending: false, updatePendingIsLatest: false, release: '', releases: [] }); const changeLogDialog = ref({ visible: false, buildName: '', changeLog: '' }); const whatsNewDialog = ref(emptyWhatsNewDialog()); const pendingVRCXUpdate = ref(false); const pendingVRCXInstall = ref(''); const updateInProgress = ref(false); const updateProgress = ref(0); const updateToastRelease = ref(''); async function initVRCXUpdaterSettings() { if (LINUX) { arch.value = await window.electron.getArch(); noUpdater.value = await window.electron.getNoUpdater(); console.log('Architecture:', arch.value); } if (isMacOS.value) { noUpdater.value = true; } const [VRCX_autoUpdateVRCX, VRCX_id] = await Promise.all([ configRepository.getString('VRCX_autoUpdateVRCX', 'Auto Download'), configRepository.getString('VRCX_id', '') ]); if (VRCX_autoUpdateVRCX === 'Auto Install') { autoUpdateVRCX.value = 'Auto Download'; } else { autoUpdateVRCX.value = VRCX_autoUpdateVRCX; } if (noUpdater.value) { autoUpdateVRCX.value = 'Off'; } appVersion.value = await AppApi.GetVersion(); vrcxId.value = VRCX_id; await initBranch(); await loadVrcxId(); let checkedForUpdatesDuringAnnouncement = false; if (await shouldAnnounceCurrentVersion()) { const shown = await showWhatsNewDialog(); if (shown) { await markCurrentVersionAsSeen(); } else if (isRecognizedStableReleaseVersion()) { const result = await showChangeLogDialog({ prefetch: true }); checkedForUpdatesDuringAnnouncement = result.checkedForUpdates; if (result.shown) { await markCurrentVersionAsSeen(); } } } else { await syncCurrentVersionState(); } if ( autoUpdateVRCX.value !== 'Off' && !checkedForUpdatesDuringAnnouncement ) { await checkForVRCXUpdate(); } } const currentVersion = computed(() => appVersion.value.replace(' (Linux)', '') ); /** * @param {string} value */ async function setAutoUpdateVRCX(value) { if (value === 'Off') { pendingVRCXUpdate.value = false; } autoUpdateVRCX.value = value; await configRepository.setString('VRCX_autoUpdateVRCX', value); } /** * @param {string} value */ function setLatestAppVersion(value) { latestAppVersion.value = value; } /** * @param {string} value */ function setBranch(value) { branch.value = value; configRepository.setString('VRCX_branch', value); } async function initBranch() { if (!appVersion.value) { return; } if (currentVersion.value.includes('VRCX Nightly')) { branch.value = 'Nightly'; } else { branch.value = 'Stable'; } await configRepository.setString('VRCX_branch', branch.value); } async function hasVersionChanged() { const lastVersion = await configRepository.getString( 'VRCX_lastVRCXVersion', '' ); return lastVersion !== currentVersion.value; } async function markCurrentVersionAsSeen() { await configRepository.setString( 'VRCX_lastVRCXVersion', currentVersion.value ); } async function syncCurrentVersionState() { if (await hasVersionChanged()) { await markCurrentVersionAsSeen(); return true; } return false; } async function shouldAnnounceCurrentVersion() { if (branch.value !== 'Stable' || !isRecognizedStableReleaseVersion()) { return false; } const lastVersion = await configRepository.getString( 'VRCX_lastVRCXVersion', '' ); return Boolean(lastVersion) && lastVersion !== currentVersion.value; } function isRecognizedStableReleaseVersion() { return Boolean(normalizeReleaseVersion(currentVersion.value)); } /** * @returns {Promise} */ async function showWhatsNewDialog() { const release = getWhatsNewRelease(currentVersion.value); if (!release) { whatsNewDialog.value = emptyWhatsNewDialog(); return false; } whatsNewDialog.value = { visible: true, titleKey: release.titleKey, items: release.items.map((item) => ({ ...item })) }; return true; } /** * @returns {boolean} */ function showLatestWhatsNewDialog() { const release = getLatestWhatsNewRelease(); if (!release) { return false; } whatsNewDialog.value = { visible: true, titleKey: release.titleKey, items: release.items.map((item) => ({ ...item })) }; return true; } function closeWhatsNewDialog() { whatsNewDialog.value.visible = false; } async function openChangeLogDialogOnly() { changeLogDialog.value.visible = true; if ( !changeLogDialog.value.buildName || !changeLogDialog.value.changeLog ) { await checkForVRCXUpdate(); } } async function loadVrcxId() { if (!vrcxId.value) { vrcxId.value = crypto.randomUUID(); await configRepository.setString('VRCX_id', vrcxId.value); } } function getAssetOfInterest(assets) { let downloadUrl = ''; let hashString = ''; let size = 0; for (const asset of assets) { if (asset.state !== 'uploaded') { continue; } if ( WINDOWS && asset.name.endsWith('.exe') && (asset.content_type === 'application/x-msdownload' || asset.content_type === 'application/x-msdos-program') ) { downloadUrl = asset.browser_download_url; if (asset.digest && asset.digest.startsWith('sha256:')) { hashString = asset.digest.replace('sha256:', ''); } size = asset.size; break; } if ( LINUX && asset.name.endsWith(`${arch.value}.AppImage`) && asset.content_type === 'application/octet-stream' ) { downloadUrl = asset.browser_download_url; if (asset.digest && asset.digest.startsWith('sha256:')) { hashString = asset.digest.replace('sha256:', ''); } size = asset.size; break; } } return { downloadUrl, hashString, size }; } async function checkForVRCXUpdate() { if ( !currentVersion.value || currentVersion.value === 'VRCX Nightly Build' || currentVersion.value === 'VRCX Build' ) { // ignore custom builds return false; } if (branch.value === 'Beta') { // move Beta users to stable setBranch('Stable'); } if (typeof branches[branch.value] === 'undefined') { // handle invalid branch setBranch('Stable'); } const url = branches[branch.value].urlLatest; checkingForVRCXUpdate.value = true; let response; let json; try { response = await webApiService.execute({ url, method: 'GET', headers: { 'VRCX-ID': vrcxId.value } }); json = JSON.parse(response.data); } catch (error) { console.error('Failed to check for VRCX update', error); return false; } finally { checkingForVRCXUpdate.value = false; } if (response.status !== 200) { toast.error( t('message.vrcx_updater.failed', { message: `${response.status} ${response.data}` }) ); return false; } pendingVRCXUpdate.value = false; logWebRequest('[EXTERNAL GET]', url, `(${response.status})`, json); if (json === Object(json) && json.name && json.published_at) { changeLogDialog.value.buildName = json.name; changeLogDialog.value.changeLog = changeLogRemoveLinks(json.body); const releaseName = json.name; setLatestAppVersion(releaseName); VRCXUpdateDialog.value.updatePendingIsLatest = false; if (autoUpdateVRCX.value === 'Off') { return true; } if (releaseName === pendingVRCXInstall.value) { // update already downloaded VRCXUpdateDialog.value.updatePendingIsLatest = true; } else if (releaseName > currentVersion.value) { const { downloadUrl, hashString, size } = getAssetOfInterest( json.assets ); if (!downloadUrl) { return true; } pendingVRCXUpdate.value = true; if (updateToastRelease.value !== releaseName) { updateToastRelease.value = releaseName; toast(t('nav_menu.update_available'), { description: releaseName, duration: 5000, action: { label: t('nav_menu.update'), onClick: () => showVRCXUpdateDialog() } }); } if (autoUpdateVRCX.value === 'Notify') { // this.showVRCXUpdateDialog(); } else if (autoUpdateVRCX.value === 'Auto Download') { await downloadVRCXUpdate( downloadUrl, hashString, size, releaseName ); } } return true; } return false; } async function showVRCXUpdateDialog() { const D = VRCXUpdateDialog.value; D.visible = true; D.updatePendingIsLatest = false; D.updatePending = await AppApi.CheckForUpdateExe(); if (updateInProgress.value) { return; } await loadBranchVersions(); } async function loadBranchVersions() { const D = VRCXUpdateDialog.value; const url = branches[branch.value].urlReleases; checkingForVRCXUpdate.value = true; let response; let json; try { response = await webApiService.execute({ url, method: 'GET', headers: { 'VRCX-ID': vrcxId.value } }); json = JSON.parse(response.data); } catch (error) { console.error('Failed to check for VRCX update', error); return; } finally { checkingForVRCXUpdate.value = false; } if (response.status !== 200) { toast.error( t('message.vrcx_updater.failed', { message: `${response.status} ${response.data}` }) ); return; } logWebRequest('[EXTERNAL GET]', url, `(${response.status})`, json); const releases = []; if (typeof json !== 'object' || json.message) { toast.error( t('message.vrcx_updater.failed', { message: json.message }) ); return; } for (const release of json) { if (release.prerelease) { continue; } assetLoop: for (const asset of release.assets) { if (asset.state === 'uploaded') { releases.push(release); break assetLoop; } } } D.releases = releases; D.release = json[0].name; VRCXUpdateDialog.value.updatePendingIsLatest = false; if (D.release === pendingVRCXInstall.value) { // update already downloaded and latest version VRCXUpdateDialog.value.updatePendingIsLatest = true; } setBranch(branch.value); } async function downloadVRCXUpdate( downloadUrl, hashString, size, releaseName ) { if (updateInProgress.value) { return; } try { updateInProgress.value = true; await downloadFileProgress(); await AppApi.DownloadUpdate(downloadUrl, hashString, size); pendingVRCXInstall.value = releaseName; } catch (err) { console.error(err); toast.error(`${t('message.vrcx_updater.failed_install')} ${err}`); } finally { updateInProgress.value = false; updateProgress.value = 0; } } async function downloadFileProgress() { updateProgress.value = await AppApi.CheckUpdateProgress(); if (updateInProgress.value) { workerTimers.setTimeout(() => downloadFileProgress(), 150); } } function installVRCXUpdate() { for (const release of VRCXUpdateDialog.value.releases) { if (release.name !== VRCXUpdateDialog.value.release) { continue; } const { downloadUrl, hashString, size } = getAssetOfInterest( release.assets ); if (!downloadUrl) { return; } const releaseName = release.name; downloadVRCXUpdate(downloadUrl, hashString, size, releaseName); break; } } async function showChangeLogDialog(options = {}) { const { prefetch = false } = options; if (prefetch) { const loaded = await ensureChangeLogReady(); if (!loaded) { return { shown: false, checkedForUpdates: true }; } changeLogDialog.value.visible = true; return { shown: true, checkedForUpdates: true }; } changeLogDialog.value.visible = true; void ensureChangeLogReady(); return { shown: true, checkedForUpdates: true }; } async function ensureChangeLogReady() { if ( changeLogDialog.value.buildName && changeLogDialog.value.changeLog ) { return true; } return checkForVRCXUpdate(); } function restartVRCX(isUpgrade) { if (!LINUX) { AppApi.RestartApplication(isUpgrade); } else { window.electron.restartApp(); } } function updateProgressText() { if (updateProgress.value === 100) { return t('message.vrcx_updater.checking_hash'); } return `${updateProgress.value}%`; } async function cancelUpdate() { await AppApi.CancelUpdate(); updateInProgress.value = false; updateProgress.value = 0; } initVRCXUpdaterSettings(); return { appVersion, autoUpdateVRCX, latestAppVersion, branch, currentVersion, vrcxId, checkingForVRCXUpdate, VRCXUpdateDialog, changeLogDialog, whatsNewDialog, pendingVRCXUpdate, pendingVRCXInstall, updateInProgress, updateProgress, noUpdater, setAutoUpdateVRCX, setBranch, showWhatsNewDialog, showLatestWhatsNewDialog, closeWhatsNewDialog, openChangeLogDialogOnly, checkForVRCXUpdate, loadBranchVersions, installVRCXUpdate, showVRCXUpdateDialog, showChangeLogDialog, restartVRCX, updateProgressText, cancelUpdate }; });