2024-04-13 09:53:41 +08:00
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import {
|
|
|
|
|
Ref,
|
|
|
|
|
computed,
|
|
|
|
|
h,
|
|
|
|
|
nextTick,
|
|
|
|
|
onActivated,
|
|
|
|
|
onMounted,
|
|
|
|
|
onUnmounted,
|
|
|
|
|
ref,
|
|
|
|
|
} from "vue";
|
|
|
|
|
import {
|
|
|
|
|
Device,
|
|
|
|
|
adbDevices,
|
|
|
|
|
pushServerFile,
|
|
|
|
|
forwardServerPort,
|
|
|
|
|
startScrcpyServer,
|
2024-04-30 09:57:47 +08:00
|
|
|
|
getDeviceScreenSize,
|
2024-04-30 22:02:40 +08:00
|
|
|
|
adbConnect,
|
2024-04-13 09:53:41 +08:00
|
|
|
|
} from "../invoke";
|
|
|
|
|
import {
|
|
|
|
|
NH4,
|
2024-04-16 22:18:42 +08:00
|
|
|
|
NP,
|
2024-04-30 22:02:40 +08:00
|
|
|
|
NInput,
|
2024-04-13 09:53:41 +08:00
|
|
|
|
NInputNumber,
|
|
|
|
|
NButton,
|
|
|
|
|
NDataTable,
|
|
|
|
|
NDropdown,
|
|
|
|
|
NEmpty,
|
|
|
|
|
NTooltip,
|
|
|
|
|
NFlex,
|
2024-04-16 17:07:25 +08:00
|
|
|
|
NFormItem,
|
2024-04-13 09:53:41 +08:00
|
|
|
|
NIcon,
|
|
|
|
|
NSpin,
|
2024-04-16 21:14:37 +08:00
|
|
|
|
NScrollbar,
|
2024-04-13 09:53:41 +08:00
|
|
|
|
DataTableColumns,
|
|
|
|
|
DropdownOption,
|
|
|
|
|
useDialog,
|
2024-04-16 17:07:25 +08:00
|
|
|
|
useMessage,
|
2024-04-30 22:02:40 +08:00
|
|
|
|
NInputGroup,
|
2024-04-13 09:53:41 +08:00
|
|
|
|
} from "naive-ui";
|
|
|
|
|
import { CloseCircle, InformationCircle } from "@vicons/ionicons5";
|
|
|
|
|
import { Refresh } from "@vicons/ionicons5";
|
|
|
|
|
import { UnlistenFn, listen } from "@tauri-apps/api/event";
|
2024-04-16 22:18:42 +08:00
|
|
|
|
import { Store } from "@tauri-apps/plugin-store";
|
2024-04-13 09:53:41 +08:00
|
|
|
|
import { shutdown } from "../frontcommand/scrcpyMaskCmd";
|
|
|
|
|
import { useGlobalStore } from "../store/global";
|
|
|
|
|
|
|
|
|
|
const dialog = useDialog();
|
|
|
|
|
const store = useGlobalStore();
|
2024-04-16 17:07:25 +08:00
|
|
|
|
const message = useMessage();
|
2024-04-13 09:53:41 +08:00
|
|
|
|
|
|
|
|
|
const port = ref(27183);
|
2024-04-30 22:02:40 +08:00
|
|
|
|
const address = ref("");
|
2024-04-13 09:53:41 +08:00
|
|
|
|
|
2024-04-16 22:18:42 +08:00
|
|
|
|
const localStore = new Store("store.bin");
|
|
|
|
|
|
2024-04-13 09:53:41 +08:00
|
|
|
|
//#region listener
|
2024-04-17 22:52:17 +08:00
|
|
|
|
let deviceWaitForMetadataTask: ((deviceName: string) => void) | null = null;
|
2024-04-13 09:53:41 +08:00
|
|
|
|
|
|
|
|
|
let unlisten: UnlistenFn | undefined;
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
unlisten = await listen("device-reply", (event) => {
|
|
|
|
|
try {
|
|
|
|
|
let payload = JSON.parse(event.payload as string);
|
|
|
|
|
switch (payload.type) {
|
|
|
|
|
case "MetaData":
|
2024-04-17 22:52:17 +08:00
|
|
|
|
deviceWaitForMetadataTask?.(payload.deviceName);
|
2024-04-13 09:53:41 +08:00
|
|
|
|
break;
|
|
|
|
|
case "ClipboardChanged":
|
|
|
|
|
console.log("剪切板变动", payload.clipboard);
|
|
|
|
|
break;
|
|
|
|
|
case "ClipboardSetAck":
|
|
|
|
|
console.log("剪切板设置成功", payload.sequence);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
console.log("Unknown reply", payload);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onActivated(async () => {
|
|
|
|
|
await refreshDevices();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
if (unlisten !== undefined) unlisten();
|
|
|
|
|
});
|
|
|
|
|
//#endregion
|
|
|
|
|
|
|
|
|
|
//#region table
|
|
|
|
|
const devices: Ref<Device[]> = ref([]);
|
|
|
|
|
const availableDevice = computed(() => {
|
|
|
|
|
return devices.value.filter((d) => {
|
|
|
|
|
return store.controledDevice?.device.id !== d.id;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
const tableCols: DataTableColumns = [
|
|
|
|
|
{
|
|
|
|
|
title: "ID",
|
|
|
|
|
key: "id",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: "Status",
|
|
|
|
|
key: "status",
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// record last operated row index
|
|
|
|
|
let rowIndex = -1;
|
|
|
|
|
|
|
|
|
|
// table row contextmenu and click event handler
|
|
|
|
|
const tableRowProps = (_: any, index: number) => {
|
|
|
|
|
return {
|
|
|
|
|
onContextmenu: (e: MouseEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
showMenu.value = false;
|
|
|
|
|
rowIndex = index;
|
|
|
|
|
nextTick().then(() => {
|
|
|
|
|
showMenu.value = true;
|
|
|
|
|
menuX.value = e.clientX;
|
|
|
|
|
menuY.value = e.clientY;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
onclick: (e: MouseEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
showMenu.value = false;
|
|
|
|
|
rowIndex = index;
|
|
|
|
|
nextTick().then(() => {
|
|
|
|
|
showMenu.value = true;
|
|
|
|
|
menuX.value = e.clientX;
|
|
|
|
|
menuY.value = e.clientY;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
//#endregion
|
|
|
|
|
|
|
|
|
|
//#region controled device
|
|
|
|
|
|
|
|
|
|
async function shutdownSC() {
|
|
|
|
|
dialog.warning({
|
|
|
|
|
title: "Warning",
|
|
|
|
|
content: "确定关闭Scrcpy控制服务?",
|
|
|
|
|
positiveText: "确定",
|
|
|
|
|
negativeText: "取消",
|
|
|
|
|
onPositiveClick: async () => {
|
|
|
|
|
await shutdown();
|
|
|
|
|
store.controledDevice = null;
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
//#endregion
|
|
|
|
|
|
|
|
|
|
//#region menu
|
|
|
|
|
const menuX = ref(0);
|
|
|
|
|
const menuY = ref(0);
|
|
|
|
|
const showMenu = ref(false);
|
|
|
|
|
const menuOptions: DropdownOption[] = [
|
|
|
|
|
{
|
|
|
|
|
label: () => h("span", "控制此设备"),
|
|
|
|
|
key: "control",
|
|
|
|
|
},
|
2024-04-30 09:57:47 +08:00
|
|
|
|
{
|
|
|
|
|
label: () => h("span", "获取屏幕尺寸"),
|
|
|
|
|
key: "screen",
|
|
|
|
|
},
|
2024-04-13 09:53:41 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function onMenuClickoutside() {
|
|
|
|
|
showMenu.value = false;
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-30 09:57:47 +08:00
|
|
|
|
async function deviceControl() {
|
|
|
|
|
if (!port.value) {
|
|
|
|
|
port.value = 27183;
|
|
|
|
|
}
|
2024-04-16 17:07:25 +08:00
|
|
|
|
|
2024-04-30 09:57:47 +08:00
|
|
|
|
if (!(store.screenSizeW > 0) || !(store.screenSizeH > 0)) {
|
|
|
|
|
message.error("请正确输入当前控制设备的屏幕尺寸");
|
|
|
|
|
store.screenSizeW = 0;
|
|
|
|
|
store.screenSizeH = 0;
|
|
|
|
|
store.hideLoading();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-04-16 17:07:25 +08:00
|
|
|
|
|
2024-04-30 09:57:47 +08:00
|
|
|
|
if (store.controledDevice) {
|
|
|
|
|
message.error("请先关闭当前控制设备");
|
|
|
|
|
store.hideLoading();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-04-25 22:48:05 +08:00
|
|
|
|
|
2024-04-30 09:57:47 +08:00
|
|
|
|
localStore.set("screenSize", {
|
|
|
|
|
sizeW: store.screenSizeW,
|
|
|
|
|
sizeH: store.screenSizeH,
|
|
|
|
|
});
|
|
|
|
|
message.info("屏幕尺寸已保存,正在启动控制服务,请保持设备亮屏");
|
2024-04-16 22:18:42 +08:00
|
|
|
|
|
2024-04-30 09:57:47 +08:00
|
|
|
|
const device = devices.value[rowIndex];
|
2024-04-13 09:53:41 +08:00
|
|
|
|
|
2024-04-30 09:57:47 +08:00
|
|
|
|
let scid = (
|
|
|
|
|
"00000000" + Math.floor(Math.random() * 100000).toString(16)
|
|
|
|
|
).slice(-8);
|
2024-04-13 09:53:41 +08:00
|
|
|
|
|
2024-04-30 09:57:47 +08:00
|
|
|
|
await pushServerFile(device.id);
|
|
|
|
|
await forwardServerPort(device.id, scid, port.value);
|
|
|
|
|
await startScrcpyServer(device.id, scid, `127.0.0.1:${port.value}`);
|
2024-04-13 09:53:41 +08:00
|
|
|
|
|
2024-04-30 09:57:47 +08:00
|
|
|
|
// connection timeout check
|
|
|
|
|
let id = setTimeout(async () => {
|
|
|
|
|
if (deviceWaitForMetadataTask) {
|
|
|
|
|
await shutdown();
|
|
|
|
|
store.controledDevice = null;
|
|
|
|
|
store.hideLoading();
|
|
|
|
|
message.error("设备连接超时");
|
|
|
|
|
}
|
|
|
|
|
}, 6000);
|
|
|
|
|
|
|
|
|
|
// add cb for metadata
|
|
|
|
|
deviceWaitForMetadataTask = (deviceName: string) => {
|
|
|
|
|
store.controledDevice = {
|
|
|
|
|
scid,
|
|
|
|
|
deviceName,
|
|
|
|
|
device,
|
|
|
|
|
};
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
deviceWaitForMetadataTask = null;
|
|
|
|
|
clearTimeout(id);
|
|
|
|
|
store.hideLoading();
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
}
|
2024-04-17 22:52:17 +08:00
|
|
|
|
|
2024-04-30 09:57:47 +08:00
|
|
|
|
async function deviceGetScreenSize() {
|
|
|
|
|
let id = devices.value[rowIndex].id;
|
|
|
|
|
const size = await getDeviceScreenSize(id);
|
|
|
|
|
store.hideLoading();
|
|
|
|
|
message.success(`设备屏幕尺寸为: ${size[0]} x ${size[1]}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function onMenuSelect(key: string) {
|
|
|
|
|
showMenu.value = false;
|
|
|
|
|
store.showLoading();
|
|
|
|
|
switch (key) {
|
|
|
|
|
case "control":
|
|
|
|
|
await deviceControl();
|
|
|
|
|
break;
|
|
|
|
|
case "screen":
|
|
|
|
|
await deviceGetScreenSize();
|
2024-04-13 09:53:41 +08:00
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//#endregion
|
|
|
|
|
|
|
|
|
|
async function refreshDevices() {
|
|
|
|
|
store.showLoading();
|
|
|
|
|
devices.value = await adbDevices();
|
|
|
|
|
store.hideLoading();
|
|
|
|
|
}
|
2024-04-30 22:02:40 +08:00
|
|
|
|
|
|
|
|
|
async function connectDevice() {
|
|
|
|
|
if (!address.value) {
|
|
|
|
|
message.error("请输入无线调试地址");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
store.showLoading();
|
|
|
|
|
const msg = await adbConnect(address.value);
|
|
|
|
|
store.hideLoading();
|
|
|
|
|
message.info(msg);
|
|
|
|
|
}
|
2024-04-13 09:53:41 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
2024-04-16 21:14:37 +08:00
|
|
|
|
<NScrollbar>
|
|
|
|
|
<div class="device">
|
|
|
|
|
<NSpin :show="store.showLoadingRef">
|
|
|
|
|
<NH4 prefix="bar">本地端口</NH4>
|
|
|
|
|
<NInputNumber
|
|
|
|
|
v-model:value="port"
|
|
|
|
|
:show-button="false"
|
|
|
|
|
:min="16384"
|
|
|
|
|
:max="49151"
|
2024-04-30 22:02:40 +08:00
|
|
|
|
placeholder="Scrcpy 本地端口"
|
2024-04-16 21:14:37 +08:00
|
|
|
|
style="max-width: 300px"
|
2024-04-13 09:53:41 +08:00
|
|
|
|
/>
|
2024-04-30 22:02:40 +08:00
|
|
|
|
<NH4 prefix="bar">无线连接</NH4>
|
|
|
|
|
<NInputGroup style="max-width: 300px">
|
|
|
|
|
<NInput
|
|
|
|
|
v-model:value="address"
|
|
|
|
|
clearable
|
|
|
|
|
placeholder="无线调试地址"
|
|
|
|
|
/>
|
|
|
|
|
<NButton type="primary" @click="connectDevice">连接</NButton>
|
|
|
|
|
</NInputGroup>
|
2024-04-16 21:14:37 +08:00
|
|
|
|
<NH4 prefix="bar">设备尺寸</NH4>
|
|
|
|
|
<NFlex justify="left" align="center">
|
|
|
|
|
<NFormItem label="宽度">
|
|
|
|
|
<NInputNumber
|
|
|
|
|
v-model:value="store.screenSizeW"
|
|
|
|
|
placeholder="屏幕宽度"
|
|
|
|
|
:min="0"
|
|
|
|
|
:disabled="store.controledDevice !== null"
|
|
|
|
|
/>
|
|
|
|
|
</NFormItem>
|
|
|
|
|
<NFormItem label="高度">
|
|
|
|
|
<NInputNumber
|
|
|
|
|
v-model:value="store.screenSizeH"
|
|
|
|
|
placeholder="屏幕高度"
|
|
|
|
|
:min="0"
|
|
|
|
|
:disabled="store.controledDevice !== null"
|
|
|
|
|
/>
|
|
|
|
|
</NFormItem>
|
|
|
|
|
</NFlex>
|
|
|
|
|
<NP
|
|
|
|
|
>提示:请正确输入当前控制设备的屏幕尺寸,这是成功发送触摸事件的必要参数</NP
|
|
|
|
|
>
|
|
|
|
|
<NH4 prefix="bar">受控设备</NH4>
|
|
|
|
|
<div class="controled-device-list">
|
|
|
|
|
<NEmpty
|
|
|
|
|
size="small"
|
|
|
|
|
description="No Controled Device"
|
|
|
|
|
v-if="!store.controledDevice"
|
|
|
|
|
/>
|
|
|
|
|
<div class="controled-device" v-if="store.controledDevice">
|
|
|
|
|
<div>
|
|
|
|
|
{{ store.controledDevice.deviceName }} ({{
|
|
|
|
|
store.controledDevice.device.id
|
|
|
|
|
}})
|
|
|
|
|
</div>
|
|
|
|
|
<div class="device-op">
|
|
|
|
|
<NTooltip trigger="hover">
|
|
|
|
|
<template #trigger>
|
|
|
|
|
<NButton quaternary circle type="info">
|
|
|
|
|
<template #icon>
|
|
|
|
|
<NIcon><InformationCircle /></NIcon>
|
|
|
|
|
</template>
|
|
|
|
|
</NButton>
|
|
|
|
|
</template>
|
|
|
|
|
scid: {{ store.controledDevice.scid }} <br />status:
|
2024-04-30 09:57:47 +08:00
|
|
|
|
{{ store.controledDevice.device.status }}
|
2024-04-16 21:14:37 +08:00
|
|
|
|
</NTooltip>
|
|
|
|
|
<NButton quaternary circle type="error" @click="shutdownSC()">
|
|
|
|
|
<template #icon>
|
|
|
|
|
<NIcon><CloseCircle /></NIcon>
|
|
|
|
|
</template>
|
|
|
|
|
</NButton>
|
|
|
|
|
</div>
|
2024-04-13 09:53:41 +08:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2024-04-16 21:14:37 +08:00
|
|
|
|
<NFlex justify="space-between" align="center">
|
2024-04-25 22:48:05 +08:00
|
|
|
|
<NH4 style="margin: 20px 0" prefix="bar">可用设备</NH4>
|
2024-04-16 21:14:37 +08:00
|
|
|
|
<NButton
|
|
|
|
|
tertiary
|
|
|
|
|
circle
|
|
|
|
|
type="primary"
|
|
|
|
|
@click="refreshDevices"
|
|
|
|
|
style="margin-right: 20px"
|
|
|
|
|
>
|
|
|
|
|
<template #icon>
|
|
|
|
|
<NIcon><Refresh /></NIcon>
|
|
|
|
|
</template>
|
|
|
|
|
</NButton>
|
|
|
|
|
</NFlex>
|
|
|
|
|
<NDataTable
|
|
|
|
|
max-height="120"
|
|
|
|
|
:columns="tableCols"
|
|
|
|
|
:data="availableDevice"
|
|
|
|
|
:row-props="tableRowProps"
|
|
|
|
|
:pagination="false"
|
|
|
|
|
:bordered="false"
|
|
|
|
|
/>
|
|
|
|
|
<NDropdown
|
|
|
|
|
placement="bottom-start"
|
|
|
|
|
trigger="manual"
|
|
|
|
|
:x="menuX"
|
|
|
|
|
:y="menuY"
|
|
|
|
|
:options="menuOptions"
|
|
|
|
|
:show="showMenu"
|
|
|
|
|
:on-clickoutside="onMenuClickoutside"
|
|
|
|
|
@select="onMenuSelect"
|
|
|
|
|
/>
|
|
|
|
|
</NSpin>
|
|
|
|
|
</div>
|
|
|
|
|
</NScrollbar>
|
2024-04-13 09:53:41 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
.device {
|
|
|
|
|
color: var(--light-color);
|
|
|
|
|
background-color: var(--bg-color);
|
2024-04-16 21:14:37 +08:00
|
|
|
|
padding: 0 20px;
|
2024-04-16 22:18:42 +08:00
|
|
|
|
height: 100%;
|
2024-04-13 09:53:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.controled-device-list {
|
|
|
|
|
.controled-device {
|
|
|
|
|
padding: 10px 20px;
|
|
|
|
|
background-color: var(--content-bg-color);
|
|
|
|
|
border: 2px solid var(--content-hl-color);
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
transition: background-color 0.3s;
|
|
|
|
|
&:last-child {
|
|
|
|
|
border-bottom: 2px solid var(--content-hl-color);
|
|
|
|
|
}
|
|
|
|
|
&:hover {
|
|
|
|
|
background-color: var(--content-hl-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.device-op {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|