diff --git a/electron-builder.json5 b/electron-builder.json5 index 05ad4f3..fd9886a 100644 --- a/electron-builder.json5 +++ b/electron-builder.json5 @@ -33,5 +33,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..a4540b7 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 '../preload/update' // The built directory structure // @@ -72,6 +73,7 @@ async function createWindow() { if (url.startsWith('https:')) shell.openExternal(url) return { action: 'deny' } }) + update(win) } app.whenReady().then(createWindow) @@ -113,4 +115,5 @@ ipcMain.handle('open-win', (_, arg) => { } else { childWindow.loadFile(indexHtml, { hash: arg }) } -}) \ No newline at end of file +}) + diff --git a/electron/preload/update.ts b/electron/preload/update.ts new file mode 100644 index 0000000..76b0a05 --- /dev/null +++ b/electron/preload/update.ts @@ -0,0 +1,95 @@ +import { autoUpdater } from "electron-updater" +import { app, ipcMain } from "electron"; +export const update = (win: Electron.CrossProcessExports.BrowserWindow) => { + + // When set to false, the update download will be triggered through the API + autoUpdater.autoDownload = false; + + autoUpdater.disableWebInstaller = false + + autoUpdater.allowDowngrade = false; + + // Save the version status of whether the update needs to be installed, + // Because the user needs to update immediately and later after the update is downloaded + let NEED_INSTALL = false; + + // Check whether update is used + ipcMain.on('check-update',()=>{ + autoUpdater.checkForUpdatesAndNotify() + .then((res) => { + win.webContents.send('check-update-type',{ checkUpdate: true}) + }).catch(err => { + // network error + win.webContents.send('check-update-type', { checkUpdate: false}) + }); + }) + + // start check + autoUpdater.on('checking-for-update', function () { + console.log('checking-for-update') + }) + // update available + autoUpdater.on('update-available', (arg) => { + console.log('update-available') + win.webContents.send('is-update-available', { isUpdate: true, oldVersion: app.getVersion(), newVersion: arg?.version }) + }) + // update not available + autoUpdater.on('update-not-available', (arg) => { + console.log('update-not-available') + win.webContents.send('is-update-available', { isUpdate: false, oldVersion: app.getVersion(), newVersion: arg?.version }) + }) + + const startDownload = (callback: any, successCallback: any) => { + // Monitor the download progress and push it to the update window + autoUpdater.on('download-progress', (data) => { + console.log("progress", data) + win.webContents.send('download-progress-data', data) + callback && callback instanceof Function && callback(null, data); + }); + // Listen for download errors and push to the update window + autoUpdater.on('error', (err) => { + callback && callback instanceof Function && callback(err); + }); + // Listen to the download completion and push it to the update window + autoUpdater.on('update-downloaded', () => { + NEED_INSTALL = true; + successCallback && successCallback instanceof Function && successCallback(); + }); + + autoUpdater.downloadUpdate(); + }; + + // Listen to the process message sent by the application layer and start downloading updates + ipcMain.on('start-download', (event) => { + console.log("start") + startDownload( + (err: any, progressInfo: { percent: any; }) => { + if (err) { + // callback download error message + event.sender.send('update-error', { updateError:true}); + } else { + // callback update progress message + event.sender.send('update-progress', { progressInfo: progressInfo.percent }); + } + }, + () => { + // callback update downed message + event.sender.send('update-downed'); + } + ); + }); + + // install now + ipcMain.on('quit-and-install', () => { + autoUpdater.quitAndInstall(false, true); + }) + + // install later + app.on('will-quit', () => { + console.log("NEED_INSTALL=true") + if (NEED_INSTALL) { + autoUpdater.quitAndInstall(true, false); + } + }); + +} \ No newline at end of file diff --git a/package.json b/package.json index 08962a4..a373bcc 100644 --- a/package.json +++ b/package.json @@ -36,5 +36,8 @@ }, "engines": { "node": "^14.18.0 || >=16.0.0" + }, + "dependencies": { + "electron-updater": "^5.3.0" } } diff --git a/src/App.tsx b/src/App.tsx index a5cb431..c4c091d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,21 +1,22 @@ -import nodeLogo from "./assets/node.svg" +import nodeLogo from './assets/node.svg' import { useState } from 'react' import './App.scss' +import Update from '@/components/update' 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 +24,13 @@ 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..7ecce0b --- /dev/null +++ b/src/components/update/Modal/index.tsx @@ -0,0 +1,62 @@ +import { createPortal } from 'react-dom'; +import { ModalChildType, ModalPropsType } from './type'; +import modalScss from './modal.module.scss' +const ModalTemplate = (child: ModalChildType) => { + return ( +
+
+
+
+ {child.isHeaderShow ? ( +
+
{child.titleText}
+ + + +
+ ) : null} + +
{child.body}
+ {child.isFooterShow ? ( +
+ {(child.isSubmitShow ?? true) ? : null} + {(child.isCanCelShow ?? true) ? : null} +
+ ) : null} +
+
+
+ ); +}; + +const Modal = (props: ModalPropsType) => { + return createPortal( + props.isOpenModal? + ModalTemplate({ + titleText: props.titleText, + isHeaderShow: props.isHeaderShow ?? true, + isFooterShow: props.isFooterShow ?? true, + isCanCelShow: props.isCanCelShow ?? true, + isSubmitShow: props.isSubmitShow ?? true, + body: props.children, + submitText: props.submitText, + canCelText: props.canCelText, + onCanCel: props.onCanCel, + onSubmit: props.onSubmit, + }):
, + 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..a9536de --- /dev/null +++ b/src/components/update/Modal/modal.module.scss @@ -0,0 +1,63 @@ +.modal{ + :global{ + .modal-bg { + width: 100vw; + height: 100vh; + position: fixed; + left: 0; + top: 0; + z-index: 9999; + background: rgba(0, 0, 0, 0.3); + } + + .modal-outboard { + position: absolute; + top: 20vh; + left: 30vw; + z-index: 10000; + } + + .modal-panel { + border: 1px solid #000000; + border-radius: 5px; + + .modal-header { + $titleheight: 38px; + width: 530px; + height: $titleheight; + line-height: $titleheight; + background-color: rgb(99, 153, 255); + display: flex; + + .modal-header-text { + text-align: center; + width: 480px; + } + } + + .modal-body { + background-color: #ffffff; + } + + .modal-footer { + background-color: #ffffff; + display: flex; + justify-content: end; + + button { + margin: 10px; + } + } + } + + .icon { + padding: 0 15px; + width: 20px; + fill: currentColor; + + &:hover { + color: rgba(0, 0, 0, 0.4); + } + } + } +} \ No newline at end of file diff --git a/src/components/update/Modal/type.d.ts b/src/components/update/Modal/type.d.ts new file mode 100644 index 0000000..2ba43c3 --- /dev/null +++ b/src/components/update/Modal/type.d.ts @@ -0,0 +1,20 @@ +import { ReactNode } from 'react' +interface childrens { + titleText?: string + isHeaderShow?: boolean + isFooterShow?: boolean + isCanCelShow?: boolean + isSubmitShow?: boolean + canCelText?: string + submitText?: string + onSubmit?: () => void + onCanCel?: () => void +} +export interface ModalChildType extends childrens { + body: ReactNode | null +} + +export interface ModalPropsType extends childrens { + isOpenModal: boolean + children: ReactNode | null +} diff --git a/src/components/update/Progress/index.tsx b/src/components/update/Progress/index.tsx new file mode 100644 index 0000000..bb82162 --- /dev/null +++ b/src/components/update/Progress/index.tsx @@ -0,0 +1,22 @@ +import { RsProgressType } from './type' +import progressScss from './progress.module.scss' + +const Progress = (props: RsProgressType) => { + + return ( +
+
+
+
+ {props.percent > 100 ? 100 :(props.percent.toString().substring(0,4) ?? 0) }% +
+ ); +}; + +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..ff84486 --- /dev/null +++ b/src/components/update/Progress/progress.module.scss @@ -0,0 +1,21 @@ +.progress { + display: flex; + align-items: center; + + :global { + .progress-pr { + border: 1px solid #000000; + border-radius: 3px; + height: 6px; + } + + .progress-rate { + height: 6px; + border-radius: 3px; + } + + .progress-num { + margin: 0 10px; + } + } +} \ No newline at end of file diff --git a/src/components/update/Progress/type.d.ts b/src/components/update/Progress/type.d.ts new file mode 100644 index 0000000..24a0970 --- /dev/null +++ b/src/components/update/Progress/type.d.ts @@ -0,0 +1,5 @@ +export interface RsProgressType { + rateColor?: string + rateWidth?: number + percent: number +} diff --git a/src/components/update/index.tsx b/src/components/update/index.tsx new file mode 100644 index 0000000..3eb4fd0 --- /dev/null +++ b/src/components/update/index.tsx @@ -0,0 +1,123 @@ +import Modal from '@/components/update/Modal' +import Progress from '@/components/update/Progress' +import { ipcRenderer } from 'electron' +import { useEffect, useState } from 'react' +import updateScss from './update.module.scss' +import { checkUpdateType, isUpdateAvailable, ModalBtnText, VersionInfo } from './type' + + +let onModalSubmit = () => { } +let onModalCanCel = () => { } + +const Update = () => { + const [checkBtnText, setCheckBtnText] = useState('check update') + const [checkType, setCheckType] = useState(false) + const [checkLoading, setCheckLoading] = useState(false) + const [isOpenModal, setIsOpenModal] = useState(false) + const [percentNum, setPercentNum] = useState(0) + const [isNeedUpdate, setIsNeedUpdate] = useState(false) + const [updateError, setUpdateError] = useState(false) + const [versionInfo, setVersionInfo] = useState({ + oldVersion: '', + newVersion: '' + }) + const [modalBtnText, setModalBtnText] = useState({ + canCelText: '', + submitText: '' + }) + + useEffect(() => { + onModalCanCel = () => setIsOpenModal(false) + }, []) + + // Check for updates + const checkUpdate = () => { + setCheckLoading(true) + setCheckBtnText('checking Update ...') + ipcRenderer.send('check-update') + } + + // Listen to get the check result + ipcRenderer.on('check-update-type', (_event, ...args: checkUpdateType[]) => { + setCheckLoading(false) + setCheckBtnText('check update') + setCheckType(args[0].checkUpdate) + setIsOpenModal(true) + }) + + // Get version information and whether to update + ipcRenderer.on('is-update-available', (_event, ...args: isUpdateAvailable[]) => { + setVersionInfo({ + oldVersion: args[0].oldVersion, + newVersion: args[0].newVersion, + }) + setIsNeedUpdate(args[0].isUpdate) + // Update required + if (args[0].isUpdate) { + setModalBtnText({ + canCelText: 'cancel', + submitText: 'update' + }) + onModalSubmit = () => ipcRenderer.send('start-download') + onModalCanCel = () => setIsOpenModal(false) + } + }) + + // Throw the update failure message when the update fails + ipcRenderer.on('update-error', (_event, ...args: { updateError: boolean }[]) => { + setUpdateError(args[0].updateError) + setCheckType(false) + }) + + // Get update progress + ipcRenderer.on('update-progress', (_event, ...args: { progressInfo: number }[]) => { + setPercentNum(args[0].progressInfo) + }) + + // is update been completed + ipcRenderer.on('update-downed', (_event, ...args) => { + setPercentNum(100) + setModalBtnText({ + canCelText: 'install later', + submitText: 'install now' + }) + onModalSubmit = () => ipcRenderer.send('quit-and-install') + onModalCanCel = () => { + ipcRenderer.send('will-quit') + setIsOpenModal(false) + } + }) + + return ( + <> + +
+ {updateError ? +
Error downloading the latest version, please contact the developer
: + checkType ? ( + isNeedUpdate ? ( +
+
+ oldVersion : v.{versionInfo.oldVersion} + newVersion : v.{versionInfo.newVersion} +
+
+ update progress : + +
+
) + : This is last version : v.{versionInfo.oldVersion} ! + ) : Check update is Error,Please check your network! + } +
+
+ + + ) +} + +export default Update \ No newline at end of file diff --git a/src/components/update/type.d.ts b/src/components/update/type.d.ts new file mode 100644 index 0000000..de30813 --- /dev/null +++ b/src/components/update/type.d.ts @@ -0,0 +1,18 @@ + +export interface checkUpdateType { + checkUpdate: boolean +} + +export interface VersionInfo { + oldVersion: string + newVersion: string +} + +export interface isUpdateAvailable extends VersionInfo { + isUpdate: boolean + +} +export interface ModalBtnText { + canCelText: string + submitText: string +} diff --git a/src/components/update/update.module.scss b/src/components/update/update.module.scss new file mode 100644 index 0000000..926bb44 --- /dev/null +++ b/src/components/update/update.module.scss @@ -0,0 +1,19 @@ +.modalslot{ + display: flex; + align-items: center; + justify-content: center; + height: 100px; + + :global { + .progress-title { + width: 150px; + } + + .update-progress { + display: flex; + } + } +} +.a{ + color: red; +} \ No newline at end of file