mirror of
https://github.com/AkiChase/scrcpy-mask
synced 2025-02-22 14:42:15 +08:00
commit
067d4ee45b
@ -17,7 +17,7 @@
|
||||
- [x] 国际化
|
||||
- [ ] 手柄按键映射
|
||||
- [ ] 更好的宏
|
||||
- [ ] 通过 WebSocket 提供外部接口
|
||||
- [x] 通过 WebSocket 提供外部控制,见[外部控制](https://github.com/AkiChase/scrcpy-mask-external-control)
|
||||
- [ ] 帮助文档
|
||||
|
||||
## 视频演示
|
||||
@ -27,6 +27,12 @@
|
||||
- [如何用 PC 控制安卓手机打王者?只要思想不滑坡,办法总比困难多!-哔哩哔哩](https://b23.tv/dmUOpff)
|
||||
- [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)
|
||||
- [ ] Gamepad key mapping
|
||||
- [ ] 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
|
||||
|
||||
## Demonstration video
|
||||
@ -29,6 +29,12 @@ This project only implements the Scrcpy control protocol and **does not provide
|
||||
- [如何用 PC 控制安卓手机打王者?只要思想不滑坡,办法总比困难多!-哔哩哔哩](https://b23.tv/dmUOpff)
|
||||
- [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
|
||||
|
||||
- Device control
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "scrcpy-mask",
|
||||
"private": true,
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "scrcpy-mask"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
description = "A Tauri App"
|
||||
authors = ["AkiChase"]
|
||||
edition = "2021"
|
||||
@ -16,6 +16,7 @@ tauri-plugin-store = "2.0.0-beta"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
anyhow = "1.0"
|
||||
lazy_static = "1.4.0"
|
||||
tokio = { version = "1.36.0", features = ["rt-multi-thread", "net", "macros", "io-util", "time", "sync"] }
|
||||
tauri-plugin-process = "2.0.0-beta"
|
||||
tauri-plugin-shell = "2.0.0-beta"
|
||||
|
@ -3,7 +3,7 @@ use std::{io::BufRead, path::PathBuf};
|
||||
|
||||
use crate::{
|
||||
adb::{Adb, Device},
|
||||
resource::{ResHelper, ResourceName},
|
||||
resource::{ResHelper, ResourceName}, share,
|
||||
};
|
||||
|
||||
/**
|
||||
@ -119,6 +119,8 @@ impl ScrcpyClient {
|
||||
// clear string to store new line only
|
||||
s.clear();
|
||||
}
|
||||
|
||||
*share::CLIENT_INFO.lock().unwrap() = None;
|
||||
println!("Scrcpy server closed");
|
||||
Ok(())
|
||||
}
|
||||
|
@ -5,3 +5,4 @@ pub mod control_msg;
|
||||
pub mod resource;
|
||||
pub mod scrcpy_mask_cmd;
|
||||
pub mod socket;
|
||||
pub mod share;
|
||||
|
@ -5,6 +5,7 @@ use scrcpy_mask::{
|
||||
adb::{Adb, Device},
|
||||
client::ScrcpyClient,
|
||||
resource::{ResHelper, ResourceName},
|
||||
share,
|
||||
socket::connect_socket,
|
||||
};
|
||||
use std::{fs::read_to_string, sync::Arc};
|
||||
@ -54,6 +55,17 @@ fn start_scrcpy_server(
|
||||
address: String,
|
||||
app: tauri::AppHandle,
|
||||
) -> 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 version = ScrcpyClient::get_scrcpy_version();
|
||||
|
||||
@ -108,6 +120,15 @@ fn start_scrcpy_server(
|
||||
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]
|
||||
/// get device screen size
|
||||
fn get_device_screen_size(id: String, app: tauri::AppHandle) -> Result<(u32, u32), String> {
|
||||
@ -217,6 +238,7 @@ async fn main() {
|
||||
forward_server_port,
|
||||
push_server_file,
|
||||
start_scrcpy_server,
|
||||
get_cur_client_info,
|
||||
get_device_screen_size,
|
||||
adb_connect,
|
||||
load_default_keyconfig
|
||||
|
@ -35,7 +35,7 @@ pub async fn handle_sm_cmd(
|
||||
// up
|
||||
let buf = gen_inject_key_ctrl_msg(
|
||||
ctrl_msg_type,
|
||||
0, // AKEY_EVENT_ACTION_DOWN
|
||||
1, // AKEY_EVENT_ACTION_UP
|
||||
keycode,
|
||||
0,
|
||||
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::{
|
||||
control_msg::{self, ControlMsgType},
|
||||
scrcpy_mask_cmd::{self, ScrcpyMaskCmdType},
|
||||
share,
|
||||
};
|
||||
|
||||
pub async fn connect_socket(
|
||||
@ -26,7 +27,7 @@ pub async fn connect_socket(
|
||||
.await
|
||||
.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();
|
||||
|
||||
@ -71,6 +72,9 @@ async fn read_socket(
|
||||
end -= 1;
|
||||
}
|
||||
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!({
|
||||
"type": "MetaData",
|
||||
"deviceName": device_name,
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"productName": "scrcpy-mask",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"identifier": "com.akichase.mask",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
startScrcpyServer,
|
||||
getDeviceScreenSize,
|
||||
adbConnect,
|
||||
getCurClientInfo,
|
||||
} from "../invoke";
|
||||
import {
|
||||
NH4,
|
||||
@ -39,13 +40,13 @@ import {
|
||||
useMessage,
|
||||
NInputGroup,
|
||||
} from "naive-ui";
|
||||
import { CloseCircle, InformationCircle } from "@vicons/ionicons5";
|
||||
import { Refresh } from "@vicons/ionicons5";
|
||||
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";
|
||||
import { closeExternalControl, connectExternalControl } from "../websocket";
|
||||
|
||||
const { t } = useI18n();
|
||||
const dialog = useDialog();
|
||||
@ -53,7 +54,8 @@ const store = useGlobalStore();
|
||||
const message = useMessage();
|
||||
|
||||
const port = ref(27183);
|
||||
const address = ref("");
|
||||
const wireless_address = ref("");
|
||||
const ws_address = ref("");
|
||||
|
||||
const localStore = new Store("store.bin");
|
||||
|
||||
@ -86,6 +88,26 @@ onMounted(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();
|
||||
});
|
||||
|
||||
@ -98,7 +120,7 @@ onUnmounted(() => {
|
||||
const devices: Ref<Device[]> = ref([]);
|
||||
const availableDevice = computed(() => {
|
||||
return devices.value.filter((d) => {
|
||||
return store.controledDevice?.device.id !== d.id;
|
||||
return store.controledDevice?.deviceID !== d.id;
|
||||
});
|
||||
});
|
||||
const tableCols: DataTableColumns = [
|
||||
@ -178,6 +200,18 @@ function onMenuClickoutside() {
|
||||
}
|
||||
|
||||
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) {
|
||||
port.value = 27183;
|
||||
}
|
||||
@ -227,7 +261,7 @@ async function deviceControl() {
|
||||
store.controledDevice = {
|
||||
scid,
|
||||
deviceName,
|
||||
device,
|
||||
deviceID: device.id,
|
||||
};
|
||||
nextTick(() => {
|
||||
deviceWaitForMetadataTask = null;
|
||||
@ -267,15 +301,29 @@ async function refreshDevices() {
|
||||
}
|
||||
|
||||
async function connectDevice() {
|
||||
if (!address.value) {
|
||||
if (!wireless_address.value) {
|
||||
message.error(t("pages.Device.inputWirelessAddress"));
|
||||
return;
|
||||
}
|
||||
|
||||
store.showLoading();
|
||||
message.info(await adbConnect(address.value));
|
||||
message.info(await adbConnect(wireless_address.value));
|
||||
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>
|
||||
|
||||
<template>
|
||||
@ -294,7 +342,7 @@ async function connectDevice() {
|
||||
<NH4 prefix="bar">{{ $t("pages.Device.wireless") }}</NH4>
|
||||
<NInputGroup style="max-width: 300px">
|
||||
<NInput
|
||||
v-model:value="address"
|
||||
v-model:value="wireless_address"
|
||||
clearable
|
||||
:placeholder="$t('pages.Device.wirelessPlaceholder')"
|
||||
/>
|
||||
@ -302,6 +350,24 @@ async function connectDevice() {
|
||||
$t("pages.Device.connect")
|
||||
}}</NButton>
|
||||
</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>
|
||||
<NFlex justify="left" align="center">
|
||||
<NFormItem :label="$t('pages.Device.deviceSize.width')">
|
||||
@ -332,7 +398,7 @@ async function connectDevice() {
|
||||
<div class="controled-device" v-if="store.controledDevice">
|
||||
<div>
|
||||
{{ store.controledDevice.deviceName }} ({{
|
||||
store.controledDevice.device.id
|
||||
store.controledDevice.deviceID
|
||||
}})
|
||||
</div>
|
||||
<div class="device-op">
|
||||
@ -344,8 +410,7 @@ async function connectDevice() {
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
scid: {{ store.controledDevice.scid }} <br />status:
|
||||
{{ store.controledDevice.device.status }}
|
||||
scid: {{ store.controledDevice.scid }}
|
||||
</NTooltip>
|
||||
<NButton quaternary circle type="error" @click="shutdownSC()">
|
||||
<template #icon>
|
||||
|
@ -14,18 +14,12 @@ import { KeyMappingConfig, KeySteeringWheel } from "../keyMappingConfig";
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
import { open } from "@tauri-apps/plugin-shell";
|
||||
import {
|
||||
sendInjectKeycode,
|
||||
sendSetClipboard,
|
||||
} from "../frontcommand/controlMsg";
|
||||
import { sendSetClipboard } from "../frontcommand/controlMsg";
|
||||
import { getCurrent, PhysicalSize } from "@tauri-apps/api/window";
|
||||
import {
|
||||
AndroidKeyEventAction,
|
||||
AndroidKeycode,
|
||||
AndroidMetastate,
|
||||
} from "../frontcommand/android";
|
||||
import { AndroidKeycode } from "../frontcommand/android";
|
||||
import { Store } from "@tauri-apps/plugin-store";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { SendKeyAction, sendKey } from "../frontcommand/scrcpyMaskCmd";
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useGlobalStore();
|
||||
@ -203,18 +197,9 @@ async function pasteText() {
|
||||
});
|
||||
await sleep(300);
|
||||
// send enter
|
||||
await sendInjectKeycode({
|
||||
action: AndroidKeyEventAction.AKEY_EVENT_ACTION_DOWN,
|
||||
await sendKey({
|
||||
action: SendKeyAction.Default,
|
||||
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 { NIcon, useMessage } from "naive-ui";
|
||||
import { useGlobalStore } from "../store/global";
|
||||
import {
|
||||
sendInjectKeycode,
|
||||
sendSetScreenPowerMode,
|
||||
} from "../frontcommand/controlMsg";
|
||||
import {
|
||||
AndroidKeyEventAction,
|
||||
AndroidKeycode,
|
||||
AndroidMetastate,
|
||||
} from "../frontcommand/android";
|
||||
import { sendSetScreenPowerMode } from "../frontcommand/controlMsg";
|
||||
import { AndroidKeycode } from "../frontcommand/android";
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { SendKeyAction, sendKey } from "../frontcommand/scrcpyMaskCmd";
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
@ -39,28 +33,11 @@ function nav(name: string) {
|
||||
router.replace({ name });
|
||||
}
|
||||
|
||||
function sleep(time: number) {
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, time);
|
||||
});
|
||||
}
|
||||
|
||||
async function sendKeyCodeToDevice(code: AndroidKeycode) {
|
||||
if (store.controledDevice) {
|
||||
await sendInjectKeycode({
|
||||
action: AndroidKeyEventAction.AKEY_EVENT_ACTION_DOWN,
|
||||
await sendKey({
|
||||
action: SendKeyAction.Default,
|
||||
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 {
|
||||
message.error(t("sidebar.noControledDevice"));
|
||||
|
@ -86,7 +86,7 @@ function changeLanguage(language: "zh-CN" | "en-US") {
|
||||
if (language === curLanguage.value) return;
|
||||
curLanguage.value = language;
|
||||
localStore.set("language", language);
|
||||
i18n.global.locale = language;
|
||||
i18n.global.locale.value = language;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -35,7 +35,7 @@ export enum ScrcpyMaskCmdType {
|
||||
|
||||
type ScrcpyMaskCmdData = CmdDataSendKey | CmdDataTouch | CmdDataSwipe | String;
|
||||
|
||||
enum SendKeyAction {
|
||||
export enum SendKeyAction {
|
||||
Default = 0,
|
||||
Down = 1,
|
||||
Up = 2,
|
||||
|
@ -803,7 +803,7 @@ const loopDownKeyCBMap: Map<string, () => Promise<void>> = new Map();
|
||||
const upKeyCBMap: Map<string, () => Promise<void>> = new Map();
|
||||
const cancelAbleKeyList: string[] = [];
|
||||
|
||||
function keydownHandler(event: KeyboardEvent) {
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
event.preventDefault();
|
||||
if (event.repeat) return;
|
||||
if (downKeyMap.has(event.code)) {
|
||||
@ -814,7 +814,7 @@ function keydownHandler(event: KeyboardEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
function keyupHandler(event: KeyboardEvent) {
|
||||
function handleKeyup(event: KeyboardEvent) {
|
||||
event.preventDefault();
|
||||
if (downKeyMap.has(event.code)) {
|
||||
downKeyMap.set(event.code, false);
|
||||
@ -1209,8 +1209,8 @@ function applyKeyMappingConfigShortcuts(
|
||||
}
|
||||
|
||||
export function listenToEvent() {
|
||||
window.addEventListener("keydown", keydownHandler);
|
||||
window.addEventListener("keyup", keyupHandler);
|
||||
window.addEventListener("keydown", handleKeydown);
|
||||
window.addEventListener("keyup", handleKeyup);
|
||||
window.addEventListener("mousedown", handleMouseDown);
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
window.addEventListener("mouseup", handleMouseUp);
|
||||
@ -1220,8 +1220,8 @@ export function listenToEvent() {
|
||||
}
|
||||
|
||||
export function unlistenToEvent() {
|
||||
window.removeEventListener("keydown", keydownHandler);
|
||||
window.removeEventListener("keyup", keyupHandler);
|
||||
window.removeEventListener("keydown", handleKeydown);
|
||||
window.removeEventListener("keyup", handleKeyup);
|
||||
window.removeEventListener("mousedown", handleMouseDown);
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
window.removeEventListener("mouseup", handleMouseUp);
|
||||
|
@ -35,7 +35,14 @@
|
||||
},
|
||||
"controledDevice": "Controlled device",
|
||||
"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": {
|
||||
"inputBoxPlaceholder": "Input text and then press enter/esc",
|
||||
@ -216,5 +223,10 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"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({
|
||||
allowComposition: true,
|
||||
legacy: false,
|
||||
messages: {
|
||||
"en-US": enUS,
|
||||
"zh-CN": zhCN,
|
||||
@ -15,7 +16,7 @@ const i18n = createI18n({
|
||||
});
|
||||
|
||||
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;
|
||||
|
@ -35,7 +35,14 @@
|
||||
},
|
||||
"controledDevice": "受控设备",
|
||||
"noControledDevice": "无受控设备",
|
||||
"availableDevice": "可用设备"
|
||||
"availableDevice": "可用设备",
|
||||
"alreadyControled": "已存在受控设备",
|
||||
"alreadyDisconnected": "受控设备连接已断开",
|
||||
"inputWsAddress": "请输入 Websocket 地址",
|
||||
"externalControl": "外部控制",
|
||||
"wsAddress": "Websocket 地址",
|
||||
"wsClose": "断开",
|
||||
"wsConnect": "控制"
|
||||
},
|
||||
"Mask": {
|
||||
"keyconfigException": "按键方案异常,请删除此方案",
|
||||
@ -216,5 +223,10 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"noControledDevice": "未控制任何设备"
|
||||
},
|
||||
"websocket": {
|
||||
"open": "已连接到外部控制服务端",
|
||||
"close": "外部控制连接断开",
|
||||
"error": "未知错误,外部控制连接断开"
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,14 @@ export async function startScrcpyServer(
|
||||
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(
|
||||
id: string
|
||||
): Promise<[number, number]> {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { Ref, ref } from "vue";
|
||||
import { Device } from "../invoke";
|
||||
import {
|
||||
KeyMapping,
|
||||
KeyMappingConfig,
|
||||
@ -22,7 +21,7 @@ export const useGlobalStore = defineStore("global", () => {
|
||||
interface ControledDevice {
|
||||
scid: string;
|
||||
deviceName: string;
|
||||
device: Device;
|
||||
deviceID: string;
|
||||
}
|
||||
|
||||
const controledDevice: Ref<ControledDevice | null> = ref(null);
|
||||
@ -70,6 +69,8 @@ export const useGlobalStore = defineStore("global", () => {
|
||||
localStore.set("curKeyMappingIndex", index);
|
||||
}
|
||||
|
||||
const externalControlled = ref(false);
|
||||
|
||||
// persistent storage
|
||||
const screenSizeW: Ref<number> = ref(0);
|
||||
const screenSizeH: Ref<number> = ref(0);
|
||||
@ -89,6 +90,7 @@ export const useGlobalStore = defineStore("global", () => {
|
||||
curKeyMappingIndex,
|
||||
maskButton,
|
||||
checkUpdateAtStart,
|
||||
externalControlled,
|
||||
// in-memory storage
|
||||
showLoading,
|
||||
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