├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── feature_request.md
│ └── questiom.md
├── pull_request_template.md
└── workflows
│ └── publish.yml
├── .gitignore
├── LICENSE
├── README-zh.md
├── README.md
├── index.html
├── package.json
├── public
└── favicon.ico
├── publish.js
├── src-tauri
├── .gitignore
├── Cargo.toml
├── build.rs
├── capabilities
│ └── default.json
├── icons
│ ├── 128x128.png
│ ├── 128x128@2x.png
│ ├── 32x32.png
│ ├── icon.icns
│ ├── icon.ico
│ └── icon.png
├── resource
│ ├── default-key-config.json
│ └── scrcpy-mask-server-v2.4
├── src
│ ├── adb.rs
│ ├── binary.rs
│ ├── client.rs
│ ├── control_msg.rs
│ ├── lib.rs
│ ├── main.rs
│ ├── resource.rs
│ ├── scrcpy_mask_cmd.rs
│ ├── share.rs
│ └── socket.rs
└── tauri.conf.json
├── src
├── App.vue
├── components
│ ├── Device.vue
│ ├── Header.vue
│ ├── Mask.vue
│ ├── ScreenStream.vue
│ ├── Sidebar.vue
│ ├── keyboard
│ │ ├── KeyBoard.vue
│ │ ├── KeyCommon.vue
│ │ ├── KeyFire.vue
│ │ ├── KeyInfo.vue
│ │ ├── KeyObservation.vue
│ │ ├── KeySetting.vue
│ │ ├── KeySight.vue
│ │ ├── KeySkill.vue
│ │ ├── KeySteeringWheel.vue
│ │ └── KeySwipe.vue
│ └── setting
│ │ ├── About.vue
│ │ ├── Basic.vue
│ │ ├── Data.vue
│ │ ├── Mask.vue
│ │ └── Setting.vue
├── frontcommand
│ ├── KeyToCodeMap.ts
│ ├── UIEventsCode.ts
│ ├── android.ts
│ ├── controlMsg.ts
│ └── scrcpyMaskCmd.ts
├── hotkey.ts
├── i18n
│ ├── en-US.json
│ ├── index.ts
│ └── zh-CN.json
├── invoke.ts
├── keyMappingConfig.ts
├── main.ts
├── router.ts
├── screenStream.ts
├── store
│ ├── global.ts
│ └── keyboard.ts
├── storeLoader.ts
├── styles.css
├── vite-env.d.ts
└── websocket.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[BUG] XXX"
5 | assignees: ''
6 |
7 | ---
8 |
9 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 |
12 | **To Reproduce**
13 | Steps to reproduce the behavior:
14 | 1. Go to '...'
15 | 2. Click on '....'
16 | 3. Scroll down to '....'
17 | 4. See error
18 |
19 | **Expected behavior**
20 | A clear and concise description of what you expected to happen.
21 |
22 | **Screenshots**
23 | If applicable, add screenshots to help explain your problem.
24 |
25 | **Environment (please complete the following information):**
26 | - OS: [e.g. macOS 14.2.1]
27 | - Version [e.g. 1.0.0]
28 |
29 | **Additional context**
30 | Add any other context about the problem here.
31 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: "[Feature] XXX"
5 | assignees: ''
6 |
7 | ---
8 |
9 | **Is your feature request related to a problem? Please describe.**
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 |
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen.
14 |
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/questiom.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Question
3 | about: Ask a question about scrcpy-mask
4 | title: "[Question] XXX"
5 | assignees: ''
6 |
7 | ---
8 |
9 | **Describe the Question**
10 | A clear and concise description of what the question is.
11 |
12 | **Additional context**
13 | Add any other context about the problem here.
14 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## What does this PR do?
2 |
3 |
4 |
5 |
6 | ## Related issues
7 |
8 |
15 |
16 | - [ ] Does this PR introduce any public API change?
17 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: "Multi platform compile"
2 | on:
3 | push:
4 | # 匹配特定标签 (refs/tags)
5 | tags:
6 | - "v*" # 推送事件匹配 v*, 例如 v1.0,v20.15.10 等来触发工作流
7 |
8 | # This workflow will trigger on each push to the `release` branch to create or update a GitHub release, build your app, and upload the artifacts to the release.
9 |
10 | jobs:
11 | publish-tauri:
12 | permissions:
13 | contents: write
14 | strategy:
15 | fail-fast: false
16 | matrix:
17 | include:
18 | - platform: "macos-latest" # for Arm based macs (M1 and above).
19 | args: "--target aarch64-apple-darwin"
20 | - platform: "macos-latest" # for Intel based macs.
21 | args: "--target x86_64-apple-darwin"
22 | - platform: "ubuntu-22.04" # for Tauri v1 you could replace this with ubuntu-20.04.
23 | args: ""
24 | - platform: "windows-latest"
25 | args: ""
26 |
27 | runs-on: ${{ matrix.platform }}
28 | steps:
29 | - uses: actions/checkout@v4
30 |
31 | - name: setup node
32 | uses: actions/setup-node@v4
33 | with:
34 | node-version: lts/*
35 |
36 | - name: Install pnpm
37 | uses: pnpm/action-setup@v3
38 | with:
39 | version: 8
40 |
41 | - name: install Rust stable
42 | uses: dtolnay/rust-toolchain@stable
43 | with:
44 | # Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
45 | targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
46 |
47 | - name: install dependencies (ubuntu only)
48 | if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above.
49 | run: |
50 | sudo apt-get update
51 | sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
52 | # webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
53 | # You can remove the one that doesn't apply to your app to speed up the workflow a bit.
54 |
55 | - name: install frontend dependencies
56 | run: pnpm install # change this to npm, pnpm or bun depending on which one you use.
57 |
58 | - uses: tauri-apps/tauri-action@v0
59 | env:
60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
61 | with:
62 | tagName: "v__VERSION__"
63 | releaseName: "Scrcpy Mask v__VERSION__"
64 | releaseBody: |
65 | ## 更新说明
66 |
67 | 本次更新主要新增了...
68 |
69 | 本次更新主要修复了...
70 |
71 | ---
72 |
73 | ## Update Notice
74 |
75 | ...
76 |
77 | ---
78 |
79 | ## Commits
80 |
81 | ...
82 | releaseDraft: true
83 | prerelease: false
84 | args: ${{ matrix.args }}
85 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
4 | .vscode
5 | .DS_Store
6 | pnpm-lock.yaml
7 | scrcpy-mask.code-workspace
8 | src-tauri/Cargo.lock
9 |
--------------------------------------------------------------------------------
/README-zh.md:
--------------------------------------------------------------------------------
1 | # Scrcpy Mask
2 |
3 | 为了实现电脑控制安卓设备,本人使用 Tarui + Vue 3 + Rust 开发了一款跨平台桌面客户端。该客户端能够提供可视化的鼠标和键盘按键映射配置。通过按键映射实现了实现类似安卓模拟器的多点触控操作,具有毫秒级响应速度。该工具可广泛用于电脑控制安卓设备玩手游等等,提供流畅的触控体验。
4 |
5 | 本人对 Scrcpy 项目的开发者表示深深的敬意和感谢。Scrcpy 是一个强大而高效的开源工具,极大地方便了对 Android 设备的控制。本项目的实现基于 Scrcpy 的优秀架构,进行了鼠标键盘控制的优化和调整。
6 |
7 | **本项目不提供 Scrcpy 的投屏功能!本项目仅实现了 Scrcpy 的控制协议。**
8 |
9 | 原因是投屏会存在延迟和模糊问题,本项目另辟蹊径,直接放弃投屏,而使用透明的蒙版显示窗口背后的内容(可以使用电脑安卓模拟器 、手机厂商提供的低延迟投屏等),从根本上杜绝了 Scrcpy 的投屏体验差的问题。
10 |
11 | **如果您确实需要一个开箱即用的投屏功能,并且不在意延迟和性能问题**,可以使用安卓应用 [ScreenStream](https://github.com/dkrivoruchko/ScreenStream) 在局域网内投屏。本项目已适配 ScreenStream 投屏(自 `v0.5.0` 版本起),虽然它的性能可能不太理想,但开箱即用。
12 |
13 | 除此之外,为了更好的支持 Scrcpy Mask 与安卓设备交互,本人对 scrcpy-server 进行了一些修改,在此扩展出了一个分支项目 [scrcpy-mask-server](https://github.com/AkiChase/scrcpy-mask-server)
14 |
15 | ## 特性
16 |
17 | - [x] 有线、无线连接安卓设备
18 | - [x] 启动并连接 Scrcpy 服务端
19 | - [x] 实现 Scrcpy 控制协议
20 | - [x] 鼠标和键盘按键映射
21 | - [x] 可视化编辑按键映射配置
22 | - [x] 按键映射配置的导入与导出
23 | - [x] 更新检查
24 | - [x] 在按键映射和按键输入之间切换
25 | - [x] 国际化
26 | - [x] ScreenStream 投屏
27 | - [ ] 手柄按键映射
28 | - [ ] 更好的宏
29 | - [x] 通过 WebSocket 提供外部控制,见[外部控制](https://github.com/AkiChase/scrcpy-mask-external-control)
30 | - [ ] 帮助文档
31 |
32 | ## 视频演示
33 |
34 | - [别再说你不会用电脑控制手机玩手游了,Scrcpy Mask 纯小白教程+常见问题解答](https://www.bilibili.com/video/BV1Sm42157md/?share_source=copy_web&vd_source=36923115230d8a46ae8b587fc5348e6e)
35 | - [DNF 手游触屏操作反人类?但又不能在模拟器上玩 DNF 手游?不好意思,Scrcpy Mask “模拟器”的机制遥遥领先](https://www.bilibili.com/video/BV17U411Z7cN/?share_source=copy_web&vd_source=36923115230d8a46ae8b587fc5348e6e)
36 | - [如何用电脑玩 FPS 手游?这样的“安卓模拟器”,也不是不可以-哔哩哔哩](https://www.bilibili.com/video/BV1EU411Z7TC/?share_source=copy_web&vd_source=36923115230d8a46ae8b587fc5348e6e)
37 | - [M 系列 Mac 电脑玩王者,暃排位实录,使用 Android Stuido 模拟器和开源 Scrcpy Mask 按键映射工具-哔哩哔哩](https://b23.tv/q6iDW1w)
38 | - [自制跨平台开源项目 Scrcpy Mask ,像模拟器一样用键鼠控制任意安卓设备!以 M 系列芯片 MacBook 打王者为例-哔哩哔哩](https://b23.tv/gqmriXr)
39 | - [如何用 PC 控制安卓手机打王者?只要思想不滑坡,办法总比困难多!-哔哩哔哩](https://b23.tv/dmUOpff)
40 | - [M 芯片 Mac 怎么用 Android Studio 模拟器打王者?这是 Up 耗时数个月给出的答案-哔哩哔哩](https://b23.tv/ckJgyK5)
41 |
42 | ## 实现原理
43 |
44 | - [Scrcpy Mask 实现原理剖析,如何像模拟器一样用键鼠控制你的安卓设备?架构、通信篇 - 掘金](https://juejin.cn/post/7366799820734939199)
45 | - [Scrcpy Mask 实现原理剖析,如何像模拟器一样用键鼠控制你的安卓设备?前端可视化、按键映射篇 - 掘金](https://juejin.cn/post/7367620233140748299)
46 | - [Scrcpy Mask 实现原理剖析,如何在前端实现王者荣耀中技能的准确释放? - 掘金](https://juejin.cn/post/7367568884198047807)
47 |
48 | ## 截图
49 |
50 | - 设备控制
51 |
52 | 
53 |
54 | - 可视化编辑按键映射配置
55 |
56 | 
57 |
58 | - 游戏控制
59 |
60 | 
61 |
62 | 
63 |
64 | ## 基本使用
65 |
66 | 1. 从 [releases](https://github.com/AkiChase/scrcpy-mask/releases) 中安装适合你系统平台的软件包
67 | 2. 确认你的安卓设备类型
68 | 1. 对于手机或平板电脑等物理设备
69 | 1. 你需要自己解决投屏的问题。推荐使用设备品牌的官方投屏方式,这样一般延迟最小。自 `v0.5.0` 版本起,可以配合[ScreenStream](https://github.com/dkrivoruchko/ScreenStream)在同一局域网下投屏。
70 | 2. 通过 USB 或无线方式在设备上启用 ADB 调试,然后将其连接到电脑。
71 | 2. 对于模拟器,不仅不需要投屏,而且模拟器通常默认已经启用了 ADB 有线调试。所以几乎不用操作就能获得最好的体验。
72 | 3. 启动软件并导航到设备页面。
73 | 1. 在可用的设备中查找你的设备(如果未找到,请自行搜索如何为安装设备启用 ADB 调试)。
74 | 2. 右击设备并选择“控制此设备”。
75 | 4. 导航到设置页面->蒙版设置,将蒙版的宽度和高度设置为设备屏幕尺寸相同的比例,确保蒙版大小合适。
76 | 5. 导航到蒙版页面,你可以在其中看到一个完全透明的蒙版区域。接下来,调整并移动模拟器窗口或投屏窗口,让其内容区域与透明蒙版区域完全对齐。
77 | 6. 导航到键映射页面,切换或编辑键映射配置。
78 | 7. 返回到蒙版界面,开始使用吧!
79 |
80 | ## 关于宏
81 |
82 | 目前宏的结构仅仅是一个 JSON 对象,功能有限,仅仅是作为过渡使用的。请勿投入太多时间来编写宏,因为**宏的编写规范随时可能因版本更新而变动**。
83 |
84 | 宏的示例可见 [hotkey.ts](https://github.com/AkiChase/scrcpy-mask/blob/master/src/hotkey.ts) 的 `async function execMacro` 函数注释。
85 |
86 | 比如 `key-input-mode` 宏,可以从按键映射模式切换到按键输入模式,常用于文本输入。示例如下:
87 |
88 | ```json
89 | [{ "args": [], "type": "key-input-mode" }]
90 | ```
91 |
92 | ## 错误报告
93 |
94 | 提问时请尽可能全面而清晰地提供问题相关的信息,包括操作系统和软件版本。特别是如果有错误输出,请务必附带相关日志。
95 |
96 | 日志有两个来源,可能对定位并解决错误有所帮助。一般来说,Web 日志中就可以找到错误输出。
97 |
98 | 1. Web 日志:通过 `Ctrl+Shift+I` 或 `Cmd+Opt+I` 打开开发者工具,点击控制台 (console),查看控制台内输出的信息。
99 | 2. Rust 日志:
100 | 1. 在 macOS 或 Linux 系统下,可以进入安装位置,使用**终端**运行 `scrcpy-mask`,可在终端中实时看到程序的输出信息。
101 | 2. 在 Windows 系统下,目前只能克隆项目后自行运行,查看 Rust 输出信息。
102 |
103 | ## 贡献
104 |
105 | 如果你对这个项目感兴趣,欢迎提 PR 或 Issue。但我的时间和精力有限,所以可能无法全部及时处理。
106 |
107 | [](https://star-history.com/#AkiChase/scrcpy-mask&Date)
108 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Scrcpy Mask
2 |
3 | [中文介绍](./README-zh.md)
4 |
5 | To achieve computer control of Android devices, I developed a cross-platform desktop client using Tarui + Vue 3 + Rust. This client provides visual mouse and keyboard mapping configuration, enabling multi-touch operations similar to Android emulators through key mapping, with millisecond-level response time. This tool can be widely used for controlling Android devices from computers to play mobile games, providing a smooth touch experience.
6 |
7 | I express my deep respect and gratitude to the developers of the Scrcpy project. Scrcpy is a powerful and efficient open-source tool that greatly facilitates control over Android devices. This project is built upon the excellent architecture of Scrcpy, with optimizations and adjustments for mouse and keyboard control.
8 |
9 | **This project does not provide Scrcpy's screen mirroring feature! It only implements Scrcpy's control protocol.**
10 |
11 | Because screen mirroring may involve latency and blurriness issues, this project takes a different approach by directly abandoning screen mirroring and instead using a transparent mask to display the content behind the window (which can be AVD, low-latency screen mirroring provided by your phone manufacturers, etc.), Completely eliminates the problem of poor screen casting experience inherent in Scrcpy.
12 |
13 | **If you really need screen mirroring and don't mind the latency and performance issues,** you can use the Android app [ScreenStream](https://github.com/dkrivoruchko/ScreenStream) for LAN screen mirroring. Scrcpy Mask has been adapted to work with ScreenStream since version `v0.5.0`. While its performance may leave something to be desired, it is ready to use out of the box.
14 |
15 | Furthermore, to better support interaction between Scrcpy Mask and Android devices, I have made some modifications to the scrcpy-server, leading to the creation of a separate branch project called [scrcpy-mask-server](https://github.com/AkiChase/scrcpy-mask-server).
16 |
17 | ## Features
18 |
19 | - [x] Wired and wireless connections to Android devices
20 | - [x] Start scrcpy-server and connect to it
21 | - [x] Implement scrcpy client control protocol
22 | - [x] Mouse and keyboard key mapping
23 | - [x] Visually setting the mapping
24 | - [x] Key mapping config import and export
25 | - [x] Update check
26 | - [x] Toggle between key mapping and key input
27 | - [x] Internationalization (i18n)
28 | - [x] ScreenStream screen mirror
29 | - [ ] Gamepad key mapping
30 | - [ ] Better macro support
31 | - [x] Provide external control through websocket, see [external control](https://github.com/AkiChase/scrcpy-mask-external-control)
32 | - [ ] Help document
33 |
34 | ## Demonstration video
35 |
36 | - [别再说你不会用电脑控制手机玩手游了,Scrcpy Mask 纯小白教程+常见问题解答](https://www.bilibili.com/video/BV1Sm42157md/?share_source=copy_web&vd_source=36923115230d8a46ae8b587fc5348e6e)
37 | - [DNF 手游触屏操作反人类?但又不能在模拟器上玩 DNF 手游?不好意思,Scrcpy Mask “模拟器”的机制遥遥领先](https://www.bilibili.com/video/BV17U411Z7cN/?share_source=copy_web&vd_source=36923115230d8a46ae8b587fc5348e6e)
38 | - [如何用电脑玩 FPS 手游?这样的“安卓模拟器”,也不是不可以-哔哩哔哩](https://www.bilibili.com/video/BV1EU411Z7TC/?share_source=copy_web&vd_source=36923115230d8a46ae8b587fc5348e6e)
39 | - [M 系列 Mac 电脑玩王者,暃排位实录,使用 Android Stuido 模拟器和开源 Scrcpy Mask 按键映射工具-哔哩哔哩](https://b23.tv/q6iDW1w)
40 | - [自制跨平台开源项目 Scrcpy Mask ,像模拟器一样用键鼠控制任意安卓设备!以 M 系列芯片 MacBook 打王者为例-哔哩哔哩](https://b23.tv/gqmriXr)
41 | - [如何用 PC 控制安卓手机打王者?只要思想不滑坡,办法总比困难多!-哔哩哔哩](https://b23.tv/dmUOpff)
42 | - [M 芯片 Mac 怎么用 Android Studio 模拟器打王者?这是 Up 耗时数个月给出的答案-哔哩哔哩](https://b23.tv/ckJgyK5)
43 |
44 | ## Implementation principle
45 |
46 | - [Scrcpy Mask 实现原理剖析,如何像模拟器一样用键鼠控制你的安卓设备?架构、通信篇 - 掘金](https://juejin.cn/post/7366799820734939199)
47 | - [Scrcpy Mask 实现原理剖析,如何像模拟器一样用键鼠控制你的安卓设备?前端可视化、按键映射篇 - 掘金](https://juejin.cn/post/7367620233140748299)
48 | - [Scrcpy Mask 实现原理剖析,如何在前端实现王者荣耀中技能的准确释放? - 掘金](https://juejin.cn/post/7367568884198047807)
49 |
50 | ## Screenshot
51 |
52 | - Device control
53 |
54 | 
55 |
56 | - Key mapping setting
57 |
58 | 
59 |
60 | - Mask above game
61 |
62 | 
63 |
64 | 
65 |
66 | ## Basic using
67 |
68 | 1. Install software suitable for your system platform from [releases](https://github.com/AkiChase/scrcpy-mask/releases)
69 | 2. Identify your Android device type
70 | 1. For physical devices like phones or tablets
71 | 1. You need to solve the problem of screen casting on your own. Recommend using the official screen mirror method of your device brand to achieve the minimum delay. Since `v0.5.0` version, it can be used with [ScreenStream](https://github.com/dkrivoruchko/ScreenStream) to cast screen under the same LAN.
72 | 2. Enable ADB debugging on your device via USB or wirelessly, then connect it to your computer.
73 | 2. For emulator, you don't need screen mirror, and emulator generally default to enabling ADB wired debugging. So this is the best way for game, I think.
74 | 3. Launch the software and navigate to the Device page.
75 | 1. Find your device among the available devices (if not found, please search for how to enable ADB debugging for your device).
76 | 2. Right-click on your device again and choose "Control this device".
77 | 4. Navigate to the Settings page -> Mask Settings, set the width and height of the mask to the same ratio of the device screen size and ensure that the mask size is appropriate.
78 | 5. Navigate to the Mask page where you can see a transparent mask. Next, adjust and move your emulator window or screen mirroring window to align the displayed content area with the transparent mask area.
79 | 6. Navigate to the Key mapping page and switch or edit the key mapping configs.
80 | 7. Return to the Mask page and start enjoying.
81 |
82 | ## About Macros
83 |
84 | Currently, the structure of macros is simply a JSON object with limited functionality, serving as a transitional solution. **Please refrain from investing too much time in writing macros, as the specifications for macro creation may change with version updates.**
85 |
86 | An example of macros can be found in the `async function execMacro` function in [hotkey.ts](https://github.com/AkiChase/scrcpy-mask/blob/master/src/hotkey.ts) file.
87 |
88 | For instance, the `key-input-mode` macro can switch from key mapping mode to key input mode, commonly used for text input. An example is as follows:
89 |
90 | ```json
91 | [{ "args": [], "type": "key-input-mode" }]
92 | ```
93 |
94 | ## Error Report
95 |
96 | When asking a question, please provide as much detailed information as possible regarding the issue, including the operating system and software version. Specifically, if there is an error output, please be sure to include the relevant logs.
97 |
98 | There are two sources of logs that might help in identifying and solving the error. Generally, the error output can be found in the Web logs.
99 |
100 | 1. Web Logs: Open Developer Tools by pressing `Ctrl+Shift+I` or `Cmd+Opt+I`, click on the console tab, and check the information output in the console.
101 | 2. Rust Logs:
102 | 1. On macOS or Linux, navigate to the installation directory, use the **terminal** to run `scrcpy-mask`, and you can see the program's output in real-time in the terminal.
103 | 2. On Windows, you need to clone the project and run it yourself to view the Rust output.
104 |
105 | ## Contribution.
106 |
107 | If you are interested in this project, you are welcome to submit pull request or issue. But my time and energy is limited, so I may not be able to deal with it all.
108 |
109 | [](https://star-history.com/#AkiChase/scrcpy-mask&Date)
110 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Scrcpy Mask
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "scrcpy-mask",
3 | "private": true,
4 | "version": "0.6.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vue-tsc --noEmit && vite build",
9 | "preview": "vite preview",
10 | "tauri": "tauri",
11 | "publish": "node publish.js"
12 | },
13 | "dependencies": {
14 | "@tauri-apps/api": ">=2.0.0-beta.8",
15 | "@tauri-apps/plugin-clipboard-manager": "2.1.0-beta.1",
16 | "@tauri-apps/plugin-http": "2.0.0-beta.3",
17 | "@tauri-apps/plugin-process": "2.0.0-beta.2",
18 | "@tauri-apps/plugin-shell": "2.0.0-beta.3",
19 | "@tauri-apps/plugin-store": "2.0.0-beta.2",
20 | "pinia": "^2.1.7",
21 | "vue": "^3.3.4",
22 | "vue-i18n": "^9.13.1",
23 | "vue-router": "4"
24 | },
25 | "devDependencies": {
26 | "@tauri-apps/cli": ">=2.0.0-beta.0",
27 | "@vicons/fluent": "^0.12.0",
28 | "@vicons/ionicons5": "^0.12.0",
29 | "@vitejs/plugin-vue": "^5.0.4",
30 | "naive-ui": "^2.38.1",
31 | "sass": "^1.71.1",
32 | "typescript": "^5.0.2",
33 | "vite": "^5.0.0",
34 | "vue-tsc": "^1.8.5"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AkiChase/scrcpy-mask/8ce4b90ab8f189a6e8c00dcef71366a6641fb040/public/favicon.ico
--------------------------------------------------------------------------------
/publish.js:
--------------------------------------------------------------------------------
1 | import { execSync } from "child_process";
2 | import { readFileSync, writeFileSync } from "fs";
3 |
4 | // receive version arg
5 | process.stdin.setEncoding("utf8");
6 | console.log("Input new version:");
7 | process.stdin.on("data", function (data) {
8 | var version = data.trim();
9 | if (!version) {
10 | console.log("version is required");
11 | console.log("Input new version:");
12 | return;
13 | }
14 | process.stdin.pause();
15 | console.log("publishing version: " + version);
16 |
17 | console.log("update tauri.conf.json version");
18 | const tauri_path = "./src-tauri/tauri.conf.json";
19 | const tauri_lines = readFileSync(tauri_path, "utf8").split(
20 | "\n"
21 | );
22 | for (let i = 0; i < tauri_lines.length; i++) {
23 | if (tauri_lines[i].includes("version")) {
24 | tauri_lines[i] = ` "version": "${version}",`;
25 | break;
26 | }
27 | }
28 | writeFileSync(tauri_path, tauri_lines.join("\n"));
29 |
30 | console.log("update package.json version");
31 | console.log(
32 | execSync("pnpm version --no-git-tag-version " + version).toString()
33 | );
34 |
35 | console.log("update cargo.toml version\n");
36 | const cargo_path = "./src-tauri/Cargo.toml";
37 | const cargo_lines = readFileSync(cargo_path, "utf8").split(
38 | "\n"
39 | );
40 | for (let i = 0; i < cargo_lines.length; i++) {
41 | if (cargo_lines[i].startsWith("version")) {
42 | cargo_lines[i] = `version = "${version}"`;
43 | break;
44 | }
45 | }
46 | writeFileSync(cargo_path, cargo_lines.join("\n"));
47 |
48 | console.log("git commit and tag");
49 | console.log(
50 | execSync(
51 | `git add . && git commit -m "Scrcpy Mask v${version}" && git tag v${version}`
52 | ).toString()
53 | );
54 |
55 | console.log(
56 | "Pleash push commit and tag to github manually:\ngit push && git push --tags"
57 | );
58 | });
59 |
--------------------------------------------------------------------------------
/src-tauri/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
5 | # Generated by Tauri
6 | # will have schema files for capabilities auto-completion
7 | /gen/schemas
8 |
9 | /Cargo.lock
10 |
--------------------------------------------------------------------------------
/src-tauri/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "scrcpy-mask"
3 | version = "0.6.0"
4 | description = "A Tauri App"
5 | authors = ["AkiChase"]
6 | edition = "2021"
7 |
8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
9 |
10 | [build-dependencies]
11 | tauri-build = { version = "2.0.0-beta", features = [] }
12 |
13 | [dependencies]
14 | tauri = { version = "2.0.0-beta.18", features = ["macos-private-api", "devtools"] }
15 | tauri-plugin-store = "2.0.0-beta"
16 | serde = { version = "1", features = ["derive"] }
17 | serde_json = "1"
18 | anyhow = "1.0"
19 | lazy_static = "1.4.0"
20 | tokio = { version = "1.36.0", features = ["rt-multi-thread", "net", "macros", "io-util", "time", "sync"] }
21 | tauri-plugin-process = "2.0.0-beta"
22 | tauri-plugin-shell = "2.0.0-beta"
23 | tauri-plugin-http = "2.0.0-beta"
24 | tauri-plugin-clipboard-manager = "2.1.0-beta.2"
25 |
--------------------------------------------------------------------------------
/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/src-tauri/capabilities/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../gen/schemas/desktop-schema.json",
3 | "identifier": "default",
4 | "description": "Capability for the main window",
5 | "windows": ["main"],
6 | "permissions": [
7 | "event:default",
8 | "window:default",
9 | "window:allow-set-position",
10 | "window:allow-set-size",
11 | "window:allow-maximize",
12 | "window:allow-minimize",
13 | "window:allow-close",
14 | "window:allow-is-maximizable",
15 | "window:allow-start-dragging",
16 | "window:allow-unmaximize",
17 | "window:allow-set-cursor-position",
18 | "window:allow-set-cursor-visible",
19 | "store:default",
20 | "store:allow-get",
21 | "store:allow-set",
22 | "store:allow-save",
23 | "store:allow-load",
24 | "store:allow-clear",
25 | "store:allow-entries",
26 | "store:allow-delete",
27 | "process:default",
28 | "process:allow-restart",
29 | "webview:default",
30 | "webview:allow-internal-toggle-devtools",
31 | "shell:default",
32 | "shell:allow-open",
33 | "http:default",
34 | {
35 | "identifier": "http:default",
36 | "allow": [
37 | {
38 | "url": "https://api.github.com/repos/AkiChase/scrcpy-mask/*"
39 | }
40 | ]
41 | },
42 | "http:allow-fetch",
43 | "app:default",
44 | "app:allow-version",
45 | "clipboard-manager:default",
46 | "clipboard-manager:allow-read-text",
47 | "clipboard-manager:allow-write-text"
48 | ]
49 | }
50 |
--------------------------------------------------------------------------------
/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AkiChase/scrcpy-mask/8ce4b90ab8f189a6e8c00dcef71366a6641fb040/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AkiChase/scrcpy-mask/8ce4b90ab8f189a6e8c00dcef71366a6641fb040/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AkiChase/scrcpy-mask/8ce4b90ab8f189a6e8c00dcef71366a6641fb040/src-tauri/icons/32x32.png
--------------------------------------------------------------------------------
/src-tauri/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AkiChase/scrcpy-mask/8ce4b90ab8f189a6e8c00dcef71366a6641fb040/src-tauri/icons/icon.icns
--------------------------------------------------------------------------------
/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AkiChase/scrcpy-mask/8ce4b90ab8f189a6e8c00dcef71366a6641fb040/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/src-tauri/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AkiChase/scrcpy-mask/8ce4b90ab8f189a6e8c00dcef71366a6641fb040/src-tauri/icons/icon.png
--------------------------------------------------------------------------------
/src-tauri/resource/scrcpy-mask-server-v2.4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AkiChase/scrcpy-mask/8ce4b90ab8f189a6e8c00dcef71366a6641fb040/src-tauri/resource/scrcpy-mask-server-v2.4
--------------------------------------------------------------------------------
/src-tauri/src/adb.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | io::BufRead,
3 | process::{Child, Command, Stdio},
4 | };
5 |
6 | #[cfg(target_os = "windows")]
7 | use std::os::windows::process::CommandExt;
8 |
9 | use anyhow::{Context, Ok, Result};
10 |
11 | use crate::share;
12 |
13 | #[derive(Clone, Debug, serde::Serialize)]
14 | pub struct Device {
15 | pub id: String,
16 | pub status: String,
17 | }
18 |
19 | impl Device {
20 | /// execute "adb push" to push file from src to des
21 | pub fn cmd_push(id: &str, src: &str, des: &str) -> Result {
22 | let mut adb_command = Adb::cmd_base();
23 | let res = adb_command
24 | .args(&["-s", id, "push", src, des])
25 | .output()
26 | .with_context(|| format!("Failed to execute 'adb push {} {}'", src, des))?;
27 | Ok(String::from_utf8(res.stdout).unwrap())
28 | }
29 |
30 | /// execute "adb reverse" to reverse the device port to local port
31 | pub fn cmd_reverse(id: &str, remote: &str, local: &str) -> Result<()> {
32 | let mut adb_command = Adb::cmd_base();
33 | adb_command
34 | .args(&["-s", id, "reverse", remote, local])
35 | .output()
36 | .with_context(|| format!("Failed to execute 'adb reverse {} {}'", remote, local))?;
37 | Ok(())
38 | }
39 |
40 | /// execute "adb forward" to forward the local port to the device
41 | pub fn cmd_forward(id: &str, local: &str, remote: &str) -> Result<()> {
42 | let mut adb_command = Adb::cmd_base();
43 | adb_command
44 | .args(&["-s", id, "forward", local, remote])
45 | .output()
46 | .with_context(|| format!("Failed to execute 'adb forward {} {}'", local, remote))?;
47 | Ok(())
48 | }
49 |
50 | /// execute "adb shell" to execute shell command on the device
51 | pub fn cmd_shell(id: &str, shell_args: &[&str]) -> Result {
52 | let mut adb_command = Adb::cmd_base();
53 | let mut args = vec!["-s", id, "shell"];
54 | args.extend_from_slice(shell_args);
55 | Ok(adb_command
56 | .args(args)
57 | .stdout(Stdio::piped())
58 | .spawn()
59 | .context("Failed to execute 'adb shell'")?)
60 | }
61 |
62 | /// execute "adb shell wm size" to get screen size
63 | pub fn cmd_screen_size(id: &str) -> Result<(u32, u32)> {
64 | let mut adb_command = Adb::cmd_base();
65 | let output = adb_command
66 | .args(&["-s", id, "shell", "wm", "size"])
67 | .output()
68 | .context("Failed to execute 'adb shell wm size'")?;
69 |
70 | for line in output.stdout.lines() {
71 | if let std::result::Result::Ok(line) = line {
72 | if line.starts_with("Physical size: ") {
73 | let size_str = line.trim_start_matches("Physical size: ").split('x');
74 | let width = size_str.clone().next().unwrap().parse::().unwrap();
75 | let height = size_str.clone().last().unwrap().parse::().unwrap();
76 | return Ok((width, height));
77 | }
78 | }
79 | }
80 | Err(anyhow::anyhow!("Failed to get screen size"))
81 | }
82 | }
83 |
84 | pub struct Adb;
85 |
86 | /// Module to execute adb command and fetch output.
87 | /// But some output of command won't be output, like adb service startup information.
88 | impl Adb {
89 | pub fn cmd_base() -> Command {
90 | let adb_path = share::ADB_PATH.lock().unwrap().clone();
91 | #[cfg(target_os = "windows")]
92 | {
93 | let mut cmd = Command::new(adb_path);
94 | cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
95 | return cmd;
96 | }
97 | #[cfg(not(target_os = "windows"))]
98 | Command::new(adb_path)
99 | }
100 |
101 | /// execute "adb devices" and return devices list
102 | pub fn cmd_devices() -> Result> {
103 | let mut adb_command = Adb::cmd_base();
104 | let output = adb_command
105 | .args(&["devices"])
106 | .output()
107 | .context("Failed to execute 'adb devices'")?;
108 |
109 | let mut devices_vec: Vec = Vec::new();
110 | let mut lines = output.stdout.lines();
111 | // skip first line
112 | lines.next();
113 |
114 | // parse string to Device
115 | for line in lines {
116 | if let std::result::Result::Ok(s) = line {
117 | let device_info: Vec<&str> = s.split('\t').collect();
118 | if device_info.len() == 2 {
119 | devices_vec.push(Device {
120 | id: device_info[0].to_string(),
121 | status: device_info[1].to_string(),
122 | });
123 | }
124 | }
125 | }
126 | Ok(devices_vec)
127 | }
128 |
129 | /// execute "adb kill-server"
130 | pub fn cmd_kill_server() -> Result<()> {
131 | let mut adb_command = Adb::cmd_base();
132 | adb_command
133 | .args(&["kill-server"])
134 | .output()
135 | .context("Failed to execute 'adb kill-server'")?;
136 | Ok(())
137 | }
138 |
139 | /// execute "adb reverse --remove-all"
140 | pub fn cmd_reverse_remove() -> Result<()> {
141 | let mut adb_command = Adb::cmd_base();
142 | adb_command
143 | .args(&["reverse", " --remove-all"])
144 | .output()
145 | .context("Failed to execute 'adb reverse --remove-all'")?;
146 | Ok(())
147 | }
148 |
149 | /// execute "adb forward --remove-all"
150 | pub fn cmd_forward_remove() -> Result<()> {
151 | let mut adb_command = Adb::cmd_base();
152 | adb_command
153 | .args(&["forward", " --remove-all"])
154 | .output()
155 | .context("Failed to execute 'adb forward --remove-all'")?;
156 | Ok(())
157 | }
158 |
159 | /// execute "adb start-server"
160 | pub fn cmd_start_server() -> Result<()> {
161 | let mut adb_command = Adb::cmd_base();
162 | adb_command
163 | .args(&["start-server"])
164 | .output()
165 | .context("Failed to execute 'adb start-server'")?;
166 | Ok(())
167 | }
168 |
169 | pub fn cmd_connect(address: &str) -> Result {
170 | let mut adb_command = Adb::cmd_base();
171 | let output = adb_command
172 | .args(&["connect", address])
173 | .output()
174 | .context(format!("Failed to execute 'adb connect {}'", address))?;
175 |
176 | let res = String::from_utf8(output.stdout)?;
177 | Ok(res)
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/src-tauri/src/binary.rs:
--------------------------------------------------------------------------------
1 | pub fn write_16be(buf: &mut [u8], val: u16) {
2 | buf[0] = (val >> 8) as u8;
3 | buf[1] = val as u8;
4 | }
5 |
6 | pub fn write_32be(buf: &mut [u8], val: u32) {
7 | buf[0] = (val >> 24) as u8;
8 | buf[1] = (val >> 16) as u8;
9 | buf[2] = (val >> 8) as u8;
10 | buf[3] = val as u8;
11 | }
12 |
13 | pub fn write_64be(buf: &mut [u8], val: u64) {
14 | buf[0] = (val >> 56) as u8;
15 | buf[1] = (val >> 48) as u8;
16 | buf[2] = (val >> 40) as u8;
17 | buf[3] = (val >> 32) as u8;
18 | buf[4] = (val >> 24) as u8;
19 | buf[5] = (val >> 16) as u8;
20 | buf[6] = (val >> 8) as u8;
21 | buf[7] = val as u8;
22 | }
23 |
24 | pub fn float_to_u16fp(mut f: f32) -> u16 {
25 | if f < 0.0 || f > 1.0 {
26 | f = 1.0;
27 | }
28 | let mut u: u32 = (f * (1 << 16) as f32) as u32;
29 | if u >= 0xffff {
30 | u = 0xffff;
31 | }
32 | u as u16
33 | }
34 |
35 | pub fn float_to_i16fp(f: f32) -> i16 {
36 | assert!(f >= -1.0 && f <= 1.0);
37 | let mut i: i32 = (f * (1 << 15) as f32) as i32;
38 | assert!(i >= -0x8000);
39 | if i >= 0x7fff {
40 | assert_eq!(i, 0x8000); // for f == 1.0
41 | i = 0x7fff;
42 | }
43 | i as i16
44 | }
45 |
46 | pub fn write_posion(buf: &mut [u8], x: i32, y: i32, w: u16, h: u16) {
47 | write_32be(buf, x as u32);
48 | write_32be(&mut buf[4..8], y as u32);
49 | write_16be(&mut buf[8..10], w);
50 | write_16be(&mut buf[10..12], h);
51 | }
52 |
53 | pub fn write_string(utf8: &str, max_len: usize, buf: &mut Vec) {
54 | let len = str_utf8_truncation_index(utf8, max_len) as u32;
55 | // first 4 bytes for length
56 | let len_bytes = len.to_be_bytes();
57 | buf.extend_from_slice(&len_bytes);
58 | // then [len] bytes for the string
59 | buf.extend_from_slice(utf8.as_bytes())
60 | }
61 |
62 | // truncate utf8 string to max_len bytes
63 | fn str_utf8_truncation_index(utf8: &str, max_len: usize) -> usize {
64 | let len = utf8.len();
65 | if len <= max_len {
66 | return len;
67 | }
68 | let mut len = max_len;
69 | while utf8.is_char_boundary(len) {
70 | len -= 1;
71 | }
72 | len
73 | }
74 |
--------------------------------------------------------------------------------
/src-tauri/src/client.rs:
--------------------------------------------------------------------------------
1 | use anyhow::{Ok, Result};
2 | use std::{io::BufRead, path::PathBuf};
3 |
4 | use crate::{
5 | adb::{Adb, Device},
6 | resource::{ResHelper, ResourceName},
7 | share,
8 | };
9 |
10 | /**
11 | * the client of scrcpy
12 | */
13 | #[derive(Debug)]
14 | pub struct ScrcpyClient {
15 | pub device: Device,
16 | pub version: String,
17 | pub scid: String,
18 | pub port: u16,
19 | }
20 |
21 | impl ScrcpyClient {
22 | pub fn get_scrcpy_version() -> String {
23 | ResHelper::get_scrcpy_version()
24 | }
25 |
26 | pub fn adb_devices() -> Result> {
27 | Adb::cmd_devices()
28 | }
29 |
30 | pub fn adb_restart_server() -> Result<()> {
31 | Adb::cmd_kill_server()?;
32 | Adb::cmd_start_server()?;
33 | Ok(())
34 | }
35 |
36 | pub fn adb_reverse_remove() -> Result<()> {
37 | Adb::cmd_reverse_remove()
38 | }
39 |
40 | pub fn adb_forward_remove() -> Result<()> {
41 | Adb::cmd_forward_remove()
42 | }
43 |
44 | // get the screen size of the device
45 | pub fn get_device_screen_size(id: &str) -> Result<(u32, u32)> {
46 | Device::cmd_screen_size(id)
47 | }
48 |
49 | /// push server file to current device
50 | pub fn push_server_file(dir: &PathBuf, id: &str) -> Result<()> {
51 | let info = Device::cmd_push(
52 | id,
53 | &ResHelper::get_file_path(dir, ResourceName::ScrcpyServer).to_string_lossy(),
54 | "/data/local/tmp/scrcpy-server.jar",
55 | )?;
56 |
57 | println!("{}\nSuccessfully push server files", info);
58 | Ok(())
59 | }
60 |
61 | /// forward the local port to the device
62 | pub fn forward_server_port(id: &str, scid: &str, port: u16) -> Result<()> {
63 | Device::cmd_forward(
64 | id,
65 | &format!("tcp:{}", port),
66 | &format!("localabstract:scrcpy_{}", scid),
67 | )?;
68 | println!("Successfully forward port");
69 | Ok(())
70 | }
71 |
72 | /// reverse the device port to the local port
73 | pub fn reverse_server_port(id: &str, scid: &str, port: u16) -> Result<()> {
74 | Device::cmd_reverse(
75 | id,
76 | &format!("localabstract:scrcpy_{}", scid),
77 | &format!("tcp:{}", port),
78 | )?;
79 | println!("Successfully reverse port");
80 | Ok(())
81 | }
82 |
83 | /// spawn a new thread to start scrcpy server
84 | pub fn shell_start_server(id: &str, scid: &str, version: &str) -> Result<()> {
85 | let mut child = Device::cmd_shell(
86 | id,
87 | &[
88 | "CLASSPATH=/data/local/tmp/scrcpy-server.jar",
89 | "app_process",
90 | "/",
91 | "com.genymobile.scrcpy.Server",
92 | version,
93 | &format!("scid={}", scid),
94 | "tunnel_forward=true",
95 | "video=false",
96 | "audio=false",
97 | ],
98 | )?;
99 |
100 | println!("Starting scrcpy server...");
101 | let out = child.stdout.take().unwrap();
102 | let mut out = std::io::BufReader::new(out);
103 | let mut s = String::new();
104 |
105 | while let core::result::Result::Ok(_) = out.read_line(&mut s) {
106 | // break at the end of program
107 | if let core::result::Result::Ok(Some(_)) = child.try_wait() {
108 | break;
109 | }
110 | print!("{}", s);
111 | // clear string to store new line only
112 | s.clear();
113 | }
114 |
115 | *share::CLIENT_INFO.lock().unwrap() = None;
116 | println!("Scrcpy server closed");
117 | Ok(())
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src-tauri/src/control_msg.rs:
--------------------------------------------------------------------------------
1 | use crate::binary;
2 | use tokio::{io::AsyncWriteExt, net::tcp::OwnedWriteHalf};
3 |
4 | pub const SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH: usize = 300;
5 | pub const SC_CONTROL_MSG_MAX_SIZE: usize = 1 << 18; // 256k
6 | pub const SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH: usize = SC_CONTROL_MSG_MAX_SIZE - 14;
7 |
8 | pub fn gen_ctrl_msg(ctrl_msg_type: ControlMsgType, payload: &serde_json::Value) -> Vec {
9 | match ctrl_msg_type {
10 | ControlMsgType::ControlMsgTypeInjectKeycode => gen_inject_key_ctrl_msg(
11 | ctrl_msg_type as u8,
12 | payload["action"].as_u64().unwrap() as u8,
13 | payload["keycode"].as_u64().unwrap() as u32,
14 | payload["repeat"].as_u64().unwrap() as u32,
15 | payload["metastate"].as_u64().unwrap() as u32,
16 | ),
17 | ControlMsgType::ControlMsgTypeInjectText => {
18 | let mut buf: Vec = vec![ctrl_msg_type as u8];
19 | let text = payload["text"].as_str().unwrap();
20 | binary::write_string(text, SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH, &mut buf);
21 | buf
22 | }
23 | ControlMsgType::ControlMsgTypeInjectTouchEvent => gen_inject_touch_ctrl_msg(
24 | ctrl_msg_type as u8,
25 | payload["action"].as_u64().unwrap() as u8,
26 | payload["pointerId"].as_u64().unwrap(),
27 | payload["position"]["x"].as_i64().unwrap() as i32,
28 | payload["position"]["y"].as_i64().unwrap() as i32,
29 | payload["position"]["w"].as_i64().unwrap() as u16,
30 | payload["position"]["h"].as_i64().unwrap() as u16,
31 | binary::float_to_u16fp(payload["pressure"].as_f64().unwrap() as f32),
32 | payload["actionButton"].as_u64().unwrap() as u32,
33 | payload["buttons"].as_u64().unwrap() as u32,
34 | ),
35 | ControlMsgType::ControlMsgTypeInjectScrollEvent => {
36 | let mut buf = vec![0; 21];
37 | buf[0] = ctrl_msg_type as u8;
38 | binary::write_posion(
39 | &mut buf[1..13],
40 | payload["position"]["x"].as_i64().unwrap() as i32,
41 | payload["position"]["y"].as_i64().unwrap() as i32,
42 | payload["position"]["w"].as_i64().unwrap() as u16,
43 | payload["position"]["h"].as_i64().unwrap() as u16,
44 | );
45 | binary::write_16be(
46 | &mut buf[13..15],
47 | binary::float_to_i16fp(payload["hscroll"].as_f64().unwrap() as f32) as u16,
48 | );
49 | binary::write_16be(
50 | &mut buf[15..17],
51 | binary::float_to_i16fp(payload["vscroll"].as_f64().unwrap() as f32) as u16,
52 | );
53 | binary::write_32be(
54 | &mut buf[17..21],
55 | payload["buttons"].as_u64().unwrap() as u32,
56 | );
57 | buf
58 | }
59 | ControlMsgType::ControlMsgTypeBackOrScreenOn => {
60 | vec![
61 | ctrl_msg_type as u8,
62 | payload["action"].as_u64().unwrap() as u8,
63 | ]
64 | }
65 | ControlMsgType::ControlMsgTypeGetClipboard => {
66 | vec![
67 | ctrl_msg_type as u8,
68 | payload["copyKey"].as_u64().unwrap() as u8,
69 | ]
70 | }
71 | ControlMsgType::ControlMsgTypeSetClipboard => {
72 | let mut buf: Vec = vec![0; 10];
73 | buf[0] = ctrl_msg_type as u8;
74 | binary::write_64be(&mut buf[1..9], payload["sequence"].as_u64().unwrap());
75 | buf[9] = payload["paste"].as_bool().unwrap_or(false) as u8;
76 | let text = payload["text"].as_str().unwrap();
77 | binary::write_string(text, SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH, &mut buf);
78 | buf
79 | }
80 | ControlMsgType::ControlMsgTypeSetScreenPowerMode => {
81 | vec![ctrl_msg_type as u8, payload["mode"].as_u64().unwrap() as u8]
82 | }
83 | ControlMsgType::ControlMsgTypeUhidCreate => {
84 | let size = payload["reportDescSize"].as_u64().unwrap() as u16;
85 | let mut buf: Vec = vec![0; 5];
86 | buf[0] = ctrl_msg_type as u8;
87 | binary::write_16be(&mut buf[1..3], payload["id"].as_u64().unwrap() as u16);
88 | binary::write_16be(&mut buf[3..5], size);
89 | let report_desc = payload["reportDesc"].as_array().unwrap();
90 | let report_desc_u8: Vec = report_desc
91 | .iter()
92 | .map(|x| x.as_u64().unwrap() as u8)
93 | .collect();
94 | buf.extend_from_slice(&report_desc_u8);
95 | buf
96 | }
97 | ControlMsgType::ControlMsgTypeUhidInput => {
98 | let size = payload["size"].as_u64().unwrap() as u16;
99 | let mut buf: Vec = vec![0; 5];
100 | buf[0] = ctrl_msg_type as u8;
101 | binary::write_16be(&mut buf[1..3], payload["id"].as_u64().unwrap() as u16);
102 | binary::write_16be(&mut buf[3..5], size);
103 | let data = payload["data"].as_array().unwrap();
104 | let data_u8: Vec = data.iter().map(|x| x.as_u64().unwrap() as u8).collect();
105 | buf.extend_from_slice(&data_u8);
106 | buf
107 | }
108 | // other control message types do not have a payload
109 | _ => {
110 | vec![ctrl_msg_type as u8]
111 | }
112 | }
113 | }
114 |
115 | pub fn gen_inject_key_ctrl_msg(
116 | ctrl_msg_type: u8,
117 | action: u8,
118 | keycode: u32,
119 | repeat: u32,
120 | metastate: u32,
121 | ) -> Vec {
122 | let mut buf = vec![0; 14];
123 | buf[0] = ctrl_msg_type;
124 | buf[1] = action;
125 | binary::write_32be(&mut buf[2..6], keycode);
126 | binary::write_32be(&mut buf[6..10], repeat);
127 | binary::write_32be(&mut buf[10..14], metastate);
128 | buf
129 | }
130 |
131 | pub fn gen_inject_touch_ctrl_msg(
132 | ctrl_msg_type: u8,
133 | action: u8,
134 | pointer_id: u64,
135 | x: i32,
136 | y: i32,
137 | w: u16,
138 | h: u16,
139 | pressure: u16,
140 | action_button: u32,
141 | buttons: u32,
142 | ) -> Vec {
143 | let mut buf = vec![0; 32];
144 | buf[0] = ctrl_msg_type;
145 | buf[1] = action;
146 | binary::write_64be(&mut buf[2..10], pointer_id);
147 | binary::write_posion(&mut buf[10..22], x, y, w, h);
148 | binary::write_16be(&mut buf[22..24], pressure);
149 | binary::write_32be(&mut buf[24..28], action_button);
150 | binary::write_32be(&mut buf[28..32], buttons);
151 | buf
152 | }
153 |
154 | pub async fn send_ctrl_msg(
155 | ctrl_msg_type: ControlMsgType,
156 | payload: &serde_json::Value,
157 | writer: &mut OwnedWriteHalf,
158 | ) {
159 | let buf = gen_ctrl_msg(ctrl_msg_type, payload);
160 | writer.write_all(&buf).await.unwrap();
161 | writer.flush().await.unwrap();
162 | }
163 |
164 | pub enum ControlMsgType {
165 | ControlMsgTypeInjectKeycode, //发送原始按键
166 | ControlMsgTypeInjectText, //发送文本,不知道是否能输入中文(估计只是把文本转为keycode的输入效果)
167 | ControlMsgTypeInjectTouchEvent, //发送触摸事件
168 | ControlMsgTypeInjectScrollEvent, //发送滚动事件(类似接入鼠标后滚动滚轮的效果,不是通过触摸实现的)
169 | ControlMsgTypeBackOrScreenOn, //应该就是发送返回键
170 | ControlMsgTypeExpandNotificationPanel, //打开消息面板
171 | ControlMsgTypeExpandSettingsPanel, //打开设置面板(就是消息面板右侧的)
172 | ControlMsgTypeCollapsePanels, //折叠上述面板
173 | ControlMsgTypeGetClipboard, //获取剪切板
174 | ControlMsgTypeSetClipboard, //设置剪切板
175 | ControlMsgTypeSetScreenPowerMode, //设置屏幕电源模式,是关闭设备屏幕的(SC_SCREEN_POWER_MODE_OFF 和 SC_SCREEN_POWER_MODE_NORMAL )
176 | ControlMsgTypeRotateDevice, //旋转设备屏幕
177 | ControlMsgTypeUhidCreate, //创建虚拟设备?从而模拟真实的键盘、鼠标用的,目前没用
178 | ControlMsgTypeUhidInput, //同上转发键盘、鼠标的输入,目前没用
179 | ControlMsgTypeOpenHardKeyboardSettings, //打开设备的硬件键盘设置,目前没用
180 | }
181 |
182 | impl ControlMsgType {
183 | pub fn from_i64(value: i64) -> Option {
184 | match value {
185 | 0 => Some(Self::ControlMsgTypeInjectKeycode),
186 | 1 => Some(Self::ControlMsgTypeInjectText),
187 | 2 => Some(Self::ControlMsgTypeInjectTouchEvent),
188 | 3 => Some(Self::ControlMsgTypeInjectScrollEvent),
189 | 4 => Some(Self::ControlMsgTypeBackOrScreenOn),
190 | 5 => Some(Self::ControlMsgTypeExpandNotificationPanel),
191 | 6 => Some(Self::ControlMsgTypeExpandSettingsPanel),
192 | 7 => Some(Self::ControlMsgTypeCollapsePanels),
193 | 8 => Some(Self::ControlMsgTypeGetClipboard),
194 | 9 => Some(Self::ControlMsgTypeSetClipboard),
195 | 10 => Some(Self::ControlMsgTypeSetScreenPowerMode),
196 | 11 => Some(Self::ControlMsgTypeRotateDevice),
197 | 12 => Some(Self::ControlMsgTypeUhidCreate),
198 | 13 => Some(Self::ControlMsgTypeUhidInput),
199 | 14 => Some(Self::ControlMsgTypeOpenHardKeyboardSettings),
200 | _ => None,
201 | }
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/src-tauri/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod adb;
2 | pub mod binary;
3 | pub mod client;
4 | pub mod control_msg;
5 | pub mod resource;
6 | pub mod scrcpy_mask_cmd;
7 | pub mod share;
8 | pub mod socket;
9 |
--------------------------------------------------------------------------------
/src-tauri/src/main.rs:
--------------------------------------------------------------------------------
1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!!
2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
3 |
4 | use scrcpy_mask::{
5 | adb::{Adb, Device},
6 | client::ScrcpyClient,
7 | resource::{ResHelper, ResourceName},
8 | share,
9 | socket::connect_socket,
10 | };
11 | use std::{fs::read_to_string, sync::Arc};
12 | use tauri::Manager;
13 |
14 | #[tauri::command]
15 | /// get devices info list
16 | fn adb_devices() -> Result, String> {
17 | match Adb::cmd_devices() {
18 | Ok(devices) => Ok(devices),
19 | Err(e) => Err(e.to_string()),
20 | }
21 | }
22 |
23 | #[tauri::command]
24 | /// forward local port to the device port
25 | fn forward_server_port(id: String, scid: String, port: u16) -> Result<(), String> {
26 | match ScrcpyClient::forward_server_port(&id, &scid, port) {
27 | Ok(_) => Ok(()),
28 | Err(e) => Err(e.to_string()),
29 | }
30 | }
31 |
32 | #[tauri::command]
33 | /// push scrcpy-server file to the device
34 | fn push_server_file(id: String, app: tauri::AppHandle) -> Result<(), String> {
35 | let dir = app.path().resource_dir().unwrap().join("resource");
36 | match ScrcpyClient::push_server_file(&dir, &id) {
37 | Ok(_) => Ok(()),
38 | Err(e) => Err(e.to_string()),
39 | }
40 | }
41 |
42 | #[tauri::command]
43 | /// start scrcpy server and connect to it
44 | fn start_scrcpy_server(
45 | id: String,
46 | scid: String,
47 | address: String,
48 | app: tauri::AppHandle,
49 | ) -> Result<(), String> {
50 | let mut client_info = share::CLIENT_INFO.lock().unwrap();
51 | if let Some(_) = &*client_info {
52 | return Err("client already exists".to_string());
53 | }
54 |
55 | *client_info = Some(share::ClientInfo::new(
56 | "unknow".to_string(),
57 | id.clone(),
58 | scid.clone(),
59 | ));
60 |
61 | let version = ScrcpyClient::get_scrcpy_version();
62 |
63 | // start scrcpy server
64 | tokio::spawn(async move {
65 | ScrcpyClient::shell_start_server(&id, &scid, &version).unwrap();
66 | });
67 |
68 | // connect to scrcpy server
69 | tokio::spawn(async move {
70 | // wait 1 second for scrcpy-server to start
71 | tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
72 |
73 | let app = Arc::new(app);
74 |
75 | // create channel to transmit device reply to front
76 | let share_app = app.clone();
77 | let (device_reply_sender, mut device_reply_receiver) =
78 | tokio::sync::mpsc::channel::(16);
79 | println!("device reply channel created");
80 | tokio::spawn(async move {
81 | while let Some(reply) = device_reply_receiver.recv().await {
82 | share_app.emit("device-reply", reply).unwrap();
83 | }
84 | println!("device reply channel closed");
85 | });
86 |
87 | // create channel to transmit front msg to TcpStream handler
88 | let (front_msg_sender, front_msg_receiver) = tokio::sync::mpsc::channel::(16);
89 | let share_app = app.clone();
90 | let listen_handler = share_app.listen("front-command", move |event| {
91 | let sender = front_msg_sender.clone();
92 | // println!("收到front-command: {}", event.payload());
93 | tokio::spawn(async move {
94 | if let Err(_) = sender.send(event.payload().into()).await {
95 | println!("front-command forwarding failure, please restart the program !");
96 | };
97 | });
98 | });
99 |
100 | // connect
101 | let share_app = app.clone();
102 | tokio::spawn(connect_socket(
103 | address,
104 | front_msg_receiver,
105 | device_reply_sender,
106 | listen_handler,
107 | share_app,
108 | ));
109 | });
110 |
111 | Ok(())
112 | }
113 |
114 | #[tauri::command]
115 | fn get_cur_client_info() -> Result, String> {
116 | let client_info = share::CLIENT_INFO.lock().unwrap();
117 | match &*client_info {
118 | Some(client) => Ok(Some(client.clone())),
119 | None => Ok(None),
120 | }
121 | }
122 |
123 | #[tauri::command]
124 | /// get device screen size
125 | fn get_device_screen_size(id: String) -> Result<(u32, u32), String> {
126 | match ScrcpyClient::get_device_screen_size(&id) {
127 | Ok(size) => Ok(size),
128 | Err(e) => Err(e.to_string()),
129 | }
130 | }
131 |
132 | #[tauri::command]
133 | /// connect to wireless device
134 | fn adb_connect(address: String) -> Result {
135 | match Adb::cmd_connect(&address) {
136 | Ok(res) => Ok(res),
137 | Err(e) => Err(e.to_string()),
138 | }
139 | }
140 |
141 | #[tauri::command]
142 | /// load default key mapping config file
143 | fn load_default_keyconfig(app: tauri::AppHandle) -> Result {
144 | let dir = app.path().resource_dir().unwrap().join("resource");
145 | let file = ResHelper::get_file_path(&dir, ResourceName::DefaultKeyConfig);
146 | match read_to_string(file) {
147 | Ok(content) => Ok(content),
148 | Err(e) => Err(e.to_string()),
149 | }
150 | }
151 |
152 | #[tauri::command]
153 | fn check_adb_available() -> Result<(), String> {
154 | match Adb::cmd_base().output() {
155 | Ok(_) => Ok(()),
156 | Err(e) => Err(e.to_string()),
157 | }
158 | }
159 |
160 | #[tauri::command]
161 | fn set_adb_path(adb_path: String, app: tauri::AppHandle) -> Result<(), String> {
162 | let app_h = app.app_handle().clone();
163 | let stores = app_h.state::>();
164 | let path = std::path::PathBuf::from("store.bin");
165 | let store_res: Result<(), tauri_plugin_store::Error> =
166 | tauri_plugin_store::with_store(app, stores, path, |store| {
167 | store.insert(
168 | "adbPath".to_string(),
169 | serde_json::Value::String(adb_path.clone()),
170 | )?;
171 | *share::ADB_PATH.lock().unwrap() = adb_path;
172 | Ok(())
173 | });
174 |
175 | match store_res {
176 | Ok(_) => Ok(()),
177 | Err(e) => Err(e.to_string()),
178 | }
179 | }
180 |
181 | #[tokio::main]
182 | async fn main() {
183 | tauri::Builder::default()
184 | .plugin(tauri_plugin_clipboard_manager::init())
185 | .plugin(tauri_plugin_http::init())
186 | .plugin(tauri_plugin_shell::init())
187 | .plugin(tauri_plugin_process::init())
188 | .plugin(tauri_plugin_store::Builder::new().build())
189 | .setup(|app| {
190 | let stores = app
191 | .app_handle()
192 | .state::>();
193 | let path: std::path::PathBuf = std::path::PathBuf::from("store.bin");
194 | tauri_plugin_store::with_store(app.app_handle().clone(), stores, path, |store| {
195 | // load adb path
196 | match store.get("adbPath") {
197 | Some(value) => {
198 | *share::ADB_PATH.lock().unwrap() = value.as_str().unwrap().to_string()
199 | }
200 | None => store
201 | .insert(
202 | "adbPath".to_string(),
203 | serde_json::Value::String("adb".to_string()),
204 | )
205 | .unwrap(),
206 | };
207 |
208 | // restore window position and size
209 | match store.get("maskArea") {
210 | Some(value) => {
211 | let pos_x = value["posX"].as_i64();
212 | let pos_y = value["posY"].as_i64();
213 | let size_w = value["sizeW"].as_i64().unwrap_or(800);
214 | let size_h = value["sizeH"].as_i64().unwrap_or(600);
215 |
216 | let main_window: tauri::WebviewWindow =
217 | app.get_webview_window("main").unwrap();
218 |
219 | main_window.set_zoom(1.).unwrap_or(());
220 |
221 | if pos_x.is_none() || pos_y.is_none() {
222 | main_window.center().unwrap_or(());
223 | } else {
224 | main_window
225 | .set_position(tauri::Position::Logical(tauri::LogicalPosition {
226 | x: (pos_x.unwrap() - 70) as f64,
227 | y: (pos_y.unwrap() - 30) as f64,
228 | }))
229 | .unwrap();
230 | }
231 |
232 | main_window
233 | .set_size(tauri::Size::Logical(tauri::LogicalSize {
234 | width: (size_w + 70) as f64,
235 | height: (size_h + 30) as f64,
236 | }))
237 | .unwrap();
238 | }
239 | None => {
240 | let main_window: tauri::WebviewWindow =
241 | app.get_webview_window("main").unwrap();
242 |
243 | main_window.center().unwrap_or(());
244 |
245 | main_window
246 | .set_size(tauri::Size::Logical(tauri::LogicalSize {
247 | width: (800 + 70) as f64,
248 | height: (600 + 30) as f64,
249 | }))
250 | .unwrap();
251 | }
252 | }
253 |
254 | Ok(())
255 | })
256 | .unwrap();
257 |
258 | // check resource files
259 | ResHelper::res_init(
260 | &app.path()
261 | .resource_dir()
262 | .expect("failed to find resource")
263 | .join("resource"),
264 | )
265 | .unwrap();
266 | Ok(())
267 | })
268 | .invoke_handler(tauri::generate_handler![
269 | adb_devices,
270 | forward_server_port,
271 | push_server_file,
272 | start_scrcpy_server,
273 | get_cur_client_info,
274 | get_device_screen_size,
275 | adb_connect,
276 | load_default_keyconfig,
277 | check_adb_available,
278 | set_adb_path
279 | ])
280 | .run(tauri::generate_context!())
281 | .expect("error while running tauri application");
282 | }
283 |
--------------------------------------------------------------------------------
/src-tauri/src/resource.rs:
--------------------------------------------------------------------------------
1 | use anyhow::{anyhow, Ok, Result};
2 | use std::path::PathBuf;
3 |
4 | pub enum ResourceName {
5 | ScrcpyServer,
6 | DefaultKeyConfig,
7 | }
8 |
9 | pub struct ResHelper {
10 | pub res_dir: PathBuf,
11 | }
12 |
13 | impl ResHelper {
14 | pub fn res_init(res_dir: &PathBuf) -> Result<()> {
15 | let res = [ResourceName::ScrcpyServer, ResourceName::DefaultKeyConfig];
16 |
17 | for name in res {
18 | let file_path = ResHelper::get_file_path(res_dir, name);
19 | if !file_path.exists() {
20 | return Err(anyhow!(format!(
21 | "Resource missing! {}",
22 | file_path.to_str().unwrap()
23 | )));
24 | }
25 | }
26 |
27 | Ok(())
28 | }
29 | pub fn get_file_path(dir: &PathBuf, file_name: ResourceName) -> PathBuf {
30 | match file_name {
31 | ResourceName::ScrcpyServer => dir.join("scrcpy-mask-server-v2.4"),
32 | ResourceName::DefaultKeyConfig => dir.join("default-key-config.json"),
33 | }
34 | }
35 |
36 | pub fn get_scrcpy_version() -> String {
37 | String::from("2.4")
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src-tauri/src/scrcpy_mask_cmd.rs:
--------------------------------------------------------------------------------
1 | use tokio::{io::AsyncWriteExt, net::tcp::OwnedWriteHalf};
2 |
3 | use crate::{
4 | binary,
5 | control_msg::{gen_inject_key_ctrl_msg, gen_inject_touch_ctrl_msg, ControlMsgType},
6 | };
7 |
8 | pub async fn handle_sm_cmd(
9 | cmd_type: ScrcpyMaskCmdType,
10 | payload: &serde_json::Value,
11 | writer: &mut OwnedWriteHalf,
12 | ) {
13 | match cmd_type {
14 | ScrcpyMaskCmdType::SendKey => {
15 | let ctrl_msg_type = ControlMsgType::ControlMsgTypeInjectKeycode as u8;
16 | let keycode = payload["keycode"].as_u64().unwrap() as u32;
17 | let metastate = match payload.get("metastate") {
18 | Some(metastate) => metastate.as_u64().unwrap() as u32,
19 | None => 0, // AMETA_NONE
20 | };
21 | match payload["action"].as_u64().unwrap() {
22 | // default
23 | 0 => {
24 | // down
25 | let buf = gen_inject_key_ctrl_msg(
26 | ctrl_msg_type,
27 | 0, // AKEY_EVENT_ACTION_DOWN
28 | keycode,
29 | 0,
30 | metastate,
31 | );
32 | writer.write_all(&buf).await.unwrap();
33 | writer.flush().await.unwrap();
34 | tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
35 | // up
36 | let buf = gen_inject_key_ctrl_msg(
37 | ctrl_msg_type,
38 | 1, // AKEY_EVENT_ACTION_UP
39 | keycode,
40 | 0,
41 | metastate,
42 | );
43 | writer.write_all(&buf).await.unwrap();
44 | writer.flush().await.unwrap();
45 | }
46 | // down
47 | 1 => {
48 | let buf = gen_inject_key_ctrl_msg(
49 | ctrl_msg_type,
50 | 1, // AKEY_EVENT_ACTION_UP
51 | keycode,
52 | 0,
53 | metastate,
54 | );
55 | writer.write_all(&buf).await.unwrap();
56 | writer.flush().await.unwrap();
57 | }
58 | // up
59 | 2 => {
60 | let buf = gen_inject_key_ctrl_msg(
61 | ctrl_msg_type,
62 | 1, // AKEY_EVENT_ACTION_UP
63 | keycode,
64 | 0,
65 | metastate,
66 | );
67 | writer.write_all(&buf).await.unwrap();
68 | writer.flush().await.unwrap();
69 | }
70 | _ => {}
71 | };
72 | }
73 | ScrcpyMaskCmdType::Touch => {
74 | let ctrl_msg_type = ControlMsgType::ControlMsgTypeInjectTouchEvent as u8;
75 | let pointer_id = payload["pointerId"].as_u64().unwrap();
76 | let w = payload["screen"]["w"].as_u64().unwrap() as u16;
77 | let h = payload["screen"]["h"].as_u64().unwrap() as u16;
78 | let x = payload["pos"]["x"].as_i64().unwrap() as i32;
79 | let y = payload["pos"]["y"].as_i64().unwrap() as i32;
80 | let time = payload["time"].as_u64().unwrap();
81 | match payload["action"].as_u64().unwrap() {
82 | // default
83 | 0 => {
84 | // down
85 | touch(ctrl_msg_type, pointer_id, x, y, w, h, 0, writer).await;
86 | tokio::time::sleep(tokio::time::Duration::from_millis(time)).await;
87 | // up
88 | touch(ctrl_msg_type, pointer_id, x, y, w, h, 1, writer).await;
89 | }
90 | // down
91 | 1 => {
92 | touch(ctrl_msg_type, pointer_id, x, y, w, h, 0, writer).await;
93 | }
94 | // up
95 | 2 => {
96 | touch(ctrl_msg_type, pointer_id, x, y, w, h, 1, writer).await;
97 | }
98 | // move
99 | 3 => {
100 | touch(ctrl_msg_type, pointer_id, x, y, w, h, 2, writer).await;
101 | }
102 | _ => {}
103 | }
104 | }
105 | ScrcpyMaskCmdType::Swipe => {
106 | let ctrl_msg_type = ControlMsgType::ControlMsgTypeInjectTouchEvent as u8;
107 | let pointer_id = payload["pointerId"].as_u64().unwrap();
108 | let w = payload["screen"]["w"].as_u64().unwrap() as u16;
109 | let h = payload["screen"]["h"].as_u64().unwrap() as u16;
110 | let pos_arr = payload["pos"].as_array().unwrap();
111 | let pos_arr: Vec<(i32, i32)> = pos_arr
112 | .iter()
113 | .map(|pos| {
114 | (
115 | pos["x"].as_i64().unwrap() as i32,
116 | pos["y"].as_i64().unwrap() as i32,
117 | )
118 | })
119 | .collect();
120 | let interval_between_pos = payload["intervalBetweenPos"].as_u64().unwrap();
121 | match payload["action"].as_u64().unwrap() {
122 | // default
123 | 0 => {
124 | swipe(
125 | ctrl_msg_type,
126 | pointer_id,
127 | w,
128 | h,
129 | pos_arr,
130 | interval_between_pos,
131 | writer,
132 | true,
133 | true,
134 | )
135 | .await;
136 | }
137 | // no up
138 | 1 => {
139 | swipe(
140 | ctrl_msg_type,
141 | pointer_id,
142 | w,
143 | h,
144 | pos_arr,
145 | interval_between_pos,
146 | writer,
147 | true,
148 | false,
149 | )
150 | .await;
151 | }
152 | // no down
153 | 2 => {
154 | swipe(
155 | ctrl_msg_type,
156 | pointer_id,
157 | w,
158 | h,
159 | pos_arr,
160 | interval_between_pos,
161 | writer,
162 | false,
163 | true,
164 | )
165 | .await;
166 | }
167 | _ => {}
168 | };
169 | }
170 | ScrcpyMaskCmdType::Shutdown => {}
171 | }
172 | }
173 |
174 | pub async fn touch(
175 | ctrl_msg_type: u8,
176 | pointer_id: u64,
177 | x: i32,
178 | y: i32,
179 | w: u16,
180 | h: u16,
181 | action: u8, // 0: down, 1: up, 2: move
182 | writer: &mut OwnedWriteHalf,
183 | ) {
184 | let pressure = binary::float_to_u16fp(0.8);
185 | let action_button: u32 = 1;
186 | let buttons: u32 = 1;
187 | let buf = gen_inject_touch_ctrl_msg(
188 | ctrl_msg_type,
189 | action,
190 | pointer_id,
191 | x,
192 | y,
193 | w,
194 | h,
195 | pressure,
196 | action_button,
197 | buttons,
198 | );
199 | writer.write_all(&buf).await.unwrap();
200 | writer.flush().await.unwrap();
201 | }
202 |
203 | /// Determine the number of segments based on the distance between two points
204 | fn get_divide_num(x1: i32, y1: i32, x2: i32, y2: i32, segment_length: i32) -> i32 {
205 | let dx = (x2 - x1).abs();
206 | let dy = (y2 - y1).abs();
207 | let d = (dx.pow(2) + dy.pow(2)) as f64;
208 | let d = d.sqrt();
209 | let divide_num = (d / segment_length as f64).ceil() as i32;
210 | divide_num
211 | }
212 |
213 | pub async fn swipe(
214 | ctrl_msg_type: u8,
215 | pointer_id: u64,
216 | w: u16,
217 | h: u16,
218 | pos_arr: Vec<(i32, i32)>,
219 | interval_between_pos: u64,
220 | writer: &mut OwnedWriteHalf,
221 | down_flag: bool,
222 | up_flag: bool,
223 | ) {
224 | // down
225 | if down_flag {
226 | touch(
227 | ctrl_msg_type,
228 | pointer_id,
229 | pos_arr[0].0,
230 | pos_arr[0].1,
231 | w,
232 | h,
233 | 0,
234 | writer,
235 | )
236 | .await;
237 | }
238 |
239 | // move
240 | let mut cur_index = 1;
241 | while cur_index < pos_arr.len() {
242 | let (x, y) = pos_arr[cur_index];
243 | let (prev_x, prev_y) = pos_arr[cur_index - 1];
244 | // divide it into several segments
245 | let segment_length = 100;
246 | let divide_num = get_divide_num(prev_x, prev_y, x, y, segment_length);
247 | let dx = (x - prev_x) / divide_num;
248 | let dy = (y - prev_y) / divide_num;
249 | let d_interval = interval_between_pos / (divide_num as u64);
250 |
251 | for i in 1..divide_num + 1 {
252 | let nx = prev_x + dx * i;
253 | let ny = prev_y + dy * i;
254 | touch(ctrl_msg_type, pointer_id, nx, ny, w, h, 2, writer).await;
255 | if d_interval > 0 {
256 | tokio::time::sleep(tokio::time::Duration::from_millis(d_interval)).await;
257 | }
258 | }
259 |
260 | cur_index += 1;
261 | }
262 |
263 | // up
264 | if up_flag {
265 | touch(
266 | ctrl_msg_type,
267 | pointer_id,
268 | pos_arr[pos_arr.len() - 1].0,
269 | pos_arr[pos_arr.len() - 1].1,
270 | w,
271 | h,
272 | 1,
273 | writer,
274 | )
275 | .await;
276 | }
277 | }
278 |
279 | #[derive(Debug)]
280 | pub enum ScrcpyMaskCmdType {
281 | SendKey,
282 | Touch,
283 | Swipe,
284 | Shutdown,
285 | }
286 |
287 | impl ScrcpyMaskCmdType {
288 | pub fn from_i64(value: i64) -> Option {
289 | match value {
290 | 15 => Some(Self::SendKey),
291 | 16 => Some(Self::Touch),
292 | 17 => Some(Self::Swipe),
293 | 18 => Some(Self::Shutdown),
294 | _ => None,
295 | }
296 | }
297 | }
298 |
--------------------------------------------------------------------------------
/src-tauri/src/share.rs:
--------------------------------------------------------------------------------
1 | use lazy_static::lazy_static;
2 | use std::sync::Mutex;
3 |
4 | #[derive(Debug, Clone, serde::Serialize)]
5 | pub struct ClientInfo {
6 | pub device_name: String,
7 | pub device_id: String,
8 | pub scid: String,
9 | pub width: i32,
10 | pub height: i32,
11 | }
12 |
13 | impl ClientInfo {
14 | pub fn new(device_name: String, device_id: String, scid: String) -> Self {
15 | Self {
16 | device_name,
17 | device_id,
18 | scid,
19 | width: 0,
20 | height: 0,
21 | }
22 | }
23 |
24 | pub fn set_size(&mut self, width: i32, height: i32) {
25 | self.width = width;
26 | self.height = height;
27 | }
28 | }
29 |
30 | lazy_static! {
31 | pub static ref CLIENT_INFO: Mutex> = Mutex::new(None);
32 | }
33 |
34 | lazy_static! {
35 | pub static ref ADB_PATH: Mutex = Mutex::new(String::from("adb"));
36 | }
37 |
--------------------------------------------------------------------------------
/src-tauri/src/socket.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Arc;
2 |
3 | use anyhow::Context;
4 | use serde_json::json;
5 | use tokio::{
6 | io::AsyncReadExt,
7 | net::{
8 | tcp::{OwnedReadHalf, OwnedWriteHalf},
9 | TcpStream,
10 | },
11 | };
12 |
13 | use crate::{
14 | control_msg::{self, ControlMsgType},
15 | scrcpy_mask_cmd::{self, ScrcpyMaskCmdType},
16 | share,
17 | };
18 |
19 | pub async fn connect_socket(
20 | address: String,
21 | front_msg_receiver: tokio::sync::mpsc::Receiver,
22 | device_reply_sender: tokio::sync::mpsc::Sender,
23 | listen_handler: u32,
24 | app: Arc,
25 | ) -> anyhow::Result<()> {
26 | let client = TcpStream::connect(address)
27 | .await
28 | .context("Socket connect failed")?;
29 |
30 | println!("connect to scrcpy-server:{:?}", client.local_addr());
31 |
32 | let (read_half, write_half) = client.into_split();
33 |
34 | // 开启线程读取设备发送的信息,并通过通道传递到与前端通信的线程,最后与前端通信的线程发送全局事件,告知前端设备发送的信息
35 | tokio::spawn(async move {
36 | read_socket(read_half, device_reply_sender).await;
37 | });
38 |
39 | // 开启线程接收通道消息,其中通道消息来自前端发送的事件
40 | tokio::spawn(async move {
41 | recv_front_msg(write_half, front_msg_receiver, listen_handler, app).await;
42 | });
43 | anyhow::Ok(())
44 | }
45 |
46 | // 从客户端读取
47 | async fn read_socket(
48 | mut reader: OwnedReadHalf,
49 | device_reply_sender: tokio::sync::mpsc::Sender,
50 | ) {
51 | // read dummy byte
52 | let mut buf: [u8; 1] = [0; 1];
53 | if let Err(_e) = reader.read_exact(&mut buf).await {
54 | eprintln!("failed to read dummy byte");
55 | return;
56 | }
57 |
58 | // read metadata (device name)
59 | let mut buf: [u8; 64] = [0; 64];
60 | match reader.read(&mut buf).await {
61 | Err(_e) => {
62 | eprintln!("failed to read metadata");
63 | return;
64 | }
65 | Ok(0) => {
66 | eprintln!("failed to read metadata");
67 | return;
68 | }
69 | Ok(n) => {
70 | let mut end = n;
71 | while buf[end - 1] == 0 {
72 | end -= 1;
73 | }
74 | let device_name = std::str::from_utf8(&buf[..end]).unwrap();
75 | // update device name for share
76 | share::CLIENT_INFO
77 | .lock()
78 | .unwrap()
79 | .as_mut()
80 | .unwrap()
81 | .device_name = device_name.to_string();
82 |
83 | let msg = json!({
84 | "type": "MetaData",
85 | "deviceName": device_name,
86 | })
87 | .to_string();
88 | device_reply_sender.send(msg).await.unwrap();
89 | }
90 | };
91 |
92 | loop {
93 | match reader.read_u8().await {
94 | Err(e) => {
95 | eprintln!(
96 | "Failed to read from scrcpy server, maybe it was closed. Error:{}",
97 | e
98 | );
99 | println!("Drop TcpStream reader");
100 | drop(reader);
101 | return;
102 | }
103 | Ok(message_type) => {
104 | let message_type = match DeviceMsgType::from_u8(message_type) {
105 | Some(t) => t,
106 | None => {
107 | println!("Ignore unkonw message type: {}", message_type);
108 | continue;
109 | }
110 | };
111 | if let Err(e) =
112 | handle_device_message(message_type, &mut reader, &device_reply_sender).await
113 | {
114 | eprintln!("Failed to handle device message: {}", e);
115 | }
116 | }
117 | }
118 | }
119 | }
120 |
121 | async fn handle_device_message(
122 | message_type: DeviceMsgType,
123 | reader: &mut OwnedReadHalf,
124 | device_reply_sender: &tokio::sync::mpsc::Sender,
125 | ) -> anyhow::Result<()> {
126 | match message_type {
127 | // 设备剪切板变动
128 | DeviceMsgType::DeviceMsgTypeClipboard => {
129 | let text_length = reader.read_u32().await?;
130 | let mut buf: Vec = vec![0; text_length as usize];
131 | reader.read_exact(&mut buf).await?;
132 | let cb = String::from_utf8(buf)?;
133 | let msg = json!({
134 | "type": "ClipboardChanged",
135 | "clipboard": cb
136 | })
137 | .to_string();
138 | device_reply_sender.send(msg).await?;
139 | }
140 | // 设备剪切板设置成功的回复
141 | DeviceMsgType::DeviceMsgTypeAckClipboard => {
142 | let sequence = reader.read_u64().await?;
143 | let msg = json!({
144 | "type": "ClipboardSetAck",
145 | "sequence": sequence
146 | })
147 | .to_string();
148 | device_reply_sender.send(msg).await?;
149 | }
150 | // 虚拟设备输出,仅读取但不做进一步处理
151 | DeviceMsgType::DeviceMsgTypeUhidOutput => {
152 | let _id = reader.read_u16().await?;
153 | let size = reader.read_u16().await?;
154 | let mut buf: Vec = vec![0; size as usize];
155 | reader.read_exact(&mut buf).await?;
156 | }
157 | // 设备旋转
158 | DeviceMsgType::DeviceMsgTypeRotation => {
159 | let rotation = reader.read_u16().await?;
160 | let width = reader.read_i32().await?;
161 | let height = reader.read_i32().await?;
162 | let msg = json!({
163 | "type": "DeviceRotation",
164 | "rotation": rotation,
165 | "width": width,
166 | "height": height
167 | })
168 | .to_string();
169 | share::CLIENT_INFO
170 | .lock()
171 | .unwrap()
172 | .as_mut()
173 | .unwrap()
174 | .set_size(width, height);
175 | device_reply_sender.send(msg).await?;
176 | }
177 | };
178 | anyhow::Ok(())
179 | }
180 |
181 | // 接收前端发送的消息,执行相关操作
182 | async fn recv_front_msg(
183 | mut write_half: OwnedWriteHalf,
184 | mut front_msg_receiver: tokio::sync::mpsc::Receiver,
185 | listen_handler: u32,
186 | app: Arc,
187 | ) {
188 | while let Some(msg) = front_msg_receiver.recv().await {
189 | match serde_json::from_str::(&msg) {
190 | Err(_e) => {
191 | println!("无法解析的Json数据: {}", msg);
192 | }
193 | Ok(payload) => {
194 | if let Some(front_msg_type) = payload["msgType"].as_i64() {
195 | // 发送原始控制信息
196 | if front_msg_type >= 0 && front_msg_type <= 14 {
197 | let ctrl_msg_type = ControlMsgType::from_i64(front_msg_type).unwrap();
198 | control_msg::send_ctrl_msg(
199 | ctrl_msg_type,
200 | &payload["msgData"],
201 | &mut write_half,
202 | )
203 | .await;
204 | continue;
205 | } else {
206 | // 处理Scrcpy Mask命令
207 | if let Some(cmd_type) = ScrcpyMaskCmdType::from_i64(front_msg_type) {
208 | if let ScrcpyMaskCmdType::Shutdown = cmd_type {
209 | *share::CLIENT_INFO.lock().unwrap() = None;
210 |
211 | drop(write_half);
212 | println!("Drop TcpStream writer");
213 | app.unlisten(listen_handler);
214 | println!("front msg channel closed");
215 | return;
216 | }
217 |
218 | scrcpy_mask_cmd::handle_sm_cmd(
219 | cmd_type,
220 | &payload["msgData"],
221 | &mut write_half,
222 | )
223 | .await
224 | }
225 | }
226 | } else {
227 | eprintln!("fc-command invalid!");
228 | eprintln!("{:?}", payload);
229 | }
230 | }
231 | };
232 | }
233 |
234 | println!("font msg channel closed");
235 | }
236 |
237 | #[derive(Debug)]
238 | enum DeviceMsgType {
239 | DeviceMsgTypeClipboard,
240 | DeviceMsgTypeAckClipboard,
241 | DeviceMsgTypeUhidOutput,
242 | DeviceMsgTypeRotation,
243 | }
244 |
245 | impl DeviceMsgType {
246 | fn from_u8(value: u8) -> Option {
247 | match value {
248 | 0 => Some(Self::DeviceMsgTypeClipboard),
249 | 1 => Some(Self::DeviceMsgTypeAckClipboard),
250 | 2 => Some(Self::DeviceMsgTypeUhidOutput),
251 | 3 => Some(Self::DeviceMsgTypeRotation),
252 | _ => None,
253 | }
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "productName": "scrcpy-mask",
3 | "version": "0.6.0",
4 | "identifier": "com.akichase.mask",
5 | "build": {
6 | "beforeDevCommand": "pnpm dev",
7 | "devUrl": "http://localhost:1420",
8 | "beforeBuildCommand": "pnpm build",
9 | "frontendDist": "../dist"
10 | },
11 | "app": {
12 | "windows": [
13 | {
14 | "title": "scrcpy-mask",
15 | "transparent": true,
16 | "decorations": false
17 | }
18 | ],
19 | "macOSPrivateApi": true,
20 | "security": {
21 | "csp": null
22 | }
23 | },
24 | "bundle": {
25 | "active": true,
26 | "targets": "all",
27 | "icon": [
28 | "icons/32x32.png",
29 | "icons/128x128.png",
30 | "icons/128x128@2x.png",
31 | "icons/icon.icns",
32 | "icons/icon.ico"
33 | ],
34 | "resources": [
35 | "resource/default-key-config.json",
36 | "resource/scrcpy-mask-server-v2.4"
37 | ]
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
67 |
--------------------------------------------------------------------------------
/src/components/Header.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
41 |
42 |
43 |
75 |
--------------------------------------------------------------------------------
/src/components/Mask.vue:
--------------------------------------------------------------------------------
1 |
181 |
182 |
183 |
195 |
196 |
197 |
201 |
244 |
245 |
246 |
247 |
319 |
--------------------------------------------------------------------------------
/src/components/ScreenStream.vue:
--------------------------------------------------------------------------------
1 |
46 |
47 |
48 |
49 |
56 |
57 |
58 |
59 |
78 |
--------------------------------------------------------------------------------
/src/components/Sidebar.vue:
--------------------------------------------------------------------------------
1 |
56 |
57 |
58 |
120 |
121 |
122 |
210 |
--------------------------------------------------------------------------------
/src/components/keyboard/KeyFire.vue:
--------------------------------------------------------------------------------
1 |
93 |
94 |
95 |
105 |
106 |
111 |
114 |
115 |
116 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
149 | {{ $t("pages.KeyBoard.KeyFire.fire") }}
150 | {{ $t("pages.KeyBoard.KeyFire.drag") }}
156 |
157 |
163 |
164 |
165 |
171 |
172 |
173 |
179 |
180 |
181 |
186 |
187 |
188 |
189 |
190 |
256 |
--------------------------------------------------------------------------------
/src/components/keyboard/KeyInfo.vue:
--------------------------------------------------------------------------------
1 |
108 |
109 |
110 |
111 |
120 |
121 |
122 | {{ mouseX }}, {{ mouseY }}
123 |
124 |
{{ $t('pages.KeyBoard.KeyInfo.note') }}
125 |
126 | {{ code }}
127 |
128 |
129 |
130 |
131 |
132 |
181 |
--------------------------------------------------------------------------------
/src/components/keyboard/KeyObservation.vue:
--------------------------------------------------------------------------------
1 |
80 |
81 |
82 |
92 |
93 | {{ keyMapping.key }}
94 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
127 | {{ $t("pages.KeyBoard.Observation.observation") }}
128 |
129 |
135 |
136 |
137 |
143 |
144 |
145 |
150 |
151 |
152 |
153 |
154 |
220 |
--------------------------------------------------------------------------------
/src/components/keyboard/KeySight.vue:
--------------------------------------------------------------------------------
1 |
80 |
81 |
82 |
92 |
93 |
98 |
101 |
102 |
103 |
{{ keyMapping.key }}
104 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
137 | {{ $t('pages.KeyBoard.KeySight.sight') }}
138 |
139 |
145 |
146 |
147 |
153 |
154 |
155 |
161 |
162 |
163 |
168 |
169 |
170 |
171 |
172 |
238 |
--------------------------------------------------------------------------------
/src/components/keyboard/KeySteeringWheel.vue:
--------------------------------------------------------------------------------
1 |
93 |
94 |
95 |
107 |
108 | {{ keyMapping.key.up }}
116 |
117 | {{ keyMapping.key.left }}
125 |
126 |
127 |
128 | {{ keyMapping.key.right }}
136 |
137 | {{ keyMapping.key.down }}
145 |
146 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
187 | {{
188 | $t("pages.KeyBoard.SteeringWheel.steeringWheel")
189 | }}
190 |
191 |
196 |
197 |
198 |
204 |
205 |
206 |
211 |
212 |
213 |
214 |
215 |
282 |
--------------------------------------------------------------------------------
/src/components/keyboard/KeySwipe.vue:
--------------------------------------------------------------------------------
1 |
149 |
150 |
151 |
161 |
162 |
{{ keyMapping.key }}
163 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
196 | {{ $t("pages.KeyBoard.Swipe.swipe") }}
197 |
198 | {{
199 | $t("pages.KeyBoard.Swipe.editPos")
200 | }}
201 |
202 |
203 |
208 |
209 |
210 |
216 |
217 |
218 |
223 |
224 |
225 |
226 |
227 |
232 |
233 |
234 | swipePointDragHandlue(e, i)"
240 | />
241 |
242 | {{ i }}
243 |
244 |
245 |
246 |
247 |
248 |
249 |
356 |
--------------------------------------------------------------------------------
/src/components/setting/About.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 |
31 |
{{ $t("pages.Setting.About.about") }}
32 |
{{ $t("pages.Setting.About.introduction") }}
33 |
34 |
38 |
39 |
40 |
41 | {{ $t("pages.Setting.About.github") }}
42 |
43 |
47 |
48 |
56 |
60 |
61 |
62 |
63 | BiliBili
64 |
65 |
66 |
67 |
68 |
69 | {{ $t("pages.Setting.About.blog") }}
70 |
71 |
72 |
{{ $t("pages.Setting.About.update") }}
73 |
{{ $t("pages.Setting.About.checkUpdateOnStartup") }}
80 |
{{ $t("pages.Setting.About.curVersion", [appVersion]) }}
81 |
{{
82 | $t("pages.Setting.About.checkUpdate")
83 | }}
84 |
85 |
86 |
87 |
96 |
--------------------------------------------------------------------------------
/src/components/setting/Basic.vue:
--------------------------------------------------------------------------------
1 |
59 |
60 |
61 |
62 | {{ $t("pages.Setting.Basic.language") }}
63 |
69 | {{ $t("pages.Setting.Basic.adbPath.title") }}
70 |
71 |
76 | {{
77 | $t("pages.Setting.Basic.adbPath.set")
78 | }}
79 |
80 | 剪切板同步
81 |
82 |
86 |
87 | 从设备同步
88 | 设备剪切板发生变化时自动同步更新电脑剪切板
89 |
90 |
91 |
95 |
96 | 粘贴时同步
97 | 在按键输入模式下,按下 Ctrl + V 可将电脑剪切板内容同步粘贴到设备
98 |
99 |
100 |
101 |
102 |
103 |
104 |
109 |
--------------------------------------------------------------------------------
/src/components/setting/Data.vue:
--------------------------------------------------------------------------------
1 |
75 |
76 |
77 |
78 |
79 | {{ $t("pages.Setting.Data.localStore") }}
80 |
81 |
88 |
89 |
90 |
91 |
92 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
{{ $t("pages.Setting.Data.delLocalStore.warning") }}
106 |
107 |
111 |
112 | {{ entrie[0] }}
113 |
114 |
115 |
116 |
117 |
118 |
122 |
123 |
130 | {{ $t("pages.Setting.Data.delCurData") }}
136 |
137 |
138 |
139 |
140 |
141 |
150 |
--------------------------------------------------------------------------------
/src/components/setting/Mask.vue:
--------------------------------------------------------------------------------
1 |
165 |
166 |
167 |
168 | {{ $t("pages.Setting.Mask.buttonPrompts") }}
169 |
173 |
177 |
178 |
179 |
187 |
188 |
189 |
197 |
198 | {{ $t("pages.Setting.Mask.areaAdjust") }}
199 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
217 |
218 |
219 |
223 |
224 |
225 |
229 |
230 |
231 |
235 |
236 |
237 |
238 |
239 | {{ $t("pages.Setting.Mask.rotation.title") }}
240 |
244 |
248 |
249 |
250 |
254 |
259 |
260 |
264 |
269 |
270 |
271 |
272 | ScreenStream
273 |
277 |
281 |
282 |
286 |
292 |
293 |
294 |
295 |
296 |
297 |
--------------------------------------------------------------------------------
/src/components/setting/Setting.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
62 |
--------------------------------------------------------------------------------
/src/frontcommand/KeyToCodeMap.ts:
--------------------------------------------------------------------------------
1 | import { UIEventsCode } from "./UIEventsCode";
2 | import { AndroidKeycode } from "./android";
3 |
4 | export const KeyToCodeMap = new Map([
5 | [UIEventsCode.Backquote, AndroidKeycode.AKEYCODE_GRAVE],
6 | [UIEventsCode.Backslash, AndroidKeycode.AKEYCODE_BACKSLASH],
7 | [UIEventsCode.BracketLeft, AndroidKeycode.AKEYCODE_LEFT_BRACKET],
8 | [UIEventsCode.BracketRight, AndroidKeycode.AKEYCODE_RIGHT_BRACKET],
9 | [UIEventsCode.Comma, AndroidKeycode.AKEYCODE_COMMA],
10 | [UIEventsCode.Digit0, AndroidKeycode.AKEYCODE_0],
11 | [UIEventsCode.Digit1, AndroidKeycode.AKEYCODE_1],
12 | [UIEventsCode.Digit2, AndroidKeycode.AKEYCODE_2],
13 | [UIEventsCode.Digit3, AndroidKeycode.AKEYCODE_3],
14 | [UIEventsCode.Digit4, AndroidKeycode.AKEYCODE_4],
15 | [UIEventsCode.Digit5, AndroidKeycode.AKEYCODE_5],
16 | [UIEventsCode.Digit6, AndroidKeycode.AKEYCODE_6],
17 | [UIEventsCode.Digit7, AndroidKeycode.AKEYCODE_7],
18 | [UIEventsCode.Digit8, AndroidKeycode.AKEYCODE_8],
19 | [UIEventsCode.Digit9, AndroidKeycode.AKEYCODE_9],
20 | [UIEventsCode.Equal, AndroidKeycode.AKEYCODE_EQUALS],
21 | [UIEventsCode.IntlRo, AndroidKeycode.AKEYCODE_RO],
22 | [UIEventsCode.IntlYen, AndroidKeycode.AKEYCODE_YEN],
23 | [UIEventsCode.KeyA, AndroidKeycode.AKEYCODE_A],
24 | [UIEventsCode.KeyB, AndroidKeycode.AKEYCODE_B],
25 | [UIEventsCode.KeyC, AndroidKeycode.AKEYCODE_C],
26 | [UIEventsCode.KeyD, AndroidKeycode.AKEYCODE_D],
27 | [UIEventsCode.KeyE, AndroidKeycode.AKEYCODE_E],
28 | [UIEventsCode.KeyF, AndroidKeycode.AKEYCODE_F],
29 | [UIEventsCode.KeyG, AndroidKeycode.AKEYCODE_G],
30 | [UIEventsCode.KeyH, AndroidKeycode.AKEYCODE_H],
31 | [UIEventsCode.KeyI, AndroidKeycode.AKEYCODE_I],
32 | [UIEventsCode.KeyJ, AndroidKeycode.AKEYCODE_J],
33 | [UIEventsCode.KeyK, AndroidKeycode.AKEYCODE_K],
34 | [UIEventsCode.KeyL, AndroidKeycode.AKEYCODE_L],
35 | [UIEventsCode.KeyM, AndroidKeycode.AKEYCODE_M],
36 | [UIEventsCode.KeyN, AndroidKeycode.AKEYCODE_N],
37 | [UIEventsCode.KeyO, AndroidKeycode.AKEYCODE_O],
38 | [UIEventsCode.KeyP, AndroidKeycode.AKEYCODE_P],
39 | [UIEventsCode.KeyQ, AndroidKeycode.AKEYCODE_Q],
40 | [UIEventsCode.KeyR, AndroidKeycode.AKEYCODE_R],
41 | [UIEventsCode.KeyS, AndroidKeycode.AKEYCODE_S],
42 | [UIEventsCode.KeyT, AndroidKeycode.AKEYCODE_T],
43 | [UIEventsCode.KeyU, AndroidKeycode.AKEYCODE_U],
44 | [UIEventsCode.KeyV, AndroidKeycode.AKEYCODE_V],
45 | [UIEventsCode.KeyW, AndroidKeycode.AKEYCODE_W],
46 | [UIEventsCode.KeyX, AndroidKeycode.AKEYCODE_X],
47 | [UIEventsCode.KeyY, AndroidKeycode.AKEYCODE_Y],
48 | [UIEventsCode.KeyZ, AndroidKeycode.AKEYCODE_Z],
49 | [UIEventsCode.Minus, AndroidKeycode.AKEYCODE_MINUS],
50 | [UIEventsCode.Period, AndroidKeycode.AKEYCODE_PERIOD],
51 | [UIEventsCode.Quote, AndroidKeycode.AKEYCODE_APOSTROPHE],
52 | [UIEventsCode.Semicolon, AndroidKeycode.AKEYCODE_SEMICOLON],
53 | [UIEventsCode.Slash, AndroidKeycode.AKEYCODE_SLASH],
54 | [UIEventsCode.KanaMode, AndroidKeycode.AKEYCODE_KANA],
55 | [UIEventsCode.Delete, AndroidKeycode.AKEYCODE_FORWARD_DEL],
56 | [UIEventsCode.End, AndroidKeycode.AKEYCODE_MOVE_END],
57 | [UIEventsCode.Help, AndroidKeycode.AKEYCODE_HELP],
58 | [UIEventsCode.Home, AndroidKeycode.AKEYCODE_MOVE_HOME],
59 | [UIEventsCode.Insert, AndroidKeycode.AKEYCODE_INSERT],
60 | [UIEventsCode.PageDown, AndroidKeycode.AKEYCODE_PAGE_DOWN],
61 | [UIEventsCode.PageUp, AndroidKeycode.AKEYCODE_PAGE_UP],
62 | [UIEventsCode.AltLeft, AndroidKeycode.AKEYCODE_ALT_LEFT],
63 | [UIEventsCode.AltRight, AndroidKeycode.AKEYCODE_ALT_RIGHT],
64 | [UIEventsCode.Backspace, AndroidKeycode.AKEYCODE_DEL],
65 | [UIEventsCode.CapsLock, AndroidKeycode.AKEYCODE_CAPS_LOCK],
66 | [UIEventsCode.ControlLeft, AndroidKeycode.AKEYCODE_CTRL_LEFT],
67 | [UIEventsCode.ControlRight, AndroidKeycode.AKEYCODE_CTRL_RIGHT],
68 | [UIEventsCode.Enter, AndroidKeycode.AKEYCODE_ENTER],
69 | [UIEventsCode.MetaLeft, AndroidKeycode.AKEYCODE_META_LEFT],
70 | [UIEventsCode.MetaRight, AndroidKeycode.AKEYCODE_META_RIGHT],
71 | [UIEventsCode.ShiftLeft, AndroidKeycode.AKEYCODE_SHIFT_LEFT],
72 | [UIEventsCode.ShiftRight, AndroidKeycode.AKEYCODE_SHIFT_RIGHT],
73 | [UIEventsCode.Space, AndroidKeycode.AKEYCODE_SPACE],
74 | [UIEventsCode.Tab, AndroidKeycode.AKEYCODE_TAB],
75 | [UIEventsCode.ArrowLeft, AndroidKeycode.AKEYCODE_DPAD_LEFT],
76 | [UIEventsCode.ArrowUp, AndroidKeycode.AKEYCODE_DPAD_UP],
77 | [UIEventsCode.ArrowRight, AndroidKeycode.AKEYCODE_DPAD_RIGHT],
78 | [UIEventsCode.ArrowDown, AndroidKeycode.AKEYCODE_DPAD_DOWN],
79 |
80 | [UIEventsCode.NumLock, AndroidKeycode.AKEYCODE_NUM_LOCK],
81 | [UIEventsCode.Numpad0, AndroidKeycode.AKEYCODE_NUMPAD_0],
82 | [UIEventsCode.Numpad1, AndroidKeycode.AKEYCODE_NUMPAD_1],
83 | [UIEventsCode.Numpad2, AndroidKeycode.AKEYCODE_NUMPAD_2],
84 | [UIEventsCode.Numpad3, AndroidKeycode.AKEYCODE_NUMPAD_3],
85 | [UIEventsCode.Numpad4, AndroidKeycode.AKEYCODE_NUMPAD_4],
86 | [UIEventsCode.Numpad5, AndroidKeycode.AKEYCODE_NUMPAD_5],
87 | [UIEventsCode.Numpad6, AndroidKeycode.AKEYCODE_NUMPAD_6],
88 | [UIEventsCode.Numpad7, AndroidKeycode.AKEYCODE_NUMPAD_7],
89 | [UIEventsCode.Numpad8, AndroidKeycode.AKEYCODE_NUMPAD_8],
90 | [UIEventsCode.Numpad9, AndroidKeycode.AKEYCODE_NUMPAD_9],
91 | [UIEventsCode.NumpadAdd, AndroidKeycode.AKEYCODE_NUMPAD_ADD],
92 | [UIEventsCode.NumpadComma, AndroidKeycode.AKEYCODE_NUMPAD_COMMA],
93 | [UIEventsCode.NumpadDecimal, AndroidKeycode.AKEYCODE_NUMPAD_DOT],
94 | [UIEventsCode.NumpadDivide, AndroidKeycode.AKEYCODE_NUMPAD_DIVIDE],
95 | [UIEventsCode.NumpadEnter, AndroidKeycode.AKEYCODE_NUMPAD_ENTER],
96 | [UIEventsCode.NumpadEqual, AndroidKeycode.AKEYCODE_NUMPAD_EQUALS],
97 | [UIEventsCode.NumpadMultiply, AndroidKeycode.AKEYCODE_NUMPAD_MULTIPLY],
98 | [UIEventsCode.NumpadParenLeft, AndroidKeycode.AKEYCODE_NUMPAD_LEFT_PAREN],
99 | [UIEventsCode.NumpadParenRight, AndroidKeycode.AKEYCODE_NUMPAD_RIGHT_PAREN],
100 | [UIEventsCode.NumpadSubtract, AndroidKeycode.AKEYCODE_NUMPAD_SUBTRACT],
101 |
102 | [UIEventsCode.Escape, AndroidKeycode.AKEYCODE_ESCAPE],
103 | [UIEventsCode.F1, AndroidKeycode.AKEYCODE_F1],
104 | [UIEventsCode.F2, AndroidKeycode.AKEYCODE_F2],
105 | [UIEventsCode.F3, AndroidKeycode.AKEYCODE_F3],
106 | [UIEventsCode.F4, AndroidKeycode.AKEYCODE_F4],
107 | [UIEventsCode.F5, AndroidKeycode.AKEYCODE_F5],
108 | [UIEventsCode.F6, AndroidKeycode.AKEYCODE_F6],
109 | [UIEventsCode.F7, AndroidKeycode.AKEYCODE_F7],
110 | [UIEventsCode.F8, AndroidKeycode.AKEYCODE_F8],
111 | [UIEventsCode.F9, AndroidKeycode.AKEYCODE_F9],
112 | [UIEventsCode.F10, AndroidKeycode.AKEYCODE_F10],
113 | [UIEventsCode.F11, AndroidKeycode.AKEYCODE_F11],
114 | [UIEventsCode.F12, AndroidKeycode.AKEYCODE_F12],
115 | [UIEventsCode.Fn, AndroidKeycode.AKEYCODE_FUNCTION],
116 | [UIEventsCode.PrintScreen, AndroidKeycode.AKEYCODE_SYSRQ],
117 | [UIEventsCode.Pause, AndroidKeycode.AKEYCODE_BREAK],
118 | ]);
119 |
--------------------------------------------------------------------------------
/src/frontcommand/UIEventsCode.ts:
--------------------------------------------------------------------------------
1 | // https://w3c.github.io/uievents-code/
2 |
3 | export enum UIEventsCode {
4 | // 3.1.1.1. Writing System Keys
5 | Backquote = "Backquote",
6 | Backslash = "Backslash",
7 | BracketLeft = "BracketLeft",
8 | BracketRight = "BracketRight",
9 | Comma = "Comma",
10 | Digit0 = "Digit0",
11 | Digit1 = "Digit1",
12 | Digit2 = "Digit2",
13 | Digit3 = "Digit3",
14 | Digit4 = "Digit4",
15 | Digit5 = "Digit5",
16 | Digit6 = "Digit6",
17 | Digit7 = "Digit7",
18 | Digit8 = "Digit8",
19 | Digit9 = "Digit9",
20 | Equal = "Equal",
21 | IntlBackslash = "IntlBackslash",
22 | IntlRo = "IntlRo",
23 | IntlYen = "IntlYen",
24 | KeyA = "KeyA",
25 | KeyB = "KeyB",
26 | KeyC = "KeyC",
27 | KeyD = "KeyD",
28 | KeyE = "KeyE",
29 | KeyF = "KeyF",
30 | KeyG = "KeyG",
31 | KeyH = "KeyH",
32 | KeyI = "KeyI",
33 | KeyJ = "KeyJ",
34 | KeyK = "KeyK",
35 | KeyL = "KeyL",
36 | KeyM = "KeyM",
37 | KeyN = "KeyN",
38 | KeyO = "KeyO",
39 | KeyP = "KeyP",
40 | KeyQ = "KeyQ",
41 | KeyR = "KeyR",
42 | KeyS = "KeyS",
43 | KeyT = "KeyT",
44 | KeyU = "KeyU",
45 | KeyV = "KeyV",
46 | KeyW = "KeyW",
47 | KeyX = "KeyX",
48 | KeyY = "KeyY",
49 | KeyZ = "KeyZ",
50 | Minus = "Minus",
51 | Period = "Period",
52 | Quote = "Quote",
53 | Semicolon = "Semicolon",
54 | Slash = "Slash",
55 |
56 | // 3.1.1.2. Functional Keys
57 | AltLeft = "AltLeft",
58 | AltRight = "AltRight",
59 | Backspace = "Backspace",
60 | CapsLock = "CapsLock",
61 | ContextMenu = "ContextMenu",
62 | ControlLeft = "ControlLeft",
63 | ControlRight = "ControlRight",
64 | Enter = "Enter",
65 | MetaLeft = "MetaLeft",
66 | MetaRight = "MetaRight",
67 | ShiftLeft = "ShiftLeft",
68 | ShiftRight = "ShiftRight",
69 | Space = "Space",
70 | Tab = "Tab",
71 | Convert = "Convert",
72 | KanaMode = "KanaMode",
73 | Lang1 = "Lang1",
74 | Lang2 = "Lang2",
75 | Lang3 = "Lang3",
76 | Lang4 = "Lang4",
77 | Lang5 = "Lang5",
78 | NonConvert = "NonConvert",
79 |
80 | // 3.1.2. Control Pad Section
81 | Delete = "Delete",
82 | End = "End",
83 | Help = "Help",
84 | Home = "Home",
85 | Insert = "Insert",
86 | PageDown = "PageDown",
87 | PageUp = "PageUp",
88 |
89 | // 3.1.3. Arrow Pad Section
90 | ArrowDown = "ArrowDown",
91 | ArrowLeft = "ArrowLeft",
92 | ArrowRight = "ArrowRight",
93 | ArrowUp = "ArrowUp",
94 |
95 | // 3.1.4. Numpad Section
96 | NumLock = "NumLock",
97 | Numpad0 = "Numpad0",
98 | Numpad1 = "Numpad1",
99 | Numpad2 = "Numpad2",
100 | Numpad3 = "Numpad3",
101 | Numpad4 = "Numpad4",
102 | Numpad5 = "Numpad5",
103 | Numpad6 = "Numpad6",
104 | Numpad7 = "Numpad7",
105 | Numpad8 = "Numpad8",
106 | Numpad9 = "Numpad9",
107 | NumpadAdd = "NumpadAdd",
108 | NumpadBackspace = "NumpadBackspace",
109 | NumpadClear = "NumpadClear",
110 | NumpadClearEntry = "NumpadClearEntry",
111 | NumpadComma = "NumpadComma",
112 | NumpadDecimal = "NumpadDecimal",
113 | NumpadDivide = "NumpadDivide",
114 | NumpadEnter = "NumpadEnter",
115 | NumpadEqual = "NumpadEqual",
116 | NumpadHash = "NumpadHash",
117 | NumpadMemoryAdd = "NumpadMemoryAdd",
118 | NumpadMemoryClear = "NumpadMemoryClear",
119 | NumpadMemoryRecall = "NumpadMemoryRecall",
120 | NumpadMemoryStore = "NumpadMemoryStore",
121 | NumpadMemorySubtract = "NumpadMemorySubtract",
122 | NumpadMultiply = "NumpadMultiply",
123 | NumpadParenLeft = "NumpadParenLeft",
124 | NumpadParenRight = "NumpadParenRight",
125 | NumpadStar = "NumpadStar",
126 | NumpadSubtract = "NumpadSubtract",
127 |
128 | // 3.1.5. Function Section
129 | Escape = "Escape",
130 | F1 = "F1",
131 | F2 = "F2",
132 | F3 = "F3",
133 | F4 = "F4",
134 | F5 = "F5",
135 | F6 = "F6",
136 | F7 = "F7",
137 | F8 = "F8",
138 | F9 = "F9",
139 | F10 = "F10",
140 | F11 = "F11",
141 | F12 = "F12",
142 | Fn = "Fn",
143 | FnLock = "FnLock",
144 | PrintScreen = "PrintScreen",
145 | ScrollLock = "ScrollLock",
146 | Pause = "Pause",
147 |
148 | // 3.1.6. Media Keys
149 | BrowserBack = "BrowserBack",
150 | BrowserFavorites = "BrowserFavorites",
151 | BrowserForward = "BrowserForward",
152 | BrowserHome = "BrowserHome",
153 | BrowserRefresh = "BrowserRefresh",
154 | BrowserSearch = "BrowserSearch",
155 | BrowserStop = "BrowserStop",
156 | Eject = "Eject",
157 | LaunchApp1 = "LaunchApp1",
158 | LaunchApp2 = "LaunchApp2",
159 | LaunchMail = "LaunchMail",
160 | MediaPlayPause = "MediaPlayPause",
161 | MediaSelect = "MediaSelect",
162 | MediaStop = "MediaStop",
163 | MediaTrackNext = "MediaTrackNext",
164 | MediaTrackPrevious = "MediaTrackPrevious",
165 | Power = "Power",
166 | Sleep = "Sleep",
167 | AudioVolumeDown = "AudioVolumeDown",
168 | AudioVolumeMute = "AudioVolumeMute",
169 | AudioVolumeUp = "AudioVolumeUp",
170 | WakeUp = "WakeUp",
171 |
172 | // 3.1.7. Legacy, Non-Standard and Special Keys
173 | Hyper = "Hyper",
174 | Super = "Super",
175 | Turbo = "Turbo",
176 | Abort = "Abort",
177 | Resume = "Resume",
178 | Suspend = "Suspend",
179 | Again = "Again",
180 | Copy = "Copy",
181 | Cut = "Cut",
182 | Find = "Find",
183 | Open = "Open",
184 | Paste = "Paste",
185 | Props = "Props",
186 | Select = "Select",
187 | Undo = "Undo",
188 | Hiragana = "Hiragana",
189 | Katakana = "Katakana",
190 | Unidentified = "Unidentified",
191 | }
192 |
--------------------------------------------------------------------------------
/src/frontcommand/controlMsg.ts:
--------------------------------------------------------------------------------
1 | import { emit } from "@tauri-apps/api/event";
2 | import {
3 | AndroidKeyEventAction,
4 | AndroidKeycode,
5 | AndroidMetastate,
6 | AndroidMotionEventAction,
7 | AndroidMotionEventButtons,
8 | } from "./android";
9 |
10 | interface ControlMsgPayload {
11 | msgType: ControlMsgType;
12 | msgData?: ControlMsgData;
13 | }
14 |
15 | async function sendControlMsg(payload: ControlMsgPayload) {
16 | await emit("front-command", payload);
17 | }
18 |
19 | export async function sendInjectKeycode(payload: InjectKeycode) {
20 | await sendControlMsg({
21 | msgType: ControlMsgType.ControlMsgTypeInjectKeycode,
22 | msgData: payload,
23 | });
24 | }
25 |
26 | export async function sendInjectText(payload: InjectText) {
27 | await sendControlMsg({
28 | msgType: ControlMsgType.ControlMsgTypeInjectText,
29 | msgData: payload,
30 | });
31 | }
32 |
33 | export async function sendInjectTouchEvent(payload: InjectTouchEvent) {
34 | await sendControlMsg({
35 | msgType: ControlMsgType.ControlMsgTypeInjectTouchEvent,
36 | msgData: payload,
37 | });
38 | }
39 |
40 | export async function sendInjectScrollEvent(payload: InjectScrollEvent) {
41 | await sendControlMsg({
42 | msgType: ControlMsgType.ControlMsgTypeInjectScrollEvent,
43 | msgData: payload,
44 | });
45 | }
46 |
47 | export async function sendBackOrScreenOn(payload: BackOrScreenOn) {
48 | await sendControlMsg({
49 | msgType: ControlMsgType.ControlMsgTypeBackOrScreenOn,
50 | msgData: payload,
51 | });
52 | }
53 |
54 | export async function sendExpandNotificationPanel() {
55 | await sendControlMsg({
56 | msgType: ControlMsgType.ControlMsgTypeExpandNotificationPanel,
57 | });
58 | }
59 |
60 | export async function sendExpandSettingsPanel() {
61 | await sendControlMsg({
62 | msgType: ControlMsgType.ControlMsgTypeExpandSettingsPanel,
63 | });
64 | }
65 |
66 | export async function sendCollapsePanels() {
67 | await sendControlMsg({
68 | msgType: ControlMsgType.ControlMsgTypeCollapsePanels,
69 | });
70 | }
71 |
72 | export async function sendGetClipboard(payload: GetClipboard) {
73 | await sendControlMsg({
74 | msgType: ControlMsgType.ControlMsgTypeGetClipboard,
75 | msgData: payload,
76 | });
77 | }
78 |
79 | export async function sendSetClipboard(payload: SetClipboard) {
80 | await sendControlMsg({
81 | msgType: ControlMsgType.ControlMsgTypeSetClipboard,
82 | msgData: payload,
83 | });
84 | }
85 |
86 | export async function sendSetScreenPowerMode(payload: SetScreenPowerMode) {
87 | await sendControlMsg({
88 | msgType: ControlMsgType.ControlMsgTypeSetScreenPowerMode,
89 | msgData: payload,
90 | });
91 | }
92 |
93 | export async function sendRotateDevice() {
94 | await sendControlMsg({
95 | msgType: ControlMsgType.ControlMsgTypeRotateDevice,
96 | });
97 | }
98 |
99 | export async function sendUhidCreate(payload: UhidCreate) {
100 | await sendControlMsg({
101 | msgType: ControlMsgType.ControlMsgTypeUhidCreate,
102 | msgData: payload,
103 | });
104 | }
105 |
106 | export async function sendUhidInput(payload: UhidInput) {
107 | await sendControlMsg({
108 | msgType: ControlMsgType.ControlMsgTypeUhidInput,
109 | msgData: payload,
110 | });
111 | }
112 |
113 | export async function sendOpenHardKeyboardSettings() {
114 | await sendControlMsg({
115 | msgType: ControlMsgType.ControlMsgTypeOpenHardKeyboardSettings,
116 | });
117 | }
118 |
119 | export enum ControlMsgType {
120 | ControlMsgTypeInjectKeycode, //发送原始按键
121 | ControlMsgTypeInjectText, //发送文本,不知道是否能输入中文(估计只是把文本转为keycode的输入效果)
122 | ControlMsgTypeInjectTouchEvent, //发送触摸事件
123 | ControlMsgTypeInjectScrollEvent, //发送滚动事件(类似接入鼠标后滚动滚轮的效果,不是通过触摸实现的)
124 | ControlMsgTypeBackOrScreenOn, //应该就是发送返回键
125 | ControlMsgTypeExpandNotificationPanel, //打开消息面板
126 | ControlMsgTypeExpandSettingsPanel, //打开设置面板(就是消息面板右侧的)
127 | ControlMsgTypeCollapsePanels, //折叠上述面板
128 | ControlMsgTypeGetClipboard, //获取剪切板
129 | ControlMsgTypeSetClipboard, //设置剪切板
130 | ControlMsgTypeSetScreenPowerMode, //设置屏幕电源模式,是关闭设备屏幕的(SC_SCREEN_POWER_MODE_OFF 和 SC_SCREEN_POWER_MODE_NORMAL )
131 | ControlMsgTypeRotateDevice, //旋转设备屏幕
132 | ControlMsgTypeUhidCreate, //创建虚拟设备?从而模拟真实的键盘、鼠标用的,目前没用
133 | ControlMsgTypeUhidInput, //同上转发键盘、鼠标的输入,目前没用
134 | ControlMsgTypeOpenHardKeyboardSettings, //打开设备的硬件键盘设置,目前没用
135 | }
136 |
137 | type ControlMsgData =
138 | | InjectKeycode
139 | | InjectText
140 | | InjectTouchEvent
141 | | InjectScrollEvent
142 | | BackOrScreenOn
143 | | GetClipboard
144 | | SetClipboard
145 | | SetScreenPowerMode
146 | | UhidCreate
147 | | UhidInput;
148 |
149 | interface ScPosition {
150 | x: number;
151 | y: number;
152 | // screen width
153 | w: number;
154 | // screen height
155 | h: number;
156 | }
157 |
158 | interface InjectKeycode {
159 | action: AndroidKeyEventAction;
160 | keycode: AndroidKeycode;
161 | // https://developer.android.com/reference/android/view/KeyEvent#getRepeatCount()
162 | repeat: number;
163 | metastate: AndroidMetastate;
164 | }
165 |
166 | export enum ScCopyKey {
167 | SC_COPY_KEY_NONE,
168 | SC_COPY_KEY_COPY,
169 | SC_COPY_KEY_CUT,
170 | }
171 |
172 | export enum ScScreenPowerMode {
173 | // see
174 | SC_SCREEN_POWER_MODE_OFF = 0,
175 | SC_SCREEN_POWER_MODE_NORMAL = 2,
176 | }
177 |
178 | interface InjectText {
179 | text: string;
180 | }
181 |
182 | interface InjectTouchEvent {
183 | action: AndroidMotionEventAction;
184 | actionButton: AndroidMotionEventButtons;
185 | buttons: AndroidMotionEventButtons;
186 | pointerId: number;
187 | position: ScPosition;
188 | pressure: number;
189 | }
190 |
191 | interface InjectScrollEvent {
192 | position: ScPosition;
193 | hscroll: number;
194 | vscroll: number;
195 | buttons: AndroidMotionEventButtons;
196 | }
197 |
198 | interface BackOrScreenOn {
199 | action: AndroidKeyEventAction; // action for the BACK key
200 | // screen may only be turned on on ACTION_DOWN
201 | }
202 |
203 | interface GetClipboard {
204 | copyKey: ScCopyKey;
205 | }
206 |
207 | interface SetClipboard {
208 | sequence: number;
209 | text: string;
210 | paste: boolean;
211 | }
212 |
213 | interface SetScreenPowerMode {
214 | mode: ScScreenPowerMode;
215 | }
216 |
217 | interface UhidCreate {
218 | id: number;
219 | reportDescSize: number;
220 | reportDesc: Uint8Array;
221 | }
222 |
223 | interface UhidInput {
224 | id: number;
225 | size: number;
226 | data: Uint8Array;
227 | }
228 |
--------------------------------------------------------------------------------
/src/frontcommand/scrcpyMaskCmd.ts:
--------------------------------------------------------------------------------
1 | import { emit } from "@tauri-apps/api/event";
2 | import { AndroidKeycode, AndroidMetastate } from "./android";
3 |
4 | async function sendScrcpyMaskCmd(
5 | commandType: ScrcpyMaskCmdType,
6 | msgData: ScrcpyMaskCmdData
7 | ) {
8 | const payload: ScrcpyMaskCmdPayload = { msgType: commandType, msgData };
9 | await emit("front-command", payload);
10 | }
11 |
12 | export async function sendKey(payload: CmdDataSendKey) {
13 | await sendScrcpyMaskCmd(ScrcpyMaskCmdType.SendKey, payload);
14 | }
15 |
16 | export async function touch(payload: CmdDataTouch) {
17 | if (!("time" in payload) || payload.time === undefined) payload.time = 80;
18 | await sendScrcpyMaskCmd(ScrcpyMaskCmdType.Touch, payload);
19 | }
20 |
21 | export async function swipe(payload: CmdDataSwipe) {
22 | await sendScrcpyMaskCmd(ScrcpyMaskCmdType.Swipe, payload);
23 | }
24 |
25 | export async function shutdown() {
26 | await sendScrcpyMaskCmd(ScrcpyMaskCmdType.Shutdown, "");
27 | }
28 |
29 | export enum ScrcpyMaskCmdType {
30 | SendKey = 15,
31 | Touch = 16,
32 | Swipe = 17,
33 | Shutdown = 18,
34 | }
35 |
36 | type ScrcpyMaskCmdData = CmdDataSendKey | CmdDataTouch | CmdDataSwipe | String;
37 |
38 | export enum SendKeyAction {
39 | Default = 0,
40 | Down = 1,
41 | Up = 2,
42 | }
43 |
44 | interface CmdDataSendKey {
45 | action: SendKeyAction;
46 | keycode: AndroidKeycode;
47 | metastate?: AndroidMetastate;
48 | }
49 |
50 | export enum TouchAction {
51 | Default = 0,
52 | Down = 1,
53 | Up = 2,
54 | Move = 3,
55 | }
56 |
57 | interface CmdDataTouch {
58 | action: TouchAction;
59 | pointerId: number;
60 | screen: { w: number; h: number };
61 | pos: { x: number; y: number };
62 | time?: number; // valid only when action is Default, default 80 milliseconds
63 | }
64 |
65 | export enum SwipeAction {
66 | Default = 0,
67 | // cooperate with touch action
68 | NoUp = 1,
69 | NoDown = 2,
70 | }
71 |
72 | interface CmdDataSwipe {
73 | action: SwipeAction;
74 | pointerId: number;
75 | screen: { w: number; h: number };
76 | pos: { x: number; y: number }[];
77 | intervalBetweenPos: number;
78 | }
79 |
80 | interface ScrcpyMaskCmdPayload {
81 | msgType: ScrcpyMaskCmdType;
82 | msgData: ScrcpyMaskCmdData;
83 | }
84 |
--------------------------------------------------------------------------------
/src/i18n/index.ts:
--------------------------------------------------------------------------------
1 | import { createI18n } from "vue-i18n";
2 | import { Store } from "@tauri-apps/plugin-store";
3 |
4 | import enUS from "./en-US.json";
5 | import zhCN from "./zh-CN.json";
6 |
7 | const localStore = new Store("store.bin");
8 |
9 | const i18n = createI18n({
10 | allowComposition: true,
11 | legacy: false,
12 | messages: {
13 | "en-US": enUS,
14 | "zh-CN": zhCN,
15 | },
16 | });
17 |
18 | localStore.get<"en-US" | "zh-CN">("language").then((language) => {
19 | i18n.global.locale.value = language ?? "en-US";
20 | });
21 |
22 | export default i18n;
23 |
--------------------------------------------------------------------------------
/src/i18n/zh-CN.json:
--------------------------------------------------------------------------------
1 | {
2 | "pages": {
3 | "Device": {
4 | "status": "状态",
5 | "shutdown": {
6 | "title": "警告",
7 | "content": "确定关闭Scrcpy控制服务?",
8 | "positiveText": "确定",
9 | "negativeText": "取消"
10 | },
11 | "menu": {
12 | "control": "控制此设备",
13 | "screen": "获取屏幕尺寸"
14 | },
15 | "deviceControl": {
16 | "controlInfo": "正在启动控制服务,请保持设备亮屏",
17 | "connectTimeout": "设备连接超时"
18 | },
19 | "deviceGetScreenSize": "设备屏幕尺寸为:",
20 | "inputWirelessAddress": "请输入无线调试地址",
21 | "localPort": "本地端口",
22 | "localPortPlaceholder": "Scrcpy 本地端口",
23 | "wireless": "无线连接",
24 | "wirelessPlaceholder": "无线连接地址",
25 | "connect": "连接",
26 | "deviceSize": {},
27 | "controledDevice": "受控设备",
28 | "noControledDevice": "无受控设备",
29 | "availableDevice": "可用设备",
30 | "alreadyControled": "已存在受控设备",
31 | "alreadyDisconnected": "受控设备连接已断开",
32 | "inputWsAddress": "请输入 Websocket 地址",
33 | "externalControl": "外部控制",
34 | "wsAddress": "Websocket 地址",
35 | "wsClose": "断开",
36 | "wsConnect": "控制",
37 | "adbDeviceError": "无法获取可用设备",
38 | "adbConnectError": "无线连接失败",
39 | "rotation": "设备旋转 {0}°"
40 | },
41 | "Mask": {
42 | "keyconfigException": "按键方案异常,请删除此方案",
43 | "blankConfig": "空白方案",
44 | "checkUpdate": {
45 | "failed": "检查更新失败",
46 | "isLatest": "最新版本: {0},当前已是最新版本: {1}",
47 | "notLatest": {
48 | "title": "最新版本:{0}",
49 | "positiveText": "前往发布页",
50 | "negativeText": "取消"
51 | }
52 | },
53 | "noControledDevice": {
54 | "title": "未找到受控设备",
55 | "content": "请前往设备页面,控制任意设备",
56 | "positiveText": "去控制"
57 | },
58 | "sightMode": "鼠标已锁定, 按 {0} 键解锁",
59 | "checkAdb": "adb不可用,软件无法正常运行,请确保系统已安装adb。请将adb文件路径填入软件设置界面,或者将其所在文件夹添加到Path环境变量中: {0}",
60 | "keyInputMode": "已进入按键输入模式,关闭本消息可退出"
61 | },
62 | "Setting": {
63 | "tabs": {
64 | "basic": "基本设置",
65 | "mask": "蒙版设置",
66 | "about": "关于",
67 | "data": "数据管理"
68 | },
69 | "Mask": {
70 | "incorrectArea": "请正确输入蒙版的坐标和尺寸",
71 | "areaSaved": "蒙版区域已保存",
72 | "buttonPrompts": "按键提示",
73 | "ifButtonPrompts": "显示按键提示",
74 | "opacity": "不透明度",
75 | "areaAdjust": "蒙版区域",
76 | "areaPlaceholder": {
77 | "x": "左上角X坐标"
78 | },
79 | "areaFormPlaceholder": {
80 | "y": "左上角Y坐标",
81 | "w": "蒙版宽度",
82 | "h": "蒙版高度"
83 | },
84 | "areaFormMissing": {
85 | "x": "请输入蒙版左上角X坐标",
86 | "y": "请输入蒙版左上角Y坐标",
87 | "w": "请输入蒙版宽度",
88 | "h": "请输入蒙版高度"
89 | },
90 | "rotation": {
91 | "title": "设备旋转",
92 | "rotateWithDevice": "跟随设备旋转",
93 | "verticalLength": "竖屏蒙版高度",
94 | "horizontalLength": "横屏蒙版宽度"
95 | },
96 | "screenStream": {
97 | "enable": "启用投屏",
98 | "address": "投屏地址",
99 | "addressPlaceholder": "请输入 ScreenStream 投屏地址"
100 | }
101 | },
102 | "Basic": {
103 | "language": "语言",
104 | "adbPath": {
105 | "setSuccess": "adb 路径设置成功",
106 | "title": "adb 路径",
107 | "placeholder": "adb 路径",
108 | "set": "设置"
109 | }
110 | },
111 | "Data": {
112 | "delLocalStore": {
113 | "dialog": {
114 | "title": "警告",
115 | "positiveText": "删除",
116 | "negativeText": "取消",
117 | "delKey": "即将删除数据\"{0}\",删除操作不可撤回,是否继续?",
118 | "delAll": "即将清空数据,操作不可撤回,且清空后将重启软件,是否继续?"
119 | },
120 | "warning": "删除数据可能导致无法预料的后果,请慎重操作。若出现异常请尝试清空数据并重启软件。"
121 | },
122 | "localStore": "本地数据",
123 | "delCurData": "删除当前数据"
124 | },
125 | "About": {
126 | "about": "关于",
127 | "introduction": "一个基于 Rust & Tarui 的 Scrcpy 客户端,旨在提供鼠标键盘按键映射来控制安卓设备。",
128 | "github": "Github 仓库",
129 | "blog": "AkiChase 博客",
130 | "update": "更新",
131 | "curVersion": "当前版本:{0}",
132 | "checkUpdate": "检查更新",
133 | "checkUpdateOnStartup": "启动时检查软件更新"
134 | }
135 | },
136 | "KeyBoard": {
137 | "addButton": {
138 | "Tap": "普通点击",
139 | "SteeringWheel": "键盘行走",
140 | "Skill": "技能",
141 | "CancelSkill": "技能取消",
142 | "Observation": "观察视角",
143 | "Macro": "宏",
144 | "Sight": "准星",
145 | "Fire": "开火",
146 | "existSight": "已存在准星按钮",
147 | "existFire": "已存在开火按钮",
148 | "Swipe": "滑动"
149 | },
150 | "buttonKeyRepeat": "按键重复: {0}",
151 | "noSaveDialog": {
152 | "title": "警告",
153 | "content": "当前方案尚未保存,是否保存?",
154 | "positiveText": "保存",
155 | "negativeText": "取消",
156 | "keyRepeat": "存在重复按键,无法保存"
157 | },
158 | "KeyCommon": {
159 | "macroParseSuccess": "宏代码解析成功,但不保证代码正确性,请自行测试",
160 | "macroParseFailed": "宏代码保存失败,请检查代码格式是否正确",
161 | "editMacro": "编辑宏代码",
162 | "macroModal": {
163 | "title": "宏编辑",
164 | "down": "按下按键执行的宏",
165 | "loop": "按住执行的宏",
166 | "placeholder": "JSON宏代码, 可为空",
167 | "up": "抬起执行的宏"
168 | },
169 | "cancelSkill": "技能取消",
170 | "tap": "普通点击",
171 | "macroCode": "宏代码",
172 | "macro": "宏"
173 | },
174 | "setting": {
175 | "touchTime": "触摸时长",
176 | "touchTimePlaceholder": "请输入触摸时长(ms)",
177 | "pointerID": "触点ID",
178 | "pointerIDPlaceholder": "请输入触点ID",
179 | "note": "备注",
180 | "notePlaceholder": "请输入备注"
181 | },
182 | "KeyInfo": {
183 | "title": "Key Info",
184 | "note": "Press any key"
185 | },
186 | "Observation": {
187 | "observation": "观察视角",
188 | "scale": "灵敏度",
189 | "scalePlaceholder": "请输入灵敏度"
190 | },
191 | "KeySetting": {
192 | "onlyOneConfig": "当前仅有一个按键方案,点击导入默认,可导入预设方案",
193 | "importFailed": "导入失败",
194 | "importSuccess": "按键方案已导入",
195 | "importDefaultFailed": "默认按键方案导入失败",
196 | "importDefaultSuccess": "已导入{0}个默认方案",
197 | "configEdited": "请先保存或还原当前方案",
198 | "newConfig": "新方案",
199 | "newConfigSuccess": "新方案已创建",
200 | "copyConfigTitle": "{0}-副本",
201 | "copyConfigSuccess": "方案已复制为:{0}",
202 | "exportSuccess": "当前按键方案已导出到剪切板",
203 | "exportFailed": "按键方案导出失败",
204 | "checkConfigSizeWarning": "请注意当前按键方案\"{0}\"与蒙版尺寸不一致,若有需要可进行迁移",
205 | "migrateConfigTitle": "{0}-迁移",
206 | "migrateConfigSuccess": "已迁移到新方案:{0}",
207 | "migrateConfigNeedless": "当前方案符合蒙版尺寸,无需迁移",
208 | "buttonDrag": "长按可拖动",
209 | "config": "按键映射方案",
210 | "configRelativeSize": "相对蒙版尺寸: {0}x{1}",
211 | "saveConfig": "保存方案",
212 | "resetConfig": "还原方案",
213 | "createConfig": "新建方案",
214 | "copyConfig": "复制方案",
215 | "migrateConfig": "迁移方案",
216 | "delConfig": "删除方案",
217 | "renameConfig": "重命名",
218 | "others": "其他",
219 | "importConfig": "导入方案",
220 | "exportConfig": "导出方案",
221 | "importDefaultConfig": "导入默认",
222 | "keyInfo": "按键信息",
223 | "addButtonTip": "提示:右键空白区域可添加按键",
224 | "importPlaceholder": "粘贴单个按键方案的JSON文本 (此处无法对按键方案的合法性进行判断, 请确保JSON内容正确)",
225 | "import": "导入",
226 | "renameTitle": "重命名按键方案",
227 | "delConfigLeast": "至少保留一个方案",
228 | "delSuccess": "方案已删除:{0}",
229 | "renameSuccess": "方案已重命名为:{0}",
230 | "renameEmpty": "方案名不能为空",
231 | "saveKeyRepeat": "存在重复按键,无法保存"
232 | },
233 | "KeySkill": {
234 | "skill": "技能",
235 | "options": "选项",
236 | "double": "双击施放",
237 | "directionless": "无方向技能",
238 | "triggerWhenPressed": "按下时触发",
239 | "range": "范围"
240 | },
241 | "SteeringWheel": {
242 | "steeringWheel": "键盘行走",
243 | "offset": "偏移"
244 | },
245 | "KeySight": {
246 | "sight": "准星",
247 | "scaleX": "水平灵敏度",
248 | "scaleY": "垂直灵敏度",
249 | "scalePlaceholder": "请输入灵敏度"
250 | },
251 | "KeyFire": {
252 | "fire": "开火",
253 | "drag": "拖动施法",
254 | "scaleX": "水平灵敏度",
255 | "scaleY": "垂直灵敏度",
256 | "scalePlaceholder": "请输入灵敏度"
257 | },
258 | "Swipe": {
259 | "swipe": "滑动",
260 | "interval": "滑动时间间隔",
261 | "intervalPlaceholder": "输入坐标点之间的时间间隔",
262 | "pos": "坐标点",
263 | "editPos": "编辑",
264 | "editTips": "左键点击空白区域添加新坐标点。左键拖拽移动特定坐标点,右键删除特定坐标点"
265 | }
266 | }
267 | },
268 | "sidebar": {
269 | "noControledDevice": "未控制任何设备"
270 | },
271 | "websocket": {
272 | "open": "已连接到外部控制服务端",
273 | "close": "外部控制连接断开",
274 | "error": "未知错误,外部控制连接断开"
275 | }
276 | }
277 |
--------------------------------------------------------------------------------
/src/invoke.ts:
--------------------------------------------------------------------------------
1 | import { invoke } from "@tauri-apps/api/core";
2 |
3 | interface Device {
4 | id: string;
5 | status: string;
6 | }
7 |
8 | export async function adbDevices(): Promise {
9 | return await invoke("adb_devices");
10 | }
11 |
12 | export async function forwardServerPort(
13 | id: string,
14 | scid: string,
15 | port: number
16 | ): Promise {
17 | return await invoke("forward_server_port", { id, scid, port });
18 | }
19 |
20 | export async function pushServerFile(id: string): Promise {
21 | return await invoke("push_server_file", { id });
22 | }
23 |
24 | export async function startScrcpyServer(
25 | id: string,
26 | scid: string,
27 | address: string
28 | ): Promise {
29 | return await invoke("start_scrcpy_server", { id, scid, address });
30 | }
31 |
32 | export async function getCurClientInfo(): Promise<{
33 | device_name: string;
34 | device_id: string;
35 | scid: string;
36 | width: number;
37 | height: number;
38 | } | null> {
39 | return await invoke("get_cur_client_info");
40 | }
41 |
42 | export async function getDeviceScreenSize(
43 | id: string
44 | ): Promise<[number, number]> {
45 | return await invoke("get_device_screen_size", { id });
46 | }
47 |
48 | export async function adbConnect(address: string): Promise {
49 | return await invoke("adb_connect", { address });
50 | }
51 |
52 | export async function loadDefaultKeyconfig(): Promise {
53 | return await invoke("load_default_keyconfig");
54 | }
55 |
56 | export async function checkAdbAvailable(): Promise {
57 | return await invoke("check_adb_available");
58 | }
59 |
60 | export async function setAdbPath(path: string): Promise {
61 | return await invoke("set_adb_path", { adbPath: path });
62 | }
63 |
64 | export type { Device };
65 |
--------------------------------------------------------------------------------
/src/keyMappingConfig.ts:
--------------------------------------------------------------------------------
1 | interface KeyBase {
2 | note: string;
3 | // pos relative to the mask
4 | posX: number;
5 | posY: number;
6 | }
7 |
8 | export interface KeySteeringWheel extends KeyBase {
9 | type: "SteeringWheel";
10 | pointerId: number;
11 | key: {
12 | left: string;
13 | right: string;
14 | up: string;
15 | down: string;
16 | };
17 | offset: number;
18 | }
19 |
20 | export interface KeyDirectionalSkill extends KeyBase {
21 | type: "DirectionalSkill";
22 | pointerId: number;
23 | key: string;
24 | range: number;
25 | }
26 |
27 | export interface KeyDirectionlessSkill extends KeyBase {
28 | type: "DirectionlessSkill";
29 | pointerId: number;
30 | key: string;
31 | }
32 |
33 | export interface KeyCancelSkill extends KeyBase {
34 | type: "CancelSkill";
35 | pointerId: number;
36 | key: string;
37 | }
38 |
39 | export interface KeyTriggerWhenPressedSkill extends KeyBase {
40 | type: "TriggerWhenPressedSkill";
41 | pointerId: number;
42 | key: string;
43 | directional: boolean;
44 | rangeOrTime: number;
45 | }
46 |
47 | export interface KeyTriggerWhenDoublePressedSkill extends KeyBase {
48 | type: "TriggerWhenDoublePressedSkill";
49 | pointerId: number;
50 | key: string;
51 | range: number;
52 | }
53 |
54 | export interface KeyObservation extends KeyBase {
55 | type: "Observation";
56 | pointerId: number;
57 | key: string;
58 | scale: number;
59 | }
60 |
61 | export interface KeyTap extends KeyBase {
62 | type: "Tap";
63 | pointerId: number;
64 | key: string;
65 | time: number;
66 | }
67 |
68 | export interface KeySwipe extends KeyBase {
69 | type: "Swipe";
70 | pointerId: number;
71 | key: string;
72 | pos: { x: number; y: number }[];
73 | intervalBetweenPos: number;
74 | }
75 |
76 | export type KeyMacroList = Array<{
77 | type: "touch" | "sleep" | "swipe" | "key-input-mode";
78 | args: any[];
79 | }> | null;
80 |
81 | export interface KeyMacro extends KeyBase {
82 | type: "Macro";
83 | key: string;
84 | macro: {
85 | down: KeyMacroList;
86 | loop: KeyMacroList;
87 | up: KeyMacroList;
88 | };
89 | }
90 |
91 | export interface KeySight extends KeyBase {
92 | type: "Sight";
93 | key: string;
94 | pointerId: number;
95 | scaleX: number;
96 | scaleY: number;
97 | }
98 |
99 | export interface KeyFire extends KeyBase {
100 | type: "Fire";
101 | drag: boolean;
102 | pointerId: number;
103 | scaleX: number;
104 | scaleY: number;
105 | }
106 |
107 | export type KeyMapping =
108 | | KeySteeringWheel
109 | | KeyDirectionalSkill
110 | | KeyDirectionlessSkill
111 | | KeyTriggerWhenPressedSkill
112 | | KeyTriggerWhenDoublePressedSkill
113 | | KeyObservation
114 | | KeyMacro
115 | | KeyCancelSkill
116 | | KeyTap
117 | | KeySwipe
118 | | KeySight
119 | | KeyFire;
120 |
121 | export type KeyCommon = KeyMacro | KeyCancelSkill | KeyTap;
122 |
123 | export type KeySkill =
124 | | KeyDirectionalSkill
125 | | KeyDirectionlessSkill
126 | | KeyTriggerWhenPressedSkill
127 | | KeyTriggerWhenDoublePressedSkill;
128 |
129 | export interface KeyMappingConfig {
130 | relativeSize: { w: number; h: number };
131 | title: string;
132 | list: KeyMapping[];
133 | }
134 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 | import { createPinia } from "pinia";
3 | import "./styles.css";
4 | import App from "./App.vue";
5 | import router from "./router";
6 | import i18n from "./i18n";
7 |
8 | const pinia = createPinia();
9 |
10 | const app = createApp(App);
11 | app.use(router);
12 | app.use(pinia);
13 | app.use(i18n);
14 | app.mount("#app");
15 |
--------------------------------------------------------------------------------
/src/router.ts:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHashHistory } from "vue-router";
2 | import Mask from "./components/Mask.vue";
3 | import Setting from "./components/setting/Setting.vue";
4 | import KeyBoard from "./components/keyboard/KeyBoard.vue";
5 | import Device from "./components/Device.vue";
6 |
7 | const routes = [
8 | { path: "/", name: "mask", component: Mask },
9 | { path: "/device", name: "device", component: Device },
10 | { path: "/setting", name: "setting", component: Setting },
11 | { path: "/keyboard", name: "keyboard", component: KeyBoard },
12 | ];
13 |
14 | const router = createRouter({
15 | history: createWebHashHistory(),
16 | routes,
17 | });
18 |
19 | export default router;
20 |
--------------------------------------------------------------------------------
/src/screenStream.ts:
--------------------------------------------------------------------------------
1 | export class ScreenStream {
2 | img: HTMLImageElement;
3 | clientId: string;
4 | connectTimeoutId: number | undefined;
5 |
6 | public constructor(imgElement: HTMLImageElement, clientId: string) {
7 | this.img = imgElement;
8 | this.clientId = clientId;
9 | }
10 |
11 | public connect(address: string, onConnect: () => void, onError: () => void) {
12 | if (address.endsWith("/")) address = address.slice(0, -1);
13 | const that = this;
14 | const img = that.img;
15 | const url = `${address}/stream.mjpeg?clientId=${this.clientId}`;
16 |
17 | img.src = "";
18 | clearTimeout(that.connectTimeoutId);
19 | new Promise((resolve, reject) => {
20 | img.onload = function () {
21 | img.onload = null;
22 | img.onerror = null;
23 | resolve();
24 | };
25 | img.onerror = function (e) {
26 | img.onerror = null;
27 | img.onload = null;
28 | reject(e);
29 | };
30 | img.src = url;
31 | })
32 | .then(() => {
33 | onConnect();
34 | })
35 | .catch(() => {
36 | img.src = "";
37 | onError();
38 | });
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/store/global.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from "pinia";
2 | import { Ref, ref } from "vue";
3 | import {
4 | KeyMapping,
5 | KeyMappingConfig,
6 | KeySteeringWheel,
7 | } from "../keyMappingConfig";
8 | import { Store } from "@tauri-apps/plugin-store";
9 |
10 | const localStore = new Store("store.bin");
11 |
12 | export const useGlobalStore = defineStore("global", () => {
13 | const showLoadingRef = ref(false);
14 | function showLoading() {
15 | showLoadingRef.value = true;
16 | }
17 | function hideLoading() {
18 | showLoadingRef.value = false;
19 | }
20 |
21 | interface ControledDevice {
22 | scid: string;
23 | deviceName: string;
24 | deviceID: string;
25 | }
26 |
27 | const controledDevice: Ref = ref(null);
28 | const editKeyMappingList: Ref = ref([]);
29 |
30 | let checkUpdate: () => Promise = async () => {};
31 | let checkAdb: () => Promise = async () => {};
32 |
33 | function applyEditKeyMappingList(): boolean {
34 | const set = new Set();
35 | for (const keyMapping of editKeyMappingList.value) {
36 | if (keyMapping.type === "SteeringWheel") {
37 | const nameList: ["up", "down", "left", "right"] = [
38 | "up",
39 | "down",
40 | "left",
41 | "right",
42 | ];
43 | for (const name of nameList) {
44 | if (set.has((keyMapping as KeySteeringWheel).key[name])) return false;
45 | set.add((keyMapping as KeySteeringWheel).key[name]);
46 | }
47 | } else if (keyMapping.type !== "Fire") {
48 | if (set.has(keyMapping.key as string)) return false;
49 | set.add(keyMapping.key as string);
50 | }
51 | }
52 |
53 | keyMappingConfigList.value[curKeyMappingIndex.value].list =
54 | editKeyMappingList.value;
55 | localStore.set("keyMappingConfigList", keyMappingConfigList.value);
56 | return true;
57 | }
58 |
59 | function resetEditKeyMappingList() {
60 | editKeyMappingList.value = JSON.parse(
61 | JSON.stringify(keyMappingConfigList.value[curKeyMappingIndex.value].list)
62 | );
63 | }
64 |
65 | function setKeyMappingIndex(index: number) {
66 | curKeyMappingIndex.value = index;
67 | resetEditKeyMappingList();
68 | localStore.set("curKeyMappingIndex", index);
69 | }
70 |
71 | const externalControlled = ref(false);
72 |
73 | const screenSizeW: Ref = ref(0);
74 | const screenSizeH: Ref = ref(0);
75 |
76 | const keyInputFlag = ref(false);
77 |
78 | const maskSizeW: Ref = ref(0);
79 | const maskSizeH: Ref = ref(0);
80 |
81 | const screenStreamClientId = ref("scrcpy-mask");
82 |
83 | // persistent storage
84 | const keyMappingConfigList: Ref = ref([]);
85 | const curKeyMappingIndex = ref(0);
86 | const maskButton = ref({
87 | transparency: 0.5,
88 | show: true,
89 | });
90 | const checkUpdateAtStart = ref(true);
91 |
92 | const screenStream = ref({
93 | enable: false,
94 | address: "",
95 | });
96 |
97 | const rotation = ref({
98 | enable: true,
99 | verticalLength: 600,
100 | horizontalLength: 800,
101 | });
102 |
103 | const clipboardSync = ref({
104 | syncFromDevice: true,
105 | pasteFromPC: true,
106 | });
107 |
108 | return {
109 | // persistent storage
110 | keyMappingConfigList,
111 | curKeyMappingIndex,
112 | maskButton,
113 | checkUpdateAtStart,
114 | externalControlled,
115 | screenStream,
116 | rotation,
117 | clipboardSync,
118 | // in-memory storage
119 | screenStreamClientId,
120 | maskSizeW,
121 | maskSizeH,
122 | screenSizeW,
123 | screenSizeH,
124 | keyInputFlag,
125 | showLoading,
126 | hideLoading,
127 | showLoadingRef,
128 | controledDevice,
129 | editKeyMappingList,
130 | applyEditKeyMappingList,
131 | resetEditKeyMappingList,
132 | setKeyMappingIndex,
133 | checkUpdate,
134 | checkAdb,
135 | };
136 | });
137 |
--------------------------------------------------------------------------------
/src/store/keyboard.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from "pinia";
2 | import { ref } from "vue";
3 |
4 | export const useKeyboardStore = defineStore("keyboard", () => {
5 | const showKeyInfoFlag = ref(false);
6 | const showSettingFlag = ref(false);
7 | const showButtonSettingFlag = ref(false);
8 | const showButtonAddFlag = ref(false);
9 | const editSwipePointsFlag = ref(false);
10 | const activeButtonIndex = ref(-1);
11 | const activeSteeringWheelButtonKeyIndex = ref(-1);
12 | const edited = ref(false);
13 |
14 | return {
15 | showKeyInfoFlag,
16 | showSettingFlag,
17 | showButtonSettingFlag,
18 | showButtonAddFlag,
19 | editSwipePointsFlag,
20 | activeButtonIndex,
21 | activeSteeringWheelButtonKeyIndex,
22 | edited,
23 | };
24 | });
25 |
--------------------------------------------------------------------------------
/src/storeLoader.ts:
--------------------------------------------------------------------------------
1 | import { Store } from "@tauri-apps/plugin-store";
2 | import { KeyMappingConfig } from "./keyMappingConfig";
3 | import { useGlobalStore } from "./store/global";
4 | import { useI18n } from "vue-i18n";
5 |
6 | let localStore: Store;
7 | let store: ReturnType;
8 | let t: ReturnType["t"];
9 |
10 | async function loadKeyMappingConfigList() {
11 | // loading keyMappingConfigList from local store
12 | let keyMappingConfigList = await localStore.get(
13 | "keyMappingConfigList"
14 | );
15 | if (keyMappingConfigList === null || keyMappingConfigList.length === 0) {
16 | // add empty key mapping config
17 | // unable to get mask element when app is not ready
18 | // so we use the stored mask area to get relative size
19 | const maskArea = await localStore.get<{
20 | posX: number;
21 | posY: number;
22 | sizeW: number;
23 | sizeH: number;
24 | }>("maskArea");
25 | let relativeSize = { w: 800, h: 600 };
26 | if (maskArea !== null) {
27 | relativeSize = {
28 | w: maskArea.sizeW,
29 | h: maskArea.sizeH,
30 | };
31 | }
32 | keyMappingConfigList = [
33 | {
34 | relativeSize,
35 | title: t("pages.Mask.blankConfig"),
36 | list: [],
37 | },
38 | ];
39 | await localStore.set("keyMappingConfigList", keyMappingConfigList);
40 | }
41 | store.keyMappingConfigList = keyMappingConfigList;
42 | }
43 |
44 | async function loadCurKeyMappingIndex() {
45 | // loading curKeyMappingIndex from local store
46 | let curKeyMappingIndex = await localStore.get("curKeyMappingIndex");
47 | if (
48 | curKeyMappingIndex === null ||
49 | curKeyMappingIndex >= store.keyMappingConfigList.length
50 | ) {
51 | curKeyMappingIndex = 0;
52 | localStore.set("curKeyMappingIndex", curKeyMappingIndex);
53 | }
54 | store.curKeyMappingIndex = curKeyMappingIndex;
55 | }
56 |
57 | async function loadMaskButton() {
58 | // loading maskButton from local store
59 | let maskButton = await localStore.get<{
60 | show: boolean;
61 | transparency: number;
62 | }>("maskButton");
63 | store.maskButton = maskButton ?? {
64 | show: true,
65 | transparency: 0.5,
66 | };
67 | }
68 |
69 | async function loadCheckUpdateAtStart() {
70 | // loading checkUpdateAtStart from local store
71 | const checkUpdateAtStart = await localStore.get(
72 | "checkUpdateAtStart"
73 | );
74 | store.checkUpdateAtStart = checkUpdateAtStart ?? true;
75 | }
76 |
77 | async function loadRotation() {
78 | // loading rotation from local store
79 | const rotation = await localStore.get<{
80 | enable: boolean;
81 | verticalLength: number;
82 | horizontalLength: number;
83 | }>("rotation");
84 | if (rotation) store.rotation = rotation;
85 | }
86 |
87 | async function loadScreenStream() {
88 | // loading screenStream from local store
89 | const screenStream = await localStore.get<{
90 | enable: boolean;
91 | address: string;
92 | }>("screenStream");
93 | if (screenStream) store.screenStream = screenStream;
94 | }
95 |
96 | async function loadClipboardSync() {
97 | // loading clipboardSync from local store
98 | const clipboardSync = await localStore.get<{
99 | syncFromDevice: boolean;
100 | pasteFromPC: boolean;
101 | }>("clipboardSync");
102 | if (clipboardSync) store.clipboardSync = clipboardSync;
103 | console.log(store.clipboardSync);
104 | }
105 |
106 | export async function loadLocalStorage(
107 | theLocalStore: Store,
108 | theStore: ReturnType,
109 | theT: ReturnType["t"]
110 | ) {
111 | localStore = theLocalStore;
112 | store = theStore;
113 | t = theT;
114 |
115 | await loadKeyMappingConfigList();
116 | await loadCurKeyMappingIndex();
117 | await loadMaskButton();
118 | await loadCheckUpdateAtStart();
119 | await loadRotation();
120 | await loadScreenStream();
121 | await loadClipboardSync();
122 | }
123 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --primary-color: #63e2b7;
3 | --primary-hover-color: #7fe7c4;
4 | --primary-pressed-color: #5acea7;
5 |
6 | --bg-color: #101014;
7 | --content-bg-color: #18181c;
8 | --content-hl-color: #26262a;
9 | --light-color: rgba(255, 255, 255, 0.82);
10 | --gray-color: #6b6e76;
11 | --red-color: #fc5185;
12 | --red-pressed-color: #f4336d;
13 | --blue-color: #70C0E8;
14 | --blue-pressed-color: #66AFD3;
15 | }
16 |
17 | html,
18 | body {
19 | background-color: transparent;
20 | height: 100vh;
21 | margin: 0;
22 | overflow: hidden;
23 | }
24 |
25 | div#app {
26 | height: 100%;
27 | box-sizing: border-box;
28 | }
29 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module "*.vue" {
4 | import type { DefineComponent } from "vue";
5 | const component: DefineComponent<{}, {}, any>;
6 | export default component;
7 | }
8 |
--------------------------------------------------------------------------------
/src/websocket.ts:
--------------------------------------------------------------------------------
1 | import { useMessage } from "naive-ui";
2 | import { useGlobalStore } from "./store/global";
3 | import { sendKey, shutdown, swipe, touch } from "./frontcommand/scrcpyMaskCmd";
4 | import { useI18n } from "vue-i18n";
5 |
6 | let ws: WebSocket;
7 | let sharedMessage: ReturnType;
8 | let sharedStore: ReturnType;
9 | let t: ReturnType["t"];
10 |
11 | export function connectExternalControl(
12 | url: string,
13 | message: ReturnType,
14 | store: ReturnType,
15 | i18nT: ReturnType["t"]
16 | ) {
17 | sharedMessage = message;
18 | sharedStore = store;
19 | t = i18nT;
20 |
21 | ws = new WebSocket(url);
22 | ws.addEventListener("open", handleOpen);
23 | ws.addEventListener("message", handleMessage);
24 | ws.addEventListener("close", handleClose);
25 | ws.addEventListener("error", handleError);
26 | }
27 |
28 | export function closeExternalControl() {
29 | if (ws) ws.close();
30 | }
31 |
32 | function handleOpen() {
33 | sharedStore.externalControlled = true;
34 | sharedStore.hideLoading();
35 | sharedMessage.success(t("websocket.open"));
36 | }
37 |
38 | async function handleMessage(event: MessageEvent) {
39 | try {
40 | const msg = JSON.parse(event.data);
41 | if (msg.type === "showMessage") {
42 | sharedMessage.create(msg.msgContent, { type: msg.msgType });
43 | } else if (msg.type === "getControlledDevice") {
44 | msg.controledDevice = sharedStore.controledDevice;
45 | ws.send(JSON.stringify(msg));
46 | } else if (msg.type === "sendKey") {
47 | delete msg.type;
48 | await sendKey(msg);
49 | } else if (msg.type === "touch") {
50 | msg.screen = { w: sharedStore.screenSizeW, h: sharedStore.screenSizeH };
51 | delete msg.type;
52 | await touch(msg);
53 | } else if (msg.type === "swipe") {
54 | console.log(msg);
55 | msg.screen = { w: sharedStore.screenSizeW, h: sharedStore.screenSizeH };
56 | delete msg.type;
57 | await swipe(msg);
58 | } else if (msg.type === "shutdown") {
59 | await shutdown();
60 | sharedStore.controledDevice = null;
61 | } else {
62 | console.error("Invalid message received", msg);
63 | }
64 | } catch (error) {
65 | console.error("Message received failed", error);
66 | }
67 | }
68 |
69 | function handleClose() {
70 | sharedMessage.info(t("websocket.close"));
71 | ws.close();
72 | sharedStore.externalControlled = false;
73 | sharedStore.hideLoading();
74 | }
75 |
76 | function handleError() {
77 | sharedMessage.error(t("websocket.error"));
78 | ws.close();
79 | sharedStore.externalControlled = false;
80 | sharedStore.hideLoading();
81 | }
82 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "preserve",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
24 | }
25 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import vue from "@vitejs/plugin-vue";
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig(async () => ({
6 | plugins: [vue()],
7 |
8 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
9 | //
10 | // 1. prevent vite from obscuring rust errors
11 | clearScreen: false,
12 | // 2. tauri expects a fixed port, fail if that port is not available
13 | server: {
14 | port: 1420,
15 | strictPort: true,
16 | watch: {
17 | // 3. tell vite to ignore watching `src-tauri`
18 | ignored: ["**/src-tauri/**"],
19 | },
20 | },
21 | }));
22 |
--------------------------------------------------------------------------------