diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 808e298e..ea4f2a59 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,7 +5,10 @@ on: - 'v*.*.*' jobs: build: - runs-on: windows-2019 + strategy: + matrix: + platform: [ubuntu-latest, windows-2019] + runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 @@ -13,3 +16,9 @@ jobs: node-version: '14' - run: npm ci - run: npm test + - name: Build/release Electron app + uses: samuelmeuli/action-electron-builder@v1 + with: + github_token: ${{ secrets.github_token }} + release: true + build_script_name: webpack:build diff --git a/configs/common.ts b/configs/common.ts index 8cae36fc..79dd3605 100644 --- a/configs/common.ts +++ b/configs/common.ts @@ -18,6 +18,7 @@ export const commonPlugins: WebpackPluginInstance[] = [ HOMEPAGE_URL: JSON.stringify(pkg.homepage), BUGS_URL: JSON.stringify(pkg.bugs), PRODUCT_NAME: JSON.stringify(pkg.build.productName), + APP_ID: JSON.stringify(pkg.build.appId), BUILD_PLATFORM: JSON.stringify(process.platform), }), new LicenseWebpackPlugin({ diff --git a/package-lock.json b/package-lock.json index 4ddee4cb..6ae88d6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,8 @@ "date-fns": "^2.22.1", "electron-is-accelerator": "^0.2.0", "electron-store": "^8.0.0", + "electron-updater": "^4.3.9", + "is-electron-renderer": "^2.0.1", "is-wsl": "^2.2.0", "react": "^17.0.2", "react-dom": "^17.0.2", @@ -1333,6 +1335,11 @@ "integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==", "dev": true }, + "node_modules/@types/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-D/2EJvAlCEtYFEYmmlGwbGXuK886HzyCc3nZX/tkFTQdEU8jZDAgiv08P162yB17y4ZXZoq7yFAnW4GDBb9Now==" + }, "node_modules/@types/sharp": { "version": "0.27.1", "resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.27.1.tgz", @@ -5128,6 +5135,95 @@ "integrity": "sha512-B6zLTxxaOFP4WZm6DrvgRk8kLFYWNhQ5TrHMC0l5WtkMXhU5UbnvWoTfeEwqOruUSlNMhVLfYak7REX6oC5Yfw==", "dev": true }, + "node_modules/electron-updater": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.3.9.tgz", + "integrity": "sha512-LCNfedSwZfS4Hza+pDyPR05LqHtGorCStaBgVpRnfKxOlZcvpYEX0AbMeH5XUtbtGRoH2V8osbbf2qKPNb7AsA==", + "dependencies": { + "@types/semver": "^7.3.5", + "builder-util-runtime": "8.7.5", + "fs-extra": "^10.0.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.4", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "^7.3.5" + } + }, + "node_modules/electron-updater/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/electron-updater/node_modules/builder-util-runtime": { + "version": "8.7.5", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.7.5.tgz", + "integrity": "sha512-fgUFHKtMNjdvH6PDRFntdIGUPgwZ69sXsAqEulCtoiqgWes5agrMq/Ud274zjJRTbckYh2PHh8/1CpFc6dpsbQ==", + "dependencies": { + "debug": "^4.3.2", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/electron-updater/node_modules/fs-extra": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", + "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-updater/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/electron-updater/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-updater/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-updater/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/emittery": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.7.2.tgz", @@ -6460,8 +6556,7 @@ "node_modules/graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", - "dev": true + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" }, "node_modules/graceful-readlink": { "version": "1.0.1", @@ -7090,6 +7185,11 @@ "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.0.tgz", "integrity": "sha512-SpMppC2XR3YdxSzczXReBjqs2zGscWQpBIKqwXYBFic0ERaxNVgwLCHwOLZeESfdJQjX0RDvrJ1lBXX2ij+G1Q==" }, + "node_modules/is-electron-renderer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-electron-renderer/-/is-electron-renderer-2.0.1.tgz", + "integrity": "sha1-pGnQVvl1aXxYyYxgI+sKp5r4laI=" + }, "node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -8386,8 +8486,7 @@ "node_modules/lazy-val": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", - "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "dev": true + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==" }, "node_modules/leven": { "version": "3.1.0", @@ -8531,6 +8630,16 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, "node_modules/lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", @@ -10965,8 +11074,7 @@ "node_modules/sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "node_modules/saxes": { "version": "5.0.1", @@ -14899,6 +15007,11 @@ "integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==", "dev": true }, + "@types/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-D/2EJvAlCEtYFEYmmlGwbGXuK886HzyCc3nZX/tkFTQdEU8jZDAgiv08P162yB17y4ZXZoq7yFAnW4GDBb9Now==" + }, "@types/sharp": { "version": "0.27.1", "resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.27.1.tgz", @@ -17911,6 +18024,77 @@ "integrity": "sha512-B6zLTxxaOFP4WZm6DrvgRk8kLFYWNhQ5TrHMC0l5WtkMXhU5UbnvWoTfeEwqOruUSlNMhVLfYak7REX6oC5Yfw==", "dev": true }, + "electron-updater": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.3.9.tgz", + "integrity": "sha512-LCNfedSwZfS4Hza+pDyPR05LqHtGorCStaBgVpRnfKxOlZcvpYEX0AbMeH5XUtbtGRoH2V8osbbf2qKPNb7AsA==", + "requires": { + "@types/semver": "^7.3.5", + "builder-util-runtime": "8.7.5", + "fs-extra": "^10.0.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.4", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "^7.3.5" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "builder-util-runtime": { + "version": "8.7.5", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.7.5.tgz", + "integrity": "sha512-fgUFHKtMNjdvH6PDRFntdIGUPgwZ69sXsAqEulCtoiqgWes5agrMq/Ud274zjJRTbckYh2PHh8/1CpFc6dpsbQ==", + "requires": { + "debug": "^4.3.2", + "sax": "^1.2.4" + } + }, + "fs-extra": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", + "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + } + } + }, "emittery": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.7.2.tgz", @@ -18964,8 +19148,7 @@ "graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", - "dev": true + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" }, "graceful-readlink": { "version": "1.0.1", @@ -19452,6 +19635,11 @@ "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.0.tgz", "integrity": "sha512-SpMppC2XR3YdxSzczXReBjqs2zGscWQpBIKqwXYBFic0ERaxNVgwLCHwOLZeESfdJQjX0RDvrJ1lBXX2ij+G1Q==" }, + "is-electron-renderer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-electron-renderer/-/is-electron-renderer-2.0.1.tgz", + "integrity": "sha1-pGnQVvl1aXxYyYxgI+sKp5r4laI=" + }, "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -20461,8 +20649,7 @@ "lazy-val": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", - "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "dev": true + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==" }, "leven": { "version": "3.1.0", @@ -20578,6 +20765,16 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, + "lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, "lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", @@ -22490,8 +22687,7 @@ "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "saxes": { "version": "5.0.1", diff --git a/package.json b/package.json index db12f510..68024e25 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "target": "nsis" }, "linux": { + "target": "AppImage", "asarUnpack": [ "node_modules/sharp" ] @@ -98,6 +99,8 @@ "date-fns": "^2.22.1", "electron-is-accelerator": "^0.2.0", "electron-store": "^8.0.0", + "electron-updater": "^4.3.9", + "is-electron-renderer": "^2.0.1", "is-wsl": "^2.2.0", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/src/electron/common/actions.ts b/src/electron/common/actions.ts index 4b6b90c1..60ec6f79 100644 --- a/src/electron/common/actions.ts +++ b/src/electron/common/actions.ts @@ -4,6 +4,7 @@ import { AppSettings, HistoryEntry, Origin, + UpdateStatus, WorkerStatus, } from './common'; @@ -14,6 +15,7 @@ export const ActionTypes = { UPDATE_SETTINGS: 'UPDATE_SETTINGS', REMOVE_LAST_N_HISTORY_ENTRIES: 'REMOVE_LAST_N_HISTORY_ENTRIES', REMOVE_HISTORY_ENTRY: 'REMOVE_HISTORY_ENTRY', + SET_UPDATE_STATUS: 'SET_UPDATE_STATUS', } as const; export class SetStatusAction implements Action { @@ -64,3 +66,10 @@ export class RemoveHistoryEntryAction implements Action { constructor(public readonly payload: string) {} } + +export class SetUpdateStatusAction implements Action { + readonly type = ActionTypes.SET_UPDATE_STATUS; + readonly origin: Origin = null; + + constructor(public readonly payload: UpdateStatus) {} +} diff --git a/src/electron/common/common.ts b/src/electron/common/common.ts index ee3a0bbb..b2eaeeee 100644 --- a/src/electron/common/common.ts +++ b/src/electron/common/common.ts @@ -87,11 +87,21 @@ export interface AppStats { daemonsSolvedCount: number; } +export enum UpdateStatus { + Error, + CheckingForUpdate, + UpdateNotAvailable, + UpdateAvailable, + Downloading, + UpdateDownloaded, +} + export interface State { history: HistoryEntry[]; displays: ScreenshotDisplayOutput[]; settings: AppSettings; status: WorkerStatus; + updateStatus: UpdateStatus; stats: AppStats; } @@ -198,6 +208,12 @@ export class NativeDialog { private static showMessageBox( options: Electron.MessageBoxOptions ): Promise { - return ipc.invoke('renderer:show-message-box', options); + if (require('is-electron-renderer')) { + return ipc.invoke('renderer:show-message-box', options); + } else { + const { dialog } = require('electron'); + + return dialog.showMessageBox(options); + } } } diff --git a/src/electron/common/options.ts b/src/electron/common/options.ts index c70f77d0..59601007 100644 --- a/src/electron/common/options.ts +++ b/src/electron/common/options.ts @@ -15,7 +15,7 @@ const options: BreachProtocolOption[] = [ { id: 'autoUpdate', description: 'Update autosolver automatically.', - defaultValue: true, + defaultValue: false, }, { id: 'delay', diff --git a/src/electron/main/main.ts b/src/electron/main/main.ts index 66cfccdf..b198432e 100644 --- a/src/electron/main/main.ts +++ b/src/electron/main/main.ts @@ -13,6 +13,7 @@ import { extname, join } from 'path'; import icon from '../../../resources/icon.png'; import { Action, ActionTypes, WorkerStatus } from '../common'; import { Store } from './store/store'; +import { BreachProtocolAutosolverUpdater } from './updater'; import { createBrowserWindows } from './windows'; export class Main { @@ -25,6 +26,8 @@ export class Main { /** Hidden "worker" window, does all the heavy lifting(ocr, solving). */ private worker: Electron.BrowserWindow = null; + private updater: BreachProtocolAutosolverUpdater = null; + private helpMenuTemplate: Electron.MenuItemConstructorOptions[] = [ { label: 'About', @@ -58,6 +61,12 @@ export class Main { shell.openExternal(BUGS_URL); }, }, + { + label: 'Check for updates', + click: () => { + this.updater.checkForUpdates(); + }, + }, ]; private trayMenu: Electron.MenuItemConstructorOptions[] = [ @@ -79,6 +88,10 @@ export class Main { tray: Electron.Tray; init() { + if (BUILD_PLATFORM === 'win32') { + app.setAppUserModelId(APP_ID); + } + const { worker, renderer } = createBrowserWindows(); this.store = new Store(worker.webContents, renderer.webContents, [ this.toggleKeyBind.bind(this), @@ -88,6 +101,19 @@ export class Main { this.worker = worker; this.registerListeners(); + this.updateApp(); + } + + private async updateApp() { + const { checkForUpdates } = this.getSettings(); + this.updater = new BreachProtocolAutosolverUpdater( + this.store, + this.renderer.webContents + ); + + if (checkForUpdates) { + this.updater.checkForUpdates(); + } } private createTray() { @@ -213,6 +239,7 @@ export class Main { } private onRendererClosed() { + this.updater.dispose(); this.store.dispose(); this.store = null; diff --git a/src/electron/main/store/reducer.ts b/src/electron/main/store/reducer.ts index c8f71627..3b89bd76 100644 --- a/src/electron/main/store/reducer.ts +++ b/src/electron/main/store/reducer.ts @@ -7,6 +7,7 @@ import { BreachProtocolStatus, HistoryEntry, State, + UpdateStatus, WorkerStatus, } from '@/electron/common'; import { ScreenshotDisplayOutput } from 'screenshot-desktop'; @@ -104,6 +105,11 @@ const removeHistoryEntry: Handler = (state, { payload }) => ({ history: state.history.filter((e) => e.uuid !== payload), }); +const setUpdateStatus: Handler = (state, { payload }) => ({ + ...state, + updateStatus: payload, +}); + export const appReducer = createReducer({ [ActionTypes.SET_DISPLAYS]: setDisplays, [ActionTypes.SET_STATUS]: setStatus, @@ -111,4 +117,5 @@ export const appReducer = createReducer({ [ActionTypes.UPDATE_SETTINGS]: updateSettings, [ActionTypes.REMOVE_LAST_N_HISTORY_ENTRIES]: removeLastNHistoryEntries, [ActionTypes.REMOVE_HISTORY_ENTRY]: removeHistoryEntry, + [ActionTypes.SET_UPDATE_STATUS]: setUpdateStatus, }); diff --git a/src/electron/main/store/store.ts b/src/electron/main/store/store.ts index fdb26ea2..f7b744d3 100644 --- a/src/electron/main/store/store.ts +++ b/src/electron/main/store/store.ts @@ -57,9 +57,13 @@ export class Store { this.registerStoreListeners(); } - dispatch(action: Action) { + dispatch(action: Action, notify = false) { this.applyMiddleware(action); this.state = appReducer(this.state, action); + + if (notify) { + this.notify(action); + } } getState() { @@ -138,6 +142,7 @@ export class Store { displays: [], settings: { ...this.settings.store, screenshotDir }, status: null, + updateStatus: null, stats: this.stats.store, }; } @@ -155,16 +160,17 @@ export class Store { } private onState(event: IpcMainEvent, action: Action) { - this.dispatch(action); + this.dispatch(action, true); + } - const dest = this.getDest(action); - const returnAction = { payload: this.state, type: action.type }; + private notify(action: Action) { + const stateAction = { payload: this.state, type: action.type }; - dest.send('state', returnAction); - event.sender.send('state', returnAction); + this.worker.send('state', stateAction); + this.renderer.send('state', stateAction); - event.sender.send(action.type, action); - dest.send(action.type, action); + this.worker.send(action.type, stateAction); + this.renderer.send(action.type, stateAction); } private onGetState(event: IpcMainEvent) { diff --git a/src/electron/main/updater.ts b/src/electron/main/updater.ts new file mode 100644 index 00000000..30ae1285 --- /dev/null +++ b/src/electron/main/updater.ts @@ -0,0 +1,88 @@ +import { autoUpdater, ProgressInfo, UpdateInfo } from 'electron-updater'; +import { + NativeDialog, + SetStatusAction, + SetUpdateStatusAction, + UpdateStatus, + WorkerStatus, +} from '../common'; +import { Store } from './store/store'; + +export class BreachProtocolAutosolverUpdater { + private autoUpdate: boolean = null; + + constructor(private store: Store, private renderer: Electron.webContents) { + this.registerListeners(); + } + + checkForUpdates() { + this.autoUpdate = this.store.getState().settings.autoUpdate; + autoUpdater.autoDownload = this.autoUpdate; + + autoUpdater.checkForUpdates(); + } + + dispose() { + autoUpdater.removeAllListeners(); + } + + private registerListeners() { + autoUpdater.on('checking-for-update', this.onCheckingForUpdates.bind(this)); + autoUpdater.on('update-available', this.onUpdateAvailable.bind(this)); + autoUpdater.on( + 'update-not-available', + this.onUpdateNotAvailable.bind(this) + ); + autoUpdater.on('update-downloaded', this.onUpdateDownloaded.bind(this)); + autoUpdater.on('download-progress', this.onDownloadProgress.bind(this)); + } + + private onCheckingForUpdates() { + this.setUpdateStatus(UpdateStatus.CheckingForUpdate); + } + + private async onUpdateAvailable({ version }: UpdateInfo) { + this.setUpdateStatus(UpdateStatus.UpdateAvailable); + + if (!this.autoUpdate) { + const result = await NativeDialog.confirm({ + message: `New version ${version} is available.`, + buttons: ['Download and install', 'Cancel'], + type: 'info', + }); + + if (!result) return; + + autoUpdater.downloadUpdate(); + } + + this.disableWorker(); + this.setUpdateStatus(UpdateStatus.Downloading); + } + + private onUpdateNotAvailable() { + this.setUpdateStatus(UpdateStatus.UpdateNotAvailable); + } + + private onUpdateDownloaded() { + this.setUpdateStatus(UpdateStatus.UpdateDownloaded); + + autoUpdater.quitAndInstall(); + } + + private onDownloadProgress(info: ProgressInfo) { + this.renderer.send('download-progress', info); + } + + private disableWorker() { + const action = new SetStatusAction(WorkerStatus.Disabled); + + return this.store.dispatch(action, true); + } + + private setUpdateStatus(status: UpdateStatus) { + const action = new SetUpdateStatusAction(status); + + return this.store.dispatch(action, true); + } +} diff --git a/src/electron/renderer/components/StatusBar.tsx b/src/electron/renderer/components/StatusBar.tsx index 52be1c98..aa67ce70 100644 --- a/src/electron/renderer/components/StatusBar.tsx +++ b/src/electron/renderer/components/StatusBar.tsx @@ -1,5 +1,6 @@ -import { WorkerStatus } from '@/electron/common'; -import { FC, useContext, useState } from 'react'; +import { UpdateStatus, WorkerStatus } from '@/electron/common'; +import { ProgressInfo } from 'electron-updater'; +import { FC, useContext, useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; import styled from 'styled-components'; import { getDisplayName, useIpcEvent } from '../common'; @@ -47,6 +48,7 @@ const StatusBarWrapper = styled.footer` gap: 0.5rem; display: flex; flex-shrink: 0; + position: relative; `; function getWorkerStatusMessage(status: WorkerStatus) { @@ -89,13 +91,62 @@ function useSettingsChangeListener(delay = 2000) { return show; } +function useDownloadProgress() { + const [progress, setProgress] = useState(0); + + useIpcEvent(['download-progress'], (e, info: ProgressInfo) => { + setProgress(info.percent); + }); + + return progress; +} + +function useShowUpdateStatus(status: UpdateStatus, delay = 3000) { + const [showUpdateStatus, setShowUpdateStatus] = useState(false); + let id: any = null; + + useEffect(() => { + clearTimeout(id); + setShowUpdateStatus(true); + + id = setTimeout(() => { + setShowUpdateStatus(false); + }, delay); + }, [status]); + + return showUpdateStatus; +} + +const updateStatusMessage = { + [UpdateStatus.Error]: 'Error occurred', + [UpdateStatus.CheckingForUpdate]: 'Checking for updates..', + [UpdateStatus.UpdateNotAvailable]: 'Up to date', + [UpdateStatus.UpdateAvailable]: 'Update available', + [UpdateStatus.Downloading]: 'Downloading..', + [UpdateStatus.UpdateDownloaded]: 'Update downloaded', +}; + +const DownloadProgressBar = styled.div<{ value: number }>` + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: var(--accent); + height: 2px; + width: ${(p) => p.value}%; + transition: width 0.5s; +`; + export const StatusBar: FC = () => { const { displays, status, + updateStatus, settings: { activeDisplayId }, } = useContext(StateContext); const show = useSettingsChangeListener(); + const showUpdateStatus = useShowUpdateStatus(updateStatus); + const progress = useDownloadProgress(); const history = useHistory(); const activeDisplay = displays.find((d) => d.id === activeDisplayId); const isWorkerDetatch = @@ -116,6 +167,10 @@ export const StatusBar: FC = () => { Saved + {showUpdateStatus && ( + {updateStatusMessage[updateStatus]} + )} + ); }; diff --git a/types/constants/index.d.ts b/types/constants/index.d.ts index f528a616..e65ac10b 100644 --- a/types/constants/index.d.ts +++ b/types/constants/index.d.ts @@ -18,3 +18,6 @@ declare const PRODUCT_NAME: string; /** Platform code from builded on. */ declare const BUILD_PLATFORM: NodeJS.Platform; + +/** Application ID, Windows only. */ +declare const APP_ID: string;