Browse Source

init

master
wangxiang 1 year ago
commit
9db76c82ec
  1. 8
      .changeset/README.md
  2. 16
      .changeset/config.json
  3. 3
      .czrc
  4. 11
      .editorconfig
  5. 14
      .eslintrc.js
  6. 51
      .github/workflows/build.yml
  7. 30
      .github/workflows/syncToGitee.yml
  8. 41
      .gitignore
  9. 6
      .npmrc
  10. 34
      .prettierignore
  11. 5
      .prettierrc.js
  12. 6
      .stylelintrc.js
  13. 201
      LICENSE
  14. 94
      README.de-DE.md
  15. 159
      README.md
  16. 171
      README.zh-CN.md
  17. 70
      package.json
  18. 5
      packages/desktop/.env
  19. 101
      packages/desktop/CHANGELOG.md
  20. BIN
      packages/desktop/build/icons/mac/icon.icns
  21. BIN
      packages/desktop/build/icons/png/1024x1024.png
  22. BIN
      packages/desktop/build/icons/png/128x128.png
  23. BIN
      packages/desktop/build/icons/png/16x16.png
  24. BIN
      packages/desktop/build/icons/png/24x24.png
  25. BIN
      packages/desktop/build/icons/png/256x256.png
  26. BIN
      packages/desktop/build/icons/png/32x32.png
  27. BIN
      packages/desktop/build/icons/png/48x48.png
  28. BIN
      packages/desktop/build/icons/png/512x512.png
  29. BIN
      packages/desktop/build/icons/png/64x64.png
  30. BIN
      packages/desktop/build/icons/win/icon.ico
  31. 8
      packages/desktop/e2e/example.spec.ts
  32. BIN
      packages/desktop/e2e/screenshots/example.png
  33. 44
      packages/desktop/electron-builder.json5
  34. 11
      packages/desktop/electron/electron-env.d.ts
  35. 106
      packages/desktop/electron/i18n/de-DE.json
  36. 109
      packages/desktop/electron/i18n/en-US.json
  37. 109
      packages/desktop/electron/i18n/zh-CN.json
  38. 46
      packages/desktop/electron/main/app.ts
  39. 181
      packages/desktop/electron/main/constant.ts
  40. 77
      packages/desktop/electron/main/globalShortcut.ts
  41. 83
      packages/desktop/electron/main/index.ts
  42. 447
      packages/desktop/electron/main/ipcMain.ts
  43. 79
      packages/desktop/electron/main/logger.ts
  44. 7
      packages/desktop/electron/main/notification.ts
  45. 28
      packages/desktop/electron/main/protocol.ts
  46. 28
      packages/desktop/electron/main/serverProcess.ts
  47. 126
      packages/desktop/electron/main/tray.ts
  48. 80
      packages/desktop/electron/main/update.ts
  49. 142
      packages/desktop/electron/main/utils.ts
  50. 164
      packages/desktop/electron/preload/electronAPI.ts
  51. 166
      packages/desktop/electron/preload/index.ts
  52. 43
      packages/desktop/electron/win/canvasWin.ts
  53. 124
      packages/desktop/electron/win/clipScreenWin.ts
  54. 53
      packages/desktop/electron/win/editGifWin.ts
  55. 66
      packages/desktop/electron/win/editImageWin.ts
  56. 100
      packages/desktop/electron/win/mainWin.ts
  57. 93
      packages/desktop/electron/win/pinImageWin.ts
  58. 73
      packages/desktop/electron/win/pinVideoWin.ts
  59. 68
      packages/desktop/electron/win/recorderAudioWin.ts
  60. 77
      packages/desktop/electron/win/recorderFullScreenWin.ts
  61. 164
      packages/desktop/electron/win/recorderScreenWin.ts
  62. 76
      packages/desktop/electron/win/recorderVideoWin.ts
  63. 48
      packages/desktop/electron/win/recordsWin.ts
  64. 54
      packages/desktop/electron/win/settingWin.ts
  65. 130
      packages/desktop/electron/win/shotScreenWin.ts
  66. 43
      packages/desktop/electron/win/spliceImageWin.ts
  67. 47
      packages/desktop/electron/win/videoConverterWin.ts
  68. 61
      packages/desktop/electron/win/viewAudioWin.ts
  69. 124
      packages/desktop/electron/win/viewImageWin.ts
  70. 95
      packages/desktop/electron/win/viewVideoWin.ts
  71. 16
      packages/desktop/index.html
  72. 43
      packages/desktop/package.json
  73. 54
      packages/desktop/playwright.config.ts
  74. BIN
      packages/desktop/public/imgs/icons/mac/icon.icns
  75. BIN
      packages/desktop/public/imgs/icons/png/1024x1024.png
  76. BIN
      packages/desktop/public/imgs/icons/png/128x128.png
  77. BIN
      packages/desktop/public/imgs/icons/png/16x16.png
  78. BIN
      packages/desktop/public/imgs/icons/png/24x24.png
  79. BIN
      packages/desktop/public/imgs/icons/png/256x256.png
  80. BIN
      packages/desktop/public/imgs/icons/png/32x32.png
  81. BIN
      packages/desktop/public/imgs/icons/png/48x48.png
  82. BIN
      packages/desktop/public/imgs/icons/png/512x512.png
  83. BIN
      packages/desktop/public/imgs/icons/png/64x64.png
  84. BIN
      packages/desktop/public/imgs/icons/win/icon.ico
  85. BIN
      packages/desktop/public/imgs/logo/favicon.ico
  86. BIN
      packages/desktop/public/imgs/logo/logo.png
  87. 1
      packages/desktop/src/vite-env.d.ts
  88. 40
      packages/desktop/tsconfig.json
  89. 13
      packages/desktop/tsconfig.node.json
  90. 55
      packages/desktop/vite.config.ts
  91. 48
      packages/docs/.vitepress/config.ts
  92. BIN
      packages/docs/assets/imgs/eg.jpg
  93. BIN
      packages/docs/assets/imgs/ei.jpg
  94. BIN
      packages/docs/assets/imgs/home.jpg
  95. BIN
      packages/docs/assets/imgs/logo.png
  96. BIN
      packages/docs/assets/imgs/ra.jpg
  97. BIN
      packages/docs/assets/imgs/rs.jpg
  98. BIN
      packages/docs/assets/imgs/rv.jpg
  99. BIN
      packages/docs/assets/imgs/setting.jpg
  100. BIN
      packages/docs/assets/imgs/ss.jpg
  101. Some files were not shown because too many files have changed in this diff Show More

