mirror of
https://github.com/AkiChase/scrcpy-mask
synced 2025-02-22 23:12:16 +08:00
commit
067d4ee45b
@ -17,7 +17,7 @@
|
|||||||
- [x] 国际化
|
- [x] 国际化
|
||||||
- [ ] 手柄按键映射
|
- [ ] 手柄按键映射
|
||||||
- [ ] 更好的宏
|
- [ ] 更好的宏
|
||||||
- [ ] 通过 WebSocket 提供外部接口
|
- [x] 通过 WebSocket 提供外部控制,见[外部控制](https://github.com/AkiChase/scrcpy-mask-external-control)
|
||||||
- [ ] 帮助文档
|
- [ ] 帮助文档
|
||||||
|
|
||||||
## 视频演示
|
## 视频演示
|
||||||
@ -27,6 +27,12 @@
|
|||||||
- [如何用 PC 控制安卓手机打王者?只要思想不滑坡,办法总比困难多!-哔哩哔哩](https://b23.tv/dmUOpff)
|
- [如何用 PC 控制安卓手机打王者?只要思想不滑坡,办法总比困难多!-哔哩哔哩](https://b23.tv/dmUOpff)
|
||||||
- [M 芯片 Mac 怎么用 Android Studio 模拟器打王者?这是 Up 耗时数个月给出的答案-哔哩哔哩](https://b23.tv/ckJgyK5)
|
- [M 芯片 Mac 怎么用 Android Studio 模拟器打王者?这是 Up 耗时数个月给出的答案-哔哩哔哩](https://b23.tv/ckJgyK5)
|
||||||
|
|
||||||
|
## 实现原理
|
||||||
|
|
||||||
|
- [Scrcpy Mask 实现原理剖析,如何像模拟器一样用键鼠控制你的安卓设备?架构、通信篇 - 掘金](https://juejin.cn/post/7366799820734939199)
|
||||||
|
- [Scrcpy Mask 实现原理剖析,如何像模拟器一样用键鼠控制你的安卓设备?前端可视化、按键映射篇 - 掘金](https://juejin.cn/post/7367620233140748299)
|
||||||
|
- [Scrcpy Mask 实现原理剖析,如何在前端实现王者荣耀中技能的准确释放? - 掘金](https://juejin.cn/post/7367568884198047807)
|
||||||
|
|
||||||
## 截图
|
## 截图
|
||||||
|
|
||||||
- 设备控制
|
- 设备控制
|
||||||
|
@ -19,7 +19,7 @@ This project only implements the Scrcpy control protocol and **does not provide
|
|||||||
- [x] Internationalization (i18n)
|
- [x] Internationalization (i18n)
|
||||||
- [ ] Gamepad key mapping
|
- [ ] Gamepad key mapping
|
||||||
- [ ] Better macro support
|
- [ ] Better macro support
|
||||||
- [ ] Provide external interface through websocket
|
- [x] Provide external control through websocket, see [external control](https://github.com/AkiChase/scrcpy-mask-external-control)
|
||||||
- [ ] Help document
|
- [ ] Help document
|
||||||
|
|
||||||
## Demonstration video
|
## Demonstration video
|
||||||
@ -29,6 +29,12 @@ This project only implements the Scrcpy control protocol and **does not provide
|
|||||||
- [如何用 PC 控制安卓手机打王者?只要思想不滑坡,办法总比困难多!-哔哩哔哩](https://b23.tv/dmUOpff)
|
- [如何用 PC 控制安卓手机打王者?只要思想不滑坡,办法总比困难多!-哔哩哔哩](https://b23.tv/dmUOpff)
|
||||||
- [M 芯片 Mac 怎么用 Android Studio 模拟器打王者?这是 Up 耗时数个月给出的答案-哔哩哔哩](https://b23.tv/ckJgyK5)
|
- [M 芯片 Mac 怎么用 Android Studio 模拟器打王者?这是 Up 耗时数个月给出的答案-哔哩哔哩](https://b23.tv/ckJgyK5)
|
||||||
|
|
||||||
|
## Implementation principle
|
||||||
|
|
||||||
|
- [Scrcpy Mask 实现原理剖析,如何像模拟器一样用键鼠控制你的安卓设备?架构、通信篇 - 掘金](https://juejin.cn/post/7366799820734939199)
|
||||||
|
- [Scrcpy Mask 实现原理剖析,如何像模拟器一样用键鼠控制你的安卓设备?前端可视化、按键映射篇 - 掘金](https://juejin.cn/post/7367620233140748299)
|
||||||
|
- [Scrcpy Mask 实现原理剖析,如何在前端实现王者荣耀中技能的准确释放? - 掘金](https://juejin.cn/post/7367568884198047807)
|
||||||
|
|
||||||
## Screenshot
|
## Screenshot
|
||||||
|
|
||||||
- Device control
|
- Device control
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "scrcpy-mask",
|
"name": "scrcpy-mask",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "scrcpy-mask"
|
name = "scrcpy-mask"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["AkiChase"]
|
authors = ["AkiChase"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@ -16,6 +16,7 @@ tauri-plugin-store = "2.0.0-beta"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
tokio = { version = "1.36.0", features = ["rt-multi-thread", "net", "macros", "io-util", "time", "sync"] }
|
tokio = { version = "1.36.0", features = ["rt-multi-thread", "net", "macros", "io-util", "time", "sync"] }
|
||||||
tauri-plugin-process = "2.0.0-beta"
|
tauri-plugin-process = "2.0.0-beta"
|
||||||
tauri-plugin-shell = "2.0.0-beta"
|
tauri-plugin-shell = "2.0.0-beta"
|
||||||
|
@ -3,7 +3,7 @@ use std::{io::BufRead, path::PathBuf};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
adb::{Adb, Device},
|
adb::{Adb, Device},
|
||||||
resource::{ResHelper, ResourceName},
|
resource::{ResHelper, ResourceName}, share,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -119,6 +119,8 @@ impl ScrcpyClient {
|
|||||||
// clear string to store new line only
|
// clear string to store new line only
|
||||||
s.clear();
|
s.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*share::CLIENT_INFO.lock().unwrap() = None;
|
||||||
println!("Scrcpy server closed");
|
println!("Scrcpy server closed");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -5,3 +5,4 @@ pub mod control_msg;
|
|||||||
pub mod resource;
|
pub mod resource;
|
||||||
pub mod scrcpy_mask_cmd;
|
pub mod scrcpy_mask_cmd;
|
||||||
pub mod socket;
|
pub mod socket;
|
||||||
|
pub mod share;
|
||||||
|
@ -5,6 +5,7 @@ use scrcpy_mask::{
|
|||||||
adb::{Adb, Device},
|
adb::{Adb, Device},
|
||||||
client::ScrcpyClient,
|
client::ScrcpyClient,
|
||||||
resource::{ResHelper, ResourceName},
|
resource::{ResHelper, ResourceName},
|
||||||
|
share,
|
||||||
socket::connect_socket,
|
socket::connect_socket,
|
||||||
};
|
};
|
||||||
use std::{fs::read_to_string, sync::Arc};
|
use std::{fs::read_to_string, sync::Arc};
|
||||||
@ -54,6 +55,17 @@ fn start_scrcpy_server(
|
|||||||
address: String,
|
address: String,
|
||||||
app: tauri::AppHandle,
|
app: tauri::AppHandle,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
|
let mut client_info = share::CLIENT_INFO.lock().unwrap();
|
||||||
|
if let Some(_) = &*client_info {
|
||||||
|
return Err("client already exists".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
*client_info = Some(share::ClientInfo::new(
|
||||||
|
"unknow".to_string(),
|
||||||
|
id.clone(),
|
||||||
|
scid.clone(),
|
||||||
|
));
|
||||||
|
|
||||||
let dir = app.path().resource_dir().unwrap().join("resource");
|
let dir = app.path().resource_dir().unwrap().join("resource");
|
||||||
let version = ScrcpyClient::get_scrcpy_version();
|
let version = ScrcpyClient::get_scrcpy_version();
|
||||||
|
|
||||||
@ -108,6 +120,15 @@ fn start_scrcpy_server(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_cur_client_info() -> Result<Option<share::ClientInfo>, String> {
|
||||||
|
let client_info = share::CLIENT_INFO.lock().unwrap();
|
||||||
|
match &*client_info {
|
||||||
|
Some(client) => Ok(Some(client.clone())),
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
/// get device screen size
|
/// get device screen size
|
||||||
fn get_device_screen_size(id: String, app: tauri::AppHandle) -> Result<(u32, u32), String> {
|
fn get_device_screen_size(id: String, app: tauri::AppHandle) -> Result<(u32, u32), String> {
|
||||||
@ -217,6 +238,7 @@ async fn main() {
|
|||||||
forward_server_port,
|
forward_server_port,
|
||||||
push_server_file,
|
push_server_file,
|
||||||
start_scrcpy_server,
|
start_scrcpy_server,
|
||||||
|
get_cur_client_info,
|
||||||
get_device_screen_size,
|
get_device_screen_size,
|
||||||
adb_connect,
|
adb_connect,
|
||||||
load_default_keyconfig
|
load_default_keyconfig
|
||||||
|
@ -35,7 +35,7 @@ pub async fn handle_sm_cmd(
|
|||||||
// up
|
// up
|
||||||
let buf = gen_inject_key_ctrl_msg(
|
let buf = gen_inject_key_ctrl_msg(
|
||||||
ctrl_msg_type,
|
ctrl_msg_type,
|
||||||
0, // AKEY_EVENT_ACTION_DOWN
|
1, // AKEY_EVENT_ACTION_UP
|
||||||
keycode,
|
keycode,
|
||||||
0,
|
0,
|
||||||
metastate,
|
metastate,
|
||||||
|
23
src-tauri/src/share.rs
Normal file
23
src-tauri/src/share.rs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
use lazy_static::lazy_static;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
|
pub struct ClientInfo {
|
||||||
|
pub device_name: String,
|
||||||
|
pub device_id: String,
|
||||||
|
pub scid: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientInfo {
|
||||||
|
pub fn new(device_name: String, device_id: String, scid: String) -> Self {
|
||||||
|
Self {
|
||||||
|
device_name,
|
||||||
|
device_id,
|
||||||
|
scid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref CLIENT_INFO: Mutex<Option<ClientInfo>> = Mutex::new(None);
|
||||||
|
}
|
@ -13,6 +13,7 @@ use tokio::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
control_msg::{self, ControlMsgType},
|
control_msg::{self, ControlMsgType},
|
||||||
scrcpy_mask_cmd::{self, ScrcpyMaskCmdType},
|
scrcpy_mask_cmd::{self, ScrcpyMaskCmdType},
|
||||||
|
share,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn connect_socket(
|
pub async fn connect_socket(
|
||||||
@ -26,7 +27,7 @@ pub async fn connect_socket(
|
|||||||
.await
|
.await
|
||||||
.context("Socket connect failed")?;
|
.context("Socket connect failed")?;
|
||||||
|
|
||||||
println!("成功连接scrcpy-server:{:?}", client.local_addr());
|
println!("connect to scrcpy-server:{:?}", client.local_addr());
|
||||||
|
|
||||||
let (read_half, write_half) = client.into_split();
|
let (read_half, write_half) = client.into_split();
|
||||||
|
|
||||||
@ -71,6 +72,9 @@ async fn read_socket(
|
|||||||
end -= 1;
|
end -= 1;
|
||||||
}
|
}
|
||||||
let device_name = std::str::from_utf8(&buf[..end]).unwrap();
|
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();
|
||||||
|
|
||||||
let msg = json!({
|
let msg = json!({
|
||||||
"type": "MetaData",
|
"type": "MetaData",
|
||||||
"deviceName": device_name,
|
"deviceName": device_name,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"productName": "scrcpy-mask",
|
"productName": "scrcpy-mask",
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"identifier": "com.akichase.mask",
|
"identifier": "com.akichase.mask",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
@ -17,6 +17,7 @@ import {
|
|||||||
startScrcpyServer,
|
startScrcpyServer,
|
||||||
getDeviceScreenSize,
|
getDeviceScreenSize,
|
||||||
adbConnect,
|
adbConnect,
|
||||||
|
getCurClientInfo,
|
||||||
} from "../invoke";
|
} from "../invoke";
|
||||||
import {
|
import {
|
||||||
NH4,
|
NH4,
|
||||||
@ -39,13 +40,13 @@ import {
|
|||||||
useMessage,
|
useMessage,
|
||||||
NInputGroup,
|
NInputGroup,
|
||||||
} from "naive-ui";
|
} from "naive-ui";
|
||||||
import { CloseCircle, InformationCircle } from "@vicons/ionicons5";
|
import { CloseCircle, InformationCircle, Refresh } from "@vicons/ionicons5";
|
||||||
import { Refresh } from "@vicons/ionicons5";
|
|
||||||
import { UnlistenFn, listen } from "@tauri-apps/api/event";
|
import { UnlistenFn, listen } from "@tauri-apps/api/event";
|
||||||
import { Store } from "@tauri-apps/plugin-store";
|
import { Store } from "@tauri-apps/plugin-store";
|
||||||
import { shutdown } from "../frontcommand/scrcpyMaskCmd";
|
import { shutdown } from "../frontcommand/scrcpyMaskCmd";
|
||||||
import { useGlobalStore } from "../store/global";
|
import { useGlobalStore } from "../store/global";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { closeExternalControl, connectExternalControl } from "../websocket";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
@ -53,7 +54,8 @@ const store = useGlobalStore();
|
|||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
|
||||||
const port = ref(27183);
|
const port = ref(27183);
|
||||||
const address = ref("");
|
const wireless_address = ref("");
|
||||||
|
const ws_address = ref("");
|
||||||
|
|
||||||
const localStore = new Store("store.bin");
|
const localStore = new Store("store.bin");
|
||||||
|
|
||||||
@ -86,6 +88,26 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onActivated(async () => {
|
onActivated(async () => {
|
||||||
|
let curClientInfo = await getCurClientInfo();
|
||||||
|
if (store.controledDevice) {
|
||||||
|
// update controledDevice if client not exists
|
||||||
|
if (!curClientInfo) {
|
||||||
|
await shutdown();
|
||||||
|
store.controledDevice = null;
|
||||||
|
message.warning(t("pages.Device.alreadyDisconnected"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// restore controledDevice if client exists
|
||||||
|
if (curClientInfo) {
|
||||||
|
message.warning(t("pages.Device.alreadyControled"));
|
||||||
|
store.controledDevice = {
|
||||||
|
scid: curClientInfo.scid,
|
||||||
|
deviceName: curClientInfo.device_name,
|
||||||
|
deviceID: curClientInfo.device_id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await refreshDevices();
|
await refreshDevices();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -98,7 +120,7 @@ onUnmounted(() => {
|
|||||||
const devices: Ref<Device[]> = ref([]);
|
const devices: Ref<Device[]> = ref([]);
|
||||||
const availableDevice = computed(() => {
|
const availableDevice = computed(() => {
|
||||||
return devices.value.filter((d) => {
|
return devices.value.filter((d) => {
|
||||||
return store.controledDevice?.device.id !== d.id;
|
return store.controledDevice?.deviceID !== d.id;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const tableCols: DataTableColumns = [
|
const tableCols: DataTableColumns = [
|
||||||
@ -178,6 +200,18 @@ function onMenuClickoutside() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deviceControl() {
|
async function deviceControl() {
|
||||||
|
let curClientInfo = await getCurClientInfo();
|
||||||
|
if (curClientInfo) {
|
||||||
|
message.warning(t("pages.Device.alreadyControled"));
|
||||||
|
store.controledDevice = {
|
||||||
|
scid: curClientInfo.scid,
|
||||||
|
deviceName: curClientInfo.device_name,
|
||||||
|
deviceID: curClientInfo.device_id,
|
||||||
|
};
|
||||||
|
store.hideLoading();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!port.value) {
|
if (!port.value) {
|
||||||
port.value = 27183;
|
port.value = 27183;
|
||||||
}
|
}
|
||||||
@ -227,7 +261,7 @@ async function deviceControl() {
|
|||||||
store.controledDevice = {
|
store.controledDevice = {
|
||||||
scid,
|
scid,
|
||||||
deviceName,
|
deviceName,
|
||||||
device,
|
deviceID: device.id,
|
||||||
};
|
};
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
deviceWaitForMetadataTask = null;
|
deviceWaitForMetadataTask = null;
|
||||||
@ -267,15 +301,29 @@ async function refreshDevices() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function connectDevice() {
|
async function connectDevice() {
|
||||||
if (!address.value) {
|
if (!wireless_address.value) {
|
||||||
message.error(t("pages.Device.inputWirelessAddress"));
|
message.error(t("pages.Device.inputWirelessAddress"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
store.showLoading();
|
store.showLoading();
|
||||||
message.info(await adbConnect(address.value));
|
message.info(await adbConnect(wireless_address.value));
|
||||||
await refreshDevices();
|
await refreshDevices();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function connectWS() {
|
||||||
|
if (!ws_address.value) {
|
||||||
|
message.error(t("pages.Device.inputWsAddress"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
store.showLoading();
|
||||||
|
connectExternalControl(ws_address.value, message, store, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeWS() {
|
||||||
|
closeExternalControl();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -294,7 +342,7 @@ async function connectDevice() {
|
|||||||
<NH4 prefix="bar">{{ $t("pages.Device.wireless") }}</NH4>
|
<NH4 prefix="bar">{{ $t("pages.Device.wireless") }}</NH4>
|
||||||
<NInputGroup style="max-width: 300px">
|
<NInputGroup style="max-width: 300px">
|
||||||
<NInput
|
<NInput
|
||||||
v-model:value="address"
|
v-model:value="wireless_address"
|
||||||
clearable
|
clearable
|
||||||
:placeholder="$t('pages.Device.wirelessPlaceholder')"
|
:placeholder="$t('pages.Device.wirelessPlaceholder')"
|
||||||
/>
|
/>
|
||||||
@ -302,6 +350,24 @@ async function connectDevice() {
|
|||||||
$t("pages.Device.connect")
|
$t("pages.Device.connect")
|
||||||
}}</NButton>
|
}}</NButton>
|
||||||
</NInputGroup>
|
</NInputGroup>
|
||||||
|
<NH4 prefix="bar">{{ $t("pages.Device.externalControl") }}</NH4>
|
||||||
|
<NInputGroup style="max-width: 300px">
|
||||||
|
<NInput
|
||||||
|
v-model:value="ws_address"
|
||||||
|
clearable
|
||||||
|
:placeholder="$t('pages.Device.wsAddress')"
|
||||||
|
:disabled="store.externalControlled"
|
||||||
|
/>
|
||||||
|
<NButton
|
||||||
|
v-if="store.externalControlled"
|
||||||
|
type="error"
|
||||||
|
@click="closeWS"
|
||||||
|
>{{ $t("pages.Device.wsClose") }}</NButton
|
||||||
|
>
|
||||||
|
<NButton v-else type="primary" @click="connectWS">{{
|
||||||
|
$t("pages.Device.wsConnect")
|
||||||
|
}}</NButton>
|
||||||
|
</NInputGroup>
|
||||||
<NH4 prefix="bar">{{ $t("pages.Device.deviceSize.title") }}</NH4>
|
<NH4 prefix="bar">{{ $t("pages.Device.deviceSize.title") }}</NH4>
|
||||||
<NFlex justify="left" align="center">
|
<NFlex justify="left" align="center">
|
||||||
<NFormItem :label="$t('pages.Device.deviceSize.width')">
|
<NFormItem :label="$t('pages.Device.deviceSize.width')">
|
||||||
@ -332,7 +398,7 @@ async function connectDevice() {
|
|||||||
<div class="controled-device" v-if="store.controledDevice">
|
<div class="controled-device" v-if="store.controledDevice">
|
||||||
<div>
|
<div>
|
||||||
{{ store.controledDevice.deviceName }} ({{
|
{{ store.controledDevice.deviceName }} ({{
|
||||||
store.controledDevice.device.id
|
store.controledDevice.deviceID
|
||||||
}})
|
}})
|
||||||
</div>
|
</div>
|
||||||
<div class="device-op">
|
<div class="device-op">
|
||||||
@ -344,8 +410,7 @@ async function connectDevice() {
|
|||||||
</template>
|
</template>
|
||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
scid: {{ store.controledDevice.scid }} <br />status:
|
scid: {{ store.controledDevice.scid }}
|
||||||
{{ store.controledDevice.device.status }}
|
|
||||||
</NTooltip>
|
</NTooltip>
|
||||||
<NButton quaternary circle type="error" @click="shutdownSC()">
|
<NButton quaternary circle type="error" @click="shutdownSC()">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
|
@ -14,18 +14,12 @@ import { KeyMappingConfig, KeySteeringWheel } from "../keyMappingConfig";
|
|||||||
import { getVersion } from "@tauri-apps/api/app";
|
import { getVersion } from "@tauri-apps/api/app";
|
||||||
import { fetch } from "@tauri-apps/plugin-http";
|
import { fetch } from "@tauri-apps/plugin-http";
|
||||||
import { open } from "@tauri-apps/plugin-shell";
|
import { open } from "@tauri-apps/plugin-shell";
|
||||||
import {
|
import { sendSetClipboard } from "../frontcommand/controlMsg";
|
||||||
sendInjectKeycode,
|
|
||||||
sendSetClipboard,
|
|
||||||
} from "../frontcommand/controlMsg";
|
|
||||||
import { getCurrent, PhysicalSize } from "@tauri-apps/api/window";
|
import { getCurrent, PhysicalSize } from "@tauri-apps/api/window";
|
||||||
import {
|
import { AndroidKeycode } from "../frontcommand/android";
|
||||||
AndroidKeyEventAction,
|
|
||||||
AndroidKeycode,
|
|
||||||
AndroidMetastate,
|
|
||||||
} from "../frontcommand/android";
|
|
||||||
import { Store } from "@tauri-apps/plugin-store";
|
import { Store } from "@tauri-apps/plugin-store";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { SendKeyAction, sendKey } from "../frontcommand/scrcpyMaskCmd";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const store = useGlobalStore();
|
const store = useGlobalStore();
|
||||||
@ -203,18 +197,9 @@ async function pasteText() {
|
|||||||
});
|
});
|
||||||
await sleep(300);
|
await sleep(300);
|
||||||
// send enter
|
// send enter
|
||||||
await sendInjectKeycode({
|
await sendKey({
|
||||||
action: AndroidKeyEventAction.AKEY_EVENT_ACTION_DOWN,
|
action: SendKeyAction.Default,
|
||||||
keycode: AndroidKeycode.AKEYCODE_ENTER,
|
keycode: AndroidKeycode.AKEYCODE_ENTER,
|
||||||
repeat: 0,
|
|
||||||
metastate: AndroidMetastate.AMETA_NONE,
|
|
||||||
});
|
|
||||||
await sleep(50);
|
|
||||||
await sendInjectKeycode({
|
|
||||||
action: AndroidKeyEventAction.AKEY_EVENT_ACTION_UP,
|
|
||||||
keycode: AndroidKeycode.AKEYCODE_ENTER,
|
|
||||||
repeat: 0,
|
|
||||||
metastate: AndroidMetastate.AMETA_NONE,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,17 +15,11 @@ import {
|
|||||||
import { Keyboard24Regular } from "@vicons/fluent";
|
import { Keyboard24Regular } from "@vicons/fluent";
|
||||||
import { NIcon, useMessage } from "naive-ui";
|
import { NIcon, useMessage } from "naive-ui";
|
||||||
import { useGlobalStore } from "../store/global";
|
import { useGlobalStore } from "../store/global";
|
||||||
import {
|
import { sendSetScreenPowerMode } from "../frontcommand/controlMsg";
|
||||||
sendInjectKeycode,
|
import { AndroidKeycode } from "../frontcommand/android";
|
||||||
sendSetScreenPowerMode,
|
|
||||||
} from "../frontcommand/controlMsg";
|
|
||||||
import {
|
|
||||||
AndroidKeyEventAction,
|
|
||||||
AndroidKeycode,
|
|
||||||
AndroidMetastate,
|
|
||||||
} from "../frontcommand/android";
|
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { SendKeyAction, sendKey } from "../frontcommand/scrcpyMaskCmd";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -39,28 +33,11 @@ function nav(name: string) {
|
|||||||
router.replace({ name });
|
router.replace({ name });
|
||||||
}
|
}
|
||||||
|
|
||||||
function sleep(time: number) {
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve();
|
|
||||||
}, time);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendKeyCodeToDevice(code: AndroidKeycode) {
|
async function sendKeyCodeToDevice(code: AndroidKeycode) {
|
||||||
if (store.controledDevice) {
|
if (store.controledDevice) {
|
||||||
await sendInjectKeycode({
|
await sendKey({
|
||||||
action: AndroidKeyEventAction.AKEY_EVENT_ACTION_DOWN,
|
action: SendKeyAction.Default,
|
||||||
keycode: code,
|
keycode: code,
|
||||||
repeat: 0,
|
|
||||||
metastate: AndroidMetastate.AMETA_NONE,
|
|
||||||
});
|
|
||||||
await sleep(50);
|
|
||||||
await sendInjectKeycode({
|
|
||||||
action: AndroidKeyEventAction.AKEY_EVENT_ACTION_UP,
|
|
||||||
keycode: code,
|
|
||||||
repeat: 0,
|
|
||||||
metastate: AndroidMetastate.AMETA_NONE,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
message.error(t("sidebar.noControledDevice"));
|
message.error(t("sidebar.noControledDevice"));
|
||||||
|
@ -86,7 +86,7 @@ function changeLanguage(language: "zh-CN" | "en-US") {
|
|||||||
if (language === curLanguage.value) return;
|
if (language === curLanguage.value) return;
|
||||||
curLanguage.value = language;
|
curLanguage.value = language;
|
||||||
localStore.set("language", language);
|
localStore.set("language", language);
|
||||||
i18n.global.locale = language;
|
i18n.global.locale.value = language;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ export enum ScrcpyMaskCmdType {
|
|||||||
|
|
||||||
type ScrcpyMaskCmdData = CmdDataSendKey | CmdDataTouch | CmdDataSwipe | String;
|
type ScrcpyMaskCmdData = CmdDataSendKey | CmdDataTouch | CmdDataSwipe | String;
|
||||||
|
|
||||||
enum SendKeyAction {
|
export enum SendKeyAction {
|
||||||
Default = 0,
|
Default = 0,
|
||||||
Down = 1,
|
Down = 1,
|
||||||
Up = 2,
|
Up = 2,
|
||||||
|
@ -803,7 +803,7 @@ const loopDownKeyCBMap: Map<string, () => Promise<void>> = new Map();
|
|||||||
const upKeyCBMap: Map<string, () => Promise<void>> = new Map();
|
const upKeyCBMap: Map<string, () => Promise<void>> = new Map();
|
||||||
const cancelAbleKeyList: string[] = [];
|
const cancelAbleKeyList: string[] = [];
|
||||||
|
|
||||||
function keydownHandler(event: KeyboardEvent) {
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (event.repeat) return;
|
if (event.repeat) return;
|
||||||
if (downKeyMap.has(event.code)) {
|
if (downKeyMap.has(event.code)) {
|
||||||
@ -814,7 +814,7 @@ function keydownHandler(event: KeyboardEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function keyupHandler(event: KeyboardEvent) {
|
function handleKeyup(event: KeyboardEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (downKeyMap.has(event.code)) {
|
if (downKeyMap.has(event.code)) {
|
||||||
downKeyMap.set(event.code, false);
|
downKeyMap.set(event.code, false);
|
||||||
@ -1209,8 +1209,8 @@ function applyKeyMappingConfigShortcuts(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function listenToEvent() {
|
export function listenToEvent() {
|
||||||
window.addEventListener("keydown", keydownHandler);
|
window.addEventListener("keydown", handleKeydown);
|
||||||
window.addEventListener("keyup", keyupHandler);
|
window.addEventListener("keyup", handleKeyup);
|
||||||
window.addEventListener("mousedown", handleMouseDown);
|
window.addEventListener("mousedown", handleMouseDown);
|
||||||
window.addEventListener("mousemove", handleMouseMove);
|
window.addEventListener("mousemove", handleMouseMove);
|
||||||
window.addEventListener("mouseup", handleMouseUp);
|
window.addEventListener("mouseup", handleMouseUp);
|
||||||
@ -1220,8 +1220,8 @@ export function listenToEvent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function unlistenToEvent() {
|
export function unlistenToEvent() {
|
||||||
window.removeEventListener("keydown", keydownHandler);
|
window.removeEventListener("keydown", handleKeydown);
|
||||||
window.removeEventListener("keyup", keyupHandler);
|
window.removeEventListener("keyup", handleKeyup);
|
||||||
window.removeEventListener("mousedown", handleMouseDown);
|
window.removeEventListener("mousedown", handleMouseDown);
|
||||||
window.removeEventListener("mousemove", handleMouseMove);
|
window.removeEventListener("mousemove", handleMouseMove);
|
||||||
window.removeEventListener("mouseup", handleMouseUp);
|
window.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
@ -35,7 +35,14 @@
|
|||||||
},
|
},
|
||||||
"controledDevice": "Controlled device",
|
"controledDevice": "Controlled device",
|
||||||
"availableDevice": "Available devices",
|
"availableDevice": "Available devices",
|
||||||
"noControledDevice": "No Controled Device"
|
"noControledDevice": "No Controled Device",
|
||||||
|
"alreadyControled": "Controlled device already exists",
|
||||||
|
"alreadyDisconnected": "Controlled device connection has been disconnected",
|
||||||
|
"externalControl": "External control",
|
||||||
|
"wsAddress": "Websocket address",
|
||||||
|
"inputWsAddress": "Please enter the Websocket address",
|
||||||
|
"wsClose": "Close",
|
||||||
|
"wsConnect": "Control"
|
||||||
},
|
},
|
||||||
"Mask": {
|
"Mask": {
|
||||||
"inputBoxPlaceholder": "Input text and then press enter/esc",
|
"inputBoxPlaceholder": "Input text and then press enter/esc",
|
||||||
@ -216,5 +223,10 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"noControledDevice": "No devices are controlled"
|
"noControledDevice": "No devices are controlled"
|
||||||
|
},
|
||||||
|
"websocket": {
|
||||||
|
"open": "Connected to external control server",
|
||||||
|
"close": "External control connection disconnected",
|
||||||
|
"error": "Something was wrong, the exter connection is closed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ const localStore = new Store("store.bin");
|
|||||||
|
|
||||||
const i18n = createI18n({
|
const i18n = createI18n({
|
||||||
allowComposition: true,
|
allowComposition: true,
|
||||||
|
legacy: false,
|
||||||
messages: {
|
messages: {
|
||||||
"en-US": enUS,
|
"en-US": enUS,
|
||||||
"zh-CN": zhCN,
|
"zh-CN": zhCN,
|
||||||
@ -15,7 +16,7 @@ const i18n = createI18n({
|
|||||||
});
|
});
|
||||||
|
|
||||||
localStore.get<"en-US" | "zh-CN">("language").then((language) => {
|
localStore.get<"en-US" | "zh-CN">("language").then((language) => {
|
||||||
i18n.global.locale = language ?? "en-US";
|
i18n.global.locale.value = language ?? "en-US";
|
||||||
});
|
});
|
||||||
|
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
@ -35,7 +35,14 @@
|
|||||||
},
|
},
|
||||||
"controledDevice": "受控设备",
|
"controledDevice": "受控设备",
|
||||||
"noControledDevice": "无受控设备",
|
"noControledDevice": "无受控设备",
|
||||||
"availableDevice": "可用设备"
|
"availableDevice": "可用设备",
|
||||||
|
"alreadyControled": "已存在受控设备",
|
||||||
|
"alreadyDisconnected": "受控设备连接已断开",
|
||||||
|
"inputWsAddress": "请输入 Websocket 地址",
|
||||||
|
"externalControl": "外部控制",
|
||||||
|
"wsAddress": "Websocket 地址",
|
||||||
|
"wsClose": "断开",
|
||||||
|
"wsConnect": "控制"
|
||||||
},
|
},
|
||||||
"Mask": {
|
"Mask": {
|
||||||
"keyconfigException": "按键方案异常,请删除此方案",
|
"keyconfigException": "按键方案异常,请删除此方案",
|
||||||
@ -216,5 +223,10 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"noControledDevice": "未控制任何设备"
|
"noControledDevice": "未控制任何设备"
|
||||||
|
},
|
||||||
|
"websocket": {
|
||||||
|
"open": "已连接到外部控制服务端",
|
||||||
|
"close": "外部控制连接断开",
|
||||||
|
"error": "未知错误,外部控制连接断开"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,14 @@ export async function startScrcpyServer(
|
|||||||
return await invoke("start_scrcpy_server", { id, scid, address });
|
return await invoke("start_scrcpy_server", { id, scid, address });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCurClientInfo(): Promise<{
|
||||||
|
device_name: string;
|
||||||
|
device_id: string;
|
||||||
|
scid: string;
|
||||||
|
} | null> {
|
||||||
|
return await invoke("get_cur_client_info");
|
||||||
|
}
|
||||||
|
|
||||||
export async function getDeviceScreenSize(
|
export async function getDeviceScreenSize(
|
||||||
id: string
|
id: string
|
||||||
): Promise<[number, number]> {
|
): Promise<[number, number]> {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { Ref, ref } from "vue";
|
import { Ref, ref } from "vue";
|
||||||
import { Device } from "../invoke";
|
|
||||||
import {
|
import {
|
||||||
KeyMapping,
|
KeyMapping,
|
||||||
KeyMappingConfig,
|
KeyMappingConfig,
|
||||||
@ -22,7 +21,7 @@ export const useGlobalStore = defineStore("global", () => {
|
|||||||
interface ControledDevice {
|
interface ControledDevice {
|
||||||
scid: string;
|
scid: string;
|
||||||
deviceName: string;
|
deviceName: string;
|
||||||
device: Device;
|
deviceID: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const controledDevice: Ref<ControledDevice | null> = ref(null);
|
const controledDevice: Ref<ControledDevice | null> = ref(null);
|
||||||
@ -70,6 +69,8 @@ export const useGlobalStore = defineStore("global", () => {
|
|||||||
localStore.set("curKeyMappingIndex", index);
|
localStore.set("curKeyMappingIndex", index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const externalControlled = ref(false);
|
||||||
|
|
||||||
// persistent storage
|
// persistent storage
|
||||||
const screenSizeW: Ref<number> = ref(0);
|
const screenSizeW: Ref<number> = ref(0);
|
||||||
const screenSizeH: Ref<number> = ref(0);
|
const screenSizeH: Ref<number> = ref(0);
|
||||||
@ -89,6 +90,7 @@ export const useGlobalStore = defineStore("global", () => {
|
|||||||
curKeyMappingIndex,
|
curKeyMappingIndex,
|
||||||
maskButton,
|
maskButton,
|
||||||
checkUpdateAtStart,
|
checkUpdateAtStart,
|
||||||
|
externalControlled,
|
||||||
// in-memory storage
|
// in-memory storage
|
||||||
showLoading,
|
showLoading,
|
||||||
hideLoading,
|
hideLoading,
|
||||||
|
81
src/websocket.ts
Normal file
81
src/websocket.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { useMessage } from "naive-ui";
|
||||||
|
import { useGlobalStore } from "./store/global";
|
||||||
|
import { sendKey, shutdown, swipe, touch } from "./frontcommand/scrcpyMaskCmd";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
let ws: WebSocket;
|
||||||
|
let sharedMessage: ReturnType<typeof useMessage>;
|
||||||
|
let sharedStore: ReturnType<typeof useGlobalStore>;
|
||||||
|
let t: ReturnType<typeof useI18n>["t"];
|
||||||
|
|
||||||
|
export function connectExternalControl(
|
||||||
|
url: string,
|
||||||
|
message: ReturnType<typeof useMessage>,
|
||||||
|
store: ReturnType<typeof useGlobalStore>,
|
||||||
|
i18nT: ReturnType<typeof useI18n>["t"]
|
||||||
|
) {
|
||||||
|
sharedMessage = message;
|
||||||
|
sharedStore = store;
|
||||||
|
t = i18nT;
|
||||||
|
|
||||||
|
ws = new WebSocket(url);
|
||||||
|
ws.addEventListener("open", handleOpen);
|
||||||
|
ws.addEventListener("message", handleMessage);
|
||||||
|
ws.addEventListener("close", handleClose);
|
||||||
|
ws.addEventListener("error", handleError);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeExternalControl() {
|
||||||
|
if (ws) ws.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOpen() {
|
||||||
|
sharedStore.externalControlled = true;
|
||||||
|
sharedStore.hideLoading();
|
||||||
|
sharedMessage.success(t("websocket.open"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleMessage(event: MessageEvent) {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(event.data);
|
||||||
|
if (msg.type === "showMessage") {
|
||||||
|
sharedMessage.create(msg.msgContent, { type: msg.msgType });
|
||||||
|
} else if (msg.type === "getControlledDevice") {
|
||||||
|
msg.controledDevice = sharedStore.controledDevice;
|
||||||
|
ws.send(JSON.stringify(msg));
|
||||||
|
} else if (msg.type === "sendKey") {
|
||||||
|
delete msg.type;
|
||||||
|
await sendKey(msg);
|
||||||
|
} else if (msg.type === "touch") {
|
||||||
|
msg.screen = { w: sharedStore.screenSizeW, h: sharedStore.screenSizeH };
|
||||||
|
delete msg.type;
|
||||||
|
await touch(msg);
|
||||||
|
} else if (msg.type === "swipe") {
|
||||||
|
console.log(msg);
|
||||||
|
msg.screen = { w: sharedStore.screenSizeW, h: sharedStore.screenSizeH };
|
||||||
|
delete msg.type;
|
||||||
|
await swipe(msg);
|
||||||
|
} else if (msg.type === "shutdown") {
|
||||||
|
await shutdown();
|
||||||
|
sharedStore.controledDevice = null;
|
||||||
|
} else {
|
||||||
|
console.error("Invalid message received", msg);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Message received failed", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
sharedMessage.info(t("websocket.close"));
|
||||||
|
ws.close();
|
||||||
|
sharedStore.externalControlled = false;
|
||||||
|
sharedStore.hideLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleError() {
|
||||||
|
sharedMessage.error(t("websocket.error"));
|
||||||
|
ws.close();
|
||||||
|
sharedStore.externalControlled = false;
|
||||||
|
sharedStore.hideLoading();
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user