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
+
+
+
+
+
+
+
+
+
+
+---
+
+## 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
+
+
+
+
+
+
+
+
+
+
+---
+
+## 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
+
+
+
+
+
+
+
+
+
+
+---
+
+## 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 例子
+
+
+本页展示 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(
+ ,
+ 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(
+