8
.changeset/README.md

@ -0,0 +1,8 @@ @@ -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)

16
.changeset/config.json

@ -0,0 +1,16 @@ @@ -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": []
}

3
.czrc

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
{
"path": "cz-conventional-changelog"
}

11
.editorconfig

@ -0,0 +1,11 @@ @@ -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

14
.eslintrc.js

@ -0,0 +1,14 @@ @@ -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"],
},
};

51
.github/workflows/build.yml

@ -0,0 +1,51 @@ @@ -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 }}'

30
.github/workflows/syncToGitee.yml

@ -0,0 +1,30 @@ @@ -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

41
.gitignore vendored

@ -0,0 +1,41 @@ @@ -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/

6
.npmrc

@ -0,0 +1,6 @@ @@ -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/"

34
.prettierignore

@ -0,0 +1,34 @@ @@ -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/

5
.prettierrc.js

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
const fabric = require('@umijs/fabric');
module.exports = {
...fabric.prettier,
};

6
.stylelintrc.js

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
module.exports = {
extends: [require.resolve("@umijs/fabric/dist/stylelint")],
rules: {
// your rules
},
};

201
LICENSE

@ -0,0 +1,201 @@ @@ -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.

94
README.de-DE.md

@ -0,0 +1,94 @@ @@ -0,0 +1,94 @@
<p align="center">
<img src="https://027xiguapi.github.io/pear-rec/logo.png" height="120" />
<h1>pear-rec</h1>
<p>
<img src="https://img.shields.io/github/stars/027xiguapi/pear-rec" alt="stars">
<img src="https://img.shields.io/badge/react-v18-blue" alt="react">
<img src="https://img.shields.io/badge/electron-v26-blue" alt="electron">
<img src="https://img.shields.io/badge/nestjs-v3-blue" alt="nestjs">
<img src="https://img.shields.io/badge/-TypeScript-blue?logo=typescript&logoColor=white" alt="typescript">
<img src="https://img.shields.io/badge/-Vite-blue?logo=vite&logoColor=white" alt="vite">
</p>
</p>
---
## 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
<p align="center">
<img src="https://027xiguapi.github.io/pear-rec/imgs/pear-rec_qq_qrcode.png" />
</p>
## Lizenz
[pear-rec wird unter der Apache License V2 bereitgestellt.](LICENSE)

