├── .env ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── ---bug-report.md │ └── ---feature-request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .huskyrc ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .stylelintrc ├── .vscode ├── launch.json └── tasks.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Docs ├── Schedule │ └── milestone.md └── design.md ├── README.md ├── app ├── browser-window │ ├── index.ts │ ├── window-center.ts │ └── windows │ │ ├── api-window.ts │ │ ├── download-manager-window.ts │ │ ├── editor-window.ts │ │ └── start-window.ts ├── chrome-extensions │ └── index.ts ├── collect │ ├── logger.ts │ └── sentry.ts ├── config │ └── index.ts ├── data │ └── store │ │ ├── index.ts │ │ └── schema.ts ├── dialog │ └── index.ts ├── event │ └── code-runner.ts ├── file-manager │ ├── download │ │ ├── helper.ts │ │ ├── icon_default.png │ │ └── index.ts │ ├── index.ts │ ├── interface.ts │ ├── ipc-main.ts │ └── util.ts ├── global.d.ts ├── index.ts ├── mac-app.ts ├── menu │ └── index.ts ├── preload │ ├── demo-communication.ts │ ├── demo-communication2.ts │ ├── demo-full-screen.ts │ ├── demo-window-type.ts │ ├── draggable.ts │ ├── index.ts │ └── jsapi.ts ├── protocol │ └── index.ts ├── tray │ └── index.ts └── updater │ └── index.ts ├── build ├── pack.ts ├── pack │ ├── buid.ts │ ├── change-file.ts │ └── tool.ts ├── playground │ ├── config │ │ ├── api-menus.js │ │ ├── env.js │ │ ├── paths.js │ │ ├── webpack.config.js │ │ └── webpackDevServer.config.js │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── manifest.json │ │ └── robots.txt │ └── scripts │ │ ├── build.js │ │ └── start.js ├── quick-start.ts ├── webpack.config.base.js ├── webpack.config.main.dev.js ├── webpack.config.main.prod.js └── webpack.config.preload.js ├── commitlint.config.js ├── electron-builder-template.yml ├── electron-builder.yml ├── example └── terminal │ ├── .gitignore │ ├── .npmrc │ ├── .vscode │ └── launch.json │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── index.css │ ├── index.html │ ├── index.js │ └── processMessage.js │ └── yarn.lock ├── license.md ├── package-lock.json ├── package.json ├── playground ├── App.less ├── App.tsx ├── apidocs │ ├── 工程化 │ │ ├── img │ │ │ ├── auto_update_workflow.png │ │ │ ├── debug_main_process_in_webstorm.png │ │ │ ├── inspect_main_process_in_chrome.png │ │ │ ├── quick_start_electron_app.png │ │ │ └── sentry.png │ │ ├── 安全性.md │ │ ├── 崩溃收集.md │ │ ├── 开发.md │ │ ├── 打包.md │ │ └── 自动更新.md │ ├── 应用 │ │ ├── Dialog与文件选择.md │ │ ├── img │ │ │ ├── create-context-menu.gif │ │ │ ├── create-glimmer-tray.gif │ │ │ ├── create-menu.gif │ │ │ ├── create-toggle-tray.gif │ │ │ ├── create-tray.gif │ │ │ ├── demo.gif │ │ │ ├── download_progress.png │ │ │ ├── flow_chart.png │ │ │ ├── hide-menu.gif │ │ │ ├── mac-menu.png │ │ │ ├── mac_download_progress.png │ │ │ ├── protocolWatch.gif │ │ │ ├── select_path.gif │ │ │ ├── set-tray-title.png │ │ │ ├── setProtocol.gif │ │ │ ├── updated_event.png │ │ │ ├── wakeUp.jpg │ │ │ ├── win-menu.png │ │ │ └── windows_progress.png │ │ ├── 下载管理器.md │ │ ├── 协议.md │ │ ├── 托盘.md │ │ └── 菜单.md │ └── 窗口管理 │ │ ├── img │ │ ├── close-window-model.png │ │ ├── close-window-model2.png │ │ ├── window-create.png │ │ ├── window-event.png │ │ ├── window-type-frame.gif │ │ ├── window-type-frame2.gif │ │ ├── window-type-frame3.gif │ │ ├── window-type-frameless.gif │ │ └── yargv-parse.png │ │ ├── 主窗口隐藏和恢复.md │ │ ├── 全屏、最大化、最小化、恢复.md │ │ ├── 创建和管理窗口.md │ │ ├── 窗口之间的通信.md │ │ ├── 窗口事件.md │ │ ├── 窗口的聚焦和失焦.md │ │ └── 窗口类型.md ├── components │ ├── code-block │ │ ├── index.tsx │ │ └── style.module.less │ ├── code-pre │ │ ├── index.tsx │ │ └── style.less │ ├── code-runner │ │ ├── editor-toolbar │ │ │ ├── index.tsx │ │ │ └── style.module.less │ │ ├── index.tsx │ │ └── style.module.less │ ├── markdown │ │ ├── code-renderer.tsx │ │ ├── header-renderer.tsx │ │ ├── image-renderer.tsx │ │ ├── index.tsx │ │ └── style.module.less │ ├── monaco-editor │ │ ├── index.tsx │ │ ├── style.module.less │ │ └── utils.ts │ └── open-document │ │ ├── index.tsx │ │ └── style.less ├── example │ └── template │ │ ├── index.html │ │ ├── main.ts │ │ ├── preload.ts │ │ └── renderer.ts ├── global.d.ts ├── index.css ├── index.tsx ├── page │ ├── apidoc │ │ ├── index.tsx │ │ └── style.module.less │ ├── browser-demo │ │ ├── communication-part1 │ │ │ ├── client.tsx │ │ │ └── main.tsx │ │ ├── communication-part2 │ │ │ ├── client.tsx │ │ │ └── main.tsx │ │ ├── communication-part3 │ │ │ ├── bottom-input.tsx │ │ │ ├── client.tsx │ │ │ ├── main.tsx │ │ │ ├── message-box.tsx │ │ │ ├── style.module.less │ │ │ └── use-article.ts │ │ ├── demo-window-type │ │ │ ├── index.tsx │ │ │ └── style.module.less │ │ ├── full-screen │ │ │ └── index.tsx │ │ ├── style.module.less │ │ └── window-close │ │ │ └── index.tsx │ ├── download-manager │ │ ├── components │ │ │ ├── download-item.tsx │ │ │ ├── icon-button.tsx │ │ │ ├── manager-menu.tsx │ │ │ └── style.module.less │ │ ├── create.tsx │ │ ├── index.tsx │ │ ├── ipc-renderer.ts │ │ └── style.module.less │ ├── editor │ │ ├── editor-operate.tsx │ │ ├── index.tsx │ │ ├── style.module.less │ │ └── utils.ts │ └── start │ │ ├── index.tsx │ │ └── style.module.less ├── react-app-env.d.ts ├── router.tsx ├── theme.less └── utils │ ├── id.ts │ └── path.ts ├── resources ├── entitlements.mac.plist ├── icon.icns ├── icon.ico ├── icon.png ├── markdown │ ├── window-dependency.png │ ├── window-event1.png │ ├── window-event2.png │ └── window-operate.png ├── readme │ ├── 01.gif │ ├── 02.gif │ ├── 03.gif │ ├── wechat-group.jpeg │ └── wechat-group.svg └── tray │ ├── StatusIcon_dark.png │ ├── StatusIcon_dark@1.25x.png │ ├── StatusIcon_dark@1.5x.png │ ├── StatusIcon_dark@2x.png │ ├── StatusIcon_light.png │ ├── StatusIcon_light@1.25x.png │ ├── StatusIcon_light@1.5x.png │ └── StatusIcon_light@2x.png ├── tsconfig.json └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | BABEL_POLYFILL = none 2 | PORT = 7000 3 | 4 | SENTRY_DSN = https://08991a340c9a4b8ca70db510b5ab5287@o283386.ingest.sentry.io/5402405 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # build 6 | build 7 | 8 | # Coverage directory used by tools like istanbul 9 | coverage 10 | 11 | 12 | # node-waf configuration 13 | .lock-wscript 14 | 15 | # Compiled binary addons (http://nodejs.org/api/addons.html) 16 | build/Release 17 | .eslintcache 18 | 19 | # Dependency directory 20 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 21 | node_modules 22 | 23 | # OSX 24 | .DS_Store 25 | 26 | # App packaged 27 | release 28 | dist 29 | latest-output 30 | dll 31 | main.js 32 | main.js.map 33 | 34 | .idea 35 | npm-debug.log.* 36 | __snapshots__ 37 | 38 | # Package.json 39 | package.json 40 | .travis.yml 41 | *.css.d.ts 42 | *.sass.d.ts 43 | *.scss.d.ts 44 | 45 | # .d.ts 46 | /**/*.d.ts 47 | **/*.code.ts 48 | 49 | # mock 50 | mock 51 | 52 | # dist 53 | all/* 54 | 55 | # playground 56 | config 57 | 58 | tsconfig.json 59 | scripts 60 | 61 | example -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@heibanfe/eslint-config-react'], 3 | env: { 4 | browser: true, 5 | commonjs: true, 6 | node: true, 7 | es6: true, 8 | }, 9 | parserOptions: { 10 | ecmaVersion: 2018, 11 | sourceType: 'module', 12 | ecmaFeatures: { 13 | jsx: true, 14 | }, 15 | }, 16 | settings: { 17 | react: { 18 | version: 'detect', 19 | }, 20 | }, 21 | rules: { 22 | 'no-console': 'off', 23 | 'max-params': 'off', 24 | semi: ['error', 'never'], 25 | 'prettier/prettier': 'off', 26 | '@typescript-eslint/no-empty-interface': 'off', 27 | '@typescript-eslint/no-unused-vars': 'off', 28 | 'no-console': 'off', 29 | strict: ['error', 'global'], 30 | curly: 'warn', 31 | semi: ['error', 'never'], 32 | 'prettier/prettier': 'off', 33 | '@typescript-eslint/ban-ts-comment': 'off', 34 | camelcase: 'off', 35 | '@typescript-eslint/ban-types': 'off', 36 | '@typescript-eslint/camelcase': 'off', 37 | '@typescript-eslint/no-empty-interface': 'off', 38 | '@typescript-eslint/no-unused-vars': 'off', 39 | 'no-use-before-define': 'off', 40 | '@typescript-eslint/no-use-before-define': 'off', 41 | }, 42 | } 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41E Bug report" 3 | about: 'Report a bug in the electron-playground. ' 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | # 🐞 bug report 11 | 12 | ## Description 13 | 14 | 15 | ## Exception or Error 16 | 17 | 18 | ## Environment 19 | 20 | Platform: 21 | OS Version: 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature request" 3 | about: Suggest a feature for electron-playground 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | # 🚀 Feature request 11 | 12 | ## Description 13 | 14 | ## Do you have any ideas? 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## PR Type 2 | 3 | What kind of change does this PR introduce? 4 | 5 | 6 | 7 | - [ ] Bugfix 8 | - [ ] Feature 9 | - [ ] Code style update (formatting, local variables) 10 | - [ ] Refactoring (no functional changes, no api changes) 11 | - [ ] Documentation 12 | - [ ] Other... Please describe: 13 | 14 | ## PR description 15 | 16 | 17 | Issue Number: N/A 18 | 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | design 4 | *.log 5 | packages/test 6 | dist 7 | all 8 | temp 9 | .vuerc 10 | .version 11 | .idea 12 | .vscode/settings.json 13 | 14 | # dependencies 15 | /npm-debug.log* 16 | /yarn-error.log 17 | 18 | # production 19 | /dist 20 | 21 | 22 | # misc 23 | .DS_Store 24 | 25 | # release 26 | release 27 | *.tgz 28 | x-app-store 29 | 30 | xp-template/build 31 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged", 4 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 5 | } 6 | } -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npm.taobao.org/ 2 | electron_mirror=https://npm.taobao.org/mirrors/electron/ 3 | chromedriver_cdnurl=https://npm.taobao.org/mirrors/chromedriver 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.md 2 | **/*.svg 3 | **/*.ejs 4 | **/*.html 5 | package.json 6 | .umi 7 | .umi-production 8 | .umi-test 9 | !/all -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "bracketSpacing": true, 6 | "jsxBracketSameLine": true, 7 | "semi": false, 8 | "useTabs": false, 9 | "arrowParens": "avoid", 10 | "jsxSingleQuote": true, 11 | "htmlWhitespaceSensitivity": "ignore", 12 | "quoteProps": "as-needed", 13 | "overrides": [ 14 | { 15 | "files": "*.json", 16 | "options": { "parser": "json" } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@heibanfe/stylelint-config"], 3 | "ignoreFiles": [ 4 | "all/**/*.less" 5 | ] 6 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "cwd": "${workspaceFolder}", 12 | "skipFiles": [ 13 | "/**" 14 | ], 15 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", 16 | "windows": { 17 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" 18 | }, 19 | // 本地启动 yarn build-main-dev:watch,就可以不用每次重新走编译TS 20 | // "preLaunchTask": "npm: build-main-dev", 21 | "outputCapture": "std", 22 | "args": [ 23 | "." 24 | ] 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "build-main-dev", 9 | "group": "build", 10 | "problemMatcher": [] 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## 版本:0.0.2 3 | 4 | - 2020-10-23 07:55:47 5 | 6 | 版本更新 7 | 8 | ``` 9 | 10 | ``` 11 | ## 版本:0.0.2 12 | 13 | - 2020-12-04 01:35:30 14 | 15 | 版本更新 16 | 17 | ``` 18 | 19 | ``` 20 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at tal.xiaoheiban@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /Docs/Schedule/milestone.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tal-tech/electron-playground/7635a86bfe4de138288e3c73b1674f7ef4338c39/Docs/Schedule/milestone.md -------------------------------------------------------------------------------- /Docs/design.md: -------------------------------------------------------------------------------- 1 | # 项目设计理念 2 | > 项目最终需要成为一个字典形式的存在 3 | 4 | ### 提供尽可能多的示例 5 | 6 | * 尽量让功能能够看得到效果 7 | 8 | ### 分层级 9 | 10 | * 常用的放在第一层 11 | 12 | * 偏定制化,偏业务层面的放在第二层甚至更深的位置 13 | 14 | ### 一个详尽的文档 15 | 16 | 17 | ### 帮助用户少走弯路 18 | 19 | * 以自身踩坑经验,搭配丰富的文档以及示例,帮助用户少走弯路 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Electron Playground 2 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/tal-tech/electron-playground/blob/master/license.md) 3 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/tal-tech/electron-playground/pulls) 4 | 5 | 如果想快速的把一个web app应用打包成一个electron应用,那我们还准备了[create-electron-app](https://github.com/tal-tech/create-electron-app),这个项目可以让你五分钟就拥有自己的electron app 6 | 7 | ## 1. electron-playground介绍 8 | 9 | 一个快速尝试和学习[electron](https://github.com/electron/electron)的项目,对electron的api进行了归纳和总结,对常用的业务功能做了demo演示。 10 | 11 | ## 2. 特性 12 | 13 | 在文档和演练场中,几乎所有的代码都可以即时运行看到效果。亦可直接在界面上修改代码运行。 14 | 15 | ![](./resources/readme/01.gif) 16 | 17 | 编辑器全部集成[monaco-editor](https://github.com/microsoft/monaco-editor),编码体验接近vscode; 18 | 19 | ![](./resources/readme/02.gif) 20 | 21 | 想要实现更复杂的操作,我们参考[fiddle](https://github.com/electron/fiddle)创建了演练场,这里编写的应用都可以独立运行。 22 | 23 | ![](./resources/readme/03.gif) 24 | 25 | ## 3. 启动 26 | 27 | 克隆仓库到本地,安装依赖后执行 28 | ```shell 29 | yarn start 30 | ``` 31 | 编译完成后将自动启动应用 32 | 33 | ## 4. 已实现 34 | 35 | 工程化 36 | - 崩溃分析和收集 37 | - 开发调试 38 | - 打包问题 39 | - 应用更新 40 | - 应用 41 | - 自定义协议 42 | - 系统提示和文件选择 43 | - 菜单 44 | - 系统托盘 45 | - 文件下载 46 | - 窗口管理 47 | - 创建和管理窗口 48 | - 隐藏和恢复 49 | - 聚焦、失焦 50 | - 全屏、最大化、最小化 51 | - 窗口通信 52 | - 窗口类型 53 | - 窗口事件 54 | - 其他 55 | - 安全性 56 | 57 | ## 5. 规划中 58 | - 小程序 59 | - 小应用 60 | - 截屏/录屏 61 | - 微服务集成 62 | - 自启动管理 63 | - 性能优化 64 | - 打包体积优化 65 | - 更多... 66 | 67 | ## 6. 最后 68 | 69 | 如果觉得这个项目对你有用,欢迎star,另外更欢迎大家提issue哈。 70 | 71 | 当然如果有问题,可以加下面微信,有时间我们会第一时间回复。 72 | 73 | 74 | 75 | 因为国内网络原因,图片可能无法加载,可以微信搜索**”晓前端“**,关注公众号**“晓前端”**,输入“ele”或者“electron”,也能自动获取群图片。 76 | 77 | 有一个公众号叫“晓前端团队”,这个不是**我们的公众号**,我们公众号就是**晓前端** 78 | -------------------------------------------------------------------------------- /app/browser-window/index.ts: -------------------------------------------------------------------------------- 1 | import logger from 'app/collect/logger' 2 | import { messageBox } from 'app/dialog' 3 | import { app, BrowserWindow, ipcMain, IpcMainEvent } from 'electron' 4 | import { createDownloadManagerWindow } from 'app/browser-window/windows/download-manager-window' 5 | import { createApiWindow } from './windows/api-window' 6 | import { createEditorWindow } from './windows/editor-window' 7 | import { createStartWindow } from './windows/start-window' 8 | 9 | export interface OpenWindowOptions { 10 | name: WindowName 11 | } 12 | 13 | // types declaration 14 | export type WindowName = 'api' | 'editor' | 'start' | 'download-manager' 15 | export type CreateWindowOptions = {} 16 | export type CreateWindowHandler = (options?: CreateWindowOptions) => BrowserWindow 17 | 18 | // handlers for creating window 19 | const HandlersMap: { [key in WindowName]: CreateWindowHandler } = { 20 | api: createApiWindow, 21 | editor: createEditorWindow, 22 | start: createStartWindow, 23 | 'download-manager': createDownloadManagerWindow, 24 | } 25 | Object.freeze(HandlersMap) 26 | let CLOSE_WINDOW = false 27 | 28 | // browserWindow store 29 | const WindowMap = new Map() 30 | 31 | function hackFakeCloseMainWindow(win: BrowserWindow) { 32 | win.on('close', event => { 33 | if (CLOSE_WINDOW) return 34 | event.preventDefault() 35 | if (win.isFullScreen()) { 36 | win.setFullScreen(false) 37 | } else { 38 | win.hide() 39 | } 40 | }) 41 | } 42 | 43 | // create window by name 44 | export function createWindow(name: WindowName, options?: CreateWindowOptions): BrowserWindow { 45 | const handler = HandlersMap[name] 46 | if (typeof handler !== 'function') { 47 | throw new Error(`no handler for ${name}!`) 48 | } 49 | 50 | const win = handler(options) 51 | WindowMap.set(name, win) 52 | 53 | // listener for all window 54 | win.on('closed', () => WindowMap.delete(name)) 55 | 56 | // listener for all webcontents 57 | win.webContents.on('render-process-gone', async (event, details) => { 58 | console.log(event, details) 59 | messageBox.error({ 60 | message: `The renderer process gone. ${details.reason}`, 61 | buttons: ['quit', 'relaunch'], 62 | }) 63 | }) 64 | win.webContents.on('console-message', (e: Event, level: number, message: string) => { 65 | const arr = ['debug', 'log', 'warn', 'error'] as const 66 | logger[arr[level] || 'log'](message) 67 | }) 68 | 69 | hackFakeCloseMainWindow(win) 70 | return win 71 | } 72 | 73 | export function getMainWindow() { 74 | return WindowMap.get('start') || WindowMap.get('api') || WindowMap.get('editor') 75 | } 76 | 77 | export function restoreMainWindow() { 78 | const win = WindowMap.get('start') || WindowMap.get('api') || WindowMap.get('editor') 79 | win?.restore() 80 | win?.show() 81 | } 82 | 83 | export function closeMainWindow() { 84 | CLOSE_WINDOW = true 85 | for (const win of WindowMap) { 86 | win[1].close() 87 | } 88 | CLOSE_WINDOW = false 89 | } 90 | 91 | ;(async () => { 92 | await app.whenReady() 93 | ipcMain.on('OPEN_WINDOW', (event: IpcMainEvent, optionsProps: OpenWindowOptions) => { 94 | const { name } = optionsProps 95 | createWindow(name) 96 | 97 | event.returnValue = 1 98 | }) 99 | 100 | ipcMain.on('CLOSE_WINDOW', (e, options) => { 101 | const win = BrowserWindow.fromWebContents(e.sender) 102 | win?.close() 103 | }) 104 | 105 | ipcMain.on('MAXIMIZE_WINDOW', (e, options) => { 106 | const win = BrowserWindow.fromWebContents(e.sender) 107 | console.log(win?.isMaximized()) 108 | win?.isMaximized() ? win?.unmaximize() : win?.maximize() 109 | }) 110 | 111 | ipcMain.on('MINIMIZE_WINDOW', (e, options) => { 112 | const win = BrowserWindow.fromWebContents(e.sender) 113 | win?.isMinimized() ? win?.restore() : win?.minimize() 114 | }) 115 | })() 116 | -------------------------------------------------------------------------------- /app/browser-window/window-center.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron' 2 | 3 | export type WindowType = 'start' | 'api' | 'playground' 4 | 5 | export class WindowCenter{ 6 | 7 | private static map: Map = new Map() 8 | 9 | static create(name: WindowType, options: Electron.BrowserWindowConstructorOptions) { 10 | const win = new BrowserWindow(options) 11 | WindowCenter.map.set(name, win) 12 | 13 | return win 14 | } 15 | 16 | static getWindow(name: WindowType) { 17 | return WindowCenter.map.get(name) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/browser-window/windows/api-window.ts: -------------------------------------------------------------------------------- 1 | import { PLAYGROUND_FILE_URL, PRELOAD_FILE } from 'app/config' 2 | import { registerFileManagerService } from 'app/file-manager' 3 | import { BrowserWindow, shell } from 'electron' 4 | import { CreateWindowHandler } from '..' 5 | 6 | const OPTIONS: Electron.BrowserWindowConstructorOptions = { 7 | width: 1280, 8 | height: 900, 9 | minWidth: 960, 10 | minHeight: 640, 11 | titleBarStyle: 'hidden', 12 | autoHideMenuBar: true, 13 | webPreferences: { 14 | nodeIntegration: true, 15 | webSecurity: false, 16 | preload: PRELOAD_FILE, 17 | enableRemoteModule: true, 18 | } 19 | } 20 | 21 | const URL = `${PLAYGROUND_FILE_URL}#/apidoc` 22 | 23 | export const createApiWindow: CreateWindowHandler = () => { 24 | const win = new BrowserWindow(OPTIONS) 25 | win.loadURL(URL) 26 | 27 | win.webContents.on('will-navigate', (event, url) => { 28 | if (/^http(s)?:/.test(url)) { 29 | event.preventDefault() 30 | shell.openExternal(url) 31 | } 32 | }) 33 | 34 | registerFileManagerService(win) 35 | 36 | return win 37 | } 38 | -------------------------------------------------------------------------------- /app/browser-window/windows/download-manager-window.ts: -------------------------------------------------------------------------------- 1 | import { PLAYGROUND_FILE_URL, PRELOAD_FILE } from 'app/config' 2 | import { registerFileManagerService } from 'app/file-manager' 3 | import { BrowserWindow, shell } from 'electron' 4 | import { CreateWindowHandler } from '..' 5 | 6 | const OPTIONS: Electron.BrowserWindowConstructorOptions = { 7 | title: '下载管理器', 8 | width: 600, 9 | height: 400, 10 | titleBarStyle: 'hidden', 11 | maximizable: false, 12 | show: false, 13 | webPreferences: { 14 | nodeIntegration: true, 15 | webSecurity: false, 16 | preload: PRELOAD_FILE, 17 | enableRemoteModule: true, 18 | } 19 | } 20 | 21 | const URL = `${PLAYGROUND_FILE_URL}#/download-manager/demo` 22 | 23 | export const createDownloadManagerWindow: CreateWindowHandler = () => { 24 | const win = new BrowserWindow(OPTIONS) 25 | win.loadURL(URL) 26 | 27 | win.webContents.on('will-navigate', (event, url) => { 28 | if (/^http(s)?:/.test(url)) { 29 | event.preventDefault() 30 | shell.openExternal(url) 31 | } 32 | }) 33 | 34 | return win 35 | } 36 | -------------------------------------------------------------------------------- /app/browser-window/windows/editor-window.ts: -------------------------------------------------------------------------------- 1 | import { PLAYGROUND_FILE_URL, PRELOAD_FILE } from 'app/config' 2 | import { BrowserWindow } from 'electron' 3 | import { CreateWindowHandler } from '..' 4 | 5 | const OPTIONS: Electron.BrowserWindowConstructorOptions = { 6 | width: 1280, 7 | height: 900, 8 | minWidth: 960, 9 | minHeight: 640, 10 | titleBarStyle: 'hidden', 11 | autoHideMenuBar: true, 12 | webPreferences: { 13 | nodeIntegration: true, 14 | webSecurity: false, 15 | preload: PRELOAD_FILE, 16 | enableRemoteModule: true, 17 | } 18 | } 19 | 20 | const URL = `${PLAYGROUND_FILE_URL}#/editor` 21 | 22 | export const createEditorWindow: CreateWindowHandler = () => { 23 | const win = new BrowserWindow(OPTIONS) 24 | win.loadURL(URL) 25 | 26 | return win 27 | } 28 | -------------------------------------------------------------------------------- /app/browser-window/windows/start-window.ts: -------------------------------------------------------------------------------- 1 | import { PLAYGROUND_FILE_URL, PRELOAD_FILE } from 'app/config' 2 | import { BrowserWindow } from 'electron' 3 | import { CreateWindowHandler } from '..' 4 | 5 | const OPTIONS: Electron.BrowserWindowConstructorOptions = { 6 | width: 600, 7 | height: 640, 8 | resizable: false, 9 | titleBarStyle: 'hidden', 10 | autoHideMenuBar: true, 11 | frame: false, 12 | webPreferences: { 13 | nodeIntegration: true, 14 | webSecurity: false, 15 | preload: PRELOAD_FILE, 16 | enableRemoteModule: true, 17 | }, 18 | } 19 | 20 | const URL = `${PLAYGROUND_FILE_URL}#start` 21 | 22 | export const createStartWindow: CreateWindowHandler = () => { 23 | const win = new BrowserWindow(OPTIONS) 24 | // 隐藏Mac下的交通灯🚥和windows/linux下的菜单操作按钮 25 | if (process.platform === 'darwin') win.setWindowButtonVisibility(false) 26 | else win.setMenuBarVisibility(false) 27 | win.loadURL(URL) 28 | 29 | return win 30 | } 31 | -------------------------------------------------------------------------------- /app/chrome-extensions/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { readdirSync, existsSync } from 'fs' 3 | import { app, session } from 'electron' 4 | 5 | const EXTENSION_FOLDER = (function extensionFolder() { 6 | const { platform } = process 7 | 8 | if (platform !== 'darwin' && platform !== 'win32' && platform !== 'linux') { 9 | throw new Error(`not support platform: ${platform}`) 10 | } 11 | 12 | let folderPath!: string 13 | if (platform === 'darwin') { 14 | folderPath = path.resolve( 15 | app.getPath('home'), 16 | 'Library/Application Support/Google/Chrome/Default/Extensions', 17 | ) 18 | } 19 | 20 | if (platform === 'win32') { 21 | folderPath = path.resolve( 22 | app.getPath('home'), 23 | 'AppData\\Local\\Google\\Chrome\\User Data\\Default\\Extensions', 24 | ) 25 | } 26 | 27 | if (platform === 'linux') { 28 | const availablePath = [ 29 | '.config/google-chrome/Default/Extensions/', 30 | '.config/google-chrome-beta/Default/Extensions/', 31 | '.config/google-chrome-canary/Default/Extensions/', 32 | '.config/chromium/Default/Extensions/', 33 | ].map(p => path.resolve(app.getPath('home'), p)) 34 | 35 | const exactPath = availablePath.find(p => existsSync(p)) 36 | if (!exactPath) { 37 | throw new Error('no extension folder') 38 | } 39 | 40 | folderPath = exactPath 41 | } 42 | 43 | if (existsSync(folderPath)) { 44 | return folderPath 45 | } else { 46 | console.error('no extension folder') 47 | return '' 48 | } 49 | })() 50 | 51 | function addDevToolsExtension(id: string) { 52 | if(!EXTENSION_FOLDER) return 53 | const extensionPath = path.resolve(EXTENSION_FOLDER, id) 54 | 55 | if (!existsSync(extensionPath)) { 56 | return 57 | } 58 | 59 | const versionName = readdirSync(extensionPath).find( 60 | v => existsSync(path.resolve(extensionPath, v)) && /\d+\.\d+\.\d/.test(v), 61 | ) 62 | 63 | if (versionName) { 64 | session.defaultSession.loadExtension(path.resolve(extensionPath, versionName)) 65 | } 66 | } 67 | 68 | const EXTENSION_IDS: string[] = [ 69 | 'fmkadmapgofadopljbjfkapdkoienihi', // React Developer Tools 70 | // 'aapbdbdomjkkjkaonfhkkikfgjllcleb', // Google Translate 71 | ] 72 | 73 | export function addDevToolsExtensionAtDevelopmentMode() { 74 | if (process.env.NODE_ENV !== 'development') { 75 | return 76 | } 77 | EXTENSION_IDS.forEach(id => addDevToolsExtension(id)) 78 | } 79 | -------------------------------------------------------------------------------- /app/collect/logger.ts: -------------------------------------------------------------------------------- 1 | // 保存console的log日志 2 | import log4js from 'log4js' 3 | 4 | log4js.configure({ 5 | appenders: { cheese: { type: 'file', filename: 'cheese.log' } }, 6 | categories: { default: { appenders: ['cheese'], level: 'error' } } 7 | }) 8 | 9 | const logger = log4js.getLogger('cheese') 10 | 11 | export default logger -------------------------------------------------------------------------------- /app/collect/sentry.ts: -------------------------------------------------------------------------------- 1 | // https://docs.sentry.io/platforms/javascript/electron/#configuring-the-client 2 | import { Scope, configureScope } from '@sentry/electron' 3 | import { dialog, app, ipcMain,BrowserWindow, MessageBoxOptions } from 'electron' 4 | import { getMachineId } from '../config' 5 | 6 | const { init } = 7 | process.type === 'browser' 8 | ? require('@sentry/electron/dist/main') 9 | : require('@sentry/electron/dist/renderer') 10 | 11 | 12 | // https://docs.sentry.io/enriching-error-data/additional-data/ 13 | configureScope((scope: Scope) => { 14 | // Users are applied to construct a unique identity in Sentry. 15 | scope.setUser({id: getMachineId()}) 16 | }) 17 | // Initialize the sentry 18 | init({ 19 | dsn: process.env.SENTRY_DSN, 20 | debug: process.env.ENV_TYPE !== 'prod', 21 | }) 22 | // Monitor error messages sent by the renderer 23 | ipcMain.on('renderer.error', (event, option) => { 24 | console.error(event, option) 25 | throw new Error('Error triggered in main processes') 26 | }) 27 | 28 | 29 | /** 30 | * Crash listen on the created window 31 | * @param win BrowserWindow 32 | */ 33 | export function addCrashListener(win: BrowserWindow) { 34 | // https://www.electronjs.org/docs/api/web-contents#event-crashed-deprecated 35 | win.webContents.on('crashed', async (event,killed) => { 36 | console.log(event,killed) 37 | const options: MessageBoxOptions = { 38 | type: 'info', 39 | title: 'The renderer process crashes', 40 | message: 'The renderer process crashes.', 41 | buttons: ['quit app', 'reload'], 42 | } 43 | const { response } = await dialog.showMessageBox(win, options) 44 | // 1 reload 0 quit app 45 | response ? win.reload() : app.quit() 46 | }) 47 | } -------------------------------------------------------------------------------- /app/config/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import url from 'url' 3 | 4 | import { machineIdSync } from 'node-machine-id' 5 | 6 | let machineId = '' 7 | export const getMachineId = () => { 8 | if (machineId) { 9 | return machineId 10 | } 11 | try { 12 | machineId = machineIdSync() 13 | } catch (err) { 14 | console.error('Machine ID retrieval failed:', err) 15 | } 16 | return machineId 17 | } 18 | 19 | 20 | // 注入渲染进程窗口的地址 21 | export const PLAYGROUND_FILE_URL = url.format({ 22 | protocol: 'file:', 23 | pathname: path.resolve(__dirname, '..', '..', 'dist', 'playground', 'index.html'), 24 | slashes: true, 25 | }) 26 | 27 | export const PRELOAD_FILE = path.resolve(__dirname, 'preload.js') 28 | -------------------------------------------------------------------------------- /app/data/store/index.ts: -------------------------------------------------------------------------------- 1 | // store存储 2 | import Store from 'electron-store' 3 | import schema from './schema' 4 | 5 | const store = new Store({ 6 | schema, 7 | // 每当升级版本时,都可以使用migrations对store执行回调操作 8 | migrations: { 9 | '0.0.2': store => { 10 | store.set('foo', 'package change string change too') 11 | } 12 | }, 13 | }) 14 | 15 | export default store 16 | -------------------------------------------------------------------------------- /app/data/store/schema.ts: -------------------------------------------------------------------------------- 1 | // store的字段数据以及store的默认值 2 | 3 | const schema = { 4 | foo: { 5 | type: 'string', 6 | default: 'This is a test default string' 7 | } 8 | } as const 9 | 10 | 11 | export default schema -------------------------------------------------------------------------------- /app/dialog/index.ts: -------------------------------------------------------------------------------- 1 | import { dialog, BrowserWindow } from 'electron' 2 | 3 | function createMessageBoxShow(type: NonNullable) { 4 | // 这里将window参数反置,因为一般情况下其实不会用到window参数,这里参考了vscode的做法 5 | return function dialogShowMessageBox( 6 | options: Omit, 7 | window?: BrowserWindow 8 | ) { 9 | if (window) { 10 | return dialog.showMessageBox(window, { type, ...options }) 11 | } 12 | return dialog.showMessageBox({ type, ...options }) 13 | } 14 | } 15 | 16 | // 将不同类型的messageBox封装成不同方法,简化调用,有点儿类似antd的message、toast等 17 | export const messageBox = { 18 | none: createMessageBoxShow('none'), 19 | info: createMessageBoxShow('info'), 20 | error: createMessageBoxShow('error'), 21 | question: createMessageBoxShow('question'), 22 | warning: createMessageBoxShow('warning') 23 | } 24 | -------------------------------------------------------------------------------- /app/event/code-runner.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-new-func */ 2 | /* eslint-disable no-var */ 3 | 4 | import { createWindow, WindowName } from 'app/browser-window' 5 | import { PLAYGROUND_FILE_URL } from 'app/config' 6 | import { IpcMainEvent, ipcMain, BrowserWindowConstructorOptions, BrowserWindow } from 'electron' 7 | import util from 'util' 8 | 9 | // require会被webpack代理,要使用原生的require需要做判断 10 | declare var __webpack_require__: Function 11 | declare var __non_webpack_require__: Function 12 | const nativeRequire = typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require 13 | 14 | type LogType = 'log' | 'error' | 'warn' | 'info' | 'debug' 15 | export interface LogItem { 16 | type: LogType 17 | content: string 18 | } 19 | 20 | // 拦截运行时的log 21 | class MockConsole { 22 | private _logs: { type: LogType; content: string }[] = [] 23 | 24 | private createConsole(type: LogType) { 25 | return (...args: unknown[]) => { 26 | if (!args.length) { 27 | return 28 | } 29 | try { 30 | console.log('args', args) 31 | 32 | const content = args.reduce( 33 | (prev, curr) => prev + util.inspect(curr, { showHidden: true }), 34 | '', 35 | ) as string 36 | console.log('content', content) 37 | this._logs.push({ type, content }) 38 | } catch (error) { 39 | console.error(error) 40 | } 41 | } 42 | } 43 | 44 | public get logs() { 45 | return this._logs 46 | } 47 | 48 | public log = this.createConsole('log') 49 | 50 | public error = this.createConsole('error') 51 | 52 | public warn = this.createConsole('warn') 53 | 54 | public info = this.createConsole('info') 55 | 56 | public debug = this.createConsole('debug') 57 | } 58 | 59 | // 在主进程执行electron的代码 60 | export function addCodeRunnerListener() { 61 | ipcMain.on('ACTION_CODE', (event: IpcMainEvent, fnStr: string) => { 62 | try { 63 | const mockConsole = new MockConsole() 64 | const fn = new Function( 65 | 'exports', 66 | 'require', 67 | 'module', 68 | '__filename', 69 | '__dirname', 70 | 'console', 71 | `return function(){ 72 | try{ 73 | ${fnStr} 74 | }catch(error){ 75 | console.error('程序执行错误',error) 76 | } 77 | }`, 78 | )(exports, nativeRequire, module, __filename, __dirname, mockConsole) 79 | const result = fn() 80 | if (result) { 81 | mockConsole.log(result) 82 | } 83 | event.returnValue = mockConsole.logs 84 | } catch (err) { 85 | console.log('执行动态代码错误', err) 86 | } 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /app/file-manager/download/icon_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tal-tech/electron-playground/7635a86bfe4de138288e3c73b1674f7ef4338c39/app/file-manager/download/icon_default.png -------------------------------------------------------------------------------- /app/file-manager/index.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron' 2 | import { registerDownloadService } from './download' 3 | 4 | export const registerFileManagerService = (win: BrowserWindow): void => { 5 | registerDownloadService() 6 | } 7 | -------------------------------------------------------------------------------- /app/file-manager/interface.ts: -------------------------------------------------------------------------------- 1 | import { DownloadItem, WebContents } from 'electron' 2 | 3 | export type DownloadItemState = 'progressing' | 'completed' | 'cancelled' | 'interrupted' 4 | 5 | export type IPCEventName = 6 | | 'openDownloadManager' 7 | | 'getDownloadData' 8 | | 'newDownloadFile' 9 | | 'retryDownloadFile' 10 | | 'openFileDialog' 11 | | 'openFile' 12 | | 'openFileInFolder' 13 | | 'initDownloadItem' 14 | | 'pauseOrResume' 15 | | 'removeDownloadItem' 16 | | 'clearDownloadDone' 17 | | 'newDownloadItem' 18 | | 'downloadItemUpdate' 19 | | 'downloadItemDone' 20 | 21 | export interface INewDownloadFile { 22 | url: string 23 | fileName?: string 24 | path: string 25 | } 26 | 27 | export interface IDownloadFile { 28 | id: string 29 | url: string 30 | icon: string 31 | fileName: string 32 | path: string 33 | state: DownloadItemState 34 | startTime: number 35 | speed: number 36 | progress: number 37 | totalBytes: number 38 | receivedBytes: number 39 | paused: boolean 40 | _sourceItem: DownloadItem | undefined 41 | } 42 | 43 | export interface IDownloadBytes { 44 | receivedBytes: number 45 | totalBytes: number 46 | } 47 | 48 | export interface IPagination { 49 | pageIndex: number 50 | pageCount: number 51 | } 52 | 53 | export interface IAddDownloadItem { 54 | item: DownloadItem 55 | downloadIds: string[] 56 | data: IDownloadFile[] 57 | newDownloadItem: INewDownloadFile | null 58 | } 59 | 60 | export interface IUpdateDownloadItem { 61 | item: DownloadItem 62 | data: IDownloadFile[] 63 | downloadItem: IDownloadFile 64 | prevReceivedBytes: number 65 | state: DownloadItemState 66 | } 67 | -------------------------------------------------------------------------------- /app/file-manager/ipc-main.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain, IpcMainInvokeEvent } from 'electron' 2 | import { IPCEventName } from './interface' 3 | 4 | /** 5 | * 添加 ipc 调用的处理事件 6 | * @param eventName - ipc 事件名 7 | * @param listener - 回调事件 8 | */ 9 | export const ipcMainHandle = ( 10 | eventName: IPCEventName, 11 | listener: (event: IpcMainInvokeEvent, ...args: any[]) => Promise | void | T, 12 | ): void => { 13 | ipcMain.handle(eventName, async (event, ...args: any[]) => { 14 | return listener(event, ...args) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /app/file-manager/util.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import { app, shell } from 'electron' 4 | 5 | export { v4 as uuidV4 } from 'uuid' 6 | 7 | /** 8 | * 获取文件后缀名 9 | * @param fileName - 文件名 10 | */ 11 | export const getFileExt = (fileName: string): string => path.extname(fileName) 12 | 13 | /** 14 | * 拼接路径 15 | * @param p - 路径 16 | */ 17 | export const pathJoin = (...p: string[]): string => path.join(...p) 18 | 19 | /** 20 | * 获取文件名 21 | * @param fileName - 文件名 22 | * @param defaultName - 默认文件名 23 | */ 24 | export const getFileName = (fileName: string, defaultName: string): string => { 25 | // 处理 Windows 文件名不允许的字符 26 | fileName = fileName.replace(/(\/|\|?:|\?|\*|"|>|<|\|)/g, '') || path.basename(defaultName) 27 | fileName = /^\.(.*)/.test(fileName) ? defaultName : fileName 28 | 29 | const extName = getFileExt(fileName) 30 | if (!extName) { 31 | const ext = getFileExt(defaultName) 32 | fileName = `${fileName}.${ext}` 33 | } 34 | 35 | return decodeURIComponent(fileName) 36 | } 37 | 38 | /** 39 | * 获取文件图标。 40 | * 系统关联图标 41 | * @param path - 文件路径 42 | */ 43 | export const getFileIcon = async (path: string): Promise => { 44 | const iconDefault = './icon_default.png' 45 | if (!path) Promise.resolve(iconDefault) 46 | 47 | const icon = await app.getFileIcon(path, { 48 | size: 'normal', 49 | }) 50 | 51 | return icon.toDataURL() 52 | } 53 | 54 | /** 55 | * 检查文件是否存在 56 | * @param path - 文件路径 57 | */ 58 | export const isExistFile = (path: string): boolean => fs.existsSync(path) 59 | 60 | /** 61 | * 删除指定路径文件 62 | * @param path - 文件路径 63 | */ 64 | export const removeFile = (path: string): void => { 65 | if (!isExistFile(path)) return 66 | 67 | fs.unlinkSync(path) 68 | } 69 | 70 | /** 71 | * 打开文件 72 | * @param path - 文件路径 73 | */ 74 | export const openFile = (path: string): boolean => { 75 | if (!isExistFile(path)) return false 76 | 77 | shell.openPath(path) 78 | return true 79 | } 80 | 81 | /** 82 | * 打开文件所在位置 83 | * @param path - 文件路径 84 | */ 85 | export const openFileInFolder = (path: string): boolean => { 86 | if (!isExistFile(path)) return false 87 | 88 | shell.showItemInFolder(path) 89 | return true 90 | } 91 | 92 | /** 93 | * 获取 base64 图片字节 94 | * @param base64 - base64 字符串 95 | */ 96 | export const getBase64Bytes = (base64: string): number => { 97 | if (!/^data:.*;base64/.test(base64)) return 0 98 | 99 | const data = base64.split(',')[1].split('=')[0] 100 | const { length } = data 101 | 102 | return Math.floor(length - (length / 8) * 2) 103 | } 104 | -------------------------------------------------------------------------------- /app/global.d.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | declare global { 5 | interface Window { 6 | $EB: any 7 | fs: typeof fs 8 | path: typeof path 9 | } 10 | namespace NodeJS { 11 | interface Global { 12 | __native_console_log__: typeof console.log 13 | __native_console_error__: typeof console.error 14 | __native_console_warn__: typeof console.warn 15 | __native_console_info__: typeof console.info 16 | 17 | __dirname: string 18 | title: string 19 | mainId: number 20 | hasAutoUpdateClicked: boolean 21 | } 22 | } 23 | } 24 | 25 | export {} 26 | -------------------------------------------------------------------------------- /app/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { app } from 'electron' 3 | import 'app/collect/sentry' 4 | 5 | import { closeMainWindow, restoreMainWindow } from 'app/browser-window' 6 | import { startUpdaterSchedule } from 'app/updater' 7 | import { addCodeRunnerListener } from 'app/event/code-runner' 8 | import ProtocolService from 'app/protocol' 9 | import { setUpTray } from './tray' 10 | import { addDevToolsExtensionAtDevelopmentMode } from 'app/chrome-extensions' 11 | import { setupMenu } from './menu' 12 | import { createWindow } from './browser-window' 13 | 14 | 15 | function run() { 16 | // https://www.electronjs.org/docs/api/app#apprequestsingleinstancelock 17 | // 请求单例锁,避免打开多个electron实例 18 | const gotTheLock = app.requestSingleInstanceLock() 19 | if (!gotTheLock) { 20 | app.quit() 21 | return 22 | } 23 | ProtocolService.setDefaultProtocol() 24 | // 如果有第二个实例 将重启应用 25 | app.on('second-instance', () => { 26 | restoreMainWindow() 27 | }) 28 | 29 | app.on('ready', () => { 30 | ProtocolService.registerStringProtocol() 31 | addDevToolsExtensionAtDevelopmentMode() 32 | 33 | const win = createWindow('start') 34 | 35 | setupMenu() 36 | setUpTray() 37 | }) 38 | 39 | app.allowRendererProcessReuse = false 40 | 41 | app.on('will-finish-launching', () => { 42 | ProtocolService.watchMacProtocol() 43 | startUpdaterSchedule() // 五小时检测一次更新 44 | addCodeRunnerListener() // 监听渲染进程的code-runner 45 | }) 46 | ProtocolService.watchWindowProtocol() 47 | 48 | app.on('window-all-closed', () => { 49 | if (process.platform !== 'darwin') { 50 | app.quit() 51 | } 52 | }) 53 | 54 | app.on('before-quit', () => { 55 | closeMainWindow() 56 | }) 57 | 58 | app.on('activate', () => { 59 | restoreMainWindow() 60 | }) 61 | } 62 | 63 | 64 | 65 | run() 66 | -------------------------------------------------------------------------------- /app/mac-app.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tal-tech/electron-playground/7635a86bfe4de138288e3c73b1674f7ef4338c39/app/mac-app.ts -------------------------------------------------------------------------------- /app/menu/index.ts: -------------------------------------------------------------------------------- 1 | import { Menu, MenuItemConstructorOptions, ipcMain } from 'electron' 2 | import { appUpdater } from 'app/updater' 3 | 4 | export function setupMenu() { 5 | type MenuItemsType = MenuItemConstructorOptions[] 6 | const menuOption: MenuItemsType = [ 7 | { 8 | label: '操作', 9 | submenu: [ 10 | { role: 'about' }, 11 | { 12 | label: 'check update', 13 | click() { 14 | appUpdater.manualCheck() 15 | }, 16 | }, 17 | { role: 'hide' }, 18 | { role: 'hideOthers' }, 19 | { role: 'quit' }, 20 | ], 21 | }, 22 | {role: 'editMenu'}, 23 | {role: 'fileMenu'}, 24 | {role: 'viewMenu'}, 25 | {role: 'windowMenu'}, 26 | // TODO: 待添加,访问官网文档,访问github,上报issue等 27 | // { 28 | // label: 'Help', 29 | // submenu: [ 30 | // ] 31 | // }, 32 | ] 33 | const handleOption = Menu.buildFromTemplate(menuOption) // 构造MenuItem的选项数组。 34 | // 设置菜单 35 | // Menu.setApplicationMenu(null) 36 | Menu.setApplicationMenu(handleOption) 37 | 38 | } 39 | 40 | ipcMain.on('SetupMenu', setupMenu) 41 | 42 | -------------------------------------------------------------------------------- /app/preload/demo-communication.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 这个是用于窗口通信例子的preload, 3 | * preload执行顺序在窗口js执行顺序之前 4 | */ 5 | /* eslint-disable */ 6 | import { ipcRenderer, remote } from 'electron' 7 | const { argv } = require('yargs') 8 | 9 | const { BrowserWindow } = remote 10 | 11 | // 父窗口监听子窗口事件 12 | ipcRenderer.on('communication-to-parent', (event, msg) => { 13 | alert(msg) 14 | }) 15 | 16 | const { parentWindowId } = argv 17 | if (parentWindowId !== 'undefined') { 18 | const parentWindow = BrowserWindow.fromId(parentWindowId as number) 19 | // 挂载到window 20 | // @ts-ignore 21 | window.send = (params: any) => { 22 | parentWindow.webContents.send('communication-to-parent', params) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/preload/demo-communication2.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { remote, ipcRenderer } from 'electron' 3 | 4 | // 父窗口监听子窗口事件 5 | ipcRenderer.on('communication-to-parent', (event, msg) => { 6 | alert(msg) 7 | }) 8 | 9 | const parentWindow = remote.getCurrentWindow().getParentWindow() 10 | // @ts-ignore 11 | window.sendToParent = (params: any) => 12 | parentWindow.webContents.send('communication-to-parent', params) 13 | -------------------------------------------------------------------------------- /app/preload/demo-full-screen.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { remote } from 'electron' 3 | 4 | const setFullScreen = remote.getCurrentWindow().setFullScreen 5 | const isFullScreen = remote.getCurrentWindow().isFullScreen 6 | // @ts-ignore 7 | window.setFullScreen = setFullScreen 8 | // @ts-ignore 9 | window.isFullScreen = isFullScreen 10 | -------------------------------------------------------------------------------- /app/preload/demo-window-type.ts: -------------------------------------------------------------------------------- 1 | function initTopDrag() { 2 | const topDiv = document.createElement('div') // 创建节点 3 | topDiv.style.position = 'fixed' // 一直在顶部 4 | topDiv.style.top = '0' 5 | topDiv.style.left = '0' 6 | topDiv.style.height = '20px' // 顶部20px才可拖动 7 | topDiv.style.width = '100%' // 宽度100% 8 | topDiv.style.zIndex = '9999' // 悬浮于最外层 9 | topDiv.style.pointerEvents = 'none' // 用于点击穿透 10 | // @ts-ignore 11 | topDiv.style['-webkit-user-select'] = 'none' // 禁止选择文字 12 | // @ts-ignore 13 | topDiv.style['-webkit-app-region'] = 'drag' // 拖动 14 | document.body.appendChild(topDiv) // 添加节点 15 | } 16 | 17 | window.addEventListener('DOMContentLoaded', function onDOMContentLoaded() { 18 | initTopDrag() 19 | }) 20 | -------------------------------------------------------------------------------- /app/preload/draggable.ts: -------------------------------------------------------------------------------- 1 | // Insert a removable dom at the top 2 | export function initDraggable() { 3 | const topDiv = document.createElement('div') 4 | topDiv.style.position = 'fixed' 5 | topDiv.style.top = '0' 6 | topDiv.style.left = '0' 7 | topDiv.style.height = '20px' 8 | topDiv.style.width = '100%' 9 | topDiv.style.zIndex = '9999' 10 | topDiv.style.pointerEvents = 'none' // click through 11 | // @ts-ignore 12 | topDiv.style['-webkit-user-select'] = 'none' // prohibit text selection 13 | // @ts-ignore 14 | topDiv.style['-webkit-app-region'] = 'drag' 15 | document.body.appendChild(topDiv) 16 | } 17 | -------------------------------------------------------------------------------- /app/preload/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { initJSAPI } from './jsapi' 3 | import { argv } from 'yargs' 4 | import { initDraggable } from './draggable' 5 | import fs from 'fs' 6 | import path from 'path' 7 | 8 | initJSAPI() 9 | 10 | window.addEventListener('DOMContentLoaded', async () => { 11 | initDraggable() 12 | }) 13 | 14 | console.log(argv) 15 | window.fs = fs 16 | window.path = path 17 | -------------------------------------------------------------------------------- /app/preload/jsapi.ts: -------------------------------------------------------------------------------- 1 | import { WindowName } from 'app/browser-window' 2 | import { ipcRenderer, BrowserWindowConstructorOptions } from 'electron' 3 | 4 | // 执行electron代码 5 | function actionCode(fnStr: string) { 6 | const result = ipcRenderer.sendSync('ACTION_CODE', fnStr) 7 | return result 8 | } 9 | 10 | // 打开新的窗口 11 | function openWindow(name: WindowName) { 12 | const result = ipcRenderer.send('OPEN_WINDOW', { name }) 13 | return result 14 | } 15 | 16 | function closeWindow(name?: WindowName) { 17 | const result = ipcRenderer.send('CLOSE_WINDOW', name) 18 | return result 19 | } 20 | 21 | function maximizeWindow(name?: WindowName) { 22 | const result = ipcRenderer.send('MAXIMIZE_WINDOW', name) 23 | return result 24 | } 25 | 26 | function minimizeWindow(name?: WindowName) { 27 | const result = ipcRenderer.send('MINIMIZE_WINDOW', name) 28 | return result 29 | } 30 | 31 | export function initJSAPI() { 32 | window.$EB = { 33 | ipcRenderer, 34 | actionCode, 35 | openWindow, 36 | closeWindow, 37 | maximizeWindow, 38 | minimizeWindow, 39 | crash: process.crash, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/protocol/index.ts: -------------------------------------------------------------------------------- 1 | // 自定义协议 2 | import { app, protocol } from 'electron' 3 | import path from 'path' 4 | import { messageBox } from 'app/dialog' 5 | 6 | const PROTOCOL_URL = 'electron-playground' 7 | 8 | // 自定义协议的链接 正则 9 | const DEFAULT_PROTOCOL_REGEXP = new RegExp(`^${PROTOCOL_URL}://`) 10 | 11 | // 设置自定义协议 12 | function setDefaultProtocol() { 13 | // 每次运行删除 这样就可以重新注册了 14 | app.removeAsDefaultProtocolClient(PROTOCOL_URL) 15 | // 开发模式下在window运行需要做兼容 16 | if (process.env.NODE_ENV === 'development' && process.platform === 'win32') { 17 | // 设置electron.exe 和 app的路径 18 | app.setAsDefaultProtocolClient(PROTOCOL_URL, process.execPath, [ 19 | path.resolve(process.argv[1]), 20 | ]) 21 | } else { 22 | app.setAsDefaultProtocolClient(PROTOCOL_URL) 23 | } 24 | } 25 | 26 | // 监听mac下自定义协议打开 27 | function watchMacProtocol() { 28 | // mac会激活open-url事件 29 | app.on('open-url', (event, url) => { 30 | // electron-playground://asdsadsaddsfd 31 | const isProtocol = DEFAULT_PROTOCOL_REGEXP.test(url) 32 | if (isProtocol) { 33 | messageBox.info({ 34 | message: 'Mac protocol 自定义协议打开', 35 | detail: `链接:${url}`, 36 | }) 37 | } 38 | }) 39 | } 40 | 41 | // 监听window下 自定义协议打开 42 | function watchWindowProtocol() { 43 | app.on('second-instance', (event, commandLine) => { 44 | commandLine.forEach(str => { 45 | if (DEFAULT_PROTOCOL_REGEXP.test(str)) { 46 | messageBox.info({ 47 | message: 'window protocol 自定义协议打开', 48 | detail: `链接:${str}`, 49 | }) 50 | } 51 | }) 52 | }) 53 | } 54 | 55 | const myScheme = 'myscheme' 56 | 57 | // 需要再ready事件前调用, 并且只调用一次 58 | protocol.registerSchemesAsPrivileged([ 59 | { scheme: myScheme, privileges: { bypassCSP: true } }, 60 | ]) 61 | 62 | // 请求文件自定义协议拦截 重新设置请求链接 63 | function registerStringProtocol() { 64 | protocol.registerFileProtocol( 65 | myScheme, 66 | (request, callback) => { 67 | // 重新拼接文件资源路径 68 | const resolvePath = path.resolve(__dirname, '../../playground') 69 | let url = request.url.replace( `${myScheme}://`, '' ) 70 | url = `${resolvePath}/${url}` 71 | return callback({ path: decodeURIComponent(url) }) 72 | }, 73 | ) 74 | } 75 | 76 | export default { 77 | setDefaultProtocol, 78 | watchMacProtocol, 79 | watchWindowProtocol, 80 | registerStringProtocol, 81 | } 82 | -------------------------------------------------------------------------------- /app/tray/index.ts: -------------------------------------------------------------------------------- 1 | import { Tray, Menu, app, nativeTheme } from 'electron' 2 | import path from 'path' 3 | import { restoreMainWindow } from 'app/browser-window' 4 | 5 | let tray: Tray 6 | // 设置顶部APP图标的操作和图标 7 | export function setUpTray() { 8 | const lightIcon = path.join(__dirname, '..', '..', 'resources', 'tray', 'StatusIcon_light.png') 9 | const darkIcon = path.join(__dirname, '..', '..', 'resources', 'tray', 'StatusIcon_dark.png') 10 | 11 | tray = new Tray(nativeTheme.shouldUseDarkColors ? darkIcon : lightIcon) 12 | 13 | const contextMenu = Menu.buildFromTemplate([ 14 | { 15 | label: '打开Playground', 16 | click: () => { 17 | restoreMainWindow() 18 | }, 19 | }, 20 | { 21 | label: '退出', 22 | click: () => { 23 | app.quit() 24 | }, 25 | }, 26 | ]) 27 | tray.setToolTip('Electron-Playground') 28 | tray.setContextMenu(contextMenu) 29 | 30 | nativeTheme.on('updated', () => { 31 | tray.setImage(nativeTheme.shouldUseDarkColors ? darkIcon : lightIcon) 32 | }) 33 | 34 | // windows下双击托盘图标打开app 35 | tray.on('double-click', () => { 36 | restoreMainWindow() 37 | }) 38 | } 39 | 40 | export function destroyTray() { 41 | tray.destroy() 42 | } -------------------------------------------------------------------------------- /build/pack.ts: -------------------------------------------------------------------------------- 1 | import * as inquirer from 'inquirer' 2 | import { execCommand, NPM_COMMAND } from './pack/tool' 3 | import { readPackageJSON, updateChangeLog } from './pack/change-file' 4 | import { build } from './pack/buid' 5 | 6 | // 校验版本号 7 | function validateVersion(input: string) { 8 | if (!input || /^\d+(?:\.\d+){2}$/.test(input)) { 9 | return true 10 | } 11 | console.log('请输入正确的版本号(X.Y.Z)') 12 | return false 13 | } 14 | 15 | export interface BuildOptions { 16 | platforms: string[] 17 | version: string 18 | releaseName: string 19 | releaseNotes: string 20 | } 21 | 22 | async function startPack() { 23 | const options = await inquirer.prompt([ 24 | { 25 | type: 'list', 26 | name: 'platforms', 27 | message: '平台?', 28 | choices: [ 29 | { name: 'all', value: ['win', 'mac'] }, 30 | { name: 'win', value: ['win'] }, 31 | { name: 'mac', value: ['mac'] }, 32 | ], 33 | }, 34 | { 35 | type: 'input', 36 | name: 'version', 37 | message: `版本号?(当前为${readPackageJSON().version})`, 38 | validate: validateVersion, 39 | }, 40 | { 41 | type: 'input', 42 | name: 'releaseName', 43 | message: `更新标题(不输入则为默认标题 \`版本更新\`):`, 44 | default: '版本更新', 45 | }, 46 | { type: 'input', name: 'releaseNotes', message: `更新描述:` }, 47 | ]) 48 | 49 | console.log(options) 50 | 51 | // 更新版本号 52 | await execCommand(NPM_COMMAND, [`version`, options.version || 'patch', '--allow-same-version']) 53 | options.version = readPackageJSON().version 54 | 55 | // 更新changelog (只有打包包括正式环境时更新) 56 | updateChangeLog({ ...options }) 57 | 58 | // 打包 59 | await build(options) 60 | 61 | // git提交 62 | await execCommand('git', ['commit', '-a', '-m', `feat: CHANGELOG更新 V${options.version}`]) 63 | } 64 | 65 | startPack() -------------------------------------------------------------------------------- /build/pack/buid.ts: -------------------------------------------------------------------------------- 1 | import { execCommand, NPM_COMMAND } from './tool' 2 | import { updateFillBuilderYAML } from './change-file' 3 | 4 | interface BuildOptions { 5 | platforms: string[] 6 | version: string 7 | releaseName: string 8 | releaseNotes: string 9 | } 10 | 11 | export async function build(option: BuildOptions) { 12 | for (const platform of option.platforms) { 13 | await execCommand(NPM_COMMAND, ['run', 'clean']) // 删除之前打包的 14 | await packTask(option, platform) 15 | } 16 | } 17 | 18 | async function packTask(option: BuildOptions, platform: string) { 19 | const params = { 20 | RELEASE_NAME: option.releaseName, 21 | RELEASE_NOTES: option.releaseNotes, 22 | } 23 | updateFillBuilderYAML(params) 24 | 25 | process.env.NODE_ENV = 'production' 26 | // 编译主进程和渲染进程的文件到dist 27 | await execCommand(NPM_COMMAND, ['run', 'build']) 28 | // 打包对应平台的安装包 29 | await execCommand(NPM_COMMAND, ['run', `pack-${platform}`]) 30 | } 31 | -------------------------------------------------------------------------------- /build/pack/change-file.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as path from 'path' 3 | import dayjs from 'dayjs' 4 | import * as os from 'os' 5 | import { readJsonSync } from 'fs-extra' 6 | 7 | interface ChangeLog { 8 | version: string 9 | releaseName: string 10 | releaseNotes: string 11 | } 12 | 13 | const CHANGE_LOG_PATH = path.resolve(__dirname, '..', '..', 'CHANGELOG.md') 14 | 15 | export function updateChangeLog(cl: ChangeLog) { 16 | const { version, releaseName, releaseNotes } = cl 17 | const content = `## 版本:${version} 18 | 19 | - ${dayjs().format('YYYY-MM-DD hh:mm:ss')} 20 | 21 | ${releaseName} 22 | 23 | \`\`\` 24 | ${releaseNotes} 25 | \`\`\` 26 | ` 27 | 28 | fs.appendFileSync(CHANGE_LOG_PATH, content) 29 | } 30 | 31 | // 根据变量名构建一个正则,匹配对应模板中的变量表示 32 | function createReg(variable: string) { 33 | return new RegExp(`{{${variable}}}`, 'g') 34 | } 35 | 36 | // 处理换行,yaml中的换行需要空一行来表示,需要将\n变成\n\n 37 | function processEOL(str: string) { 38 | return str.replace(new RegExp(os.EOL, 'g'), os.EOL + os.EOL) 39 | } 40 | 41 | // 修改electron-builder-template配置,替换electron-builder.yaml 42 | export function updateFillBuilderYAML(option: object) { 43 | // 配置路径 44 | const ELECTRON_BUILDER_TEMPLATE = path.resolve( 45 | __dirname, 46 | '..', 47 | '..', 48 | 'electron-builder-template.yml', 49 | ) 50 | const ELECTRON_BUILDER_OUTPUT = path.resolve(__dirname, '..', '..', 'electron-builder.yml') 51 | let content = fs.readFileSync(ELECTRON_BUILDER_TEMPLATE).toString() 52 | // 替换匹配到的每个变量 53 | Object.entries(option).forEach( 54 | ([key, val]) => (content = content.replace(createReg(key), processEOL(val))) 55 | ) 56 | 57 | fs.writeFileSync(ELECTRON_BUILDER_OUTPUT, content) 58 | } 59 | 60 | 61 | 62 | // 读取json内容 63 | export const readJSON = (path: string) => () => readJsonSync(path) 64 | 65 | // 覆写json变量 66 | export const writeJSON = (path: string) => (vars: { [index: string]: any }) => { 67 | const content = readJSON(path)() 68 | for (const key in vars) { 69 | if (Object.prototype.hasOwnProperty.call(vars, key)) { 70 | const element = vars[key] 71 | content[key] = element 72 | } 73 | } 74 | 75 | const contentStr = JSON.stringify(content, null, 2) 76 | fs.writeFileSync(path, contentStr, { encoding: 'utf8' }) 77 | } 78 | 79 | const package_json_path = path.resolve(__dirname, '..', '..', 'package.json') 80 | export const readPackageJSON = readJSON(package_json_path) 81 | export const writePackageJSON = writeJSON(package_json_path) -------------------------------------------------------------------------------- /build/pack/tool.ts: -------------------------------------------------------------------------------- 1 | import { spawn, exec, execSync } from 'child_process' 2 | import chalk from 'chalk' 3 | 4 | export const NPM_COMMAND = process.platform === 'win32' ? 'npm.cmd' : 'npm' 5 | 6 | // 执行命令 7 | export function execCommand(command: string, args: string[]) { 8 | return new Promise((resolve, reject) => { 9 | const ls = spawn(command, args, { stdio: 'inherit' }) 10 | 11 | ls.on('error', error => { 12 | console.log(chalk.red(error.message)) 13 | }) 14 | 15 | ls.on('close', code => { 16 | console.log(chalk.blue(`[${command} ${args.join(' ')}]`) + `exited with code ${code}`) 17 | code === 0 ? resolve() : reject(code) 18 | }) 19 | }) 20 | } 21 | 22 | // 执行异步命令 23 | export function actionCommand(cmd: string, callBack?: Function) { 24 | try { 25 | exec(cmd, (error, stdout, stderr) => { 26 | if (error) { 27 | console.error(`执行的错误: ${error}`) 28 | return 29 | } 30 | if (callBack) { 31 | callBack(stdout, stderr) 32 | } 33 | }) 34 | } catch (err) { 35 | console.log(`执行命令出错:${cmd}`) 36 | throw err 37 | } 38 | } 39 | // 执行同步命令 40 | export function actionCommandSync(cmd: string) { 41 | try { 42 | const res = execSync(cmd, { 43 | encoding: 'utf8', 44 | timeout: 0, 45 | maxBuffer: 200 * 1024, 46 | killSignal: 'SIGTERM', 47 | cwd: undefined, 48 | env: undefined, 49 | }) 50 | return res 51 | } catch (err) { 52 | console.log(`执行命令出错:${cmd}`) 53 | throw err 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /build/playground/config/api-menus.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | 4 | // 找到apidoc下所有的markdown文件 5 | const apidoc_path = path.resolve(__dirname,'..','..','..','playground', 'apidocs') 6 | const menus = [] 7 | 8 | function findMarkdowns(dirPath, parentChildrenFolder) { 9 | if (!fs.statSync(dirPath).isDirectory()) return 10 | const files = fs.readdirSync(dirPath) 11 | files.forEach(f => { 12 | const fullPath = path.join(dirPath, f) 13 | if(['img', 'components'].includes(f)) return 14 | if (fs.statSync(fullPath).isFile() && path.extname(fullPath) === '.md') { 15 | parentChildrenFolder.push({filePath: fullPath, title: f.replace(/.md$/, '')}) 16 | return 17 | } 18 | if (fs.statSync(fullPath).isDirectory()) { 19 | const folder = {title: f, children: [], filePath: fullPath} 20 | parentChildrenFolder.push(folder) 21 | findMarkdowns(fullPath, folder.children) 22 | } 23 | }) 24 | } 25 | 26 | findMarkdowns(apidoc_path, menus) 27 | 28 | module.exports = menus -------------------------------------------------------------------------------- /build/playground/config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | const menus = require('./api-menus') 7 | 8 | console.log(menus) 9 | 10 | // Make sure that including paths.js after env.js will read .env variables. 11 | delete require.cache[require.resolve('./paths')]; 12 | 13 | const NODE_ENV = process.env.NODE_ENV; 14 | if (!NODE_ENV) { 15 | throw new Error( 16 | 'The NODE_ENV environment variable is required but was not specified.' 17 | ); 18 | } 19 | 20 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 21 | const dotenvFiles = [ 22 | `${paths.dotenv}.${NODE_ENV}.local`, 23 | `${paths.dotenv}.${NODE_ENV}`, 24 | // Don't include `.env.local` for `test` environment 25 | // since normally you expect tests to produce the same 26 | // results for everyone 27 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 28 | paths.dotenv, 29 | ].filter(Boolean); 30 | 31 | // Load environment variables from .env* files. Suppress warnings using silent 32 | // if this file is missing. dotenv will never modify any environment variables 33 | // that have already been set. Variable expansion is supported in .env files. 34 | // https://github.com/motdotla/dotenv 35 | // https://github.com/motdotla/dotenv-expand 36 | dotenvFiles.forEach(dotenvFile => { 37 | if (fs.existsSync(dotenvFile)) { 38 | require('dotenv-expand')( 39 | require('dotenv').config({ 40 | path: dotenvFile, 41 | }) 42 | ); 43 | } 44 | }); 45 | 46 | // We support resolving modules according to `NODE_PATH`. 47 | // This lets you use absolute paths in imports inside large monorepos: 48 | // https://github.com/facebook/create-react-app/issues/253. 49 | // It works similar to `NODE_PATH` in Node itself: 50 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 51 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 52 | // Otherwise, we risk importing Node.js core modules into an app instead of webpack shims. 53 | // https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 54 | // We also resolve them to make sure all tools using them work consistently. 55 | const appDirectory = fs.realpathSync(process.cwd()); 56 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 57 | .split(path.delimiter) 58 | .filter(folder => folder && !path.isAbsolute(folder)) 59 | .map(folder => path.resolve(appDirectory, folder)) 60 | .join(path.delimiter); 61 | 62 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 63 | // injected into the application via DefinePlugin in webpack configuration. 64 | const REACT_APP = /^REACT_APP_/i; 65 | 66 | function getClientEnvironment(publicUrl) { 67 | const raw = Object.keys(process.env) 68 | .filter(key => REACT_APP.test(key)) 69 | .reduce( 70 | (env, key) => { 71 | env[key] = process.env[key]; 72 | return env; 73 | }, 74 | { 75 | API_MENUS: menus, 76 | // Useful for determining whether we’re running in production mode. 77 | // Most importantly, it switches React into the correct mode. 78 | NODE_ENV: process.env.NODE_ENV || 'development', 79 | // Useful for resolving the correct path to static assets in `public`. 80 | // For example, . 81 | // This should only be used as an escape hatch. Normally you would put 82 | // images into the `src` and `import` them in code to get their paths. 83 | PUBLIC_URL: publicUrl, 84 | // We support configuring the sockjs pathname during development. 85 | // These settings let a developer run multiple simultaneous projects. 86 | // They are used as the connection `hostname`, `pathname` and `port` 87 | // in webpackHotDevClient. They are used as the `sockHost`, `sockPath` 88 | // and `sockPort` options in webpack-dev-server. 89 | WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST, 90 | WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH, 91 | WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT, 92 | } 93 | ); 94 | // Stringify all values so we can feed into webpack DefinePlugin 95 | const stringified = { 96 | 'process.env': Object.keys(raw).reduce((env, key) => { 97 | env[key] = JSON.stringify(raw[key]); 98 | return env; 99 | }, {}), 100 | }; 101 | 102 | return { raw, stringified }; 103 | } 104 | 105 | module.exports = getClientEnvironment; 106 | -------------------------------------------------------------------------------- /build/playground/config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const fs = require('fs') 5 | const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath') 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()) 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath) 11 | 12 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 13 | // "public path" at which the app is served. 14 | // webpack needs to know it to put the right , 11 | Chromium , 12 | and Electron . 13 | 14 | 18 | 19 | -------------------------------------------------------------------------------- /playground/example/template/main.ts: -------------------------------------------------------------------------------- 1 | // Modules to control application life and create native browser window 2 | const { app, BrowserWindow } = require('electron') 3 | 4 | function createWindow() { 5 | // Create the browser window. 6 | const mainWindow = new BrowserWindow({ 7 | width: 800, 8 | height: 600, 9 | webPreferences: { 10 | nodeIntegration: true, 11 | }, 12 | }) 13 | 14 | // and load the index.html of the app. 15 | mainWindow.loadFile('index.html') 16 | 17 | // Open the DevTools. 18 | // mainWindow.webContents.openDevTools() 19 | } 20 | 21 | // This method will be called when Electron has finished 22 | // initialization and is ready to create browser windows. 23 | // Some APIs can only be used after this event occurs. 24 | app.on('ready', createWindow) 25 | 26 | // Quit when all windows are closed, except on macOS. There, it's common 27 | // for applications and their menu bar to stay active until the user quits 28 | // explicitly with Cmd + Q. 29 | app.on('window-all-closed', function () { 30 | if (process.platform !== 'darwin') { 31 | app.quit() 32 | } 33 | }) 34 | 35 | app.on('activate', function () { 36 | // On OS X it's common to re-create a window in the app when the 37 | // dock icon is clicked and there are no other windows open. 38 | if (BrowserWindow.getAllWindows().length === 0) { 39 | createWindow() 40 | } 41 | }) 42 | 43 | // In this file you can include the rest of your app's specific main process 44 | // code. You can also put them in separate files and require them here. 45 | -------------------------------------------------------------------------------- /playground/example/template/preload.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tal-tech/electron-playground/7635a86bfe4de138288e3c73b1674f7ef4338c39/playground/example/template/preload.ts -------------------------------------------------------------------------------- /playground/example/template/renderer.ts: -------------------------------------------------------------------------------- 1 | // This file is required by the index.html file and will 2 | // be executed in the renderer process for that window. 3 | // All of the Node.js APIs are available in this process. 4 | -------------------------------------------------------------------------------- /playground/global.d.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | declare global { 5 | interface Window { 6 | $EB: any 7 | fs: typeof fs 8 | path: typeof path 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /playground/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /playground/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './index.css' 4 | import App from './App' 5 | 6 | ReactDOM.render(, document.getElementById('root')) 7 | -------------------------------------------------------------------------------- /playground/page/apidoc/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useMemo, useState } from 'react' 2 | import fs from 'fs' 3 | import path from 'path' 4 | import { Layout, Menu } from 'antd' 5 | import Markdown from 'components/markdown' 6 | import { EditFilled } from '@ant-design/icons' 7 | 8 | import style from './style.module.less' 9 | 10 | interface IApidocProps { } 11 | 12 | interface MenusInfo { 13 | filePath: string 14 | title: string 15 | children?: MenusInfo[] 16 | } 17 | 18 | const apidoc_path = path.resolve(__dirname, '..', '..', 'playground', 'apidocs') 19 | let menus: MenusInfo[] = [] 20 | 21 | function findMarkdowns(dirPath: string, parentChildrenFolder: MenusInfo[]) { 22 | if (!fs.statSync(dirPath).isDirectory()) return 23 | const files = fs.readdirSync(dirPath) 24 | files.forEach(f => { 25 | const fullPath = path.join(dirPath, f) 26 | // img和components不展示到目录中 27 | if (['img', 'components'].includes(f)) return 28 | if (fs.statSync(fullPath).isFile() && path.extname(fullPath) === '.md') { 29 | parentChildrenFolder.push({ filePath: fullPath, title: f.replace(/.md$/, '') }) 30 | return 31 | } 32 | if (fs.statSync(fullPath).isDirectory()) { 33 | const folder = { title: f, children: [], filePath: fullPath } 34 | parentChildrenFolder.push(folder) 35 | findMarkdowns(fullPath, folder.children) 36 | } 37 | }) 38 | } 39 | if (process.env.NODE_ENV !== 'production') { 40 | findMarkdowns(apidoc_path, menus) 41 | } else { 42 | menus = process.env.API_MENUS as any 43 | } 44 | 45 | console.log(process.env) 46 | 47 | const Apidoc: React.FunctionComponent = props => { 48 | const [markdownPath, setMarkdownPath] = useState('') 49 | const [content, setContent] = useState('') 50 | 51 | const handleMenuClick = (filePath: string) => { 52 | const relativePath = filePath.replace(apidoc_path, '') 53 | import(`../../apidocs${relativePath.replace(/\\/g, '/')}`).then(res => { 54 | setContent('') 55 | setContent(res.default) 56 | setMarkdownPath(relativePath) 57 | }) 58 | } 59 | 60 | // 用于menu的子菜单展开 61 | const defaultOpenKeys = useMemo(() => { 62 | return menus.filter(m => m.children?.length).map(i => i.filePath) 63 | }, []) 64 | 65 | const generateMenus = (menu: MenusInfo) => { 66 | const { filePath, title, children } = menu 67 | if (children?.length) { 68 | return 69 | {children.map((item, index) => generateMenus(item))} 70 | 71 | } 72 | return handleMenuClick(filePath as string)} key={filePath}>{title} 73 | } 74 | 75 | return ( 76 | 77 | 78 | 79 | {menus.map(i => generateMenus(i))} 80 | 81 | 82 | 83 | 84 | {content && } 85 | {content &&

