From 2f2880a9f19de50ff14a0785b32a4d5427477e55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8D=89=E9=9E=8B=E6=B2=A1=E5=8F=B7?= <308487730@qq.com> Date: Sun, 12 Mar 2023 09:35:20 +0800 Subject: [PATCH] feat: add electron-builder (#123) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # feat : add electron auto update # feat : add some components url : https://github.com/RSS1101/electron-vite-react # * feat : Add electron auto update function # * chore : use css module conduct style isolation # * refactor: cleanup # * chore : electron-auto-update refinement # * chore: cleanup # * chore : update electron-auto-update # * chore: typo # * feat: add `mac.target.dmg` for electron-updater --------- Co-authored-by: 任帅 <1064425721@qq.com> --- electron-builder.json5 | 8 +- electron/main/index.ts | 7 +- electron/main/update.ts | 73 ++++++++++ package.json | 3 + src/App.tsx | 20 +-- src/components/update/Modal/index.tsx | 67 +++++++++ src/components/update/Modal/modal.module.scss | 89 ++++++++++++ src/components/update/Progress/index.tsx | 22 +++ .../update/Progress/progress.module.scss | 23 +++ src/components/update/electron-updater.d.ts | 10 ++ src/components/update/index.tsx | 133 ++++++++++++++++++ src/components/update/update.module.scss | 31 ++++ 12 files changed, 475 insertions(+), 11 deletions(-) create mode 100644 electron/main/update.ts create mode 100644 src/components/update/Modal/index.tsx create mode 100644 src/components/update/Modal/modal.module.scss create mode 100644 src/components/update/Progress/index.tsx create mode 100644 src/components/update/Progress/progress.module.scss create mode 100644 src/components/update/electron-updater.d.ts create mode 100644 src/components/update/index.tsx create mode 100644 src/components/update/update.module.scss diff --git a/electron-builder.json5 b/electron-builder.json5 index 05ad4f3..e568aa9 100644 --- a/electron-builder.json5 +++ b/electron-builder.json5 @@ -14,7 +14,8 @@ "mac": { "artifactName": "${productName}_${version}.${ext}", "target": [ - "dmg" + "dmg", + "zip" ] }, "win": { @@ -33,5 +34,10 @@ "perMachine": false, "allowToChangeInstallationDirectory": true, "deleteAppDataOnUninstall": false + }, + publish:{ + provider: 'generic', + channel: 'latest', + url: 'https://github.com/electron-vite/electron-vite-react/releases/download/v0.9.9/', } } \ No newline at end of file diff --git a/electron/main/index.ts b/electron/main/index.ts index d99e0cf..0deb058 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -1,6 +1,7 @@ import { app, BrowserWindow, shell, ipcMain } from 'electron' import { release } from 'node:os' import { join } from 'node:path' +import { update } from './update' // The built directory structure // @@ -72,6 +73,9 @@ async function createWindow() { if (url.startsWith('https:')) shell.openExternal(url) return { action: 'deny' } }) + + // Apply electron-updater + update(win) } app.whenReady().then(createWindow) @@ -113,4 +117,5 @@ ipcMain.handle('open-win', (_, arg) => { } else { childWindow.loadFile(indexHtml, { hash: arg }) } -}) \ No newline at end of file +}) + diff --git a/electron/main/update.ts b/electron/main/update.ts new file mode 100644 index 0000000..473671c --- /dev/null +++ b/electron/main/update.ts @@ -0,0 +1,73 @@ +import { app, ipcMain } from 'electron' +import { + type ProgressInfo, + type UpdateDownloadedEvent, + autoUpdater +} from 'electron-updater' + +export function update(win: Electron.BrowserWindow) { + + // When set to false, the update download will be triggered through the API + autoUpdater.autoDownload = false + autoUpdater.disableWebInstaller = false + autoUpdater.allowDowngrade = false + + // start check + autoUpdater.on('checking-for-update', function () { }) + // update available + autoUpdater.on('update-available', (arg) => { + win.webContents.send('update-can-available', { update: true, version: app.getVersion(), newVersion: arg?.version }) + }) + // update not available + autoUpdater.on('update-not-available', (arg) => { + win.webContents.send('update-can-available', { update: false, version: app.getVersion(), newVersion: arg?.version }) + }) + + // Checking for updates + ipcMain.handle('check-update', async () => { + if (!app.isPackaged) { + const error = new Error('The update feature is only available after the package.') + return { message: error.message, error } + } + + try { + return await autoUpdater.checkForUpdatesAndNotify() + } catch (error) { + return { message: 'Network error', error } + } + }) + + // Start downloading and feedback on progress + ipcMain.handle('start-download', (event) => { + startDownload( + (error, progressInfo) => { + if (error) { + // feedback download error message + event.sender.send('update-error', { message: error.message, error }) + } else { + // feedback update progress message + event.sender.send('download-progress', progressInfo) + } + }, + () => { + // feedback update downloaded message + event.sender.send('update-downloaded') + } + ) + }) + + // Install now + ipcMain.handle('quit-and-install', () => { + autoUpdater.quitAndInstall(false, true) + }) +} + +function startDownload( + callback: (error: Error | null, info: ProgressInfo) => void, + complete: (evnet: UpdateDownloadedEvent) => void, +) { + autoUpdater.on('download-progress', info => callback(null, info)) + autoUpdater.on('error', error => callback(error, null)) + autoUpdater.on('update-downloaded', complete) + autoUpdater.downloadUpdate() +} diff --git a/package.json b/package.json index 08962a4..7d217c8 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,9 @@ "pree2e": "vite build --mode=test", "e2e": "playwright test" }, + "dependencies": { + "electron-updater": "^5.3.0" + }, "devDependencies": { "@playwright/test": "^1.31.0", "@types/react": "^18.0.28", diff --git a/src/App.tsx b/src/App.tsx index a5cb431..6566091 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,21 +1,21 @@ -import nodeLogo from "./assets/node.svg" +import nodeLogo from './assets/node.svg' import { useState } from 'react' +import Update from '@/components/update' import './App.scss' console.log('[App.tsx]', `Hello world from Electron ${process.versions.electron}!`) function App() { const [count, setCount] = useState(0) - return ( -
+
- - Electron + Vite logo + + Electron + Vite logo

Electron + Vite + React

-
+
@@ -23,12 +23,14 @@ function App() { Edit src/App.tsx and save to test HMR

-

+

Click on the Electron + Vite logo to learn more

-
- Place static files into the/public folder Node logo +
+ Place static files into the/public folder Node logo
+ +
) } diff --git a/src/components/update/Modal/index.tsx b/src/components/update/Modal/index.tsx new file mode 100644 index 0000000..c8d6922 --- /dev/null +++ b/src/components/update/Modal/index.tsx @@ -0,0 +1,67 @@ +import React, { ReactNode } from 'react' +import { createPortal } from 'react-dom' +import styles from './modal.module.scss' + +const ModalTemplate: React.FC void + onOk?: () => void + width?: number +}>> = props => { + const { + title, + children, + footer, + cancelText = 'Cancel', + okText = 'OK', + onCancel, + onOk, + width = 530, + } = props + + return ( +
+
+
+
+
+
{title}
+ + + + + + +
+
{children}
+ {typeof footer !== 'undefined' ? ( +
+ + +
+ ) : footer} +
+
+
+ ) +} + +const Modal = (props: Parameters[0] & { open: boolean }) => { + const { open, ...omit } = props + + return createPortal( + open ? ModalTemplate(omit) : null, + document.body, + ) +} + +export default Modal diff --git a/src/components/update/Modal/modal.module.scss b/src/components/update/Modal/modal.module.scss new file mode 100644 index 0000000..7218815 --- /dev/null +++ b/src/components/update/Modal/modal.module.scss @@ -0,0 +1,89 @@ +.modal { + --primary-color: rgb(224, 30, 90); + + :global { + .modal-mask { + width: 100vw; + height: 100vh; + position: fixed; + left: 0; + top: 0; + z-index: 9; + background: rgba(0, 0, 0, 0.45); + } + + .modal-warp { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 19; + } + + .modal-content { + box-shadow: 0 0 10px -4px rgb(130, 86, 208); + overflow: hidden; + border-radius: 4px; + + .modal-header { + display: flex; + line-height: 38px; + background-color: var(--primary-color); + + .modal-header-text { + font-weight: bold; + width: 0; + flex-grow: 1; + } + } + + .modal-close { + width: 30px; + height: 30px; + margin: 4px; + line-height: 34px; + text-align: center; + cursor: pointer; + + svg { + width: 17px; + height: 17px; + } + } + + .modal-body { + padding: 10px; + background-color: #fff; + color: #333; + } + + .modal-footer { + padding: 10px; + background-color: #fff; + display: flex; + justify-content: end; + + button { + padding: 7px 11px; + background-color: var(--primary-color); + font-size: 14px; + margin-left: 10px; + + &:first-child { + margin-left: 0; + } + } + } + } + + .icon { + padding: 0 15px; + width: 20px; + fill: currentColor; + + &:hover { + color: rgba(0, 0, 0, 0.4); + } + } + } +} diff --git a/src/components/update/Progress/index.tsx b/src/components/update/Progress/index.tsx new file mode 100644 index 0000000..f7c9525 --- /dev/null +++ b/src/components/update/Progress/index.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import styles from './progress.module.scss' + +const Progress: React.FC> = props => { + const { percent = 0 } = props + + return ( +
+
+
+
+ {(percent ?? 0).toString().substring(0,4)}% +
+ ) +} + +export default Progress diff --git a/src/components/update/Progress/progress.module.scss b/src/components/update/Progress/progress.module.scss new file mode 100644 index 0000000..a963e64 --- /dev/null +++ b/src/components/update/Progress/progress.module.scss @@ -0,0 +1,23 @@ +.progress { + display: flex; + align-items: center; + + :global { + .progress-pr { + border: 1px solid #000; + border-radius: 3px; + height: 6px; + width: 200px; + } + + .progress-rate { + height: 6px; + border-radius: 3px; + background-image: linear-gradient(to right, rgb(130, 86, 208) 0%, var(--primary-color) 100%) + } + + .progress-num { + margin: 0 10px; + } + } +} diff --git a/src/components/update/electron-updater.d.ts b/src/components/update/electron-updater.d.ts new file mode 100644 index 0000000..5b26aba --- /dev/null +++ b/src/components/update/electron-updater.d.ts @@ -0,0 +1,10 @@ +interface VersionInfo { + update: boolean + version: string + newVersion?: string +} + +interface ErrorType { + message: string + error: Error +} diff --git a/src/components/update/index.tsx b/src/components/update/index.tsx new file mode 100644 index 0000000..742784b --- /dev/null +++ b/src/components/update/index.tsx @@ -0,0 +1,133 @@ +import { ipcRenderer } from 'electron' +import type { ProgressInfo } from 'electron-updater' +import { useCallback, useEffect, useState } from 'react' +import Modal from '@/components/update/Modal' +import Progress from '@/components/update/Progress' +import styles from './update.module.scss' + +const Update = () => { + const [checking, setChecking] = useState(false) + const [updateAvailable, setUpdateAvailable] = useState(false) + const [versionInfo, setVersionInfo] = useState() + const [updateError, setUpdateError] = useState() + const [progressInfo, setProgressInfo] = useState>() + const [modalOpen, setModalOpen] = useState(false) + const [modalBtn, setModalBtn] = useState<{ + cancelText?: string + okText?: string + onCancel?: () => void + onOk?: () => void + }>({ + onCancel: () => setModalOpen(false), + onOk: () => ipcRenderer.invoke('start-download'), + }) + + const checkUpdate = async () => { + setChecking(true) + /** + * @type {import('electron-updater').UpdateCheckResult | null | { message: string, error: Error }} + */ + const result = await ipcRenderer.invoke('check-update') + setProgressInfo({ percent: 0 }) + setChecking(false) + setModalOpen(true) + if (result?.error) { + setUpdateAvailable(false) + setUpdateError(result?.error) + } + } + + const onUpdateCanAvailable = useCallback((_event: Electron.IpcRendererEvent, arg1: VersionInfo) => { + setVersionInfo(arg1) + setUpdateError(undefined) + // Can be update + if (arg1.update) { + setModalBtn(state => ({ + ...state, + cancelText: 'Cancel', + okText: 'Update', + onOk: () => ipcRenderer.invoke('start-download'), + })) + setUpdateAvailable(true) + } else { + setUpdateAvailable(false) + } + }, []) + + const onUpdateError = useCallback((_event: Electron.IpcRendererEvent, arg1: ErrorType) => { + setUpdateAvailable(false) + setUpdateError(arg1) + }, []) + + const onDownloadProgress = useCallback((_event: Electron.IpcRendererEvent, arg1: ProgressInfo) => { + setProgressInfo(arg1) + }, []) + + const onUpdateDownloaded = useCallback((_event: Electron.IpcRendererEvent, ...args: any[]) => { + setProgressInfo({ percent: 100 }) + setModalBtn(state => ({ + ...state, + cancelText: 'Later', + okText: 'Install now', + onOk: () => ipcRenderer.invoke('quit-and-install'), + })) + }, []) + + useEffect(() => { + // Get version information and whether to update + ipcRenderer.on('update-can-available', onUpdateCanAvailable) + ipcRenderer.on('update-error', onUpdateError) + ipcRenderer.on('download-progress', onDownloadProgress) + ipcRenderer.on('update-downloaded', onUpdateDownloaded) + + return () => { + ipcRenderer.off('update-can-available', onUpdateCanAvailable) + ipcRenderer.off('update-error', onUpdateError) + ipcRenderer.off('download-progress', onDownloadProgress) + ipcRenderer.off('update-downloaded', onUpdateDownloaded) + } + }, []) + + return ( + <> + +
+ {updateError + ? ( +
+

Error downloading the latest version.

+

{updateError.message}

+
+ ) : updateAvailable + ? ( +
+
The last version is: v{versionInfo?.newVersion}
+
v{versionInfo?.version} -> v{versionInfo?.newVersion}
+
+
Update progress:
+
+ +
+
+
+ ) + : ( +
{JSON.stringify(versionInfo ?? {}, null, 2)}
+ )} +
+
+ + + ) +} + +export default Update diff --git a/src/components/update/update.module.scss b/src/components/update/update.module.scss new file mode 100644 index 0000000..32fa35b --- /dev/null +++ b/src/components/update/update.module.scss @@ -0,0 +1,31 @@ +.modalslot { + // display: flex; + // align-items: center; + // justify-content: center; + + :global { + .update-progress { + display: flex; + } + + .progress-title { + margin-right: 10px; + } + + .progress-bar { + width: 0; + flex-grow: 1; + } + + .can-available { + .new-version-target,.update-progress { + margin-left: 40px; + } + } + + .can-not-available { + padding: 20px; + text-align: center; + } + } +} \ No newline at end of file