commit 9db76c82ec4155b5ab6ac70826aa6d67b6cd8c0c Author: wangxiang <1827945911@qq.com> Date: Sat Apr 27 02:32:14 2024 +0800 init diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..e5b6d8d --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..71d7dac --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", + "changelog": [ + "@changesets/changelog-github", + { + "repo": "027xiguapi/pear-rec" + } + ], + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} \ No newline at end of file diff --git a/.czrc b/.czrc new file mode 100644 index 0000000..e6f6f0b --- /dev/null +++ b/.czrc @@ -0,0 +1,3 @@ +{ + "path": "cz-conventional-changelog" +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2af4448 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf + +[*.{html,css,ts,json,tsx,js,scss}] +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..58aff3d --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,14 @@ +module.exports = { + extends: [require.resolve("@umijs/fabric/dist/eslint")], + + // in antd-design-pro + globals: { + ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true, + page: true, + }, + + rules: { + // your rules + "@typescript-eslint/no-explicit-any": ["off"], + }, +}; diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c8b11ca --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,51 @@ +name: Build +'on': + push: + tags: + - v* +jobs: + build: + name: build and release electron app + runs-on: '${{ matrix.os }}' + strategy: + matrix: + os: [macos-latest, windows-latest, ubuntu-latest] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install pnpm + uses: pnpm/action-setup@v3 + with: + version: 8 + + - name: Install Dependencies + run: pnpm install + + - name: Build Electron App + run: 'pnpm run build:desktop' + env: + GITHUB_TOKEN: '${{ secrets.ACCESS_TOKEN }}' + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: '${{ matrix.os }}' + path: packages/desktop/release + + - name: Release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + files: | + packages/desktop/release/*.AppImage + packages/desktop/release/*.exe + packages/desktop/release/*.dmg + packages/desktop/release/*.yml + env: + GITHUB_TOKEN: '${{ secrets.ACCESS_TOKEN }}' diff --git a/.github/workflows/syncToGitee.yml b/.github/workflows/syncToGitee.yml new file mode 100644 index 0000000..0ed0044 --- /dev/null +++ b/.github/workflows/syncToGitee.yml @@ -0,0 +1,30 @@ +name: syncToGitee +'on': + push: + branches: + - main +jobs: + repo-sync: + runs-on: ubuntu-latest + steps: + - name: Mirror with force push (git push -f) + uses: Yikun/hub-mirror-action@master + with: + src: github/027xiguapi + dst: gitee/xiguapi027 + dst_key: '${{ secrets.GITEE_PRIVATE_KEY }}' + dst_token: '${{ secrets.GITEE_TOKEN }}' + static_list: pear-rec + force_update: true + debug: true + - name: Build Gitee Pages + uses: yanglbme/gitee-pages-action@main + with: + # 注意替换为你的 Gitee 用户名 + gitee-username: xiguapi027 + # 注意在 Settings->Secrets 配置 GITEE_PASSWORD + gitee-password: '${{ secrets.GITEE_PASSWORD }}' + # 注意替换为你的 Gitee 仓库,仓库名严格区分大小写,请准确填写,否则会出错 + gitee-repo: xiguapi027/pear-rec + # 要部署的分支,默认是 master,若是其他分支,则需要指定(指定的分支必须存在) + branch: gh-pages diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6f50d4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +lib +dist-electron +release +*.local +dev-dist + +stats.html + +# Editor directories and files +.vscode +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +#lockfile +package-lock.json +pnpm-lock.yaml +yarn.lock +/test-results/ +/playwright-report/ +/playwright/.cache/ +Pear Files/ + +# docs +cache/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..31cba9b --- /dev/null +++ b/.npmrc @@ -0,0 +1,6 @@ +shamefully-hoist=true + +# 在国内使用pnpm安装electron需要配置一下electron的下载路径 +registry="https://registry.npmmirror.com/" +electron_mirror="https://registry.npmmirror.com/-/binary/electron/" +electron_builder_binaries_mirror="https://registry.npmmirror.com/-/binary/electron-builder-binaries/" diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..90a7f29 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,34 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +packages/*/node_modules +dist +dist-ssr +dist-electron +release +*.local + +# Editor directories and files +.vscode/.debug.env +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +#lockfile +package-lock.json +pnpm-lock.yaml +yarn.lock +/test-results/ +/playwright-report/ +/playwright/.cache/ diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..7b597d7 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,5 @@ +const fabric = require('@umijs/fabric'); + +module.exports = { + ...fabric.prettier, +}; diff --git a/.stylelintrc.js b/.stylelintrc.js new file mode 100644 index 0000000..456ce7d --- /dev/null +++ b/.stylelintrc.js @@ -0,0 +1,6 @@ +module.exports = { + extends: [require.resolve("@umijs/fabric/dist/stylelint")], + rules: { + // your rules + }, +}; diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.de-DE.md b/README.de-DE.md new file mode 100644 index 0000000..7084401 --- /dev/null +++ b/README.de-DE.md @@ -0,0 +1,94 @@ +

+ +

pear-rec

+

+ stars + react + electron + nestjs + typescript + vite +

+

+ +--- + +## README + +[中文](README.zh-CN.md) | [English](README.md) | [Deutsch](README.de-DE.md) + +## Dokumentation + +> pear-rec (pear rec) ist eine plattformübergreifende Software für Bildschirmfotos, Bildschirmaufnahmen, Audio- und Videoaufnahmen. +> +> pear-rec(pear rec) ist ein Projekt basierend auf react + electron + vite + viewerjs + plyr + aplayer + react-screenshots. +> +> Weitere Funktionen und APIs finden Sie auf der offiziellen [Website](https://027xiguapi.github.io/pear-rec). + +## Installation + +``` +gitee: https://gitee.com/xiguapi027/pear-rec +github: https://github.com/027xiguapi/pear-rec +``` + +## Verwendung + +### Erste Schritte + +Bevor Sie dieses Repository klonen und ausführen, müssen Sie [Git](https://git-scm.com), [Node.js](https://nodejs.org/en/download/) (mit [npm](https://www.npmjs.com/)) und [pnpm](https://pnpm.io/) auf Ihrem Computer installiert haben. Kommandozeilenaufrufe: + +```shell +# Clone this repository +git clone https://github.com/027xiguapi/pear-rec.git +# Go into the repository +cd pear-rec +# Install dependencies +pnpm install +# Run the web +pnpm run dev:web +# Run the server +pnpm run dev:server +# Run the desktop +pnpm run dev:desktop +# Run the web +pnpm run start:web +# Run the desktop +pnpm run start:desktop +# Build the web +pnpm run build:web +# Build the desktop +pnpm run build:desktop +# Clear node_modules +pnpm run clear +``` + +## Internationalization(I18n) + +- [x] Chinesisch +- [x] Englisch +- [x] Deutsch + +## Test + +| OS | Windows | Linux | Macos | +| ---- | ------- | ----- | ----- | +| Test | 🟢 | ◯ | ◯ | + +## Download + +| OS | Windows | Linux | Macos | +| ---- | ----------------------------------------------------------- | ----- | ----- | +| link | [Download](https://github.com/027xiguapi/pear-rec/releases) | ◯ | ◯ | + +## Feedback + +- QQ group + +

+ +

+ +## Lizenz + +[pear-rec wird unter der Apache License V2 bereitgestellt.](LICENSE) diff --git a/README.md b/README.md new file mode 100644 index 0000000..645e8a5 --- /dev/null +++ b/README.md @@ -0,0 +1,159 @@ +

+ +

pear-rec

+

+ stars + react + electron + nestjs + typescript + vite +

+

+ +--- + +## README + +[中文](README.zh-CN.md) | [English](README.md) | [Deutsch](README.de-DE.md) + +## 🧱 Frameworks + +The cross-Platform of `pear-rec` is based on `electronjs`, and the front-end is based on `reactjs`. The functions of screenshot, screen recording, recording, recording (dynamic image) gif are a project based on `webrtc` and `webcodecs`. + +## 📖 Documentation + +> pear-rec(pear rec) is a cross-Platform screenshot, screen recording, audio recording, and video recording software. +> +> pear-rec(pear rec) is a project based on react + electron + vite + viewerjs + plyr + aplayer + react-screenshots. +> +> More functions and APIs can be found on [the official website(https://027xiguapi.github.io/pear-rec)](https://027xiguapi.github.io/pear-rec) or [https://xiguapi027.gitee.io/pear-rec](https://xiguapi027.gitee.io/pear-rec). + +## 🌰 Example + +[web pages](https://pear-rec-xiguapi.vercel.app/) + +## 🧲 Repository + +> gitee: https://gitee.com/xiguapi027/pear-rec +> +> github: https://github.com/027xiguapi/pear-rec + +## 🔨 Usage + +### Getting Started + +To clone and run this repository you'll need [Git](https://git-scm.com) , [Node.js](https://nodejs.org/en/download/) (which comes with [npm](https://www.npmjs.com/)) and [pnpm](https://pnpm.io/) installed on your computer. From your command line: + +```shell +# Clone this repository +git clone https://github.com/027xiguapi/pear-rec.git +# Go into the repository +cd pear-rec +# Install dependencies +pnpm install +# Run the web +pnpm run dev:web +# Run the server +pnpm run dev:server +# Run the desktop +pnpm run dev:desktop +# Run the web +pnpm run start:web +# Run the desktop +pnpm run start:desktop +# Build the web +pnpm run build:web +# Build the desktop +pnpm run build:desktop +# Clear node_modules +pnpm run clear +``` + +## 🥰 Functions + +
+ +
+ +Features that have been ticked are the latest in the development process but may not have been released in the latest version + +- [x] gif(gif.js) + - [x] record + - [x] edit +- [x] Screenshot(react-screenshots) + - [x] Frame crop + - [x] Resizable frame position + - [x] Colour picker + - [x] Magnifying glass + - [x] Brush (freehand brush) + - [x] Geometric shapes (border fill support adjustment) + - [x] Advanced palette settings + - [x] Image filters (local mosaic blur and colour adjustment supported) + - [x] Customize what happens when the frame is released + - [x] Map search by map + - [x] QR code recognition + - [ ] Quick full screen capture to clipboard or custom directory + - [ ] Screenshot history + - [ ] Window and control selection (using OpenCV edge recognition) + - [ ] Long screen capture + - [ ] Multi-screen +- [x] Record screen(WebRTC) + - [x] Recording full screen + - [x] Screenshot + - [x] Customize size + - [x] Mute + - [ ] Key prompt + - [ ] Cursor Location Tips + - [ ] Recorder bar + - [ ] Stream Write +- [x] Record audio(WebRTC) + - [x] Setting + - [x] Watch audio + - [x] Download audio + - [ ] Edit audio +- [x] Record video(WebRTC) + - [ ] Custom bit rate +- [x] Picture Preview(viewerjs) + - [x] Zoom in + - [x] Zoom out + - [x] Drag + - [x] Flip + - [x] Pin + - [x] Watch local image + - [x] Download + - [x] Print + - [ ] ocr + - [x] Watch list + - [x] Map search by map + - [x] QR code recognition +- [x] edit image(tui-image-editor) +- [x] Video Preview(plyr) +- [x] Audio Previews(aplayer) +- [x] setting + - [x] user uuid + - [x] Save address + - [x] Self-starting + - [x] internationalization(zh,en,de ) + +## 🌍 Internationalization(I18n) + +- [x] Chinese +- [x] English +- [x] German + +## 👇 Download + +| OS | Windows | Linux | Macos | +| ---- | ----------------------------------------------------------- | ----- | ----- | +| link | [Download](https://github.com/027xiguapi/pear-rec/releases) | ◯ | ◯ | + +## 👨‍👨‍👦‍👦 Feedback + +We recommend that [issue](https://github.com/027xiguapi/pear-rec/issues) be used for problem feedback. + +## 🤝 License + +[pear-rec is available under the Apache License V2.](LICENSE) + +[Open source etiquette](https://developer.mozilla.org/en-US/docs/MDN/Community/Open_source_etiquette) diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..d51263b --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,171 @@ +

+ +

pear-rec

+

+ stars + react + electron + nestjs + typescript + vite +

+

+ +--- + +## README + +[中文](README.zh-CN.md) | [English](README.md) | [Deutsch](README.de-DE.md) + +## 📖 简介 + +> pear-rec(梨子 rec) 是一个跨平台的截图、录屏、录音、录像、录制(动图)gif、查看图片、查看视频、查看音频和修改图片的软件。 +> +> 更多功能和 api 可以查看[官网(https://027xiguapi.github.io/pear-rec)](https://027xiguapi.github.io/pear-rec) 或 [https://xiguapi027.gitee.io/pear-rec](https://xiguapi027.gitee.io/pear-rec) + +## 🧱 架构 + +> pear-rec(梨子 rec) 的跨平台是基于 `electronjs`,前端是基于 `reactjs`,截图、录屏、录音、录像、录制(动图)gif 等功能是基于 `webrtc` 和 `webcodecs` 的一个项目。 + +## 🌰 例子 + +[网页](https://pear-rec-xiguapi.vercel.app/) + +## 🧲 下载地址 + +> gitee: https://gitee.com/xiguapi027/pear-rec +> +> github: https://github.com/027xiguapi/pear-rec + +## 🔨 源码运行&编译 + +编译需要`nodejs`和`pnpm`环境 + +``` +nodejs >= 18 +pnpm: 8 +``` + +### 开始 + +```shell +# 拷贝代码 +git clone https://gitee.com/xiguapi027/pear-rec.git +# 进入项目 +cd pear-rec +# 安装依赖 +pnpm install +# 调试页面 +pnpm run dev:web +# 调试服务 +pnpm run dev:server +# 调试软件 +pnpm run dev:desktop +# 运行页面 +pnpm run start:web +# 运行软件 +pnpm run start:desktop +# 编译软件 +pnpm run build:desktop +# 清除 node_modules +pnpm run clear +``` + +## 🥰 功能 + +
+ +
+ +已经勾选的功能是开发过程最新功能,但可能还没发布在最新版本 + +- [x] 动图(gif.js) + - [x] 录制 + - [x] 编辑 +- [x] 截图(react-screenshots) + - [x] 框选裁切 + - [x] 框选大小位置可调整 + - [x] 取色器 + - [x] 放大镜 + - [x] 画笔(自由画笔) + - [x] 几何形状(边框填充支持调节) + - [x] 高级画板设置 + - [x] 图像滤镜(支持局部马赛克模糊和色彩调节) + - [x] 自定义框选松开后的操作 + - [x] 以图搜图 + - [x] 扫描二维码 + - [ ] 快速截取全屏到剪贴板或自定义的目录 + - [ ] 截屏历史记录 + - [ ] 窗口和控件选择(使用 OpenCV 边缘识别) + - [ ] 长截屏 + - [ ] 多屏幕 +- [x] 录屏(WebRTC) + - [x] 录制全屏 + - [x] 截图 + - [x] 自定义大小 + - [x] 静音 + - [ ] 按键提示 + - [ ] 光标位置提示 + - [ ] 录制栏 + - [ ] 流写入 +- [x] 录音(WebRTC) + - [x] 录音设置 + - [x] 查看录音 + - [x] 下载录音 + - [ ] 编辑录音 +- [x] 录像(WebRTC) + - [ ] 自定义比特率 +- [x] 图片预览(viewerjs) + - [x] 放大 + - [x] 缩小 + - [x] 拖拽 + - [x] 翻转 + - [x] 钉上层 + - [x] 查看 + - [x] 下载 + - [x] 打印 + - [ ] ocr + - [x] 查看列表 + - [x] 以图搜图 + - [x] 扫描二维码 +- [x] 图片编辑(tui-image-editor) +- [x] 视频预览(plyr) +- [x] 音频预览(aplayer) +- [x] 基本设置 + - [x] 用户 uuid + - [x] 保存地址 + - [x] 开机自启动 + - [x] 国际化(中、英、德) + - [x] 服务设置 + - [ ] 快捷键设置 + - [ ] 重置设置 + +## 🌍 国际化(I18n) + +- [x] 简体中文 +- [x] 英语 +- [x] 德语 + +## 👇 Download + +| 系统 | Windows | Linux | Macos | +| ---- | ------------------------------------------------------- | ----- | ----- | +| 链接 | [下载](https://github.com/027xiguapi/pear-rec/releases) | ◯ | ◯ | + +国内可以用 [GitHub Proxy](https://ghproxy.com/) 加速下载 + +## 👨‍👨‍👦‍👦 反馈和交流 + +我们推荐使用 [issue](https://github.com/027xiguapi/pear-rec/issues) 列表进行最直接有效的反馈,也可以下面的方式 + +- qq 群 + +

+ +