86 | 对文档内容有不同意见?欢迎 87 | 提供修改 88 |

} 89 |
90 |
91 |
92 | ) 93 | } 94 | 95 | export default Apidoc 96 | -------------------------------------------------------------------------------- /playground/page/apidoc/style.module.less: -------------------------------------------------------------------------------- 1 | @footerHeight: 36px; 2 | @mainMargin: 24px; 3 | 4 | .container { 5 | height: 100vh; 6 | 7 | .main { 8 | margin: @mainMargin; 9 | height: calc(100% - 2 * @mainMargin); 10 | } 11 | 12 | .content { 13 | background-color: #fff; 14 | border-radius: 4px; 15 | padding: 24px; 16 | overflow-y: auto; 17 | } 18 | .menu{ 19 | height: 100%; 20 | padding-top: 20px; 21 | overflow-y: auto; 22 | overflow-x: hidden; 23 | } 24 | 25 | .edit{ 26 | margin-top: 40px; 27 | border-top: 1px dashed #999; 28 | padding: 12px 0; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /playground/page/browser-demo/communication-part1/client.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useEffect, useState } from 'react' 2 | import style from '../style.module.less' 3 | 4 | const COUNT_NUM = 5 5 | 6 | export default function Communication(): ReactElement { 7 | const [num, setNum] = useState(COUNT_NUM) 8 | 9 | useEffect(() => { 10 | document.title = '子窗口' 11 | let timer: number 12 | 13 | if (num > 0) { 14 | timer = window.setTimeout(() => { 15 | setNum(num - 1) 16 | }, 1000) 17 | } else { 18 | // @ts-ignore 19 | window.send('hello') 20 | window.close() 21 | } 22 | return () => { 23 | timer && window.clearTimeout(timer) 24 | } 25 | }, [num]) 26 | 27 | return
子窗口 {num} 秒之后,请看主窗口
28 | } 29 | -------------------------------------------------------------------------------- /playground/page/browser-demo/communication-part1/main.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useEffect } from 'react' 2 | import style from '../style.module.less' 3 | 4 | export default function Communication(): ReactElement { 5 | useEffect(() => { 6 | document.title = '父窗口' 7 | }, []) 8 | 9 | return ( 10 |
11 | 12 | 通过a标签target=__blank打开新的窗口 13 | 14 |
{ 16 | window.open('http://www.github.com') 17 | }}> 18 | 通过window.open打开新的窗口 19 |
20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /playground/page/browser-demo/communication-part2/client.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useEffect, useState } from 'react' 2 | import style from '../style.module.less' 3 | 4 | const COUNT_NUM = 5 5 | 6 | export default function Communication(): ReactElement { 7 | const [num, setNum] = useState(COUNT_NUM) 8 | 9 | useEffect(() => { 10 | document.title = '子窗口' 11 | let timer: NodeJS.Timeout 12 | 13 | if (num > 0) { 14 | timer = setTimeout(() => { 15 | setNum(num - 1) 16 | }, 1000) 17 | } else { 18 | // @ts-ignore 19 | window.sendToParent('hello') 20 | window.close() 21 | } 22 | return () => { 23 | timer && clearTimeout(timer) 24 | } 25 | }, [num]) 26 | 27 | return
子窗口 {num} 秒之后,请看主窗口
28 | } 29 | -------------------------------------------------------------------------------- /playground/page/browser-demo/communication-part2/main.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useEffect } from 'react' 2 | import style from '../style.module.less' 3 | 4 | export default function Communication(): ReactElement { 5 | 6 | useEffect(() => { 7 | document.title = '父窗口' 8 | }, []) 9 | 10 | return ( 11 |
12 | 13 | 通过a标签target=__blank打开新的窗口 14 | 15 |
{ 17 | window.open('http://www.github.com') 18 | }}> 19 | 通过window.open打开新的窗口 20 |
21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /playground/page/browser-demo/communication-part3/bottom-input.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Input, Button } from 'antd' 3 | import style from './style.module.less' 4 | import useArticle from './use-article' 5 | 6 | interface IBottomInputProps { 7 | article?: string[] 8 | buttonText?: string 9 | onSend(text: string): void 10 | } 11 | 12 | const BottomInput: React.FunctionComponent = props => { 13 | const { buttonText = '发送', onSend, article = [] } = props 14 | const [value, setValue] = useState('') 15 | const {text, nextLine} = useArticle(article) 16 | 17 | const onSubmit = () => { 18 | onSend(value || text) 19 | setValue('') 20 | nextLine() 21 | } 22 | 23 | return ( 24 |
25 | setValue(e.target.value)} value={value} /> 26 | 27 |
28 | ) 29 | } 30 | 31 | export default BottomInput 32 | -------------------------------------------------------------------------------- /playground/page/browser-demo/communication-part3/client.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import MessageBox from './message-box' 3 | import BottomInput from './bottom-input' 4 | 5 | import style from './style.module.less' 6 | import { tengwanggexu } from './use-article' 7 | 8 | interface IState { 9 | messages: string[] 10 | } 11 | 12 | export default class Communication3Child extends Component{ 13 | constructor(props: any) { 14 | super(props) 15 | this.state = { 16 | messages: [], 17 | } 18 | } 19 | 20 | componentDidMount() { 21 | window.addEventListener('message', this.onMessage) 22 | } 23 | 24 | componentWillUnmount() { 25 | window.removeEventListener('message', this.onMessage) 26 | } 27 | 28 | onMessage = (e: MessageEvent) => { 29 | const { messages } = this.state 30 | const newMessages = [...messages, e.data] 31 | this.setState({ messages: newMessages }) 32 | } 33 | 34 | sendToParent = (text: string) => { 35 | const { opener } = window 36 | opener.postMessage(text, '*') 37 | } 38 | 39 | public render() { 40 | const { messages } = this.state 41 | 42 | return ( 43 |
44 |

子窗口

45 | 46 | 47 |
48 | ) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /playground/page/browser-demo/communication-part3/main.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Button } from 'antd' 3 | 4 | import style from './style.module.less' 5 | import BottomInput from './bottom-input' 6 | import MessageBox from './message-box' 7 | import { yueyanglouji } from './use-article' 8 | 9 | interface IState { 10 | messages: string[] 11 | childWindow: Window | null 12 | } 13 | 14 | export default class Communication3Parent extends Component { 15 | constructor(props: any) { 16 | super(props) 17 | this.state = { 18 | messages: [], 19 | childWindow: null, 20 | } 21 | } 22 | 23 | componentDidMount() { 24 | window.addEventListener('message', this.onMessage) 25 | } 26 | 27 | componentWillUnmount() { 28 | window.removeEventListener('message', this.onMessage) 29 | } 30 | 31 | onMessage = (e: MessageEvent) => { 32 | const { messages } = this.state 33 | const newMessages = [...messages, e.data] 34 | this.setState({ messages: newMessages }) 35 | } 36 | 37 | openPage = () => { 38 | const { childWindow } = this.state 39 | childWindow?.close() 40 | 41 | const newChildWindow = window.open( 42 | `${window.location.pathname}#demo/communication-part3/client`, 43 | ) 44 | this.setState({ childWindow: newChildWindow }) 45 | } 46 | 47 | sendToChild = (text: string) => { 48 | const { childWindow } = this.state 49 | childWindow?.postMessage(text, '*') 50 | } 51 | 52 | public render() { 53 | const { messages, childWindow } = this.state 54 | 55 | return ( 56 |
57 |

父窗口

58 | {!childWindow ? ( 59 | 62 | ) : ( 63 | <> 64 | 65 | 66 | 67 | )} 68 |
69 | ) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /playground/page/browser-demo/communication-part3/message-box.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from 'react' 2 | import style from './style.module.less' 3 | 4 | interface IMessageBoxProps { 5 | title?: string 6 | messages: string[] 7 | } 8 | 9 | const MessageBox: React.FunctionComponent = props => { 10 | const { title, messages = [] } = props 11 | const contentRef = useRef(null) 12 | 13 | useEffect(()=>{ 14 | if(!contentRef.current) return 15 | contentRef.current.scrollTo({top: contentRef.current.scrollHeight, behavior: 'smooth'}) 16 | },[messages.length]) 17 | 18 | return ( 19 |
20 | {title &&
{title}
} 21 |
22 | {messages.map((m, i) => ( 23 |

{m}

24 | ))} 25 |
26 |
27 | ) 28 | } 29 | 30 | export default MessageBox 31 | -------------------------------------------------------------------------------- /playground/page/browser-demo/communication-part3/style.module.less: -------------------------------------------------------------------------------- 1 | .container{ 2 | padding: 20px; 3 | background-color: white; 4 | height: 100vh; 5 | } 6 | 7 | .footer{ 8 | display: flex; 9 | position: absolute; 10 | bottom: 0; 11 | left: 0; 12 | width: 100%; 13 | padding: 10px; 14 | border-top: 1px solid #e3e3e3; 15 | button{ 16 | margin-left: 10px; 17 | } 18 | } 19 | 20 | .message-box{ 21 | border: 1px solid #c3c3c3; 22 | border-radius: 4px; 23 | .box-title{ 24 | padding: 2px 8px; 25 | background-color: #c3c3c3; 26 | color: #000; 27 | } 28 | .box-content{ 29 | padding: 4px 8px; 30 | overflow-y: auto; 31 | height: calc(~"100vh - 200px"); 32 | } 33 | } -------------------------------------------------------------------------------- /playground/page/browser-demo/communication-part3/use-article.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | export const yueyanglouji = [ 4 | '庆历四年春', 5 | '滕子京谪守巴陵郡', 6 | '越明年', 7 | '政通人和', 8 | '百废具兴', 9 | '乃重修岳阳楼', 10 | '增其旧制', 11 | '刻唐贤今人诗赋于其上', 12 | '属予作文以记之', 13 | '予观夫巴陵胜状', 14 | '在洞庭一湖', 15 | '衔远山', 16 | '吞长江', 17 | '浩浩汤汤', 18 | '横无际涯', 19 | '朝晖夕阴', 20 | '气象万千', 21 | '此则岳阳楼之大观也', 22 | '前人之述备矣', 23 | '然则北通巫峡', 24 | '南极潇湘', 25 | '迁客骚人', 26 | '多会于此', 27 | '览物之情', 28 | '得无异乎', 29 | '若夫霪雨霏霏', 30 | '连月不开', 31 | '阴风怒号', 32 | '浊浪排空', 33 | '日星隐曜', 34 | '山岳潜形', 35 | '商旅不行', 36 | '樯倾楫摧', 37 | '薄暮冥冥', 38 | '虎啸猿啼', 39 | '登斯楼也', 40 | '则有去国怀乡', 41 | '忧谗畏讥', 42 | '满目萧然', 43 | '感极而悲者矣', 44 | '至若春和景明', 45 | '波澜不惊', 46 | '上下天光', 47 | '一碧万顷', 48 | '沙鸥翔集', 49 | '锦鳞游泳', 50 | '岸芷汀兰', 51 | '郁郁青青', 52 | '而或长烟一空', 53 | '皓月千里', 54 | '浮光跃金', 55 | '静影沉璧', 56 | '渔歌互答', 57 | '此乐何极', 58 | '登斯楼也', 59 | '则有心旷神怡', 60 | '宠辱皆忘', 61 | '把酒临风', 62 | '其喜洋洋者矣', 63 | '嗟夫', 64 | '予尝求古仁人之心', 65 | '或异二者之为', 66 | '何哉', 67 | '不以物喜', 68 | '不以己悲', 69 | '居庙堂之高', 70 | '则忧其民', 71 | '处江湖之远', 72 | '则忧其君', 73 | '是进亦忧', 74 | '退亦忧', 75 | '然则何时而乐耶', 76 | '其必曰', 77 | '先天下之忧而忧', 78 | '后天下之乐而乐', 79 | '欤', 80 | '噫', 81 | '微斯人', 82 | '吾谁与归', 83 | '时六年九月十五日', 84 | ] 85 | 86 | export const tengwanggexu = [ 87 | '豫章故郡', 88 | '洪都新府', 89 | '星分翼轸', 90 | '地接衡庐', 91 | '襟三江而带五湖', 92 | '控蛮荆而引瓯越', 93 | '物华天宝', 94 | '龙光射牛斗之墟', 95 | '人杰地灵', 96 | '徐孺下陈蕃之榻', 97 | '雄州雾列', 98 | '俊采星驰', 99 | '台隍枕夷夏之交', 100 | '宾主尽东南之美', 101 | '都督阎公之雅望', 102 | '棨戟遥临', 103 | '宇文新州之懿范', 104 | '襜帷暂驻', 105 | '十旬休假', 106 | '胜友如云', 107 | '千里逢迎', 108 | '高朋满座', 109 | '腾蛟起凤', 110 | '孟学士之词宗', 111 | '紫电青霜', 112 | '王将军之武库', 113 | '家君作宰', 114 | '路出名区', 115 | '童子何知', 116 | '躬逢胜饯', 117 | '时维九月', 118 | '序属三秋', 119 | '潦水尽而寒潭清', 120 | '烟光凝而暮山紫', 121 | '俨骖𬴂于上路', 122 | '访风景于崇阿', 123 | '临帝子之长洲', 124 | '得天人之旧馆', 125 | '层峦耸翠', 126 | '上出重霄', 127 | '飞阁流丹', 128 | '下临无地', 129 | '鹤汀凫渚', 130 | '穷岛屿之萦回', 131 | '桂殿兰宫', 132 | '即冈峦之体势', 133 | '披绣闼', 134 | '俯雕甍', 135 | '山原旷其盈视', 136 | '川泽纡其骇瞩', 137 | '闾阎扑地', 138 | '钟鸣鼎食之家', 139 | '舸舰弥津', 140 | '青雀黄龙之舳', 141 | '云销雨霁', 142 | '彩彻区明', 143 | '落霞与孤鹜齐飞', 144 | '秋水共长天一色', 145 | '渔舟唱晚', 146 | '响穷彭蠡之滨', 147 | '雁阵惊寒', 148 | '声断衡阳之浦', 149 | '遥襟甫畅', 150 | '逸兴遄飞', 151 | '爽籁发而清风生', 152 | '纤歌凝而白云遏', 153 | '睢园绿竹', 154 | '气凌彭泽之樽', 155 | '邺水朱华', 156 | '光照临川之笔', 157 | '四美具', 158 | '二难并', 159 | '穷睇眄于中天', 160 | '极娱游于暇日', 161 | '天高地迥', 162 | '觉宇宙之无穷', 163 | '兴尽悲来', 164 | '识盈虚之有数', 165 | '望长安于日下', 166 | '目吴会于云间', 167 | '地势极而南溟深', 168 | '天柱高而北辰远', 169 | '关山难越', 170 | '谁悲失路之人', 171 | '萍水相逢', 172 | '尽是他乡之客', 173 | '怀帝阍而不见', 174 | '奉宣室以何年', 175 | '嗟乎!时运不齐', 176 | '命途多舛', 177 | '冯唐易老', 178 | '李广难封', 179 | '屈贾谊于长沙', 180 | '非无圣主', 181 | '窜梁鸿于海曲', 182 | '岂乏明时', 183 | '所赖君子见机', 184 | '达人知命', 185 | '老当益壮', 186 | '宁移白首之心', 187 | '穷且益坚', 188 | '不坠青云之志', 189 | '酌贪泉而觉爽', 190 | '处涸辙以犹欢', 191 | '北海虽赊', 192 | '扶摇可接', 193 | '东隅已逝', 194 | '桑榆非晚', 195 | '孟尝高洁', 196 | '空余报国之情', 197 | '阮籍猖狂', 198 | '岂效穷途之哭', 199 | '勃', 200 | '三尺微命', 201 | '一介书生', 202 | '无路请缨', 203 | '等终军之弱冠', 204 | '有怀投笔', 205 | '慕宗悫之长风', 206 | '舍簪笏于百龄', 207 | '奉晨昏于万里', 208 | '非谢家之宝树', 209 | '接孟氏之芳邻', 210 | '他日趋庭', 211 | '叨陪鲤对', 212 | '今兹捧袂', 213 | '喜托龙门', 214 | '杨意不逢', 215 | '抚凌云而自惜', 216 | '钟期既遇', 217 | '奏流水以何惭', 218 | '呜乎!胜地不常', 219 | '盛筵难再', 220 | '兰亭已矣', 221 | '梓泽丘墟', 222 | '临别赠言', 223 | '幸承恩于伟饯', 224 | '登高作赋', 225 | '是所望于群公', 226 | '敢竭鄙怀', 227 | '恭疏短引', 228 | '一言均赋', 229 | '四韵俱成', 230 | '请洒潘江', 231 | '各倾陆海云尔', 232 | '滕王高阁临江渚', 233 | '佩玉鸣鸾罢歌舞', 234 | '画栋朝飞南浦云', 235 | '珠帘暮卷西山雨', 236 | '闲云潭影日悠悠', 237 | '物换星移几度秋', 238 | '阁中帝子今何在', 239 | '槛外长江空自流', 240 | ] 241 | 242 | export default function useArticle(article: string[]) { 243 | const [line, setLine] = useState(0) 244 | const [text, setText] = useState(article[0] || '') 245 | 246 | const nextLine = () => { 247 | if (line + 1 > article.length) { 248 | setText('') 249 | return 250 | } 251 | setLine(line + 1) 252 | setText(article[line + 1]) 253 | } 254 | 255 | return { 256 | text, 257 | nextLine, 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /playground/page/browser-demo/demo-window-type/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react' 2 | import styles from './style.module.less' 3 | 4 | export default function Frameless(): ReactElement { 5 | return ( 6 |
7 |
这个是标题
8 | 无边框窗口 9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /playground/page/browser-demo/demo-window-type/style.module.less: -------------------------------------------------------------------------------- 1 | .frame { 2 | background-color: #abcdef; 3 | font-size: 20px; 4 | text-align: center; 5 | margin-top: 100px; 6 | position: relative; 7 | .title { 8 | position: absolute; 9 | left: 0; 10 | top: 0; 11 | width: 100%; 12 | height: 32px; 13 | background-color: #868a8f; 14 | color: #fff; 15 | line-height: 32px; 16 | 17 | -webkit-user-drag: none; 18 | -webkit-app-region: drag; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /playground/page/browser-demo/full-screen/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useState } from 'react' 2 | import styles from '../style.module.less' 3 | 4 | function fullScreen() { 5 | const element = document.documentElement 6 | if (element.requestFullscreen) { 7 | element.requestFullscreen() 8 | // @ts-ignore 9 | } else if (element.msRequestFullscreen) { 10 | // @ts-ignore 11 | element.msRequestFullscreen() 12 | // @ts-ignore 13 | } else if (element.mozRequestFullScreen) { 14 | // @ts-ignore 15 | element.mozRequestFullScreen() 16 | // @ts-ignore 17 | } else if (element.webkitRequestFullscreen) { 18 | // @ts-ignore 19 | element.webkitRequestFullscreen() 20 | } 21 | } 22 | 23 | function exitFullscreen() { 24 | if (document.exitFullscreen) { 25 | document.exitFullscreen() 26 | // @ts-ignore 27 | } else if (document.msExitFullscreen) { 28 | // @ts-ignore 29 | document.msExitFullscreen() 30 | // @ts-ignore 31 | } else if (document.mozCancelFullScreen) { 32 | // @ts-ignore 33 | document.mozCancelFullScreen() 34 | // @ts-ignore 35 | } else if (document.webkitExitFullscreen) { 36 | // @ts-ignore 37 | document.webkitExitFullscreen() 38 | } 39 | } 40 | 41 | function isFull(): boolean { 42 | // @ts-ignore 43 | const { webkitIsFullScreen, mozFullScreen, msFullscreenElement, fullscreenElement } = document 44 | return !!(webkitIsFullScreen || mozFullScreen || msFullscreenElement || fullscreenElement) 45 | } 46 | 47 | export default function FullScreen(): ReactElement { 48 | const [text, setText] = useState<'全屏' | '退出全屏'>('全屏') 49 | 50 | const handleFullScreen = () => { 51 | const flag = isFull() 52 | setText(flag ? '全屏' : '退出全屏') 53 | return flag ? exitFullscreen() : fullScreen() 54 | } 55 | 56 | const handleClick = () => { 57 | // @ts-ignore 58 | const { setFullScreen, isFullScreen } = window 59 | setFullScreen(!isFullScreen()) 60 | } 61 | 62 | return ( 63 |
64 |

全屏和恢复的示例窗口

65 |
66 | {text}(通过HTML api 实现的按钮) 67 |
68 | 69 |
70 | {text}(通过 window.setFullScreen 实现的按钮) 71 |
72 |
73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /playground/page/browser-demo/style.module.less: -------------------------------------------------------------------------------- 1 | .wrap { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | padding: 30px; 6 | a { 7 | width: 100%; 8 | height: 100px; 9 | line-height: 100px; 10 | text-align: center; 11 | border: 1px solid #1986f4; 12 | border-radius: 5px; 13 | margin-top: 20px; 14 | margin-bottom: 20px; 15 | } 16 | a:hover, 17 | div:hover { 18 | background-color: #eeeeee; 19 | } 20 | 21 | div { 22 | width: 100%; 23 | height: 100px; 24 | line-height: 100px; 25 | text-align: center; 26 | border: 1px solid #1986f4; 27 | border-radius: 5px; 28 | margin-top: 20px; 29 | margin-bottom: 20px; 30 | cursor: pointer; 31 | color: #1986f4; 32 | } 33 | } 34 | 35 | .countDown { 36 | height: 100vh; 37 | text-align: center; 38 | line-height: 100vh; 39 | color: #f40; 40 | } 41 | 42 | .close { 43 | background-color: #f1f4f7; 44 | height: 100%; 45 | width: 100%; 46 | 47 | h3 { 48 | text-align: center; 49 | } 50 | .model { 51 | margin: 0 auto; 52 | width: 300px; 53 | height: 200px; 54 | background-color: #fff; 55 | border-radius: 4px; 56 | border: 1px solid #eee; 57 | box-shadow: 2px 2px 2px 2px #ccc; 58 | padding: 30px; 59 | .bg, 60 | img { 61 | background-color: #fff; 62 | width: 100%; 63 | height: 50px; 64 | } 65 | 66 | // .bg { 67 | // background-image: url( 68 | // https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=1297937920,2807167311&fm=26&gp=0.jpg 69 | // ); 70 | // } 71 | .confirm { 72 | display: flex; 73 | height: 50px; 74 | justify-content: space-around; 75 | margin-top: 20px; 76 | .ok { 77 | width: 100px; 78 | height: 50px; 79 | background-color: #ccc; 80 | text-align: center; 81 | line-height: 50px; 82 | border-radius: 4px; 83 | cursor: pointer; 84 | } 85 | .cancel { 86 | width: 100px; 87 | height: 50px; 88 | border: 1px solid #ccc; 89 | text-align: center; 90 | line-height: 50px; 91 | border-radius: 4px; 92 | cursor: pointer; 93 | } 94 | } 95 | } 96 | } 97 | 98 | .full-screen{ 99 | .btn{ 100 | height: 40px; 101 | background-color: #abcded; 102 | border-radius: 3px; 103 | cursor: pointer; 104 | color: #000; 105 | line-height: 40px; 106 | text-align: center; 107 | margin-top: 20px; 108 | } 109 | } -------------------------------------------------------------------------------- /playground/page/browser-demo/window-close/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useState, useEffect } from 'react' 2 | import styles from '../style.module.less' 3 | 4 | export default function BeforeCloseModalDemo(): ReactElement { 5 | const [show, setShow] = useState(false) 6 | 7 | const beforeUnload = (e: BeforeUnloadEvent) => { 8 | setShow(true) 9 | e.returnValue = false 10 | } 11 | 12 | const onCancel = () => { 13 | setShow(false) 14 | } 15 | 16 | const onOk = () => { 17 | window.onbeforeunload = null 18 | window.close() 19 | // setShow(true) 20 | } 21 | useEffect(() => { 22 | window.onbeforeunload = beforeUnload 23 | return () => { 24 | window.onbeforeunload = null 25 | } 26 | }, []) 27 | 28 | return ( 29 |
30 |

关闭这个窗口之前,会出现弹窗。

31 | {show && ( 32 |
33 |
Do you really want to leave?
34 | 35 | 39 |
40 |
41 | YES! 42 |
43 |
44 | NO! 45 |
46 | {/*
*/} 47 |
48 |
49 | )} 50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /playground/page/download-manager/components/download-item.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tooltip } from 'antd' 3 | import { 4 | PauseOutlined, 5 | CaretRightOutlined, 6 | FolderViewOutlined, 7 | CloseOutlined, 8 | } from '@ant-design/icons' 9 | 10 | import IconButton from './icon-button' 11 | import { IDownloadFile } from '../../../../app/file-manager/interface' 12 | 13 | import styles from './style.module.less' 14 | 15 | interface DownloadProps { 16 | item: IDownloadFile 17 | index: number 18 | onOpenFile?: (path: string) => void 19 | onPauseOrResume?: (item: IDownloadFile) => void 20 | onOpenFolder?: (path: string) => void 21 | onCancel?: (item: IDownloadFile, index: number) => void 22 | } 23 | 24 | /** 25 | * 处理文件大小 26 | * @param bytes - 字节 27 | * @param isUnit - 是否需要单位,默认 `true` 28 | */ 29 | export const getFileSize = (bytes: number, isUnit?: boolean): string => { 30 | const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] 31 | isUnit = isUnit ?? true 32 | 33 | if (bytes === 0) { 34 | return isUnit ? '0B' : '0' 35 | } 36 | 37 | const i = Math.floor(Math.log(bytes) / Math.log(1024)) 38 | if (i === 0) { 39 | return bytes + (isUnit ? sizes[i] : '') 40 | } 41 | return (bytes / 1024 ** i).toFixed(2) + (isUnit ? sizes[i] : '') 42 | } 43 | 44 | const DownloadItem = ({ 45 | item, 46 | index, 47 | onOpenFile, 48 | onPauseOrResume, 49 | onOpenFolder, 50 | onCancel, 51 | }: DownloadProps) => { 52 | return ( 53 |
54 | {/* 下载进度 */} 55 | {item.state === 'progressing' && ( 56 |
60 | )} 61 | 62 |
63 | {/* 下载项的图标 */} 64 |
onOpenFile?.(item.path)}> 65 | 66 |
67 | {/* 文件名、下载大小、速度 */} 68 |
69 | 70 |

{item.fileName}

71 |
72 |
73 | {item.state === 'progressing' ? ( 74 | <> 75 |
76 | {getFileSize(item.receivedBytes, false)}/{getFileSize(item.totalBytes)} 77 |
78 | {getFileSize(item.speed)}/s 79 | 80 | ) : null} 81 | {item.state === 'completed' &&

{getFileSize(item.totalBytes)}

} 82 |
83 |
84 | {/* 操作 */} 85 |
86 | {item.state === 'progressing' && ( 87 | onPauseOrResume?.(item)}> 91 | {item.paused ? : } 92 | 93 | )} 94 | 95 | {item.state === 'completed' && ( 96 | onOpenFolder?.(item.path)}> 100 | 101 | 102 | )} 103 | 104 | onCancel?.(item, index)}> 108 | 109 | 110 |
111 |
112 |
113 | ) 114 | } 115 | 116 | export default DownloadItem 117 | -------------------------------------------------------------------------------- /playground/page/download-manager/components/icon-button.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react' 2 | import { Tooltip } from 'antd' 3 | 4 | interface IconButtonProps { 5 | className?: string 6 | title: string 7 | onClick?: (...agrs: any) => void 8 | } 9 | 10 | const IconButton: FunctionComponent = ({ 11 | className = '', 12 | title, 13 | children, 14 | onClick 15 | }) => { 16 | return ( 17 | 18 | 19 | {children} 20 | 21 | 22 | ) 23 | } 24 | 25 | export default IconButton 26 | -------------------------------------------------------------------------------- /playground/page/download-manager/components/manager-menu.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button } from 'antd' 3 | 4 | import { PlusOutlined, DeleteOutlined } from '@ant-design/icons' 5 | 6 | import styles from './style.module.less' 7 | 8 | interface DownloadManagerMenuProps { 9 | onCreate?: (event: React.MouseEvent) => void 10 | onClear?: (event: React.MouseEvent) => void 11 | } 12 | 13 | const DownloadManagerMenu = ({ onCreate, onClear }: DownloadManagerMenuProps) => { 14 | return ( 15 |
16 | 19 | 22 |
23 | ) 24 | } 25 | 26 | export default DownloadManagerMenu 27 | -------------------------------------------------------------------------------- /playground/page/download-manager/components/style.module.less: -------------------------------------------------------------------------------- 1 | .menu-container { 2 | border-bottom: 1px solid #eee; 3 | display: flex; 4 | flex-wrap: wrap; 5 | } 6 | 7 | 8 | .download-item-container { 9 | padding: 10px 15px; 10 | position: relative; 11 | -webkit-app-region: no-drag; 12 | 13 | &:not(:last-child) { 14 | border-bottom: 1px solid #eee; 15 | } 16 | 17 | .download-item-progress { 18 | background-color: #e6f7ff; 19 | position: absolute; 20 | left: 0; 21 | top: 0; 22 | max-width: 100%; 23 | height: 100%; 24 | transition: width linear 0.3s; 25 | } 26 | 27 | .download-item-main { 28 | display: flex; 29 | align-items: center; 30 | position: relative; 31 | z-index: 1; 32 | } 33 | 34 | .file-icon { 35 | width: 32px; 36 | height: 32px; 37 | } 38 | 39 | .file-info { 40 | flex: 1; 41 | padding: 0 10px; 42 | overflow: hidden; 43 | 44 | p { 45 | margin: 0; 46 | } 47 | } 48 | 49 | .file-name { 50 | font-weight: bold; 51 | text-overflow: ellipsis; 52 | white-space: nowrap; 53 | overflow: hidden; 54 | } 55 | 56 | .file-desc { 57 | color: #999; 58 | display: flex; 59 | font-size: 14px; 60 | 61 | .file-size { 62 | flex: 1; 63 | } 64 | 65 | .download-speed { 66 | flex: 1; 67 | } 68 | } 69 | 70 | .operating { 71 | .operating-item { 72 | &:not(:last-child) { 73 | margin-right: 10px; 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /playground/page/download-manager/create.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useMemo } from 'react' 2 | import { Form, Input, message, Modal } from 'antd' 3 | import { EllipsisOutlined } from '@ant-design/icons' 4 | 5 | import { 6 | retryDownloadFile, 7 | getDownloadPath, 8 | newDownloadFile, 9 | openFileDialog, 10 | } from './ipc-renderer' 11 | import { INewDownloadFile } from '../../../app/file-manager/interface' 12 | 13 | interface CreateModalProps { 14 | show: boolean 15 | onClose?: () => void 16 | } 17 | 18 | const CreateModal = ({ show, onClose }: CreateModalProps) => { 19 | const [showCreate, setShowCreate] = useState(show) 20 | const [formData, setFormData] = useState({ 21 | url: '', 22 | path: '', 23 | }) 24 | 25 | const disabled = useMemo(() => !(formData.url && formData.path), [formData.url, formData.path]) 26 | 27 | // 获取光标,选中内容 28 | const handleFocus = (event: React.FocusEvent) => { 29 | event.target.select() 30 | } 31 | 32 | // 设置表单值 33 | const handleFormChange = (field: string, data?: string) => { 34 | setFormData({ 35 | ...formData, 36 | [field]: data, 37 | }) 38 | } 39 | 40 | // 下载开始 41 | const handleOk = async () => { 42 | if (!/^(http(s?)|ftp|blob):|data:.*;base64/.test(formData.url)) { 43 | message.error('下载地址只支持 http、ftp、base64、blob 协议') 44 | return 45 | } 46 | 47 | const item = await newDownloadFile(formData) 48 | if (!item) return 49 | 50 | Modal.confirm({ 51 | content: ( 52 |

53 | 已存在{item.fileName}文件,确认覆盖? 54 |

55 | ), 56 | okText: '确认', 57 | cancelText: '取消', 58 | okButtonProps: { 59 | type: 'default', 60 | }, 61 | cancelButtonProps: { 62 | type: 'primary', 63 | }, 64 | onOk: () => { 65 | retryDownloadFile(item) 66 | }, 67 | }) 68 | } 69 | 70 | // 关闭新建对话框 71 | const handleCancel = () => { 72 | setShowCreate(false) 73 | onClose?.() 74 | } 75 | 76 | // 选择保存位置 77 | const handleChoosePath = async () => { 78 | const newPath = await openFileDialog(formData.path || '') 79 | 80 | setFormData({ 81 | ...formData, 82 | path: newPath, 83 | }) 84 | handleFormChange('path', newPath) 85 | } 86 | 87 | useEffect(() => { 88 | setShowCreate(show) 89 | 90 | return () => { 91 | setFormData({ 92 | url: '', 93 | fileName: '', 94 | path: getDownloadPath(), 95 | }) 96 | } 97 | }, [show]) 98 | 99 | return ( 100 | 109 |
110 | 111 | handleFormChange('url', e.target.value)} 115 | onFocus={handleFocus} 116 | /> 117 | 118 | 119 | handleFormChange('fileName', e.target.value)} 122 | onFocus={handleFocus} 123 | /> 124 | 125 | 126 | } 130 | onClick={handleChoosePath} 131 | /> 132 | 133 |
134 |
135 | ) 136 | } 137 | 138 | export default CreateModal 139 | -------------------------------------------------------------------------------- /playground/page/download-manager/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useRef, useState } from 'react' 2 | import { message, Modal } from 'antd' 3 | 4 | import CreateModal from './create' 5 | import styles from './style.module.less' 6 | 7 | import DownloadManagerMenu from './components/manager-menu' 8 | import DownloadItem from './components/download-item' 9 | import { IDownloadFile } from '../../../app/file-manager/interface' 10 | import { 11 | clearDownloadDone, 12 | getDownloadData, 13 | listenerDownloadItemDone, 14 | listenerDownloadItemUpdate, 15 | listenerNewDownloadItem, 16 | openFile, 17 | openFileInFolder, 18 | pauseOrResume, 19 | removeDownloadItem, 20 | } from './ipc-renderer' 21 | 22 | const DownloadManager = () => { 23 | const [show, setShow] = useState(false) 24 | const [downloadItem, setDownloadItem] = useState([]) 25 | const [hasMore, setHasMore] = useState(true) 26 | 27 | const downloadItemRef = useRef([]) 28 | const pageIndex = useRef(1) 29 | const pageCount = useRef(6) 30 | 31 | // 初始化下载数据 32 | const initData = useCallback(async () => { 33 | const data = await getDownloadData({ 34 | pageIndex: pageIndex.current, 35 | pageCount: pageCount.current, 36 | }) 37 | 38 | if (!data.length) { 39 | pageIndex.current -= 1 40 | setHasMore(false) 41 | return 42 | } 43 | 44 | downloadItemRef.current.push(...data) 45 | setDownloadItem(downloadItemRef.current) 46 | setHasMore(true) 47 | }, []) 48 | 49 | // 更新下载数据 50 | const handleUpdateData = useCallback((item: IDownloadFile) => { 51 | const index = downloadItemRef.current.findIndex(d => d.id === item.id) 52 | 53 | if (index < 0) { 54 | downloadItemRef.current.unshift(item) 55 | } else { 56 | downloadItemRef.current[index] = item 57 | } 58 | 59 | setDownloadItem([...downloadItemRef.current]) 60 | }, []) 61 | 62 | // 滚动到底部自动加载更多 63 | const handleScroll = (event: any) => { 64 | // 滚动条的总高度,可视区的高度,距离顶部的距离 65 | const { scrollHeight, clientHeight, scrollTop } = event.target 66 | 67 | if (scrollTop + clientHeight + 10 >= scrollHeight && hasMore) { 68 | // 滚动到底部 69 | pageIndex.current += 1 70 | initData() 71 | } 72 | } 73 | 74 | // 打开新建下载弹框 75 | const handleOpenCreate = () => { 76 | setShow(true) 77 | } 78 | 79 | // 关闭新建下载弹框 80 | const handleCloseCreate = () => { 81 | setShow(false) 82 | } 83 | 84 | // 暂停或恢复下载 85 | const handlePauseOrResume = async (item: IDownloadFile) => { 86 | const data = await pauseOrResume(item) 87 | handleUpdateData(data) 88 | } 89 | 90 | // 双击图标打开文件 91 | const handleOpenFile = async (path: string) => { 92 | const res = await openFile(path) 93 | 94 | if (!res) { 95 | message.error('文件不存在') 96 | } 97 | } 98 | 99 | // 打开文件所在目录 100 | const handleOpenFolder = async (path: string) => { 101 | const res = await openFileInFolder(path) 102 | 103 | if (!res) { 104 | message.error('文件不存在') 105 | } 106 | } 107 | 108 | // 删除下载项 109 | const handleRemove = (item: IDownloadFile, index: number) => { 110 | Modal.confirm({ 111 | content: `确定${item.state === 'progressing' ? '取消并' : ''}移除下载项吗?`, 112 | okText: '确定', 113 | cancelText: '取消', 114 | onOk: () => { 115 | removeDownloadItem(item, index).finally(() => { 116 | downloadItemRef.current.splice(index, 1) 117 | setDownloadItem([...downloadItemRef.current]) 118 | }) 119 | }, 120 | }) 121 | } 122 | 123 | // 清空已完成 124 | const handleClearDone = () => { 125 | Modal.confirm({ 126 | content: '确定清空已完成的下载吗?', 127 | okText: '确定', 128 | cancelText: '取消', 129 | onOk: async () => { 130 | const data = await clearDownloadDone() 131 | 132 | downloadItemRef.current = data 133 | setDownloadItem([...data]) 134 | }, 135 | }) 136 | } 137 | 138 | useEffect(() => { 139 | listenerNewDownloadItem((event, item: IDownloadFile) => { 140 | handleCloseCreate() 141 | handleUpdateData(item) 142 | }) 143 | 144 | listenerDownloadItemUpdate((event, item: IDownloadFile) => { 145 | handleUpdateData(item) 146 | }) 147 | 148 | listenerDownloadItemDone((event, item: IDownloadFile) => { 149 | handleUpdateData(item) 150 | }) 151 | 152 | initData() 153 | }, [handleUpdateData, initData]) 154 | 155 | return ( 156 | <> 157 |
158 |
下载管理器 - demo
159 | 160 | 161 |
162 | {downloadItem.map((item, index) => ( 163 | 172 | ))} 173 |
174 |
175 | 176 | 177 | 178 | ) 179 | } 180 | 181 | export default DownloadManager 182 | -------------------------------------------------------------------------------- /playground/page/download-manager/ipc-renderer.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer, IpcRendererEvent, remote } from 'electron' 2 | import { 3 | IDownloadFile, 4 | INewDownloadFile, 5 | IPagination, 6 | IPCEventName, 7 | } from '../../../app/file-manager/interface' 8 | 9 | /** 10 | * 添加 ipc 调用监听事件 11 | * @param eventName - ipc 事件名 12 | * @param callback - 回调函数 13 | */ 14 | export const ipcRendererListener = ( 15 | eventName: IPCEventName, 16 | callback: (event: IpcRendererEvent, ...args: any[]) => void, 17 | ): void => { 18 | ipcRenderer.on(eventName, (event, ...args: any[]) => { 19 | callback(event, ...args) 20 | }) 21 | } 22 | 23 | /** 24 | * 调用 ipc 的处理事件 25 | * @param eventName - ipc 事件名 26 | * @param args - 参数 27 | * @returns `Promise` 28 | */ 29 | export const ipcRendererInvoke = (eventName: IPCEventName, ...args: any[]): Promise => 30 | ipcRenderer.invoke(eventName, ...args) 31 | 32 | /** 33 | * 获取下载路径 34 | */ 35 | export const getDownloadPath = (): string => remote.app.getPath('downloads') 36 | 37 | /** 38 | * 打开文件 39 | * @param path - 路径 40 | */ 41 | export const openFile = (path: string): Promise => ipcRendererInvoke('openFile', path) 42 | 43 | /** 44 | * 打开下载管理器 45 | */ 46 | export const openDownloadManager = (): void => { 47 | ipcRendererInvoke('openDownloadManager', '/download-manager/demo') 48 | } 49 | 50 | /** 51 | * 新建下载项 52 | * @param formData - 下载数据 53 | */ 54 | export const newDownloadFile = (formData: INewDownloadFile): Promise => 55 | ipcRendererInvoke('newDownloadFile', formData) 56 | 57 | /** 58 | * 重新下载 59 | */ 60 | export const retryDownloadFile = (item: IDownloadFile): Promise => 61 | ipcRendererInvoke('retryDownloadFile', item) 62 | 63 | /** 64 | * 打开选择保存位置对话框 65 | * @param path - 路径 66 | */ 67 | export const openFileDialog = (path: string): Promise => 68 | ipcRendererInvoke('openFileDialog', path) 69 | 70 | /** 71 | * 暂停或恢复下载 72 | * @param item - 下载项 73 | */ 74 | export const pauseOrResume = (item: IDownloadFile): Promise => 75 | ipcRendererInvoke('pauseOrResume', item) 76 | 77 | /** 78 | * 打开文件所在位置 79 | * @param path - 路径 80 | */ 81 | export const openFileInFolder = (path: string): Promise => 82 | ipcRendererInvoke('openFileInFolder', path) 83 | 84 | /** 85 | * 获取下载数据 86 | * @param page - 分页 87 | */ 88 | export const getDownloadData = (page: IPagination): Promise => 89 | ipcRendererInvoke('getDownloadData', page) 90 | 91 | /** 92 | * 删除下载项。下载中的将先取消,再删除 93 | * @param item - 下载项 94 | * @param index - 下载项的下标 95 | */ 96 | export const removeDownloadItem = (item: IDownloadFile, index: number): Promise => 97 | ipcRendererInvoke('removeDownloadItem', item, index) 98 | 99 | /** 100 | * 清空下载完成项 101 | */ 102 | export const clearDownloadDone = (): Promise => 103 | ipcRendererInvoke('clearDownloadDone') 104 | 105 | /** 106 | * 监听新建下载项事件 107 | * @param callback - 回调函数 108 | */ 109 | export const listenerNewDownloadItem = ( 110 | callback: (event: IpcRendererEvent, ...args: any[]) => void, 111 | ): void => ipcRendererListener('newDownloadItem', callback) 112 | 113 | /** 114 | * 监听下载项更新事件 115 | * @param callback - 回调函数 116 | */ 117 | export const listenerDownloadItemUpdate = ( 118 | callback: (event: IpcRendererEvent, ...args: any[]) => void, 119 | ): void => ipcRendererListener('downloadItemUpdate', callback) 120 | 121 | /** 122 | * 监听下载项完成事件 123 | * @param callback - 回调函数 124 | */ 125 | export const listenerDownloadItemDone = ( 126 | callback: (event: IpcRendererEvent, ...args: any[]) => void, 127 | ): void => ipcRendererListener('downloadItemDone', callback) 128 | -------------------------------------------------------------------------------- /playground/page/download-manager/style.module.less: -------------------------------------------------------------------------------- 1 | .container { 2 | background-color: #fff; 3 | height: 100%; 4 | overflow: hidden; 5 | display: flex; 6 | flex-direction: column; 7 | -webkit-app-region: drag; 8 | 9 | a, 10 | button, 11 | p, 12 | label, 13 | span, 14 | input { 15 | -webkit-app-region: no-drag; 16 | } 17 | 18 | .title { 19 | text-align: center; 20 | font-weight: bold; 21 | } 22 | 23 | .main { 24 | overflow-y: auto; 25 | flex: 1; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /playground/page/editor/editor-operate.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-curly-brace-presence */ 2 | import React, { ReactElement, useState, useRef } from 'react' 3 | import { Button, Dropdown, Menu } from 'antd' 4 | import { MosaicNode } from 'react-mosaic-component' 5 | import { 6 | CaretRightOutlined, 7 | FileTextOutlined, 8 | LoadingOutlined, 9 | EyeInvisibleOutlined, 10 | EyeOutlined, 11 | } from '@ant-design/icons' 12 | 13 | import { ChildProcess, createMosaicArrangement, getVisibleMosaics } from './utils' 14 | import styles from './style.module.less' 15 | import { EditorId } from '.' 16 | 17 | interface IProp { 18 | handleExec: () => Promise 19 | layout: MosaicNode 20 | setLayout: (layout: MosaicNode) => void 21 | } 22 | 23 | interface MProp { 24 | layout: MosaicNode 25 | setLayout: (layout: MosaicNode) => void 26 | } 27 | 28 | const WrapMenu = function ({ layout, setLayout }: MProp) { 29 | const visibleMosaics = getVisibleMosaics(layout) 30 | const menuItem = ['main.js', 'renderer.js', 'index.html', 'preload.js'] 31 | 32 | const onItemClick = (str: string) => { 33 | return () => { 34 | let newMosaicsArrangement: Array 35 | 36 | if (visibleMosaics.includes(str as EditorId)) { 37 | newMosaicsArrangement = visibleMosaics.filter(i => i !== str) 38 | } else { 39 | newMosaicsArrangement = [...visibleMosaics, str as EditorId] 40 | } 41 | setLayout(createMosaicArrangement(newMosaicsArrangement)) 42 | } 43 | } 44 | 45 | return ( 46 | 47 | {menuItem.map(item => ( 48 | : 54 | }> 55 | {item} 56 | 57 | ))} 58 | 59 | ) 60 | } 61 | 62 | export default function EditorOperate({ handleExec, setLayout, layout }: IProp): ReactElement { 63 | const [exec, setExec] = useState<'RUN' | 'STOP'>('RUN') 64 | 65 | const childRef = useRef() 66 | 67 | const wrapExec = async () => { 68 | if (childRef.current) { 69 | childRef.current.kill() 70 | // @ts-ignore 71 | childRef.current = null 72 | } else { 73 | const child = await handleExec() 74 | // @ts-ignore 75 | childRef.current = child 76 | setExec('STOP') 77 | child?.on('close', code => { 78 | setExec('RUN') 79 | }) 80 | } 81 | } 82 | 83 | return ( 84 |
85 |
86 | 89 | 95 |
96 |
97 | }> 98 | 101 | 102 |
103 |
104 | ) 105 | } 106 | -------------------------------------------------------------------------------- /playground/page/editor/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable quotes */ 2 | import React, { useState, useEffect, useLayoutEffect } from 'react' 3 | import { Mosaic, MosaicWindow, MosaicZeroState, MosaicNode } from 'react-mosaic-component' 4 | import MonacoEditor from 'components/monaco-editor' 5 | import Title from './editor-operate' 6 | 7 | import 'react-mosaic-component/react-mosaic-component.css' 8 | import '@blueprintjs/core/lib/css/blueprint.css' 9 | import '@blueprintjs/icons/lib/css/blueprint-icons.css' 10 | 11 | import styles from './style.module.less' 12 | import { saveToTemp, Files, execute, getFiles, ChildProcess } from './utils' 13 | 14 | const defaultOptions = { 15 | scrollbar: { alwaysConsumeMouseWheel: true }, 16 | scrollBeyondLastLine: true, 17 | } 18 | 19 | interface WrapEditorProps { 20 | code: string 21 | language: string 22 | onChange: (str: string) => void 23 | } 24 | 25 | export enum EditorId { 26 | 'main.js' = 'main.js', 27 | 'renderer.js' = 'renderer.js', 28 | 'index.html' = 'index.html', 29 | 'preload.js' = 'preload.js', 30 | } 31 | 32 | export const TitleMAP = { 33 | 'main.js': '主进程代码', 34 | 'renderer.js': '渲染进程代码', 35 | 'index.html': '窗口HTML', 36 | 'preload.js': 'preload.js代码', 37 | } 38 | 39 | const LangMAP = { 40 | 'main.js': 'javascript', 41 | 'renderer.js': 'javascript', 42 | 'index.html': 'html', 43 | 'preload.js': 'javascript', 44 | } 45 | 46 | export const DEFAULT_MOSAIC_ARRANGEMENT: MosaicNode = { 47 | direction: 'row', 48 | first: { 49 | direction: 'column', 50 | first: EditorId['main.js'], 51 | second: EditorId['preload.js'], 52 | }, 53 | second: { 54 | direction: 'column', 55 | first: EditorId['renderer.js'], 56 | second: EditorId['index.html'], 57 | }, 58 | } 59 | 60 | const WrapEditor = function ({ code, language, onChange }: WrapEditorProps) { 61 | return ( 62 | 69 | ) 70 | } 71 | 72 | const EditorWindow = function () { 73 | const [layout, setLayout] = useState>(DEFAULT_MOSAIC_ARRANGEMENT) 74 | const [editorFiles, setEditorFiles] = useState() 75 | 76 | const initFiles = async () => { 77 | const files = await getFiles() 78 | setEditorFiles(files) 79 | } 80 | 81 | const handleLayoutChange = (layoutProp: MosaicNode | null) => { 82 | if (layoutProp) { 83 | setLayout(layoutProp) 84 | } 85 | } 86 | 87 | const handleExec = async (): Promise => { 88 | if (editorFiles) { 89 | await saveToTemp(editorFiles) 90 | const child = await execute() 91 | return child 92 | } 93 | return null 94 | } 95 | 96 | const handelEditorChange = (flag: string) => { 97 | return (val: string) => { 98 | setEditorFiles(editorFiles => ({ ...editorFiles, [flag]: val })) 99 | } 100 | } 101 | 102 | useEffect(() => { 103 | initFiles() 104 | }, []) 105 | 106 | if (!editorFiles) return null 107 | 108 | return ( 109 |
110 | 111 | <div className={styles.editor}> 112 | <Mosaic<EditorId> 113 | renderTile={(count, path) => ( 114 | <MosaicWindow<EditorId> title={count} path={path}> 115 | <WrapEditor 116 | code={editorFiles[count] as string} 117 | language={LangMAP[count]} 118 | onChange={handelEditorChange(count)} 119 | /> 120 | </MosaicWindow> 121 | )} 122 | onChange={handleLayoutChange} 123 | value={layout} 124 | zeroStateView={<MosaicZeroState />} 125 | /> 126 | </div> 127 | </div> 128 | ) 129 | } 130 | 131 | export default EditorWindow 132 | -------------------------------------------------------------------------------- /playground/page/editor/style.module.less: -------------------------------------------------------------------------------- 1 | @import (reference) '~@blueprintjs/core/lib/less/variables'; 2 | 3 | .wrapper { 4 | width: 100%; 5 | height: 100vh; 6 | padding: 30px 10px 10px; 7 | .editor { 8 | margin: 0; 9 | width: 100%; 10 | height: calc(100% - 30px); 11 | overflow: hidden; 12 | .window { 13 | width: 100%; 14 | height: 100%; 15 | padding: 20px; 16 | text-align: center; 17 | } 18 | } 19 | 20 | .wrap { 21 | display: flex; 22 | justify-content: space-between; 23 | align-items: center; 24 | .title { 25 | display: flex; 26 | margin-bottom: 10px; 27 | .run, 28 | .console { 29 | margin: 0 3px; 30 | cursor: pointer; 31 | border: 1px solid #abcdef; 32 | padding: 2px 10px; 33 | border-radius: 3px; 34 | &:hover { 35 | background-color: #eee; 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /playground/page/start/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Terminal from 'terminal-in-react' 3 | import style from './style.module.less' 4 | 5 | const Terminal2 = Terminal as any 6 | 7 | interface IStartPageProps {} 8 | 9 | const StartPage: React.FunctionComponent<IStartPageProps> = props => { 10 | const openDoc = () => { 11 | console.log('opening api panel') 12 | window.$EB.openWindow('api') 13 | window.$EB.closeWindow() 14 | } 15 | 16 | const openEditor = () => { 17 | console.log('opening editor panel') 18 | window.$EB.openWindow('editor') 19 | window.$EB.closeWindow() 20 | } 21 | 22 | const handleClose = () => { 23 | console.log('closing window') 24 | window.$EB.closeWindow() 25 | } 26 | 27 | const handleMaximise = () => { 28 | window.$EB.maximizeWindow() 29 | } 30 | 31 | const handleMinimise = () => { 32 | window.$EB.minimizeWindow() 33 | } 34 | 35 | return ( 36 | <div className={style.container}> 37 | <Terminal2 38 | msg={`欢迎来到 Electron Playground! 39 | 40 | 输入"open api" 打开 交互式api文档面板; 41 | 输入"open editor" 打开 演练场。 42 | 43 | 更多请输入"help"`} 44 | actionHandlers={{ 45 | handleClose, 46 | handleMaximise, 47 | handleMinimise, 48 | }} 49 | watchConsoleLogging 50 | commands={{ 51 | open: { 52 | method: (args: any, print: any, runCommand: any) => { 53 | const openType = args._[0] 54 | if (openType === 'api') openDoc() 55 | else if (openType === 'editor') openEditor() 56 | else print(`not valid open type: ${openType}`) 57 | }, 58 | options: [ 59 | { 60 | name: 'open', 61 | description: 'open the target panel', 62 | defaultValue: 'api', 63 | }, 64 | ], 65 | }, 66 | }} 67 | descriptions={{ 68 | open: 'open the target panel', 69 | }} 70 | allowTabs={false} 71 | /> 72 | </div> 73 | ) 74 | } 75 | 76 | export default StartPage 77 | -------------------------------------------------------------------------------- /playground/page/start/style.module.less: -------------------------------------------------------------------------------- 1 | .container { 2 | //height: 100%; 3 | background-color: black; 4 | padding-top: 10px; 5 | min-height:100%; 6 | font-size: 18px; 7 | ::-webkit-scrollbar { 8 | display: none; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /playground/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="node" /> 2 | /// <reference types="react" /> 3 | /// <reference types="react-dom" /> 4 | 5 | declare namespace NodeJS { 6 | interface ProcessEnv { 7 | readonly NODE_ENV: 'development' | 'production' | 'test' 8 | readonly PUBLIC_URL: string 9 | } 10 | } 11 | 12 | declare module '*.bmp' { 13 | const src: string 14 | export default src 15 | } 16 | 17 | declare module '*.gif' { 18 | const src: string 19 | export default src 20 | } 21 | 22 | declare module '*.jpg' { 23 | const src: string 24 | export default src 25 | } 26 | 27 | declare module '*.jpeg' { 28 | const src: string 29 | export default src 30 | } 31 | 32 | declare module '*.png' { 33 | const src: string 34 | export default src 35 | } 36 | 37 | declare module '*.webp' { 38 | const src: string 39 | export default src 40 | } 41 | 42 | declare module '*.svg' { 43 | import * as React from 'react' 44 | 45 | export const ReactComponent: React.FunctionComponent< 46 | React.SVGProps<SVGSVGElement> & { title?: string } 47 | > 48 | 49 | const src: string 50 | export default src 51 | } 52 | 53 | declare module '*.md' { 54 | const src: string 55 | export default src 56 | } 57 | 58 | declare module '*.txt' { 59 | const src: string 60 | export default src 61 | } 62 | 63 | declare module '*.module.css' { 64 | const classes: { readonly [key: string]: string } 65 | export default classes 66 | } 67 | 68 | declare module '*.module.scss' { 69 | const classes: { readonly [key: string]: string } 70 | export default classes 71 | } 72 | 73 | declare module '*.module.sass' { 74 | const classes: { readonly [key: string]: string } 75 | export default classes 76 | } 77 | 78 | declare module '*.module.less' { 79 | const classes: { readonly [key: string]: string } 80 | export default classes 81 | } 82 | -------------------------------------------------------------------------------- /playground/router.tsx: -------------------------------------------------------------------------------- 1 | import React,{lazy,Suspense} from 'react' 2 | import { HashRouter as Router, Switch, Route } from 'react-router-dom' 3 | 4 | export default function router() { 5 | return ( 6 | <Router> 7 | <Suspense fallback={<div>Loading...</div>}> 8 | <Switch> 9 | <Route path="/demo/communication-part1/client" component={lazy(() => import('./page/browser-demo/communication-part1/client'))} /> 10 | <Route path="/demo/communication-part1/main" component={lazy(() => import('./page/browser-demo/communication-part1/main'))} /> 11 | <Route path="/demo/communication-part2/client" component={lazy(() => import('./page/browser-demo/communication-part2/client'))} /> 12 | <Route path="/demo/communication-part2/main" component={lazy(() => import('./page/browser-demo/communication-part2/main'))} /> 13 | <Route path="/demo/communication-part3/client" component={lazy(() => import('./page/browser-demo/communication-part3/client'))} /> 14 | <Route path="/demo/communication-part3/main" component={lazy(() => import('./page/browser-demo/communication-part3/main'))} /> 15 | <Route path="/demo/window-close" component={lazy(() => import('./page/browser-demo/window-close'))} /> 16 | <Route path="/demo/window-type" component={lazy(() => import('./page/browser-demo/demo-window-type'))} /> 17 | <Route path="/demo/full-screen" component={lazy(() => import('./page/browser-demo/full-screen'))} /> 18 | 19 | <Route path="/editor" component={lazy(() => import('./page/editor'))} /> 20 | <Route path="/download-manager/demo" component={lazy(() => import('./page/download-manager'))} /> 21 | <Route path="/start" component={lazy(() => import('./page/start'))} /> 22 | <Route path="/apidoc" component={lazy(() => import('./page/apidoc'))} /> 23 | </Switch> 24 | </Suspense> 25 | </Router> 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /playground/theme.less: -------------------------------------------------------------------------------- 1 | @primary-color: #fa8c16; // 全局主色 2 | 3 | // @primary-color: #ffd630; // 全局主色 4 | 5 | // @btn-primary-color: #1d2126; 6 | // @btn-primary-bg: #ffd630; 7 | // @btn-default-color: #1d2126; 8 | // @btn-default-bg: #ffffff; 9 | // @btn-default-border: #e4e6e8; 10 | // @btn-danger-color: #eb3b3b; 11 | // @btn-danger-bg: #ffffff; 12 | // @link-color: #ff8c00; 13 | 14 | // @link-color: #1890ff; 15 | // @success-color: #52c41a; // 成功色 16 | // @warning-color: #faad14; // 警告色 17 | // @error-color: #f5222d; // 错误色 18 | // @font-size-base: 14px; // 主字号 19 | // @heading-color: #1d2126; // 标题色 20 | // @text-color: #4e5359; // 主文本色 21 | // @text-color-secondary: rgba(0, 0, 0, 0.45); // 次文本色 22 | // @disabled-color: rgba(0, 0, 0, 0.25); // 失效色 23 | // @border-radius-base: 4px; // 组件/浮层圆角 24 | // @border-color-base: #d9d9d9; // 边框色 25 | // @box-shadow-base: 0 2px 8px rgba(0, 0, 0, 0.15); // 浮层阴影 26 | -------------------------------------------------------------------------------- /playground/utils/id.ts: -------------------------------------------------------------------------------- 1 | export const randomId = () => { 2 | return Date.now() + Math.random() * 10 ** 20 3 | } 4 | -------------------------------------------------------------------------------- /playground/utils/path.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | export const ROOT_PATH = process.cwd() 4 | 5 | export const getSrcRelativePath = (relativePath: string) => path.join(ROOT_PATH, relativePath) -------------------------------------------------------------------------------- /resources/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 | <plist version="1.0"> 4 | <dict> 5 | <key>com.apple.security.cs.allow-unsigned-executable-memory</key> 6 | <true/> 7 | <key>com.apple.security.cs.allow-dyld-environment-variables</key> 8 | <true/> 9 | <key>com.apple.security.device.audio-input</key> 10 | <true/> 11 | <key>com.apple.security.device.camera</key> 12 | <true/> 13 | </dict> 14 | </plist> -------------------------------------------------------------------------------- /resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tal-tech/electron-playground/7635a86bfe4de138288e3c73b1674f7ef4338c39/resources/icon.icns -------------------------------------------------------------------------------- /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tal-tech/electron-playground/7635a86bfe4de138288e3c73b1674f7ef4338c39/resources/icon.ico -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tal-tech/electron-playground/7635a86bfe4de138288e3c73b1674f7ef4338c39/resources/icon.png -------------------------------------------------------------------------------- /resources/markdown/window-dependency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tal-tech/electron-playground/7635a86bfe4de138288e3c73b1674f7ef4338c39/resources/markdown/window-dependency.png -------------------------------------------------------------------------------- /resources/markdown/window-event1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tal-tech/electron-playground/7635a86bfe4de138288e3c73b1674f7ef4338c39/resources/markdown/window-event1.png -------------------------------------------------------------------------------- /resources/markdown/window-event2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tal-tech/electron-playground/7635a86bfe4de138288e3c73b1674f7ef4338c39/resources/markdown/window-event2.png -------------------------------------------------------------------------------- /resources/markdown/window-operate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tal-tech/electron-playground/7635a86bfe4de138288e3c73b1674f7ef4338c39/resources/markdown/window-operate.png -------------------------------------------------------------------------------- /resources/readme/01.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tal-tech/electron-playground/7635a86bfe4de138288e3c73b1674f7ef4338c39/resources/readme/01.gif -------------------------------------------------------------------------------- /resources/readme/02.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tal-tech/electron-playground/7635a86bfe4de138288e3c73b1674f7ef4338c39/resources/readme/02.gif -------------------------------------------------------------------------------- /resources/readme/03.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tal-tech/electron-playground/7635a86bfe4de138288e3c73b1674f7ef4338c39/resources/readme/03.gif -------------------------------------------------------------------------------- /resources/readme/wechat-group.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tal-tech/electron-playground/7635a86bfe4de138288e3c73b1674f7ef4338c39/resources/readme/wechat-group.jpeg -------------------------------------------------------------------------------- /resources/tray/StatusIcon_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tal-tech/electron-playground/7635a86bfe4de138288e3c73b1674f7ef4338c39/resources/tray/StatusIcon_dark.png -------------------------------------------------------------------------------- /resources/tray/StatusIcon_dark@1.25x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tal-tech/electron-playground/7635a86bfe4de138288e3c73b1674f7ef4338c39/resources/tray/StatusIcon_dark@1.25x.png -------------------------------------------------------------------------------- /resources/tray/StatusIcon_dark@1.5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tal-tech/electron-playground/7635a86bfe4de138288e3c73b1674f7ef4338c39/resources/tray/StatusIcon_dark@1.5x.png -------------------------------------------------------------------------------- /resources/tray/StatusIcon_dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tal-tech/electron-playground/7635a86bfe4de138288e3c73b1674f7ef4338c39/resources/tray/StatusIcon_dark@2x.png -------------------------------------------------------------------------------- /resources/tray/StatusIcon_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tal-tech/electron-playground/7635a86bfe4de138288e3c73b1674f7ef4338c39/resources/tray/StatusIcon_light.png -------------------------------------------------------------------------------- /resources/tray/StatusIcon_light@1.25x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tal-tech/electron-playground/7635a86bfe4de138288e3c73b1674f7ef4338c39/resources/tray/StatusIcon_light@1.25x.png -------------------------------------------------------------------------------- /resources/tray/StatusIcon_light@1.5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tal-tech/electron-playground/7635a86bfe4de138288e3c73b1674f7ef4338c39/resources/tray/StatusIcon_light@1.5x.png -------------------------------------------------------------------------------- /resources/tray/StatusIcon_light@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tal-tech/electron-playground/7635a86bfe4de138288e3c73b1674f7ef4338c39/resources/tray/StatusIcon_light@2x.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | // https://www.typescriptlang.org/docs/handbook/tsconfig-json.html 2 | { 3 | "compilerOptions": { 4 | "skipLibCheck": true, 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "importHelpers": true, 8 | "target": "es2015", 9 | "jsx": "react", 10 | "esModuleInterop": true, 11 | "sourceMap": true, 12 | "baseUrl": "./", 13 | "inlineSources": true, 14 | "noImplicitAny": true, 15 | "outDir": "./lib", 16 | "strict": true, 17 | "importsNotUsedAsValues": "remove", 18 | "paths": { 19 | "*": ["node_modules/*"], 20 | "app/*": ["app/*"], 21 | "user-config/*": ["user-config/*"], 22 | "resources/*": ["resources/*"], 23 | "src/*": ["src/*"], 24 | "common/*": ["common/*"], 25 | "components/*": ["playground/components/*"], 26 | "page/*": ["playground/page/*"], 27 | "utils/*": ["playground/utils/*"], 28 | "views/*": ["playground/views/*"], 29 | "images/*": ["playground/images/*"] 30 | }, 31 | "allowSyntheticDefaultImports": true, 32 | "experimentalDecorators": true, 33 | "allowJs": true 34 | }, 35 | "include": ["app/**/*", "config/entry.dev.js", "playground/*"], 36 | "exclude": [ 37 | "node_modules", 38 | "packages", 39 | "public", 40 | "mock", 41 | "app/js-sdk/dist", 42 | "**/*.code.ts", 43 | "example/*" 44 | ] 45 | } 46 | --------------------------------------------------------------------------------