Merge pull request #1 from AkiChase/dev
Complete the construction of the basic mouse and key mapping
16
README.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Scrcpy-mask
|
||||||
|
|
||||||
|
A Scrcpy (control) client in Rust & Tarui aimed at providing mouse and key mapping.
|
||||||
|
|
||||||
|
Due to the delay and blurred image quality of the mirror screen. This project found another way, directly abandoned the mirror screen, and instead used a transparent mask to display the screen content behind the window, which fundamentally put an end to the delay in casting the screen.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- [x] Start scrcpy-server and connect to it
|
||||||
|
- [ ] Mouse and keyboard mapping (Partially completed)
|
||||||
|
- [ ] Visually setting the mapping
|
||||||
|
- [ ] Other setting
|
||||||
|
|
||||||
|
## contribution.
|
||||||
|
|
||||||
|
If you are interested in this project, you are welcome to submit pull request or issue. But my time and energy is limited, so I may not be able to deal with it all.
|
17
package.json
@ -10,21 +10,22 @@
|
|||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": ">=2.0.0-beta.0",
|
||||||
|
"@tauri-apps/plugin-os": "2.0.0-beta.2",
|
||||||
|
"@tauri-apps/plugin-shell": ">=2.0.0-beta.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-router": "4",
|
"vue-router": "4"
|
||||||
"@tauri-apps/api": ">=2.0.0-beta.0",
|
|
||||||
"@tauri-apps/plugin-shell": ">=2.0.0-beta.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
|
||||||
"typescript": "^5.0.2",
|
|
||||||
"vite": "^5.0.0",
|
|
||||||
"vue-tsc": "^1.8.5",
|
|
||||||
"@tauri-apps/cli": ">=2.0.0-beta.0",
|
"@tauri-apps/cli": ">=2.0.0-beta.0",
|
||||||
"@vicons/fluent": "^0.12.0",
|
"@vicons/fluent": "^0.12.0",
|
||||||
"@vicons/ionicons5": "^0.12.0",
|
"@vicons/ionicons5": "^0.12.0",
|
||||||
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"naive-ui": "^2.38.1",
|
"naive-ui": "^2.38.1",
|
||||||
"sass": "^1.71.1"
|
"sass": "^1.71.1",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^5.0.0",
|
||||||
|
"vue-tsc": "^1.8.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 37 KiB |
@ -2,7 +2,7 @@
|
|||||||
name = "scrcpy-mask"
|
name = "scrcpy-mask"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["AkiChase"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
@ -12,7 +12,7 @@ tauri-build = { version = "2.0.0-beta", features = [] }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2.0.0-beta", features = ["macos-private-api"] }
|
tauri = { version = "2.0.0-beta", features = ["macos-private-api"] }
|
||||||
tauri-plugin-shell = "2.0.0-beta"
|
tauri-plugin-os = "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"
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
"window:allow-close",
|
"window:allow-close",
|
||||||
"window:allow-is-maximizable",
|
"window:allow-is-maximizable",
|
||||||
"window:allow-start-dragging",
|
"window:allow-start-dragging",
|
||||||
"window:allow-unmaximize"
|
"window:allow-unmaximize",
|
||||||
|
"os:default",
|
||||||
|
"os:allow-platform"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
BIN
src-tauri/icons/128x128.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
src-tauri/icons/icon.ico
Normal file
After Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 123 KiB |
@ -5,6 +5,9 @@ use std::{
|
|||||||
process::{Child, Command, Stdio},
|
process::{Child, Command, Stdio},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
|
||||||
use anyhow::{Context, Ok, Result};
|
use anyhow::{Context, Ok, Result};
|
||||||
|
|
||||||
#[derive(Clone, Debug, serde::Serialize)]
|
#[derive(Clone, Debug, serde::Serialize)]
|
||||||
@ -89,8 +92,15 @@ pub struct Adb;
|
|||||||
/// But some output of command won't be output, like adb service startup information.
|
/// But some output of command won't be output, like adb service startup information.
|
||||||
impl Adb {
|
impl Adb {
|
||||||
fn cmd_base(res_dir: &PathBuf) -> Command {
|
fn cmd_base(res_dir: &PathBuf) -> Command {
|
||||||
|
#[cfg(target_os = "windows")]{
|
||||||
|
let mut cmd = Command::new(ResHelper::get_file_path(res_dir, ResourceName::Adb));
|
||||||
|
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "windows"))]{
|
||||||
Command::new(ResHelper::get_file_path(res_dir, ResourceName::Adb))
|
Command::new(ResHelper::get_file_path(res_dir, ResourceName::Adb))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// execute "adb devices" and return devices list
|
/// execute "adb devices" and return devices list
|
||||||
pub fn cmd_devices(res_dir: &PathBuf) -> Result<Vec<Device>> {
|
pub fn cmd_devices(res_dir: &PathBuf) -> Result<Vec<Device>> {
|
||||||
|
@ -96,7 +96,7 @@ fn start_scrcpy_server(
|
|||||||
let share_app = app.clone();
|
let share_app = app.clone();
|
||||||
let listen_handler = share_app.listen("front-command", move |event| {
|
let listen_handler = share_app.listen("front-command", move |event| {
|
||||||
let sender = front_msg_sender.clone();
|
let sender = front_msg_sender.clone();
|
||||||
println!("收到front-command: {}", event.payload());
|
// println!("收到front-command: {}", event.payload());
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = sender.send(event.payload().into()).await {
|
if let Err(e) = sender.send(event.payload().into()).await {
|
||||||
println!("front-command转发失败: {}", e);
|
println!("front-command转发失败: {}", e);
|
||||||
@ -130,9 +130,44 @@ async fn main() {
|
|||||||
.join("resource"),
|
.join("resource"),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let main_window = app.get_webview_window("main").unwrap();
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let scale_factor = main_window.scale_factor().unwrap();
|
||||||
|
main_window
|
||||||
|
.set_size(tauri::Size::Physical(tauri::PhysicalSize {
|
||||||
|
width: 1350,
|
||||||
|
height: 750,
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
main_window
|
||||||
|
.with_webview(move |webview| {
|
||||||
|
unsafe {
|
||||||
|
// see https://docs.rs/webview2-com/0.19.1/webview2_com/Microsoft/Web/WebView2/Win32/struct.ICoreWebView2Controller.html
|
||||||
|
webview
|
||||||
|
.controller()
|
||||||
|
.SetZoomFactor(1.0 / scale_factor)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
main_window
|
||||||
|
.set_size(tauri::Size::Logical(tauri::LogicalSize {
|
||||||
|
width: 1350.,
|
||||||
|
height: 750.,
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_os::init())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
adb_devices,
|
adb_devices,
|
||||||
get_screen_size,
|
get_screen_size,
|
||||||
|
@ -173,7 +173,6 @@ async fn recv_front_msg(
|
|||||||
&mut write_half,
|
&mut write_half,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
println!("控制信息发送完成!");
|
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
// 处理Scrcpy Mask命令
|
// 处理Scrcpy Mask命令
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"productName": "scrcpy-mask",
|
"productName": "scrcpy-mask",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"identifier": "com.tauri.dev",
|
"identifier": "com.akichase.mask",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
"devUrl": "http://localhost:1420",
|
"devUrl": "http://localhost:1420",
|
||||||
@ -25,10 +25,12 @@
|
|||||||
"active": true,
|
"active": true,
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/icon.icns"
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
"resources":[
|
"resources": ["resource/*"]
|
||||||
"resource/*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
pushServerFile,
|
pushServerFile,
|
||||||
forwardServerPort,
|
forwardServerPort,
|
||||||
startScrcpyServer,
|
startScrcpyServer,
|
||||||
|
getScreenSize,
|
||||||
} from "../invoke";
|
} from "../invoke";
|
||||||
import {
|
import {
|
||||||
NH4,
|
NH4,
|
||||||
@ -43,9 +44,7 @@ const store = useGlobalStore();
|
|||||||
const port = ref(27183);
|
const port = ref(27183);
|
||||||
|
|
||||||
//#region listener
|
//#region listener
|
||||||
const deviceWaitForMetadataTask: ((
|
const deviceWaitForMetadataTask: ((deviceName: string) => void)[] = [];
|
||||||
deviceName: string
|
|
||||||
) => void)[] = [];
|
|
||||||
|
|
||||||
let unlisten: UnlistenFn | undefined;
|
let unlisten: UnlistenFn | undefined;
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@ -175,6 +174,8 @@ async function onMenuSelect(key: string) {
|
|||||||
"00000000" + Math.floor(Math.random() * 100000).toString(16)
|
"00000000" + Math.floor(Math.random() * 100000).toString(16)
|
||||||
).slice(-8);
|
).slice(-8);
|
||||||
|
|
||||||
|
let screenSize = await getScreenSize(device.id);
|
||||||
|
|
||||||
await pushServerFile(device.id);
|
await pushServerFile(device.id);
|
||||||
await forwardServerPort(device.id, scid, port.value);
|
await forwardServerPort(device.id, scid, port.value);
|
||||||
await startScrcpyServer(device.id, scid, `127.0.0.1:${port.value}`);
|
await startScrcpyServer(device.id, scid, `127.0.0.1:${port.value}`);
|
||||||
@ -185,7 +186,8 @@ async function onMenuSelect(key: string) {
|
|||||||
scid,
|
scid,
|
||||||
deviceName,
|
deviceName,
|
||||||
device,
|
device,
|
||||||
}
|
screenSize,
|
||||||
|
};
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
store.hideLoading();
|
store.hideLoading();
|
||||||
});
|
});
|
||||||
@ -200,6 +202,13 @@ async function refreshDevices() {
|
|||||||
devices.value = await adbDevices();
|
devices.value = await adbDevices();
|
||||||
store.hideLoading();
|
store.hideLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const screenSizeInfo = computed(() => {
|
||||||
|
if (store.controledDevice) {
|
||||||
|
return `${store.controledDevice.screenSize[0]} x ${store.controledDevice.screenSize[1]}`;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -221,7 +230,11 @@ async function refreshDevices() {
|
|||||||
v-if="!store.controledDevice"
|
v-if="!store.controledDevice"
|
||||||
/>
|
/>
|
||||||
<div class="controled-device" v-if="store.controledDevice">
|
<div class="controled-device" v-if="store.controledDevice">
|
||||||
<div>{{ store.controledDevice.deviceName }} ({{ store.controledDevice.device.id }})</div>
|
<div>
|
||||||
|
{{ store.controledDevice.deviceName }} ({{
|
||||||
|
store.controledDevice.device.id
|
||||||
|
}})
|
||||||
|
</div>
|
||||||
<div class="device-op">
|
<div class="device-op">
|
||||||
<NTooltip trigger="hover">
|
<NTooltip trigger="hover">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
@ -231,16 +244,11 @@ async function refreshDevices() {
|
|||||||
</template>
|
</template>
|
||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
scid: {{ store.controledDevice.scid }}
|
scid: {{ store.controledDevice.scid }} <br />status:
|
||||||
<br />status: {{ store.controledDevice.device.status }}
|
{{ store.controledDevice.device.status }} <br />screen:
|
||||||
|
{{ screenSizeInfo }}
|
||||||
</NTooltip>
|
</NTooltip>
|
||||||
|
<NButton quaternary circle type="error" @click="shutdownSC()">
|
||||||
<NButton
|
|
||||||
quaternary
|
|
||||||
circle
|
|
||||||
type="error"
|
|
||||||
@click="shutdownSC()"
|
|
||||||
>
|
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon><CloseCircle /></NIcon>
|
<NIcon><CloseCircle /></NIcon>
|
||||||
</template>
|
</template>
|
||||||
|
@ -15,7 +15,7 @@ async function maximizeOrRestore() {
|
|||||||
<template>
|
<template>
|
||||||
<div data-tauri-drag-region class="header">
|
<div data-tauri-drag-region class="header">
|
||||||
<NButtonGroup>
|
<NButtonGroup>
|
||||||
<NButton quaternary :focusable="false" @click="appWindow.minimize()">
|
<NButton quaternary :focusable="false" @click="getCurrent().minimize()">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon><Subtract16Regular /></NIcon>
|
<NIcon><Subtract16Regular /></NIcon>
|
||||||
</template>
|
</template>
|
||||||
|
@ -2,10 +2,12 @@
|
|||||||
import { onActivated, ref } from "vue";
|
import { onActivated, ref } from "vue";
|
||||||
import { NDialog } from "naive-ui";
|
import { NDialog } from "naive-ui";
|
||||||
import { useGlobalStore } from "../store/global";
|
import { useGlobalStore } from "../store/global";
|
||||||
import { useRouter } from "vue-router";
|
import { onBeforeRouteLeave, useRouter } from "vue-router";
|
||||||
import { getCurrent } from "@tauri-apps/api/window";
|
import {
|
||||||
import { initShortcuts } from "../hotkey";
|
initShortcuts,
|
||||||
import { getScreenSize } from "../invoke";
|
listenToKeyEvent,
|
||||||
|
unlistenToKeyEvent,
|
||||||
|
} from "../hotkey";
|
||||||
|
|
||||||
const maskRef = ref<HTMLElement | null>(null);
|
const maskRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
@ -14,20 +16,26 @@ const router = useRouter();
|
|||||||
|
|
||||||
let isShortcutInited = false;
|
let isShortcutInited = false;
|
||||||
|
|
||||||
|
onBeforeRouteLeave(() => {
|
||||||
|
if (isShortcutInited) {
|
||||||
|
if (maskRef.value) {
|
||||||
|
unlistenToKeyEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onActivated(async () => {
|
onActivated(async () => {
|
||||||
if (isShortcutInited) {
|
if (isShortcutInited) {
|
||||||
maskRef.value?.focus();
|
if (maskRef.value) {
|
||||||
|
listenToKeyEvent();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (store.controledDevice) {
|
if (store.controledDevice) {
|
||||||
let screenSize = await getScreenSize(store.controledDevice.device.id);
|
|
||||||
if (maskRef.value) {
|
if (maskRef.value) {
|
||||||
const appWindow = getCurrent();
|
initShortcuts(store.controledDevice.screenSize, maskRef.value);
|
||||||
let posFactor = await appWindow.scaleFactor();
|
listenToKeyEvent();
|
||||||
initShortcuts(maskRef.value, posFactor, screenSize);
|
|
||||||
isShortcutInited = true;
|
isShortcutInited = true;
|
||||||
maskRef.value.focus();
|
|
||||||
console.log("热键已载入");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -36,7 +44,6 @@ function toStartServer() {
|
|||||||
router.replace({ name: "device" });
|
router.replace({ name: "device" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO 监听快捷键
|
|
||||||
// TODO 按键设置
|
// TODO 按键设置
|
||||||
// TODO 渲染按钮
|
// TODO 渲染按钮
|
||||||
</script>
|
</script>
|
||||||
@ -56,7 +63,7 @@ function toStartServer() {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-show="store.controledDevice"
|
v-show="store.controledDevice"
|
||||||
tabindex="-1"
|
@contextmenu.prevent
|
||||||
class="mask"
|
class="mask"
|
||||||
ref="maskRef"
|
ref="maskRef"
|
||||||
></div>
|
></div>
|
||||||
@ -66,11 +73,7 @@ function toStartServer() {
|
|||||||
.mask {
|
.mask {
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: 0 0 5px var(--primary-color);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.notice {
|
.notice {
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
@ -10,14 +10,47 @@ import {
|
|||||||
} from "@vicons/ionicons5";
|
} from "@vicons/ionicons5";
|
||||||
import { Keyboard24Regular } from "@vicons/fluent";
|
import { Keyboard24Regular } from "@vicons/fluent";
|
||||||
import { NIcon } from "naive-ui";
|
import { NIcon } from "naive-ui";
|
||||||
|
import { useGlobalStore } from "../store/global";
|
||||||
|
import { sendInjectKeycode } from "../frontcommand/controlMsg";
|
||||||
|
import {
|
||||||
|
AndroidKeyEventAction,
|
||||||
|
AndroidKeycode,
|
||||||
|
AndroidMetastate,
|
||||||
|
} from "../frontcommand/android";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const store = useGlobalStore();
|
||||||
|
|
||||||
function nav(name: string) {
|
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) {
|
||||||
|
if (store.controledDevice) {
|
||||||
|
await sendInjectKeycode({
|
||||||
|
action: AndroidKeyEventAction.AKEY_EVENT_ACTION_DOWN,
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -50,17 +83,17 @@ function nav(name: string) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="nav">
|
<div class="nav">
|
||||||
<div>
|
<div @click="sendKeyCodeToDevice(AndroidKeycode.AKEYCODE_BACK)">
|
||||||
<NIcon>
|
<NIcon>
|
||||||
<ReturnDownBackOutline />
|
<ReturnDownBackOutline />
|
||||||
</NIcon>
|
</NIcon>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div @click="sendKeyCodeToDevice(AndroidKeycode.AKEYCODE_HOME)">
|
||||||
<NIcon>
|
<NIcon>
|
||||||
<StopOutline />
|
<StopOutline />
|
||||||
</NIcon>
|
</NIcon>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div @click="sendKeyCodeToDevice(AndroidKeycode.AKEYCODE_APP_SWITCH)">
|
||||||
<NIcon>
|
<NIcon>
|
||||||
<ListOutline />
|
<ListOutline />
|
||||||
</NIcon>
|
</NIcon>
|
||||||
@ -79,6 +112,7 @@ function nav(name: string) {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
@ -88,6 +122,7 @@ function nav(name: string) {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--light-color);
|
color: var(--light-color);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module {
|
.module {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getCurrent } from "@tauri-apps/api/window";
|
import { Ref, onActivated, ref } from "vue";
|
||||||
import { onActivated, onMounted, ref } from "vue";
|
|
||||||
import { onBeforeRouteLeave } from "vue-router";
|
import { onBeforeRouteLeave } from "vue-router";
|
||||||
|
|
||||||
// TODO 添加右侧按键列表用于拖放
|
// TODO 添加右侧按键列表用于拖放
|
||||||
@ -10,13 +9,12 @@ const keyboardElement = ref<HTMLElement | null>(null);
|
|||||||
const mouseX = ref(0);
|
const mouseX = ref(0);
|
||||||
const mouseY = ref(0);
|
const mouseY = ref(0);
|
||||||
|
|
||||||
let posFactor = 1;
|
|
||||||
function clientxToPosx(clientx: number) {
|
function clientxToPosx(clientx: number) {
|
||||||
return clientx < 70 ? 0 : Math.floor((clientx - 70) * posFactor);
|
return clientx < 70 ? 0 : Math.floor(clientx - 70);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clientyToPosy(clienty: number) {
|
function clientyToPosy(clienty: number) {
|
||||||
return clienty < 30 ? 0 : Math.floor((clienty - 30) * posFactor);
|
return clienty < 30 ? 0 : Math.floor(clienty - 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
let ignoreMousemove = true;
|
let ignoreMousemove = true;
|
||||||
@ -27,17 +25,23 @@ function mousemoveHandler(event: MouseEvent) {
|
|||||||
mouseY.value = clientyToPosy(event.clientY);
|
mouseY.value = clientyToPosy(event.clientY);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
const keyboardCodeList: Ref<string[]> = ref([]);
|
||||||
const appWindow = getCurrent();
|
function keyupHandler(event: KeyboardEvent) {
|
||||||
posFactor = await appWindow.scaleFactor();
|
event.preventDefault();
|
||||||
});
|
if (keyboardCodeList.value.length > 10) {
|
||||||
|
keyboardCodeList.value.shift();
|
||||||
|
keyboardCodeList.value.push(event.code);
|
||||||
|
} else keyboardCodeList.value.push(event.code);
|
||||||
|
}
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
keyboardElement.value?.addEventListener("mousemove", mousemoveHandler);
|
keyboardElement.value?.addEventListener("mousemove", mousemoveHandler);
|
||||||
|
document.addEventListener("keyup", keyupHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeRouteLeave(() => {
|
onBeforeRouteLeave(() => {
|
||||||
keyboardElement.value?.removeEventListener("mousemove", mousemoveHandler);
|
keyboardElement.value?.removeEventListener("mousemove", mousemoveHandler);
|
||||||
|
document.removeEventListener("keyup", keyupHandler);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -45,6 +49,9 @@ onBeforeRouteLeave(() => {
|
|||||||
<div ref="keyboardElement" class="keyboard">
|
<div ref="keyboardElement" class="keyboard">
|
||||||
此处最好用其他颜色的蒙版,和右侧的按键列表区同色
|
此处最好用其他颜色的蒙版,和右侧的按键列表区同色
|
||||||
<div>{{ mouseX }}, {{ mouseY }}</div>
|
<div>{{ mouseX }}, {{ mouseY }}</div>
|
||||||
|
<div v-for="code in keyboardCodeList">
|
||||||
|
{{ code }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -12,32 +12,49 @@ import {
|
|||||||
NIcon,
|
NIcon,
|
||||||
FormInst,
|
FormInst,
|
||||||
useMessage,
|
useMessage,
|
||||||
|
NP,
|
||||||
} from "naive-ui";
|
} from "naive-ui";
|
||||||
import {
|
import {
|
||||||
PhysicalPosition,
|
PhysicalPosition,
|
||||||
PhysicalSize,
|
PhysicalSize,
|
||||||
getCurrent,
|
getCurrent,
|
||||||
} from "@tauri-apps/api/window";
|
} from "@tauri-apps/api/window";
|
||||||
|
import { platform } from "@tauri-apps/plugin-os";
|
||||||
import { SettingsOutline } from "@vicons/ionicons5";
|
import { SettingsOutline } from "@vicons/ionicons5";
|
||||||
import { UnlistenFn } from "@tauri-apps/api/event";
|
import { UnlistenFn } from "@tauri-apps/api/event";
|
||||||
|
|
||||||
let unlistenResize: UnlistenFn = () => {};
|
let unlistenResize: UnlistenFn = () => {};
|
||||||
let unlistenMove: UnlistenFn = () => {};
|
let unlistenMove: UnlistenFn = () => {};
|
||||||
|
|
||||||
async function refreshAreaModel(size?: PhysicalSize, pos?: PhysicalPosition) {
|
let factor = 1;
|
||||||
const appWindow = getCurrent();
|
let platformName = "";
|
||||||
const factor = await appWindow.scaleFactor();
|
|
||||||
// header size and sidebar size
|
|
||||||
const mt = 30 * factor;
|
|
||||||
const ml = 70 * factor;
|
|
||||||
|
|
||||||
|
// macos: use logical position and size to refresh the area model
|
||||||
|
// others: use pyhsical position and size to refresh the area model
|
||||||
|
async function refreshAreaModel(size?: PhysicalSize, pos?: PhysicalPosition) {
|
||||||
|
// header size and sidebar size
|
||||||
|
const mt = 30;
|
||||||
|
const ml = 70;
|
||||||
|
|
||||||
|
if (platformName === "macos") {
|
||||||
|
// use logical position and size
|
||||||
|
if (size !== undefined) {
|
||||||
|
areaModel.value.sizeW = Math.floor((size.width - ml) / factor);
|
||||||
|
areaModel.value.sizeH = Math.floor((size.height - mt) / factor);
|
||||||
|
}
|
||||||
|
if (pos !== undefined) {
|
||||||
|
areaModel.value.posX = Math.floor((pos.x + ml) / factor);
|
||||||
|
areaModel.value.posY = Math.floor((pos.y + mt) / factor);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (size !== undefined) {
|
||||||
|
areaModel.value.sizeW = Math.floor(size.width - ml);
|
||||||
|
areaModel.value.sizeH = Math.floor(size.height - mt);
|
||||||
|
}
|
||||||
if (pos !== undefined) {
|
if (pos !== undefined) {
|
||||||
areaModel.value.posX = Math.floor(pos.x + ml);
|
areaModel.value.posX = Math.floor(pos.x + ml);
|
||||||
areaModel.value.posY = Math.floor(pos.y + mt);
|
areaModel.value.posY = Math.floor(pos.y + mt);
|
||||||
}
|
}
|
||||||
if (size !== undefined) {
|
|
||||||
areaModel.value.sizeW = Math.floor(size.width - ml);
|
|
||||||
areaModel.value.sizeH = Math.floor(size.height - mt);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,6 +62,7 @@ const message = useMessage();
|
|||||||
|
|
||||||
const formRef = ref<FormInst | null>(null);
|
const formRef = ref<FormInst | null>(null);
|
||||||
|
|
||||||
|
// logical pos and size of the mask area
|
||||||
const areaModel = ref({
|
const areaModel = ref({
|
||||||
posX: 0,
|
posX: 0,
|
||||||
posY: 0,
|
posY: 0,
|
||||||
@ -92,6 +110,7 @@ function handleAdjustClick(e: MouseEvent) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO 等待官方合并修复分支后检查表现是否正常
|
||||||
// move and resize window to the selected window (control) area
|
// move and resize window to the selected window (control) area
|
||||||
async function adjustMaskArea() {
|
async function adjustMaskArea() {
|
||||||
// header size and sidebar size
|
// header size and sidebar size
|
||||||
@ -99,32 +118,32 @@ async function adjustMaskArea() {
|
|||||||
const ml = 70;
|
const ml = 70;
|
||||||
|
|
||||||
const appWindow = getCurrent();
|
const appWindow = getCurrent();
|
||||||
const factor = await appWindow.scaleFactor();
|
|
||||||
|
|
||||||
const pos = new PhysicalPosition(
|
const pos = new PhysicalPosition(
|
||||||
areaModel.value.posX,
|
areaModel.value.posX - ml,
|
||||||
areaModel.value.posY
|
areaModel.value.posY - mt
|
||||||
).toLogical(factor);
|
);
|
||||||
pos.y -= mt;
|
|
||||||
pos.x -= ml;
|
|
||||||
|
|
||||||
if (pos.x <= 0 || pos.y <= 0) {
|
|
||||||
message.warning("蒙版区域坐标过小,可能导致其他部分不可见");
|
|
||||||
}
|
|
||||||
|
|
||||||
const size = new PhysicalSize(
|
const size = new PhysicalSize(
|
||||||
areaModel.value.sizeW,
|
areaModel.value.sizeW + ml,
|
||||||
areaModel.value.sizeH
|
areaModel.value.sizeH + mt
|
||||||
).toLogical(factor);
|
);
|
||||||
size.width += ml;
|
|
||||||
size.height += mt;
|
|
||||||
|
|
||||||
|
if (platformName === "macos") {
|
||||||
|
// use logical position and size
|
||||||
|
await appWindow.setPosition(pos.toLogical(factor));
|
||||||
|
await appWindow.setSize(size.toLogical(factor));
|
||||||
|
} else {
|
||||||
await appWindow.setPosition(pos);
|
await appWindow.setPosition(pos);
|
||||||
await appWindow.setSize(size);
|
await appWindow.setSize(size);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const appWindow = getCurrent();
|
const appWindow = getCurrent();
|
||||||
|
factor = await appWindow.scaleFactor();
|
||||||
|
platformName = await platform();
|
||||||
|
|
||||||
unlistenResize = await appWindow.onResized(({ payload: size }) => {
|
unlistenResize = await appWindow.onResized(({ payload: size }) => {
|
||||||
refreshAreaModel(size, undefined);
|
refreshAreaModel(size, undefined);
|
||||||
});
|
});
|
||||||
@ -194,6 +213,7 @@ onUnmounted(() => {
|
|||||||
/>
|
/>
|
||||||
</NFormItemGi>
|
</NFormItemGi>
|
||||||
</NGrid>
|
</NGrid>
|
||||||
|
<NP>提示:使用物理坐标、尺寸</NP>
|
||||||
</NForm>
|
</NForm>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
791
src/hotkey.ts
@ -15,6 +15,7 @@ export const useGlobalStore = defineStore("counter", () => {
|
|||||||
scid: string;
|
scid: string;
|
||||||
deviceName: string;
|
deviceName: string;
|
||||||
device: Device;
|
device: Device;
|
||||||
|
screenSize: [number, number];
|
||||||
}
|
}
|
||||||
|
|
||||||
const controledDevice: Ref<ControledDevice|null> = ref(null);
|
const controledDevice: Ref<ControledDevice|null> = ref(null);
|
||||||
|