+ +## 🤝 开源协议 + +[pear-rec(梨子 rec) 可在 Apache License V2 下使用。](LICENSE) + +[开源项目礼节](https://developer.mozilla.org/zh-CN/docs/MDN/Community/Open_source_etiquette) diff --git a/package.json b/package.json new file mode 100644 index 0000000..e36fabd --- /dev/null +++ b/package.json @@ -0,0 +1,70 @@ +{ + "name": "pear-rec", + "version": "1.4.0", + "description": " pear-rec is a cross platform software with screenshot, screen recording, audio recording and video recording.", + "scripts": { + "start:desktop": "concurrently --names \"WEB,DESKTOP\" -c \"red,blue\" \"npm run dev:web\" \"wait-on tcp:0.0.0.0:9191 && pnpm run dev:desktop\"", + "dev:desktop": "pnpm run -C packages/desktop dev", + "build:desktop": "pnpm run -C packages/desktop build && pnpm run copy:web && pnpm run -C packages/desktop build:win", + "copy:web": "pnpm run -C packages/web build && node tools/copy-files-web2desktop.js", + "copy:server": "pnpm run -C packages/server build && node tools/copy-files-server2desktop.js", + "dev:web": "pnpm run -C packages/web dev", + "build:web": "pnpm run -C packages/web build", + "only-build:web": "pnpm run -C packages/web build", + "watch:web": "pnpm run -C packages/web watch", + "project:web": "pnpm run -C packages/web build && node tools/copy-files-web2server.js", + "dev:server": "pnpm run -C packages/server dev", + "build:server": "pnpm run -C packages/server build", + "dev:docs": "pnpm run -C packages/docs dev", + "build:docs": "pnpm run -C packages/docs build", + "preview:docs": "pnpm run -C packages/docs preview", + "deploy:docs": "pnpm run -C packages/docs deploy", + "dev:timer": "pnpm run -C packages/timer dev", + "build:timer": "pnpm run -C packages/timer build", + "watch:timer": "pnpm run -C packages/timer watch", + "dev:recorder": "pnpm run -C packages/recorder dev", + "build:recorder": "pnpm run -C packages/recorder build", + "watch:recorder": "pnpm run -C packages/recorder watch", + "dev:screenshot": "pnpm run -C packages/screenshot dev", + "build:screenshot": "pnpm run -C packages/screenshot build", + "watch:screenshot": "pnpm run -C packages/screenshot watch", + "clear": "concurrently --names \"SERVER,WEB,DESKTOP\" \"pnpm run -C packages/web clear\" \"pnpm run -C packages/server clear\" \"pnpm run -C packages/desktop clear\"", + "copy": "node tools/copy-web-files.js", + "changeset": "changeset", + "vp": "changeset version", + "commit": "cz" + }, + "devDependencies": { + "@changesets/changelog-github": "^0.4.8", + "@changesets/cli": "^2.26.2", + "@umijs/fabric": "^4.0.1", + "commitizen": "^4.3.0", + "concurrently": "^8.2.1", + "cz-conventional-changelog": "^3.3.0", + "typescript": "^5.2.2", + "wait-on": "^7.2.0" + }, + "keywords": [ + "electron", + "react", + "antd", + "aplayer", + "plyr", + "viewerjs", + "tui-image-editor", + "react-screenshots", + "react-timer-hook", + "webav", + "gif.js" + ], + "author": "027xiguapi", + "homepage": "https://027xiguapi.github.io/pear-rec", + "repository": { + "type": "git", + "url": "git@github.com:027xiguapi/pear-rec.git" + }, + "bugs": { + "url": "https://github.com/027xiguapi/pear-rec/issues" + }, + "license": "Apache-2.0" +} \ No newline at end of file diff --git a/packages/desktop/.env b/packages/desktop/.env new file mode 100644 index 0000000..bbf79d7 --- /dev/null +++ b/packages/desktop/.env @@ -0,0 +1,5 @@ +# port 端口号 +VITE_API_URL = http://localhost:9190/ +VITE_WEB_URL = http://localhost:9191/ + +PORT=9190 \ No newline at end of file diff --git a/packages/desktop/CHANGELOG.md b/packages/desktop/CHANGELOG.md new file mode 100644 index 0000000..e3d65d1 --- /dev/null +++ b/packages/desktop/CHANGELOG.md @@ -0,0 +1,101 @@ +# @pear-rec/desktop + +## 1.3.14 + +feat: 增加视频转换 + +## 1.3.13 + +perf: 升级 electron + +## 1.3.12 + +fix: 配置修改 + +## 1.3.11 + +feat: 增加画布 + +## 1.3.9 + +feat: 首页增加编辑动图 + +## 1.3.8 + +perf: 录制动图优化 + +## 1.3.7 + +feat: 增加日志 + +## 1.3.6 + +feat: 修改 GIF + +## 1.3.5 + +feat: 增加服务子进程 + +## 1.3.4 + +feat: 重置设置、显示快捷键 + +## 1.3.3 + +fix: 录屏 bug + +## 1.3.2 + +feat: 自动更新软件 + +## 1.3.1 + +feat: 增加录全屏 + +## 1.3.0 + +feat: 增加钉图功能 + +## 1.2.11 + +feat: 打包发布到 git + +## 1.2.10 + +perf: 设置保存地址 + +## 1.2.9 + +feat: 设置快捷键 + +## 1.2.8 + +perf: 优化桌面录屏 + +## 1.2.7 + +feat: 增加记录 + +## 1.2.6 + +fix: electron 修改图片加载 bug, perf: 修改端口 + +## 1.2.5 + +fix: 以管理运行、点击运行软件 + +## 1.2.4 + +fix: 打包服务无法启动 bug + +## 1.2.3 + +fix: 录屏下载 bug + +## 1.2.2 + +feat: 引入@pear-rec/server 服务 + +## 1.2.1 + +fix: fluent-ffmpeg 引入 bug diff --git a/packages/desktop/build/icons/mac/icon.icns b/packages/desktop/build/icons/mac/icon.icns new file mode 100644 index 0000000..e8a26db Binary files /dev/null and b/packages/desktop/build/icons/mac/icon.icns differ diff --git a/packages/desktop/build/icons/png/1024x1024.png b/packages/desktop/build/icons/png/1024x1024.png new file mode 100644 index 0000000..46f0cdf Binary files /dev/null and b/packages/desktop/build/icons/png/1024x1024.png differ diff --git a/packages/desktop/build/icons/png/128x128.png b/packages/desktop/build/icons/png/128x128.png new file mode 100644 index 0000000..6fe3118 Binary files /dev/null and b/packages/desktop/build/icons/png/128x128.png differ diff --git a/packages/desktop/build/icons/png/16x16.png b/packages/desktop/build/icons/png/16x16.png new file mode 100644 index 0000000..faed2bc Binary files /dev/null and b/packages/desktop/build/icons/png/16x16.png differ diff --git a/packages/desktop/build/icons/png/24x24.png b/packages/desktop/build/icons/png/24x24.png new file mode 100644 index 0000000..326a51b Binary files /dev/null and b/packages/desktop/build/icons/png/24x24.png differ diff --git a/packages/desktop/build/icons/png/256x256.png b/packages/desktop/build/icons/png/256x256.png new file mode 100644 index 0000000..10385c8 Binary files /dev/null and b/packages/desktop/build/icons/png/256x256.png differ diff --git a/packages/desktop/build/icons/png/32x32.png b/packages/desktop/build/icons/png/32x32.png new file mode 100644 index 0000000..70fd66c Binary files /dev/null and b/packages/desktop/build/icons/png/32x32.png differ diff --git a/packages/desktop/build/icons/png/48x48.png b/packages/desktop/build/icons/png/48x48.png new file mode 100644 index 0000000..62db1d6 Binary files /dev/null and b/packages/desktop/build/icons/png/48x48.png differ diff --git a/packages/desktop/build/icons/png/512x512.png b/packages/desktop/build/icons/png/512x512.png new file mode 100644 index 0000000..7cba681 Binary files /dev/null and b/packages/desktop/build/icons/png/512x512.png differ diff --git a/packages/desktop/build/icons/png/64x64.png b/packages/desktop/build/icons/png/64x64.png new file mode 100644 index 0000000..9d912d4 Binary files /dev/null and b/packages/desktop/build/icons/png/64x64.png differ diff --git a/packages/desktop/build/icons/win/icon.ico b/packages/desktop/build/icons/win/icon.ico new file mode 100644 index 0000000..30caea1 Binary files /dev/null and b/packages/desktop/build/icons/win/icon.ico differ diff --git a/packages/desktop/e2e/example.spec.ts b/packages/desktop/e2e/example.spec.ts new file mode 100644 index 0000000..59ff80f --- /dev/null +++ b/packages/desktop/e2e/example.spec.ts @@ -0,0 +1,8 @@ +import { test, expect, _electron as electron } from "@playwright/test"; + +test("homepage has title and links to intro page", async () => { + const app = await electron.launch({ args: [".", "--no-sandbox"] }); + const page = await app.firstWindow(); + expect(await page.title()).toBe("Electron + Vite + React"); + await page.screenshot({ path: "e2e/screenshots/example.png" }); +}); diff --git a/packages/desktop/e2e/screenshots/example.png b/packages/desktop/e2e/screenshots/example.png new file mode 100644 index 0000000..dad308e Binary files /dev/null and b/packages/desktop/e2e/screenshots/example.png differ diff --git a/packages/desktop/electron-builder.json5 b/packages/desktop/electron-builder.json5 new file mode 100644 index 0000000..bf37763 --- /dev/null +++ b/packages/desktop/electron-builder.json5 @@ -0,0 +1,44 @@ +/** + * @see https://www.electron.build/configuration/configuration + */ +{ + appId: 'com.electron.pear-rec', + productName: 'pear-rec', + copyright: 'Copyright © 2024 ${author}', + asar: true, + directories: { + output: 'release', + }, + files: ['dist-electron', 'dist', '.env'], + mac: { + icon: 'build/icons/mac/icon.icns', + target: { target: 'dmg' }, + artifactName: '${productName}-Mac-${version}-Installer.${ext}', + }, + linux: { + icon: 'build/icons/png', + target: ['AppImage'], + artifactName: '${productName}-Linux-${version}.${ext}', + }, + win: { + icon: 'build/icons/win/icon.ico', + target: [ + { + target: 'nsis', + }, + ], + artifactName: '${productName}-Windows-${version}-Setup.${ext}', + requestedExecutionLevel: 'requireAdministrator', + }, + nsis: { + oneClick: false, + perMachine: false, + allowToChangeInstallationDirectory: true, + deleteAppDataOnUninstall: false, + }, + publish: { + provider: 'github', + repo: 'pear-rec', + owner: '027xiguapi', + }, +} diff --git a/packages/desktop/electron/electron-env.d.ts b/packages/desktop/electron/electron-env.d.ts new file mode 100644 index 0000000..3408b2a --- /dev/null +++ b/packages/desktop/electron/electron-env.d.ts @@ -0,0 +1,11 @@ +/// + +declare namespace NodeJS { + interface ProcessEnv { + VSCODE_DEBUG?: "true"; + DIST_ELECTRON: string; + DIST: string; + /** /dist/ or /public/ */ + VITE_PUBLIC: string; + } +} diff --git a/packages/desktop/electron/i18n/de-DE.json b/packages/desktop/electron/i18n/de-DE.json new file mode 100644 index 0000000..efc2301 --- /dev/null +++ b/packages/desktop/electron/i18n/de-DE.json @@ -0,0 +1,106 @@ +{ + "tray": { + "screenshot": "Bildschirmfoto", + "audioRecording": "Audioaufnahme", + "screenRecording": "Bildschirmaufnahme", + "videoRecording": "Videoaufnahme", + "viewImage": "Bild anzeigen", + "playAudio": "Audio abspielen", + "watchVideo": "Video abspielen", + "home": "Startseite", + "setting": "Einstellung", + "help": "Hilfe", + "relaunch": "Neustart", + "quit": "Beenden" + }, + "nav": { + "setting": "Einstellung", + "minimize": "minimieren", + "maximize": "maximieren", + "close": "schließen", + "zoomIn": "zoomIn", + "zoomOut": "zoomOut", + "oneToOne": "oneToOne", + "download": "download", + "alwaysOnTopWin": "alwaysOnTopWin", + "openFile": "openFile", + "uploadFile": "uploadFile", + "scan": "scan", + "search": "search", + "prev": "prev", + "next": "next", + "rotateLeft": "rotateLeft", + "rotateRight": "rotateRight", + "flipHorizontal": "flipHorizontal", + "flipVertical": "flipVertical" + }, + "home": { + "audioRecording": "Audioaufnahme", + "screenRecording": "Bildschirmaufnahme", + "videoRecording": "Videoaufnahme", + "screenshot": "Bildschirmfoto", + "fullScreen": "Vollbild", + "viewImage": "Bild anzeigen", + "playAudio": "Audio abspielen", + "watchVideo": "Video abspielen", + "history": "Verlauf" + }, + "editImage": { + "save": "speichern" + }, + "recorderAudio": { + "mute": "stummschalten", + "unmute": "stummschalten aus", + "delete": "löschen", + "save": "speichern", + "play": "abspielen", + "resume": "fortsetzen", + "pause": "pausieren" + }, + "recorderScreen": { + "tip": "Zum Starten der Aufnahme auf 'Start' klicken", + "saving": "speichern", + "mute": "stummschalten", + "unmute": "stummschalten aus", + "resume": "fortsetzen", + "pause": "pausieren", + "save": "speichern", + "play": "abspielen", + "width": "Breite", + "height": "Höhe", + "shotScreen": "shotScreen" + }, + "recorderVideo": { + "tip": "Zum Starten der Aufnahme auf 'Start' klicken", + "saving": "speichern", + "mute": "stummschalten", + "unmute": "stummschalten aus", + "resume": "fortsetzen", + "pause": "pausieren", + "save": "speichern", + "play": "abspielen", + "shotScreen": "shotScreen" + }, + "viewImage": { + "uploadText": "Bild auswählen oder reinziehen", + "uploadHint": "Formate: .jpg, .jpeg, .jfif, .pjpeg, .pjp, .png, .apng, .webp, .avif, .bmp, .gif, .webp, .ico" + }, + "viewVideo": { + "emptyDescription": "Kein Video", + "emptyBtn": "Video öffnen" + }, + "setting": { + "userSetting": "Nutzereinstellungen", + "basicSetting": "Basiseinstellungen", + "serverSetting": "server", + "shortcutSetting": "shortcut", + "address": "web address", + "openFilePath": "open folder", + "language": "Sprache", + "filePath": "Dateipfad", + "openAtLogin": "Bei Anmeldung öffnen", + "open": "öffnen", + "close": "schließen", + "download": "Download" + } +} \ No newline at end of file diff --git a/packages/desktop/electron/i18n/en-US.json b/packages/desktop/electron/i18n/en-US.json new file mode 100644 index 0000000..700107c --- /dev/null +++ b/packages/desktop/electron/i18n/en-US.json @@ -0,0 +1,109 @@ +{ + "tray": { + "screenshot": "screenshot", + "audioRecording": "sound recording", + "screenRecording": "screen recording", + "videoRecording": "video recording", + "viewImage": "view image", + "playAudio": "play audio", + "watchVideo": "watch video", + "home": "home", + "setting": "setting", + "help": "help", + "relaunch": "restart", + "quit": "quit" + }, + "nav": { + "setting": "setting", + "minimize": "minimize", + "maximize": "maximize", + "close": "close", + "zoomIn": "zoomIn", + "zoomOut": "zoomOut", + "oneToOne": "oneToOne", + "download": "download", + "alwaysOnTopWin": "alwaysOnTopWin", + "openFile": "openFile", + "uploadFile": "uploadFile", + "scan": "scan", + "search": "search", + "prev": "prev", + "next": "next", + "rotateLeft": "rotateLeft", + "rotateRight": "rotateRight", + "flipHorizontal": "flipHorizontal", + "flipVertical": "flipVertical" + }, + "home": { + "audioRecording": "sound recording", + "screenRecording": "screen recording", + "videoRecording": "video recording", + "screenshot": "screenshot", + "fullScreen": "full", + "viewImage": "view image", + "playAudio": "play audio", + "watchVideo": "watch video", + "history": "history" + }, + "editImage": { + "save": "save" + }, + "recorderAudio": { + "mute": "mute", + "unmute": "unmute", + "delete": "delete", + "save": "save", + "play": "play", + "resume": "resume", + "pause": "pause" + }, + "recorderScreen": { + "tip": "click the start button below to start recording", + "saving": "saving", + "mute": "mute", + "unmute": "unmute", + "resume": "resume", + "pause": "pause", + "save": "save", + "play": "play", + "width": "width", + "height": "height", + "shotScreen": "shotScreen" + }, + "recorderVideo": { + "tip": "click the start button below to start recording", + "saving": "saving", + "mute": "mute", + "unmute": "unmute", + "resume": "resume", + "pause": "pause", + "save": "save", + "play": "play", + "shotScreen": "shotScreen" + }, + "viewImage": { + "uploadText": "click or drag the image", + "uploadHint": "allow .jpg、.jpeg、.jfif、.pjpeg、.pjp、.png、.apng、.webp、.avif、.bmp、.gif、.webp、.ico" + }, + "viewVideo": { + "emptyDescription": "暂无视频", + "emptyBtn": "open video" + }, + "setting": { + "userSetting": "user", + "basicSetting": "basic", + "serverSetting": "server", + "shortcutSetting": "shortcut", + "address": "web address", + "openFilePath": "open folder", + "reset": "reset", + "language": "language", + "filePath": "filePath", + "openAtLogin": "openAtLogin", + "open": "open", + "close": "close", + "download": "download", + "openServer": "open server", + "serverPath": "server path" + } +} \ No newline at end of file diff --git a/packages/desktop/electron/i18n/zh-CN.json b/packages/desktop/electron/i18n/zh-CN.json new file mode 100644 index 0000000..f8cd876 --- /dev/null +++ b/packages/desktop/electron/i18n/zh-CN.json @@ -0,0 +1,109 @@ +{ + "tray": { + "screenshot": "截图", + "audioRecording": "录音", + "screenRecording": "录屏", + "videoRecording": "录像", + "viewImage": "查看图片", + "playAudio": "查看音频", + "watchVideo": "查看视频", + "home": "主页面", + "setting": "设置", + "help": "教程帮助", + "relaunch": "重启", + "quit": "退出" + }, + "nav": { + "setting": "设置", + "minimize": "最小化", + "maximize": "最大化", + "close": "关闭", + "zoomIn": "放大", + "zoomOut": "缩小", + "oneToOne": "还原", + "download": "下载", + "alwaysOnTopWin": "置顶", + "openFile": "打开图片", + "uploadFile": "打开文件夹", + "scan": "扫码", + "search": "搜图", + "prev": "上一个", + "next": "下一个", + "rotateLeft": "左转", + "rotateRight": "右转", + "flipHorizontal": "水平翻转", + "flipVertical": "垂直翻转" + }, + "home": { + "audioRecording": "录音", + "screenRecording": "录屏", + "videoRecording": "录像", + "screenshot": "截图", + "fullScreen": "全屏", + "viewImage": "查看图片", + "playAudio": "查看音频", + "watchVideo": "查看视频", + "history": "历史" + }, + "editImage": { + "save": "保存" + }, + "recorderAudio": { + "mute": "静音", + "unmute": "打开声音", + "delete": "删除", + "save": "保存", + "play": "开始", + "resume": "继续", + "pause": "暂停" + }, + "recorderScreen": { + "tip": "点击下面开始按钮开始录制", + "saving": "正在保存", + "mute": "静音", + "unmute": "打开声音", + "resume": "继续", + "pause": "暂停", + "save": "保存", + "play": "开始", + "width": "长", + "height": "高", + "shotScreen": "截图" + }, + "recorderVideo": { + "tip": "点击下面开始按钮开始录制", + "saving": "正在保存", + "mute": "静音", + "unmute": "打开声音", + "resume": "继续", + "pause": "暂停", + "save": "保存", + "play": "开始", + "shotScreen": "截图" + }, + "viewImage": { + "uploadText": "点击或拖着图片", + "uploadHint": "支持.jpg、.jpeg、.jfif、.pjpeg、.pjp、.png、.apng、.webp、.avif、.bmp、.gif、.webp、.ico" + }, + "viewVideo": { + "emptyDescription": "暂无视频", + "emptyBtn": "打开视频" + }, + "setting": { + "userSetting": "账户设置", + "basicSetting": "通用设置", + "serverSetting": "服务设置", + "shortcutSetting": "快捷键", + "address": "软件地址", + "openFilePath": "打开下载文件夹", + "reset": "重置", + "language": "语言", + "filePath": "保存地址", + "openAtLogin": "开机自启动", + "open": "开启", + "close": "关闭", + "download": "浏览器下载", + "openServer": "打开服务", + "serverPath": "服务地址" + } +} diff --git a/packages/desktop/electron/main/app.ts b/packages/desktop/electron/main/app.ts new file mode 100644 index 0000000..c7a160c --- /dev/null +++ b/packages/desktop/electron/main/app.ts @@ -0,0 +1,46 @@ +import express, { Application, Request, Response } from 'express'; +import cors from 'cors'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import { createProxyMiddleware, fixRequestBody } from 'http-proxy-middleware'; + +export function initApp() { + const app: Application = express(); + + app.use(cors()); + + app.get('/', (req: Request, res: Response) => { + res.send('Hello World!'); + }); + + app.use( + '/apiGoogle', + createProxyMiddleware({ + target: 'https://lens.google.com', + changeOrigin: true, + secure: false, + logLevel: 'debug', + onProxyReq: fixRequestBody, + agent: new HttpsProxyAgent('http://127.0.0.1:7890'), + pathRewrite: { + '^/apiGoogle': '', + }, + }), + ); + + app.use( + '/apiBaidu', + createProxyMiddleware({ + target: 'https://graph.baidu.com/', + changeOrigin: true, + secure: false, + logLevel: 'debug', + pathRewrite: { + '^/apiBaidu': '', + }, + }), + ); + + app.listen(9190, () => { + console.log('Express app listening on port 9190!'); + }); +} diff --git a/packages/desktop/electron/main/constant.ts b/packages/desktop/electron/main/constant.ts new file mode 100644 index 0000000..2df76c3 --- /dev/null +++ b/packages/desktop/electron/main/constant.ts @@ -0,0 +1,181 @@ +import { homedir } from 'node:os'; +import path from 'node:path'; + +process.env.DIST_ELECTRON = path.join(__dirname, '../'); +process.env.DIST = path.join(process.env.DIST_ELECTRON, '../dist'); + +export const isMac = process.platform === 'darwin'; +export const isLinux = process.platform == 'linux'; +export const isWin = process.platform == 'win32'; + +export const url = import.meta.env.VITE_DEV_SERVER_URL; +export const WEB_URL = import.meta.env.VITE_WEB_URL; +export const VITE_API_URL = import.meta.env.VITE_API_URL; + +export const preload = path.join(__dirname, '../preload/index.js'); +export const serverPath = path.join(__dirname, '../server/main.js'); +export const DIST_ELECTRON = path.join(__dirname, '../'); +export const DIST = path.join(DIST_ELECTRON, '../dist'); + +export const PUBLIC = url ? path.join(DIST_ELECTRON, '../public') : process.env.DIST; + +export const ICON = path.join(PUBLIC, './imgs/icons/png/32x32.png'); + +export const DOCS_PATH = path.join(homedir(), 'Documents'); + +export const PEAR_FILES_PATH = path.join(DOCS_PATH, 'Pear Files'); + +export const CONFIG_FILE_PATH = path.join(PEAR_FILES_PATH, `config.json`); + +export const DEFAULT_CONFIG_FILE_PATH = path.join(PEAR_FILES_PATH, `default-config.json`); + +export const DB_PATH = path.join(PEAR_FILES_PATH, 'db/pear-rec.db'); + +export const LOG_PATH = path.join(PEAR_FILES_PATH, 'log'); + +export const WIN_CONFIG = { + main: { + html: path.join(process.env.DIST, 'index.html'), + width: 660, + height: 410, + autoHideMenuBar: true, + maximizable: false, + resizable: false, + }, + canvas: { + html: path.join(process.env.DIST, 'canvas.html'), + height: 768, + width: 1024, + autoHideMenuBar: true, + }, + clipScreen: { + html: path.join(process.env.DIST, 'clipScreen.html'), + autoHideMenuBar: true, + frame: false, // 无边框窗口 + resizable: true, // 窗口大小是否可调整 + transparent: true, // 使窗口透明 + fullscreenable: false, // 窗口是否可以进入全屏状态 + alwaysOnTop: true, + skipTaskbar: true, + }, + editGif: { + html: path.join(process.env.DIST, 'editGif.html'), + height: 768, + width: 1024, + autoHideMenuBar: true, + }, + editImage: { + html: path.join(process.env.DIST, 'editImage.html'), + height: 768, + width: 1024, + autoHideMenuBar: true, + }, + videoConverter: { + html: path.join(process.env.DIST, 'videoConverter.html'), + height: 768, + width: 1024, + autoHideMenuBar: true, + }, + pinImage: { + html: path.join(process.env.DIST, 'pinImage.html'), + frame: false, // 无边框窗口 + transparent: true, // 使窗口透明 + fullscreenable: false, // 窗口是否可以进入全屏状态 + alwaysOnTop: true, // 窗口是否永远在别的窗口的上面 + autoHideMenuBar: true, + }, + pinVideo: { + html: path.join(process.env.DIST, 'pinVideo.html'), + height: 450, + width: 600, + frame: false, // 无边框窗口 + resizable: true, // 窗口大小是否可调整 + transparent: true, // 使窗口透明 + fullscreenable: false, // 窗口是否可以进入全屏状态 + alwaysOnTop: true, // 窗口是否永远在别的窗口的上面 + autoHideMenuBar: true, // 自动隐藏菜单栏 + }, + recorderAudio: { + html: path.join(process.env.DIST, 'recorderAudio.html'), + height: 768, + width: 1024, + autoHideMenuBar: true, // 自动隐藏菜单栏 + }, + recorderFullScreen: { + html: path.join(process.env.DIST, 'recorderFullScreen.html'), + height: 40, + width: 365, + center: true, + transparent: true, // 使窗口透明 + autoHideMenuBar: true, // 自动隐藏菜单栏 + frame: false, // 无边框窗口 + hasShadow: false, // 窗口是否有阴影 + fullscreenable: false, // 窗口是否可以进入全屏状态 + alwaysOnTop: true, // 窗口是否永远在别的窗口的上面 + skipTaskbar: true, + resizable: false, + }, + recorderScreen: { + html: path.join(process.env.DIST, 'recorderScreen.html'), + width: 340, + height: 130, + autoHideMenuBar: true, // 自动隐藏菜单栏 + maximizable: false, // 最大 + hasShadow: false, // 窗口是否有阴影 + fullscreenable: false, // 窗口是否可以进入全屏状态 + alwaysOnTop: true, // 窗口是否永远在别的窗口的上面 + skipTaskbar: true, + // resizable: false, + }, + recorderVideo: { + html: path.join(process.env.DIST, 'recorderVideo.html'), + height: 768, + width: 1024, + autoHideMenuBar: true, + }, + records: { + html: path.join(process.env.DIST, 'records.html'), + width: 1024, + autoHideMenuBar: true, + }, + setting: { + html: path.join(process.env.DIST, 'setting.html'), + autoHideMenuBar: true, + width: 600, + height: 380, + }, + shotScreen: { + html: path.join(process.env.DIST, 'shotScreen.html'), + autoHideMenuBar: true, // 自动隐藏菜单栏 + useContentSize: true, // width 和 height 将设置为 web 页面的尺寸 + movable: false, // 是否可移动 + frame: false, // 无边框窗口 + resizable: false, // 窗口大小是否可调整 + hasShadow: false, // 窗口是否有阴影 + transparent: true, // 使窗口透明 + fullscreenable: true, // 窗口是否可以进入全屏状态 + fullscreen: true, // 窗口是否全屏 + simpleFullscreen: true, // 在 macOS 上使用 pre-Lion 全屏 + alwaysOnTop: true, + skipTaskbar: true, + }, + spliceImage: { + html: path.join(process.env.DIST, 'spliceImage.html'), + height: 768, + width: 1024, + autoHideMenuBar: true, + }, + viewAudio: { + html: path.join(process.env.DIST, 'viewAudio.html'), + autoHideMenuBar: true, + }, + viewImage: { + html: path.join(process.env.DIST, 'viewImage.html'), + frame: false, + autoHideMenuBar: true, + }, + viewVideo: { + html: path.join(process.env.DIST, 'viewVideo.html'), + autoHideMenuBar: true, + }, +}; diff --git a/packages/desktop/electron/main/globalShortcut.ts b/packages/desktop/electron/main/globalShortcut.ts new file mode 100644 index 0000000..0e10ed6 --- /dev/null +++ b/packages/desktop/electron/main/globalShortcut.ts @@ -0,0 +1,77 @@ +import { globalShortcut } from 'electron'; +import { hideShotScreenWin, showShotScreenWin } from '../win/shotScreenWin'; +import { openRecorderAudioWin } from '../win/recorderAudioWin'; +import { openClipScreenWin } from '../win/clipScreenWin'; +import { openRecorderVideoWin } from '../win/recorderVideoWin'; + +function registerGlobalShortcut(data) { + globalShortcut.register(data['screenshot'], () => { + showShotScreenWin(); + }); + + globalShortcut.register(data['screenRecording'], () => { + openClipScreenWin(); + }); + + globalShortcut.register(data['audioRecording'], () => { + openRecorderAudioWin(); + }); + + globalShortcut.register(data['videoRecording'], () => { + openRecorderVideoWin(); + }); + + globalShortcut.register('Esc', () => { + hideShotScreenWin(); + }); +} + +function registerShotScreenShortcut(data) { + globalShortcut.unregister(data.oldKey); + globalShortcut.register(data.key, () => { + showShotScreenWin(); + }); +} + +function registerRecorderScreenShortcut(data) { + globalShortcut.unregister(data.oldKey); + globalShortcut.register(data.key, () => { + openClipScreenWin(); + }); +} + +function registerRecorderAudioShortcut(data) { + globalShortcut.unregister(data.oldKey); + globalShortcut.register(data.key, () => { + openRecorderAudioWin(); + }); +} + +function registerRecorderVideoShortcut(data) { + globalShortcut.unregister(data.oldKey); + globalShortcut.register(data.key, () => { + openRecorderVideoWin(); + }); +} + +function unregisterGlobalShortcut() { + globalShortcut.unregister('Alt+Shift+q'); + globalShortcut.unregister('Alt+Shift+s'); + globalShortcut.unregister('Alt+Shift+a'); + globalShortcut.unregister('Alt+Shift+v'); + globalShortcut.unregister('Esc'); +} + +function unregisterAllGlobalShortcut() { + globalShortcut.unregisterAll(); +} + +export { + registerGlobalShortcut, + unregisterGlobalShortcut, + unregisterAllGlobalShortcut, + registerShotScreenShortcut, + registerRecorderScreenShortcut, + registerRecorderAudioShortcut, + registerRecorderVideoShortcut, +}; diff --git a/packages/desktop/electron/main/index.ts b/packages/desktop/electron/main/index.ts new file mode 100644 index 0000000..77ea929 --- /dev/null +++ b/packages/desktop/electron/main/index.ts @@ -0,0 +1,83 @@ +import { getConfig, initConfig } from '@pear-rec/server/src/config'; +import { BrowserWindow, app } from 'electron'; +import { release } from 'node:os'; +import * as mainWin from '../win/mainWin'; +import * as shotScreenWin from '../win/shotScreenWin'; +import { isWin } from './constant'; +import { unregisterAllGlobalShortcut } from './globalShortcut'; +import './ipcMain'; +import './logger'; +import { protocolHandle, registerSchemesAsPrivileged } from './protocol'; +import { initTray } from './tray'; +import { update } from './update'; +import { initApp } from './app'; + +initConfig(); +initApp(); + +// The built directory structure +// +// ├─┬ dist-electron +// │ ├─┬ main +// │ │ └── index.js > Electron-Main +// │ ├─┬ preload +// │ │ └── index.js > Preload-Scripts +// │ └─┬ server +// │ └── index.js > Server-Scripts +// ├─┬ dist +// │ └── index.html > Electron-Renderer +// + +// Disable GPU Acceleration for Windows 7 +if (release().startsWith('6.1')) app.disableHardwareAcceleration(); + +// Set application name for Windows 10+ notifications +if (isWin) app.setAppUserModelId(app.getName()); + +app.commandLine.appendSwitch('in-process-gpu'); + +if (!app.requestSingleInstanceLock()) { + app.quit(); + process.exit(0); +} + +// Remove electron security warnings +// This warning only shows in development mode +// Read more on https://www.electronjs.org/docs/latest/tutorial/security +// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true' + +async function createWindow() { + mainWin.openMainWin(); + shotScreenWin.openShotScreenWin(); +} + +registerSchemesAsPrivileged(); + +app.whenReady().then(() => { + const config = getConfig(); + protocolHandle(); + createWindow(); + initTray(config.language); + update(); +}); + +app.on('will-quit', () => { + unregisterAllGlobalShortcut(); +}); + +app.on('window-all-closed', () => { + // if (process.platform !== 'darwin') app.quit(); +}); + +app.on('second-instance', () => { + mainWin.focusMainWin(); +}); + +app.on('activate', () => { + const allWindows = BrowserWindow.getAllWindows(); + if (allWindows.length) { + allWindows[0].focus(); + } else { + createWindow(); + } +}); diff --git a/packages/desktop/electron/main/ipcMain.ts b/packages/desktop/electron/main/ipcMain.ts new file mode 100644 index 0000000..7eb1c62 --- /dev/null +++ b/packages/desktop/electron/main/ipcMain.ts @@ -0,0 +1,447 @@ +import { editConfig } from '@pear-rec/server/src/config'; +import { + BrowserWindow, + app, + desktopCapturer, + dialog, + ipcMain, + screen, + shell, + webContents, +} from 'electron'; +import * as canvasWin from '../win/canvasWin'; +import * as clipScreenWin from '../win/clipScreenWin'; +import * as editGifWin from '../win/editGifWin'; +import * as editImageWin from '../win/editImageWin'; +import * as mainWin from '../win/mainWin'; +import * as pinImageWin from '../win/pinImageWin'; +import * as pinVideoWin from '../win/pinVideoWin'; +import * as recorderAudioWin from '../win/recorderAudioWin'; +import * as recorderFullScreenWin from '../win/recorderFullScreenWin'; +import * as recorderScreenWin from '../win/recorderScreenWin'; +import * as recorderVideoWin from '../win/recorderVideoWin'; +import * as recordsWin from '../win/recordsWin'; +import * as settingWin from '../win/settingWin'; +import * as shotScreenWin from '../win/shotScreenWin'; +import * as spliceImageWin from '../win/spliceImageWin'; +import * as viewAudioWin from '../win/viewAudioWin'; +import * as viewImageWin from '../win/viewImageWin'; +import * as viewVideoWin from '../win/viewVideoWin'; +import * as videoConverterWin from '../win/videoConverterWin'; +import * as globalShortcut from './globalShortcut'; +import logger from './logger'; +import { showNotification } from './notification'; +import * as utils from './utils'; + +const selfWindws = async () => + await Promise.all( + webContents + .getAllWebContents() + .filter((item) => { + const win = BrowserWindow.fromWebContents(item); + return win && win.isVisible(); + }) + .map(async (item) => { + const win = BrowserWindow.fromWebContents(item); + const thumbnail = await win?.capturePage(); + return { + name: win?.getTitle() + (item.devToolsWebContents === null ? '' : '-dev'), // 给dev窗口加上后缀 + id: win?.getMediaSourceId(), + thumbnail, + display_id: '', + appIcon: null, + }; + }), + ); + +function initIpcMain() { + // 日志 + ipcMain.on('lg:send-msg', (e, msg) => { + logger.info(msg); + }); + + // 通知 + ipcMain.on('nt:send-msg', (e, options) => { + showNotification(options); + }); + + // 主页 + ipcMain.on('ma:open-win', () => { + mainWin.openMainWin(); + }); + ipcMain.on('ma:close-win', () => { + mainWin.closeMainWin(); + }); + // 录屏 + ipcMain.on('rs:open-win', (e, search) => { + clipScreenWin.closeClipScreenWin(); + recorderScreenWin.closeRecorderScreenWin(); + mainWin.closeMainWin(); + recorderScreenWin.openRecorderScreenWin(search); + }); + ipcMain.on('rs:close-win', (e, filePath) => { + recorderScreenWin.closeRecorderScreenWin(); + }); + ipcMain.on('rs:hide-win', () => { + recorderScreenWin.hideRecorderScreenWin(); + }); + ipcMain.on('rs:minimize-win', () => { + recorderScreenWin.minimizeRecorderScreenWin(); + }); + ipcMain.handle('rs:get-desktop-capturer-source', async () => { + return [...(await desktopCapturer.getSources({ types: ['screen'] })), ...(await selfWindws())]; + }); + ipcMain.handle('rs:get-bounds-clip', async () => { + return clipScreenWin.getBoundsClipScreenWin(); + }); + ipcMain.on('rs:start-record', (event) => { + clipScreenWin.setMovableClipScreenWin(false); + clipScreenWin.setResizableClipScreenWin(false); + clipScreenWin.setIgnoreMouseEventsClipScreenWin(event, true, { + forward: true, + }); + clipScreenWin.setIsPlayClipScreenWin(true); + }); + ipcMain.on('rs:pause-record', (event) => { + clipScreenWin.setMovableClipScreenWin(true); + clipScreenWin.setResizableClipScreenWin(true); + clipScreenWin.setIgnoreMouseEventsClipScreenWin(event, false); + clipScreenWin.setIsPlayClipScreenWin(false); + }); + ipcMain.on('rs:stop-record', (event) => { + clipScreenWin.setMovableClipScreenWin(true); + clipScreenWin.setResizableClipScreenWin(true); + clipScreenWin.setIgnoreMouseEventsClipScreenWin(event, false); + clipScreenWin.setIsPlayClipScreenWin(false); + }); + ipcMain.handle('rs:get-cursor-screen-point', () => { + return recorderScreenWin.getCursorScreenPointRecorderScreenWin(); + }); + ipcMain.handle('rs:is-focused', () => { + return recorderScreenWin.isFocusedRecorderScreenWin(); + }); + ipcMain.on('rs:focus', () => { + recorderScreenWin.focusRecorderScreenWin(); + }); + // 录屏截图 + ipcMain.on('cs:open-win', (e, search) => { + clipScreenWin.closeClipScreenWin(); + clipScreenWin.openClipScreenWin(search); + }); + ipcMain.on('cs:close-win', () => { + clipScreenWin.closeClipScreenWin(); + recorderScreenWin.closeRecorderScreenWin(); + }); + ipcMain.on('cs:hide-win', () => { + clipScreenWin.hideClipScreenWin(); + recorderScreenWin.hideRecorderScreenWin(); + }); + ipcMain.on('cs:minimize-win', () => { + clipScreenWin.minimizeClipScreenWin(); + }); + ipcMain.on('cs:set-bounds', (event, bounds) => { + clipScreenWin.setBoundsClipScreenWin(bounds); + }); + ipcMain.on('cs:set-ignore-mouse-events', (event, ignore, options) => { + recorderScreenWin.setIgnoreMouseEventsRecorderScreenWin(event, ignore, options); + }); + ipcMain.handle('cs:get-bounds', () => clipScreenWin.getBoundsClipScreenWin()); + // 截图 + ipcMain.handle('ss:get-shot-screen-img', async () => { + const { id } = screen.getPrimaryDisplay(); + const { width, height } = utils.getScreenSize(); + const sources = [ + ...(await desktopCapturer.getSources({ + types: ['screen'], + thumbnailSize: { + width, + height, + }, + })), + ]; + + let source = sources.filter((e: any) => parseInt(e.display_id, 10) == id)[0]; + source || (source = sources[0]); + const img = source.thumbnail.toDataURL(); + return img; + }); + ipcMain.on('ss:open-win', () => { + shotScreenWin.hideShotScreenWin(); + shotScreenWin.showShotScreenWin(); + }); + ipcMain.on('ss:close-win', () => { + shotScreenWin.hideShotScreenWin(); + }); + ipcMain.on('ss:start-win', async () => { + mainWin.closeMainWin(); + setTimeout(() => { + shotScreenWin.hideShotScreenWin(); + shotScreenWin.showShotScreenWin(); + }, 100 * 2); + }); + ipcMain.on('ss:save-img', async (e, downloadUrl) => { + shotScreenWin.downloadURLShotScreenWin(downloadUrl); + }); + ipcMain.on('ss:download-img', async (e, downloadUrl) => { + shotScreenWin.downloadURLShotScreenWin(downloadUrl, true); + }); + ipcMain.on('ss:open-external', async (e, tabUrl) => { + shell.openExternal(tabUrl); + }); + ipcMain.handle('ss:get-desktop-capturer-source', async () => { + return [...(await desktopCapturer.getSources({ types: ['screen'] })), ...(await selfWindws())]; + }); + ipcMain.on('ss:copy-img', (e, imgUrl) => { + shotScreenWin.copyImg(imgUrl); + }); + // 图片展示 + ipcMain.on('vi:open-win', (e, search) => { + viewImageWin.closeViewImageWin(); + viewImageWin.openViewImageWin(search); + }); + ipcMain.on('vi:close-win', () => { + viewImageWin.closeViewImageWin(); + }); + ipcMain.on('vi:hide-win', () => { + viewImageWin.hideViewImageWin(); + }); + ipcMain.on('vi:minimize-win', () => { + viewImageWin.minimizeViewImageWin(); + }); + ipcMain.on('vi:maximize-win', () => { + viewImageWin.maximizeViewImageWin(); + }); + ipcMain.on('vi:unmaximize-win', () => { + viewImageWin.unmaximizeViewImageWin(); + }); + ipcMain.on('vi:open-file', (e, imgUrl) => { + shell.openExternal(imgUrl); + }); + ipcMain.on('vi:alwaysOnTop-win', (e, isTop) => { + viewImageWin.setIsAlwaysOnTopViewImageWin(isTop); + }); + ipcMain.handle('vi:set-always-on-top', () => { + const isAlwaysOnTop = viewImageWin.getIsAlwaysOnTopViewImageWin(); + return viewImageWin.setIsAlwaysOnTopViewImageWin(!isAlwaysOnTop); + }); + ipcMain.handle('vi:get-imgs', async (e, img) => { + const imgs = await viewImageWin.getImgs(img); + return imgs; + }); + ipcMain.on('vi:download-img', async (e, imgUrl) => { + viewImageWin.downloadImg(imgUrl); + }); + + // 图片编辑 + ipcMain.on('ei:close-win', () => { + editImageWin.closeEditImageWin(); + }); + ipcMain.on('ei:open-win', (e, search) => { + editImageWin.openEditImageWin(search); + }); + ipcMain.on('ei:download-img', (e, imgUrl) => { + editImageWin.downloadImg(imgUrl); + }); + + // 图片拼接 + ipcMain.on('si:close-win', () => { + spliceImageWin.closeSpliceImageWin(); + }); + ipcMain.on('si:open-win', () => { + spliceImageWin.openSpliceImageWin(); + }); + + // 视频转换 + ipcMain.on('vc:close-win', () => { + videoConverterWin.closeVideoConverterWin(); + }); + ipcMain.on('vc:open-win', (e, search) => { + videoConverterWin.openVideoConverterWin(search); + }); + + // 动图编辑 + ipcMain.on('eg:close-win', () => { + editGifWin.closeEditGifWin(); + }); + ipcMain.on('eg:open-win', (e, search) => { + editGifWin.openEditGifWin(search); + }); + + // 画画 + ipcMain.on('ca:close-win', () => { + canvasWin.closeCanvasWin(); + }); + ipcMain.on('ca:open-win', () => { + canvasWin.openCanvasWin(); + }); + + // 视频音频展示; + ipcMain.on('vv:open-win', (e, search) => { + viewVideoWin.closeViewVideoWin(); + viewVideoWin.openViewVideoWin(search); + }); + ipcMain.on('vv:close-win', () => { + viewVideoWin.closeViewVideoWin(); + }); + ipcMain.on('vv:hide-win', () => { + viewVideoWin.hideViewVideoWin(); + }); + ipcMain.on('vv:minimize-win', () => { + viewVideoWin.minimizeViewVideoWin(); + }); + ipcMain.on('vv:maximize-win', () => { + viewVideoWin.maximizeViewVideoWin(); + }); + ipcMain.on('vv:unmaximize-win', () => { + viewVideoWin.unmaximizeViewVideoWin(); + }); + ipcMain.on('vv:set-always-on-top', (e, isAlwaysOnTop) => { + viewVideoWin.setAlwaysOnTopViewVideoWin(isAlwaysOnTop); + }); + + // 录音; + ipcMain.on('ra:open-win', () => { + recorderAudioWin.closeRecorderAudioWin(); + recorderAudioWin.openRecorderAudioWin(); + }); + ipcMain.on('ra:close-win', () => { + recorderAudioWin.closeRecorderAudioWin(); + }); + ipcMain.on('ra:hide-win', () => { + recorderAudioWin.hideRecorderAudioWin(); + }); + ipcMain.on('ra:minimize-win', () => { + recorderAudioWin.minimizeRecorderAudioWin(); + }); + ipcMain.on('ra:download-record', (e, downloadUrl) => { + recorderAudioWin.downloadURLRecorderAudioWin(downloadUrl); + }); + ipcMain.on('ra:start-record', () => { + recorderAudioWin.setSizeRecorderAudioWin(285, 43); + }); + ipcMain.on('ra:pause-record', () => { + recorderAudioWin.setSizeRecorderAudioWin(260, 43); + }); + ipcMain.on('ra:stop-record', () => {}); + // 录像 + ipcMain.on('rv:open-win', () => { + recorderVideoWin.closeRecorderVideoWin(); + mainWin.hideMainWin(); + recorderVideoWin.openRecorderVideoWin(); + }); + ipcMain.on('rv:close-win', () => { + recorderVideoWin.closeRecorderVideoWin(); + }); + ipcMain.on('rv:download-record', (e, downloadUrl) => { + recorderVideoWin.downloadURLRecorderVideoWin(downloadUrl); + }); + // 音频 + ipcMain.on('va:open-win', (e, search) => { + viewAudioWin.closeViewAudioWin(); + viewAudioWin.openViewAudioWin(search); + }); + ipcMain.handle('va:get-audios', async (e, audioUrl) => { + const audios = await viewAudioWin.getAudios(audioUrl); + return audios; + }); + // 设置 + ipcMain.on('se:open-win', () => { + settingWin.closeSettingWin(); + settingWin.openSettingWin(); + }); + ipcMain.on('se:close-win', () => { + settingWin.closeSettingWin(); + }); + ipcMain.handle('se:set-filePath', async (e, filePath) => { + let res = await dialog.showOpenDialog({ + properties: ['openDirectory'], + }); + if (!res.canceled) { + filePath = res.filePaths[0]; + } + return filePath; + }); + ipcMain.on('se:set-openAtLogin', (e, isOpen) => { + app.setLoginItemSettings({ openAtLogin: isOpen }); + }); + ipcMain.on('se:set-language', (e, lng) => { + editConfig('language', lng, () => { + app.relaunch(); + app.exit(0); + }); + }); + ipcMain.on('se:set-shortcut', (e, data) => { + if (data.name == 'screenshot') { + globalShortcut.registerShotScreenShortcut(data); + } else if (data.name == 'videoRecording') { + globalShortcut.registerRecorderVideoShortcut(data); + } else if (data.name == 'screenRecording') { + globalShortcut.registerRecorderScreenShortcut(data); + } else if (data.name == 'audioRecording') { + globalShortcut.registerRecorderAudioShortcut(data); + } + }); + ipcMain.on('se:set-shortcuts', (e, data) => { + globalShortcut.registerGlobalShortcut(data); + }); + ipcMain.handle('se:get-openAtLogin', () => { + return app.getLoginItemSettings(); + }); + + // 记录 + ipcMain.on('re:open-win', () => { + recordsWin.closeRecordsWin(); + recordsWin.openRecordsWin(); + }); + + // 钉图 + ipcMain.on('pi:set-size-win', (e, size) => { + pinImageWin.setSizePinImageWin(size); + }); + ipcMain.on('pi:open-win', (e, search) => { + pinImageWin.openPinImageWin(search); + }); + ipcMain.on('pi:close-win', () => { + pinImageWin.closePinImageWin(); + }); + ipcMain.on('pi:minimize-win', () => { + pinImageWin.minimizePinImageWin(); + }); + ipcMain.on('pi:maximize-win', () => { + pinImageWin.maximizePinImageWin(); + }); + ipcMain.on('pi:unmaximize-win', () => { + pinImageWin.unmaximizePinImageWin(); + }); + ipcMain.handle('pi:get-size-win', () => { + return pinImageWin.getSizePinImageWin(); + }); + + // 钉图 + ipcMain.on('pv:open-win', (e, search) => { + pinVideoWin.openPinVideoWin(search); + }); + ipcMain.on('pv:close-win', () => { + pinVideoWin.closePinVideoWin(); + }); + ipcMain.on('pv:minimize-win', () => { + pinVideoWin.minimizePinVideoWin(); + }); + ipcMain.on('pv:maximize-win', () => { + pinVideoWin.maximizePinVideoWin(); + }); + ipcMain.on('pv:unmaximize-win', () => { + pinVideoWin.unmaximizePinVideoWin(); + }); + + // 录全屏 + ipcMain.on('rfs:open-win', () => { + recorderFullScreenWin.closeRecorderFullScreenWin(); + recorderFullScreenWin.openRecorderFullScreenWin(); + }); + ipcMain.on('rfs:close-win', () => { + recorderFullScreenWin.closeRecorderFullScreenWin(); + }); +} + +initIpcMain(); diff --git a/packages/desktop/electron/main/logger.ts b/packages/desktop/electron/main/logger.ts new file mode 100644 index 0000000..fff0568 --- /dev/null +++ b/packages/desktop/electron/main/logger.ts @@ -0,0 +1,79 @@ +// logger.ts +import { app } from 'electron'; +import log from 'electron-log'; +import path from 'node:path'; +import { LOG_PATH } from './constant'; + +// 关闭控制台打印 +log.transports.console.level = false; +log.transports.file.level = 'debug'; +log.transports.file.maxSize = 10024300; // 文件最大不超过 10M +// 输出格式 +log.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}]{scope} {text}'; +let date = new Date(); +let dateStr = date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate(); +// 文件位置及命名方式 +// 默认位置为:C:\Users\[user]\AppData\Roaming\[appname]\electron_log\ +// 文件名为:年-月-日.log +// 自定义文件保存位置为安装目录下 \log\年-月-日.log +log.transports.file.resolvePathFn = () => path.join(LOG_PATH, dateStr + '.log'); + +// 有六个日志级别error, warn, info, verbose, debug, silly。默认是silly +export default { + info(param: any) { + log.info(param); + }, + warn(param: any) { + log.warn(param); + }, + error(param: any) { + log.error(param); + }, + debug(param: any) { + log.debug(param); + }, + verbose(param: any) { + log.verbose(param); + }, + silly(param: any) { + log.silly(param); + }, +}; + +app.on('ready', async () => { + // 渲染进程崩溃 + // app.on('renderer-process-crashed', (event, webContents, killed) => { + // log.error( + // `APP-ERROR:renderer-process-crashed; event: ${JSON.stringify( + // event, + // )}; webContents:${JSON.stringify(webContents)}; killed:${JSON.stringify(killed)}`, + // ); + // }); + + // // GPU进程崩溃 + // app.on('gpu-process-crashed', (event, killed) => { + // log.error( + // `APP-ERROR:gpu-process-crashed; event: ${JSON.stringify(event)}; killed: ${JSON.stringify( + // killed, + // )}`, + // ); + // }); + + // 渲染进程结束 + app.on('render-process-gone', async (event, webContents, details) => { + log.error( + `APP-ERROR:render-process-gone; event: ${JSON.stringify(event)}; webContents:${JSON.stringify( + webContents, + )}; details:${JSON.stringify(details)}`, + ); + }); + + // 子进程结束 + app.on('child-process-gone', async (event, details) => { + log.error( + `APP-ERROR:child-process-gone; event: ${JSON.stringify(event)}; details:${JSON.stringify( + details, + )}`, + ); + }); +}); diff --git a/packages/desktop/electron/main/notification.ts b/packages/desktop/electron/main/notification.ts new file mode 100644 index 0000000..75f063e --- /dev/null +++ b/packages/desktop/electron/main/notification.ts @@ -0,0 +1,7 @@ +import { Notification } from 'electron'; +import { ICON } from './constant'; + +export function showNotification(_options) { + const options = { icon: ICON, ..._options }; + new Notification(options).show(); +} diff --git a/packages/desktop/electron/main/protocol.ts b/packages/desktop/electron/main/protocol.ts new file mode 100644 index 0000000..58ee02f --- /dev/null +++ b/packages/desktop/electron/main/protocol.ts @@ -0,0 +1,28 @@ +import { net, protocol } from 'electron'; + +export function registerSchemesAsPrivileged() { + protocol.registerSchemesAsPrivileged([ + { + scheme: 'pearrec', + privileges: { + standard: true, + secure: true, + supportFetchAPI: true, + stream: true, + }, + }, + ]); +} + +export function protocolHandle() { + protocol.handle('pearrec', (req) => { + const { host, pathname } = new URL(req.url); + + try { + return net.fetch(`file://${host}:\\${pathname}`); + // return net.fetch(`file://C://Users/Administrator/Desktop/pear-rec_1709102672477.mp4`); + } catch (error) { + console.error('protocolHandle', error); + } + }); +} diff --git a/packages/desktop/electron/main/serverProcess.ts b/packages/desktop/electron/main/serverProcess.ts new file mode 100644 index 0000000..3c01e45 --- /dev/null +++ b/packages/desktop/electron/main/serverProcess.ts @@ -0,0 +1,28 @@ +import { type UtilityProcess, utilityProcess } from 'electron'; +import { url, serverPath } from './constant'; +import logger from './logger'; + +let serverProcess: null | UtilityProcess = null; + +export function initServerProcess() { + serverProcess = + url || + utilityProcess.fork(serverPath, [], { + stdio: 'pipe', + }); + + serverProcess.on?.('spawn', () => { + serverProcess.stdout?.on('data', (data) => { + console.log(`serverProcess output: ${data}`); + logger.info(`serverProcess output: ${data}`); + }); + serverProcess.stderr?.on('data', (data) => { + console.error(`serverProcess err: ${data}`); + logger.error(`serverProcess output: ${data}`); + }); + }); +} + +export function quitServerProcess() { + url || serverProcess?.kill(); +} diff --git a/packages/desktop/electron/main/tray.ts b/packages/desktop/electron/main/tray.ts new file mode 100644 index 0000000..b3b773e --- /dev/null +++ b/packages/desktop/electron/main/tray.ts @@ -0,0 +1,126 @@ +import { Menu, Tray, app, shell } from 'electron'; +import { ICON } from './constant'; +import { openMainWin } from '../win/mainWin'; +import { showShotScreenWin } from '../win/shotScreenWin'; +import { openClipScreenWin } from '../win/clipScreenWin'; +import { openRecorderAudioWin } from '../win/recorderAudioWin'; +import { openRecorderVideoWin } from '../win/recorderVideoWin'; +import { openViewImageWin } from '../win/viewImageWin'; +import { openViewVideoWin } from '../win/viewVideoWin'; +import { openViewAudioWin } from '../win/viewAudioWin'; +import { openSettingWin } from '../win/settingWin'; +import * as zhCN from '../i18n/zh-CN.json'; +import * as enUS from '../i18n/en-US.json'; +import * as deDE from '../i18n/de-DE.json'; + +const lngMap = { + zh: zhCN, + en: enUS, + de: deDE, +} as any; + +export function initTray(lng: string) { + let appIcon = new Tray(ICON); + const t = lngMap[lng].tray; + const contextMenu = Menu.buildFromTemplate([ + { + label: t.screenshot, + click: () => { + showShotScreenWin(); + }, + }, + { + label: t.screenRecording, + click: () => { + openClipScreenWin(); + }, + }, + { + label: t.audioRecording, + click: () => { + openRecorderAudioWin(); + }, + }, + { + label: t.videoRecording, + click: () => { + openRecorderVideoWin(); + }, + }, + { + type: 'separator', + }, + { + label: t.viewImage, + click: () => { + openViewImageWin(); + }, + }, + { + label: t.watchVideo, + click: () => { + openViewVideoWin(); + }, + }, + { + label: t.playAudio, + click: () => { + openViewAudioWin(); + }, + }, + // { + // type: "separator", + // }, + // { + // label: "开机自启动", + // type: "checkbox", + // checked: true, + // click: (i) => { + // app.setLoginItemSettings({ openAtLogin: i.checked }); + // }, + // }, + { + type: 'separator', + }, + { + label: t.home, + click: () => { + openMainWin(); + }, + }, + { + label: t.setting, + click: () => { + openSettingWin(); + }, + }, + { + label: t.help, + click: () => { + shell.openExternal('https://027xiguapi.github.io/pear-rec/'); + }, + }, + { + type: 'separator', + }, + { + label: t.relaunch, + click: () => { + app.relaunch(); + app.exit(0); + }, + }, + { + label: t.quit, + click: () => { + app.quit(); + }, + }, + ]); + appIcon.setToolTip('梨子REC'); + appIcon.setContextMenu(contextMenu); + appIcon.addListener('click', function () { + openMainWin(); + }); + return appIcon; +} diff --git a/packages/desktop/electron/main/update.ts b/packages/desktop/electron/main/update.ts new file mode 100644 index 0000000..57421e0 --- /dev/null +++ b/packages/desktop/electron/main/update.ts @@ -0,0 +1,80 @@ +import { app, ipcMain } from 'electron'; +import { type ProgressInfo, type UpdateDownloadedEvent, autoUpdater } from 'electron-updater'; +import * as mainWin from '../win/mainWin'; + +export function update() { + // When set to false, the update download will be triggered through the API + autoUpdater.autoDownload = false; + autoUpdater.disableWebInstaller = false; + autoUpdater.allowDowngrade = false; + // start check + autoUpdater.on('checking-for-update', function () { + console.log('Checking for update.'); + }); + // update available + autoUpdater.on('update-available', (arg) => { + mainWin.sendEuUpdateCanAvailable(arg, true); + // win.webContents.send('eu:update-can-available', { + // update: true, + // version: app.getVersion(), + // newVersion: arg?.version, + // }); + }); + // update not available + autoUpdater.on('update-not-available', (arg) => { + mainWin.sendEuUpdateCanAvailable(arg, false); + // win.webContents.send('eu:update-can-available', { + // update: false, + // version: app.getVersion(), + // newVersion: arg?.version, + // }); + }); + + // Checking for updates + ipcMain.handle('eu:check-update', async () => { + if (!app.isPackaged) { + const error = new Error('The update feature is only available after the package.'); + return { message: error.message, error }; + } + + try { + return await autoUpdater.checkForUpdatesAndNotify(); + } catch (error) { + return { message: 'Network error', error }; + } + }); + + // Start downloading and feedback on progress + ipcMain.handle('eu:start-download', (event) => { + startDownload( + (error, progressInfo) => { + if (error) { + // feedback download error message + event.sender.send('eu:update-error', { message: error.message, error }); + } else { + // feedback update progress message + event.sender.send('eu:download-progress', progressInfo); + } + }, + () => { + // feedback update downloaded message + event.sender.send('eu:update-downloaded'); + }, + ); + }); + + // Install now + ipcMain.handle('eu:quit-and-install', () => { + autoUpdater.quitAndInstall(false, true); + }); +} + +function startDownload( + callback: (error: Error | null, info: ProgressInfo | null) => void, + complete: (event: UpdateDownloadedEvent) => void, +) { + autoUpdater.on('download-progress', (info) => callback(null, info)); + autoUpdater.on('error', (error) => callback(error, null)); + autoUpdater.on('update-downloaded', complete); + autoUpdater.downloadUpdate(); +} diff --git a/packages/desktop/electron/main/utils.ts b/packages/desktop/electron/main/utils.ts new file mode 100644 index 0000000..f3b62a4 --- /dev/null +++ b/packages/desktop/electron/main/utils.ts @@ -0,0 +1,142 @@ +import { screen } from 'electron'; +import * as fs from 'node:fs'; +import path from 'node:path'; +import { PEAR_FILES_PATH } from './constant'; + +function getScreenSize() { + const { size, scaleFactor } = screen.getPrimaryDisplay(); + return { + width: size.width * scaleFactor, + height: size.height * scaleFactor, + }; +} + +function downloadFile(fileInfo: any) { + let { base64String, fileName, fileType = 'png' } = fileInfo; + fileName || (fileName = Number(new Date())); + const url = base64String.split(',')[1]; // 移除前缀,获取base64数据部分 + const buffer = Buffer.from(url, 'base64'); // 将base64数据转化为buffer + const imagePath = path.join(PEAR_FILES_PATH, `/ss/${fileName}.${fileType}`); // 下载路径 + + const directory = path.dirname(imagePath); // 获取文件目录 + if (!fs.existsSync(directory)) { + // 检查目录是否存在 + fs.mkdirSync(directory, { recursive: true }); // 不存在则创建目录 + } + fs.writeFileSync(imagePath, buffer); // 将buffer写入本地文件 +} + +function saveFile(fileInfo: any) { + let { base64String, fileName, fileType = 'png' } = fileInfo; + fileName || (fileName = Number(new Date())); + const url = base64String.split(',')[1]; // 移除前缀,获取base64数据部分 + const buffer = Buffer.from(url, 'base64'); // 将base64数据转化为buffer + const filePath = path.join(PEAR_FILES_PATH, `/ss/${fileName}.${fileType}`); // 下载路径 + + const directory = path.dirname(filePath); // 获取文件目录 + if (!fs.existsSync(directory)) { + // 检查目录是否存在 + fs.mkdirSync(directory, { recursive: true }); // 不存在则创建目录 + } + fs.writeFileSync(filePath, buffer); // 将buffer写入本地文件 +} + +function getImgsByImgUrl(imgUrl: string) { + const directoryPath = path.dirname(imgUrl); + const files = fs.readdirSync(directoryPath); // 读取目录内容 + let imgs: any[] = []; + let index = 0; + let currentIndex = 0; + files.forEach((file) => { + const filePath = path.join(directoryPath, file); + + if (isImageFile(filePath)) { + filePath == imgUrl && (currentIndex = index); + imgs.push({ url: `pearrec:///${filePath}`, index }); + index++; + } + }); + return { imgs, currentIndex }; +} + +function isImageFile(filePath: string): boolean { + const ext = path.extname(filePath).toLowerCase(); + return [ + '.jpg', + '.jpeg', + '.jfif', + '.pjpeg', + '.pjp', + '.png', + 'apng', + '.gif', + '.bmp', + '.avif', + '.webp', + '.ico', + ].includes(ext); +} + +function getAudiosByAudioUrl(audioUrl: string) { + const directoryPath = path.dirname(audioUrl); + const files = fs.readdirSync(directoryPath); // 读取目录内容 + let audios: any[] = []; + let index = 0; + files.forEach((file) => { + const filePath = path.join(directoryPath, file); + if (isAudioFile(filePath)) { + const fileName = path.basename(filePath); + if (filePath == audioUrl) { + audios.unshift({ + url: `pearrec:///${filePath}`, + name: fileName, + cover: './imgs/music.png', + }); + } else { + audios.push({ + url: `pearrec:///${filePath}`, + name: fileName, + cover: './imgs/music.png', + }); + } + index++; + } + }); + return audios; +} + +function isAudioFile(filePath: string): boolean { + const ext = path.extname(filePath).toLowerCase(); + return [ + '.mp3', + '.wav', + '.aac', + '.ogg', + '.flac', + '.aiff', + 'aif', + '.m4a', + '.alac', + '.ac3', + ].includes(ext); +} + +function readDirectoryVideo(filePath: string) { + filePath = filePath.replace(/\\/g, '/'); + return filePath && `pearrec:///${filePath}`; +} + +function readDirectoryImg(filePath: string) { + filePath = filePath.replace(/\\/g, '/'); + return filePath && `pearrec:///${filePath}`; +} + +export { + downloadFile, + getAudiosByAudioUrl, + getImgsByImgUrl, + getScreenSize, + readDirectoryImg, + readDirectoryVideo, + saveFile, +}; diff --git a/packages/desktop/electron/preload/electronAPI.ts b/packages/desktop/electron/preload/electronAPI.ts new file mode 100644 index 0000000..68addbf --- /dev/null +++ b/packages/desktop/electron/preload/electronAPI.ts @@ -0,0 +1,164 @@ +import { contextBridge, ipcRenderer } from 'electron'; + +contextBridge.exposeInMainWorld('electronAPI', { + // logger + sendLogger: (msg: any) => ipcRenderer.send('lg:send-msg', msg), + + // notification + sendNotification: (options: any) => ipcRenderer.send('nt:send-msg', options), + + // mainWin + sendMaOpenWin: () => ipcRenderer.send('ma:open-win'), + sendMaCloseWin: () => ipcRenderer.send('ma:close-win'), + + //raWin + sendRaCloseWin: () => ipcRenderer.send('ra:close-win'), + sendRaOpenWin: () => ipcRenderer.send('ra:open-win'), + sendRaDownloadRecord: (url: string) => ipcRenderer.send('ra:download-record', url), + + //rsWin + sendRsOpenWin: (search?: any) => ipcRenderer.send('rs:open-win', search), + sendRsCloseWin: () => ipcRenderer.send('rs:close-win'), + sendRsHideWin: () => ipcRenderer.send('rs:hide-win'), + sendRsMinimizeWin: () => ipcRenderer.send('rs:minimize-win'), + sendRsStartRecord: () => ipcRenderer.send('rs:start-record'), + sendRsPauseRecord: () => ipcRenderer.send('rs:pause-record'), + sendRsStopRecord: () => ipcRenderer.send('rs:stop-record'), + invokeRsGetBoundsClip: () => ipcRenderer.invoke('rs:get-bounds-clip'), + invokeRsGetDesktopCapturerSource: () => { + return ipcRenderer.invoke('rs:get-desktop-capturer-source'); + }, + invokeRsGetCursorScreenPoint: () => ipcRenderer.invoke('rs:get-cursor-screen-point'), + invokeRsIsFocused: () => ipcRenderer.invoke('rs:is-focused'), + sendRsFocus: () => ipcRenderer.send('rs:focus'), + sendRsSetIgnoreMouseEvents: (ignore: boolean, options: any) => { + ipcRenderer.send('rs:set-ignore-mouse-events', ignore, options); + }, + handleRsGetSizeClipWin: (callback: any) => ipcRenderer.on('rs:get-size-clip-win', callback), + handleRsGetShotScreen: (callback: any) => ipcRenderer.on('rs:get-shot-screen', callback), + handleRsGetEndRecord: (callback: any) => ipcRenderer.on('rs:get-end-record', callback), + + //csWin + sendCsOpenWin: (search?: any) => ipcRenderer.send('cs:open-win', search), + sendCsCloseWin: () => ipcRenderer.send('cs:close-win'), + sendCsHideWin: () => ipcRenderer.send('cs:hide-win'), + sendCsMinimizeWin: () => ipcRenderer.send('cs:minimize-win'), + sendCsSetIgnoreMouseEvents: (ignore: boolean, options: any) => { + ipcRenderer.send('cs:set-ignore-mouse-events', ignore, options); + }, + invokeCsGetBounds: () => ipcRenderer.invoke('cs:get-bounds'), + handleCsSetIsPlay: (callback: any) => ipcRenderer.on('cs:set-isPlay', callback), + sendCsSetBounds: (bounds: any) => { + ipcRenderer.send('cs:set-bounds', bounds); + }, + + //rvWin + sendRvCloseWin: () => ipcRenderer.send('rv:close-win'), + sendRvOpenWin: () => ipcRenderer.send('rv:open-win'), + sendRvDownloadRecord: (url: string) => ipcRenderer.send('rv:download-record', url), + + //ssWin + sendSsOpenWin: () => ipcRenderer.send('ss:open-win'), + sendSsCloseWin: () => ipcRenderer.send('ss:close-win'), + sendSsStartWin: () => ipcRenderer.send('ss:start-win'), + sendSsShowWin: (callback) => ipcRenderer.on('ss:show-win', (e, img) => callback(img)), + sendSsHideWin: (callback) => ipcRenderer.on('ss:hide-win', () => callback()), + invokeSsGetShotScreenImg: () => ipcRenderer.invoke('ss:get-shot-screen-img'), + sendSsDownloadImg: (imgUrl: string) => ipcRenderer.send('ss:download-img', imgUrl), + sendSsSaveImg: (imgUrl: string) => ipcRenderer.send('ss:save-img', imgUrl), + sendSsOpenExternal: (tabUrl: string) => ipcRenderer.send('ss:open-external', tabUrl), + sendSsCopyImg: (imgUrl: string) => ipcRenderer.send('ss:copy-img', imgUrl), + + //viWin + sendViCloseWin: () => ipcRenderer.send('vi:close-win'), + sendViOpenWin: (search?: string) => ipcRenderer.send('vi:open-win', search), + sendViMinimizeWin: () => ipcRenderer.send('vi:minimize-win'), + sendViMaximizeWin: () => ipcRenderer.send('vi:maximize-win'), + sendViUnmaximizeWin: () => ipcRenderer.send('vi:unmaximize-win'), + sendViAlwaysOnTopWin: (isTop: boolean) => ipcRenderer.send('vi:alwaysOnTop-win', isTop), + sendViOpenFile: (imgUrl: string) => ipcRenderer.send('vi:open-file', imgUrl), + invokeViSetIsAlwaysOnTop: () => ipcRenderer.invoke('vi:set-always-on-top'), + invokeViGetImgs: (imgUrl: string) => ipcRenderer.invoke('vi:get-imgs', imgUrl), + sendViDownloadImg: (img: string) => ipcRenderer.send('vi:download-img', img), + sendViSetHistoryImg: (img: string) => { + ipcRenderer.send('vi:set-historyImg', img); + }, + + //eiWin + sendEiCloseWin: () => ipcRenderer.send('ei:close-win'), + sendEiOpenWin: (search?: string) => ipcRenderer.send('ei:open-win', search), + sendEiDownloadImg: (imgUrl?: string) => ipcRenderer.send('ei:download-img', imgUrl), + + //egWin + sendEgCloseWin: () => ipcRenderer.send('eg:close-win'), + sendEgOpenWin: (search?: string) => ipcRenderer.send('eg:open-win', search), + + //vcWin + sendVcCloseWin: () => ipcRenderer.send('vc:close-win'), + sendVcOpenWin: (search?: string) => ipcRenderer.send('vc:open-win', search), + + //caWin + sendCaCloseWin: () => ipcRenderer.send('ca:close-win'), + sendCaOpenWin: () => ipcRenderer.send('ca:open-win'), + + //siWin + sendSiCloseWin: () => ipcRenderer.send('si:close-win'), + sendSiOpenWin: () => ipcRenderer.send('si:open-win'), + + //vvWin + sendVvOpenWin: (search?: string) => ipcRenderer.send('vv:open-win', search), + sendVvCloseWin: () => ipcRenderer.send('vv:close-win'), + invokeVvGetHistoryVideo: () => ipcRenderer.invoke('vv:get-historyVideo'), + sendVvSetHistoryVideo: (img: string) => ipcRenderer.send('vv:set-historyVideo', img), + + //vaWin + sendVaOpenWin: (search?: any) => ipcRenderer.send('va:open-win', search), + invokeVaGetAudios: (audioUrl: any) => ipcRenderer.invoke('va:get-audios', audioUrl), + //seWin 设置 + sendSeOpenWin: () => ipcRenderer.send('se:open-win'), + invokeSeGetUser: () => ipcRenderer.invoke('se:get-user'), + invokeSeSetFilePath: (filePath: string) => ipcRenderer.invoke('se:set-filePath', filePath), + invokeSeGetFilePath: () => ipcRenderer.invoke('se:get-filePath'), + sendSeSetOpenAtLogin: (isOpen: boolean) => ipcRenderer.send('se:set-openAtLogin', isOpen), + sendSeSetLanguage: (lng: string) => ipcRenderer.send('se:set-language', lng), + sendSeSetShortcut: (data: string) => ipcRenderer.send('se:set-shortcut', data), + sendSeSetShortcuts: (data: string) => ipcRenderer.send('se:set-shortcuts', data), + invokeSeGetOpenAtLogin: () => ipcRenderer.invoke('se:get-openAtLogin'), + + //re 记录 + sendReOpenWin: () => ipcRenderer.send('re:open-win'), + + //pi 钉图 + sendPiSetSizeWin: (size: any) => ipcRenderer.send('pi:set-size-win', size), + sendPiOpenWin: (search?: any) => ipcRenderer.send('pi:open-win', search), + sendPiCloseWin: () => ipcRenderer.send('pi:close-win'), + sendPiMinimizeWin: () => ipcRenderer.send('pi:minimize-win'), + sendPiMaximizeWin: () => ipcRenderer.send('pi:maximize-win'), + sendPiUnmaximizeWin: () => ipcRenderer.send('pi:unmaximize-win'), + invokePiGetSizeWin: () => ipcRenderer.invoke('pi:get-size-win'), + + //pi 钉视频 + sendPvOpenWin: (search?: any) => ipcRenderer.send('pv:open-win', search), + sendPvCloseWin: () => ipcRenderer.send('pv:close-win'), + sendPvMinimizeWin: () => ipcRenderer.send('pv:minimize-win'), + sendPvMaximizeWin: () => ipcRenderer.send('pv:maximize-win'), + sendPvUnmaximizeWin: () => ipcRenderer.send('pv:unmaximize-win'), + + // rfs 全屏录屏 + sendRfsOpenWin: () => ipcRenderer.send('rfs:open-win'), + sendRfsCloseWin: () => ipcRenderer.send('rfs:close-win'), + + // Eu 自动更新 + handleEuUpdateCanAvailable: (callback: any) => + ipcRenderer.on('eu:update-can-available', callback), + handleEuUpdateeError: (callback: any) => ipcRenderer.on('eu:update-error', callback), + handleEuDownloadProgress: (callback: any) => ipcRenderer.on('eu:download-progress', callback), + handleEuUpdateDownloaded: (callback: any) => ipcRenderer.on('eu:update-downloaded', callback), + invokeEuQuitAndInstall: () => ipcRenderer.invoke('eu:quit-and-install'), + invokeEuStartDownload: () => ipcRenderer.invoke('eu:start-download'), + invokeEuCheckUpdate: () => ipcRenderer.invoke('eu:check-update'), + offEuUpdateCanAvailable: (callback: any) => ipcRenderer.on('eu:update-can-available', callback), + offEuUpdateeError: (callback: any) => ipcRenderer.on('eu:update-error', callback), + offEuDownloadProgress: (callback: any) => ipcRenderer.on('eu:download-progress', callback), + offEuUpdateDownloaded: (callback: any) => ipcRenderer.on('eu:update-downloaded', callback), +}); diff --git a/packages/desktop/electron/preload/index.ts b/packages/desktop/electron/preload/index.ts new file mode 100644 index 0000000..9725278 --- /dev/null +++ b/packages/desktop/electron/preload/index.ts @@ -0,0 +1,166 @@ +import './electronAPI'; + +function domReady(condition: DocumentReadyState[] = ['complete', 'interactive']) { + return new Promise((resolve) => { + if (condition.includes(document.readyState)) { + resolve(true); + } else { + document.addEventListener('readystatechange', () => { + if (condition.includes(document.readyState)) { + resolve(true); + } + }); + } + }); +} + +const safeDOM = { + append(parent: HTMLElement, child: HTMLElement) { + if (!Array.from(parent.children).find((e) => e === child)) { + return parent.appendChild(child); + } + }, + remove(parent: HTMLElement, child: HTMLElement) { + if (Array.from(parent.children).find((e) => e === child)) { + return parent.removeChild(child); + } + }, +}; + +function useLoading() { + const className = `loaders-css__square-spin`; + const styleContent = ` +@keyframes square-spin { + 25% { transform: perspective(100px) rotateX(180deg) rotateY(0); } + 50% { transform: perspective(100px) rotateX(180deg) rotateY(180deg); } + 75% { transform: perspective(100px) rotateX(0) rotateY(180deg); } + 100% { transform: perspective(100px) rotateX(0) rotateY(0); } +} +.${className} > div { + animation-fill-mode: both; + width: 50px; + height: 50px; + background: #fff; + animation: square-spin 3s 0s cubic-bezier(0.09, 0.57, 0.49, 0.9) infinite; +} +.app-loading-wrap { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: #282c34; + z-index: 9; +} + `; + const oStyle = document.createElement('style'); + const oDiv = document.createElement('div'); + + oStyle.id = 'app-loading-style'; + oStyle.innerHTML = styleContent; + oDiv.className = 'app-loading-wrap'; + oDiv.innerHTML = `
`; + + return { + appendLoading() { + safeDOM.append(document.head, oStyle); + safeDOM.append(document.body, oDiv); + }, + removeLoading() { + safeDOM.remove(document.head, oStyle); + safeDOM.remove(document.body, oDiv); + }, + }; +} + +function useSkeleton() { + const styleContent = ` +.app-skeleton-wrap { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: #fff; + z-index: 9; +} +.app-skeleton-nav { + padding: 0 0 20px; + border-bottom: 1px solid #ccc; +} +.app-skeleton-content { + height: calc(100% - 60px); +} +.app-skeleton-footer { + background-color: rgba(204, 201, 201, 0.2); + height: 40px; + width: 100%; +} +.wavesurfer { + position: absolute; + bottom: 40px; + left: 0; + width: 100%; + height: 140px; + --c: rgb(118, 218, 255); + --w1: radial-gradient(100% 57% at top, #0000 100%, var(--c) 100.5%) no-repeat; + --w2: radial-gradient(100% 57% at bottom, var(--c) 100%, #0000 100.5%) no-repeat; + background: var(--w1), var(--w2), var(--w1), var(--w2); + background-position-x: + -200%, + -100%, + 0%, + 100%; + background-position-y: 100%; + background-size: 50.5% 100%; +} + + `; + const oStyle = document.createElement('style'); + const oDiv = document.createElement('div'); + + oStyle.id = 'app-skeleton-style'; + oStyle.innerHTML = styleContent; + oDiv.className = 'app-skeleton-wrap'; + oDiv.innerHTML = `
`; + + return { + appendSkeleton() { + safeDOM.append(document.head, oStyle); + safeDOM.append(document.body, oDiv); + }, + removeSkeleton() { + safeDOM.remove(document.head, oStyle); + safeDOM.remove(document.body, oDiv); + }, + }; +} + +// ---------------------------------------------------------------------- +const { appendLoading, removeLoading } = useLoading(); +const { appendSkeleton, removeSkeleton } = useSkeleton(); + +if (location.pathname.includes('/index.html')) { + domReady().then(appendSkeleton); + setTimeout(removeSkeleton, 4999); +} else { + if ( + !location.pathname.includes('/shotScreen.html') && + !location.pathname.includes('/clipScreen.html') && + !location.pathname.includes('/canvas.html') && + !location.pathname.includes('/recorderScreen.html') + ) { + domReady().then(appendLoading); + + setTimeout(removeLoading, 4999); + } +} + +window.onmessage = (ev) => { + if (ev.data.payload === 'removeLoading') { + location.pathname.includes('/index.html') ? removeSkeleton() : removeLoading(); + } +}; diff --git a/packages/desktop/electron/win/canvasWin.ts b/packages/desktop/electron/win/canvasWin.ts new file mode 100644 index 0000000..829e559 --- /dev/null +++ b/packages/desktop/electron/win/canvasWin.ts @@ -0,0 +1,43 @@ +import { BrowserWindow } from 'electron'; +import { ICON, WEB_URL, WIN_CONFIG, preload, url } from '../main/constant'; + +let canvasWin: BrowserWindow | null = null; + +function createCanvasWin(): BrowserWindow { + canvasWin = new BrowserWindow({ + title: 'pear-rec 画布', + icon: ICON, + height: WIN_CONFIG.canvas.height, + width: WIN_CONFIG.canvas.width, + autoHideMenuBar: WIN_CONFIG.canvas.autoHideMenuBar, // 自动隐藏菜单栏 + webPreferences: { + preload, + }, + }); + + // canvasWin.webContents.openDevTools(); + if (url) { + canvasWin.loadURL(WEB_URL + `canvas.html`); + } else { + canvasWin.loadFile(WIN_CONFIG.canvas.html); + } + + canvasWin.once('ready-to-show', async () => { + canvasWin?.show(); + }); + + return canvasWin; +} + +function openCanvasWin() { + if (!canvasWin || canvasWin?.isDestroyed()) { + canvasWin = createCanvasWin(); + } + canvasWin.show(); +} + +function closeCanvasWin() { + canvasWin?.close(); +} + +export { closeCanvasWin, createCanvasWin, openCanvasWin }; diff --git a/packages/desktop/electron/win/clipScreenWin.ts b/packages/desktop/electron/win/clipScreenWin.ts new file mode 100644 index 0000000..8b98943 --- /dev/null +++ b/packages/desktop/electron/win/clipScreenWin.ts @@ -0,0 +1,124 @@ +import { BrowserWindow } from 'electron'; +import { ICON, WEB_URL, WIN_CONFIG, preload, url } from '../main/constant'; +import { + minimizeRecorderScreenWin, + openRecorderScreenWin, + setBoundsRecorderScreenWin, + showRecorderScreenWin, +} from './recorderScreenWin'; + +let clipScreenWin: BrowserWindow | null = null; + +function createClipScreenWin(): BrowserWindow { + clipScreenWin = new BrowserWindow({ + title: 'pear-rec', + icon: ICON, + autoHideMenuBar: WIN_CONFIG.clipScreen.autoHideMenuBar, // 自动隐藏菜单栏 + frame: WIN_CONFIG.clipScreen.frame, // 无边框窗口 + resizable: WIN_CONFIG.clipScreen.resizable, // 窗口大小是否可调整 + transparent: WIN_CONFIG.clipScreen.transparent, // 使窗口透明 + fullscreenable: WIN_CONFIG.clipScreen.fullscreenable, // 窗口是否可以进入全屏状态 + alwaysOnTop: WIN_CONFIG.clipScreen.alwaysOnTop, // 窗口是否永远在别的窗口的上面 + skipTaskbar: WIN_CONFIG.clipScreen.skipTaskbar, + webPreferences: { + preload, + }, + }); + + // clipScreenWin.webContents.openDevTools(); + if (url) { + clipScreenWin.loadURL(WEB_URL + 'clipScreen.html'); + } else { + clipScreenWin.loadFile(WIN_CONFIG.clipScreen.html); + } + + // clipScreenWin.on('resize', () => { + // const clipScreenWinBounds = getBoundsClipScreenWin(); + // setBoundsRecorderScreenWin(clipScreenWinBounds); + // }); + + // clipScreenWin.on('move', () => { + // const clipScreenWinBounds = getBoundsClipScreenWin(); + // setBoundsRecorderScreenWin(clipScreenWinBounds); + // }); + + // clipScreenWin.on('restore', () => { + // showRecorderScreenWin(); + // }); + + // clipScreenWin.on('minimize', () => { + // hideRecorderScreenWin(); + // }); + + return clipScreenWin; +} + +function closeClipScreenWin() { + clipScreenWin?.isDestroyed() || clipScreenWin?.close(); + clipScreenWin = null; +} + +function showClipScreenWin() { + clipScreenWin?.show(); +} + +function openClipScreenWin(search?: any) { + if (!clipScreenWin || clipScreenWin?.isDestroyed()) { + clipScreenWin = createClipScreenWin(); + } + + clipScreenWin?.show(); + openRecorderScreenWin(search); +} + +function getBoundsClipScreenWin() { + return clipScreenWin?.getBounds(); +} + +function hideClipScreenWin() { + clipScreenWin?.hide(); +} + +function setAlwaysOnTopClipScreenWin(isAlwaysOnTop: boolean) { + clipScreenWin?.setAlwaysOnTop(isAlwaysOnTop); +} + +function setMovableClipScreenWin(movable: boolean) { + clipScreenWin?.setMovable(movable); +} + +function setResizableClipScreenWin(resizable: boolean) { + clipScreenWin?.setResizable(resizable); +} + +function minimizeClipScreenWin() { + clipScreenWin?.minimize(); + minimizeRecorderScreenWin(); +} + +function setIgnoreMouseEventsClipScreenWin(event: any, ignore: boolean, options?: any) { + clipScreenWin?.setIgnoreMouseEvents(ignore, options); +} + +function setIsPlayClipScreenWin(isPlay: boolean) { + clipScreenWin?.webContents.send('cs:set-isPlay', isPlay); +} + +function setBoundsClipScreenWin(bounds: any) { + clipScreenWin?.setBounds({ ...bounds }); +} + +export { + closeClipScreenWin, + getBoundsClipScreenWin, + hideClipScreenWin, + minimizeClipScreenWin, + openClipScreenWin, + setAlwaysOnTopClipScreenWin, + setBoundsClipScreenWin, + setIgnoreMouseEventsClipScreenWin, + setIsPlayClipScreenWin, + setMovableClipScreenWin, + setResizableClipScreenWin, + showClipScreenWin, +}; diff --git a/packages/desktop/electron/win/editGifWin.ts b/packages/desktop/electron/win/editGifWin.ts new file mode 100644 index 0000000..445ba48 --- /dev/null +++ b/packages/desktop/electron/win/editGifWin.ts @@ -0,0 +1,53 @@ +import { BrowserWindow } from 'electron'; +import { ICON, WEB_URL, WIN_CONFIG, preload, url } from '../main/constant'; + +let editGifWin: BrowserWindow | null = null; + +function createEditGifWin(search?: any): BrowserWindow { + editGifWin = new BrowserWindow({ + title: 'pear-rec 动图编辑', + icon: ICON, + height: WIN_CONFIG.editGif.height, + width: WIN_CONFIG.editGif.width, + autoHideMenuBar: WIN_CONFIG.editGif.autoHideMenuBar, // 自动隐藏菜单栏 + webPreferences: { + preload, + }, + }); + + const videoUrl = search?.videoUrl || ''; + const filePath = search?.filePath || ''; + const imgUrl = search?.imgUrl || ''; + const recordId = search?.recordId || ''; + + // editGifWin.webContents.openDevTools(); + if (url) { + editGifWin.loadURL( + WEB_URL + + `editGif.html?${filePath ? 'filePath=' + filePath : ''}${imgUrl ? 'imgUrl=' + imgUrl : ''}${ + recordId ? 'recordId=' + recordId : '' + }${videoUrl ? 'videoUrl=' + videoUrl : ''}`, + ); + } else { + editGifWin.loadFile(WIN_CONFIG.editGif.html, { + search: `?${filePath ? 'filePath=' + filePath : ''}${imgUrl ? 'imgUrl=' + imgUrl : ''}${ + recordId ? 'recordId=' + recordId : '' + }${videoUrl ? 'videoUrl=' + videoUrl : ''}`, + }); + } + + return editGifWin; +} + +function openEditGifWin(search?: any) { + if (!editGifWin || editGifWin?.isDestroyed()) { + editGifWin = createEditGifWin(search); + } + editGifWin.show(); +} + +function closeEditGifWin() { + editGifWin?.close(); +} + +export { closeEditGifWin, createEditGifWin, openEditGifWin }; diff --git a/packages/desktop/electron/win/editImageWin.ts b/packages/desktop/electron/win/editImageWin.ts new file mode 100644 index 0000000..e1664db --- /dev/null +++ b/packages/desktop/electron/win/editImageWin.ts @@ -0,0 +1,66 @@ +import { BrowserWindow, dialog, nativeImage } from 'electron'; +import { writeFile } from 'node:fs'; +import { ICON, WEB_URL, WIN_CONFIG, preload, url } from '../main/constant'; + +let editImageWin: BrowserWindow | null = null; + +function createEditImageWin(search?: any): BrowserWindow { + editImageWin = new BrowserWindow({ + title: 'pear-rec 图片编辑', + icon: ICON, + height: WIN_CONFIG.editImage.height, + width: WIN_CONFIG.editImage.width, + autoHideMenuBar: WIN_CONFIG.editImage.autoHideMenuBar, // 自动隐藏菜单栏 + webPreferences: { + preload, + }, + }); + + const imgUrl = search?.imgUrl || ''; + + // editImageWin.webContents.openDevTools(); + if (url) { + editImageWin.loadURL(WEB_URL + `editImage.html?imgUrl=${imgUrl}`); + } else { + editImageWin.loadFile(WIN_CONFIG.editImage.html, { + search: `?imgUrl=${imgUrl}`, + }); + } + + editImageWin.once('ready-to-show', async () => { + editImageWin?.show(); + }); + + return editImageWin; +} + +function openEditImageWin(search?: any) { + if (!editImageWin || editImageWin?.isDestroyed()) { + editImageWin = createEditImageWin(search); + } + editImageWin.show(); +} + +function closeEditImageWin() { + editImageWin?.close(); +} + +async function downloadImg(imgUrl: any) { + let defaultPath = `pear-rec_${+new Date()}.png`; + let res = await dialog.showSaveDialog({ + defaultPath: defaultPath, + filters: [{ name: 'Images', extensions: ['png', 'jpg', 'gif'] }], + }); + if (!res.canceled) { + const imgData = nativeImage.createFromDataURL(imgUrl).toPNG(); + writeFile(res.filePath, imgData, (err) => { + if (err) { + console.error(err); + } else { + console.log(`${defaultPath}:图片保存成功`); + } + }); + } +} + +export { closeEditImageWin, createEditImageWin, downloadImg, openEditImageWin }; diff --git a/packages/desktop/electron/win/mainWin.ts b/packages/desktop/electron/win/mainWin.ts new file mode 100644 index 0000000..b99d5a0 --- /dev/null +++ b/packages/desktop/electron/win/mainWin.ts @@ -0,0 +1,100 @@ +import { BrowserWindow, app, shell } from 'electron'; +import { ICON, WEB_URL, WIN_CONFIG, preload, url } from '../main/constant'; + +let mainWin: BrowserWindow | null = null; + +const createMainWin = (): BrowserWindow => { + mainWin = new BrowserWindow({ + title: 'pear-rec', + icon: ICON, + width: WIN_CONFIG.main.width, // 宽度(px) + height: WIN_CONFIG.main.height, // 高度(px) + autoHideMenuBar: WIN_CONFIG.main.autoHideMenuBar, // 自动隐藏菜单栏 + maximizable: WIN_CONFIG.main.maximizable, + resizable: WIN_CONFIG.main.resizable, // gnome下为false时无法全屏 + webPreferences: { + preload, + }, + }); + + if (url) { + mainWin.loadURL(WEB_URL + 'index.html'); + } else { + mainWin.loadFile(WIN_CONFIG.main.html); + } + // mainWin.webContents.openDevTools(); + + // Make all links open with the browser, not with the application + mainWin.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith('https:')) shell.openExternal(url); + return { action: 'deny' }; + }); + + // mainWin.onbeforeunload = (e) => { + // console.log('I do not want to be closed'); + + // // 与通常的浏览器不同,会提示给用户一个消息框, + // //返回非空值将默认取消关闭 + // //建议使用对话框 API 让用户确认关闭应用程序. + // e.returnValue = false; + // }; + + // window.addEventListener('beforeunload', (e) => { + // e.returnValue = false; + // }); + + return mainWin; +}; + +function closeMainWin() { + if (mainWin && !mainWin?.isDestroyed()) { + mainWin?.close(); + } + mainWin = null; +} + +function openMainWin() { + if (!mainWin || mainWin?.isDestroyed()) { + mainWin = createMainWin(); + } + mainWin?.show(); +} + +function hideMainWin() { + mainWin!.hide(); +} + +function minimizeMainWin() { + mainWin!.minimize(); +} + +function focusMainWin() { + if (!mainWin || mainWin?.isDestroyed()) { + mainWin = createMainWin(); + } else { + // Focus on the main window if the user tried to open another + if (mainWin.isMinimized()) mainWin.restore(); + if (!mainWin.isVisible()) mainWin.show(); + mainWin.focus(); + } +} + +function sendEuUpdateCanAvailable(arg, update) { + if (mainWin && !mainWin?.isDestroyed()) { + mainWin.webContents.send('eu:update-can-available', { + update: update, + version: app.getVersion(), + newVersion: arg?.version, + }); + } +} + +export { + closeMainWin, + createMainWin, + focusMainWin, + hideMainWin, + minimizeMainWin, + openMainWin, + sendEuUpdateCanAvailable, +}; diff --git a/packages/desktop/electron/win/pinImageWin.ts b/packages/desktop/electron/win/pinImageWin.ts new file mode 100644 index 0000000..d0f799b --- /dev/null +++ b/packages/desktop/electron/win/pinImageWin.ts @@ -0,0 +1,93 @@ +import { BrowserWindow } from 'electron'; +import { ICON, preload, url, WEB_URL, WIN_CONFIG } from '../main/constant'; + +let pinImageWin: BrowserWindow | null = null; + +function createPinImageWin(search?: any): BrowserWindow { + const imgUrl = search?.imgUrl || ''; + const recordId = search?.recordId || ''; + const width = search?.width || 800; + const height = search?.height || 600; + + pinImageWin = new BrowserWindow({ + title: 'pear-rec 图片', + icon: ICON, + width: width, + height: height, + frame: WIN_CONFIG.pinImage.frame, // 无边框窗口 + transparent: WIN_CONFIG.pinImage.transparent, // 使窗口透明 + fullscreenable: WIN_CONFIG.pinImage.fullscreenable, // 窗口是否可以进入全屏状态 + alwaysOnTop: WIN_CONFIG.pinImage.alwaysOnTop, // 窗口是否永远在别的窗口的上面 + autoHideMenuBar: WIN_CONFIG.pinImage.autoHideMenuBar, // 自动隐藏菜单栏 + webPreferences: { + preload, + }, + }); + + // pinImageWin.webContents.openDevTools(); + if (url) { + pinImageWin.loadURL( + WEB_URL + + `pinImage.html?${imgUrl ? 'imgUrl=' + imgUrl : ''}${ + recordId ? 'recordId=' + recordId : '' + }`, + ); + } else { + pinImageWin.loadFile(WIN_CONFIG.pinImage.html, { + search: `?${imgUrl ? 'imgUrl=' + imgUrl : ''}${recordId ? 'recordId=' + recordId : ''}`, + }); + } + + return pinImageWin; +} + +function setSizePinImageWin(size: any) { + pinImageWin.setBounds(size); +} + +function closePinImageWin() { + pinImageWin?.isDestroyed() || pinImageWin?.close(); + pinImageWin = null; +} + +function openPinImageWin(search?: any) { + pinImageWin = createPinImageWin(search); + pinImageWin?.show(); +} + +function showPinImageWin() { + pinImageWin?.show(); +} + +function hidePinImageWin() { + pinImageWin?.hide(); +} + +function minimizePinImageWin() { + pinImageWin?.minimize(); +} + +function maximizePinImageWin() { + pinImageWin?.maximize(); +} + +function unmaximizePinImageWin() { + pinImageWin?.unmaximize(); +} + +function getSizePinImageWin() { + return pinImageWin.getBounds(); +} + +export { + setSizePinImageWin, + closePinImageWin, + createPinImageWin, + hidePinImageWin, + maximizePinImageWin, + minimizePinImageWin, + openPinImageWin, + showPinImageWin, + unmaximizePinImageWin, + getSizePinImageWin, +}; diff --git a/packages/desktop/electron/win/pinVideoWin.ts b/packages/desktop/electron/win/pinVideoWin.ts new file mode 100644 index 0000000..e9500e7 --- /dev/null +++ b/packages/desktop/electron/win/pinVideoWin.ts @@ -0,0 +1,73 @@ +import { BrowserWindow } from 'electron'; +import { ICON, preload, url, WEB_URL, WIN_CONFIG } from '../main/constant'; + +let pinVideoWin: BrowserWindow | null = null; + +function createPinVideoWin(search?: any): BrowserWindow { + pinVideoWin = new BrowserWindow({ + title: 'pear-rec 视频', + icon: ICON, + height: WIN_CONFIG.pinVideo.height, + width: WIN_CONFIG.pinVideo.width, + frame: WIN_CONFIG.pinVideo.frame, // 无边框窗口 + resizable: WIN_CONFIG.pinVideo.resizable, // 窗口大小是否可调整 + transparent: WIN_CONFIG.pinVideo.transparent, // 使窗口透明 + fullscreenable: WIN_CONFIG.pinVideo.fullscreenable, // 窗口是否可以进入全屏状态 + alwaysOnTop: WIN_CONFIG.pinVideo.alwaysOnTop, // 窗口是否永远在别的窗口的上面 + autoHideMenuBar: WIN_CONFIG.pinVideo.autoHideMenuBar, // 自动隐藏菜单栏 + webPreferences: { + preload, + }, + }); + + // pinVideoWin.webContents.openDevTools(); + if (url) { + pinVideoWin.loadURL(WEB_URL + `pinVideo.html`); + } else { + pinVideoWin.loadFile(WIN_CONFIG.pinVideo.html); + } + + return pinVideoWin; +} + +// 打开关闭录屏窗口 +function closePinVideoWin() { + pinVideoWin?.isDestroyed() || pinVideoWin?.close(); + pinVideoWin = null; +} + +function openPinVideoWin(search?: any) { + pinVideoWin = createPinVideoWin(search); + pinVideoWin?.show(); +} + +function showPinVideoWin() { + pinVideoWin?.show(); +} + +function hidePinVideoWin() { + pinVideoWin?.hide(); +} + +function minimizePinVideoWin() { + pinVideoWin?.minimize(); +} + +function maximizePinVideoWin() { + pinVideoWin?.maximize(); +} + +function unmaximizePinVideoWin() { + pinVideoWin?.unmaximize(); +} + +export { + closePinVideoWin, + createPinVideoWin, + hidePinVideoWin, + maximizePinVideoWin, + minimizePinVideoWin, + openPinVideoWin, + showPinVideoWin, + unmaximizePinVideoWin, +}; diff --git a/packages/desktop/electron/win/recorderAudioWin.ts b/packages/desktop/electron/win/recorderAudioWin.ts new file mode 100644 index 0000000..e9de9a2 --- /dev/null +++ b/packages/desktop/electron/win/recorderAudioWin.ts @@ -0,0 +1,68 @@ +import { BrowserWindow } from 'electron'; +import { ICON, preload, url, WEB_URL, WIN_CONFIG } from '../main/constant'; + +let recorderAudioWin: BrowserWindow | null = null; + +function createRecorderAudioWin(): BrowserWindow { + recorderAudioWin = new BrowserWindow({ + title: 'pear-rec 录音', + icon: ICON, + width: WIN_CONFIG.recorderAudio.width, // 宽度(px), 默认值为 800 + height: WIN_CONFIG.recorderAudio.height, // 高度(px), 默认值为 600 + autoHideMenuBar: WIN_CONFIG.recorderAudio.autoHideMenuBar, // 自动隐藏菜单栏 + webPreferences: { + preload, + }, + }); + + // recorderAudioWin.webContents.openDevTools(); + if (url) { + recorderAudioWin.loadURL(WEB_URL + 'recorderAudio.html'); + } else { + recorderAudioWin.loadFile(WIN_CONFIG.recorderAudio.html); + } + + return recorderAudioWin; +} + +// 打开关闭录屏窗口 +function closeRecorderAudioWin() { + recorderAudioWin?.isDestroyed() || recorderAudioWin?.close(); + recorderAudioWin = null; +} + +function openRecorderAudioWin() { + if (!recorderAudioWin || recorderAudioWin?.isDestroyed()) { + recorderAudioWin = createRecorderAudioWin(); + } + recorderAudioWin?.show(); +} + +function hideRecorderAudioWin() { + recorderAudioWin?.hide(); +} + +function minimizeRecorderAudioWin() { + recorderAudioWin?.minimize(); +} + +function downloadURLRecorderAudioWin(downloadUrl: string) { + // recorderAudioWin?.webContents.downloadURL(downloadUrl); + // downloadSet.add(downloadUrl); +} + +function setSizeRecorderAudioWin(width: number, height: number) { + recorderAudioWin?.setResizable(true); + recorderAudioWin?.setSize(width, height); + recorderAudioWin?.setResizable(false); +} + +export { + closeRecorderAudioWin, + createRecorderAudioWin, + downloadURLRecorderAudioWin, + hideRecorderAudioWin, + minimizeRecorderAudioWin, + openRecorderAudioWin, + setSizeRecorderAudioWin, +}; diff --git a/packages/desktop/electron/win/recorderFullScreenWin.ts b/packages/desktop/electron/win/recorderFullScreenWin.ts new file mode 100644 index 0000000..3b8e4dd --- /dev/null +++ b/packages/desktop/electron/win/recorderFullScreenWin.ts @@ -0,0 +1,77 @@ +import { BrowserWindow } from 'electron'; +import { ICON, WEB_URL, WIN_CONFIG, preload, url } from '../main/constant'; + +let recorderFullScreenWin: BrowserWindow | null = null; + +function createRecorderFullScreenWin(): BrowserWindow { + recorderFullScreenWin = new BrowserWindow({ + title: 'pear-rec 录屏', + icon: ICON, + height: WIN_CONFIG.recorderFullScreen.height, + width: WIN_CONFIG.recorderFullScreen.width, + center: WIN_CONFIG.recorderFullScreen.center, + transparent: WIN_CONFIG.recorderFullScreen.transparent, // 使窗口透明 + autoHideMenuBar: WIN_CONFIG.recorderFullScreen.autoHideMenuBar, // 自动隐藏菜单栏 + frame: WIN_CONFIG.recorderFullScreen.frame, // 无边框窗口 + hasShadow: WIN_CONFIG.recorderFullScreen.hasShadow, // 窗口是否有阴影 + fullscreenable: WIN_CONFIG.recorderFullScreen.fullscreenable, // 窗口是否可以进入全屏状态 + alwaysOnTop: WIN_CONFIG.recorderFullScreen.alwaysOnTop, // 窗口是否永远在别的窗口的上面 + skipTaskbar: WIN_CONFIG.recorderFullScreen.skipTaskbar, + resizable: WIN_CONFIG.recorderFullScreen.resizable, + webPreferences: { + preload, + }, + }); + recorderFullScreenWin?.setBounds({ y: 0 }); + if (url) { + recorderFullScreenWin.loadURL(WEB_URL + `recorderFullScreen.html`); + } else { + recorderFullScreenWin.loadFile(WIN_CONFIG.recorderFullScreen.html); + } + // recorderFullScreenWin.webContents.openDevTools(); + + return recorderFullScreenWin; +} + +function closeRecorderFullScreenWin() { + recorderFullScreenWin?.isDestroyed() || recorderFullScreenWin?.close(); + recorderFullScreenWin = null; +} + +function openRecorderFullScreenWin() { + if (!recorderFullScreenWin || recorderFullScreenWin?.isDestroyed()) { + recorderFullScreenWin = createRecorderFullScreenWin(); + } + recorderFullScreenWin?.show(); +} + +function hideRecorderFullScreenWin() { + recorderFullScreenWin?.hide(); +} + +function showRecorderFullScreenWin() { + recorderFullScreenWin?.show(); +} + +function minimizeRecorderFullScreenWin() { + recorderFullScreenWin?.minimize(); +} + +function setMovableRecorderFullScreenWin(movable: boolean) { + recorderFullScreenWin?.setMovable(movable); +} + +function setAlwaysOnTopRecorderFullScreenWin(isAlwaysOnTop: boolean) { + recorderFullScreenWin?.setAlwaysOnTop(isAlwaysOnTop); +} + +export { + closeRecorderFullScreenWin, + createRecorderFullScreenWin, + hideRecorderFullScreenWin, + minimizeRecorderFullScreenWin, + openRecorderFullScreenWin, + setAlwaysOnTopRecorderFullScreenWin, + setMovableRecorderFullScreenWin, + showRecorderFullScreenWin, +}; diff --git a/packages/desktop/electron/win/recorderScreenWin.ts b/packages/desktop/electron/win/recorderScreenWin.ts new file mode 100644 index 0000000..a6116f4 --- /dev/null +++ b/packages/desktop/electron/win/recorderScreenWin.ts @@ -0,0 +1,164 @@ +import { BrowserWindow, Rectangle, screen } from 'electron'; +import { ICON, WEB_URL, WIN_CONFIG, preload, url } from '../main/constant'; +import { + closeClipScreenWin, + getBoundsClipScreenWin, + showClipScreenWin, + minimizeClipScreenWin, +} from './clipScreenWin'; + +let recorderScreenWin: BrowserWindow | null = null; + +function createRecorderScreenWin(search?: any): BrowserWindow { + const { x, y, width, height } = getBoundsClipScreenWin() as Rectangle; + // let recorderScreenWinX = x + (width - WIN_CONFIG.recorderScreen.width) / 2; + let recorderScreenWinX = x + width + 4 - WIN_CONFIG.recorderScreen.width; + let recorderScreenWinY = y + height; + + recorderScreenWin = new BrowserWindow({ + title: 'pear-rec 录屏', + icon: ICON, + x: recorderScreenWinX, + y: recorderScreenWinY, + width: WIN_CONFIG.recorderScreen.width, + height: WIN_CONFIG.recorderScreen.height, + autoHideMenuBar: WIN_CONFIG.recorderScreen.autoHideMenuBar, // 自动隐藏菜单栏 + maximizable: WIN_CONFIG.recorderScreen.maximizable, + hasShadow: WIN_CONFIG.recorderScreen.hasShadow, // 窗口是否有阴影 + fullscreenable: WIN_CONFIG.recorderScreen.fullscreenable, // 窗口是否可以进入全屏状态 + alwaysOnTop: WIN_CONFIG.recorderScreen.alwaysOnTop, // 窗口是否永远在别的窗口的上面 + // skipTaskbar: WIN_CONFIG.recorderScreen.skipTaskbar, + // resizable: WIN_CONFIG.recorderScreen.resizable, + webPreferences: { + preload, + }, + }); + // recorderScreenWin.webContents.openDevTools(); + if (url) { + recorderScreenWin.loadURL(WEB_URL + `recorderScreen.html?type=${search?.type || ''}`); + } else { + recorderScreenWin.loadFile(WIN_CONFIG.recorderScreen.html, { + search: `?type=${search?.type || ''}`, + }); + } + + // recorderScreenWin.on('move', () => { + // const recorderScreenWinBounds = getBoundsRecorderScreenWin() as Rectangle; + // const clipScreenWinBounds = getBoundsClipScreenWin() as Rectangle; + // setBoundsClipScreenWin({ + // x: recorderScreenWinBounds.x, + // y: recorderScreenWinBounds.y - clipScreenWinBounds.height, + // width: clipScreenWinBounds.width, + // height: clipScreenWinBounds.height, + // }); + // }); + + recorderScreenWin.on('restore', () => { + showClipScreenWin(); + }); + + recorderScreenWin.on('minimize', () => { + minimizeClipScreenWin(); + }); + + recorderScreenWin.on('close', () => { + closeClipScreenWin(); + }); + + return recorderScreenWin; +} + +// 打开关闭录屏窗口 +function closeRecorderScreenWin() { + recorderScreenWin?.isDestroyed() || recorderScreenWin?.close(); + recorderScreenWin = null; +} + +function openRecorderScreenWin(search?: any) { + if (!recorderScreenWin || recorderScreenWin?.isDestroyed()) { + recorderScreenWin = createRecorderScreenWin(search); + } + recorderScreenWin?.show(); +} + +function hideRecorderScreenWin() { + recorderScreenWin?.hide(); +} + +function showRecorderScreenWin() { + recorderScreenWin?.show(); +} + +function minimizeRecorderScreenWin() { + recorderScreenWin?.minimize(); +} + +function setSizeRecorderScreenWin(width: number, height: number) { + // recorderScreenWin?.setResizable(true); + // recorderScreenWin?.setSize(width, height); + // recorderScreenWin?.setResizable(false); +} + +function getBoundsRecorderScreenWin() { + return recorderScreenWin?.getBounds(); +} + +function setMovableRecorderScreenWin(movable: boolean) { + recorderScreenWin?.setMovable(movable); +} + +function setResizableRecorderScreenWin(resizable: boolean) { + recorderScreenWin?.setResizable(resizable); +} + +function setAlwaysOnTopRecorderScreenWin(isAlwaysOnTop: boolean) { + recorderScreenWin?.setAlwaysOnTop(isAlwaysOnTop); +} + +function isFocusedRecorderScreenWin() { + return recorderScreenWin?.isFocused(); +} + +function focusRecorderScreenWin() { + recorderScreenWin?.focus(); +} + +function getCursorScreenPointRecorderScreenWin() { + return screen.getCursorScreenPoint(); +} + +function setBoundsRecorderScreenWin(clipScreenWinBounds: any) { + let { x, y, width, height } = clipScreenWinBounds; + let recorderScreenWinX = x; + let recorderScreenWinY = y + height; + recorderScreenWin?.setBounds({ + x: recorderScreenWinX, + y: recorderScreenWinY, + width: width, + }); + recorderScreenWin?.webContents.send('rs:get-size-clip-win', clipScreenWinBounds); +} + +function setIgnoreMouseEventsRecorderScreenWin(event: any, ignore: boolean, options: any) { + const win = BrowserWindow.fromWebContents(event.sender); + win?.setIgnoreMouseEvents(ignore, options); +} + +export { + closeRecorderScreenWin, + createRecorderScreenWin, + focusRecorderScreenWin, + getBoundsRecorderScreenWin, + getCursorScreenPointRecorderScreenWin, + hideRecorderScreenWin, + isFocusedRecorderScreenWin, + minimizeRecorderScreenWin, + openRecorderScreenWin, + setAlwaysOnTopRecorderScreenWin, + setBoundsRecorderScreenWin, + setIgnoreMouseEventsRecorderScreenWin, + setMovableRecorderScreenWin, + setResizableRecorderScreenWin, + setSizeRecorderScreenWin, + showRecorderScreenWin, +}; diff --git a/packages/desktop/electron/win/recorderVideoWin.ts b/packages/desktop/electron/win/recorderVideoWin.ts new file mode 100644 index 0000000..cf78831 --- /dev/null +++ b/packages/desktop/electron/win/recorderVideoWin.ts @@ -0,0 +1,76 @@ +import { BrowserWindow } from 'electron'; +import { ICON, preload, url, WEB_URL, WIN_CONFIG } from '../main/constant'; + +let recorderVideoWin: BrowserWindow | null = null; +let downloadSet: Set = new Set(); + +function createRecorderVideoWin(): BrowserWindow { + recorderVideoWin = new BrowserWindow({ + title: 'pear-rec 录像', + icon: ICON, + height: WIN_CONFIG.recorderVideo.height, + width: WIN_CONFIG.recorderVideo.width, + autoHideMenuBar: WIN_CONFIG.recorderVideo.autoHideMenuBar, // 自动隐藏菜单栏 + webPreferences: { + preload, + }, + }); + + if (url) { + recorderVideoWin.loadURL(WEB_URL + 'recorderVideo.html'); + // recorderVideoWin.webContents.openDevTools(); + } else { + recorderVideoWin.loadFile(WIN_CONFIG.recorderVideo.html); + } + + recorderVideoWin?.webContents.session.on( + 'will-download', + async (event: any, item: any, webContents: any) => { + const url = item.getURL(); + if (downloadSet.has(url)) { + // const fileName = item.getFilename(); + // const filePath = (await getFilePath()) as string; + // const rvFilePath = join(`${filePath}/rv`, `${fileName}`); + // item.setSavePath(rvFilePath); + // item.once('done', (event: any, state: any) => { + // if (state === 'completed') { + // setHistoryVideo(rvFilePath); + // setTimeout(() => { + // closeRecorderVideoWin(); + // // shell.showItemInFolder(filePath); + // }, 1000); + // } else { + // dialog.showErrorBox('下载失败', `文件 ${item.getFilename()} 因为某些原因被中断下载`); + // } + // }); + } + }, + ); + + return recorderVideoWin; +} + +// 打开关闭录屏窗口 +function closeRecorderVideoWin() { + recorderVideoWin?.isDestroyed() || recorderVideoWin?.close(); + recorderVideoWin = null; +} + +function openRecorderVideoWin() { + if (!recorderVideoWin || recorderVideoWin?.isDestroyed()) { + recorderVideoWin = createRecorderVideoWin(); + } + recorderVideoWin?.show(); +} + +function downloadURLRecorderVideoWin(downloadUrl: string) { + recorderVideoWin?.webContents.downloadURL(downloadUrl); + downloadSet.add(downloadUrl); +} + +export { + closeRecorderVideoWin, + createRecorderVideoWin, + downloadURLRecorderVideoWin, + openRecorderVideoWin, +}; diff --git a/packages/desktop/electron/win/recordsWin.ts b/packages/desktop/electron/win/recordsWin.ts new file mode 100644 index 0000000..1a66a7d --- /dev/null +++ b/packages/desktop/electron/win/recordsWin.ts @@ -0,0 +1,48 @@ +import { BrowserWindow } from 'electron'; +import { ICON, preload, url, WEB_URL, WIN_CONFIG } from '../main/constant'; + +let recordsWin: BrowserWindow | null = null; + +function createRecordsWin(): BrowserWindow { + recordsWin = new BrowserWindow({ + title: 'pear-rec 记录', + icon: ICON, + width: WIN_CONFIG.records.width, + autoHideMenuBar: WIN_CONFIG.records.autoHideMenuBar, // 自动隐藏菜单栏 + webPreferences: { + preload, + }, + }); + + // recordsWin.webContents.openDevTools(); + if (url) { + recordsWin.loadURL(WEB_URL + 'records.html'); + } else { + recordsWin.loadFile(WIN_CONFIG.records.html); + } + + return recordsWin; +} + +// 打开关闭录屏窗口 +function closeRecordsWin() { + recordsWin?.isDestroyed() || recordsWin?.close(); + recordsWin = null; +} + +function openRecordsWin() { + if (!recordsWin || recordsWin?.isDestroyed()) { + recordsWin = createRecordsWin(); + } + recordsWin?.show(); +} + +function showRecordsWin() { + recordsWin?.show(); +} + +function hideRecordsWin() { + recordsWin?.hide(); +} + +export { closeRecordsWin, createRecordsWin, hideRecordsWin, openRecordsWin, showRecordsWin }; diff --git a/packages/desktop/electron/win/settingWin.ts b/packages/desktop/electron/win/settingWin.ts new file mode 100644 index 0000000..82546ac --- /dev/null +++ b/packages/desktop/electron/win/settingWin.ts @@ -0,0 +1,54 @@ +import { BrowserWindow, shell } from 'electron'; +import { ICON, WEB_URL, WIN_CONFIG, preload, url } from '../main/constant'; + +let settingWin: BrowserWindow | null = null; + +function createSettingWin(): BrowserWindow { + settingWin = new BrowserWindow({ + title: 'pear-rec 设置', + icon: ICON, + autoHideMenuBar: WIN_CONFIG.setting.autoHideMenuBar, // 自动隐藏菜单栏 + width: WIN_CONFIG.setting.width, // 宽度(px) + height: WIN_CONFIG.setting.height, // 高度(px) + webPreferences: { + preload, + }, + }); + + // settingWin.webContents.openDevTools(); + if (url) { + settingWin.loadURL(WEB_URL + 'setting.html'); + } else { + settingWin.loadFile(WIN_CONFIG.setting.html); + } + + settingWin.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith('https:')) shell.openExternal(url); + return { action: 'deny' }; + }); + + return settingWin; +} + +// 打开关闭录屏窗口 +function closeSettingWin() { + settingWin?.isDestroyed() || settingWin?.close(); + settingWin = null; +} + +function openSettingWin() { + if (!settingWin || settingWin?.isDestroyed()) { + settingWin = createSettingWin(); + } + settingWin?.show(); +} + +function showSettingWin() { + settingWin?.show(); +} + +function hideSettingWin() { + settingWin?.hide(); +} + +export { closeSettingWin, createSettingWin, hideSettingWin, openSettingWin, showSettingWin }; diff --git a/packages/desktop/electron/win/shotScreenWin.ts b/packages/desktop/electron/win/shotScreenWin.ts new file mode 100644 index 0000000..ae7461b --- /dev/null +++ b/packages/desktop/electron/win/shotScreenWin.ts @@ -0,0 +1,130 @@ +import { BrowserWindow, clipboard, dialog, nativeImage, screen, desktopCapturer } from 'electron'; +import { ICON, WEB_URL, WIN_CONFIG, preload, url } from '../main/constant'; +import * as utils from '../main/utils'; + +let shotScreenWin: BrowserWindow | null = null; +let savePath: string = ''; +let downloadSet: Set = new Set(); + +function createShotScreenWin(): BrowserWindow { + shotScreenWin = new BrowserWindow({ + title: 'pear-rec 截图', + icon: ICON, + show: false, + autoHideMenuBar: WIN_CONFIG.shotScreen.autoHideMenuBar, // 自动隐藏菜单栏 + useContentSize: WIN_CONFIG.shotScreen.useContentSize, // width 和 height 将设置为 web 页面的尺寸 + movable: WIN_CONFIG.shotScreen.movable, // 是否可移动 + frame: WIN_CONFIG.shotScreen.frame, // 无边框窗口 + resizable: WIN_CONFIG.shotScreen.resizable, // 窗口大小是否可调整 + hasShadow: WIN_CONFIG.shotScreen.hasShadow, // 窗口是否有阴影 + transparent: WIN_CONFIG.shotScreen.transparent, // 使窗口透明 + fullscreenable: WIN_CONFIG.shotScreen.fullscreenable, // 窗口是否可以进入全屏状态 + fullscreen: WIN_CONFIG.shotScreen.fullscreen, // 窗口是否全屏 + simpleFullscreen: WIN_CONFIG.shotScreen.simpleFullscreen, // 在 macOS 上使用 pre-Lion 全屏 + alwaysOnTop: WIN_CONFIG.shotScreen.alwaysOnTop, + skipTaskbar: WIN_CONFIG.shotScreen.skipTaskbar, + webPreferences: { + preload, + }, + }); + + // shotScreenWin.webContents.openDevTools(); + + if (url) { + shotScreenWin.loadURL(WEB_URL + 'shotScreen.html'); + } else { + shotScreenWin.loadFile(WIN_CONFIG.shotScreen.html); + } + shotScreenWin.maximize(); + shotScreenWin.setFullScreen(true); + + return shotScreenWin; +} + +// 打开关闭录屏窗口 +function closeShotScreenWin() { + shotScreenWin?.isDestroyed() || shotScreenWin?.close(); + shotScreenWin = null; +} + +function openShotScreenWin() { + if (!shotScreenWin || shotScreenWin?.isDestroyed()) { + shotScreenWin = createShotScreenWin(); + } + // shotScreenWin?.show(); +} + +async function showShotScreenWin() { + const { id } = screen.getPrimaryDisplay(); + const { width, height } = utils.getScreenSize(); + const sources = [ + ...(await desktopCapturer.getSources({ + types: ['screen'], + thumbnailSize: { + width, + height, + }, + })), + ]; + + let source = sources.filter((e: any) => parseInt(e.display_id, 10) == id)[0]; + source || (source = sources[0]); + const img = source.thumbnail.toDataURL(); + shotScreenWin?.webContents.send('ss:show-win', img); + if (!shotScreenWin || shotScreenWin?.isDestroyed()) { + shotScreenWin = createShotScreenWin(); + } + shotScreenWin?.show(); +} + +function hideShotScreenWin() { + shotScreenWin?.webContents.send('ss:hide-win'); + shotScreenWin?.hide(); +} + +function minimizeShotScreenWin() { + shotScreenWin?.minimize(); +} + +function maximizeShotScreenWin() { + shotScreenWin?.maximize(); +} + +function unmaximizeShotScreenWin() { + shotScreenWin?.unmaximize(); +} + +async function downloadURLShotScreenWin(downloadUrl: string, isShowDialog?: boolean) { + savePath = ''; + isShowDialog && (savePath = await showOpenDialogShotScreenWin()); + downloadSet.add(downloadUrl); + shotScreenWin?.webContents.downloadURL(downloadUrl); +} + +async function showOpenDialogShotScreenWin() { + let res = await dialog.showOpenDialog({ + properties: ['openDirectory'], + }); + + const savePath = res.filePaths[0] || ''; + + return savePath; +} + +function copyImg(filePath: string) { + const image = nativeImage.createFromDataURL(filePath); + clipboard.writeImage(image); +} + +export { + closeShotScreenWin, + copyImg, + createShotScreenWin, + downloadURLShotScreenWin, + hideShotScreenWin, + maximizeShotScreenWin, + minimizeShotScreenWin, + openShotScreenWin, + showShotScreenWin, + unmaximizeShotScreenWin, +}; diff --git a/packages/desktop/electron/win/spliceImageWin.ts b/packages/desktop/electron/win/spliceImageWin.ts new file mode 100644 index 0000000..abffec1 --- /dev/null +++ b/packages/desktop/electron/win/spliceImageWin.ts @@ -0,0 +1,43 @@ +import { BrowserWindow } from 'electron'; +import { ICON, WEB_URL, WIN_CONFIG, preload, url } from '../main/constant'; + +let spliceImageWin: BrowserWindow | null = null; + +function createSpliceImageWin(): BrowserWindow { + spliceImageWin = new BrowserWindow({ + title: 'pear-rec 拼接图片', + icon: ICON, + height: WIN_CONFIG.spliceImage.height, + width: WIN_CONFIG.spliceImage.width, + autoHideMenuBar: WIN_CONFIG.spliceImage.autoHideMenuBar, // 自动隐藏菜单栏 + webPreferences: { + preload, + }, + }); + + // spliceImageWin.webContents.openDevTools(); + if (url) { + spliceImageWin.loadURL(WEB_URL + `spliceImage.html`); + } else { + spliceImageWin.loadFile(WIN_CONFIG.spliceImage.html); + } + + spliceImageWin.once('ready-to-show', async () => { + spliceImageWin?.show(); + }); + + return spliceImageWin; +} + +function openSpliceImageWin() { + if (!spliceImageWin || spliceImageWin?.isDestroyed()) { + spliceImageWin = createSpliceImageWin(); + } + spliceImageWin.show(); +} + +function closeSpliceImageWin() { + spliceImageWin?.close(); +} + +export { closeSpliceImageWin, createSpliceImageWin, openSpliceImageWin }; diff --git a/packages/desktop/electron/win/videoConverterWin.ts b/packages/desktop/electron/win/videoConverterWin.ts new file mode 100644 index 0000000..8d0b486 --- /dev/null +++ b/packages/desktop/electron/win/videoConverterWin.ts @@ -0,0 +1,47 @@ +import { BrowserWindow, dialog, nativeImage } from 'electron'; +import { ICON, WEB_URL, WIN_CONFIG, preload, url } from '../main/constant'; + +let videoConverterWin: BrowserWindow | null = null; + +function createVideoConverterWin(search?: any): BrowserWindow { + videoConverterWin = new BrowserWindow({ + title: 'pear-rec 图片编辑', + icon: ICON, + height: WIN_CONFIG.videoConverter.height, + width: WIN_CONFIG.videoConverter.width, + autoHideMenuBar: WIN_CONFIG.videoConverter.autoHideMenuBar, // 自动隐藏菜单栏 + webPreferences: { + preload, + }, + }); + + const videoUrl = search?.videoUrl || ''; + + // videoConverterWin.webContents.openDevTools(); + if (url) { + videoConverterWin.loadURL(WEB_URL + `videoConverter.html?videoUrl=${videoUrl}`); + } else { + videoConverterWin.loadFile(WIN_CONFIG.videoConverter.html, { + search: `?videoUrl=${videoUrl}`, + }); + } + + videoConverterWin.once('ready-to-show', async () => { + videoConverterWin?.show(); + }); + + return videoConverterWin; +} + +function openVideoConverterWin(search?: any) { + if (!videoConverterWin || videoConverterWin?.isDestroyed()) { + videoConverterWin = createVideoConverterWin(search); + } + videoConverterWin.show(); +} + +function closeVideoConverterWin() { + videoConverterWin?.close(); +} + +export { closeVideoConverterWin, createVideoConverterWin, openVideoConverterWin }; diff --git a/packages/desktop/electron/win/viewAudioWin.ts b/packages/desktop/electron/win/viewAudioWin.ts new file mode 100644 index 0000000..fb7f819 --- /dev/null +++ b/packages/desktop/electron/win/viewAudioWin.ts @@ -0,0 +1,61 @@ +import { BrowserWindow } from 'electron'; +import { ICON, WEB_URL, WIN_CONFIG, preload, url } from '../main/constant'; +import { getAudiosByAudioUrl } from '../main/utils'; + +let viewAudioWin: BrowserWindow | null = null; + +function createViewAudioWin(search?: any): BrowserWindow { + viewAudioWin = new BrowserWindow({ + title: 'pear-rec 音频', + icon: ICON, + autoHideMenuBar: WIN_CONFIG.viewAudio.autoHideMenuBar, // 自动隐藏菜单栏 + webPreferences: { + preload, + }, + }); + + const audioUrl = search?.audioUrl || ''; + const recordId = search?.recordId || ''; + // Open devTool if the app is not packaged + // viewAudioWin.webContents.openDevTools(); + + if (url) { + viewAudioWin.loadURL( + WEB_URL + + `viewAudio.html?${audioUrl ? 'audioUrl=' + audioUrl : ''}${ + recordId ? 'recordId=' + recordId : '' + }`, + ); + } else { + viewAudioWin.loadFile(WIN_CONFIG.viewAudio.html, { + search: `${audioUrl ? 'audioUrl=' + audioUrl : ''}${recordId ? 'recordId=' + recordId : ''}`, + }); + } + + viewAudioWin.once('ready-to-show', async () => { + viewAudioWin?.show(); + }); + + return viewAudioWin; +} + +function openViewAudioWin(search?: any) { + if (!viewAudioWin || viewAudioWin?.isDestroyed()) { + viewAudioWin = createViewAudioWin(search); + } + viewAudioWin.show(); +} + +function closeViewAudioWin() { + if (!viewAudioWin?.isDestroyed()) { + viewAudioWin?.close(); + } + viewAudioWin = null; +} + +async function getAudios(audioUrl?: any) { + let audios = await getAudiosByAudioUrl(audioUrl); + return audios; +} + +export { closeViewAudioWin, createViewAudioWin, getAudios, openViewAudioWin }; diff --git a/packages/desktop/electron/win/viewImageWin.ts b/packages/desktop/electron/win/viewImageWin.ts new file mode 100644 index 0000000..98cd279 --- /dev/null +++ b/packages/desktop/electron/win/viewImageWin.ts @@ -0,0 +1,124 @@ +import { BrowserWindow, dialog } from 'electron'; +import { readFile, writeFile } from 'node:fs'; +import { ICON, WEB_URL, WIN_CONFIG, preload, url } from '../main/constant'; +import { getImgsByImgUrl } from '../main/utils'; + +let viewImageWin: BrowserWindow | null = null; + +function createViewImageWin(search?: any): BrowserWindow { + viewImageWin = new BrowserWindow({ + title: 'pear-rec 图片', + icon: ICON, + frame: WIN_CONFIG.viewImage.frame, + autoHideMenuBar: WIN_CONFIG.viewImage.autoHideMenuBar, // 自动隐藏菜单栏 + webPreferences: { + preload, + }, + }); + + const imgUrl = search?.imgUrl || ''; + const recordId = search?.recordId || ''; + // viewImageWin.webContents.openDevTools(); + if (url) { + viewImageWin.loadURL( + WEB_URL + + `viewImage.html?${imgUrl ? 'imgUrl=' + imgUrl : ''}${ + recordId ? 'recordId=' + recordId : '' + }`, + ); + // Open devTool if the app is not packaged + } else { + viewImageWin.loadFile(WIN_CONFIG.viewImage.html, { + search: `?${imgUrl ? 'imgUrl=' + imgUrl : ''}${recordId ? 'recordId=' + recordId : ''}`, + }); + } + + return viewImageWin; +} + +function openViewImageWin(search?: any) { + if (!viewImageWin || viewImageWin?.isDestroyed()) { + viewImageWin = createViewImageWin(search); + } + viewImageWin.show(); +} + +function closeViewImageWin() { + if (!(viewImageWin && viewImageWin.isDestroyed())) { + viewImageWin?.close(); + } + viewImageWin = null; +} + +function destroyViewImageWin() { + viewImageWin?.destroy(); + viewImageWin = null; +} + +function hideViewImageWin() { + viewImageWin?.hide(); +} + +function minimizeViewImageWin() { + viewImageWin?.minimize(); +} + +function maximizeViewImageWin() { + viewImageWin?.maximize(); +} + +function unmaximizeViewImageWin() { + viewImageWin?.unmaximize(); +} + +function getIsAlwaysOnTopViewImageWin() { + return viewImageWin?.isAlwaysOnTop(); +} + +function setIsAlwaysOnTopViewImageWin(isAlwaysOnTop: boolean) { + viewImageWin?.setAlwaysOnTop(isAlwaysOnTop); + return isAlwaysOnTop; +} + +async function getImgs(imgUrl: any) { + let imgs = await getImgsByImgUrl(imgUrl); + return imgs; +} + +async function downloadImg(imgUrl: any) { + let defaultPath = `pear-rec_${+new Date()}.png`; + let res = await dialog.showSaveDialog({ + defaultPath: defaultPath, + filters: [{ name: 'Images', extensions: ['png', 'jpg', 'gif'] }], + }); + if (!res.canceled) { + readFile(imgUrl, (err, imgData) => { + if (err) { + console.error(err); + } else { + writeFile(res.filePath, imgData, (err) => { + if (err) { + console.error(err); + } else { + console.log(`${defaultPath}:图片保存成功`); + } + }); + } + }); + } + return imgUrl; +} + +export { + closeViewImageWin, + createViewImageWin, + downloadImg, + getImgs, + getIsAlwaysOnTopViewImageWin, + hideViewImageWin, + maximizeViewImageWin, + minimizeViewImageWin, + openViewImageWin, + setIsAlwaysOnTopViewImageWin, + unmaximizeViewImageWin, +}; diff --git a/packages/desktop/electron/win/viewVideoWin.ts b/packages/desktop/electron/win/viewVideoWin.ts new file mode 100644 index 0000000..12a0998 --- /dev/null +++ b/packages/desktop/electron/win/viewVideoWin.ts @@ -0,0 +1,95 @@ +import { BrowserWindow } from 'electron'; +import { ICON, WEB_URL, WIN_CONFIG, preload, url } from '../main/constant'; + +let viewVideoWin: BrowserWindow | null = null; + +function createViewVideoWin(search?: any): BrowserWindow { + viewVideoWin = new BrowserWindow({ + title: 'pear-rec 视频', + icon: ICON, + autoHideMenuBar: WIN_CONFIG.viewVideo.autoHideMenuBar, // 自动隐藏菜单栏 + webPreferences: { + preload, + }, + }); + + const videoUrl = search?.videoUrl || ''; + const recordId = search?.recordId || ''; + // Open devTool if the app is not packaged + // viewVideoWin.webContents.openDevTools(); + if (url) { + viewVideoWin.loadURL( + WEB_URL + + `viewVideo.html?${videoUrl ? 'videoUrl=' + videoUrl : ''}${ + recordId ? 'recordId=' + recordId : '' + }`, + ); + } else { + viewVideoWin.loadFile(WIN_CONFIG.viewVideo.html, { + search: `?${videoUrl ? 'videoUrl=' + videoUrl : ''}${recordId ? 'recordId=' + recordId : ''}`, + }); + } + + viewVideoWin.once('ready-to-show', async () => { + viewVideoWin?.show(); + }); + + return viewVideoWin; +} + +function openViewVideoWin(search?: any) { + if (!viewVideoWin || viewVideoWin?.isDestroyed()) { + viewVideoWin = createViewVideoWin(search); + } + viewVideoWin.show(); +} + +function closeViewVideoWin() { + if (!(viewVideoWin && viewVideoWin.isDestroyed())) { + viewVideoWin?.close(); + } + viewVideoWin = null; +} + +function hideViewVideoWin() { + viewVideoWin?.hide(); +} + +function minimizeViewVideoWin() { + viewVideoWin?.minimize(); +} + +function maximizeViewVideoWin() { + viewVideoWin?.maximize(); +} + +function unmaximizeViewVideoWin() { + viewVideoWin?.unmaximize(); +} + +function setAlwaysOnTopViewVideoWin(isAlwaysOnTop: boolean) { + viewVideoWin?.setAlwaysOnTop(isAlwaysOnTop); +} + +async function getHistoryVideoPath() { + // const historyVideoPath = ((await getHistoryVideo()) as string) || ''; + // return historyVideoPath; +} + +async function sendHistoryVideo() { + // const filePath = await getHistoryVideoPath(); + // let video = await readDirectoryVideo(filePath); + // return video; +} + +export { + closeViewVideoWin, + createViewVideoWin, + hideViewVideoWin, + maximizeViewVideoWin, + minimizeViewVideoWin, + openViewVideoWin, + sendHistoryVideo, + setAlwaysOnTopViewVideoWin, + unmaximizeViewVideoWin, +}; diff --git a/packages/desktop/index.html b/packages/desktop/index.html new file mode 100644 index 0000000..4d1e2a8 --- /dev/null +++ b/packages/desktop/index.html @@ -0,0 +1,16 @@ + + + + + + + + + pear-rec + + + +
+ + + \ No newline at end of file diff --git a/packages/desktop/package.json b/packages/desktop/package.json new file mode 100644 index 0000000..25d977d --- /dev/null +++ b/packages/desktop/package.json @@ -0,0 +1,43 @@ +{ + "name": "@pear-rec/desktop", + "version": "1.3.15", + "main": "dist-electron/main/index.js", + "description": "pear-rec", + "author": "027xiguapi", + "license": "Apache-2.0", + "private": true, + "debug": { + "env": { + "VITE_DEV_SERVER_URL": "http://127.0.0.1:7777/" + } + }, + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "build:win": "rimraf release && electron-builder", + "preview": "vite preview", + "pree2e": "vite build --mode=test", + "e2e": "playwright test", + "release": "electron-builder -p always", + "server": "node dist-electron/server/main", + "clear": "rimraf node_modules" + }, + "dependencies": { + "electron-updater": "^6.1.8" + }, + "devDependencies": { + "@pear-rec/server": "workspace:^", + "@playwright/test": "^1.37.1", + "@types/jsonfile": "^6.1.3", + "@types/uuid": "^9.0.6", + "@vitejs/plugin-react": "^4.0.4", + "electron": "^29.1.3", + "electron-builder": "^24.13.3", + "electron-log": "^5.1.2", + "jsonfile": "^6.1.0", + "typescript": "^5.2.2", + "uuid": "^9.0.1", + "vite": "^5.1.5", + "vite-plugin-electron": "^0.28.2" + } +} diff --git a/packages/desktop/playwright.config.ts b/packages/desktop/playwright.config.ts new file mode 100644 index 0000000..d323551 --- /dev/null +++ b/packages/desktop/playwright.config.ts @@ -0,0 +1,54 @@ +import type { PlaywrightTestConfig } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: "./e2e", + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + /* Folder for test artifacts such as screenshots, videos, traces, etc. */ + // outputDir: 'test-results/', + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // port: 3000, + // }, +}; + +export default config; diff --git a/packages/desktop/public/imgs/icons/mac/icon.icns b/packages/desktop/public/imgs/icons/mac/icon.icns new file mode 100644 index 0000000..4c4673f Binary files /dev/null and b/packages/desktop/public/imgs/icons/mac/icon.icns differ diff --git a/packages/desktop/public/imgs/icons/png/1024x1024.png b/packages/desktop/public/imgs/icons/png/1024x1024.png new file mode 100644 index 0000000..42d51a7 Binary files /dev/null and b/packages/desktop/public/imgs/icons/png/1024x1024.png differ diff --git a/packages/desktop/public/imgs/icons/png/128x128.png b/packages/desktop/public/imgs/icons/png/128x128.png new file mode 100644 index 0000000..c4421fe Binary files /dev/null and b/packages/desktop/public/imgs/icons/png/128x128.png differ diff --git a/packages/desktop/public/imgs/icons/png/16x16.png b/packages/desktop/public/imgs/icons/png/16x16.png new file mode 100644 index 0000000..06e6e46 Binary files /dev/null and b/packages/desktop/public/imgs/icons/png/16x16.png differ diff --git a/packages/desktop/public/imgs/icons/png/24x24.png b/packages/desktop/public/imgs/icons/png/24x24.png new file mode 100644 index 0000000..c966c41 Binary files /dev/null and b/packages/desktop/public/imgs/icons/png/24x24.png differ diff --git a/packages/desktop/public/imgs/icons/png/256x256.png b/packages/desktop/public/imgs/icons/png/256x256.png new file mode 100644 index 0000000..5ca674a Binary files /dev/null and b/packages/desktop/public/imgs/icons/png/256x256.png differ diff --git a/packages/desktop/public/imgs/icons/png/32x32.png b/packages/desktop/public/imgs/icons/png/32x32.png new file mode 100644 index 0000000..a44ff26 Binary files /dev/null and b/packages/desktop/public/imgs/icons/png/32x32.png differ diff --git a/packages/desktop/public/imgs/icons/png/48x48.png b/packages/desktop/public/imgs/icons/png/48x48.png new file mode 100644 index 0000000..f849f3e Binary files /dev/null and b/packages/desktop/public/imgs/icons/png/48x48.png differ diff --git a/packages/desktop/public/imgs/icons/png/512x512.png b/packages/desktop/public/imgs/icons/png/512x512.png new file mode 100644 index 0000000..7cb9372 Binary files /dev/null and b/packages/desktop/public/imgs/icons/png/512x512.png differ diff --git a/packages/desktop/public/imgs/icons/png/64x64.png b/packages/desktop/public/imgs/icons/png/64x64.png new file mode 100644 index 0000000..5bc75a3 Binary files /dev/null and b/packages/desktop/public/imgs/icons/png/64x64.png differ diff --git a/packages/desktop/public/imgs/icons/win/icon.ico b/packages/desktop/public/imgs/icons/win/icon.ico new file mode 100644 index 0000000..88538d4 Binary files /dev/null and b/packages/desktop/public/imgs/icons/win/icon.ico differ diff --git a/packages/desktop/public/imgs/logo/favicon.ico b/packages/desktop/public/imgs/logo/favicon.ico new file mode 100644 index 0000000..bc4f577 Binary files /dev/null and b/packages/desktop/public/imgs/logo/favicon.ico differ diff --git a/packages/desktop/public/imgs/logo/logo.png b/packages/desktop/public/imgs/logo/logo.png new file mode 100644 index 0000000..7fe7ebf Binary files /dev/null and b/packages/desktop/public/imgs/logo/logo.png differ diff --git a/packages/desktop/src/vite-env.d.ts b/packages/desktop/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/packages/desktop/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/desktop/tsconfig.json b/packages/desktop/tsconfig.json new file mode 100644 index 0000000..3e82908 --- /dev/null +++ b/packages/desktop/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ESNext", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "esModuleInterop": true, + "useDefineForClassFields": true, + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "allowJs": false, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "baseUrl": "./", + "paths": { + "@/*": [ + "src/*" + ] + } + }, + "include": [ + "src", + "electron" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} \ No newline at end of file diff --git a/packages/desktop/tsconfig.node.json b/packages/desktop/tsconfig.node.json new file mode 100644 index 0000000..032a750 --- /dev/null +++ b/packages/desktop/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true + }, + "include": [ + "vite.config.ts", + "package.json" + ] +} \ No newline at end of file diff --git a/packages/desktop/vite.config.ts b/packages/desktop/vite.config.ts new file mode 100644 index 0000000..c59fed2 --- /dev/null +++ b/packages/desktop/vite.config.ts @@ -0,0 +1,55 @@ +import { defineConfig } from 'vite'; +import electron from 'vite-plugin-electron/simple'; +import pkg from './package.json'; + +// https://vitejs.dev/config/ +export default defineConfig(({ command }) => { + // rmSync('dist-electron', { recursive: true, force: true }); + + const isServe = command === 'serve'; + const isBuild = command === 'build'; + const sourcemap = isServe || !!process.env.VSCODE_DEBUG; + + return { + plugins: [ + electron({ + main: { + entry: 'electron/main/index.ts', + vite: { + build: { + sourcemap: sourcemap ? 'inline' : undefined, // #332 + minify: isBuild, + outDir: 'dist-electron/main', + rollupOptions: { + external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}), + }, + }, + }, + }, + preload: { + input: 'electron/preload/index.ts', + vite: { + build: { + sourcemap: sourcemap ? 'inline' : undefined, // #332 + minify: isBuild, + outDir: 'dist-electron/preload', + rollupOptions: { + external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}), + }, + }, + }, + }, + }), + ], + server: + process.env.VSCODE_DEBUG && + (() => { + const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL); + return { + host: url.hostname, + port: +url.port, + }; + })(), + clearScreen: false, + }; +}); diff --git a/packages/docs/.vitepress/config.ts b/packages/docs/.vitepress/config.ts new file mode 100644 index 0000000..0c4a7d7 --- /dev/null +++ b/packages/docs/.vitepress/config.ts @@ -0,0 +1,48 @@ +import { defineConfig } from 'vitepress'; +import react from '@vitejs/plugin-react'; + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + vite: { + plugins: [react()], + }, + title: 'pear-rec', + base: '/pear-rec', + description: '一个跨平台的截图、录屏、录音、录像软件', + head: [['link', { rel: 'icon', href: '/favicon.ico' }]], + themeConfig: { + logo: '/favicon.ico', + siteTitle: '『 pear-rec 』', + outlineTitle: '🔴🟠🟡🟢🔵🟣🟤⚫⚪', + outline: [2, 6], + // 顶部导航 + nav: [ + { text: 'Home', link: '/' }, + { text: '文档', link: '/desktop/examples.md' }, + { text: '下载', link: 'https://github.com/027xiguapi/pear-rec/releases' }, + ], + // 侧边栏 + sidebar: [ + { + text: '文档', + items: [ + { text: '桌面软件', link: '/desktop/examples.md' }, + { text: '截图插件', link: '/screenshot/examples' }, + { text: '录音插件', link: '/recorder/examples.md' }, + { text: '计时插件', link: '/timer/examples' }, + { text: '网页应用', link: '/web/examples.md' }, + // { text: "markdown", link: "/markdown-examples.md" }, + // { text: "Runtime API Examples", link: "/api-examples" }, + ], + }, + ], + + socialLinks: [{ icon: 'github', link: 'https://github.com/027xiguapi/pear-rec' }], + // 页脚 + footer: { + message: + 'pear-rec is available under the Apache License V2.', + copyright: 'Copyright © 2023 西瓜皮', + }, + }, +}); diff --git a/packages/docs/assets/imgs/eg.jpg b/packages/docs/assets/imgs/eg.jpg new file mode 100644 index 0000000..a1d163c Binary files /dev/null and b/packages/docs/assets/imgs/eg.jpg differ diff --git a/packages/docs/assets/imgs/ei.jpg b/packages/docs/assets/imgs/ei.jpg new file mode 100644 index 0000000..19b55cf Binary files /dev/null and b/packages/docs/assets/imgs/ei.jpg differ diff --git a/packages/docs/assets/imgs/home.jpg b/packages/docs/assets/imgs/home.jpg new file mode 100644 index 0000000..51e3ab4 Binary files /dev/null and b/packages/docs/assets/imgs/home.jpg differ diff --git a/packages/docs/assets/imgs/logo.png b/packages/docs/assets/imgs/logo.png new file mode 100644 index 0000000..7fe7ebf Binary files /dev/null and b/packages/docs/assets/imgs/logo.png differ diff --git a/packages/docs/assets/imgs/ra.jpg b/packages/docs/assets/imgs/ra.jpg new file mode 100644 index 0000000..a323016 Binary files /dev/null and b/packages/docs/assets/imgs/ra.jpg differ diff --git a/packages/docs/assets/imgs/rs.jpg b/packages/docs/assets/imgs/rs.jpg new file mode 100644 index 0000000..c431e08 Binary files /dev/null and b/packages/docs/assets/imgs/rs.jpg differ diff --git a/packages/docs/assets/imgs/rv.jpg b/packages/docs/assets/imgs/rv.jpg new file mode 100644 index 0000000..b64b3b6 Binary files /dev/null and b/packages/docs/assets/imgs/rv.jpg differ diff --git a/packages/docs/assets/imgs/setting.jpg b/packages/docs/assets/imgs/setting.jpg new file mode 100644 index 0000000..e4b9f7f Binary files /dev/null and b/packages/docs/assets/imgs/setting.jpg differ diff --git a/packages/docs/assets/imgs/ss.jpg b/packages/docs/assets/imgs/ss.jpg new file mode 100644 index 0000000..e769726 Binary files /dev/null and b/packages/docs/assets/imgs/ss.jpg differ diff --git a/packages/docs/assets/imgs/va.jpg b/packages/docs/assets/imgs/va.jpg new file mode 100644 index 0000000..2e2cc28 Binary files /dev/null and b/packages/docs/assets/imgs/va.jpg differ diff --git a/packages/docs/assets/imgs/vi.jpg b/packages/docs/assets/imgs/vi.jpg new file mode 100644 index 0000000..d19b9d5 Binary files /dev/null and b/packages/docs/assets/imgs/vi.jpg differ diff --git a/packages/docs/assets/imgs/vv.jpg b/packages/docs/assets/imgs/vv.jpg new file mode 100644 index 0000000..1580755 Binary files /dev/null and b/packages/docs/assets/imgs/vv.jpg differ diff --git a/packages/docs/deploy.sh b/packages/docs/deploy.sh new file mode 100644 index 0000000..5d32d39 --- /dev/null +++ b/packages/docs/deploy.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env sh + +# 忽略错误 +set -e + +# 构建 +npm run build + +# 进入待发布的目录 +cd ./.vitepress/dist + +# 如果是发布到自定义域名 +# echo 'www.example.com' > CNAME + +git init +git add -A +git commit -m 'deploy' + +# 如果部署到 https://.github.io +# git push -f git@github.com:/.github.io.git master + +# 如果是部署到 https://.github.io/ +git push -f git@github.com:027xiguapi/pear-rec.git master:gh-pages + +cd - \ No newline at end of file diff --git a/packages/docs/desktop/examples.md b/packages/docs/desktop/examples.md new file mode 100644 index 0000000..ff7cb58 --- /dev/null +++ b/packages/docs/desktop/examples.md @@ -0,0 +1,79 @@ +# pear-rec 例子 + +
+ +
+ +
本页展示 pear-rec 的一些功能例子
+ +## 架构 + +
+ +
+ +## 首页 + +
+ +
+ +## 截图 + +
+ +
+ +## 录屏 + +
+ +
+ +## 录音 + +
+ +
+ +## 录像 + +
+ +
+ +## 编辑图片 + +
+ +
+ +## 编辑 gif + +
+ +
+ +## 查看图片 + +
+ +
+ +## 查看视频 + +
+ +
+ +## 查看音频 + +
+ +
+ +## 设置 + +
+ +
diff --git a/packages/docs/index.md b/packages/docs/index.md new file mode 100644 index 0000000..03ea24c --- /dev/null +++ b/packages/docs/index.md @@ -0,0 +1,53 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: "🍐 pear-rec" + text: "一个跨平台的截图、录屏、录音、录像软件" + tagline: 简单、高效、优雅 + actions: + - theme: brand + text: 🖥︎ 桌面软件 + link: /desktop/examples + - theme: alt + text: ✂️ 截图插件 + link: /screenshot/examples + - theme: alt + text: ⚙️ 录音插件 + link: /recorder/examples + - theme: alt + text: ⏲️ 计时插件 + link: /timer/examples + - theme: alt + text: 🌐 网页应用 + link: /web/examples + +features: + - title: 🖥︎ 桌面软件 + details: 有截图、录屏、录音、录像、图片查看、视频查看等功能。 + - title: 🌐 网页应用 + details: 有截图、录屏、录音、录像、图片查看、视频查看等功能。 + - title: ⚙️ recorderjs + details: 截图、录屏、录音、录像 插件。 + - title: ⏲️ timer + details: 一个计时插件。 +--- + +

+ +

diff --git a/packages/docs/md/api-examples.md b/packages/docs/md/api-examples.md new file mode 100644 index 0000000..691df9c --- /dev/null +++ b/packages/docs/md/api-examples.md @@ -0,0 +1,55 @@ +--- +outline: deep +--- + +# Runtime API Examples + +This page demonstrates usage of some of the runtime APIs provided by VitePress. + +The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files: + +```md + + +## Results + +### Theme Data + +
{{ theme }}
+ +### Page Data + +
{{ page }}
+ +### Page Frontmatter + +
{{ frontmatter }}
+``` + + + +## Results + +### Theme Data + +
{{ theme }}
+ +### Page Data + +
{{ page }}
+ +### Page Frontmatter + +
{{ frontmatter }}
+ +## More + +Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata). diff --git a/packages/docs/md/markdown-examples.md b/packages/docs/md/markdown-examples.md new file mode 100644 index 0000000..c919486 --- /dev/null +++ b/packages/docs/md/markdown-examples.md @@ -0,0 +1,107 @@ +# pear-rec 例子 + +![An image](../assets/imgs/logo.png) +本页展示 pear-rec 的一些功能例子 + +## Syntax Highlighting + +VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting: + +**Input** + +```` +```js{4} +export default { + data () { + return { + msg: 'Highlighted!' + } + } +} +``` +```` + +**Output** + +```js{4} +export default { + data () { + return { + msg: 'Highlighted!' + } + } +} +``` + +## Custom Containers + +**Input** + +```md +::: info +This is an info box. +::: + +::: tip +This is a tip. +::: + +::: warning +This is a warning. +::: + +::: danger +This is a dangerous warning. +::: + +::: details +This is a details block. +::: +``` + +**Output** + +::: info +This is an info box. +::: + +::: tip +This is a tip. +::: + +::: warning +This is a warning. +::: + +::: danger +This is a dangerous warning. +::: + +::: details +This is a details block. +::: + +## react + +https://stackblitz.com/edit/vite-dnqmyv?file=package.json + +``` +
+ + +``` + +## More + +Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown). diff --git a/packages/docs/md/qrcode-examples.md b/packages/docs/md/qrcode-examples.md new file mode 100644 index 0000000..ea970ac --- /dev/null +++ b/packages/docs/md/qrcode-examples.md @@ -0,0 +1,36 @@ +--- +title: QrCode +--- + +# QrCode + +## Scanner/Reader + +- [zxing](https://github.com/zxing/zxing) + - Apache 2.0, Java + - [Online Decoder](https://zxing.org/w/decode.jspx) +- Command + - [ZBar/ZBar](https://github.com/ZBar/ZBar) + - 2012 + - zbarimg + - [libdmtx](https://github.com/dmtx/libdmtx) + - dmtxread +- JS + - [nimiq/qr-scanner](https://github.com/nimiq/qr-scanner)(推荐) + - 不能 NodeJS + - [demo](https://nimiq.github.io/qr-scanner/demo/) + - [@zxing/library](https://github.com/zxing-js/library) + - 功能最多 + - NodeJS 需要 jimp 提取亮度通道 + - [demo](https://zxing-js.github.io/library/) + - [qrcode-reader](https://github.com/edi9999/jsqrcode) + - 很小 + - NodeJS 可直接用 jimp 数据 + - [jsqr](https://github.com/cozmo/jsQR) + - 不支持多个二维码 + - [demo](https://cozmo.github.io/jsQR/) + - [html5-qrcode](https://github.com/mebjas/html5-qrcode) + - [demo](https://scanapp.org/) + - [qr-scanner-wechat](https://github.com/antfu/qr-scanner-wechat) + - [demo](https://qrcode-wechat.netlify.app/) + - [文章](https://juejin.cn/post/7290813210276724771?searchId=202403220940400452709FA51954DFEEDF) diff --git a/packages/docs/package.json b/packages/docs/package.json new file mode 100644 index 0000000..d27ebe9 --- /dev/null +++ b/packages/docs/package.json @@ -0,0 +1,33 @@ +{ + "name": "@pear-rec/docs", + "version": "1.0.3", + "license": "Apache-2.0", + "main": "index.js", + "scripts": { + "dev": "vitepress dev", + "build": "vitepress build", + "preview": "vitepress preview", + "deploy": "deploy.sh" + }, + "devDependencies": { + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@vitejs/plugin-react": "^3.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "vitepress": "1.0.0-beta.1" + }, + "keywords": [ + "pear-rec", + "docs", + "vitepress" + ], + "author": "027xiguapi", + "description": "pear-rec docs", + "dependencies": { + "@pear-rec/screenshot": "workspace:^", + "@pear-rec/timer": "workspace:^", + "@pear-rec/web": "workspace:^", + "react-timer-hook": "^3.0.6" + } +} \ No newline at end of file diff --git a/packages/docs/public/favicon.ico b/packages/docs/public/favicon.ico new file mode 100644 index 0000000..bc4f577 Binary files /dev/null and b/packages/docs/public/favicon.ico differ diff --git a/packages/docs/public/imgs/1700442414996.jpg b/packages/docs/public/imgs/1700442414996.jpg new file mode 100644 index 0000000..c020c72 Binary files /dev/null and b/packages/docs/public/imgs/1700442414996.jpg differ diff --git a/packages/docs/public/imgs/pear-rec_qq_qrcode.png b/packages/docs/public/imgs/pear-rec_qq_qrcode.png new file mode 100644 index 0000000..417934e Binary files /dev/null and b/packages/docs/public/imgs/pear-rec_qq_qrcode.png differ diff --git a/packages/docs/public/imgs/screenshot.jpg b/packages/docs/public/imgs/screenshot.jpg new file mode 100644 index 0000000..85ac47a Binary files /dev/null and b/packages/docs/public/imgs/screenshot.jpg differ diff --git a/packages/docs/public/imgs/th.webp b/packages/docs/public/imgs/th.webp new file mode 100644 index 0000000..6d335e6 Binary files /dev/null and b/packages/docs/public/imgs/th.webp differ diff --git a/packages/docs/public/logo.png b/packages/docs/public/logo.png new file mode 100644 index 0000000..7fe7ebf Binary files /dev/null and b/packages/docs/public/logo.png differ diff --git a/packages/docs/recorder/examples.md b/packages/docs/recorder/examples.md new file mode 100644 index 0000000..ff19ade --- /dev/null +++ b/packages/docs/recorder/examples.md @@ -0,0 +1,230 @@ +--- +outline: deep +--- + +# Recorder API Examples + +本页是介绍`@pear-rec/recorder` 插件的`API`例子 + +本插件提供了录屏、录音和录像功能。 + +## 安装 + +```js +import { audio, video, screen } from "@pear-rec/recorder"; +``` + +## 功能 + +### 录屏 + +```js +const screen = mediajs.screen(); +screen.create(); +``` + +### 录音 + +```js +const audio = mediajs.audio(); +audio.create(); +``` + +### 录像 + +```javascript +const video = mediajs.video(); +video.create(); +``` + +## SoundMeter + +```javascript +soundMeter.prototype = { + // return recording instant value + getInstant: function () {}, + + // return recording slow value + getSlow: function () {}, + + // return recording clip value + getClip: function () {}, +}; +``` + +## Function + +```javascript +mediajs.prototype = { + // create the recording + create: async function () {}, + + // destroy the recording + destroy: function () {}, + + // start the recording + start: function () {}, + + // stop the recording + stop: function () {}, + + // pause the recording + pause: function () {}, + + // resume the recording + resume: function () {}, + + // return recorded Blob + getBlob: function () {}, + + // download recorded Blob + downloadBlob: function (filename) {}, + + // return recorded Blob-Url + getBlobUrl: function () {}, + + // return recorded duration (ms) + getDuration: function () {}, + + // return media state ("inactive" | "wait" | "ready") + getMediaState: function () {}, + + // return media type ("audio" | "video" | "screen") + getMediaType: function () {}, + + // return media stream + getMedisStream: function () {}, + + // return Recorder state ("inactive" | "paused" | "recording") + getRecorderState: function () {}, + + // return SoundMeter + getSoundMeter: function () {}, + + // return timeSlice (ms) + getTimeSlice: function () {}, + + // return boolean, which is true if media state is ready + isReady: function () {}, + + // return a list of the available media input and output devices + _enumerateDevices: async function () {}, + + // returns a Boolean which is true if the MIME type specified is one the user agent should be able to successfully record. + _isTypeSupported: function (mimeType) {}, +}; +``` + +## Callback Function + +```javascript +media + .onerror((err) => { + console.log(err); + }) + .oncreate(() => { + const stream = audio.getMedisStream(); + console.log(stream); + }) + .ondestroy(() => { + console.log("destroy"); + }) + .onstart(() => { + console.log("start"); + }) + .onstop(() => { + console.log("stop"); + }) + .onpause(() => { + console.log("pause"); + }) + .onresume(() => { + console.log("resume"); + }) + .ondataavailable(() => { + console.log("dataavailable"); + }); +``` + +## Configuration + +**audio:** + +```javascript +{ + audio: boolean | MediaTrackConstraints; + mimeType: string; + audioBitsPerSecond: number; + sampleRate: number; + timeSlice: number; + echoCancellation: boolean; +} +``` + +**video and screen:** + +```javascript +{ + audio: boolean | MediaTrackConstraints; + + video: boolean | MediaTrackConstraints; + + // audio/webm + // audio/webm;codecs=pcm + // video/mp4 + // video/webm;codecs=vp9 + // video/webm;codecs=vp8 + // video/webm;codecs=h264 + // video/x-matroska;codecs=avc1 + // video/mpeg -- NOT supported by any browser, yet + // audio/wav + // audio/ogg -- ONLY Firefox + mimeType: string; + + // only for audio track + // ignored when codecs=pcm + audioBitsPerSecond: number; + + // used by StereoAudioRecorder + // the range 22050 to 96000. + sampleRate: number; + + // get intervals based blobs, value in milliseconds + timeSlice: number; + + // Echo cancellation + echoCancellation: boolean; + + // only for video track + videoBitsPerSecond: number; + width: number; + height: number; + pan: boolean; + tilt: boolean; + zoom: boolean; +} +``` + +## Env APIs + +```javascript +mediajs.Env = { + isIOS: boolean, + minChromeVersion: number, + minFirefoxVersion: number, + minSafariVersion: number, + supportedBrowsers: array, + prototype = { + getBrowser: function() {}, + getVersion: function() {}, + isAudioContextSupported: function() {}, + isAudioWorkletNode: function() {}, + isBrowserSupported: function() {}, + isGetUserMediaSupported: function() {}, + isMediaDevicesSupported: function() {}, + isMediaRecorderSupported: function() {}, + isUnifiedPlanSupported: function() {}, + toString: function() {}, + } +}; +``` diff --git a/packages/docs/screenshot/app.jsx b/packages/docs/screenshot/app.jsx new file mode 100644 index 0000000..8daa8a7 --- /dev/null +++ b/packages/docs/screenshot/app.jsx @@ -0,0 +1,43 @@ +import React, { ReactElement, useCallback } from "react"; +import Screenshots from "@pear-rec/screenshot"; +import "@pear-rec/screenshot/lib/style.css"; +import "./app.scss"; +import imageUrl from "/imgs/th.webp"; + +export default function App() { + const onSave = useCallback((blob, bounds) => { + console.log("save", blob, bounds); + if (blob) { + const url = URL.createObjectURL(blob); + console.log(url); + window.open(url); + } + }, []); + const onCancel = useCallback(() => { + console.log("cancel"); + }, []); + const onOk = useCallback((blob, bounds) => { + console.log("ok", blob, bounds); + if (blob) { + const url = URL.createObjectURL(blob); + console.log(url); + window.open(url); + } + }, []); + + return ( +
+ +
+ ); +} diff --git a/packages/docs/screenshot/app.scss b/packages/docs/screenshot/app.scss new file mode 100644 index 0000000..6e2f35d --- /dev/null +++ b/packages/docs/screenshot/app.scss @@ -0,0 +1,4 @@ +.body { + height: 100vh; + overflow: hidden; +} diff --git a/packages/docs/screenshot/demo.md b/packages/docs/screenshot/demo.md new file mode 100644 index 0000000..a16619b --- /dev/null +++ b/packages/docs/screenshot/demo.md @@ -0,0 +1,22 @@ +--- +layout: false +--- + +
+ + + + diff --git a/packages/docs/screenshot/examples.md b/packages/docs/screenshot/examples.md new file mode 100644 index 0000000..0c38464 --- /dev/null +++ b/packages/docs/screenshot/examples.md @@ -0,0 +1,76 @@ +--- +outline: deep +--- + +# Screenshot API Examples + +本页是介绍`@pear-rec/screenshot` 插件的`API`例子 + +本插件提供了截图插件。 + +## 安装 + +```js +import "@pear-rec/screenshot"; +import "@pear-rec/screenshot/lib/style.css"; +``` + +## screenshot 截图插件 + +### 效果展示 + +在线示例: DEMO + +- https://027xiguapi.github.io/pear-rec/screenshot/demo.html + +
+ +
+ +### 完整代码 + +```js +import React, { ReactElement, useCallback } from "react"; +import Screenshots from "@pear-rec/screenshot"; +import "@pear-rec/screenshot/lib/style.css"; +import "./app.scss"; +import imageUrl from "/imgs/th.webp"; + +export default function App() { + const onSave = useCallback((blob, bounds) => { + console.log("save", blob, bounds); + if (blob) { + const url = URL.createObjectURL(blob); + console.log(url); + window.open(url); + } + }, []); + const onCancel = useCallback(() => { + console.log("cancel"); + }, []); + const onOk = useCallback((blob, bounds) => { + console.log("ok", blob, bounds); + if (blob) { + const url = URL.createObjectURL(blob); + console.log(url); + window.open(url); + } + }, []); + + return ( +
+ +
+ ); +} +``` diff --git a/packages/docs/timer/app.jsx b/packages/docs/timer/app.jsx new file mode 100644 index 0000000..e4d8d4b --- /dev/null +++ b/packages/docs/timer/app.jsx @@ -0,0 +1,38 @@ +import { useState } from "react"; +import Timer from "@pear-rec/timer"; +import "@pear-rec/timer/src/Timer/index.module.scss"; +import * as reactTimerHook from "react-timer-hook"; +const { useStopwatch } = reactTimerHook; + +function App() { + const { seconds, minutes, hours, days, start, pause, reset } = useStopwatch({ + autoStart: true, + }); + + const [isShowTitle] = useState(true); + return ( +
+

UseStopwatch Demo

+
+ +
+
+ Start +
+
+ Pause +
+
reset}> + Reset +
+
+ ); +} + +export default App; diff --git a/packages/docs/timer/examples.md b/packages/docs/timer/examples.md new file mode 100644 index 0000000..dc485b4 --- /dev/null +++ b/packages/docs/timer/examples.md @@ -0,0 +1,99 @@ +--- +outline: deep +--- + +# Timer API Examples + +本页是介绍`@pear-rec/timer` 插件的`API`例子 + +本插件提供了计时器功能。 + +## 安装 + +```js +import "@pear-rec/timer"; +import "@pear-rec/timer/src/Timer/index.module.scss"; +``` + +## timer 计时器 + +### 效果展示 + +
+ + + + + +### 完整代码 + +```js +import { useState } from "react"; +import { useStopwatch } from "react-timer-hook"; +import Timer from "@pear-rec/timer"; +import "@pear-rec/timer/src/Timer/index.module.scss"; + +function App() { + const { seconds, minutes, hours, days, start, pause, reset } = useStopwatch({ + autoStart: true, + }); + + const [isShowTitle] = useState(true); + return ( +
+

UseStopwatch Demo

+
+ +
+
+ Start +
+
+ Pause +
+
reset}> + Reset +
+
+ ); +} + +export default App; +``` diff --git a/packages/docs/web/examples.md b/packages/docs/web/examples.md new file mode 100644 index 0000000..bff5327 --- /dev/null +++ b/packages/docs/web/examples.md @@ -0,0 +1,16 @@ +--- +outline: deep +--- + +# Web API Examples + +本页是介绍`@pear-rec/web` 插件的`API`例子 + +本插件提供了录屏、录音、录像、预览图片、预览视频功能。 + +## 安装 + +```js +import "@pear-rec/web"; +import "@pear-rec/web/lib/style.css"; +``` diff --git a/packages/recorder/CHANGELOG.md b/packages/recorder/CHANGELOG.md new file mode 100644 index 0000000..f90f936 --- /dev/null +++ b/packages/recorder/CHANGELOG.md @@ -0,0 +1,13 @@ +# @pear-rec/recorder + +## 1.1.0 + +### Minor Changes + +- v1.1.0 + +## 1.0.0 (2023-09-05) + +### Patch Changes + +- v1.0.0 ([51d04715](https://github.com/027xiguapi/pear-rec/commit/51d04715b7f2277185ebdb6dfa78527c70b11f03)) diff --git a/packages/recorder/index.d.ts b/packages/recorder/index.d.ts new file mode 100644 index 0000000..5cd0cb1 --- /dev/null +++ b/packages/recorder/index.d.ts @@ -0,0 +1,10 @@ +import { Media } from "./lib/media"; +import type { AudioOption, VideoOption, ScreenOption } from "./lib/type"; + +export function audio(option?: AudioOption): Media; + +export function video(option?: VideoOption): Media; + +export function screen(option?: ScreenOption): Media; + +export function desktop(option?: ScreenOption): Media; diff --git a/packages/recorder/index.html b/packages/recorder/index.html new file mode 100644 index 0000000..4aca9cb --- /dev/null +++ b/packages/recorder/index.html @@ -0,0 +1,16 @@ + + + + + + + + Vite App + + + +
+ + + + \ No newline at end of file diff --git a/packages/recorder/package.json b/packages/recorder/package.json new file mode 100644 index 0000000..4ea2289 --- /dev/null +++ b/packages/recorder/package.json @@ -0,0 +1,32 @@ +{ + "name": "@pear-rec/recorder", + "private": true, + "version": "1.1.0", + "type": "module", + "files": [ + "dist", + "index.d.ts" + ], + "main": "lib/index.ts", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "watch": "tsc && vite build -w", + "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "webrtc-adapter": "^8.2.2" + }, + "devDependencies": { + "typescript": "^5.2.2", + "vite": "^4.3.9" + }, + "directories": { + "lib": "lib" + }, + "keywords": [], + "author": "", + "license": "Apache-2.0", + "description": "" +} \ No newline at end of file diff --git a/packages/recorder/src/main.ts b/packages/recorder/src/main.ts new file mode 100644 index 0000000..085d73c --- /dev/null +++ b/packages/recorder/src/main.ts @@ -0,0 +1,2 @@ +import { audio, video, screen } from "../lib"; +console.log({ audio, video, screen }); diff --git a/packages/recorder/src/vite-env.d.ts b/packages/recorder/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/packages/recorder/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/recorder/src/worklet.js b/packages/recorder/src/worklet.js new file mode 100644 index 0000000..ac69c2e --- /dev/null +++ b/packages/recorder/src/worklet.js @@ -0,0 +1,33 @@ +class SoundMeterWorklet extends AudioWorkletProcessor { + instant; + slow; + clip; + constructor() { + super(); + this.instant = 0.0; + this.slow = 0.0; + this.clip = 0.0; + } + + process(inputs, outputs, parameters) { + console.log("inputs", inputs); + console.log("outputs", outputs); + console.log("parameters", parameters); + const input = inputs[0][0]; + let i; + let sum = 0.0; + let clipcount = 0; + for (i = 0; i < input.length; ++i) { + sum += input[i] * input[i]; + if (Math.abs(input[i]) > 0.99) { + clipcount += 1; + } + } + this.instant = Math.sqrt(sum / input.length); + this.slow = 0.95 * this.slow + 0.05 * this.instant; + this.clip = clipcount / input.length; + return true; + } +} + +registerProcessor("sound-meter-processor", SoundMeterWorklet); diff --git a/packages/recorder/tsconfig.json b/packages/recorder/tsconfig.json new file mode 100644 index 0000000..3dd1d40 --- /dev/null +++ b/packages/recorder/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "sourceMap": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + // "noImplicitReturns": true, + "skipLibCheck": true + }, + "include": ["src", "lib/utils.ts"] +} diff --git a/packages/recorder/vite.config.ts b/packages/recorder/vite.config.ts new file mode 100644 index 0000000..70b119a --- /dev/null +++ b/packages/recorder/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vite"; +import { resolve } from "path"; + +export default defineConfig({ + build: { + lib: { + entry: resolve(__dirname, "lib/index.ts"), + name: "recorder", + fileName: (format) => `recorder.${format}.js`, + }, + }, +}); diff --git a/packages/screenshot/CHANGELOG.md b/packages/screenshot/CHANGELOG.md new file mode 100644 index 0000000..9343ffe --- /dev/null +++ b/packages/screenshot/CHANGELOG.md @@ -0,0 +1,13 @@ +# @pear-rec/screenshot + +## 1.1.0 + +### Minor Changes + +- v1.1.0 + +## 1.0.0 (2023-09-05) + +### Patch Changes + +- v1.0.0 ([51d04715](https://github.com/027xiguapi/pear-rec/commit/51d04715b7f2277185ebdb6dfa78527c70b11f03)) diff --git a/packages/screenshot/electron.html b/packages/screenshot/electron.html new file mode 100644 index 0000000..fff92d5 --- /dev/null +++ b/packages/screenshot/electron.html @@ -0,0 +1,15 @@ + + + + + + + screenshot + + + +
+ + + + \ No newline at end of file diff --git a/packages/screenshot/index.html b/packages/screenshot/index.html new file mode 100644 index 0000000..b0a52ae --- /dev/null +++ b/packages/screenshot/index.html @@ -0,0 +1,15 @@ + + + + + + + screenshot + + + +
+ + + + \ No newline at end of file diff --git a/packages/screenshot/package.json b/packages/screenshot/package.json new file mode 100644 index 0000000..17455ea --- /dev/null +++ b/packages/screenshot/package.json @@ -0,0 +1,39 @@ +{ + "name": "@pear-rec/screenshot", + "private": true, + "version": "1.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build && tsc --project tsconfig.build.json", + "watch": "tsc && vite build -w", + "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "qr-scanner": "^1.4.2", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@vitejs/plugin-react-swc": "^3.0.0", + "rollup-plugin-visualizer": "^5.9.2", + "typescript": "^5.2.2", + "vite": "^4.3.9" + }, + "main": "src/Screenshots/exports.ts", + "directories": { + "lib": "lib" + }, + "keywords": [ + "react-screenshots", + "screenshot", + "cropper", + "react" + ], + "author": "027xiguapi", + "license": "Apache-2.0", + "description": "a screenshot cropper tool by react" +} \ No newline at end of file diff --git a/packages/screenshot/public/imgs/svg/scan.svg b/packages/screenshot/public/imgs/svg/scan.svg new file mode 100644 index 0000000..5be03bf --- /dev/null +++ b/packages/screenshot/public/imgs/svg/scan.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/screenshot/public/imgs/svg/search.svg b/packages/screenshot/public/imgs/svg/search.svg new file mode 100644 index 0000000..4135cc8 --- /dev/null +++ b/packages/screenshot/public/imgs/svg/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/screenshot/public/imgs/th.webp b/packages/screenshot/public/imgs/th.webp new file mode 100644 index 0000000..6d335e6 Binary files /dev/null and b/packages/screenshot/public/imgs/th.webp differ diff --git a/packages/screenshot/src/Screenshots/ScreenshotsBackground/getBoundsByPoints.ts b/packages/screenshot/src/Screenshots/ScreenshotsBackground/getBoundsByPoints.ts new file mode 100644 index 0000000..361be8c --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsBackground/getBoundsByPoints.ts @@ -0,0 +1,41 @@ +import { Point, Bounds } from '../types' + +export default function getBoundsByPoints ( + { x: x1, y: y1 }: Point, + { x: x2, y: y2 }: Point, + width: number, + height: number +): Bounds { + // 交换值 + if (x1 > x2) { + [x1, x2] = [x2, x1] + } + + if (y1 > y2) { + [y1, y2] = [y2, y1] + } + + // 把图形限制在元素里面 + if (x1 < 0) { + x1 = 0 + } + + if (x2 > width) { + x2 = width + } + + if (y1 < 0) { + y1 = 0 + } + + if (y2 > height) { + y2 = height + } + + return { + x: x1, + y: y1, + width: x2 - x1, + height: y2 - y1 + } +} diff --git a/packages/screenshot/src/Screenshots/ScreenshotsBackground/index.scss b/packages/screenshot/src/Screenshots/ScreenshotsBackground/index.scss new file mode 100644 index 0000000..595fe3e --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsBackground/index.scss @@ -0,0 +1,27 @@ +.screenshots-background { + width: 100%; + height: 100%; + position: relative; + + &-image { + width: 100%; + height: 100%; + display: block; + border: none; + outline: none; + image-rendering: -webkit-optimize-contrast; + image-rendering: crisp-edges; + -webkit-font-smooting: antialiased; + } + + &-mask { + width: 100%; + height: 100%; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: rgba(0, 0, 0, 0.3); + } +} diff --git a/packages/screenshot/src/Screenshots/ScreenshotsBackground/index.tsx b/packages/screenshot/src/Screenshots/ScreenshotsBackground/index.tsx new file mode 100644 index 0000000..728ff19 --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsBackground/index.tsx @@ -0,0 +1,145 @@ +import React, { + memo, + ReactElement, + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, +} from "react"; +import useBounds from "../hooks/useBounds"; +import useStore from "../hooks/useStore"; +import ScreenshotsMagnifier from "../ScreenshotsMagnifier"; +import { Point, Position } from "../types"; +import getBoundsByPoints from "./getBoundsByPoints"; +import "./index.scss"; + +export default memo(function ScreenshotsBackground(): ReactElement | null { + const { url, image, width, height } = useStore(); + const [bounds, boundsDispatcher] = useBounds(); + + const elRef = useRef(null); + const pointRef = useRef(null); + // 用来判断鼠标是否移动过 + // 如果没有移动过位置,则mouseup时不更新 + const isMoveRef = useRef(false); + const [position, setPosition] = useState(null); + + const updateBounds = useCallback( + (p1: Point, p2: Point) => { + if (!elRef.current) { + return; + } + const { x, y } = elRef.current.getBoundingClientRect(); + + boundsDispatcher.set( + getBoundsByPoints( + { + x: p1.x - x, + y: p1.y - y, + }, + { + x: p2.x - x, + y: p2.y - y, + }, + width, + height, + ), + ); + }, + [width, height, boundsDispatcher], + ); + + const onMouseDown = useCallback( + (e: React.MouseEvent) => { + // e.button 鼠标左键 + if (pointRef.current || bounds || e.button !== 0) { + return; + } + pointRef.current = { + x: e.clientX, + y: e.clientY, + }; + isMoveRef.current = false; + }, + [bounds], + ); + + useEffect(() => { + const onMouseMove = (e: MouseEvent) => { + if (elRef.current) { + const rect = elRef.current.getBoundingClientRect(); + if ( + e.clientX < rect.left || + e.clientY < rect.top || + e.clientX > rect.right || + e.clientY > rect.bottom + ) { + setPosition(null); + } else { + setPosition({ + x: e.clientX - rect.x, + y: e.clientY - rect.y, + }); + } + } + + if (!pointRef.current) { + return; + } + updateBounds(pointRef.current, { + x: e.clientX, + y: e.clientY, + }); + isMoveRef.current = true; + }; + + const onMouseUp = (e: MouseEvent) => { + if (!pointRef.current) { + return; + } + + if (isMoveRef.current) { + updateBounds(pointRef.current, { + x: e.clientX, + y: e.clientY, + }); + } + pointRef.current = null; + isMoveRef.current = false; + }; + window.addEventListener("mousemove", onMouseMove); + window.addEventListener("mouseup", onMouseUp); + + return () => { + window.removeEventListener("mousemove", onMouseMove); + window.removeEventListener("mouseup", onMouseUp); + }; + }, [updateBounds]); + + useLayoutEffect(() => { + if (!image || bounds) { + // 重置位置 + setPosition(null); + } + }, [image, bounds]); + + // 没有加载完不显示图片 + if (!url || !image) { + return null; + } + + return ( +
+ +
+ {position && !bounds && ( + + )} +
+ ); +}); diff --git a/packages/screenshot/src/Screenshots/ScreenshotsButton/index.scss b/packages/screenshot/src/Screenshots/ScreenshotsButton/index.scss new file mode 100644 index 0000000..433dd10 --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsButton/index.scss @@ -0,0 +1,29 @@ +@import "../var.scss"; + +.screenshots-button { + width: $button-size; + height: $button-size; + line-height: $button-size; + color: #333; + font-size: 22px; + text-align: center; + margin: 0 3px; + vertical-align: middle; + cursor: pointer; + + &-checked, + &:hover { + background-color: #eee; + outline: 1px solid #777; + } + + &-disabled { + color: #bbb; + cursor: not-allowed; + + &:hover { + background-color: #fff; + outline: none; + } + } +} diff --git a/packages/screenshot/src/Screenshots/ScreenshotsButton/index.tsx b/packages/screenshot/src/Screenshots/ScreenshotsButton/index.tsx new file mode 100644 index 0000000..6100609 --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsButton/index.tsx @@ -0,0 +1,58 @@ +import React, { + memo, + ReactElement, + PointerEvent, + ReactNode, + useCallback, +} from "react"; +import ScreenshotsOption from "../ScreenshotsOption"; +import "./index.scss"; + +export interface ScreenshotsButtonProps { + title: string; + icon: string; + checked?: boolean; + disabled?: boolean; + option?: ReactNode; + onClick?: (e: PointerEvent) => unknown; +} + +export default memo(function ScreenshotsButton({ + title, + icon, + checked, + disabled, + option, + onClick, +}: ScreenshotsButtonProps): ReactElement { + const classNames = ["screenshots-button"]; + + const onButtonClick = useCallback( + (e: PointerEvent) => { + if (disabled || !onClick) { + return; + } + onClick(e); + }, + [disabled, onClick], + ); + + if (checked) { + classNames.push("screenshots-button-checked"); + } + if (disabled) { + classNames.push("screenshots-button-disabled"); + } + + return ( + +
+ +
+
+ ); +}); diff --git a/packages/screenshot/src/Screenshots/ScreenshotsCanvas/getBoundsByPoints.ts b/packages/screenshot/src/Screenshots/ScreenshotsCanvas/getBoundsByPoints.ts new file mode 100644 index 0000000..e88ae7e --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsCanvas/getBoundsByPoints.ts @@ -0,0 +1,55 @@ +import { Bounds, Point } from '../types' + +export default function getBoundsByPoints ( + { x: x1, y: y1 }: Point, + { x: x2, y: y2 }: Point, + bounds: Bounds, + width: number, + height: number, + resizeOrMove: string +): Bounds { + // 交换值 + if (x1 > x2) { + [x1, x2] = [x2, x1] + } + + if (y1 > y2) { + [y1, y2] = [y2, y1] + } + + // 把图形限制在元素里面 + if (x1 < 0) { + x1 = 0 + if (resizeOrMove === 'move') { + x2 = bounds.width + } + } + + if (x2 > width) { + x2 = width + if (resizeOrMove === 'move') { + x1 = x2 - bounds.width + } + } + + if (y1 < 0) { + y1 = 0 + if (resizeOrMove === 'move') { + y2 = bounds.height + } + } + + if (y2 > height) { + y2 = height + if (resizeOrMove === 'move') { + y1 = y2 - bounds.height + } + } + + return { + x: x1, + y: y1, + width: Math.max(x2 - x1, 1), + height: Math.max(y2 - y1, 1) + } +} diff --git a/packages/screenshot/src/Screenshots/ScreenshotsCanvas/getPoints.ts b/packages/screenshot/src/Screenshots/ScreenshotsCanvas/getPoints.ts new file mode 100644 index 0000000..2fb00e7 --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsCanvas/getPoints.ts @@ -0,0 +1,60 @@ +import { Point, Bounds } from '../types' + +export default function getBoundsByPoints (e: MouseEvent, resizeOrMove: string, point: Point, bounds: Bounds): Point[] { + const x = e.clientX - point.x + const y = e.clientY - point.y + let x1 = bounds.x + let y1 = bounds.y + let x2 = bounds.x + bounds.width + let y2 = bounds.y + bounds.height + + switch (resizeOrMove) { + case 'top': + y1 += y + break + case 'top-right': + x2 += x + y1 += y + break + case 'right': + x2 += x + break + case 'right-bottom': + x2 += x + y2 += y + break + case 'bottom': + y2 += y + break + case 'bottom-left': + x1 += x + y2 += y + break + case 'left': + x1 += x + break + case 'left-top': + x1 += x + y1 += y + break + case 'move': + x1 += x + y1 += y + x2 += x + y2 += y + break + default: + break + } + + return [ + { + x: x1, + y: y1 + }, + { + x: x2, + y: y2 + } + ] +} diff --git a/packages/screenshot/src/Screenshots/ScreenshotsCanvas/index.scss b/packages/screenshot/src/Screenshots/ScreenshotsCanvas/index.scss new file mode 100644 index 0000000..d9a30e0 --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsCanvas/index.scss @@ -0,0 +1,84 @@ +@import "../var.scss"; + +.screenshots-canvas { + position: absolute; + left: 0; + top: 0; + will-change: width, height, transform; + + &-body, + &-mask { + width: 100%; + height: 100%; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + overflow: hidden; + } + + &-image { + display: block; + border: none; + outline: none; + will-change: transform; + max-width: unset; + image-rendering: -webkit-optimize-contrast; + image-rendering: crisp-edges; + -webkit-font-smooting: antialiased; + } + + &-panel { + width: 100%; + height: 100%; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + will-change: width, height; + } + + &-size { + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + background-color: rgba(0, 0, 0, 0.8); + color: #fff; + font-size: 12px; + padding: 3px 4px; + border-radius: 2px; + white-space: nowrap; + pointer-events: none; + } + + @each $border in $borders { + @each $key, $value in $border { + &-border-#{$key} { + @each $j, $val in $value { + #{$j}: $val; + } + position: absolute; + background-color: $border-color; + pointer-events: none; + } + } + } + @each $point in $points { + @each $key, $value in $point { + &-point-#{$key} { + @each $j, $val in $value { + #{$j}: $val; + } + width: 8px; + height: 8px; + position: absolute; + background-color: $point-color; + border-radius: 50%; + transform: translate(-50%, -50%); + } + } + } +} diff --git a/packages/screenshot/src/Screenshots/ScreenshotsCanvas/index.tsx b/packages/screenshot/src/Screenshots/ScreenshotsCanvas/index.tsx new file mode 100644 index 0000000..b26baf1 --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsCanvas/index.tsx @@ -0,0 +1,277 @@ +import React, { + forwardRef, + memo, + ReactElement, + useCallback, + useEffect, + useImperativeHandle, + useLayoutEffect, + useRef, +} from "react"; +import useBounds from "../hooks/useBounds"; +import useCursor from "../hooks/useCursor"; +import useEmiter from "../hooks/useEmiter"; +import useHistory from "../hooks/useHistory"; +import useOperation from "../hooks/useOperation"; +import useStore from "../hooks/useStore"; +import { Bounds, HistoryItemType, Point } from "../types"; +import getBoundsByPoints from "./getBoundsByPoints"; +import getPoints from "./getPoints"; +import "./index.scss"; +import isPointInDraw from "./isPointInDraw"; + +const borders = ["top", "right", "bottom", "left"]; + +export enum ResizePoints { + ResizeTop = "top", + ResizetopRight = "top-right", + ResizeRight = "right", + ResizeRightBottom = "right-bottom", + ResizeBottom = "bottom", + ResizeBottomLeft = "bottom-left", + ResizeLeft = "left", + ResizeLeftTop = "left-top", + Move = "move", +} + +const resizePoints = [ + ResizePoints.ResizeTop, + ResizePoints.ResizetopRight, + ResizePoints.ResizeRight, + ResizePoints.ResizeRightBottom, + ResizePoints.ResizeBottom, + ResizePoints.ResizeBottomLeft, + ResizePoints.ResizeLeft, + ResizePoints.ResizeLeftTop, +]; + +export default memo( + forwardRef(function ScreenshotsCanvas( + props, + ref, + ): ReactElement | null { + const { url, image, width, height } = useStore(); + + const emiter = useEmiter(); + const [history] = useHistory(); + const [cursor] = useCursor(); + const [bounds, boundsDispatcher] = useBounds(); + const [operation] = useOperation(); + + const resizeOrMoveRef = useRef(); + const pointRef = useRef(null); + const boundsRef = useRef(null); + const canvasRef = useRef(null); + const ctxRef = useRef(null); + + const isCanResize = bounds && !history.stack.length && !operation; + + const draw = useCallback(() => { + if (!bounds || !ctxRef.current) { + return; + } + + const ctx = ctxRef.current; + ctx.imageSmoothingEnabled = true; + // 设置太高,图片会模糊 + ctx.imageSmoothingQuality = "low"; + ctx.clearRect(0, 0, bounds.width, bounds.height); + + history.stack.slice(0, history.index + 1).forEach((item) => { + if (item.type === HistoryItemType.Source) { + item.draw(ctx, item); + } + }); + }, [bounds, ctxRef, history]); + + const onMouseDown = useCallback( + (e: React.MouseEvent, resizeOrMove: string) => { + if (e.button !== 0 || !bounds) { + return; + } + if (!operation) { + resizeOrMoveRef.current = resizeOrMove; + pointRef.current = { + x: e.clientX, + y: e.clientY, + }; + boundsRef.current = { + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + }; + } else { + const draw = isPointInDraw( + bounds, + canvasRef.current, + history, + e.nativeEvent, + ); + if (draw) { + emiter.emit("drawselect", draw, e.nativeEvent); + } else { + emiter.emit("mousedown", e.nativeEvent); + } + } + }, + [bounds, operation, emiter, history], + ); + + const updateBounds = useCallback( + (e: MouseEvent) => { + if ( + !resizeOrMoveRef.current || + !pointRef.current || + !boundsRef.current || + !bounds + ) { + return; + } + const points = getPoints( + e, + resizeOrMoveRef.current, + pointRef.current, + boundsRef.current, + ); + boundsDispatcher.set( + getBoundsByPoints( + points[0], + points[1], + bounds, + width, + height, + resizeOrMoveRef.current, + ), + ); + }, + [width, height, bounds, boundsDispatcher], + ); + + useLayoutEffect(() => { + if (!image || !bounds || !canvasRef.current) { + ctxRef.current = null; + return; + } + + if (!ctxRef.current) { + ctxRef.current = canvasRef.current.getContext("2d"); + } + + draw(); + }, [image, bounds, draw]); + + useEffect(() => { + const onMouseMove = (e: MouseEvent) => { + if (!operation) { + if ( + !resizeOrMoveRef.current || + !pointRef.current || + !boundsRef.current + ) { + return; + } + updateBounds(e); + } else { + emiter.emit("mousemove", e); + } + }; + + const onMouseUp = (e: MouseEvent) => { + if (!operation) { + if ( + !resizeOrMoveRef.current || + !pointRef.current || + !boundsRef.current + ) { + return; + } + updateBounds(e); + resizeOrMoveRef.current = undefined; + pointRef.current = null; + boundsRef.current = null; + } else { + emiter.emit("mouseup", e); + } + }; + window.addEventListener("mousemove", onMouseMove); + window.addEventListener("mouseup", onMouseUp); + + return () => { + window.removeEventListener("mousemove", onMouseMove); + window.removeEventListener("mouseup", onMouseUp); + }; + }, [updateBounds, operation, emiter]); + + // 放到最后,保证ctxRef.current存在 + useImperativeHandle< + CanvasRenderingContext2D | null, + CanvasRenderingContext2D | null + >(ref, () => ctxRef.current); + + return ( +
+
+ {/* 保证一开始就显示,减少加载时间 */} + + +
+
onMouseDown(e, "move")} + > + {isCanResize && ( +
+ {bounds.width} × {bounds.height} +
+ )} +
+ {borders.map((border) => { + return ( +
+ ); + })} + {isCanResize && + resizePoints.map((resizePoint) => { + return ( +
onMouseDown(e, resizePoint)} + /> + ); + })} +
+ ); + }), +); diff --git a/packages/screenshot/src/Screenshots/ScreenshotsCanvas/isPointInDraw.ts b/packages/screenshot/src/Screenshots/ScreenshotsCanvas/isPointInDraw.ts new file mode 100644 index 0000000..6d7f49b --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsCanvas/isPointInDraw.ts @@ -0,0 +1,35 @@ +import { Bounds, History, HistoryItemType } from '../types' + +export default function isPointInDraw ( + bounds: Bounds, + canvas: HTMLCanvasElement | null, + history: History, + e: MouseEvent +) { + if (!canvas) { + return false + } + + const $canvas = document.createElement('canvas') + $canvas.width = bounds.width + $canvas.height = bounds.height + const ctx = $canvas.getContext('2d') + + if (!ctx) { + return false + } + + const { left, top } = canvas.getBoundingClientRect() + const x = e.clientX - left + const y = e.clientY - top + + const stack = [...history.stack.slice(0, history.index + 1)] + + return stack.reverse().find(item => { + if (item.type !== HistoryItemType.Source) { + return false + } + ctx.clearRect(0, 0, bounds.width, bounds.height) + return item.isHit?.(ctx, item, { x, y }) + }) +} diff --git a/packages/screenshot/src/Screenshots/ScreenshotsColor/index.scss b/packages/screenshot/src/Screenshots/ScreenshotsColor/index.scss new file mode 100644 index 0000000..0da02b5 --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsColor/index.scss @@ -0,0 +1,45 @@ +@import "../var.scss"; + +.screenshots-color { + height: $sizecolor-size; + display: flex; + align-items: center; + + &-item { + border: 1px solid #777; + width: $sizecolor-size; + height: $sizecolor-size; + cursor: pointer; + position: relative; + margin: 0 3px; + &:before { + content: ""; + display: none; + position: absolute; + right: 0; + bottom: 0; + width: 10px; + height: 10px; + background-color: #333; + } + &:after { + content: ""; + display: none; + width: 8px; + height: 4px; + position: absolute; + right: 5px; + bottom: 5px; + border-bottom: 2px solid #fff; + border-left: 2px solid #fff; + transform: translate(5px, 2px) rotate(-45deg) scale(0.8, 0.8); + } + } + + &-active { + &:before, + &:after { + display: block; + } + } +} diff --git a/packages/screenshot/src/Screenshots/ScreenshotsColor/index.tsx b/packages/screenshot/src/Screenshots/ScreenshotsColor/index.tsx new file mode 100644 index 0000000..c520871 --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsColor/index.tsx @@ -0,0 +1,39 @@ +import React, { memo, ReactElement } from "react"; +import "./index.scss"; + +export interface ColorProps { + value: string; + onChange: (value: string) => void; +} + +export default memo(function ScreenshotsColor({ + value, + onChange, +}: ColorProps): ReactElement { + const colors = [ + "#ee5126", + "#fceb4d", + "#90e746", + "#51c0fa", + "#7a7a7a", + "#ffffff", + ]; + return ( +
+ {colors.map((color) => { + const classNames = ["screenshots-color-item"]; + if (color === value) { + classNames.push("screenshots-color-active"); + } + return ( +
onChange && onChange(color)} + /> + ); + })} +
+ ); +}); diff --git a/packages/screenshot/src/Screenshots/ScreenshotsContext.ts b/packages/screenshot/src/Screenshots/ScreenshotsContext.ts new file mode 100644 index 0000000..655bebd --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsContext.ts @@ -0,0 +1,56 @@ +import React, { Dispatch, SetStateAction } from 'react'; +import { EmiterRef, History, Bounds, CanvasContextRef } from './types'; +import zhCN, { Lang } from './zh_CN'; + +export interface ScreenshotsContextStore { + url?: string; + image: HTMLImageElement | null; + width: number; + height: number; + lang: Lang; + emiterRef: EmiterRef; + canvasContextRef: CanvasContextRef; + history: History; + bounds: Bounds | null; + cursor?: string; + operation?: string; +} + +export interface ScreenshotsContextDispatcher { + call?: (funcName: string, ...args: T[]) => void; + setHistory?: Dispatch>; + setBounds?: Dispatch>; + setCursor?: Dispatch>; + setOperation?: Dispatch>; +} + +export interface ScreenshotsContextValue { + store: ScreenshotsContextStore; + dispatcher: ScreenshotsContextDispatcher; +} + +export default React.createContext({ + store: { + url: undefined, + image: null, + width: 0, + height: 0, + lang: zhCN, + emiterRef: { current: {} }, + canvasContextRef: { current: null }, + history: { + index: -1, + stack: [], + }, + bounds: null, + cursor: 'move', + operation: undefined, + }, + dispatcher: { + call: undefined, + setHistory: undefined, + setBounds: undefined, + setCursor: undefined, + setOperation: undefined, + }, +}); diff --git a/packages/screenshot/src/Screenshots/ScreenshotsMagnifier/index.scss b/packages/screenshot/src/Screenshots/ScreenshotsMagnifier/index.scss new file mode 100644 index 0000000..8780aba --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsMagnifier/index.scss @@ -0,0 +1,61 @@ +@import "../var.scss"; + +.screenshots-magnifier { + position: absolute; + font-family: $font-family; + left: 0; + top: 0; + width: 100px; + box-shadow: 0 0 8px 0px #000; + z-index: 9; + + &, + * { + box-sizing: border-box; + user-select: none; + } + + &-body { + position: relative; + background-color: #fff; + &:before { + content: ""; + background-color: rgb(10, 114, 161); + position: absolute; + top: 50%; + left: 0; + width: 100%; + height: 2px; + z-index: 1; + } + &:after { + content: ""; + background-color: rgb(10, 114, 161); + position: absolute; + top: 0; + left: 50%; + width: 2px; + height: 100%; + z-index: 1; + } + &-canvas { + display: block; + width: 100px; + height: 80px; + } + } + &-footer { + height: 40px; + color: #fff; + font-size: 11px; + background-color: rgb(95, 94, 94); + padding: 4px; + white-space: nowrap; + overflow: hidden; + text-align: center; + &-item { + height: 18px; + line-height: 18px; + } + } +} diff --git a/packages/screenshot/src/Screenshots/ScreenshotsMagnifier/index.tsx b/packages/screenshot/src/Screenshots/ScreenshotsMagnifier/index.tsx new file mode 100644 index 0000000..01778ee --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsMagnifier/index.tsx @@ -0,0 +1,126 @@ +import React, { + memo, + useEffect, + useLayoutEffect, + useRef, + useState, +} from "react"; +import useLang from "../hooks/useLang"; +import useStore from "../hooks/useStore"; +import { Position } from "../types"; +import "./index.scss"; + +export interface ScreenshotsMagnifierProps { + x: number; + y: number; +} + +const magnifierWidth = 100; +const magnifierHeight = 80; + +export default memo(function ScreenshotsMagnifier({ + x, + y, +}: ScreenshotsMagnifierProps) { + const { width, height, image } = useStore(); + const lang = useLang(); + const [position, setPosition] = useState(null); + const elRef = useRef(null); + const canvasRef = useRef(null); + const ctxRef = useRef(null); + const [rgb, setRgb] = useState("000000"); + + useLayoutEffect(() => { + if (!elRef.current) { + return; + } + const elRect = elRef.current.getBoundingClientRect(); + let tx = x + 20; + let ty = y + 20; + if (tx + elRect.width > width) { + tx = x - elRect.width - 20; + } + if (ty + elRect.height > height) { + ty = y - elRect.height - 20; + } + + if (tx < 0) { + tx = 0; + } + if (ty < 0) { + ty = 0; + } + setPosition({ + x: tx, + y: ty, + }); + }, [width, height, x, y]); + + useEffect(() => { + if (!image || !canvasRef.current) { + ctxRef.current = null; + return; + } + + if (!ctxRef.current) { + ctxRef.current = canvasRef.current.getContext("2d"); + } + if (!ctxRef.current) { + return; + } + + const ctx = ctxRef.current; + ctx.clearRect(0, 0, magnifierWidth, magnifierHeight); + const rx = image.naturalWidth / width; + const ry = image.naturalHeight / height; + // 显示原图比例 + ctx.drawImage( + image, + x * rx - magnifierWidth / 2, + y * ry - magnifierHeight / 2, + magnifierWidth, + magnifierHeight, + 0, + 0, + magnifierWidth, + magnifierHeight, + ); + const { data } = ctx.getImageData( + Math.floor(magnifierWidth / 2), + Math.floor(magnifierHeight / 2), + 1, + 1, + ); + const hex = Array.from(data.slice(0, 3)) + .map((val) => (val >= 16 ? val.toString(16) : `0${val.toString(16)}`)) + .join("") + .toUpperCase(); + + setRgb(hex); + }, [width, height, image, x, y]); + + return ( +
+
+ +
+
+
+ {lang.magnifier_position_label}: ({x},{y}) +
+
RGB: #{rgb}
+
+
+ ); +}); diff --git a/packages/screenshot/src/Screenshots/ScreenshotsOperations/index.scss b/packages/screenshot/src/Screenshots/ScreenshotsOperations/index.scss new file mode 100644 index 0000000..4dee246 --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsOperations/index.scss @@ -0,0 +1,25 @@ +@import "../var.scss"; + +.screenshots-operations { + position: absolute; + left: 0; + top: 0; + will-change: transform; + + &-buttons { + display: flex; + align-items: center; + padding: 3px; + border-radius: 2px; + border: 1px solid #ddd; + background-color: #fff; + overflow: hidden; + } + + &-divider { + background-color: #ddd; + width: 1px; + height: $button-size; + margin: 0 3px; + } +} diff --git a/packages/screenshot/src/Screenshots/ScreenshotsOperations/index.tsx b/packages/screenshot/src/Screenshots/ScreenshotsOperations/index.tsx new file mode 100644 index 0000000..651e343 --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsOperations/index.tsx @@ -0,0 +1,118 @@ +import React, { + memo, + MouseEvent, + ReactElement, + useCallback, + useEffect, + useRef, + useState, +} from "react"; +import useBounds from "../hooks/useBounds"; +import useStore from "../hooks/useStore"; +import OperationButtons from "../operations"; +import { Bounds, Position } from "../types"; +import "./index.scss"; + +export const ScreenshotsOperationsCtx = React.createContext( + null, +); + +export default memo(function ScreenshotsOperations(): ReactElement | null { + const { width, height } = useStore(); + const [bounds] = useBounds(); + const [operationsRect, setOperationsRect] = useState(null); + const [position, setPosition] = useState(null); + + const elRef = useRef(null); + const onDoubleClick = useCallback((e: MouseEvent) => { + e.stopPropagation(); + }, []); + + const onContextMenu = useCallback((e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + }, []); + + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => { + if (!bounds || !elRef.current) { + return; + } + + const elRect = elRef.current.getBoundingClientRect(); + + let x = bounds.x + bounds.width - elRect.width; + let y = bounds.y + bounds.height + 10; + + if (x < 0) { + x = 0; + } + + if (x > width - elRect.width) { + x = width - elRect.width; + } + + if (y > height - elRect.height) { + y = height - elRect.height - 10; + } + + // 小数存在精度问题 + if ( + !position || + Math.abs(position.x - x) > 1 || + Math.abs(position.y - y) > 1 + ) { + setPosition({ + x, + y, + }); + } + + // 小数存在精度问题 + if ( + !operationsRect || + Math.abs(operationsRect.x - elRect.x) > 1 || + Math.abs(operationsRect.y - elRect.y) > 1 || + Math.abs(operationsRect.width - elRect.width) > 1 || + Math.abs(operationsRect.height - elRect.height) > 1 + ) { + setOperationsRect({ + x: elRect.x, + y: elRect.y, + width: elRect.width, + height: elRect.height, + }); + } + }); + + if (!bounds) { + return null; + } + + return ( + +
+
+ {OperationButtons.map((OperationButton, index) => { + if (OperationButton === "|") { + return ( +
+ ); + } else { + return ; + } + })} +
+
+ + ); +}); diff --git a/packages/screenshot/src/Screenshots/ScreenshotsOption/index.scss b/packages/screenshot/src/Screenshots/ScreenshotsOption/index.scss new file mode 100644 index 0000000..37036bf --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsOption/index.scss @@ -0,0 +1,50 @@ +@import "../var.scss"; + +.screenshots-option { + position: absolute; + left: 0; + top: 0; + font-family: $font-family; + + &, + * { + box-sizing: border-box; + user-select: none; + } + + &-container { + height: $button-size + 3 * 2 + 2; + background-color: #fff; + padding: 3px; + border-radius: 2px; + border: 1px solid #ddd; + background-color: #fff; + } + + &-arrow { + position: absolute; + border: 6px solid transparent; + } + + &[data-placement="top"] { + transform: translate(-50%, -11px); + } + + &[data-placement="top"] &-arrow { + transform: translate(-50%, -1px); + border-top-color: #fff; + top: 100%; + left: 50%; + } + + &[data-placement="bottom"] { + transform: translate(-50%, 11px); + } + + &[data-placement="bottom"] &-arrow { + transform: translate(-50%, 1px); + border-bottom-color: #fff; + bottom: 100%; + left: 50%; + } +} diff --git a/packages/screenshot/src/Screenshots/ScreenshotsOption/index.tsx b/packages/screenshot/src/Screenshots/ScreenshotsOption/index.tsx new file mode 100644 index 0000000..48c5705 --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsOption/index.tsx @@ -0,0 +1,150 @@ +import React, { + cloneElement, + memo, + ReactElement, + ReactNode, + useContext, + useEffect, + useRef, + useState, +} from "react"; +import { createPortal } from "react-dom"; +import { ScreenshotsOperationsCtx } from "../ScreenshotsOperations"; +import { Point } from "../types"; +import "./index.scss"; + +export interface ScreenshotsOptionProps { + open?: boolean; + content?: ReactNode; + children: ReactElement; +} + +export type Position = Point; + +export enum Placement { + Bottom = "bottom", + Top = "top", +} + +export default memo(function ScreenshotsOption({ + open, + content, + children, +}: ScreenshotsOptionProps): ReactElement { + const childrenRef = useRef(null); + const popoverRef = useRef(null); + const contentRef = useRef(null); + const operationsRect = useContext(ScreenshotsOperationsCtx); + const [placement, setPlacement] = useState(Placement.Bottom); + const [position, setPosition] = useState(null); + const [offsetX, setOffsetX] = useState(0); + + const getPopoverEl = () => { + if (!popoverRef.current) { + popoverRef.current = document.createElement("div"); + } + return popoverRef.current; + }; + + useEffect(() => { + const $el = getPopoverEl(); + if (open) { + document.body.appendChild($el); + } + return () => { + $el.remove(); + }; + }, [open]); + + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => { + if ( + !open || + !operationsRect || + !childrenRef.current || + !contentRef.current + ) { + return; + } + + const childrenRect = childrenRef.current.getBoundingClientRect(); + const contentRect = contentRef.current.getBoundingClientRect(); + + let currentPlacement = placement; + let x = childrenRect.left + childrenRect.width / 2; + let y = childrenRect.top + childrenRect.height; + let currentOffsetX = offsetX; + + // 如果左右都越界了,就以左边界为准 + if (x + contentRect.width / 2 > operationsRect.x + operationsRect.width) { + const ox = x; + x = operationsRect.x + operationsRect.width - contentRect.width / 2; + currentOffsetX = ox - x; + } + + // 左边不能超出 + if (x < operationsRect.x + contentRect.width / 2) { + const ox = x; + x = operationsRect.x + contentRect.width / 2; + currentOffsetX = ox - x; + } + + // 如果上下都越界了,就以上边界为准 + if (y > window.innerHeight - contentRect.height) { + if (currentPlacement === Placement.Bottom) { + currentPlacement = Placement.Top; + } + y = childrenRect.top - contentRect.height; + } + + if (y < 0) { + if (currentPlacement === Placement.Top) { + currentPlacement = Placement.Bottom; + } + y = childrenRect.top + childrenRect.height; + } + if (currentPlacement !== placement) { + setPlacement(currentPlacement); + } + if (position?.x !== x || position.y !== y) { + setPosition({ + x, + y, + }); + } + + if (currentOffsetX !== offsetX) { + setOffsetX(currentOffsetX); + } + }); + + return ( + <> + {cloneElement(children, { + ref: childrenRef, + })} + {open && + content && + createPortal( +
+
{content}
+
+
, + getPopoverEl(), + )} + + ); +}); diff --git a/packages/screenshot/src/Screenshots/ScreenshotsSize/index.scss b/packages/screenshot/src/Screenshots/ScreenshotsSize/index.scss new file mode 100644 index 0000000..313e234 --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsSize/index.scss @@ -0,0 +1,28 @@ +@import "../var.scss"; + +.screenshots-size { + height: $sizecolor-size; + display: flex; + align-items: center; + + &-item { + width: $sizecolor-size; + height: $sizecolor-size; + position: relative; + margin: 0 3px; + cursor: pointer; + } + + &-pointer { + background-color: #555; + border-radius: 50%; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + + &-active > &-pointer { + background-color: #39f; + } +} diff --git a/packages/screenshot/src/Screenshots/ScreenshotsSize/index.tsx b/packages/screenshot/src/Screenshots/ScreenshotsSize/index.tsx new file mode 100644 index 0000000..7ae7409 --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsSize/index.tsx @@ -0,0 +1,41 @@ +import React, { memo, ReactElement } from "react"; +import "./index.scss"; + +export interface SizeProps { + value: number; + onChange: (value: number) => void; +} + +export default memo(function ScreenshotsSize({ + value, + onChange, +}: SizeProps): ReactElement { + const sizes = [3, 6, 9]; + return ( +
+ {sizes.map((size) => { + const classNames = ["screenshots-size-item"]; + + if (size === value) { + classNames.push("screenshots-size-active"); + } + + return ( +
onChange && onChange(size)} + > +
+
+ ); + })} +
+ ); +}); diff --git a/packages/screenshot/src/Screenshots/ScreenshotsSizeColor/index.scss b/packages/screenshot/src/Screenshots/ScreenshotsSizeColor/index.scss new file mode 100644 index 0000000..1394477 --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsSizeColor/index.scss @@ -0,0 +1,8 @@ +@import "../var.scss"; + +.screenshots-sizecolor { + height: $sizecolor-size; + padding: 3px 0; + display: flex; + align-items: center; +} diff --git a/packages/screenshot/src/Screenshots/ScreenshotsSizeColor/index.tsx b/packages/screenshot/src/Screenshots/ScreenshotsSizeColor/index.tsx new file mode 100644 index 0000000..f210b14 --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsSizeColor/index.tsx @@ -0,0 +1,25 @@ +import React, { memo, ReactElement } from "react"; +import ScreenshotsSize from "../ScreenshotsSize"; +import ScreenshotsColor from "../ScreenshotsColor"; +import "./index.scss"; + +export interface SizeColorProps { + size: number; + color: string; + onSizeChange: (value: number) => void; + onColorChange: (value: string) => void; +} + +export default memo(function ScreenshotsSizeColor({ + size, + color, + onSizeChange, + onColorChange, +}: SizeColorProps): ReactElement { + return ( +
+ + +
+ ); +}); diff --git a/packages/screenshot/src/Screenshots/ScreenshotsTextarea/calculateNodeSize.ts b/packages/screenshot/src/Screenshots/ScreenshotsTextarea/calculateNodeSize.ts new file mode 100644 index 0000000..dbeae68 --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsTextarea/calculateNodeSize.ts @@ -0,0 +1,117 @@ +// Thanks to https://github.com/react-component/textarea + +const hiddenTextareaStyle = ` +min-width: 0 !important; +width: 0 !important; +min-height: 0 !important; +height:0 !important; +visibility: hidden !important; +overflow: hidden !important; +position: absolute !important; +z-index: -1000 !important; +top:0 !important; +right:0 !important; +` + +const sizeStyle = [ + 'letter-spacing', + 'line-height', + 'padding-top', + 'padding-bottom', + 'font-family', + 'font-weight', + 'font-size', + 'font-variant', + 'text-rendering', + 'text-transform', + 'text-indent', + 'padding-left', + 'padding-right', + 'border-width', + 'box-sizing', + 'white-space', + 'word-break' +] + +export interface SizeInfo { + sizingStyle: string + paddingSize: number + borderSize: number + boxSizing: string +} + +export interface Size { + width: number + height: number +} + +let hiddenTextarea: HTMLTextAreaElement + +export function getComputedSizeInfo (node: HTMLElement) { + const style = window.getComputedStyle(node) + + const boxSizing = + style.getPropertyValue('box-sizing') || + style.getPropertyValue('-moz-box-sizing') || + style.getPropertyValue('-webkit-box-sizing') + + const paddingSize = + parseFloat(style.getPropertyValue('padding-bottom')) + parseFloat(style.getPropertyValue('padding-top')) + + const borderSize = + parseFloat(style.getPropertyValue('border-bottom-width')) + parseFloat(style.getPropertyValue('border-top-width')) + + const sizingStyle = sizeStyle.map(name => `${name}:${style.getPropertyValue(name)}`).join(';') + + return { + sizingStyle, + paddingSize, + borderSize, + boxSizing + } +} + +export default function calculateNodeSize ( + textarea: HTMLTextAreaElement, + value: string, + maxWidth: number, + maxHeight: number +): Size { + if (!hiddenTextarea) { + hiddenTextarea = document.createElement('textarea') + hiddenTextarea.setAttribute('tab-index', '-1') + document.body.appendChild(hiddenTextarea) + } + + // Copy all CSS properties that have an impact on the height of the content in + // the textbox + const { paddingSize, borderSize, boxSizing, sizingStyle } = getComputedSizeInfo(textarea) + + // Need to have the overflow attribute to hide the scrollbar otherwise + // text-lines will not calculated properly as the shadow will technically be + // narrower for content + hiddenTextarea.setAttribute( + 'style', + `${sizingStyle};${hiddenTextareaStyle};max-width:${maxWidth}px;max-height:${maxHeight}px` + ) + + hiddenTextarea.value = value || ' ' + + let width = hiddenTextarea.scrollWidth + let height = hiddenTextarea.scrollHeight + + if (boxSizing === 'border-box') { + // border-box: add border, since height = content + padding + border + width += borderSize + height += borderSize + } else if (boxSizing === 'content-box') { + // remove padding, since height = content + width -= paddingSize + height -= paddingSize + } + + return { + width: Math.min(width, maxWidth), + height: Math.min(height, maxHeight) + } +} diff --git a/packages/screenshot/src/Screenshots/ScreenshotsTextarea/index.scss b/packages/screenshot/src/Screenshots/ScreenshotsTextarea/index.scss new file mode 100644 index 0000000..733ba5d --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsTextarea/index.scss @@ -0,0 +1,19 @@ +@import "../var.scss"; + +.screenshots-textarea { + box-sizing: border-box; + position: absolute; + left: 0; + top: 0; + margin: 0; + padding: 0; + background-color: transparent; + border: 2px solid $border-color; + resize: none; + outline: none; + white-space: nowrap; + word-break: break-all; + overflow: hidden; + font-family: $font-family; + text-align: left; +} diff --git a/packages/screenshot/src/Screenshots/ScreenshotsTextarea/index.tsx b/packages/screenshot/src/Screenshots/ScreenshotsTextarea/index.tsx new file mode 100644 index 0000000..b7fb4cd --- /dev/null +++ b/packages/screenshot/src/Screenshots/ScreenshotsTextarea/index.tsx @@ -0,0 +1,96 @@ +import React, { + ReactElement, + useRef, + FocusEvent, + useLayoutEffect, + useState, + memo, +} from "react"; +import { createPortal } from "react-dom"; +import calculateNodeSize from "./calculateNodeSize"; +import "./index.scss"; + +export interface TextInputProps { + x: number; + y: number; + maxWidth: number; + maxHeight: number; + size: number; + color: string; + value: string; + onChange: (value: string) => unknown; + onBlur: (e: FocusEvent) => unknown; +} + +export default memo(function ScreenshotsTextarea({ + x, + y, + maxWidth, + maxHeight, + size, + color, + value, + onChange, + onBlur, +}: TextInputProps): ReactElement { + const popoverRef = useRef(null); + const textareaRef = useRef(null); + const [width, setWidth] = useState(0); + const [height, setHeight] = useState(0); + + const getPopoverEl = () => { + if (!popoverRef.current) { + popoverRef.current = document.createElement("div"); + } + return popoverRef.current; + }; + + useLayoutEffect(() => { + if (popoverRef.current) { + document.body.appendChild(popoverRef.current); + requestAnimationFrame(() => { + textareaRef.current?.focus(); + }); + } + + return () => { + popoverRef.current?.remove(); + }; + }, []); + + useLayoutEffect(() => { + if (!textareaRef.current) { + return; + } + + const { width, height } = calculateNodeSize( + textareaRef.current, + value, + maxWidth, + maxHeight, + ); + setWidth(width); + setHeight(height); + }, [value, maxWidth, maxHeight]); + + return createPortal( +