Scrcpy Mask v0.4.0 (#31)

* feat(adb): remove adb resource

* feat(scrcpy-mask-server): use custom scrcpy-server

* feat(Device): remove screen size input

* feat(hotkey): add resize listener

* Scrcpy Mask v0.4.0
This commit is contained in:
如初 2024-05-21 21:04:53 +08:00 committed by GitHub
parent ca01405881
commit e5f27bc00c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 259 additions and 286 deletions

View File

@ -1,5 +1,4 @@
name: "publish"
name: "Multi platform compile"
on:
push:
# 匹配特定标签 (refs/tags)

View File

@ -2,7 +2,13 @@
为了实现电脑控制安卓设备,本人使用 Tarui + Vue 3 + Rust 开发了一款跨平台桌面客户端。该客户端能够提供可视化的鼠标和键盘按键映射配置。通过按键映射实现了实现类似安卓模拟器的多点触控操作,具有毫秒级响应速度。该工具可广泛用于电脑控制安卓设备玩手游等等,提供流畅的触控体验。
本项目仅实现了 Scrcpy 控制协议,**不提供投屏功能**。因为投屏会存在延迟和模糊问题,本项目另辟蹊径,直接放弃投屏,而使用透明的蒙版显示窗口背后的内容(可以使用 AVD 、手机厂商提供的低延迟投屏等),从根本上杜绝了 Scrcpy 的投屏体验差的问题。
本人对 Scrcpy 项目的开发者表示深深的敬意和感谢。Scrcpy 是一个强大而高效的开源工具,极大地方便了对 Android 设备的控制。本项目的实现基于 Scrcpy 的优秀架构,进行了鼠标键盘控制的优化和调整。
**本项目不提供投屏功能,不提供投屏功能,不提供投屏功能!**本项目仅实现了 Scrcpy 的控制协议。
原因是投屏会存在延迟和模糊问题,本项目另辟蹊径,直接放弃投屏,而使用透明的蒙版显示窗口背后的内容(可以使用 AVD 、手机厂商提供的低延迟投屏等),从根本上杜绝了 Scrcpy 的投屏体验差的问题。
除此之外,为了更好的支持 Scrcpy Mask 与安卓设备交互,本人对 scrcpy-server 进行了一些修改,在此扩展出了一个分支项目 [scrcpy-mask-server](https://github.com/AkiChase/scrcpy-mask-server)
## 特性
@ -22,7 +28,7 @@
## 视频演示
- [如何用电脑玩FPS手游这样的“安卓模拟器”也不是不可以-哔哩哔哩](https://www.bilibili.com/video/BV1EU411Z7TC/?share_source=copy_web&vd_source=36923115230d8a46ae8b587fc5348e6e)
- [如何用电脑玩 FPS 手游?这样的“安卓模拟器”,也不是不可以-哔哩哔哩](https://www.bilibili.com/video/BV1EU411Z7TC/?share_source=copy_web&vd_source=36923115230d8a46ae8b587fc5348e6e)
- [M 系列 Mac 电脑玩王者,暃排位实录,使用 Android Stuido 模拟器和开源 Scrcpy Mask 按键映射工具-哔哩哔哩](https://b23.tv/q6iDW1w)
- [自制跨平台开源项目 Scrcpy Mask ,像模拟器一样用键鼠控制任意安卓设备!以 M 系列芯片 MacBook 打王者为例-哔哩哔哩](https://b23.tv/gqmriXr)
- [如何用 PC 控制安卓手机打王者?只要思想不滑坡,办法总比困难多!-哔哩哔哩](https://b23.tv/dmUOpff)

View File

@ -4,7 +4,13 @@
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.
This project only implements the Scrcpy control protocol and **does not provide Screen mirroring**. 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.
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.
**This project does not provide screen mirroring functionality—let me emphasize, it does not provide screen mirroring functionality!** It only implements the Scrcpy control protocol.
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.
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).
## Features

View File

@ -1,7 +1,7 @@
{
"name": "scrcpy-mask",
"private": true,
"version": "0.3.1",
"version": "0.4.0",
"type": "module",
"scripts": {
"dev": "vite",

View File

@ -1,6 +1,6 @@
[package]
name = "scrcpy-mask"
version = "0.3.1"
version = "0.4.0"
description = "A Tauri App"
authors = ["AkiChase"]
edition = "2021"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,7 +1,5 @@
use crate::resource::{ResHelper, ResourceName};
use std::{
io::BufRead,
path::PathBuf,
process::{Child, Command, Stdio},
};
@ -18,8 +16,8 @@ pub struct Device {
impl Device {
/// execute "adb push" to push file from src to des
pub fn cmd_push(res_dir: &PathBuf, id: &str, src: &str, des: &str) -> Result<String> {
let mut adb_command = Adb::cmd_base(res_dir);
pub fn cmd_push(id: &str, src: &str, des: &str) -> Result<String> {
let mut adb_command = Adb::cmd_base();
let res = adb_command
.args(&["-s", id, "push", src, des])
.output()
@ -28,8 +26,8 @@ impl Device {
}
/// execute "adb reverse" to reverse the device port to local port
pub fn cmd_reverse(res_dir: &PathBuf, id: &str, remote: &str, local: &str) -> Result<()> {
let mut adb_command = Adb::cmd_base(res_dir);
pub fn cmd_reverse(id: &str, remote: &str, local: &str) -> Result<()> {
let mut adb_command = Adb::cmd_base();
adb_command
.args(&["-s", id, "reverse", remote, local])
.output()
@ -38,8 +36,8 @@ impl Device {
}
/// execute "adb forward" to forward the local port to the device
pub fn cmd_forward(res_dir: &PathBuf, id: &str, local: &str, remote: &str) -> Result<()> {
let mut adb_command = Adb::cmd_base(res_dir);
pub fn cmd_forward(id: &str, local: &str, remote: &str) -> Result<()> {
let mut adb_command = Adb::cmd_base();
adb_command
.args(&["-s", id, "forward", local, remote])
.output()
@ -48,8 +46,8 @@ impl Device {
}
/// execute "adb shell" to execute shell command on the device
pub fn cmd_shell(res_dir: &PathBuf, id: &str, shell_args: &[&str]) -> Result<Child> {
let mut adb_command = Adb::cmd_base(res_dir);
pub fn cmd_shell(id: &str, shell_args: &[&str]) -> Result<Child> {
let mut adb_command = Adb::cmd_base();
let mut args = vec!["-s", id, "shell"];
args.extend_from_slice(shell_args);
Ok(adb_command
@ -60,8 +58,8 @@ impl Device {
}
/// execute "adb shell wm size" to get screen size
pub fn cmd_screen_size(res_dir: &PathBuf, id: &str) -> Result<(u32, u32)> {
let mut adb_command = Adb::cmd_base(res_dir);
pub fn cmd_screen_size(id: &str) -> Result<(u32, u32)> {
let mut adb_command = Adb::cmd_base();
let output = adb_command
.args(&["-s", id, "shell", "wm", "size"])
.output()
@ -86,22 +84,19 @@ pub struct Adb;
/// Module to execute adb command and fetch output.
/// But some output of command won't be output, like adb service startup information.
impl Adb {
fn cmd_base(res_dir: &PathBuf) -> Command {
pub fn cmd_base() -> Command {
#[cfg(target_os = "windows")]
{
let mut cmd = Command::new(ResHelper::get_file_path(res_dir, ResourceName::Adb));
let mut cmd = Command::new("adb");
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
cmd
}
#[cfg(not(target_os = "windows"))]
{
Command::new(ResHelper::get_file_path(res_dir, ResourceName::Adb))
return cmd;
}
Command::new("adb")
}
/// execute "adb devices" and return devices list
pub fn cmd_devices(res_dir: &PathBuf) -> Result<Vec<Device>> {
let mut adb_command = Adb::cmd_base(res_dir);
pub fn cmd_devices() -> Result<Vec<Device>> {
let mut adb_command = Adb::cmd_base();
let output = adb_command
.args(&["devices"])
.output()
@ -128,8 +123,8 @@ impl Adb {
}
/// execute "adb kill-server"
pub fn cmd_kill_server(res_dir: &PathBuf) -> Result<()> {
let mut adb_command = Adb::cmd_base(res_dir);
pub fn cmd_kill_server() -> Result<()> {
let mut adb_command = Adb::cmd_base();
adb_command
.args(&["kill-server"])
.output()
@ -138,8 +133,8 @@ impl Adb {
}
/// execute "adb reverse --remove-all"
pub fn cmd_reverse_remove(res_dir: &PathBuf) -> Result<()> {
let mut adb_command = Adb::cmd_base(res_dir);
pub fn cmd_reverse_remove() -> Result<()> {
let mut adb_command = Adb::cmd_base();
adb_command
.args(&["reverse", " --remove-all"])
.output()
@ -148,8 +143,8 @@ impl Adb {
}
/// execute "adb forward --remove-all"
pub fn cmd_forward_remove(res_dir: &PathBuf) -> Result<()> {
let mut adb_command = Adb::cmd_base(res_dir);
pub fn cmd_forward_remove() -> Result<()> {
let mut adb_command = Adb::cmd_base();
adb_command
.args(&["forward", " --remove-all"])
.output()
@ -158,8 +153,8 @@ impl Adb {
}
/// execute "adb start-server"
pub fn cmd_start_server(res_dir: &PathBuf) -> Result<()> {
let mut adb_command = Adb::cmd_base(res_dir);
pub fn cmd_start_server() -> Result<()> {
let mut adb_command = Adb::cmd_base();
adb_command
.args(&["start-server"])
.output()
@ -167,8 +162,8 @@ impl Adb {
Ok(())
}
pub fn cmd_connect(res_dir: &PathBuf, address: &str) -> Result<String> {
let mut adb_command = Adb::cmd_base(res_dir);
pub fn cmd_connect(address: &str) -> Result<String> {
let mut adb_command = Adb::cmd_base();
let output = adb_command
.args(&["connect", address])
.output()
@ -178,10 +173,3 @@ impl Adb {
Ok(res)
}
}
#[test]
fn t() {
let res_dir = PathBuf::from("/Users/akichase/Projects/github/scrcpy-mask/src-tauri/resource/");
let res = Adb::cmd_connect(&res_dir, "127.0.0.1:1234").unwrap();
println!("{}", res)
}

View File

@ -3,7 +3,8 @@ use std::{io::BufRead, path::PathBuf};
use crate::{
adb::{Adb, Device},
resource::{ResHelper, ResourceName}, share,
resource::{ResHelper, ResourceName},
share,
};
/**
@ -22,35 +23,34 @@ impl ScrcpyClient {
ResHelper::get_scrcpy_version()
}
pub fn adb_devices(res_dir: &PathBuf) -> Result<Vec<Device>> {
Adb::cmd_devices(res_dir)
pub fn adb_devices() -> Result<Vec<Device>> {
Adb::cmd_devices()
}
pub fn adb_restart_server(res_dir: &PathBuf) -> Result<()> {
Adb::cmd_kill_server(res_dir)?;
Adb::cmd_start_server(res_dir)?;
pub fn adb_restart_server() -> Result<()> {
Adb::cmd_kill_server()?;
Adb::cmd_start_server()?;
Ok(())
}
pub fn adb_reverse_remove(res_dir: &PathBuf) -> Result<()> {
Adb::cmd_reverse_remove(res_dir)
pub fn adb_reverse_remove() -> Result<()> {
Adb::cmd_reverse_remove()
}
pub fn adb_forward_remove(res_dir: &PathBuf) -> Result<()> {
Adb::cmd_forward_remove(res_dir)
pub fn adb_forward_remove() -> Result<()> {
Adb::cmd_forward_remove()
}
// get the screen size of the device
pub fn get_device_screen_size(res_dir: &PathBuf, id: &str) -> Result<(u32, u32)> {
Device::cmd_screen_size(res_dir, id)
pub fn get_device_screen_size(id: &str) -> Result<(u32, u32)> {
Device::cmd_screen_size(id)
}
/// push server file to current device
pub fn push_server_file(res_dir: &PathBuf, id: &str) -> Result<()> {
pub fn push_server_file(dir: &PathBuf, id: &str) -> Result<()> {
let info = Device::cmd_push(
res_dir,
id,
&ResHelper::get_file_path(res_dir, ResourceName::ScrcpyServer).to_string_lossy(),
&ResHelper::get_file_path(dir, ResourceName::ScrcpyServer).to_string_lossy(),
"/data/local/tmp/scrcpy-server.jar",
)?;
@ -59,9 +59,8 @@ impl ScrcpyClient {
}
/// forward the local port to the device
pub fn forward_server_port(res_dir: &PathBuf, id: &str, scid: &str, port: u16) -> Result<()> {
pub fn forward_server_port(id: &str, scid: &str, port: u16) -> Result<()> {
Device::cmd_forward(
res_dir,
id,
&format!("tcp:{}", port),
&format!("localabstract:scrcpy_{}", scid),
@ -71,9 +70,8 @@ impl ScrcpyClient {
}
/// reverse the device port to the local port
pub fn reverse_server_port(res_dir: &PathBuf, id: &str, scid: &str, port: u16) -> Result<()> {
pub fn reverse_server_port(id: &str, scid: &str, port: u16) -> Result<()> {
Device::cmd_reverse(
res_dir,
id,
&format!("localabstract:scrcpy_{}", scid),
&format!("tcp:{}", port),
@ -83,14 +81,8 @@ impl ScrcpyClient {
}
/// spawn a new thread to start scrcpy server
pub fn shell_start_server(
res_dir: &PathBuf,
id: &str,
scid: &str,
version: &str,
) -> Result<()> {
pub fn shell_start_server(id: &str, scid: &str, version: &str) -> Result<()> {
let mut child = Device::cmd_shell(
res_dir,
id,
&[
"CLASSPATH=/data/local/tmp/scrcpy-server.jar",
@ -119,7 +111,7 @@ impl ScrcpyClient {
// clear string to store new line only
s.clear();
}
*share::CLIENT_INFO.lock().unwrap() = None;
println!("Scrcpy server closed");
Ok(())

View File

@ -13,9 +13,8 @@ use tauri::Manager;
#[tauri::command]
/// get devices info list
fn adb_devices(app: tauri::AppHandle) -> Result<Vec<Device>, String> {
let dir = app.path().resource_dir().unwrap().join("resource");
match Adb::cmd_devices(&dir) {
fn adb_devices() -> Result<Vec<Device>, String> {
match Adb::cmd_devices() {
Ok(devices) => Ok(devices),
Err(e) => Err(e.to_string()),
}
@ -23,15 +22,8 @@ fn adb_devices(app: tauri::AppHandle) -> Result<Vec<Device>, String> {
#[tauri::command]
/// forward local port to the device port
fn forward_server_port(
app: tauri::AppHandle,
id: String,
scid: String,
port: u16,
) -> Result<(), String> {
let dir = app.path().resource_dir().unwrap().join("resource");
match ScrcpyClient::forward_server_port(&dir, &id, &scid, port) {
fn forward_server_port(id: String, scid: String, port: u16) -> Result<(), String> {
match ScrcpyClient::forward_server_port(&id, &scid, port) {
Ok(_) => Ok(()),
Err(e) => Err(e.to_string()),
}
@ -66,12 +58,11 @@ fn start_scrcpy_server(
scid.clone(),
));
let dir = app.path().resource_dir().unwrap().join("resource");
let version = ScrcpyClient::get_scrcpy_version();
// start scrcpy server
tokio::spawn(async move {
ScrcpyClient::shell_start_server(&dir, &id, &scid, &version).unwrap();
ScrcpyClient::shell_start_server(&id, &scid, &version).unwrap();
});
// connect to scrcpy server
@ -131,9 +122,8 @@ fn get_cur_client_info() -> Result<Option<share::ClientInfo>, String> {
#[tauri::command]
/// get device screen size
fn get_device_screen_size(id: String, app: tauri::AppHandle) -> Result<(u32, u32), String> {
let dir = app.path().resource_dir().unwrap().join("resource");
match ScrcpyClient::get_device_screen_size(&dir, &id) {
fn get_device_screen_size(id: String) -> Result<(u32, u32), String> {
match ScrcpyClient::get_device_screen_size(&id) {
Ok(size) => Ok(size),
Err(e) => Err(e.to_string()),
}
@ -141,9 +131,8 @@ fn get_device_screen_size(id: String, app: tauri::AppHandle) -> Result<(u32, u32
#[tauri::command]
/// connect to wireless device
fn adb_connect(address: String, app: tauri::AppHandle) -> Result<String, String> {
let dir = app.path().resource_dir().unwrap().join("resource");
match Adb::cmd_connect(&dir, &address) {
fn adb_connect(address: String) -> Result<String, String> {
match Adb::cmd_connect(&address) {
Ok(res) => Ok(res),
Err(e) => Err(e.to_string()),
}
@ -160,6 +149,14 @@ fn load_default_keyconfig(app: tauri::AppHandle) -> Result<String, String> {
}
}
#[tauri::command]
fn check_adb_available() -> Result<(), String> {
match Adb::cmd_base().output() {
Ok(_) => Ok(()),
Err(e) => Err(e.to_string()),
}
}
#[tokio::main]
async fn main() {
tauri::Builder::default()
@ -241,7 +238,8 @@ async fn main() {
get_cur_client_info,
get_device_screen_size,
adb_connect,
load_default_keyconfig
load_default_keyconfig,
check_adb_available
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@ -2,7 +2,6 @@ use anyhow::{anyhow, Ok, Result};
use std::path::PathBuf;
pub enum ResourceName {
Adb,
ScrcpyServer,
DefaultKeyConfig,
}
@ -13,7 +12,9 @@ pub struct ResHelper {
impl ResHelper {
pub fn res_init(res_dir: &PathBuf) -> Result<()> {
for name in [ResourceName::Adb, ResourceName::ScrcpyServer] {
let res = [ResourceName::ScrcpyServer, ResourceName::DefaultKeyConfig];
for name in res {
let file_path = ResHelper::get_file_path(res_dir, name);
if !file_path.exists() {
return Err(anyhow!(format!(
@ -27,14 +28,7 @@ impl ResHelper {
}
pub fn get_file_path(dir: &PathBuf, file_name: ResourceName) -> PathBuf {
match file_name {
#[cfg(target_os = "windows")]
ResourceName::Adb => dir.join("adb-win.exe"),
#[cfg(target_os = "linux")]
ResourceName::Adb => dir.join("adb-linux"),
#[cfg(target_os = "macos")]
ResourceName::Adb => dir.join("adb-mac"),
ResourceName::ScrcpyServer => dir.join("scrcpy-server-v2.4"),
ResourceName::ScrcpyServer => dir.join("scrcpy-mask-server-v2.4"),
ResourceName::DefaultKeyConfig => dir.join("default-key-config.json"),
}
}

View File

@ -73,7 +73,12 @@ async fn read_socket(
}
let device_name = std::str::from_utf8(&buf[..end]).unwrap();
// update device name for share
share::CLIENT_INFO.lock().unwrap().as_mut().unwrap().device_name = device_name.to_string();
share::CLIENT_INFO
.lock()
.unwrap()
.as_mut()
.unwrap()
.device_name = device_name.to_string();
let msg = json!({
"type": "MetaData",
@ -149,6 +154,20 @@ async fn handle_device_message(
let mut buf: Vec<u8> = vec![0; size as usize];
reader.read_exact(&mut buf).await?;
}
// 设备旋转
DeviceMsgType::DeviceMsgTypeRotation => {
let rotation = reader.read_u16().await?;
let width = reader.read_i32().await?;
let height = reader.read_i32().await?;
let msg = json!({
"type": "DeviceRotation",
"rotation": rotation,
"width": width,
"height": height
})
.to_string();
device_reply_sender.send(msg).await?;
}
};
anyhow::Ok(())
}
@ -181,6 +200,8 @@ async fn recv_front_msg(
// 处理Scrcpy Mask命令
if let Some(cmd_type) = ScrcpyMaskCmdType::from_i64(front_msg_type) {
if let ScrcpyMaskCmdType::Shutdown = cmd_type {
*share::CLIENT_INFO.lock().unwrap() = None;
drop(write_half);
println!("Drop TcpStream writer");
app.unlisten(listen_handler);
@ -197,7 +218,7 @@ async fn recv_front_msg(
}
}
} else {
eprintln!("fc-command非法");
eprintln!("fc-command invalid!");
eprintln!("{:?}", payload);
}
}
@ -212,6 +233,7 @@ enum DeviceMsgType {
DeviceMsgTypeClipboard,
DeviceMsgTypeAckClipboard,
DeviceMsgTypeUhidOutput,
DeviceMsgTypeRotation,
}
impl DeviceMsgType {
@ -220,6 +242,7 @@ impl DeviceMsgType {
0 => Some(Self::DeviceMsgTypeClipboard),
1 => Some(Self::DeviceMsgTypeAckClipboard),
2 => Some(Self::DeviceMsgTypeUhidOutput),
3 => Some(Self::DeviceMsgTypeRotation),
_ => None,
}
}

View File

@ -1,6 +1,6 @@
{
"productName": "scrcpy-mask",
"version": "0.3.1",
"version": "0.4.0",
"identifier": "com.akichase.mask",
"build": {
"beforeDevCommand": "pnpm dev",
@ -30,6 +30,10 @@
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"resources": [
"resource/default-key-config.json",
"resource/scrcpy-mask-server-v2.4"
]
}
}

View File

@ -1,9 +0,0 @@
{
"bundle": {
"resources": [
"resource/default-key-config.json",
"resource/scrcpy-server-v2.4",
"resource/adb-linux"
]
}
}

View File

@ -1,9 +0,0 @@
{
"bundle": {
"resources": [
"resource/default-key-config.json",
"resource/scrcpy-server-v2.4",
"resource/adb-mac"
]
}
}

View File

@ -1,11 +0,0 @@
{
"bundle": {
"resources": [
"resource/default-key-config.json",
"resource/scrcpy-server-v2.4",
"resource/adb-win.exe",
"resource/AdbWinApi.dll",
"resource/AdbWinUsbApi.dll"
]
}
}

View File

@ -21,7 +21,6 @@ import {
} from "../invoke";
import {
NH4,
NP,
NInput,
NInputNumber,
NButton,
@ -30,7 +29,6 @@ import {
NEmpty,
NTooltip,
NFlex,
NFormItem,
NIcon,
NSpin,
NScrollbar,
@ -42,7 +40,6 @@ import {
} from "naive-ui";
import { CloseCircle, InformationCircle, Refresh } from "@vicons/ionicons5";
import { UnlistenFn, listen } from "@tauri-apps/api/event";
import { Store } from "@tauri-apps/plugin-store";
import { shutdown } from "../frontcommand/scrcpyMaskCmd";
import { useGlobalStore } from "../store/global";
import { useI18n } from "vue-i18n";
@ -57,10 +54,9 @@ const port = ref(27183);
const wireless_address = ref("");
const ws_address = ref("");
const localStore = new Store("store.bin");
//#region listener
let deviceWaitForMetadataTask: ((deviceName: string) => void) | null = null;
let deviceWaitForScreenSizeTask: ((w: number, h: number) => void) | null = null;
let unlisten: UnlistenFn | undefined;
onMounted(async () => {
@ -77,6 +73,15 @@ onMounted(async () => {
case "ClipboardSetAck":
console.log("ClipboardSetAck", payload.sequence);
break;
case "DeviceRotation":
if (deviceWaitForScreenSizeTask) {
deviceWaitForScreenSizeTask(payload.width, payload.height);
} else {
store.screenSizeW = payload.width;
store.screenSizeH = payload.height;
message.info("设备旋转");
}
break;
default:
console.log("Unknown reply", payload);
break;
@ -216,24 +221,12 @@ async function deviceControl() {
port.value = 27183;
}
if (!(store.screenSizeW > 0) || !(store.screenSizeH > 0)) {
message.error(t("pages.Device.deviceControl.inputScreenSize"));
store.screenSizeW = 0;
store.screenSizeH = 0;
store.hideLoading();
return;
}
if (store.controledDevice) {
message.error(t("pages.Device.deviceControl.closeCurDevice"));
store.hideLoading();
return;
}
localStore.set("screenSize", {
sizeW: store.screenSizeW,
sizeH: store.screenSizeH,
});
message.info(t("pages.Device.deviceControl.controlInfo"));
const device = devices.value[rowIndex];
@ -263,11 +256,26 @@ async function deviceControl() {
deviceName,
deviceID: device.id,
};
nextTick(() => {
deviceWaitForMetadataTask = null;
clearTimeout(id);
store.hideLoading();
});
deviceWaitForMetadataTask = null;
if (!deviceWaitForScreenSizeTask) {
nextTick(() => {
clearTimeout(id);
store.hideLoading();
});
}
};
// add cb for screen size
deviceWaitForScreenSizeTask = (w: number, h: number) => {
store.screenSizeW = w;
store.screenSizeH = h;
deviceWaitForScreenSizeTask = null;
if (!deviceWaitForMetadataTask) {
nextTick(() => {
clearTimeout(id);
store.hideLoading();
});
}
};
}
@ -296,7 +304,12 @@ async function onMenuSelect(key: string) {
async function refreshDevices() {
store.showLoading();
devices.value = await adbDevices();
try {
devices.value = await adbDevices();
} catch (e) {
message.error(t("pages.Device.adbDeviceError"));
console.error(e);
}
store.hideLoading();
}
@ -368,26 +381,6 @@ function closeWS() {
$t("pages.Device.wsConnect")
}}</NButton>
</NInputGroup>
<NH4 prefix="bar">{{ $t("pages.Device.deviceSize.title") }}</NH4>
<NFlex justify="left" align="center">
<NFormItem :label="$t('pages.Device.deviceSize.width')">
<NInputNumber
v-model:value="store.screenSizeW"
:placeholder="$t('pages.Device.deviceSize.widthPlaceholder')"
:min="0"
:disabled="store.controledDevice !== null"
/>
</NFormItem>
<NFormItem :label="$t('pages.Device.deviceSize.height')">
<NInputNumber
v-model:value="store.screenSizeH"
:placeholder="$t('pages.Device.deviceSize.heightPlaceholder')"
:min="0"
:disabled="store.controledDevice !== null"
/>
</NFormItem>
</NFlex>
<NP>{{ $t("pages.Device.deviceSize.tip") }}</NP>
<NH4 prefix="bar">{{ $t("pages.Device.controledDevice") }}</NH4>
<div class="controled-device-list">
<NEmpty

View File

@ -8,7 +8,6 @@ import {
clearShortcuts,
listenToEvent,
unlistenToEvent,
updateScreenSizeAndMaskArea,
} from "../hotkey";
import { KeyMappingConfig, KeySteeringWheel } from "../keyMappingConfig";
import { getVersion } from "@tauri-apps/api/app";
@ -20,6 +19,7 @@ import { AndroidKeycode } from "../frontcommand/android";
import { Store } from "@tauri-apps/plugin-store";
import { useI18n } from "vue-i18n";
import { SendKeyAction, sendKey } from "../frontcommand/scrcpyMaskCmd";
import { checkAdbAvailable } from "../invoke";
const { t } = useI18n();
const store = useGlobalStore();
@ -43,15 +43,11 @@ onActivated(async () => {
const maskElement = document.getElementById("maskElement") as HTMLElement;
if (store.controledDevice) {
updateScreenSizeAndMaskArea(
[store.screenSizeW, store.screenSizeH],
[maskElement.clientWidth, maskElement.clientHeight]
);
if (
applyShortcuts(
maskElement,
store.keyMappingConfigList[store.curKeyMappingIndex],
store,
message,
t
)
@ -64,23 +60,25 @@ onActivated(async () => {
});
onMounted(async () => {
await checkAdb();
await loadLocalStore();
store.checkUpdate = checkUpdate;
store.showInputBox = showInputBox;
if (store.checkUpdateAtStart) checkUpdate();
});
async function checkAdb() {
try {
await checkAdbAvailable();
} catch (e) {
message.error(t("pages.Mask.checkAdb", [e]), {
duration: 0,
});
}
}
async function loadLocalStore() {
const localStore = new Store("store.bin");
// loading screenSize from local store
const screenSize = await localStore.get<{ sizeW: number; sizeH: number }>(
"screenSize"
);
if (screenSize !== null) {
store.screenSizeW = screenSize.sizeW;
store.screenSizeH = screenSize.sizeH;
}
// loading keyMappingConfigList from local store
let keyMappingConfigList = await localStore.get<KeyMappingConfig[]>(
"keyMappingConfigList"

View File

@ -24,17 +24,18 @@ import {
import { useGlobalStore } from "./store/global";
import { LogicalPosition, getCurrent } from "@tauri-apps/api/window";
import { useI18n } from "vue-i18n";
import { UnlistenFn } from "@tauri-apps/api/event";
function clientxToPosx(clientx: number) {
return clientx < 70
? 0
: Math.floor((clientx - 70) * (screenSizeW / maskSizeW));
: Math.floor((clientx - 70) * (store.screenSizeW / maskSizeW));
}
function clientyToPosy(clienty: number) {
return clienty < 30
? 0
: Math.floor((clienty - 30) * (screenSizeH / maskSizeH));
: Math.floor((clienty - 30) * (store.screenSizeH / maskSizeH));
}
function clientxToPosOffsetx(clientx: number, posx: number, scale = 1) {
@ -51,7 +52,7 @@ function clientPosToSkillOffset(
clientPos: { x: number; y: number },
range: number
): { offsetX: number; offsetY: number } {
const maxLength = (120 / maskSizeH) * screenSizeH;
const maxLength = (120 / maskSizeH) * store.screenSizeH;
const centerX = maskSizeW * 0.5;
const centerY = maskSizeH * 0.5;
@ -95,7 +96,7 @@ function calculateMacroPosX(
relativeSizeW: number
): number {
if (typeof posX === "number") {
return Math.round(posX * (screenSizeW / relativeSizeW));
return Math.round(posX * (store.screenSizeW / relativeSizeW));
}
if (typeof posX === "string") {
return clientxToPosx(mouseX);
@ -103,7 +104,7 @@ function calculateMacroPosX(
if (posX[0] === "mouse") {
return (
clientxToPosx(mouseX) +
Math.round(posX[1] * (screenSizeW / relativeSizeW))
Math.round(posX[1] * (store.screenSizeW / relativeSizeW))
);
} else {
throw new Error("Invalid pos");
@ -117,7 +118,7 @@ function calculateMacroPosY(
relativeSizeH: number
): number {
if (typeof posY === "number") {
return Math.round(posY * (screenSizeH / relativeSizeH));
return Math.round(posY * (store.screenSizeH / relativeSizeH));
}
if (typeof posY === "string") {
return clientyToPosy(mouseY);
@ -125,7 +126,7 @@ function calculateMacroPosY(
if (posY[0] === "mouse") {
return (
clientyToPosy(mouseY) +
Math.round(posY[1] * (screenSizeH / relativeSizeH))
Math.round(posY[1] * (store.screenSizeH / relativeSizeH))
);
} else {
throw new Error("Invalid pos");
@ -157,8 +158,8 @@ function addObservationShortcuts(
) {
let observationMouseX = 0;
let observationMouseY = 0;
posX = Math.round((posX / relativeSize.w) * screenSizeW);
posY = Math.round((posY / relativeSize.h) * screenSizeH);
posX = Math.round((posX / relativeSize.w) * store.screenSizeW);
posY = Math.round((posY / relativeSize.h) * store.screenSizeH);
addShortcut(
key,
async () => {
@ -195,8 +196,8 @@ function addTapShortcuts(
posY: number,
pointerId: number
) {
posX = Math.round((posX / relativeSize.w) * screenSizeW);
posY = Math.round((posY / relativeSize.h) * screenSizeH);
posX = Math.round((posX / relativeSize.w) * store.screenSizeW);
posY = Math.round((posY / relativeSize.h) * store.screenSizeH);
addShortcut(
key,
async () => {
@ -216,8 +217,8 @@ function addCancelSkillShortcuts(
posY: number,
pointerId: number
) {
posX = Math.round((posX / relativeSize.w) * screenSizeW);
posY = Math.round((posY / relativeSize.h) * screenSizeH);
posX = Math.round((posX / relativeSize.w) * store.screenSizeW);
posY = Math.round((posY / relativeSize.h) * store.screenSizeH);
addShortcut(
key,
async () => {
@ -259,8 +260,8 @@ function addTriggerWhenPressedSkillShortcuts(
rangeOrTime: number,
pointerId: number
) {
posX = Math.round((posX / relativeSize.w) * screenSizeW);
posY = Math.round((posY / relativeSize.h) * screenSizeH);
posX = Math.round((posX / relativeSize.w) * store.screenSizeW);
posY = Math.round((posY / relativeSize.h) * store.screenSizeH);
if (directional) {
addShortcut(
key,
@ -276,8 +277,8 @@ function addTriggerWhenPressedSkillShortcuts(
action: SwipeAction.Default,
pointerId,
screen: {
w: screenSizeW,
h: screenSizeH,
w: store.screenSizeW,
h: store.screenSizeH,
},
pos: [
{ x: posX, y: posY },
@ -316,8 +317,8 @@ function addTriggerWhenDoublePressedSkillShortcuts(
range: number,
pointerId: number
) {
posX = Math.round((posX / relativeSize.w) * screenSizeW);
posY = Math.round((posY / relativeSize.h) * screenSizeH);
posX = Math.round((posX / relativeSize.w) * store.screenSizeW);
posY = Math.round((posY / relativeSize.h) * store.screenSizeH);
doublePressedDownKey.set(key, false);
addShortcut(
key,
@ -333,8 +334,8 @@ function addTriggerWhenDoublePressedSkillShortcuts(
action: SwipeAction.NoUp,
pointerId,
screen: {
w: screenSizeW,
h: screenSizeH,
w: store.screenSizeW,
h: store.screenSizeH,
},
pos: [
{ x: posX, y: posY },
@ -392,8 +393,8 @@ function addDirectionlessSkillShortcuts(
posY: number,
pointerId: number
) {
posX = Math.round((posX / relativeSize.w) * screenSizeW);
posY = Math.round((posY / relativeSize.h) * screenSizeH);
posX = Math.round((posX / relativeSize.w) * store.screenSizeW);
posY = Math.round((posY / relativeSize.h) * store.screenSizeH);
addShortcut(
key,
// down
@ -410,8 +411,8 @@ function addDirectionlessSkillShortcuts(
action: TouchAction.Up,
pointerId,
screen: {
w: screenSizeW,
h: screenSizeH,
w: store.screenSizeW,
h: store.screenSizeH,
},
pos: {
x: posX,
@ -442,8 +443,8 @@ function addDirectionalSkillShortcuts(
range: number,
pointerId: number
) {
posX = Math.round((posX / relativeSize.w) * screenSizeW);
posY = Math.round((posY / relativeSize.h) * screenSizeH);
posX = Math.round((posX / relativeSize.w) * store.screenSizeW);
posY = Math.round((posY / relativeSize.h) * store.screenSizeH);
addShortcut(
key,
// down
@ -458,8 +459,8 @@ function addDirectionalSkillShortcuts(
action: SwipeAction.NoUp,
pointerId,
screen: {
w: screenSizeW,
h: screenSizeH,
w: store.screenSizeW,
h: store.screenSizeH,
},
pos: [
{ x: posX, y: posY },
@ -514,8 +515,8 @@ function addSteeringWheelKeyboardShortcuts(
let loopFlag = false;
let curPosX = 0;
let curPosY = 0;
posX = Math.round((posX / relativeSize.w) * screenSizeW);
posY = Math.round((posY / relativeSize.h) * screenSizeH);
posX = Math.round((posX / relativeSize.w) * store.screenSizeW);
posY = Math.round((posY / relativeSize.h) * store.screenSizeH);
// calculate the end coordinates of the eight directions of the direction wheel
let offsetHalf = Math.round(offset / 1.414);
@ -657,18 +658,18 @@ function addSightShortcuts(
const sightClientX = 70 + sightKeyMapping.posX;
const sightClientY = 30 + sightKeyMapping.posY;
const sightDeviceX = Math.round(
(sightKeyMapping.posX / relativeSize.w) * screenSizeW
(sightKeyMapping.posX / relativeSize.w) * store.screenSizeW
);
const sightDeviceY = Math.round(
(sightKeyMapping.posY / relativeSize.h) * screenSizeH
(sightKeyMapping.posY / relativeSize.h) * store.screenSizeH
);
const fireDeviceX = fireKeyMapping
? Math.round((fireKeyMapping.posX / relativeSize.w) * screenSizeW)
? Math.round((fireKeyMapping.posX / relativeSize.w) * store.screenSizeW)
: 0;
const fireDeviceY = fireKeyMapping
? Math.round((fireKeyMapping.posY / relativeSize.h) * screenSizeH)
? Math.round((fireKeyMapping.posY / relativeSize.h) * store.screenSizeH)
: 0;
const removeShortcut = (key: string) => {
@ -715,7 +716,7 @@ function addSightShortcuts(
fireDeviceX +
accOffsetX +
clientxToPosOffsetx(mouseX, sightDeviceX, fireKeyMapping.scaleX),
fireDeviceY +
fireDeviceY +
accOffsetY +
clientyToPosOffsety(mouseY, sightDeviceY, fireKeyMapping.scaleY)
);
@ -892,7 +893,7 @@ function addSightShortcuts(
sightDeviceX,
fireKeyMapping.scaleX
),
fireDeviceY +
fireDeviceY +
clientyToPosOffsety(mouseY, sightDeviceY, fireKeyMapping.scaleY)
);
// touch down sight
@ -917,22 +918,6 @@ function addSightShortcuts(
});
}
let screenSizeW: number;
let screenSizeH: number;
let maskSizeW: number;
let maskSizeH: number;
let mouseX = 0;
let mouseY = 0;
let maskElement: HTMLElement;
let message: ReturnType<typeof useMessage>;
let t: ReturnType<typeof useI18n>["t"];
const downKeyMap: Map<string, boolean> = new Map();
const downKeyCBMap: Map<string, () => Promise<void>> = new Map();
const loopDownKeyCBMap: Map<string, () => Promise<void>> = new Map();
const upKeyCBMap: Map<string, () => Promise<void>> = new Map();
const cancelAbleKeyList: string[] = [];
function handleKeydown(event: KeyboardEvent) {
event.preventDefault();
if (event.repeat) return;
@ -1166,8 +1151,8 @@ async function execMacro(
action: swipeAction,
pointerId: cmd.args[1],
screen: {
w: screenSizeW,
h: screenSizeH,
w: store.screenSizeW,
h: store.screenSizeH,
},
pos: calculateMacroPosList(cmd.args[2], relativeSize),
intervalBetweenPos: cmd.args[3],
@ -1356,8 +1341,8 @@ async function touchX(
action,
pointerId,
screen: {
w: screenSizeW,
h: screenSizeH,
w: store.screenSizeW,
h: store.screenSizeH,
},
pos: {
x: posX,
@ -1395,28 +1380,51 @@ export function clearShortcuts() {
loopDownKeyCBMap.clear();
upKeyCBMap.clear();
cancelAbleKeyList.length = 0;
}
export function updateScreenSizeAndMaskArea(
screenSize: [number, number],
maskArea: [number, number]
) {
screenSizeW = screenSize[0];
screenSizeH = screenSize[1];
maskSizeW = maskArea[0];
maskSizeH = maskArea[1];
// unlisten to resize
unlistenResize();
}
export function applyShortcuts(
element: HTMLElement,
keyMappingConfig: KeyMappingConfig,
globalStore: ReturnType<typeof useGlobalStore>,
messageAPI: ReturnType<typeof useMessage>,
i18nT: ReturnType<typeof useI18n>["t"]
) {
store = globalStore;
maskElement = element;
message = messageAPI;
t = i18nT;
maskSizeW = maskElement.clientWidth;
maskSizeH = maskElement.clientHeight;
// listen to resize to update mask size
getCurrent()
.onResized(() => {
maskSizeW = maskElement.clientWidth;
maskSizeH = maskElement.clientHeight;
})
.then((f) => (unlistenResize = f));
addClickShortcuts("M0", 0);
return applyKeyMappingConfigShortcuts(keyMappingConfig);
}
let maskSizeW: number;
let maskSizeH: number;
let mouseX = 0;
let mouseY = 0;
let store: ReturnType<typeof useGlobalStore>;
let maskElement: HTMLElement;
let message: ReturnType<typeof useMessage>;
let t: ReturnType<typeof useI18n>["t"];
let unlistenResize: UnlistenFn;
const downKeyMap: Map<string, boolean> = new Map();
const downKeyCBMap: Map<string, () => Promise<void>> = new Map();
const loopDownKeyCBMap: Map<string, () => Promise<void>> = new Map();
const upKeyCBMap: Map<string, () => Promise<void>> = new Map();
const cancelAbleKeyList: string[] = [];

View File

@ -14,9 +14,8 @@
"screen": "Get screen size"
},
"deviceControl": {
"inputScreenSize": "Please enter the screen size of the current control device correctly",
"closeCurDevice": "Please close the current control device first",
"controlInfo": "The screen size has been saved and the control service is starting. Please keep the device screen on.",
"controlInfo": "The control service is starting. Please keep the device screen on.",
"connectTimeout": "Device connection timeout"
},
"deviceGetScreenSize": "Device screen size: ",
@ -25,14 +24,7 @@
"wireless": "Wireless connection",
"wirelessPlaceholder": "Wireless connection address",
"connect": "Connect",
"deviceSize": {
"title": "Device screen size",
"width": "Width",
"widthPlaceholder": "Screen width",
"height": "Height",
"heightPlaceholder": "Screen height",
"tip": "Tip: Please enter the screen size of the current control device correctly. This is a necessary parameter to successfully send touch events."
},
"deviceSize": {},
"controledDevice": "Controlled device",
"availableDevice": "Available devices",
"noControledDevice": "No Controled Device",
@ -42,7 +34,8 @@
"wsAddress": "Websocket address",
"inputWsAddress": "Please enter the Websocket address",
"wsClose": "Close",
"wsConnect": "Control"
"wsConnect": "Control",
"adbDeviceError": "Unable to get available devices"
},
"Mask": {
"inputBoxPlaceholder": "Input text and then press enter/esc",
@ -62,7 +55,8 @@
"content": "Please go to the device page to control any device",
"positiveText": "To control"
},
"sightMode": "Mouse is locked, press {0} to unlock"
"sightMode": "Mouse is locked, press {0} to unlock",
"checkAdb": "adb is not available and the software cannot run normally. Please ensure that adb is installed on the system and added to the Path environment variable correctly: {0}"
},
"Setting": {
"tabs": {

View File

@ -13,9 +13,8 @@
"screen": "获取屏幕尺寸"
},
"deviceControl": {
"inputScreenSize": "请正确输入当前控制设备的屏幕尺寸",
"closeCurDevice": "请先关闭当前控制设备",
"controlInfo": "屏幕尺寸已保存,正在启动控制服务,请保持设备亮屏",
"controlInfo": "正在启动控制服务,请保持设备亮屏",
"connectTimeout": "设备连接超时"
},
"deviceGetScreenSize": "设备屏幕尺寸为:",
@ -25,14 +24,7 @@
"wireless": "无线连接",
"wirelessPlaceholder": "无线连接地址",
"connect": "连接",
"deviceSize": {
"title": "设备屏幕尺寸",
"width": "宽度",
"widthPlaceholder": "屏幕宽度",
"heightPlaceholder": "屏幕高度",
"height": "高度",
"tip": "提示:请正确输入当前控制设备的屏幕尺寸,这是成功发送触摸事件的必要参数"
},
"deviceSize": {},
"controledDevice": "受控设备",
"noControledDevice": "无受控设备",
"availableDevice": "可用设备",
@ -42,7 +34,8 @@
"externalControl": "外部控制",
"wsAddress": "Websocket 地址",
"wsClose": "断开",
"wsConnect": "控制"
"wsConnect": "控制",
"adbDeviceError": "无法获取可用设备"
},
"Mask": {
"keyconfigException": "按键方案异常,请删除此方案",
@ -62,7 +55,8 @@
"positiveText": "去控制"
},
"inputBoxPlaceholder": "输入文本后按Enter/Esc",
"sightMode": "鼠标已锁定, 按 {0} 键解锁"
"sightMode": "鼠标已锁定, 按 {0} 键解锁",
"checkAdb": "adb不可用软件无法正常运行请确保系统已安装adb并正确添加到了Path环境变量中: {0}"
},
"Setting": {
"tabs": {

View File

@ -51,4 +51,8 @@ export async function loadDefaultKeyconfig(): Promise<string> {
return await invoke("load_default_keyconfig");
}
export async function checkAdbAvailable(): Promise<void>{
return await invoke("check_adb_available");
}
export type { Device };

View File

@ -71,9 +71,10 @@ export const useGlobalStore = defineStore("global", () => {
const externalControlled = ref(false);
// persistent storage
const screenSizeW: Ref<number> = ref(0);
const screenSizeH: Ref<number> = ref(0);
// persistent storage
const keyMappingConfigList: Ref<KeyMappingConfig[]> = ref([]);
const curKeyMappingIndex = ref(0);
const maskButton = ref({
@ -84,14 +85,14 @@ export const useGlobalStore = defineStore("global", () => {
return {
// persistent storage
screenSizeW,
screenSizeH,
keyMappingConfigList,
curKeyMappingIndex,
maskButton,
checkUpdateAtStart,
externalControlled,
// in-memory storage
screenSizeW,
screenSizeH,
showLoading,
hideLoading,
showLoadingRef,