From aad1c72bef7589746d730bfbdce755d12971d194 Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Sat, 13 Jun 2026 17:21:04 +0200 Subject: [PATCH] feat: autoupdater --- app/composables/useElectron.ts | 48 +++++++++++++++ app/layouts/default.vue | 73 +++++++++++++++++++++- electron/main/index.ts | 103 +++++++++++++++++++++++++++++++ electron/preload/index.ts | 25 ++++++++ i18n/locales/en.json | 13 +++- i18n/locales/es.json | 13 +++- package.json | 1 + pnpm-lock.yaml | 108 +++++++++++++++++++++++++++++++++ 8 files changed, 380 insertions(+), 4 deletions(-) diff --git a/app/composables/useElectron.ts b/app/composables/useElectron.ts index 61fb13d..97cf81f 100644 --- a/app/composables/useElectron.ts +++ b/app/composables/useElectron.ts @@ -31,6 +31,21 @@ export interface StreamingClosePromptOptions { cancelLabel: string; } +export type UpdateStatus = + | "checking" + | "available" + | "not-available" + | "download-progress" + | "downloaded" + | "error"; + +export interface UpdateStatusPayload { + status: UpdateStatus; + version?: string; + percent?: number; + message?: string; +} + interface HeliumElectronAPI { isElectron: boolean; getPlatform: () => Promise; @@ -48,6 +63,9 @@ interface HeliumElectronAPI { active: boolean, promptOptions?: StreamingClosePromptOptions, ) => Promise; + checkForUpdates: () => Promise; + installUpdate: () => Promise; + onUpdateStatus: (callback: (payload: UpdateStatusPayload) => void) => () => void; } declare global { @@ -263,6 +281,33 @@ export function useElectron() { } }; + const checkForUpdates = async (): Promise => { + if (!checkElectron()) return false; + + try { + return await window.heliumElectron!.checkForUpdates(); + } catch (error) { + console.error("[useElectron] Failed to check for updates:", error); + return false; + } + }; + + const installUpdate = async (): Promise => { + if (!checkElectron()) return false; + + try { + return await window.heliumElectron!.installUpdate(); + } catch (error) { + console.error("[useElectron] Failed to install update:", error); + return false; + } + }; + + const onUpdateStatus = (callback: (payload: UpdateStatusPayload) => void): (() => void) => { + if (!checkElectron()) return () => {}; + return window.heliumElectron!.onUpdateStatus(callback); + }; + onMounted(() => { checkElectron(); if (isElectron.value) { @@ -300,6 +345,9 @@ export function useElectron() { getScreenPermissionStatus, openScreenPermissionSettings, setStreamingActive, + checkForUpdates, + installUpdate, + onUpdateStatus, startScreenShareWithAudio, stopScreenShare, diff --git a/app/layouts/default.vue b/app/layouts/default.vue index 91c4222..1351623 100644 --- a/app/layouts/default.vue +++ b/app/layouts/default.vue @@ -2,8 +2,9 @@ import SignInDialog from "~/components/app/SignInDialog.vue"; import ThemeDropdown from "~/components/ui/ThemeDropdown.vue"; import LanguageSwitcher from "~/components/app/LanguageSwitcher.vue"; -import { useElectron } from "~/composables/useElectron"; +import { useElectron, type UpdateStatusPayload } from "~/composables/useElectron"; import "vue-sonner/style.css"; +import { toast } from "vue-sonner"; import { Toaster } from "@/components/ui/sonner"; import { Sheet, @@ -17,7 +18,15 @@ import LogoSvg from "~/assets/logo.svg?component"; const { t } = useI18n(); const mobileMenuOpen = ref(false); -const { isElectron, platformInfo, getPlatformInfo } = useElectron(); +const { + isElectron, + platformInfo, + getPlatformInfo, + installUpdate, + onUpdateStatus, +} = useElectron(); +let removeUpdateStatusListener: (() => void) | undefined; +let updateProgressToastId: string | number | undefined; const isMacElectron = computed(() => { return isElectron.value && platformInfo.value?.isMac; @@ -40,6 +49,66 @@ const navLinks = [ const visibleNavLinks = computed(() => { return navLinks.filter((link) => !isElectron.value || !link.hideInElectron); }); + +const showUpdateMessage = (payload: UpdateStatusPayload): void => { + if (payload.status === "checking") { + return; + } + + if (payload.status === "available") { + toast.info(t("updateAvailable"), { + description: payload.version + ? t("updateAvailableDescription", { version: payload.version }) + : t("updateAvailableDescriptionWithoutVersion"), + }); + return; + } + + if (payload.status === "not-available") { + return; + } + + if (payload.status === "download-progress") { + const percent = payload.percent ?? 0; + updateProgressToastId = toast.loading(t("updateDownloading"), { + id: updateProgressToastId, + description: t("updateDownloadProgress", { percent }), + }); + return; + } + + if (payload.status === "downloaded") { + if (updateProgressToastId) { + toast.dismiss(updateProgressToastId); + updateProgressToastId = undefined; + } + + toast.success(t("updateReady"), { + description: payload.version + ? t("updateReadyDescription", { version: payload.version }) + : t("updateReadyDescriptionWithoutVersion"), + action: { + label: t("restartToUpdate"), + onClick: () => { + void installUpdate(); + }, + }, + }); + return; + } + + toast.error(t("updateFailed"), { + description: payload.message || t("updateFailedDescription"), + }); +}; + +onMounted(() => { + removeUpdateStatusListener = onUpdateStatus(showUpdateMessage); +}); + +onUnmounted(() => { + removeUpdateStatusListener?.(); +});