feat: add electron-builder (#123)
# 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>
This commit is contained in:
		
							parent
							
								
									29d5fa95a8
								
							
						
					
					
						commit
						2f2880a9f1
					
				| 
						 | 
					@ -14,7 +14,8 @@
 | 
				
			||||||
  "mac": {
 | 
					  "mac": {
 | 
				
			||||||
    "artifactName": "${productName}_${version}.${ext}",
 | 
					    "artifactName": "${productName}_${version}.${ext}",
 | 
				
			||||||
    "target": [
 | 
					    "target": [
 | 
				
			||||||
      "dmg"
 | 
					      "dmg",
 | 
				
			||||||
 | 
					      "zip"
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "win": {
 | 
					  "win": {
 | 
				
			||||||
| 
						 | 
					@ -33,5 +34,10 @@
 | 
				
			||||||
    "perMachine": false,
 | 
					    "perMachine": false,
 | 
				
			||||||
    "allowToChangeInstallationDirectory": true,
 | 
					    "allowToChangeInstallationDirectory": true,
 | 
				
			||||||
    "deleteAppDataOnUninstall": false
 | 
					    "deleteAppDataOnUninstall": false
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  publish:{
 | 
				
			||||||
 | 
					    provider: 'generic', 
 | 
				
			||||||
 | 
					    channel: 'latest',
 | 
				
			||||||
 | 
					    url: 'https://github.com/electron-vite/electron-vite-react/releases/download/v0.9.9/',
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,6 +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 './update'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// The built directory structure
 | 
					// The built directory structure
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
| 
						 | 
					@ -72,6 +73,9 @@ 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)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app.whenReady().then(createWindow)
 | 
					app.whenReady().then(createWindow)
 | 
				
			||||||
| 
						 | 
					@ -114,3 +118,4 @@ ipcMain.handle('open-win', (_, arg) => {
 | 
				
			||||||
    childWindow.loadFile(indexHtml, { hash: arg })
 | 
					    childWindow.loadFile(indexHtml, { hash: arg })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -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",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										20
									
								
								src/App.tsx
								
								
								
								
							
							
						
						
									
										20
									
								
								src/App.tsx
								
								
								
								
							| 
						 | 
					@ -1,21 +1,21 @@
 | 
				
			||||||
import nodeLogo from "./assets/node.svg"
 | 
					import nodeLogo from './assets/node.svg'
 | 
				
			||||||
import { useState } from 'react'
 | 
					import { useState } from 'react'
 | 
				
			||||||
 | 
					import Update from '@/components/update'
 | 
				
			||||||
import './App.scss'
 | 
					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 (
 | 
				
			||||||
    <div className="App">
 | 
					    <div className='App'>
 | 
				
			||||||
      <div>
 | 
					      <div>
 | 
				
			||||||
        <a href="https://github.com/electron-vite/electron-vite-react" target="_blank">
 | 
					        <a href='https://github.com/electron-vite/electron-vite-react' target='_blank'>
 | 
				
			||||||
          <img src="./electron-vite.svg" className="logo" alt="Electron + Vite logo" />
 | 
					          <img src='./electron-vite.svg' className='logo' alt='Electron + Vite logo' />
 | 
				
			||||||
        </a>
 | 
					        </a>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <h1>Electron + Vite + React</h1>
 | 
					      <h1>Electron + Vite + React</h1>
 | 
				
			||||||
      <div className="card">
 | 
					      <div className='card'>
 | 
				
			||||||
        <button onClick={() => setCount((count) => count + 1)}>
 | 
					        <button onClick={() => setCount((count) => count + 1)}>
 | 
				
			||||||
          count is {count}
 | 
					          count is {count}
 | 
				
			||||||
        </button>
 | 
					        </button>
 | 
				
			||||||
| 
						 | 
					@ -23,12 +23,14 @@ function App() {
 | 
				
			||||||
          Edit <code>src/App.tsx</code> and save to test HMR
 | 
					          Edit <code>src/App.tsx</code> and save to test HMR
 | 
				
			||||||
        </p>
 | 
					        </p>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <p className="read-the-docs">
 | 
					      <p className='read-the-docs'>
 | 
				
			||||||
        Click on the Electron + Vite logo to learn more
 | 
					        Click on the Electron + Vite logo to learn more
 | 
				
			||||||
      </p>
 | 
					      </p>
 | 
				
			||||||
      <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 />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,67 @@
 | 
				
			||||||
 | 
					import React, { ReactNode } from 'react'
 | 
				
			||||||
 | 
					import { createPortal } from 'react-dom'
 | 
				
			||||||
 | 
					import styles from './modal.module.scss'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ModalTemplate: React.FC<React.PropsWithChildren<{
 | 
				
			||||||
 | 
					  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'>
 | 
				
			||||||
 | 
					              <button onClick={onCancel}>{cancelText}</button>
 | 
				
			||||||
 | 
					              <button onClick={onOk}>{okText}</button>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          ) : footer}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Modal = (props: Parameters<typeof ModalTemplate>[0] & { open: boolean }) => {
 | 
				
			||||||
 | 
					  const { open, ...omit } = props
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return createPortal(
 | 
				
			||||||
 | 
					    open ? ModalTemplate(omit) : null,
 | 
				
			||||||
 | 
					    document.body,
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Modal
 | 
				
			||||||
| 
						 | 
					@ -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);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,22 @@
 | 
				
			||||||
 | 
					import React from 'react'
 | 
				
			||||||
 | 
					import styles from './progress.module.scss'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Progress: React.FC<React.PropsWithChildren<{
 | 
				
			||||||
 | 
					  percent?: number
 | 
				
			||||||
 | 
					}>> = props => {
 | 
				
			||||||
 | 
					  const { percent = 0 } = props
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={styles.progress}>
 | 
				
			||||||
 | 
					      <div className='progress-pr'>
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className='progress-rate'
 | 
				
			||||||
 | 
					          style={{ width: `${percent}%` }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <span className='progress-num'>{(percent ?? 0).toString().substring(0,4)}%</span>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Progress
 | 
				
			||||||
| 
						 | 
					@ -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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,10 @@
 | 
				
			||||||
 | 
					interface VersionInfo {
 | 
				
			||||||
 | 
					  update: boolean
 | 
				
			||||||
 | 
					  version: string
 | 
				
			||||||
 | 
					  newVersion?: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ErrorType {
 | 
				
			||||||
 | 
					  message: string
 | 
				
			||||||
 | 
					  error: Error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -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<VersionInfo>()
 | 
				
			||||||
 | 
					  const [updateError, setUpdateError] = useState<ErrorType>()
 | 
				
			||||||
 | 
					  const [progressInfo, setProgressInfo] = useState<Partial<ProgressInfo>>()
 | 
				
			||||||
 | 
					  const [modalOpen, setModalOpen] = useState<boolean>(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 (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <Modal
 | 
				
			||||||
 | 
					        open={modalOpen}
 | 
				
			||||||
 | 
					        cancelText={modalBtn?.cancelText}
 | 
				
			||||||
 | 
					        okText={modalBtn?.okText}
 | 
				
			||||||
 | 
					        onCancel={modalBtn?.onCancel}
 | 
				
			||||||
 | 
					        onOk={modalBtn?.onOk}
 | 
				
			||||||
 | 
					        footer={updateAvailable ? /* hide footer */null : undefined}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <div className={styles.modalslot}>
 | 
				
			||||||
 | 
					          {updateError
 | 
				
			||||||
 | 
					            ? (
 | 
				
			||||||
 | 
					              <div className='update-error'>
 | 
				
			||||||
 | 
					                <p>Error downloading the latest version.</p>
 | 
				
			||||||
 | 
					                <p>{updateError.message}</p>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            ) : updateAvailable
 | 
				
			||||||
 | 
					              ? (
 | 
				
			||||||
 | 
					                <div className='can-available'>
 | 
				
			||||||
 | 
					                  <div>The last version is: v{versionInfo?.newVersion}</div>
 | 
				
			||||||
 | 
					                  <div className='new-version-target'>v{versionInfo?.version} -> 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>
 | 
				
			||||||
 | 
					              )
 | 
				
			||||||
 | 
					              : (
 | 
				
			||||||
 | 
					                <div className='can-not-available'>{JSON.stringify(versionInfo ?? {}, null, 2)}</div>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </Modal>
 | 
				
			||||||
 | 
					      <button disabled={checking} onClick={checkUpdate}>
 | 
				
			||||||
 | 
					        {checking ? 'Checking...' : 'Check update'}
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Update
 | 
				
			||||||
| 
						 | 
					@ -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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue