Skip to content

Commit

Permalink
electron: Offer automatic app update
Browse files Browse the repository at this point in the history
With this new implementation, the user is informed that a new version of the application is available and can choose between downloading it or not.
  • Loading branch information
rafaellehmkuhl committed Dec 9, 2024
1 parent 45a5977 commit 93c997d
Show file tree
Hide file tree
Showing 6 changed files with 296 additions and 6 deletions.
24 changes: 18 additions & 6 deletions electron/main.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { app, BrowserWindow, protocol, screen } from 'electron'
import logger from 'electron-log'
import { join } from 'path'

import { setupAutoUpdater } from './services/auto-update'
import { setupNetworkService } from './services/network'

// If the app is packaged, push logs to the system instead of the console
if (app.isPackaged) {
Object.assign(console, logger.functions)
}

export const ROOT_PATH = {
dist: join(__dirname, '..'),
}
Expand All @@ -25,11 +32,6 @@ function createWindow(): void {
height,
})

// Test active push message to Renderer-process.
mainWindow.webContents.on('did-finish-load', () => {
mainWindow?.webContents.send('main-process-message', new Date().toLocaleString())
})

if (process.env.VITE_DEV_SERVER_URL) {
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL)
} else {
Expand Down Expand Up @@ -63,7 +65,17 @@ protocol.registerSchemesAsPrivileged([

setupNetworkService()

app.whenReady().then(createWindow)
app.whenReady().then(async () => {
console.log('Electron app is ready.')
console.log(`Cockpit version: ${app.getVersion()}`)

console.log('Creating window...')
createWindow()

setTimeout(() => {
setupAutoUpdater(mainWindow as BrowserWindow)
}, 5000)
})

app.on('before-quit', () => {
// @ts-ignore: import.meta.env does not exist in the types
Expand Down
12 changes: 12 additions & 0 deletions electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,16 @@ import { contextBridge, ipcRenderer } from 'electron'

contextBridge.exposeInMainWorld('electronAPI', {
getInfoOnSubnets: () => ipcRenderer.invoke('get-info-on-subnets'),
onUpdateAvailable: (callback: (info: any) => void) =>
ipcRenderer.on('update-available', (_event, info) => callback(info)),
onUpdateDownloaded: (callback: (info: any) => void) =>
ipcRenderer.on('update-downloaded', (_event, info) => callback(info)),
onCheckingForUpdate: (callback: () => void) => ipcRenderer.on('checking-for-update', () => callback()),
onUpdateNotAvailable: (callback: (info: any) => void) =>
ipcRenderer.on('update-not-available', (_event, info) => callback(info)),
onDownloadProgress: (callback: (info: any) => void) =>
ipcRenderer.on('download-progress', (_event, info) => callback(info)),
downloadUpdate: () => ipcRenderer.send('download-update'),
installUpdate: () => ipcRenderer.send('install-update'),
cancelUpdate: () => ipcRenderer.send('cancel-update'),
})
51 changes: 51 additions & 0 deletions electron/services/auto-update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { BrowserWindow, ipcMain } from 'electron'
import electronUpdater, { type AppUpdater } from 'electron-updater'

/**
* Setup auto updater
* @param {BrowserWindow} mainWindow - The main Electron window
*/
export const setupAutoUpdater = (mainWindow: BrowserWindow): void => {
const autoUpdater: AppUpdater = electronUpdater.autoUpdater
autoUpdater.logger = console
autoUpdater.autoDownload = false // Prevent automatic downloads

autoUpdater
.checkForUpdates()
.then((e) => console.info(e))
.catch((e) => console.error(e))

autoUpdater.on('checking-for-update', () => {
mainWindow.webContents.send('checking-for-update')
})

autoUpdater.on('update-available', (info) => {
mainWindow.webContents.send('update-available', info)
})

autoUpdater.on('update-not-available', (info) => {
mainWindow.webContents.send('update-not-available', info)
})

autoUpdater.on('download-progress', (progressInfo) => {
mainWindow.webContents.send('download-progress', progressInfo)
})

autoUpdater.on('update-downloaded', (info) => {
mainWindow.webContents.send('update-downloaded', info)
})

// Add handlers for update control
ipcMain.on('download-update', () => {
autoUpdater.downloadUpdate()
})

ipcMain.on('install-update', () => {
autoUpdater.quitAndInstall()
})

ipcMain.on('cancel-update', () => {
autoUpdater.removeAllListeners('update-downloaded')
autoUpdater.removeAllListeners('download-progress')
})
}
2 changes: 2 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@
<Tutorial :show-tutorial="interfaceStore.isTutorialVisible" />
<VideoLibraryModal :open-modal="interfaceStore.isVideoLibraryVisible" />
<VehicleDiscoveryDialog v-model="showDiscoveryDialog" show-auto-search-option />
<UpdateNotification v-if="isElectron()" />
</template>

<script setup lang="ts">
Expand All @@ -325,6 +326,7 @@ import { useRoute } from 'vue-router'
import GlassModal from '@/components/GlassModal.vue'
import Tutorial from '@/components/Tutorial.vue'
import UpdateNotification from '@/components/UpdateNotification.vue'
import VehicleDiscoveryDialog from '@/components/VehicleDiscoveryDialog.vue'
import VideoLibraryModal from '@/components/VideoLibraryModal.vue'
import { useInteractionDialog } from '@/composables/interactionDialog'
Expand Down
181 changes: 181 additions & 0 deletions src/components/UpdateNotification.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
<template>
<InteractionDialog
v-model="showUpdateDialog"
:title="dialogTitle"
:message="dialogMessage"
:variant="dialogVariant"
:actions="dialogActions"
max-width="560"
>
<template #content>
<div v-if="updateInfo" class="mt-2">
<strong>Update Details:</strong>
<p>Current Version: {{ app_version.version }}</p>
<p>New Version: {{ updateInfo.version }}</p>
<p>Release Date: {{ formatDate(updateInfo.releaseDate) }}</p>
</div>
<v-progress-linear
v-if="showProgress"
:model-value="downloadProgress"
color="primary"
height="25"
rounded
class="my-4"
>
<template #default>
<strong>{{ Math.round(downloadProgress) }}%</strong>
</template>
</v-progress-linear>
</template>
</InteractionDialog>
</template>

<script setup lang="ts">
import { useStorage } from '@vueuse/core'
import { onBeforeMount, ref } from 'vue'
import InteractionDialog, { type Action } from '@/components/InteractionDialog.vue'
import { app_version } from '@/libs/cosmos'
import { isElectron } from '@/libs/utils'
const showUpdateDialog = ref(false)
const dialogTitle = ref('')
const dialogMessage = ref('')
const dialogVariant = ref<'error' | 'info' | 'success' | 'warning' | 'text-only'>('info')
const showProgress = ref(false)
const downloadProgress = ref(0)
const dialogActions = ref<Action[]>([])
const updateInfo = ref({
version: '',
releaseDate: '',
releaseNotes: '',
})
const ignoredUpdateVersions = useStorage<string[]>('cockpit-ignored-update-versions', [])
const formatDate = (date: string): string => {
return new Date(date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })
}
onBeforeMount(() => {
if (!isElectron()) {
console.info('Not in Electron environment. UpdateNotification will not be initialized.')
return
}
if (!window.electronAPI) {
console.error('window.electronAPI is not defined. UpdateNotification will not be initialized.')
return
}
// Listen for update events
window.electronAPI.onCheckingForUpdate(() => {
console.log('Checking if there are updates for the Electron app...')
dialogTitle.value = 'Checking for Updates'
dialogMessage.value = 'Looking for new versions of the application...'
dialogVariant.value = 'info'
dialogActions.value = []
showProgress.value = false
showUpdateDialog.value = true
})
window.electronAPI.onUpdateNotAvailable(() => {
console.log('No updates available for the Electron app.')
dialogTitle.value = 'No Updates Available'
dialogMessage.value = 'You are running the latest version of the application.'
dialogVariant.value = 'success'
dialogActions.value = [
{
text: 'OK',
action: () => {
showUpdateDialog.value = false
},
},
]
showProgress.value = false
})
window.electronAPI.onUpdateAvailable((info) => {
console.log('Update available for the Electron app.', info)
dialogTitle.value = 'Update Available'
dialogMessage.value = 'A new version of the application is available. Would you like to download it now?'
dialogVariant.value = 'info'
updateInfo.value = { ...info }
dialogActions.value = [
{
text: 'Ignore This Version',
action: () => {
console.log(`User chose to ignore version ${updateInfo.value.version}`)
ignoredUpdateVersions.value.push(updateInfo.value.version)
window.electronAPI!.cancelUpdate()
showUpdateDialog.value = false
},
},
{
text: 'Download',
action: () => {
window.electronAPI!.downloadUpdate()
showProgress.value = true
dialogActions.value = [
{
text: 'Cancel',
action: () => {
console.log('User chose to cancel the update for the Electron app.')
window.electronAPI!.cancelUpdate()
showUpdateDialog.value = false
dialogMessage.value = 'Downloading update...'
},
},
]
},
},
{
text: 'Not Now',
action: () => {
window.electronAPI!.cancelUpdate()
showUpdateDialog.value = false
},
},
]
// Check if this version is in the ignored list
if (ignoredUpdateVersions.value.includes(info.version)) {
console.log(`Skipping ignored version ${info.version}.`)
showUpdateDialog.value = false
return
}
showUpdateDialog.value = true
})
window.electronAPI.onDownloadProgress((progressInfo) => {
downloadProgress.value = progressInfo.percent
})
window.electronAPI.onUpdateDownloaded(() => {
console.log('Finished downloading the update for the Electron app.')
dialogTitle.value = 'Update Ready to Install'
dialogMessage.value =
'The update has been downloaded. Would you like to install it now? The application will restart during installation.'
dialogVariant.value = 'info'
showProgress.value = false
dialogActions.value = [
{
text: 'Install Now',
action: () => {
console.log('User chose to install the update for the Electron app now.')
window.electronAPI!.installUpdate()
showUpdateDialog.value = false
},
},
{
text: 'Later',
action: () => {
console.log('User chose to install the update for the Electron app later.')
showUpdateDialog.value = false
},
},
]
showUpdateDialog.value = true
})
})
</script>
32 changes: 32 additions & 0 deletions src/libs/cosmos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,38 @@ declare global {
* @returns Promise containing subnet information
*/
getInfoOnSubnets: () => Promise<NetworkInfo[]>
/**
* Register callback for update available event
*/
onUpdateAvailable: (callback: (info: any) => void) => void
/**
* Register callback for update downloaded event
*/
onUpdateDownloaded: (callback: (info: any) => void) => void
/**
* Trigger update download
*/
downloadUpdate: () => void
/**
* Trigger update installation
*/
installUpdate: () => void
/**
* Cancel ongoing update
*/
cancelUpdate: () => void
/**
* Register callback for checking for update event
*/
onCheckingForUpdate: (callback: () => void) => void
/**
* Register callback for update not available event
*/
onUpdateNotAvailable: (callback: (info: any) => void) => void
/**
* Register callback for download progress event
*/
onDownloadProgress: (callback: (info: any) => void) => void
}
}
}
Expand Down

0 comments on commit 93c997d

Please sign in to comment.