159
README.md

@ -0,0 +1,159 @@ @@ -0,0 +1,159 @@
<p align="center">
<img src="https://027xiguapi.github.io/pear-rec/logo.png" height="120" />
<h1>pear-rec</h1>
<p>
<img src="https://img.shields.io/github/stars/027xiguapi/pear-rec" alt="stars">
<img src="https://img.shields.io/badge/react-v18-blue" alt="react">
<img src="https://img.shields.io/badge/electron-v26-blue" alt="electron">
<img src="https://img.shields.io/badge/nestjs-v3-blue" alt="nestjs">
<img src="https://img.shields.io/badge/-TypeScript-blue?logo=typescript&logoColor=white" alt="typescript">
<img src="https://img.shields.io/badge/-Vite-blue?logo=vite&logoColor=white" alt="vite">
</p>
</p>
---
## 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
<center>
<img src="https://027xiguapi.github.io/pear-rec/assets/home.7d9162cb.jpg" />
</center>
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)

171
README.zh-CN.md

@ -0,0 +1,171 @@ @@ -0,0 +1,171 @@
<p align="center">
<img src="https://027xiguapi.github.io/pear-rec/logo.png" height="120" />
<h1>pear-rec</h1>
<p>
<img src="https://img.shields.io/github/stars/027xiguapi/pear-rec" alt="stars">
<img src="https://img.shields.io/badge/react-v18-blue" alt="react">
<img src="https://img.shields.io/badge/electron-v26-blue" alt="electron">
<img src="https://img.shields.io/badge/nestjs-v3-blue" alt="nestjs">
<img src="https://img.shields.io/badge/-TypeScript-blue?logo=typescript&logoColor=white" alt="typescript">
<img src="https://img.shields.io/badge/-Vite-blue?logo=vite&logoColor=white" alt="vite">
</p>
</p>
---
## 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
```
## 🥰 功能
<center>
<img src="https://027xiguapi.github.io/pear-rec/assets/home.7d9162cb.jpg" />
</center>
已经勾选的功能是开发过程最新功能,但可能还没发布在最新版本
- [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 群
<p align="center">
<img src="https://027xiguapi.github.io/pear-rec/imgs/pear-rec_qq_qrcode.png" />
</p>
## 🤝 开源协议
[pear-rec(梨子 rec) 可在 Apache License V2 下使用。](LICENSE)
[开源项目礼节](https://developer.mozilla.org/zh-CN/docs/MDN/Community/Open_source_etiquette)

70
package.json

@ -0,0 +1,70 @@ @@ -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"
}

5
packages/desktop/.env

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
# port 端口号
VITE_API_URL = http://localhost:9190/
VITE_WEB_URL = http://localhost:9191/
PORT=9190

101
packages/desktop/CHANGELOG.md

@ -0,0 +1,101 @@ @@ -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

BIN
packages/desktop/build/icons/mac/icon.icns

Binary file not shown.

BIN
packages/desktop/build/icons/png/1024x1024.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

BIN
packages/desktop/build/icons/png/128x128.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
packages/desktop/build/icons/png/16x16.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
packages/desktop/build/icons/png/24x24.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
packages/desktop/build/icons/png/256x256.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
packages/desktop/build/icons/png/32x32.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
packages/desktop/build/icons/png/48x48.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
packages/desktop/build/icons/png/512x512.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

BIN
packages/desktop/build/icons/png/64x64.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
packages/desktop/build/icons/win/icon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

8
packages/desktop/e2e/example.spec.ts

@ -0,0 +1,8 @@ @@ -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" });
});

BIN
packages/desktop/e2e/screenshots/example.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

44
packages/desktop/electron-builder.json5

@ -0,0 +1,44 @@ @@ -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',
},
}

11
packages/desktop/electron/electron-env.d.ts vendored

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
/// <reference types="vite-electron-plugin/electron-env" />
declare namespace NodeJS {
interface ProcessEnv {
VSCODE_DEBUG?: "true";
DIST_ELECTRON: string;
DIST: string;
/** /dist/ or /public/ */
VITE_PUBLIC: string;
}
}

106
packages/desktop/electron/i18n/de-DE.json

@ -0,0 +1,106 @@ @@ -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"
}
}

109
packages/desktop/electron/i18n/en-US.json

@ -0,0 +1,109 @@ @@ -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"
}
}

109
packages/desktop/electron/i18n/zh-CN.json

@ -0,0 +1,109 @@ @@ -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": "服务地址"
}
}

46
packages/desktop/electron/main/app.ts

@ -0,0 +1,46 @@ @@ -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!');
});
}

181
packages/desktop/electron/main/constant.ts

@ -0,0 +1,181 @@ @@ -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,
},
};

77
packages/desktop/electron/main/globalShortcut.ts

@ -0,0 +1,77 @@ @@ -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,
};

83
packages/desktop/electron/main/index.ts

@ -0,0 +1,83 @@ @@ -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();
}
});

447
packages/desktop/electron/main/ipcMain.ts

@ -0,0 +1,447 @@ @@ -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();

79
packages/desktop/electron/main/logger.ts

@ -0,0 +1,79 @@ @@ -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,
)}`,
);
});
});

