refactor: cleanup

This commit is contained in:
草鞋没号 2023-03-05 10:39:45 +08:00
parent 1f8b17810a
commit 07f3854019
15 changed files with 374 additions and 385 deletions

View File

@ -1,7 +1,7 @@
import { app, BrowserWindow, shell, ipcMain } from 'electron' import { app, BrowserWindow, shell, ipcMain } from 'electron'
import { release } from 'node:os' import { release } from 'node:os'
import { join } from 'node:path' import { join } from 'node:path'
import { update } from '../preload/update' import { update } from './update'
// The built directory structure // The built directory structure
// //
@ -73,6 +73,8 @@ async function createWindow() {
if (url.startsWith('https:')) shell.openExternal(url) if (url.startsWith('https:')) shell.openExternal(url)
return { action: 'deny' } return { action: 'deny' }
}) })
// Apply electron-updater
update(win) update(win)
} }

70
electron/main/update.ts Normal file
View File

@ -0,0 +1,70 @@
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 () => {
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()
}

View File

@ -1,95 +0,0 @@
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);
}
});
}

View File

@ -18,6 +18,9 @@
"pree2e": "vite build --mode=test", "pree2e": "vite build --mode=test",
"e2e": "playwright test" "e2e": "playwright test"
}, },
"dependencies": {
"electron-updater": "^5.3.0"
},
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.31.0", "@playwright/test": "^1.31.0",
"@types/react": "^18.0.28", "@types/react": "^18.0.28",
@ -36,8 +39,5 @@
}, },
"engines": { "engines": {
"node": "^14.18.0 || >=16.0.0" "node": "^14.18.0 || >=16.0.0"
},
"dependencies": {
"electron-updater": "^5.3.0"
} }
} }

View File

