diff --git a/electron-builder.json5 b/electron-builder.json5 index 05ad4f3..02c29d7 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/RSS1101/electron-vite-react/releases/download/v9.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..4f8b355 --- /dev/null +++ b/electron/preload/update.ts @@ -0,0 +1,113 @@ +import { autoUpdater } from "electron-updater" +import { app, ipcMain } from "electron"; +export const update = (win: Electron.CrossProcessExports.BrowserWindow) => { + // 设置日志打印 + + // 是否自动下载更新,设置为 false 时将通过 API 触发更新下载 + autoUpdater.autoDownload = false; + + autoUpdater.disableWebInstaller = false + + // 是否允许版本降级,也就是服务器版本低于本地版本时,依旧以服务器版本为主 + autoUpdater.allowDowngrade = false; + + // 设置服务器版本最新版本查询接口配置 + autoUpdater.setFeedURL({ + provider: 'generic', + channel: 'latest', + url: 'https://github.com/RSS1101/electron-vite-react/releases/download/v9.9.9/', + }); + + // 保存是否需要安装更新的版本状态,因为需要提供用户在下载完成更新之后立即更新和稍后更新的操作 + let NEED_INSTALL = false; + + Object.defineProperty(app, 'isPackaged', { + get() { + return true; + } + }); + + // 调用 API 检查是否用更新 + 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}) + }); + }) + + // 检测开始 + autoUpdater.on('checking-for-update', function () { + console.log('checking-for-update') + }) + // 更新可用 + autoUpdater.on('update-available', (arg) => { + console.log('update-available') + win.webContents.send('is-update-available', { isUpdate: true, oldVersion: app.getVersion(), newVersion: arg?.version }) + }) + // 更新不可用 + 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 }) + }) + // API 触发更新下载 + const startDownload = (callback: any, successCallback: any) => { + // 监听下载进度并推送到更新窗口 + autoUpdater.on('download-progress', (data) => { + console.log("progress", data) + win.webContents.send('download-progress-data', data) + callback && callback instanceof Function && callback(null, data); + }); + // 监听下载错误并推送到更新窗口 + autoUpdater.on('error', (err) => { + console.log("error") + callback && callback instanceof Function && callback(err); + }); + // 监听下载完成并推送到更新窗口 + autoUpdater.on('update-downloaded', () => { + console.log("update-downloaded") + NEED_INSTALL = true; + successCallback && successCallback instanceof Function && successCallback(); + }); + // 下载更新 + autoUpdater.downloadUpdate(); + }; + + // 监听应用层发送来的进程消息,开始下载更新 + ipcMain.on('start-download', (event) => { + console.log("start") + startDownload( + (err: any, progressInfo: { percent: any; }) => { + if (err) { + //回推下载错误消息 + console.log("update-error") + event.sender.send('update-error', { updateError:true}); + } else { + //回推下载进度消息 + console.log("pdate-progress-percent") + event.sender.send('update-progress', { progressInfo: progressInfo.percent }); + } + }, + () => { + //回推下载完成消息 + console.log("update-downed") + event.sender.send('update-downed'); + } + ); + }); + + ipcMain.on('quit-and-install', () => { + autoUpdater.quitAndInstall(false, true); + }) + + // 用户点击稍后安装后程序退出时执行立即安装更新 + 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.scss b/src/App.scss index 78b1263..45b0b01 100644 --- a/src/App.scss +++ b/src/App.scss @@ -32,6 +32,10 @@ .card { padding: 2em; + button{ + margin: 0 20px; + background-color: #646cffaa; + } } .read-the-docs { diff --git a/src/App.tsx b/src/App.tsx index a5cb431..9dcbf08 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,48 @@ import nodeLogo from "./assets/node.svg" -import { useState } from 'react' +import { useEffect, useRef, useState } from 'react' import './App.scss' +import Update from "./pages/update" +import { ipcRenderer } from "electron" console.log('[App.tsx]', `Hello world from Electron ${process.versions.electron}!`) +interface checkUpdateType { + checkUpdate: boolean +} + function App() { const [count, setCount] = useState(0) + const ref =useRef(null) + const [checkBtnText, setCheckBtnText] = useState('check update') + const [checkLoading, setCheckLoading] = useState(false) + const [checkType, setCheckType] = useState(false) + const [openModalType, setopenModalType] = useState(false) + /** + * @description 检查是否有新的更新 Listen for new updates + */ + const checkUpdate = () => { + setCheckLoading(true) + setCheckBtnText('checking Update ...') + ipcRenderer.send('check-update') + } + /** + * @description 获取到检查结果 Get the check results + */ + ipcRenderer.on('check-update-type', (_event, ...args: checkUpdateType[]) => { + setCheckLoading(false) + setCheckBtnText('check update') + setCheckType(args[0].checkUpdate) + setopenModalType(true) + }) + /** + * @description 在通信监听的过程中获取不到ref. + */ + useEffect(()=>{ + if (openModalType){ + ref.current!.openModal() + setopenModalType(false) + } + }, [openModalType]) return (
@@ -19,6 +56,9 @@ function App() { +

Edit src/App.tsx and save to test HMR

@@ -29,6 +69,7 @@ function App() {
Place static files into the/public folder Node logo
+
) } diff --git a/src/components/RsModal/index.scss b/src/components/RsModal/index.scss new file mode 100644 index 0000000..8488a39 --- /dev/null +++ b/src/components/RsModal/index.scss @@ -0,0 +1,50 @@ +.rs-modal-bg{ + width: 100vw; + height: 100vh; + position: fixed; + left: 0; + top: 0; + z-index: 9999; + background: rgba(0, 0, 0, 0.3); +} +.rs-modal { + position: absolute; + top: 20vh; + left: 30vw; + z-index: 10000; + .rs-modal-panel { + border: 1px solid #000000; + border-radius: 5px; + .rs-modal-header { + $titleheight: 38px; + width: 530px; + height: $titleheight; + line-height: $titleheight; + background-color: rgb(99, 153, 255); + display: flex; + .rs-modal-header-text { + text-align: center; + width: 480px; + } + } + .rs-modal-body{ + background-color: #ffffff; + } + .rs-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); + } + } +} diff --git a/src/components/RsModal/index.tsx b/src/components/RsModal/index.tsx new file mode 100644 index 0000000..9e50878 --- /dev/null +++ b/src/components/RsModal/index.tsx @@ -0,0 +1,60 @@ +import { createPortal } from 'react-dom'; +import { ModalChildType, ModalPropsType } from './type'; +import './index.scss'; +const ModalTemplate = (child: ModalChildType) => { + return ( + <> +
+
+
+ {child.isHeaderShow ? ( +
+
{child.titleText}
+ + + +
+ ) : null} + +
{child.body}
+ {child.isFooterShow ? ( +
+ + +
+ ) : null} +
+
+ + ); +}; + +const RsModal = (props: ModalPropsType) => { + return createPortal( + props.isOpenModal? + ModalTemplate({ + titleText: props.titleText, + isHeaderShow: props.isHeaderShow ?? true, + isFooterShow: props.isFooterShow ?? true, + body: props.children, + submitText: props.submitText, + canCelText: props.canCelText, + onCanCel: props.onCanCel, + onSubmit: props.onSubmit, + }):
, + document.body, + ); +}; +export default RsModal; diff --git a/src/components/RsModal/type.d.ts b/src/components/RsModal/type.d.ts new file mode 100644 index 0000000..90b83fe --- /dev/null +++ b/src/components/RsModal/type.d.ts @@ -0,0 +1,18 @@ +import { ReactNode } from 'react'; +interface childrens { + titleText?: string; + isHeaderShow?: boolean; + isFooterShow?: 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/RsProgress/index.scss b/src/components/RsProgress/index.scss new file mode 100644 index 0000000..0ba7768 --- /dev/null +++ b/src/components/RsProgress/index.scss @@ -0,0 +1,16 @@ +.rs-progress{ + display: flex; + align-items: center; + .rs-progress-pr{ + border: 1px solid #000000; + border-radius: 3px; + height: 6px; + } + .rs-progress-rate{ + height: 6px; + border-radius: 3px; + } + .rs-progress-num{ + margin: 0 10px; + } +} diff --git a/src/components/RsProgress/index.tsx b/src/components/RsProgress/index.tsx new file mode 100644 index 0000000..22e8639 --- /dev/null +++ b/src/components/RsProgress/index.tsx @@ -0,0 +1,22 @@ +import { RsProgressType } from './type'; +import './index.scss'; + +const RsProgress = (props: RsProgressType) => { + + return ( +
+
+
+
+ {props.percent > 100 ? 100 :(props.percent.toString().substring(0,2) ?? 0) }% +
+ ); +}; + +export default RsProgress; diff --git a/src/components/RsProgress/type.d.ts b/src/components/RsProgress/type.d.ts new file mode 100644 index 0000000..1012a9a --- /dev/null +++ b/src/components/RsProgress/type.d.ts @@ -0,0 +1,5 @@ +export interface RsProgressType { + rateColor?: string; + rateWidth?: number; + percent: number; +} diff --git a/src/pages/update/index.scss b/src/pages/update/index.scss new file mode 100644 index 0000000..9b56d90 --- /dev/null +++ b/src/pages/update/index.scss @@ -0,0 +1,13 @@ +.modal-body{ + display: flex; + align-items: center; + justify-content: center; + height: 100px; + .progress-title{ + width: 150px; + } + + .update-progress{ + display: flex; + } +} \ No newline at end of file diff --git a/src/pages/update/index.tsx b/src/pages/update/index.tsx new file mode 100644 index 0000000..0982932 --- /dev/null +++ b/src/pages/update/index.tsx @@ -0,0 +1,123 @@ +import RsModal from "@/components/RsModal" +import RsProgress from "@/components/RsProgress" +import { ipcRenderer } from "electron" +import { forwardRef, useEffect, useImperativeHandle, useState } from "react" +import "./index.scss" + +interface VersionInfo { + oldVersion: string, + newVersion: string +} + +interface isUpdateAvailable extends VersionInfo { + isUpdate: boolean, + +} +interface ModalBtnText { + canCelText: string, + submitText: string +} + +let onModalSubmit = () => { } +let onModalCanCel = () => { } + +const Update = forwardRef((props: { checkType: boolean }, ref) => { + 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: '' + }) + + useImperativeHandle(ref, () => ({ + openModal: () => setIsOpenModal(true) + })); + + useEffect(() => { + onModalCanCel = () => setIsOpenModal(false) + }, []) + + /** + * @description 获取版本信息和是否需要更新 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) + if (args[0].isUpdate) { + setModalBtnText({ + canCelText: 'cancel', + submitText: 'update' + }) + onModalSubmit = () => ipcRenderer.send('start-download') + onModalCanCel = () => setIsOpenModal(false) + } + }) + /** + * @description 如果更新失败了抛出更新失败信息 Throw the update failure message if the update fails + */ + ipcRenderer.on('update-error', (_event, ...args: { updateError: boolean }[]) => { + setUpdateError(args[0].updateError) + }) + /** + * @description 监听更新进度 update progress + */ + ipcRenderer.on('update-progress', (_event, ...args: { progressInfo: number }[]) => { + setPercentNum(args[0].progressInfo) + }) + /** + * @description 监听是否更新完成 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
: + props.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