7
packages/desktop/electron/main/notification.ts

@ -0,0 +1,7 @@ @@ -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();
}

28
packages/desktop/electron/main/protocol.ts

@ -0,0 +1,28 @@ @@ -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);
}
});
}

28
packages/desktop/electron/main/serverProcess.ts

@ -0,0 +1,28 @@ @@ -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();
}

126
packages/desktop/electron/main/tray.ts

@ -0,0 +1,126 @@ @@ -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;
}

80
packages/desktop/electron/main/update.ts

@ -0,0 +1,80 @@ @@ -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();
}

142
packages/desktop/electron/main/utils.ts

@ -0,0 +1,142 @@ @@ -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,
};

164
packages/desktop/electron/preload/electronAPI.ts

@ -0,0 +1,164 @@ @@ -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),
});

166
packages/desktop/electron/preload/index.ts

@ -0,0 +1,166 @@ @@ -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 = `<div class="${className}"><div></div></div>`;
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 = `<div class="app-skeleton-nav"></div><div class="app-skeleton-content"></div><div class="wavesurfer"></div><div class="app-skeleton-footer"></div>`;
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();
}
};

43
packages/desktop/electron/win/canvasWin.ts

@ -0,0 +1,43 @@ @@ -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 };

124
packages/desktop/electron/win/clipScreenWin.ts

@ -0,0 +1,124 @@ @@ -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,
};

53
packages/desktop/electron/win/editGifWin.ts

@ -0,0 +1,53 @@ @@ -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 };

66
packages/desktop/electron/win/editImageWin.ts

@ -0,0 +1,66 @@ @@ -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 };

100
packages/desktop/electron/win/mainWin.ts

@ -0,0 +1,100 @@ @@ -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,
};

93
packages/desktop/electron/win/pinImageWin.ts

@ -0,0 +1,93 @@ @@ -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,
};

73
packages/desktop/electron/win/pinVideoWin.ts

