├── .editorconfig ├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.yaml ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── README.md ├── build ├── entitlements.mac.plist ├── icon.icns ├── icon.ico └── icon.png ├── electron-builder.yml ├── electron.vite.config.ts ├── package-lock.json ├── package.json ├── resources └── icon.png ├── src ├── main │ ├── common │ │ └── TypedUtils.ts │ ├── index.ts │ └── ipc │ │ ├── hook.ts │ │ └── index.ts ├── preload │ ├── index.d.ts │ └── index.ts └── renderer │ ├── index.html │ └── src │ ├── App.tsx │ ├── assets │ ├── base.css │ ├── electron.svg │ ├── main.css │ └── wavy-lines.svg │ ├── components │ └── Versions.tsx │ ├── env.d.ts │ ├── ipc.ts │ └── main.tsx ├── tsconfig.json ├── tsconfig.node.json └── tsconfig.web.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | .gitignore 5 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint:recommended', 4 | 'plugin:react/recommended', 5 | 'plugin:react/jsx-runtime', 6 | '@electron-toolkit/eslint-config-ts/recommended', 7 | '@electron-toolkit/eslint-config-prettier' 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | .DS_Store 5 | *.log* 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | pnpm-lock.yaml 4 | LICENSE.md 5 | tsconfig.json 6 | tsconfig.*.json 7 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | semi: false 3 | printWidth: 100 4 | trailingComma: none 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Main Process", 6 | "type": "node", 7 | "request": "launch", 8 | "cwd": "${workspaceRoot}", 9 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite", 10 | "windows": { 11 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd" 12 | }, 13 | "runtimeArgs": ["--sourcemap"], 14 | "env": { 15 | "REMOTE_DEBUGGING_PORT": "9222" 16 | } 17 | }, 18 | { 19 | "name": "Debug Renderer Process", 20 | "port": 9222, 21 | "request": "attach", 22 | "type": "chrome", 23 | "webRoot": "${workspaceFolder}/src/renderer", 24 | "timeout": 60000, 25 | "presentation": { 26 | "hidden": true 27 | } 28 | } 29 | ], 30 | "compounds": [ 31 | { 32 | "name": "Debug All", 33 | "configurations": ["Debug Main Process", "Debug Renderer Process"], 34 | "presentation": { 35 | "order": 1 36 | } 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | }, 5 | "[javascript]": { 6 | "editor.defaultFormatter": "esbenp.prettier-vscode" 7 | }, 8 | "[json]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 基于 Proxy 实现的 ipc 演示 2 | 3 | ![QQ20241028 143637](https://s1.imagehub.cc/images/2024/10/28/2142b71fa6a9ba84589d7da4e63da7fd.png) 4 | 5 | ## 实现方式 6 | 7 | 1. 首先需要在主进程注册 ipc 事件 8 | 9 | ```typescript 10 | ipcMain.handle('ipc', async (event, { action, args }) => { 11 | // 通过 cls-hooked 无感在调用链中传递 event 12 | return ipcHook.runAndReturn(() => { 13 | ipcHook.set('event', event) 14 | return ipc[action](...args) 15 | }) 16 | }) 17 | ``` 18 | 19 | 以上使用 cls-hooked 在调用链中传递 event,避免需要显式传递 event。 20 | 21 | 2. 在入口文件中初始化 ipc 22 | 23 | ```typescript 24 | ipc.init() 25 | ``` 26 | 27 | 3. 在渲染进程中使用 proxy 调用 ipc 28 | 29 | ```typescript 30 | // 直接通过 import 获得 ipc 类型 31 | export const ipc: import('../../main/ipc').Ipc = new Proxy( 32 | {}, 33 | { 34 | get(_, action: string) { 35 | return async (...args: any[]) => { 36 | return await window.electron.ipcRenderer.invoke('ipc', { 37 | action, 38 | args 39 | }) 40 | } 41 | } 42 | } 43 | ) as any 44 | ``` 45 | 46 | 以上。 47 | 48 | ## 黑魔法总结 49 | 50 | - cls-hooked 避免显式传递 event 51 | - ClassAsyncify 抹平 Promise 差异 52 | -------------------------------------------------------------------------------- /build/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.cs.allow-dyld-environment-variables 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lblblong/electron-ipc-demo/9074711efe505beb5d60d4023a91ebcfb19c4b62/build/icon.icns -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lblblong/electron-ipc-demo/9074711efe505beb5d60d4023a91ebcfb19c4b62/build/icon.ico -------------------------------------------------------------------------------- /build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lblblong/electron-ipc-demo/9074711efe505beb5d60d4023a91ebcfb19c4b62/build/icon.png -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | appId: com.electron.app 2 | productName: electron-ipc-demo 3 | directories: 4 | buildResources: build 5 | files: 6 | - '!**/.vscode/*' 7 | - '!src/*' 8 | - '!electron.vite.config.{js,ts,mjs,cjs}' 9 | - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' 10 | - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' 11 | - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' 12 | asarUnpack: 13 | - resources/** 14 | win: 15 | executableName: electron-ipc-demo 16 | nsis: 17 | artifactName: ${name}-${version}-setup.${ext} 18 | shortcutName: ${productName} 19 | uninstallDisplayName: ${productName} 20 | createDesktopShortcut: always 21 | mac: 22 | entitlementsInherit: build/entitlements.mac.plist 23 | extendInfo: 24 | - NSCameraUsageDescription: Application requests access to the device's camera. 25 | - NSMicrophoneUsageDescription: Application requests access to the device's microphone. 26 | - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. 27 | - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. 28 | notarize: false 29 | dmg: 30 | artifactName: ${name}-${version}.${ext} 31 | linux: 32 | target: 33 | - AppImage 34 | - snap 35 | - deb 36 | maintainer: electronjs.org 37 | category: Utility 38 | appImage: 39 | artifactName: ${name}-${version}.${ext} 40 | npmRebuild: false 41 | publish: 42 | provider: generic 43 | url: https://example.com/auto-updates 44 | -------------------------------------------------------------------------------- /electron.vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite' 3 | import react from '@vitejs/plugin-react' 4 | 5 | export default defineConfig({ 6 | main: { 7 | plugins: [externalizeDepsPlugin()] 8 | }, 9 | preload: { 10 | plugins: [externalizeDepsPlugin()] 11 | }, 12 | renderer: { 13 | resolve: { 14 | alias: { 15 | '@renderer': resolve('src/renderer/src') 16 | } 17 | }, 18 | plugins: [react()] 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-ipc-demo", 3 | "version": "1.0.0", 4 | "description": "An Electron application with React and TypeScript", 5 | "main": "./out/main/index.js", 6 | "author": "example.com", 7 | "homepage": "https://electron-vite.org", 8 | "scripts": { 9 | "format": "prettier --write .", 10 | "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", 11 | "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", 12 | "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false", 13 | "typecheck": "npm run typecheck:node && npm run typecheck:web", 14 | "start": "electron-vite preview", 15 | "dev": "electron-vite dev", 16 | "build": "npm run typecheck && electron-vite build", 17 | "postinstall": "electron-builder install-app-deps", 18 | "build:unpack": "npm run build && electron-builder --dir", 19 | "build:win": "npm run build && electron-builder --win", 20 | "build:mac": "electron-vite build && electron-builder --mac", 21 | "build:linux": "electron-vite build && electron-builder --linux" 22 | }, 23 | "dependencies": { 24 | "@electron-toolkit/preload": "^3.0.1", 25 | "@electron-toolkit/utils": "^3.0.0", 26 | "cls-hooked": "^4.2.2" 27 | }, 28 | "devDependencies": { 29 | "@electron-toolkit/eslint-config-prettier": "^2.0.0", 30 | "@electron-toolkit/eslint-config-ts": "^2.0.0", 31 | "@electron-toolkit/tsconfig": "^1.0.1", 32 | "@types/node": "^20.14.8", 33 | "@types/react": "^18.3.3", 34 | "@types/react-dom": "^18.3.0", 35 | "@vitejs/plugin-react": "^4.3.1", 36 | "electron": "^31.0.2", 37 | "electron-builder": "^24.13.3", 38 | "electron-vite": "^2.3.0", 39 | "eslint": "^8.57.0", 40 | "eslint-plugin-react": "^7.34.3", 41 | "prettier": "^3.3.2", 42 | "react": "^18.3.1", 43 | "react-dom": "^18.3.1", 44 | "typescript": "^5.5.2", 45 | "vite": "^5.3.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lblblong/electron-ipc-demo/9074711efe505beb5d60d4023a91ebcfb19c4b62/resources/icon.png -------------------------------------------------------------------------------- /src/main/common/TypedUtils.ts: -------------------------------------------------------------------------------- 1 | export type ClassAsyncify = { 2 | [K in keyof T]: T[K] extends (...args: any[]) => Promise 3 | ? T[K] 4 | : T[K] extends (...args: any[]) => infer R 5 | ? (...args: Parameters) => Promise 6 | : T[K] 7 | } 8 | -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | import { app, shell, BrowserWindow, ipcMain } from 'electron' 2 | import { join } from 'path' 3 | import { electronApp, optimizer, is } from '@electron-toolkit/utils' 4 | import icon from '../../resources/icon.png?asset' 5 | import { ipc } from './ipc' 6 | 7 | function createWindow(): void { 8 | // Create the browser window. 9 | const mainWindow = new BrowserWindow({ 10 | width: 900, 11 | height: 670, 12 | show: false, 13 | autoHideMenuBar: true, 14 | ...(process.platform === 'linux' ? { icon } : {}), 15 | webPreferences: { 16 | preload: join(__dirname, '../preload/index.js'), 17 | sandbox: false 18 | } 19 | }) 20 | 21 | mainWindow.on('ready-to-show', () => { 22 | mainWindow.show() 23 | }) 24 | 25 | mainWindow.webContents.setWindowOpenHandler((details) => { 26 | shell.openExternal(details.url) 27 | return { action: 'deny' } 28 | }) 29 | 30 | // HMR for renderer base on electron-vite cli. 31 | // Load the remote URL for development or the local html file for production. 32 | if (is.dev && process.env['ELECTRON_RENDERER_URL']) { 33 | mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) 34 | } else { 35 | mainWindow.loadFile(join(__dirname, '../renderer/index.html')) 36 | } 37 | } 38 | 39 | // This method will be called when Electron has finished 40 | // initialization and is ready to create browser windows. 41 | // Some APIs can only be used after this event occurs. 42 | app.whenReady().then(() => { 43 | ipc.init() 44 | 45 | // Set app user model id for windows 46 | electronApp.setAppUserModelId('com.electron') 47 | 48 | // Default open or close DevTools by F12 in development 49 | // and ignore CommandOrControl + R in production. 50 | // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils 51 | app.on('browser-window-created', (_, window) => { 52 | optimizer.watchWindowShortcuts(window) 53 | }) 54 | 55 | // IPC test 56 | ipcMain.on('ping', () => console.log('pong')) 57 | 58 | createWindow() 59 | 60 | app.on('activate', function () { 61 | // On macOS it's common to re-create a window in the app when the 62 | // dock icon is clicked and there are no other windows open. 63 | if (BrowserWindow.getAllWindows().length === 0) createWindow() 64 | }) 65 | }) 66 | 67 | // Quit when all windows are closed, except on macOS. There, it's common 68 | // for applications and their menu bar to stay active until the user quits 69 | // explicitly with Cmd + Q. 70 | app.on('window-all-closed', () => { 71 | if (process.platform !== 'darwin') { 72 | app.quit() 73 | } 74 | }) 75 | 76 | // In this file you can include the rest of your app"s specific main process 77 | // code. You can also put them in separate files and require them here. 78 | -------------------------------------------------------------------------------- /src/main/ipc/hook.ts: -------------------------------------------------------------------------------- 1 | import { createNamespace } from 'cls-hooked' 2 | 3 | export const ipcHook = createNamespace('ipc-hook') 4 | 5 | export function useEvent(): Electron.IpcMainInvokeEvent { 6 | return ipcHook.get('event') 7 | } 8 | -------------------------------------------------------------------------------- /src/main/ipc/index.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, ipcMain } from 'electron' 2 | import { ClassAsyncify } from '../common/TypedUtils' 3 | import { ipcHook, useEvent } from './hook' 4 | 5 | function sleep(ms: number) { 6 | return new Promise((resolve) => setTimeout(resolve, ms)) 7 | } 8 | 9 | export const ipc = new (class { 10 | hideWindow() { 11 | const event = useEvent() 12 | const win = BrowserWindow.fromWebContents(event.sender) 13 | win?.close() 14 | } 15 | 16 | getWindowSize() { 17 | const event = useEvent() 18 | const win = BrowserWindow.fromWebContents(event.sender) 19 | return win?.getSize() 20 | } 21 | 22 | paramsDemo(message: string, name: string) { 23 | console.log(message, name) 24 | } 25 | 26 | async asyncDemo() { 27 | await sleep(1000) 28 | return 'Ok!' 29 | } 30 | 31 | init() { 32 | ipcMain.handle('ipc', async (event, { action, args }) => { 33 | // 通过 cls-hooked 无感在调用链中传递 event 34 | return ipcHook.runAndReturn(() => { 35 | ipcHook.set('event', event) 36 | return ipc[action](...args) 37 | }) 38 | }) 39 | } 40 | })() 41 | 42 | export type Ipc = ClassAsyncify 43 | -------------------------------------------------------------------------------- /src/preload/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ElectronAPI } from '@electron-toolkit/preload' 2 | 3 | declare global { 4 | interface Window { 5 | electron: ElectronAPI 6 | api: unknown 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/preload/index.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge } from 'electron' 2 | import { electronAPI } from '@electron-toolkit/preload' 3 | 4 | // Custom APIs for renderer 5 | const api = {} 6 | 7 | // Use `contextBridge` APIs to expose Electron APIs to 8 | // renderer only if context isolation is enabled, otherwise 9 | // just add to the DOM global. 10 | if (process.contextIsolated) { 11 | try { 12 | contextBridge.exposeInMainWorld('electron', electronAPI) 13 | contextBridge.exposeInMainWorld('api', api) 14 | } catch (error) { 15 | console.error(error) 16 | } 17 | } else { 18 | // @ts-ignore (define in dts) 19 | window.electron = electronAPI 20 | // @ts-ignore (define in dts) 21 | window.api = api 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Electron 6 | 7 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/renderer/src/App.tsx: -------------------------------------------------------------------------------- 1 | import electronLogo from './assets/electron.svg' 2 | import Versions from './components/Versions' 3 | import { ipc } from './ipc' 4 | 5 | function App(): JSX.Element { 6 | const actions = [ 7 | { 8 | label: '最小化', 9 | handler: () => { 10 | ipc.hideWindow() 11 | } 12 | }, 13 | { 14 | label: '获取窗口尺寸', 15 | handler: async () => { 16 | const sizes = await ipc.getWindowSize() 17 | alert(`窗口尺寸:${sizes?.join(' x ')}`) 18 | } 19 | }, 20 | { 21 | label: '调用异步的 ipc 方法', 22 | handler: async () => { 23 | const res = await ipc.asyncDemo() 24 | alert(res) 25 | } 26 | }, 27 | { 28 | label: '传参到主进程(查看主进程console)', 29 | handler: async () => { 30 | await ipc.paramsDemo('来自渲染进程的问候', '---- 爱你的老鼠哥') 31 | } 32 | } 33 | ] 34 | 35 | return ( 36 | <> 37 | logo 38 |
基于 Proxy 实现的 ipc 演示
39 |
40 | 绝佳的 类型体验 41 |  和极低 心智负担 42 |
43 |

44 | 以下按钮事件从渲染进程调用主进程服务,请拉取并修改 click 中的代码以体验 ts 的魔力 45 |

46 |
47 | {actions.map((it) => { 48 | return ( 49 | 54 | ) 55 | })} 56 |
57 | 64 |

基于以下黑魔法

65 | 66 | 67 | ) 68 | } 69 | 70 | export default App 71 | -------------------------------------------------------------------------------- /src/renderer/src/assets/base.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --ev-c-white: #ffffff; 3 | --ev-c-white-soft: #f8f8f8; 4 | --ev-c-white-mute: #f2f2f2; 5 | 6 | --ev-c-black: #1b1b1f; 7 | --ev-c-black-soft: #222222; 8 | --ev-c-black-mute: #282828; 9 | 10 | --ev-c-gray-1: #515c67; 11 | --ev-c-gray-2: #414853; 12 | --ev-c-gray-3: #32363f; 13 | 14 | --ev-c-text-1: rgba(255, 255, 245, 0.86); 15 | --ev-c-text-2: rgba(235, 235, 245, 0.6); 16 | --ev-c-text-3: rgba(235, 235, 245, 0.38); 17 | 18 | --ev-button-alt-border: transparent; 19 | --ev-button-alt-text: var(--ev-c-text-1); 20 | --ev-button-alt-bg: var(--ev-c-gray-3); 21 | --ev-button-alt-hover-border: transparent; 22 | --ev-button-alt-hover-text: var(--ev-c-text-1); 23 | --ev-button-alt-hover-bg: var(--ev-c-gray-2); 24 | } 25 | 26 | :root { 27 | --color-background: var(--ev-c-black); 28 | --color-background-soft: var(--ev-c-black-soft); 29 | --color-background-mute: var(--ev-c-black-mute); 30 | 31 | --color-text: var(--ev-c-text-1); 32 | } 33 | 34 | *, 35 | *::before, 36 | *::after { 37 | box-sizing: border-box; 38 | margin: 0; 39 | font-weight: normal; 40 | } 41 | 42 | ul { 43 | list-style: none; 44 | } 45 | 46 | body { 47 | min-height: 100vh; 48 | color: var(--color-text); 49 | background: var(--color-background); 50 | line-height: 1.6; 51 | font-family: 52 | Inter, 53 | -apple-system, 54 | BlinkMacSystemFont, 55 | 'Segoe UI', 56 | Roboto, 57 | Oxygen, 58 | Ubuntu, 59 | Cantarell, 60 | 'Fira Sans', 61 | 'Droid Sans', 62 | 'Helvetica Neue', 63 | sans-serif; 64 | text-rendering: optimizeLegibility; 65 | -webkit-font-smoothing: antialiased; 66 | -moz-osx-font-smoothing: grayscale; 67 | } 68 | -------------------------------------------------------------------------------- /src/renderer/src/assets/electron.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/renderer/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import './base.css'; 2 | 3 | body { 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | overflow: hidden; 8 | background-image: url('./wavy-lines.svg'); 9 | background-size: cover; 10 | user-select: none; 11 | } 12 | 13 | code { 14 | font-weight: 600; 15 | padding: 3px 5px; 16 | border-radius: 2px; 17 | background-color: var(--color-background-mute); 18 | font-family: 19 | ui-monospace, 20 | SFMono-Regular, 21 | SF Mono, 22 | Menlo, 23 | Consolas, 24 | Liberation Mono, 25 | monospace; 26 | font-size: 85%; 27 | } 28 | 29 | #root { 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | flex-direction: column; 34 | margin-bottom: 80px; 35 | } 36 | 37 | .logo { 38 | margin-bottom: 20px; 39 | -webkit-user-drag: none; 40 | height: 128px; 41 | width: 128px; 42 | will-change: filter; 43 | transition: filter 300ms; 44 | } 45 | 46 | .logo:hover { 47 | filter: drop-shadow(0 0 1.2em #6988e6aa); 48 | } 49 | 50 | .creator { 51 | font-size: 14px; 52 | line-height: 16px; 53 | color: var(--ev-c-text-2); 54 | font-weight: 600; 55 | margin-bottom: 10px; 56 | } 57 | 58 | .text { 59 | font-size: 28px; 60 | color: var(--ev-c-text-1); 61 | font-weight: 700; 62 | line-height: 32px; 63 | text-align: center; 64 | margin: 0 10px; 65 | padding: 16px 0; 66 | } 67 | 68 | .tip { 69 | font-size: 16px; 70 | line-height: 24px; 71 | color: var(--ev-c-text-2); 72 | font-weight: 600; 73 | } 74 | 75 | .react { 76 | background: -webkit-linear-gradient(315deg, #087ea4 55%, #7c93ee); 77 | background-clip: text; 78 | -webkit-background-clip: text; 79 | -webkit-text-fill-color: transparent; 80 | font-weight: 700; 81 | } 82 | 83 | .ts { 84 | background: -webkit-linear-gradient(315deg, #3178c6 45%, #f0dc4e); 85 | background-clip: text; 86 | -webkit-background-clip: text; 87 | -webkit-text-fill-color: transparent; 88 | font-weight: 700; 89 | } 90 | 91 | .actions { 92 | display: flex; 93 | padding-top: 32px; 94 | margin: -6px; 95 | flex-wrap: wrap; 96 | justify-content: flex-start; 97 | } 98 | 99 | .action { 100 | flex-shrink: 0; 101 | padding: 6px; 102 | } 103 | 104 | .action a { 105 | cursor: pointer; 106 | text-decoration: none; 107 | display: inline-block; 108 | border: 1px solid transparent; 109 | text-align: center; 110 | font-weight: 600; 111 | white-space: nowrap; 112 | border-radius: 20px; 113 | padding: 0 20px; 114 | line-height: 38px; 115 | font-size: 14px; 116 | border-color: var(--ev-button-alt-border); 117 | color: var(--ev-button-alt-text); 118 | background-color: var(--ev-button-alt-bg); 119 | } 120 | 121 | .action a:hover { 122 | border-color: var(--ev-button-alt-hover-border); 123 | color: var(--ev-button-alt-hover-text); 124 | background-color: var(--ev-button-alt-hover-bg); 125 | } 126 | 127 | .versions { 128 | position: absolute; 129 | bottom: 30px; 130 | margin: 0 auto; 131 | padding: 15px 0; 132 | font-family: 'Menlo', 'Lucida Console', monospace; 133 | display: inline-flex; 134 | overflow: hidden; 135 | align-items: center; 136 | border-radius: 22px; 137 | background-color: #202127; 138 | backdrop-filter: blur(24px); 139 | } 140 | 141 | .versions li { 142 | display: block; 143 | float: left; 144 | border-right: 1px solid var(--ev-c-gray-1); 145 | padding: 0 20px; 146 | font-size: 14px; 147 | line-height: 14px; 148 | opacity: 0.8; 149 | &:last-child { 150 | border: none; 151 | } 152 | } 153 | 154 | @media (max-width: 720px) { 155 | .text { 156 | font-size: 20px; 157 | } 158 | } 159 | 160 | @media (max-width: 620px) { 161 | .versions { 162 | display: none; 163 | } 164 | } 165 | 166 | @media (max-width: 350px) { 167 | .tip, 168 | .actions { 169 | display: none; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/renderer/src/assets/wavy-lines.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/renderer/src/components/Versions.tsx: -------------------------------------------------------------------------------- 1 | 2 | function Versions(): JSX.Element { 3 | return ( 4 |
    5 |
  • cls-hooked
  • 6 |
  • Proxy
  • 7 |
  • ClassAsyncify
  • 8 |
9 | ) 10 | } 11 | 12 | export default Versions 13 | -------------------------------------------------------------------------------- /src/renderer/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/renderer/src/ipc.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | export const ipc: import('../../main/ipc').Ipc = new Proxy( 3 | {}, 4 | { 5 | get(_, action: string) { 6 | return async (...args: any[]) => { 7 | return await window.electron.ipcRenderer.invoke('ipc', { 8 | action, 9 | args 10 | }) 11 | } 12 | } 13 | } 14 | ) as any 15 | -------------------------------------------------------------------------------- /src/renderer/src/main.tsx: -------------------------------------------------------------------------------- 1 | import './assets/main.css' 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom/client' 5 | import App from './App' 6 | 7 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 8 | 9 | 10 | 11 | ) 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", 3 | "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "types": ["electron-vite/node"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.web.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", 3 | "include": [ 4 | "src/renderer/src/env.d.ts", 5 | "src/renderer/src/**/*", 6 | "src/renderer/src/**/*.tsx", 7 | "src/preload/*.d.ts" 8 | ], 9 | "compilerOptions": { 10 | "composite": true, 11 | "jsx": "react-jsx", 12 | "baseUrl": ".", 13 | "paths": { 14 | "@renderer/*": [ 15 | "src/renderer/src/*" 16 | ] 17 | } 18 | } 19 | } 20 | --------------------------------------------------------------------------------