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 + 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

+
+ Place static files into the
/public
folder
+
+
)
}
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 (
+
+
+
+
+
+
{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}
+
+
+ )
+ : (
+
{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