@ -0,0 +1,73 @@ @@ -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,
};

68
packages/desktop/electron/win/recorderAudioWin.ts

@ -0,0 +1,68 @@ @@ -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,
};

77
packages/desktop/electron/win/recorderFullScreenWin.ts

@ -0,0 +1,77 @@ @@ -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,
};

164
packages/desktop/electron/win/recorderScreenWin.ts

@ -0,0 +1,164 @@ @@ -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,
};

76
packages/desktop/electron/win/recorderVideoWin.ts

@ -0,0 +1,76 @@ @@ -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<string> = 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,
};

48
packages/desktop/electron/win/recordsWin.ts

@ -0,0 +1,48 @@ @@ -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 };

54
packages/desktop/electron/win/settingWin.ts

@ -0,0 +1,54 @@ @@ -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 };

130
packages/desktop/electron/win/shotScreenWin.ts

@ -0,0 +1,130 @@ @@ -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<string> = 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,
};

43
packages/desktop/electron/win/spliceImageWin.ts

@ -0,0 +1,43 @@ @@ -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 };

47
packages/desktop/electron/win/videoConverterWin.ts

@ -0,0 +1,47 @@ @@ -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 };

61
packages/desktop/electron/win/viewAudioWin.ts

@ -0,0 +1,61 @@ @@ -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 };

124
packages/desktop/electron/win/viewImageWin.ts

@ -0,0 +1,124 @@ @@ -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,
};

95
packages/desktop/electron/win/viewVideoWin.ts

@ -0,0 +1,95 @@ @@ -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,
};

16
packages/desktop/index.html

@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/imgs/logo/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
<title>pear-rec</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

43
packages/desktop/package.json

@ -0,0 +1,43 @@ @@ -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"
}
}

54
packages/desktop/playwright.config.ts

@ -0,0 +1,54 @@ @@ -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;

BIN
packages/desktop/public/imgs/icons/mac/icon.icns

Binary file not shown.

BIN
packages/desktop/public/imgs/icons/png/1024x1024.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

BIN
packages/desktop/public/imgs/icons/png/128x128.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
packages/desktop/public/imgs/icons/png/16x16.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 B

BIN
packages/desktop/public/imgs/icons/png/24x24.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
packages/desktop/public/imgs/icons/png/256x256.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
packages/desktop/public/imgs/icons/png/32x32.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
packages/desktop/public/imgs/icons/png/48x48.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
packages/desktop/public/imgs/icons/png/512x512.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
packages/desktop/public/imgs/icons/png/64x64.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
packages/desktop/public/imgs/icons/win/icon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

BIN
packages/desktop/public/imgs/logo/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
packages/desktop/public/imgs/logo/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

1
packages/desktop/src/vite-env.d.ts vendored

@ -0,0 +1 @@ @@ -0,0 +1 @@
/// <reference types="vite/client" />

40
packages/desktop/tsconfig.json

@ -0,0 +1,40 @@ @@ -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"
}
]
}

13
packages/desktop/tsconfig.node.json

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true
},
"include": [
"vite.config.ts",
"package.json"
]
}

55
packages/desktop/vite.config.ts

@ -0,0 +1,55 @@ @@ -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,
};
});

48
packages/docs/.vitepress/config.ts

@ -0,0 +1,48 @@ @@ -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:
'<a href="https://www.apache.org/licenses/LICENSE-2.0" target="_blank">pear-rec is available under the Apache License V2.</a>',
copyright: 'Copyright © 2023 西瓜皮',
},
},
});

BIN
packages/docs/assets/imgs/eg.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 KiB

BIN
packages/docs/assets/imgs/ei.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 KiB

BIN
packages/docs/assets/imgs/home.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

BIN
packages/docs/assets/imgs/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

BIN
packages/docs/assets/imgs/ra.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
packages/docs/assets/imgs/rs.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 1022 KiB

BIN
packages/docs/assets/imgs/rv.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

BIN
packages/docs/assets/imgs/setting.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
packages/docs/assets/imgs/ss.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save