diff --git a/docs/presentation.md b/docs/presentation.md index 2398a5e1..61eb1a4d 100644 --- a/docs/presentation.md +++ b/docs/presentation.md @@ -12,7 +12,8 @@ - [**`Shared/`**](./../src/presentation/components/Shared): Contains Vue components and component helpers that are shared across other components. - [**`styles/`**](./../src/presentation/styles/): Contains shared styles used throughout different components. - [**`main.ts`**](./../src/presentation/main.ts): Application entry point that mounts and starts Vue application. - - [**`background.ts`**](./../src/presentation/background.ts): Main process of Electron, started as first thing when app starts. + - [`electron/`](./../src/presentation/electron/): Electron configuration for the desktop application. + - [**`main.ts`**](./../src/presentation/main.ts): Main process of Electron, started as first thing when app starts. - [**`/public/`**](./../public/): Contains static assets that will simply be copied and not go through webpack. - [**`/vue.config.js`**](./../vue.config.js): Global Vue CLI configurations loaded by `@vue/cli-service` - [**`/postcss.config.js`**](./../postcss.config.js): PostCSS configurations that are used by Vue CLI internally diff --git a/package-lock.json b/package-lock.json index 2659f4eb..9e00c04e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,7 @@ "requires": true, "packages": { "": { - "version": "0.10.2", + "version": "0.10.3", "hasInstallScript": true, "dependencies": { "@fortawesome/fontawesome-svg-core": "^1.2.35", @@ -17,6 +17,7 @@ "ace-builds": "^1.4.12", "core-js": "^3.12.1", "cross-fetch": "^3.1.4", + "electron-progressbar": "^2.0.1", "file-saver": "^2.0.5", "inversify": "^5.1.1", "liquor-tree": "^0.2.70", @@ -7358,6 +7359,14 @@ "integrity": "sha512-J5Ew3axdk7W4jzzxKLSAi1sqbcAoo9CzHuBVsG0tT47j256xKulNrWFf3lZmHJ1KDXOQUcuwOngQF0jjmpEdpw==", "dev": true }, + "node_modules/electron-progressbar": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/electron-progressbar/-/electron-progressbar-2.0.1.tgz", + "integrity": "sha512-+N60GX2q+KH5OvZXxwtjMTZB/1AyxriFd95vOnR3sOfNpvz+30LMsM0a9SnEivZE6N8Djy7F3z4TY8pLs8aopw==", + "dependencies": { + "extend": "^3.0.1" + } + }, "node_modules/electron-publish": { "version": "22.9.1", "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-22.9.1.tgz", @@ -8038,8 +8047,7 @@ "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "node_modules/extend-shallow": { "version": "3.0.2", @@ -26826,6 +26834,14 @@ "integrity": "sha512-J5Ew3axdk7W4jzzxKLSAi1sqbcAoo9CzHuBVsG0tT47j256xKulNrWFf3lZmHJ1KDXOQUcuwOngQF0jjmpEdpw==", "dev": true }, + "electron-progressbar": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/electron-progressbar/-/electron-progressbar-2.0.1.tgz", + "integrity": "sha512-+N60GX2q+KH5OvZXxwtjMTZB/1AyxriFd95vOnR3sOfNpvz+30LMsM0a9SnEivZE6N8Djy7F3z4TY8pLs8aopw==", + "requires": { + "extend": "^3.0.1" + } + }, "electron-publish": { "version": "22.9.1", "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-22.9.1.tgz", @@ -27387,8 +27403,7 @@ "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "extend-shallow": { "version": "3.0.2", diff --git a/package.json b/package.json index 638bf4ff..bd6717b1 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "ace-builds": "^1.4.12", "core-js": "^3.12.1", "cross-fetch": "^3.1.4", + "electron-progressbar": "^2.0.1", "file-saver": "^2.0.5", "inversify": "^5.1.1", "liquor-tree": "^0.2.70", diff --git a/src/presentation/electron/UpdateProgressBar.ts b/src/presentation/electron/UpdateProgressBar.ts new file mode 100644 index 00000000..2b00ff23 --- /dev/null +++ b/src/presentation/electron/UpdateProgressBar.ts @@ -0,0 +1,86 @@ +import ProgressBar from 'electron-progressbar'; +import { ProgressInfo } from 'electron-builder'; +import { app } from 'electron'; +import log from 'electron-log'; + +export class UpdateProgressBar { + private progressBar: ProgressBar; + private showingProgress = false; + public showIndeterminateState() { + this.progressBar?.close(); + this.progressBar = progressBarFactory.createWithIndeterminateState(); + } + public showProgress(progress: ProgressInfo) { + if (!this.showingProgress) { // First time showing progress + this.progressBar?.close(); + this.showingProgress = true; + } else { + updateProgressBar(progress, this.progressBar); + } + } + public showError(e: Error) { + const reportUpdateError = () => { + this.progressBar.detail = + 'An error occurred while fetching updates.\n' + + (e && e.message ? e.message : e); + this.progressBar._window.setClosable(true); + }; + if (this.progressBar._window) { + reportUpdateError(); + } else { + this.progressBar.on('ready', () => reportUpdateError()); + } + } + public close() { + if (!this.progressBar.isCompleted()) { + this.progressBar.close(); + } + } +} + +function getUpdatePercent(progress: ProgressInfo) { + let percent = progress.percent; + if (percent) { + percent = Math.round(percent * 100) / 100; + } + return percent; +} + +const progressBarFactory = { + createWithIndeterminateState: () => { + return new ProgressBar({ + title: `${app.name} Update`, + text: `Downloading ${app.name} update...`, + }); + }, + createWithPercentile: (initialProgress: ProgressInfo) => { + const percent = getUpdatePercent(initialProgress); + const progressBar = new ProgressBar({ + indeterminate: false, + title: `${app.name} Update`, + text: `Downloading ${app.name} update...`, + detail: `${percent}% ...`, + initialValue: percent, + }); + progressBar + .on('completed', () => { + progressBar.detail = 'Download completed.'; + }) + .on('aborted', (value) => { + log.info(`progress aborted... ${value}`); + }) + .on('progress', (value) => { + progressBar.detail = `${value}% ...`; + }) + .on('ready', () => { + // initialValue doesn't set the UI, so this is needed to render it correctly + progressBar.value = percent; + }); + return progressBar; + }, +}; + + +function updateProgressBar(progress: ProgressInfo, progressBar: ProgressBar) { + progressBar.value = getUpdatePercent(progress); +} diff --git a/src/presentation/electron/Updater.ts b/src/presentation/electron/Updater.ts new file mode 100644 index 00000000..dadbdcd2 --- /dev/null +++ b/src/presentation/electron/Updater.ts @@ -0,0 +1,83 @@ +import { app, dialog } from 'electron'; +import { autoUpdater, UpdateInfo } from 'electron-updater'; +import { ProgressInfo } from 'electron-builder'; +import log from 'electron-log'; +import { UpdateProgressBar } from './UpdateProgressBar'; + +interface IUpdater { + checkForUpdatesAsync(): Promise; +} + +export function setupAutoUpdater(): IUpdater { + autoUpdater.logger = log; + autoUpdater.on('error', (error: Error) => { + log.error('@error@\n', error); + }); + autoUpdater.on('update-available', async (info: UpdateInfo) => { + log.info('@update-available@\n', info); + await handleAvailableUpdateAsync(); + }); + return { + checkForUpdatesAsync: async () => { + await autoUpdater.checkForUpdates(); + }, + }; +} + +async function handleAvailableUpdateAsync() { + if (!await dialogs.askDownloadAndInstallAsync()) { + return; + } + autoUpdater.downloadUpdate(); + handleUpdateProgress(); +} + +function handleUpdateProgress() { + const progressBar = new UpdateProgressBar(); + progressBar.showIndeterminateState(); + autoUpdater.on('error', (e) => { + progressBar.showError(e); + }); + autoUpdater.on('download-progress', (progress: ProgressInfo) => { + /* + On macOS, download-progress event is not called + so the indeterminate progress will continue until download is finished. + */ + log.info('@update-progress@\n', progress); + progressBar.showProgress(progress); + }); + autoUpdater.on('update-downloaded', async (info: UpdateInfo) => { + log.info('@update-downloaded@\n', info); + progressBar.close(); + await handleUpdateDownloadedAsync(); + }); +} + +const dialogs = { + askDownloadAndInstallAsync: async () => { + const updateDialogResult = await dialog.showMessageBox({ + type: 'question', + buttons: ['Install', 'Not now' ], + title: 'Confirm Update', + message: 'Update available.\n\nWould you like to download and install new version?', + detail: 'Application will automatically restart to apply update after download', + }); + return updateDialogResult.response === 0; + }, + askRestartAndInstallAsync: async () => { + const installDialogResult = await dialog.showMessageBox({ + type: 'question', + buttons: ['Install and restart', 'Later'], + defaultId: 0, + message: 'A new version of ' + app.name + ' has been downloaded', + detail: 'It will be installed the next time you restart the application', + }); + return installDialogResult.response === 0; + }, +}; + +async function handleUpdateDownloadedAsync() { + if (await dialogs.askRestartAndInstallAsync()) { + setTimeout(() => autoUpdater.quitAndInstall(), 1); + } +} diff --git a/src/presentation/background.ts b/src/presentation/electron/main.ts similarity index 85% rename from src/presentation/background.ts rename to src/presentation/electron/main.ts index 3d00040e..330e15b9 100644 --- a/src/presentation/background.ts +++ b/src/presentation/electron/main.ts @@ -8,9 +8,8 @@ import { app, protocol, BrowserWindow, shell, screen } from 'electron'; import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'; import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'; import path from 'path'; -import { autoUpdater } from 'electron-updater'; import log from 'electron-log'; - +import { setupAutoUpdater } from './Updater'; const isDevelopment = process.env.NODE_ENV !== 'production'; declare const __static: string; // https://github.com/electron-userland/electron-webpack/issues/172 @@ -24,8 +23,6 @@ protocol.registerSchemesAsPrivileged([ { scheme: 'app', privileges: { secure: true, standard: true } }, ]); -// Setup logging -autoUpdater.logger = log; // https://www.electron.build/auto-update#debugging log.transports.file.level = 'silly'; if (!process.env.IS_TEST) { Object.assign(console, log.functions); // override console.log, console.warn etc. @@ -90,14 +87,6 @@ app.on('ready', async () => { createWindow(); }); -// See electron-builder issue "checkForUpdatesAndNotify updates but does not notify on Windows 10" -// https://github.com/electron-userland/electron-builder/issues/2700 -// https://github.com/electron/electron/issues/10864 -if (process.platform === 'win32') { - // https://docs.microsoft.com/en-us/windows/win32/shell/appid#how-to-form-an-application-defined-appusermodelid - app.setAppUserModelId('Undergroundwires.PrivacySexy'); -} - // Exit cleanly on request from parent process in development mode. if (isDevelopment) { if (process.platform === 'win32') { @@ -118,14 +107,14 @@ function loadApplication(window: BrowserWindow) { // Load the url of the dev server if in development mode loadUrlWithNodeWorkaround(win, process.env.WEBPACK_DEV_SERVER_URL as string); if (!process.env.IS_TEST) { - win.webContents.openDevTools(); + window.webContents.openDevTools(); } } else { createProtocol('app'); // Load the index.html when not in development loadUrlWithNodeWorkaround(win, 'app://./index.html'); - // tslint:disable-next-line:max-line-length - autoUpdater.checkForUpdatesAndNotify(); // https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#check-for-updates-in-background-js-ts + const updater = setupAutoUpdater(); + updater.checkForUpdatesAsync(); } } @@ -153,3 +142,4 @@ function getWindowSize(idealWidth: number, idealHeight: number) { height = Math.min(height, idealHeight); return { width, height }; } + diff --git a/vue.config.js b/vue.config.js index 65b20d9c..b18e2300 100644 --- a/vue.config.js +++ b/vue.config.js @@ -20,7 +20,7 @@ module.exports = { pluginOptions: { // https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/guide.html#native-modules electronBuilder: { - mainProcessFile: './src/presentation/background.ts', // https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/configuration.html#webpack-configuration + mainProcessFile: './src/presentation/electron/main.ts', // https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/configuration.html#webpack-configuration nodeIntegration: true, // required to reach Node.js APIs for environment specific logic // https://www.electron.build/configuration/configuration builderOptions: {