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 Nov 29, 2024
1 parent faba007 commit 265a3e0
Show file tree
Hide file tree
Showing 6 changed files with 382 additions and 14 deletions.
88 changes: 78 additions & 10 deletions electron/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { app, BrowserWindow, protocol, screen } from 'electron'
import { app, BrowserWindow, ipcMain, protocol, screen } from 'electron'
// @ts-ignore: electron-updater is not a module
import electronUpdater, { type AppUpdater } from 'electron-updater'
import { join } from 'path'

export const ROOT_PATH = {
Expand All @@ -7,10 +9,25 @@ 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'),
Expand All @@ -23,15 +40,10 @@ 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)
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 @@ -59,7 +71,63 @@ 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()
}, 5000)
})

const setupAutoUpdater = (): void => {
const autoUpdater = getAutoUpdater()
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
16 changes: 16 additions & 0 deletions electron/preload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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', () => 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
2 changes: 1 addition & 1 deletion src/components/InteractionDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const slots = useSlots()
/**
* Interface to an array of buttons for the Interaction Dialog's footer
*/
interface Action {
export interface Action {
/**
* Button Text
*/
Expand Down
167 changes: 167 additions & 0 deletions src/components/UpdateNotification.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<template>
<InteractionDialog
v-model="showUpdateDialog"
:title="dialogTitle"
:message="dialogMessage"
:variant="dialogVariant"
:actions="dialogActions"
>
<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="mb-2"
>
<template #default>
<strong>{{ Math.round(downloadProgress) }}%</strong>
</template>
</v-progress-linear>
</template>
</InteractionDialog>
</template>

<script setup lang="ts">
import { useStorage } from '@vueuse/core'
import { ref } from 'vue'
import InteractionDialog, { type Action } from '@/components/InteractionDialog.vue'
import { app_version } from '@/libs/cosmos'
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', [])
// 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
},
},
]
},
},
{
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
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
},
},
{
text: 'Later',
action: () => {
console.log('User chose to install the update for the Electron app later.')
showUpdateDialog.value = false
},
},
]
showUpdateDialog.value = true
})
const formatDate = (date: string): string => {
return new Date(date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })
}
</script>
Loading

0 comments on commit 265a3e0

Please sign in to comment.