mirror of
https://github.com/AkiChase/scrcpy-mask
synced 2025-05-12 18:58:04 +08:00
489 lines
12 KiB
Vue
489 lines
12 KiB
Vue
<script setup lang="ts">
|
|
import {
|
|
Ref,
|
|
computed,
|
|
h,
|
|
nextTick,
|
|
onActivated,
|
|
onMounted,
|
|
onUnmounted,
|
|
ref,
|
|
} from "vue";
|
|
import {
|
|
Device,
|
|
adbDevices,
|
|
pushServerFile,
|
|
forwardServerPort,
|
|
startScrcpyServer,
|
|
getDeviceScreenSize,
|
|
adbConnect,
|
|
getCurClientInfo,
|
|
} from "../invoke";
|
|
import {
|
|
NH4,
|
|
NInput,
|
|
NInputNumber,
|
|
NButton,
|
|
NDataTable,
|
|
NDropdown,
|
|
NEmpty,
|
|
NTooltip,
|
|
NFlex,
|
|
NIcon,
|
|
NSpin,
|
|
NScrollbar,
|
|
DataTableColumns,
|
|
DropdownOption,
|
|
useDialog,
|
|
useMessage,
|
|
NInputGroup,
|
|
} from "naive-ui";
|
|
import { CloseCircle, InformationCircle, Refresh } from "@vicons/ionicons5";
|
|
import { UnlistenFn, listen } from "@tauri-apps/api/event";
|
|
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();
|
|
const store = useGlobalStore();
|
|
const message = useMessage();
|
|
|
|
const port = ref(27183);
|
|
const wireless_address = ref("");
|
|
const ws_address = ref("");
|
|
|
|
//#region listener
|
|
let deviceWaitForMetadataTask: ((deviceName: string) => void) | null = null;
|
|
let deviceWaitForScreenSizeTask: ((w: number, h: number) => void) | null = null;
|
|
|
|
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":
|
|
deviceWaitForMetadataTask?.(payload.deviceName);
|
|
break;
|
|
case "ClipboardChanged":
|
|
console.log("ClipboardChanged", payload.clipboard);
|
|
break;
|
|
case "ClipboardSetAck":
|
|
console.log("ClipboardSetAck", payload.sequence);
|
|
break;
|
|
case "DeviceRotation":
|
|
if (deviceWaitForScreenSizeTask) {
|
|
deviceWaitForScreenSizeTask(payload.width, payload.height);
|
|
} else {
|
|
store.screenSizeW = payload.width;
|
|
store.screenSizeH = payload.height;
|
|
message.info("设备旋转");
|
|
}
|
|
break;
|
|
default:
|
|
console.log("Unknown reply", payload);
|
|
break;
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
});
|
|
});
|
|
|
|
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();
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
if (unlisten !== undefined) unlisten();
|
|
});
|
|
//#endregion
|
|
|
|
//#region table
|
|
const devices: Ref<Device[]> = ref([]);
|
|
const availableDevice = computed(() => {
|
|
return devices.value.filter((d) => {
|
|
return store.controledDevice?.deviceID !== d.id;
|
|
});
|
|
});
|
|
const tableCols: DataTableColumns = [
|
|
{
|
|
title: "ID",
|
|
key: "id",
|
|
},
|
|
{
|
|
title: t("pages.Device.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: t("pages.Device.shutdown.title"),
|
|
content: t("pages.Device.shutdown.content"),
|
|
positiveText: t("pages.Device.shutdown.positiveText"),
|
|
negativeText: t("pages.Device.shutdown.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", t("pages.Device.menu.control")),
|
|
key: "control",
|
|
},
|
|
{
|
|
label: () => h("span", t("pages.Device.menu.screen")),
|
|
key: "screen",
|
|
},
|
|
];
|
|
|
|
function onMenuClickoutside() {
|
|
showMenu.value = false;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (store.controledDevice) {
|
|
message.error(t("pages.Device.deviceControl.closeCurDevice"));
|
|
store.hideLoading();
|
|
return;
|
|
}
|
|
|
|
message.info(t("pages.Device.deviceControl.controlInfo"));
|
|
|
|
const device = devices.value[rowIndex];
|
|
|
|
let scid = (
|
|
"00000000" + Math.floor(Math.random() * 100000).toString(16)
|
|
).slice(-8);
|
|
|
|
await pushServerFile(device.id);
|
|
await forwardServerPort(device.id, scid, port.value);
|
|
await startScrcpyServer(device.id, scid, `127.0.0.1:${port.value}`);
|
|
|
|
// connection timeout check
|
|
let id = setTimeout(async () => {
|
|
if (deviceWaitForMetadataTask) {
|
|
await shutdown();
|
|
store.controledDevice = null;
|
|
store.hideLoading();
|
|
message.error(t("pages.Device.deviceControl.connectTimeout"));
|
|
}
|
|
}, 6000);
|
|
|
|
// add cb for metadata
|
|
deviceWaitForMetadataTask = (deviceName: string) => {
|
|
store.controledDevice = {
|
|
scid,
|
|
deviceName,
|
|
deviceID: device.id,
|
|
};
|
|
deviceWaitForMetadataTask = null;
|
|
if (!deviceWaitForScreenSizeTask) {
|
|
nextTick(() => {
|
|
clearTimeout(id);
|
|
store.hideLoading();
|
|
});
|
|
}
|
|
};
|
|
|
|
// add cb for screen size
|
|
deviceWaitForScreenSizeTask = (w: number, h: number) => {
|
|
store.screenSizeW = w;
|
|
store.screenSizeH = h;
|
|
deviceWaitForScreenSizeTask = null;
|
|
if (!deviceWaitForMetadataTask) {
|
|
nextTick(() => {
|
|
clearTimeout(id);
|
|
store.hideLoading();
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
async function deviceGetScreenSize() {
|
|
let id = devices.value[rowIndex].id;
|
|
const size = await getDeviceScreenSize(id);
|
|
store.hideLoading();
|
|
message.success(
|
|
t("pages.Device.deviceGetScreenSize") + `${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();
|
|
break;
|
|
}
|
|
}
|
|
//#endregion
|
|
|
|
async function refreshDevices() {
|
|
store.showLoading();
|
|
try {
|
|
devices.value = await adbDevices();
|
|
} catch (e) {
|
|
message.error(t("pages.Device.adbDeviceError"));
|
|
console.error(e);
|
|
}
|
|
store.hideLoading();
|
|
}
|
|
|
|
async function connectDevice() {
|
|
if (!wireless_address.value) {
|
|
message.error(t("pages.Device.inputWirelessAddress"));
|
|
return;
|
|
}
|
|
|
|
store.showLoading();
|
|
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>
|
|
<NScrollbar>
|
|
<div class="device">
|
|
<NSpin :show="store.showLoadingRef">
|
|
<NH4 prefix="bar">{{ $t("pages.Device.localPort") }}</NH4>
|
|
<NInputNumber
|
|
v-model:value="port"
|
|
:show-button="false"
|
|
:min="16384"
|
|
:max="49151"
|
|
:placeholder="$t('pages.Device.localPortPlaceholder')"
|
|
style="max-width: 300px"
|
|
/>
|
|
<NH4 prefix="bar">{{ $t("pages.Device.wireless") }}</NH4>
|
|
<NInputGroup style="max-width: 300px">
|
|
<NInput
|
|
v-model:value="wireless_address"
|
|
clearable
|
|
:placeholder="$t('pages.Device.wirelessPlaceholder')"
|
|
/>
|
|
<NButton type="primary" @click="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.controledDevice") }}</NH4>
|
|
<div class="controled-device-list">
|
|
<NEmpty
|
|
size="small"
|
|
:description="$t('pages.Device.noControledDevice')"
|
|
v-if="!store.controledDevice"
|
|
/>
|
|
<div class="controled-device" v-if="store.controledDevice">
|
|
<div>
|
|
{{ store.controledDevice.deviceName }} ({{
|
|
store.controledDevice.deviceID
|
|
}})
|
|
</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 }}
|
|
</NTooltip>
|
|
<NButton quaternary circle type="error" @click="shutdownSC()">
|
|
<template #icon>
|
|
<NIcon><CloseCircle /></NIcon>
|
|
</template>
|
|
</NButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<NFlex justify="space-between" align="center">
|
|
<NH4 style="margin: 20px 0" prefix="bar">{{
|
|
$t("pages.Device.availableDevice")
|
|
}}</NH4>
|
|
<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>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.device {
|
|
color: var(--light-color);
|
|
background-color: var(--bg-color);
|
|
padding: 0 20px;
|
|
height: 100%;
|
|
}
|
|
|
|
.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>
|