Skip to content

Commit e221da8

Browse files
rafaellehmkuhlArturoManzoli
authored andcommitted
electron: Offer automatic app update
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.
1 parent 91b64fd commit e221da8

File tree

6 files changed

+296
-6
lines changed

6 files changed

+296
-6
lines changed

electron/main.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import { app, BrowserWindow, protocol, screen } from 'electron'
2+
import logger from 'electron-log'
23
import { join } from 'path'
34

5+
import { setupAutoUpdater } from './services/auto-update'
46
import store from './services/config-store'
57
import { setupNetworkService } from './services/network'
68

9+
// If the app is packaged, push logs to the system instead of the console
10+
if (app.isPackaged) {
11+
Object.assign(console, logger.functions)
12+
}
13+
714
export const ROOT_PATH = {
815
dist: join(__dirname, '..'),
916
}
@@ -33,11 +40,6 @@ function createWindow(): void {
3340
store.set('windowBounds', { x, y, width, height })
3441
})
3542

36-
// Test active push message to Renderer-process.
37-
mainWindow.webContents.on('did-finish-load', () => {
38-
mainWindow?.webContents.send('main-process-message', new Date().toLocaleString())
39-
})
40-
4143
if (process.env.VITE_DEV_SERVER_URL) {
4244
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL)
4345
} else {
@@ -71,7 +73,17 @@ protocol.registerSchemesAsPrivileged([
7173

7274
setupNetworkService()
7375

74-
app.whenReady().then(createWindow)
76+
app.whenReady().then(async () => {
77+
console.log('Electron app is ready.')
78+
console.log(`Cockpit version: ${app.getVersion()}`)
79+
80+
console.log('Creating window...')
81+
createWindow()
82+
83+
setTimeout(() => {
84+
setupAutoUpdater(mainWindow as BrowserWindow)
85+
}, 5000)
86+
})
7587

7688
app.on('before-quit', () => {
7789
// @ts-ignore: import.meta.env does not exist in the types

electron/preload.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,16 @@ import { contextBridge, ipcRenderer } from 'electron'
22

33
contextBridge.exposeInMainWorld('electronAPI', {
44
getInfoOnSubnets: () => ipcRenderer.invoke('get-info-on-subnets'),
5+
onUpdateAvailable: (callback: (info: any) => void) =>
6+
ipcRenderer.on('update-available', (_event, info) => callback(info)),
7+
onUpdateDownloaded: (callback: (info: any) => void) =>
8+
ipcRenderer.on('update-downloaded', (_event, info) => callback(info)),
9+
onCheckingForUpdate: (callback: () => void) => ipcRenderer.on('checking-for-update', () => callback()),
10+
onUpdateNotAvailable: (callback: (info: any) => void) =>
11+
ipcRenderer.on('update-not-available', (_event, info) => callback(info)),
12+
onDownloadProgress: (callback: (info: any) => void) =>
13+
ipcRenderer.on('download-progress', (_event, info) => callback(info)),
14+
downloadUpdate: () => ipcRenderer.send('download-update'),
15+
installUpdate: () => ipcRenderer.send('install-update'),
16+
cancelUpdate: () => ipcRenderer.send('cancel-update'),
517
})

electron/services/auto-update.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { BrowserWindow, ipcMain } from 'electron'
2+
import electronUpdater, { type AppUpdater } from 'electron-updater'
3+
4+
/**
5+
* Setup auto updater
6+
* @param {BrowserWindow} mainWindow - The main Electron window
7+
*/
8+
export const setupAutoUpdater = (mainWindow: BrowserWindow): void => {
9+
const autoUpdater: AppUpdater = electronUpdater.autoUpdater
10+
autoUpdater.logger = console
11+
autoUpdater.autoDownload = false // Prevent automatic downloads
12+
13+
autoUpdater
14+
.checkForUpdates()
15+
.then((e) => console.info(e))
16+
.catch((e) => console.error(e))
17+
18+
autoUpdater.on('checking-for-update', () => {
19+
mainWindow.webContents.send('checking-for-update')
20+
})
21+
22+
autoUpdater.on('update-available', (info) => {
23+
mainWindow.webContents.send('update-available', info)
24+
})
25+
26+
autoUpdater.on('update-not-available', (info) => {
27+
mainWindow.webContents.send('update-not-available', info)
28+
})
29+
30+
autoUpdater.on('download-progress', (progressInfo) => {
31+
mainWindow.webContents.send('download-progress', progressInfo)
32+
})
33+
34+
autoUpdater.on('update-downloaded', (info) => {
35+
mainWindow.webContents.send('update-downloaded', info)
36+
})
37+
38+
// Add handlers for update control
39+
ipcMain.on('download-update', () => {
40+
autoUpdater.downloadUpdate()
41+
})
42+
43+
ipcMain.on('install-update', () => {
44+
autoUpdater.quitAndInstall()
45+
})
46+
47+
ipcMain.on('cancel-update', () => {
48+
autoUpdater.removeAllListeners('update-downloaded')
49+
autoUpdater.removeAllListeners('download-progress')
50+
})
51+
}

src/App.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@
316316
<Tutorial :show-tutorial="interfaceStore.isTutorialVisible" />
317317
<VideoLibraryModal :open-modal="interfaceStore.isVideoLibraryVisible" />
318318
<VehicleDiscoveryDialog v-model="showDiscoveryDialog" show-auto-search-option />
319+
<UpdateNotification v-if="isElectron()" />
319320
</template>
320321

321322
<script setup lang="ts">
@@ -325,6 +326,7 @@ import { useRoute } from 'vue-router'
325326
326327
import GlassModal from '@/components/GlassModal.vue'
327328
import Tutorial from '@/components/Tutorial.vue'
329+
import UpdateNotification from '@/components/UpdateNotification.vue'
328330
import VehicleDiscoveryDialog from '@/components/VehicleDiscoveryDialog.vue'
329331
import VideoLibraryModal from '@/components/VideoLibraryModal.vue'
330332
import { useInteractionDialog } from '@/composables/interactionDialog'
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
<template>
2+
<InteractionDialog
3+
v-model="showUpdateDialog"
4+
:title="dialogTitle"
5+
:message="dialogMessage"
6+
:variant="dialogVariant"
7+
:actions="dialogActions"
8+
max-width="560"
9+
>
10+
<template #content>
11+
<div v-if="updateInfo" class="mt-2">
12+
<strong>Update Details:</strong>
13+
<p>Current Version: {{ app_version.version }}</p>
14+
<p>New Version: {{ updateInfo.version }}</p>
15+
<p>Release Date: {{ formatDate(updateInfo.releaseDate) }}</p>
16+
</div>
17+
<v-progress-linear
18+
v-if="showProgress"
19+
:model-value="downloadProgress"
20+
color="primary"
21+
height="25"
22+
rounded
23+
class="my-4"
24+
>
25+
<template #default>
26+
<strong>{{ Math.round(downloadProgress) }}%</strong>
27+
</template>
28+
</v-progress-linear>
29+
</template>
30+
</InteractionDialog>
31+
</template>
32+
33+
<script setup lang="ts">
34+
import { useStorage } from '@vueuse/core'
35+
import { onBeforeMount, ref } from 'vue'
36+
37+
import InteractionDialog, { type Action } from '@/components/InteractionDialog.vue'
38+
import { app_version } from '@/libs/cosmos'
39+
import { isElectron } from '@/libs/utils'
40+
41+
const showUpdateDialog = ref(false)
42+
const dialogTitle = ref('')
43+
const dialogMessage = ref('')
44+
const dialogVariant = ref<'error' | 'info' | 'success' | 'warning' | 'text-only'>('info')
45+
const showProgress = ref(false)
46+
const downloadProgress = ref(0)
47+
const dialogActions = ref<Action[]>([])
48+
const updateInfo = ref({
49+
version: '',
50+
releaseDate: '',
51+
releaseNotes: '',
52+
})
53+
const ignoredUpdateVersions = useStorage<string[]>('cockpit-ignored-update-versions', [])
54+
55+
const formatDate = (date: string): string => {
56+
return new Date(date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })
57+
}
58+
59+
onBeforeMount(() => {
60+
if (!isElectron()) {
61+
console.info('Not in Electron environment. UpdateNotification will not be initialized.')
62+
return
63+
}
64+
65+
if (!window.electronAPI) {
66+
console.error('window.electronAPI is not defined. UpdateNotification will not be initialized.')
67+
return
68+
}
69+
70+
// Listen for update events
71+
window.electronAPI.onCheckingForUpdate(() => {
72+
console.log('Checking if there are updates for the Electron app...')
73+
dialogTitle.value = 'Checking for Updates'
74+
dialogMessage.value = 'Looking for new versions of the application...'
75+
dialogVariant.value = 'info'
76+
dialogActions.value = []
77+
showProgress.value = false
78+
showUpdateDialog.value = true
79+
})
80+
81+
window.electronAPI.onUpdateNotAvailable(() => {
82+
console.log('No updates available for the Electron app.')
83+
dialogTitle.value = 'No Updates Available'
84+
dialogMessage.value = 'You are running the latest version of the application.'
85+
dialogVariant.value = 'success'
86+
dialogActions.value = [
87+
{
88+
text: 'OK',
89+
action: () => {
90+
showUpdateDialog.value = false
91+
},
92+
},
93+
]
94+
showProgress.value = false
95+
})
96+
97+
window.electronAPI.onUpdateAvailable((info) => {
98+
console.log('Update available for the Electron app.', info)
99+
dialogTitle.value = 'Update Available'
100+
dialogMessage.value = 'A new version of the application is available. Would you like to download it now?'
101+
dialogVariant.value = 'info'
102+
updateInfo.value = { ...info }
103+
dialogActions.value = [
104+
{
105+
text: 'Ignore This Version',
106+
action: () => {
107+
console.log(`User chose to ignore version ${updateInfo.value.version}`)
108+
ignoredUpdateVersions.value.push(updateInfo.value.version)
109+
window.electronAPI!.cancelUpdate()
110+
showUpdateDialog.value = false
111+
},
112+
},
113+
{
114+
text: 'Download',
115+
action: () => {
116+
window.electronAPI!.downloadUpdate()
117+
showProgress.value = true
118+
dialogActions.value = [
119+
{
120+
text: 'Cancel',
121+
action: () => {
122+
console.log('User chose to cancel the update for the Electron app.')
123+
window.electronAPI!.cancelUpdate()
124+
showUpdateDialog.value = false
125+
dialogMessage.value = 'Downloading update...'
126+
},
127+
},
128+
]
129+
},
130+
},
131+
{
132+
text: 'Not Now',
133+
action: () => {
134+
window.electronAPI!.cancelUpdate()
135+
showUpdateDialog.value = false
136+
},
137+
},
138+
]
139+
140+
// Check if this version is in the ignored list
141+
if (ignoredUpdateVersions.value.includes(info.version)) {
142+
console.log(`Skipping ignored version ${info.version}.`)
143+
showUpdateDialog.value = false
144+
return
145+
}
146+
147+
showUpdateDialog.value = true
148+
})
149+
150+
window.electronAPI.onDownloadProgress((progressInfo) => {
151+
downloadProgress.value = progressInfo.percent
152+
})
153+
154+
window.electronAPI.onUpdateDownloaded(() => {
155+
console.log('Finished downloading the update for the Electron app.')
156+
dialogTitle.value = 'Update Ready to Install'
157+
dialogMessage.value =
158+
'The update has been downloaded. Would you like to install it now? The application will restart during installation.'
159+
dialogVariant.value = 'info'
160+
showProgress.value = false
161+
dialogActions.value = [
162+
{
163+
text: 'Install Now',
164+
action: () => {
165+
console.log('User chose to install the update for the Electron app now.')
166+
window.electronAPI!.installUpdate()
167+
showUpdateDialog.value = false
168+
},
169+
},
170+
{
171+
text: 'Later',
172+
action: () => {
173+
console.log('User chose to install the update for the Electron app later.')
174+
showUpdateDialog.value = false
175+
},
176+
},
177+
]
178+
showUpdateDialog.value = true
179+
})
180+
})
181+
</script>

src/libs/cosmos.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,38 @@ declare global {
191191
* @returns Promise containing subnet information
192192
*/
193193
getInfoOnSubnets: () => Promise<NetworkInfo[]>
194+
/**
195+
* Register callback for update available event
196+
*/
197+
onUpdateAvailable: (callback: (info: any) => void) => void
198+
/**
199+
* Register callback for update downloaded event
200+
*/
201+
onUpdateDownloaded: (callback: (info: any) => void) => void
202+
/**
203+
* Trigger update download
204+
*/
205+
downloadUpdate: () => void
206+
/**
207+
* Trigger update installation
208+
*/
209+
installUpdate: () => void
210+
/**
211+
* Cancel ongoing update
212+
*/
213+
cancelUpdate: () => void
214+
/**
215+
* Register callback for checking for update event
216+
*/
217+
onCheckingForUpdate: (callback: () => void) => void
218+
/**
219+
* Register callback for update not available event
220+
*/
221+
onUpdateNotAvailable: (callback: (info: any) => void) => void
222+
/**
223+
* Register callback for download progress event
224+
*/
225+
onDownloadProgress: (callback: (info: any) => void) => void
194226
}
195227
}
196228
}

0 commit comments

Comments
 (0)