@ -1,11 +1,10 @@
import nodeLogo from './assets/node.svg' import nodeLogo from './assets/node.svg'
import { useState } from 'react' import { useState } from 'react'
import './App.scss'
import Update from '@/components/update' import Update from '@/components/update'
import './App.scss'
console.log('[App.tsx]', `Hello world from Electron ${process.versions.electron}!`) console.log('[App.tsx]', `Hello world from Electron ${process.versions.electron}!`)
function App() { function App() {
const [count, setCount] = useState(0) const [count, setCount] = useState(0)
return ( return (
@ -30,6 +29,7 @@ function App() {
<div className='flex-center'> <div className='flex-center'>
Place static files into the<code>/public</code> folder <img style={{ width: '5em' }} src={nodeLogo} alt='Node logo' /> Place static files into the<code>/public</code> folder <img style={{ width: '5em' }} src={nodeLogo} alt='Node logo' />
</div> </div>
<Update /> <Update />
</div> </div>
) )

View File

@ -1,62 +1,67 @@
import { createPortal } from 'react-dom'; import React, { ReactNode } from 'react'
import { ModalChildType, ModalPropsType } from './type'; import { createPortal } from 'react-dom'
import modalScss from './modal.module.scss' import styles from './modal.module.scss'
const ModalTemplate = (child: ModalChildType) => {
return (
<div className={modalScss.modal}>
<div className='modal-bg' onClick={child.onCanCel} />
<div className='modal-outboard'>
<div className='modal-panel'>
{child.isHeaderShow ? (
<div className='modal-header'>
<div className='modal-header-text'>{child.titleText}</div>
<svg
onClick={child.onCanCel}
className='icon'
viewBox='0 0 1026 1024'
version='1.1'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M585.781589 510.748226l423.38657-423.38657A51.071963
51.071963 0 0 0 937.156692 14.839469L513.770122 438.736759
90.383552 14.839469A51.071963 51.071963 0 0 0 17.861365 87.361656L441.758655
510.748226l-423.89729 423.38657A51.071963 51.071963 0 1 0 89.872832 1006.146263l423.89729-423.38657
423.38657 423.38657a51.071963 51.071963 0 0 0 72.011467-72.011467z'
/>
</svg>
</div>
) : null}
<div className='modal-body'>{child.body}</div> const ModalTemplate: React.FC<React.PropsWithChildren<{
{child.isFooterShow ? ( title?: ReactNode
footer?: ReactNode
cancelText?: string
okText?: string
onCancel?: () => void
onOk?: () => void
width?: number
}>> = props => {
const {
title,
children,
footer,
cancelText = 'Cancel',
okText = 'OK',
onCancel,
onOk,
width = 530,
} = props
return (
<div className={styles.modal}>
<div className='modal-mask' />
<div className='modal-warp'>
<div className='modal-content' style={{ width }}>
<div className='modal-header'>
<div className='modal-header-text'>{title}</div>
<span
className='modal-close'
onClick={onCancel}
>
<svg
viewBox="0 0 1024 1024"
version="1.1" xmlns="http://www.w3.org/2000/svg"
>
<path d="M557.312 513.248l265.28-263.904c12.544-12.48 12.608-32.704 0.128-45.248-12.512-12.576-32.704-12.608-45.248-0.128l-265.344 263.936-263.04-263.84C236.64 191.584 216.384 191.52 203.84 204 191.328 216.48 191.296 236.736 203.776 249.28l262.976 263.776L201.6 776.8c-12.544 12.48-12.608 32.704-0.128 45.248 6.24 6.272 14.464 9.44 22.688 9.44 8.16 0 16.32-3.104 22.56-9.312l265.216-263.808 265.44 266.24c6.24 6.272 14.432 9.408 22.656 9.408 8.192 0 16.352-3.136 22.592-9.344 12.512-12.48 12.544-32.704 0.064-45.248L557.312 513.248z" p-id="2764" fill="currentColor">
</path>
</svg>
</span>
</div>
<div className='modal-body'>{children}</div>
{typeof footer !== 'undefined' ? (
<div className='modal-footer'> <div className='modal-footer'>
{(child.isSubmitShow ?? true) ?<button onClick={child.onSubmit}>{child.submitText ?? '确认'}</button> : null} <button onClick={onCancel}>{cancelText}</button>
{(child.isCanCelShow ?? true) ? <button onClick={child.onCanCel}>{child.canCelText ?? '取消'}</button> : null} <button onClick={onOk}>{okText}</button>
</div> </div>
) : null} ) : footer}
</div> </div>
</div> </div>
</div> </div>
); )
}; }
const Modal = (props: ModalPropsType) => { const Modal = (props: Parameters<typeof ModalTemplate>[0] & { open: boolean }) => {
return createPortal( const { open, ...omit } = props
props.isOpenModal?
ModalTemplate({ return createPortal(
titleText: props.titleText, open ? ModalTemplate(omit) : null,
isHeaderShow: props.isHeaderShow ?? true, document.body,
isFooterShow: props.isFooterShow ?? true, )
isCanCelShow: props.isCanCelShow ?? true, }
isSubmitShow: props.isSubmitShow ?? true,
body: props.children, export default Modal
submitText: props.submitText,
canCelText: props.canCelText,
onCanCel: props.onCanCel,
onSubmit: props.onSubmit,
}): <div></div>,
document.body,
);
};
export default Modal;

View File

@ -1,63 +1,89 @@
.modal{ .modal {
:global{ --primary-color: rgb(224, 30, 90);
.modal-bg {
width: 100vw; :global {
height: 100vh; .modal-mask {
position: fixed; width: 100vw;
left: 0; height: 100vh;
top: 0; position: fixed;
z-index: 9999; left: 0;
background: rgba(0, 0, 0, 0.3); top: 0;
} z-index: 9;
background: rgba(0, 0, 0, 0.45);
.modal-outboard { }
position: absolute;
top: 20vh; .modal-warp {
left: 30vw; position: fixed;
z-index: 10000; top: 50%;
} left: 50%;
transform: translate(-50%, -50%);
.modal-panel { z-index: 19;
border: 1px solid #000000; }
border-radius: 5px;
.modal-content {
.modal-header { box-shadow: 0 0 10px -4px rgb(130, 86, 208);
$titleheight: 38px; overflow: hidden;
width: 530px; border-radius: 4px;
height: $titleheight;
line-height: $titleheight; .modal-header {
background-color: rgb(99, 153, 255); display: flex;
display: flex; line-height: 38px;
background-color: var(--primary-color);
.modal-header-text {
text-align: center; .modal-header-text {
width: 480px; font-weight: bold;
} width: 0;
flex-grow: 1;
} }
}
.modal-body {
background-color: #ffffff; .modal-close {
width: 30px;
height: 30px;
margin: 4px;
line-height: 34px;
text-align: center;
cursor: pointer;
svg {
width: 17px;
height: 17px;
} }
}
.modal-footer {
background-color: #ffffff; .modal-body {
display: flex; padding: 10px;
justify-content: end; background-color: #fff;
color: #333;
button { }
margin: 10px;
.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; .icon {
width: 20px; padding: 0 15px;
fill: currentColor; width: 20px;
fill: currentColor;
&:hover {
color: rgba(0, 0, 0, 0.4); &:hover {
} color: rgba(0, 0, 0, 0.4);
} }
}
} }
} }

View File

@ -1,20 +0,0 @@
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
}

View File

@ -1,22 +1,22 @@
import { RsProgressType } from './type' import React from 'react'
import progressScss from './progress.module.scss' import styles from './progress.module.scss'
const Progress = (props: RsProgressType) => { const Progress: React.FC<React.PropsWithChildren<{
percent?: number
}>> = props => {
const { percent = 0 } = props
return ( return (
<div className={progressScss.progress}> <div className={styles.progress}>
<div className='progress-pr' style={{ width: props.rateWidth ?? 250 }}> <div className='progress-pr'>
<div <div
className='progress-rate' className='progress-rate'
style={{ style={{ width: `${percent / 100}%` }}
width: (props.percent ?? 0) * ((props.rateWidth ?? 250) / 100), />
backgroundColor: props.rateColor ?? 'blue', </div>
}} <span className='progress-num'>{percent}%</span>
/>
</div> </div>
<span className='progress-num'>{props.percent > 100 ? 100 :(props.percent.toString().substring(0,4) ?? 0) }%</span> )
</div> }
);
};
export default Progress; export default Progress

View File

@ -4,7 +4,7 @@
:global { :global {
.progress-pr { .progress-pr {
border: 1px solid #000000; border: 1px solid #000;
border-radius: 3px; border-radius: 3px;
height: 6px; height: 6px;
} }
@ -12,10 +12,11 @@
.progress-rate { .progress-rate {
height: 6px; height: 6px;
border-radius: 3px; border-radius: 3px;
background-image: linear-gradient(to right, rgb(130, 86, 208) 0%, var(--primary-color) 100%)
} }
.progress-num { .progress-num {
margin: 0 10px; margin: 0 10px;
} }
} }
} }

View File

@ -1,5 +0,0 @@
export interface RsProgressType {
rateColor?: string
rateWidth?: number
percent: number
}

View File

@ -0,0 +1,10 @@
interface VersionInfo {
update: boolean
version: string
newVersion?: string
}
interface ErrorType {
message: string
error: Error
}

View File

@ -1,123 +1,135 @@
import { ipcRenderer } from 'electron'
import type { ProgressInfo } from 'electron-updater'
import { useCallback, useEffect, useState } from 'react'
import Modal from '@/components/update/Modal' import Modal from '@/components/update/Modal'
import Progress from '@/components/update/Progress' import Progress from '@/components/update/Progress'
import { ipcRenderer } from 'electron' import styles from './update.module.scss'
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 Update = () => {
const [checkBtnText, setCheckBtnText] = useState('check update') const [checking, setChecking] = useState(false)
const [checkType, setCheckType] = useState(false) const [updateAvailable, setUpdateAvailable] = useState(false)
const [checkLoading, setCheckLoading] = useState(false) const [versionInfo, setVersionInfo] = useState<VersionInfo>()
const [isOpenModal, setIsOpenModal] = useState<boolean>(false) const [updateError, setUpdateError] = useState<ErrorType>()
const [percentNum, setPercentNum] = useState<number>(0) const [progressInfo, setProgressInfo] = useState<ProgressInfo>()
const [isNeedUpdate, setIsNeedUpdate] = useState<boolean>(false) const [modalOpen, setModalOpen] = useState<boolean>(false)
const [updateError, setUpdateError] = useState<boolean>(false) const [modalBtn, setModalBtn] = useState<{
const [versionInfo, setVersionInfo] = useState<VersionInfo>({ cancelText?: string
oldVersion: '', okText?: string
newVersion: '' onCancel?: () => void
}) onOk?: () => void
const [modalBtnText, setModalBtnText] = useState<ModalBtnText>({ }>({
canCelText: '', onCancel: () => setModalOpen(false),
submitText: '' onOk: () => ipcRenderer.invoke('start-download'),
}) })
useEffect(() => { const checkUpdate = async () => {
onModalCanCel = () => setIsOpenModal(false) setChecking(true)
}, []) /**
* @type {import('electron-updater').UpdateCheckResult | null | { message: string, error: Error }}
*/
const result = await ipcRenderer.invoke('check-update')
setChecking(false)
// Check for updates if (result?.error) {
const checkUpdate = () => { console.error(result.error)
setCheckLoading(true) setUpdateAvailable(false)
setCheckBtnText('checking Update ...') } else {
ipcRenderer.send('check-update') setUpdateAvailable(true)
setModalOpen(true)
}
} }
// Listen to get the check result const onUpdateCanAvailable = useCallback((_event: Electron.IpcRendererEvent, arg1: VersionInfo) => {
ipcRenderer.on('check-update-type', (_event, ...args: checkUpdateType[]) => { setVersionInfo(arg1)
setCheckLoading(false) // Can be update
setCheckBtnText('check update') if (arg1.update) {
setCheckType(args[0].checkUpdate) setModalBtn(state => ({
setIsOpenModal(true) ...state,
}) cancelText: 'Cancel',
okText: 'Update',
// Get version information and whether to update onCancel: () => setModalOpen(false),
ipcRenderer.on('is-update-available', (_event, ...args: isUpdateAvailable[]) => { onOk: () => ipcRenderer.invoke('start-download'),
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 const onUpdateError = useCallback((_event: Electron.IpcRendererEvent, arg1: ErrorType) => {
ipcRenderer.on('update-error', (_event, ...args: { updateError: boolean }[]) => { console.error(arg1.error)
setUpdateError(args[0].updateError) setUpdateError(arg1)
setCheckType(false) }, [])
})
// Get update progress const onDownloadProgress = useCallback((_event: Electron.IpcRendererEvent, arg1: ProgressInfo) => {
ipcRenderer.on('update-progress', (_event, ...args: { progressInfo: number }[]) => { console.log(arg1)
setPercentNum(args[0].progressInfo) setProgressInfo(arg1)
}) }, [])
// is update been completed const onUpdateDownloaded = useCallback((_event: Electron.IpcRendererEvent, ...args: any[]) => {
ipcRenderer.on('update-downed', (_event, ...args) => { setModalBtn(state => ({
setPercentNum(100) ...state,
setModalBtnText({ cancelText: 'Later',
canCelText: 'install later', okText: 'Install now',
submitText: 'install now' onOk: () => ipcRenderer.invoke('quit-and-install'),
}) }))
onModalSubmit = () => ipcRenderer.send('quit-and-install') }, [])
onModalCanCel = () => {
ipcRenderer.send('will-quit') useEffect(() => {
setIsOpenModal(false) // 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 ( return (
<> <>
<Modal isOpenModal={isOpenModal} onCanCel={onModalCanCel} onSubmit={onModalSubmit} <Modal
canCelText={modalBtnText.canCelText} submitText={modalBtnText.submitText} open={modalOpen}
isFooterShow={checkType && isNeedUpdate}> cancelText={modalBtn?.cancelText}
<div className={updateScss.modalslot}> okText={modalBtn?.okText}
{updateError ? onCancel={modalBtn?.onCancel}
<div className='update-error'>Error downloading the latest version, please contact the developer</div> : onOk={modalBtn?.onOk}
checkType ? ( footer={!versionInfo?.update ? /* hide footer */null : undefined}
isNeedUpdate ? ( >
<div> <div className={styles.modalslot}>
<div> {updateError ? (
<span> oldVersion : v.{versionInfo.oldVersion} </span> <div className='update-error'>
<span> newVersion : v.{versionInfo.newVersion} </span> <p>Error downloading the latest version.</p>
<p>{updateError.message}</p>
</div>
) : null}
{!updateAvailable ? (
<div className='update-not-available'>
<span>Check update is Error, Please check your Network!</span>
</div>
) : null}
{versionInfo
? (
<div>
<div>The last version is: v{versionInfo.newVersion}</div>
<div>v{versionInfo.version} -&gt; v{versionInfo.newVersion}</div>
<div className='update-progress'>
<div className='progress-title'>Update progress:</div>
<div className='progress-bar'>
<Progress percent={progressInfo?.percent} ></Progress>
</div> </div>
<div className='update-progress'> </div>
<span className='progress-title'> update progress : </span> </div>
<Progress percent={percentNum} ></Progress> )
</div> : <span>Checking...</span>}
</div>)
: <span>This is last version : v.{versionInfo.oldVersion} !</span>
) : <span>Check update is Error,Please check your network!</span>
}
</div> </div>
</Modal> </Modal>
<button disabled={checkLoading} onClick={checkUpdate}> <button disabled={checking} onClick={checkUpdate}>
{checkBtnText} {checking ? 'Checking...' : 'Check update'}
</button> </button>
</> </>
) )
} }
export default Update export default Update

View File

@ -1,18 +0,0 @@
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
}

View File

@ -1,19 +1,20 @@
.modalslot{ .modalslot {
display: flex; // display: flex;
align-items: center; // align-items: center;
justify-content: center; // justify-content: center;
height: 100px;
:global { :global {
.progress-title {
width: 150px;
}
.update-progress { .update-progress {
display: flex; display: flex;
} }
.progress-title {
margin-right: 10px;
}
.progress-bar {
width: 0;
flex-grow: 1;
}
} }
} }
.a{
color: red;
}