Skip to content

Commit

Permalink
feat: Add app update controls
Browse files Browse the repository at this point in the history
  • Loading branch information
marvin-wtt committed May 30, 2024
1 parent ed9a694 commit f75d886
Show file tree
Hide file tree
Showing 10 changed files with 258 additions and 96 deletions.
6 changes: 6 additions & 0 deletions common/AppAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type AppUpdate =
| UpdateNotAvailable
| UpdateError
| UpdateDownloadProgress
| UpdateCanceled
| UpdateDownloaded;

export interface CheckingForUpdates {
Expand All @@ -46,6 +47,11 @@ export interface UpdateDownloadProgress {
info: ProgressInfo;
}

export interface UpdateCanceled {
name: 'update-cancelled';
info: UpdateInfo;
}

export interface UpdateDownloaded {
name: 'update-downloaded';
info: UpdateDownloadedEvent;
Expand Down
103 changes: 53 additions & 50 deletions src-electron/electron-updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,67 +20,70 @@ function getAutoUpdater(): AppUpdater {
const startAutoUpdater = async () => {
const autoUpdater = getAutoUpdater();
autoUpdater.logger = log;
registerEventListener();
await autoUpdater.checkForUpdates();
};

function registerEventListener() {
const autoUpdater = getAutoUpdater();
autoUpdater.on('checking-for-update', () => {
send({
name: 'checking-for-update',
});
const autoUpdater = getAutoUpdater();
autoUpdater.on('checking-for-update', () => {
send({
name: 'checking-for-update',
});
autoUpdater.on('update-available', (info) => {
send({
name: 'update-available',
info,
});
});
autoUpdater.on('update-cancelled', (info) => {
send({
name: 'update-cancelled',
info,
});
autoUpdater.on('update-not-available', (info) => {
send({
name: 'update-not-available',
info,
});
});
autoUpdater.on('update-available', (info) => {
send({
name: 'update-available',
info,
});
autoUpdater.on('error', (err) => {
send({
name: 'error',
error: err,
});
});
autoUpdater.on('update-not-available', (info) => {
send({
name: 'update-not-available',
info,
});
autoUpdater.on('download-progress', (progressObj) => {
send({
name: 'download-progress',
info: progressObj,
});
});
autoUpdater.on('error', (err) => {
send({
name: 'error',
error: err,
});

autoUpdater.on('update-downloaded', (info) => {
send({
name: 'update-downloaded',
info,
});
});
autoUpdater.on('download-progress', (progressObj) => {
send({
name: 'download-progress',
info: progressObj,
});
});

let cancellationToken: CancellationToken | undefined;
ipcMain.on('app:checkForUpdate', async () => {
await autoUpdater.checkForUpdates();
});
ipcMain.on('app:downloadUpdate', async () => {
if (cancellationToken) {
return;
}
await autoUpdater.downloadUpdate(cancellationToken);
});
ipcMain.on('app:cancelUpdate', () => {
cancellationToken?.cancel();
cancellationToken = undefined;
autoUpdater.on('update-downloaded', (info) => {
send({
name: 'update-downloaded',
info,
});
ipcMain.on('app:installUpdate', () => {
autoUpdater.quitAndInstall();
});
}
});

let cancellationToken: CancellationToken | undefined;
ipcMain.on('app:checkForUpdate', async () => {
await autoUpdater.checkForUpdates();
});
ipcMain.on('app:downloadUpdate', async () => {
if (cancellationToken) {
return;
}
await autoUpdater.downloadUpdate(cancellationToken);
});
ipcMain.on('app:cancelUpdate', () => {
cancellationToken?.cancel();
cancellationToken = undefined;
});
ipcMain.on('app:installUpdate', () => {
autoUpdater.quitAndInstall();
});

function send(update: AppUpdate) {
BrowserWindow.getAllWindows().forEach((win) => {
Expand Down
155 changes: 113 additions & 42 deletions src/components/layout/AppUpdateBtn.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,64 +8,86 @@
:icon="icon"
>
<q-menu
v-model="menuOpen"
anchor="bottom middle"
self="top middle"
style="width: 200px"
style="min-width: 200px"
>
<div class="column no-wrap q-pa-md q-gutter-y-sm">
<div
v-if="status?.name === 'checking-for-update'"
class="column"
>
<a class="q-mb-sm"> {{ t('update.checking-for-update') }}</a>
<a class="q-mb-sm"> {{ t('updater.checkingForUpdate') }}</a>
<q-linear-progress
rounded
indeterminate
/>
</div>
<div v-else-if="status?.name === 'update-available'">
<a>{{ t('update.available') }}</a>
<!-- update-available -->
<div
v-else-if="status?.name === 'update-available'"
class="row no-wrap justify-between q-gutter-x-sm"
>
<div class="column">
{{ t('updater.updateAvailable') }}
{{ status.info.version }}
<div class="text-caption">
{{ status.info.version }} / {{ updateSize(status.info) }}
</div>
</div>
<q-btn
icon="download"
dense
rounded
color="primary"
class="self-center"
@click="downloadUpdate"
/>
</div>
<!-- update-not-available -->
<div
v-else-if="status?.name === 'update-not-available'"
class="column q-gutter-sm"
class="row no-wrap justify-between q-gutter-x-sm"
>
{{ t('update.not_available') }}
<div class="column">
{{ t('updater.updateNotAvailable') }}
<div class="text-caption">
{{ status.info.version }}
</div>
</div>
<q-btn
icon="sync"
rounded
dense
color="primary"
class="self-center"
@click="checkForUpdates"
/>
</div>
<div v-else-if="status?.name === 'download-progress'">
{{ t('update.download-progress') }}
<!-- download-progress -->
<div
v-else-if="status?.name === 'download-progress'"
class="column no-wrap q-gutter-y-xs"
>
{{ t('updater.downloadProgress') }}
<div class="text-right">
<!-- TODO format data rate -->
{{ status.info.transferred }} / {{ status.info.total }} ({{
status.info.bytesPerSecond
}}
Bps)
<div class="text-right text-caption">
{{ humanStorageSize(status.info.transferred) }} /
{{ humanStorageSize(status.info.total) }}
</div>
<div class="row q-gutter-sm">
<div class="col-grow column justify-center">
<q-linear-progress
:value="status.info.percent"
:value="status.info.percent / 100"
stripe
/>
</div>
Expand All @@ -80,20 +102,68 @@
</div>
</div>
<div v-else-if="status?.name === 'update-downloaded'">
{{ t('update.downloaded') }}
<!-- update-cancelled -->
<div
v-else-if="status?.name === 'update-cancelled'"
class="row no-wrap justify-between q-gutter-x-sm"
>
<div class="column">
{{ t('updater.updateCanceled') }}
<div class="text-caption">
{{ status.info.version }} / {{ updateSize(status.info) }}
</div>
</div>
<q-btn
icon="download"
dense
rounded
color="primary"
class="self-center"
@click="downloadUpdate"
/>
</div>
<!-- update-downloaded -->
<div
v-else-if="status?.name === 'update-downloaded'"
class="row no-wrap justify-between q-gutter-x-sm"
>
<div class="column">
{{ t('updater.updateDownloaded') }}
<div class="text-caption">
{{ status.info.version }}
</div>
</div>
<q-btn
icon="restart_alt"
size="xs"
dense
rounded
color="primary"
class="self-center"
@click="installUpdate"
/>
</div>
<div v-else-if="status?.name === 'error'">
<q-icon name="error" />
{{ status.error.message }}
<!-- error -->
<div
v-else-if="status?.name === 'error'"
class="row no-wrap justify-between q-gutter-x-sm"
>
<q-icon
size="lg"
name="error_outline"
class="self-center"
/>
<div class="column q-gutter-sm">
{{ t('updater.error') }}
<a class="text-caption">
{{ status.error.message }}
</a>
</div>
</div>
<div v-else>
Expand All @@ -114,44 +184,45 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { AppUpdate } from 'app/common';
import { useI18n } from 'vue-i18n';
import { format } from 'quasar';
import { UpdateInfo } from 'electron-updater';
import { useUpdaterStore } from 'stores/updater-store';
import { storeToRefs } from 'pinia';
// TODO Add translations
const { t } = useI18n();
const updaterStore = useUpdaterStore();
const { status } = storeToRefs(updaterStore);
const { humanStorageSize } = format;
const status = ref<AppUpdate>({
name: 'download-progress',
info: {
total: 10000,
transferred: 100,
percent: 0.6,
delta: 0,
bytesPerSecond: 100,
},
});
window.appAPI.onUpdateInfo((data) => {
status.value = data;
});
const menuOpen = ref<boolean>(false);
const icon = computed<string>(() => {
switch (status.value?.name) {
case 'checking-for-update':
case 'update-available':
return 'download';
case 'checking-for-update':
case 'download-progress':
return 'sync';
case 'update-downloaded':
return 'restart_alt';
return 'update';
case 'error':
return 'error';
return 'error_outline';
case 'update-not-available':
default:
return 'update';
return 'o_info';
}
});
function updateSize(info: UpdateInfo): string {
const bytes = info.files.reduce(
(total, file) => (file.size ? total + file.size : total),
0,
);
return humanStorageSize(bytes);
}
function checkForUpdates() {
window.appAPI.checkForUpdate();
}
Expand Down
Loading

0 comments on commit f75d886

Please sign in to comment.