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 applicaiton is available and can choose between downloading it or not.
  • Loading branch information
rafaellehmkuhl committed Nov 6, 2024
1 parent 2c125a3 commit e4e006c
Show file tree
Hide file tree
Showing 4 changed files with 296 additions and 10 deletions.
93 changes: 83 additions & 10 deletions electron/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { app, BrowserWindow, protocol, screen } from 'electron'
import { app, BrowserWindow, ipcMain, protocol, screen } from 'electron'
import electronUpdater, { type AppUpdater } from 'electron-updater'
import { join } from 'path'

export const ROOT_PATH = {
Expand All @@ -7,32 +8,47 @@ export const ROOT_PATH = {

let mainWindow: BrowserWindow | null

/**
* Get auto updater instance
* @returns {AppUpdater}
* @see https://www.electron.build/auto-update
*/
function getAutoUpdater(): AppUpdater {
// Using destructuring to access autoUpdater due to the CommonJS module of 'electron-updater'.
// It is a workaround for ESM compatibility issues, see https://github.com/electron-userland/electron-builder/issues/7976.
const { autoUpdater } = electronUpdater
autoUpdater.logger = require('electron-log')
// @ts-ignore
autoUpdater.logger.transports.file.level = 'info'
return autoUpdater
}

/**
* Create electron window
*/
function createWindow(): void {
async function createWindow(): Promise<void> {
const { width, height } = screen.getPrimaryDisplay().workAreaSize
mainWindow = new BrowserWindow({
icon: join(ROOT_PATH.dist, 'pwa-512x512.png'),
webPreferences: {
webSecurity: false,
contextIsolation: false,
nodeIntegration: true,
preload: join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
allowRunningInsecureContent: true,
},
width,
height,
})

// Test active push message to Renderer-process.
mainWindow.webContents.on('did-finish-load', () => {
mainWindow?.webContents.send('main-process-message', new Date().toLocaleString())
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)
mainWindow!.loadURL(process.env.VITE_DEV_SERVER_URL)
} else {
mainWindow.loadFile(join(ROOT_PATH.dist, 'index.html'))
mainWindow!.loadFile(join(ROOT_PATH.dist, 'index.html'))
}
}

Expand Down Expand Up @@ -60,7 +76,64 @@ protocol.registerSchemesAsPrivileged([
},
])

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

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

console.log('Setting up auto updater...')
setTimeout(() => {
setupAutoUpdater()
}, 10000)
})

const setupAutoUpdater = (): void => {
const autoUpdater = getAutoUpdater()
autoUpdater.forceDevUpdateConfig = true // TODO: Remove this before merging
autoUpdater.autoDownload = false // Prevent automatic downloads

autoUpdater
.checkForUpdates()
.then((e) => console.log(e))
.catch((e) => console.log(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', () => {
// Cancel any ongoing download
autoUpdater.removeAllListeners('update-downloaded')
autoUpdater.removeAllListeners('download-progress')
})
}

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

contextBridge.exposeInMainWorld('electronAPI', {
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', (_event) => 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'),
})
3 changes: 3 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@
<About v-if="showAboutDialog" @update:show-about-dialog="showAboutDialog = $event" />
<Tutorial :show-tutorial="interfaceStore.isTutorialVisible" />
<VideoLibraryModal :open-modal="interfaceStore.isVideoLibraryVisible" />
<UpdateNotification v-if="isElectron()" />
</template>

<script setup lang="ts">
Expand All @@ -324,6 +325,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 VideoLibraryModal from '@/components/VideoLibraryModal.vue'
import { useInteractionDialog } from '@/composables/interactionDialog'
import {
Expand All @@ -339,6 +341,7 @@ import GlassButton from './components/GlassButton.vue'
import MiniWidgetContainer from './components/MiniWidgetContainer.vue'
import SlideToConfirm from './components/SlideToConfirm.vue'
import { useSnackbar } from './composables/snackbar'
import { isElectron } from './libs/utils'
import { useAppInterfaceStore } from './stores/appInterface'
import { useMainVehicleStore } from './stores/mainVehicle'
import { useWidgetManagerStore } from './stores/widgetManager'
Expand Down
193 changes: 193 additions & 0 deletions src/components/UpdateNotification.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
<template>
<InteractionDialog
v-model="showUpdateDialog"
:title="dialogTitle"
:message="dialogMessage"
:variant="dialogVariant"
:actions="dialogActions"
>
<template v-if="showProgress" #content>
<v-progress-linear :model-value="downloadProgress" color="primary" height="25" rounded class="mb-2">
<template #default>
<strong>{{ Math.round(downloadProgress) }}%</strong>
</template>
</v-progress-linear>
</template>
</InteractionDialog>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import InteractionDialog from '@/components/InteractionDialog.vue'
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<
Array<{
/**
* Text of the button
*/
text: string
/**
* Action to be executed when the button is clicked
*/
action: () => void
/**
* Color of the button
*/
color: string
}>
>([])
/**
* Interface for the electron API exposed through preload
*/
declare global {
/**
* Extended Window interface with Electron API
*/
interface Window {
/**
* Electron API for update management
*/
electronAPI: {
/**
* 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
}
}
}
// 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
},
color: 'primary',
},
]
showProgress.value = false
})
window.electronAPI.onUpdateAvailable(() => {
console.log('Update available for the Electron app.')
dialogTitle.value = 'Update Available'
dialogMessage.value = 'A new version of the application is available. Would you like to download it now?'
dialogVariant.value = 'info'
dialogActions.value = [
{
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
},
color: 'error',
},
]
},
color: 'primary',
},
{
text: 'Not Now',
action: () => {
window.electronAPI.cancelUpdate()
showUpdateDialog.value = false
},
color: 'error',
},
]
showUpdateDialog.value = true
})
window.electronAPI.onDownloadProgress((progressInfo) => {
downloadProgress.value = progressInfo.percent
dialogMessage.value = `Downloading update... ${Math.round(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
},
color: 'primary',
},
{
text: 'Later',
action: () => {
console.log('User chose to install the update for the Electron app later.')
showUpdateDialog.value = false
},
color: 'error',
},
]
showUpdateDialog.value = true
})
</script>

0 comments on commit e4e006c

Please sign in to comment.