Merge pull request #27 from AkiChase/dev

Scrcpy Mask v0.2.1
This commit is contained in:
如初 2024-05-17 09:41:31 +08:00 committed by GitHub
commit 067d4ee45b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 288 additions and 80 deletions

View File

@ -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)
## 截图 ## 截图
- 设备控制 - 设备控制

View File

@ -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

View File

@ -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",

View File

@ -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"

View File

@ -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(())
} }

View File

@ -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;

View File

@ -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

View File

@ -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
View 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);
}

View File

@ -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,

View File

@ -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",

View File

@ -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>

View File

@ -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,
}); });
} }

View File

@ -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"));

View File

@ -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>

View File

@ -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,

View File

@ -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);

View File

@ -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"
} }
} }

View File

@ -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;

View File

@ -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": "未知错误,外部控制连接断开"
} }
} }

View File

@ -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]> {

View File

@ -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
View 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();
}