├── .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 | ![](https://pic.superbed.cc/item/6637190cf989f2fb975b6162.png) 53 | 54 | - 可视化编辑按键映射配置 55 | 56 | ![](https://pic.superbed.cc/item/66371911f989f2fb975b62a3.png) 57 | 58 | - 游戏控制 59 | 60 | ![](https://pic.superbed.cc/item/66373c8cf989f2fb97679dfd.png) 61 | 62 | ![](https://pic.superbed.cc/item/6649cf0cfcada11d37c05b5e.jpg) 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 | [![Star History Chart](https://api.star-history.com/svg?repos=AkiChase/scrcpy-mask&type=Date)](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 | ![](https://pic.superbed.cc/item/6637190cf989f2fb975b6162.png) 55 | 56 | - Key mapping setting 57 | 58 | ![](https://pic.superbed.cc/item/66371911f989f2fb975b62a3.png) 59 | 60 | - Mask above game 61 | 62 | ![](https://pic.superbed.cc/item/66373c8cf989f2fb97679dfd.png) 63 | 64 | ![](https://pic.superbed.cc/item/6649cf0cfcada11d37c05b5e.jpg) 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 | [![Star History Chart](https://api.star-history.com/svg?repos=AkiChase/scrcpy-mask&type=Date)](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 | 35 | 36 | 67 | -------------------------------------------------------------------------------- /src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 42 | 43 | 75 | -------------------------------------------------------------------------------- /src/components/Mask.vue: -------------------------------------------------------------------------------- 1 | 181 | 182 | 246 | 247 | 319 | -------------------------------------------------------------------------------- /src/components/ScreenStream.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 58 | 59 | 78 | -------------------------------------------------------------------------------- /src/components/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 121 | 122 | 210 | -------------------------------------------------------------------------------- /src/components/keyboard/KeyFire.vue: -------------------------------------------------------------------------------- 1 | 93 | 94 | 189 | 190 | 256 | -------------------------------------------------------------------------------- /src/components/keyboard/KeyInfo.vue: -------------------------------------------------------------------------------- 1 | 108 | 109 | 131 | 132 | 181 | -------------------------------------------------------------------------------- /src/components/keyboard/KeyObservation.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 153 | 154 | 220 | -------------------------------------------------------------------------------- /src/components/keyboard/KeySight.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 171 | 172 | 238 | -------------------------------------------------------------------------------- /src/components/keyboard/KeySteeringWheel.vue: -------------------------------------------------------------------------------- 1 | 93 | 94 | 214 | 215 | 282 | -------------------------------------------------------------------------------- /src/components/keyboard/KeySwipe.vue: -------------------------------------------------------------------------------- 1 | 149 | 150 | 248 | 249 | 356 | -------------------------------------------------------------------------------- /src/components/setting/About.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 86 | 87 | 96 | -------------------------------------------------------------------------------- /src/components/setting/Basic.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 103 | 104 | 109 | -------------------------------------------------------------------------------- /src/components/setting/Data.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 140 | 141 | 150 | -------------------------------------------------------------------------------- /src/components/setting/Mask.vue: -------------------------------------------------------------------------------- 1 | 165 | 166 | 295 | 296 | 297 | -------------------------------------------------------------------------------- /src/components/setting/Setting.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 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 | --------------------------------------------------------------------------------