mirror of
https://github.com/AkiChase/scrcpy-mask
synced 2025-02-23 15:32:17 +08:00
commit
1a658d17ac
@ -6,10 +6,12 @@ Due to the delay and blurred image quality of the mirror screen. This project fo
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
- [x] Wired and wireless connections to Android devices
|
||||||
- [x] Start scrcpy-server and connect to it
|
- [x] Start scrcpy-server and connect to it
|
||||||
- [ ] Mouse and keyboard mapping (Partially completed)
|
- [x] Implement scrcpy client control protocol
|
||||||
- [ ] Visually setting the mapping
|
- [x] Mouse and keyboard mapping
|
||||||
- [ ] Other setting
|
- [x] Visually setting the mapping
|
||||||
|
- [ ] Provide external interface through websocket
|
||||||
|
|
||||||
## contribution.
|
## contribution.
|
||||||
|
|
||||||
|
@ -10,9 +10,10 @@
|
|||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": ">=2.0.0-beta.0",
|
"@tauri-apps/api": ">=2.0.0-beta.8",
|
||||||
"@tauri-apps/plugin-os": "2.0.0-beta.2",
|
"@tauri-apps/plugin-clipboard-manager": "2.1.0-beta.0",
|
||||||
"@tauri-apps/plugin-shell": ">=2.0.0-beta.0",
|
"@tauri-apps/plugin-process": "2.0.0-beta.2",
|
||||||
|
"@tauri-apps/plugin-store": "2.0.0-beta.2",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-router": "4"
|
"vue-router": "4"
|
||||||
|
@ -11,9 +11,11 @@ edition = "2021"
|
|||||||
tauri-build = { version = "2.0.0-beta", features = [] }
|
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.15", features = ["macos-private-api"] }
|
||||||
tauri-plugin-os = "2.0.0-beta"
|
tauri-plugin-store = "2.0.0-beta.4"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
anyhow = "1.0"
|
anyhow = "1.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-clipboard-manager = "2.1.0-beta.1"
|
||||||
|
tauri-plugin-process = "2.0.0-beta.3"
|
||||||
|
@ -14,7 +14,19 @@
|
|||||||
"window:allow-is-maximizable",
|
"window:allow-is-maximizable",
|
||||||
"window:allow-start-dragging",
|
"window:allow-start-dragging",
|
||||||
"window:allow-unmaximize",
|
"window:allow-unmaximize",
|
||||||
"os:default",
|
"store:default",
|
||||||
"os:allow-platform"
|
"store:allow-get",
|
||||||
|
"store:allow-set",
|
||||||
|
"store:allow-save",
|
||||||
|
"store:allow-load",
|
||||||
|
"store:allow-clear",
|
||||||
|
"store:allow-entries",
|
||||||
|
"store:allow-delete",
|
||||||
|
"clipboard-manager:default",
|
||||||
|
"clipboard-manager:allow-write-text",
|
||||||
|
"process:default",
|
||||||
|
"process:allow-restart",
|
||||||
|
"webview:default",
|
||||||
|
"webview:allow-internal-toggle-devtools"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
3
src-tauri/resource/default-key-config.json
Normal file
3
src-tauri/resource/default-key-config.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
{"list":[{"key":{"down":"KeyS","left":"KeyA","right":"KeyD","up":"KeyW"},"note":"方向轮盘","offset":175,"pointerId":1,"posX":180,"posY":560,"type":"SteeringWheel"},{"key":"KeyQ","note":"技能1","pointerId":2,"posX":950,"posY":610,"range":50,"type":"DirectionalSkill"},{"key":"AltLeft","note":"技能2","pointerId":2,"posX":1025,"posY":500,"range":50,"type":"DirectionalSkill"},{"key":"KeyE","note":"技能3","pointerId":2,"posX":1160,"posY":420,"range":50,"type":"DirectionalSkill"},{"directional":true,"key":"M4","note":"技能3","pointerId":2,"posX":1160,"posY":420,"rangeOrTime":0,"type":"TriggerWhenPressedSkill"},{"key":"M1","note":"无方向装备技能","pointerId":2,"posX":1150,"posY":280,"type":"DirectionlessSkill"},{"key":"Space","note":"取消技能","pointerId":2,"posX":1160,"posY":140,"type":"CancelSkill"},{"key":"KeyB","note":"回城","pointerId":3,"posX":650,"posY":650,"time":80,"type":"Tap"},{"key":"KeyC","note":"回复","pointerId":3,"posX":740,"posY":650,"time":80,"type":"Tap"},{"key":"KeyF","note":"召唤师技能","pointerId":2,"posX":840,"posY":650,"range":50,"type":"DirectionalSkill"},{"directional":false,"key":"ControlLeft","note":"无方向召唤师技能","pointerId":3,"posX":840,"posY":650,"rangeOrTime":80,"type":"TriggerWhenPressedSkill"},{"key":"M2","note":"攻击","pointerId":3,"posX":1165,"posY":620,"time":80,"type":"Tap"},{"key":"Digit1","note":"技能1升级","pointerId":3,"posX":880,"posY":560,"time":80,"type":"Tap"},{"key":"Digit2","note":"技能2升级","pointerId":3,"posX":960,"posY":430,"time":80,"type":"Tap"},{"key":"Digit3","note":"技能3升级","pointerId":3,"posX":1090,"posY":350,"time":80,"type":"Tap"},{"key":"Digit5","note":"快速购买1","pointerId":3,"posX":130,"posY":300,"time":80,"type":"Tap"},{"key":"Digit6","note":"快速购买2","pointerId":3,"posX":130,"posY":370,"time":80,"type":"Tap"},{"directional":false,"key":"WheelDown","note":"装备技能","pointerId":3,"posX":1150,"posY":280,"rangeOrTime":80,"type":"TriggerWhenPressedSkill"},{"key":"M3","note":"观察","pointerId":4,"posX":1000,"posY":200,"scale":0.5,"type":"Observation"},{"key":"Tab","macro":{"down":[{"args":["default",5,1185,40,80],"type":"touch"}],"loop":null,"up":[{"args":["default",5,1220,100,80],"type":"touch"}]},"note":"战绩面板","pointerId":5,"posX":1185,"posY":40,"type":"Macro"},{"key":"ShiftLeft","macro":{"down":[{"args":["default",5,40,300,80],"type":"touch"}],"loop":null,"up":[{"args":["default",5,1200,60,80],"type":"touch"}]},"note":"商店","pointerId":5,"posX":40,"posY":300,"type":"Macro"},{"key":"KeyZ","macro":{"down":[{"args":["default",5,250,230,80],"type":"touch"}],"loop":null,"up":[{"args":["default",5,640,150,80],"type":"touch"}]},"note":"地图","pointerId":5,"posX":250,"posY":230,"type":"Macro"}],"relativeSize":{"h":720,"w":1280},"title":"王者荣耀-暃"}
|
||||||
|
]
|
@ -59,30 +59,25 @@ impl Device {
|
|||||||
.context("Failed to execute 'adb shell'")?)
|
.context("Failed to execute 'adb shell'")?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cmd_screen_size(res_dir: &PathBuf, id: &str) -> Result<(u16, u16)> {
|
/// execute "adb shell wm size" to get screen size
|
||||||
|
pub fn cmd_screen_size(res_dir: &PathBuf, id: &str) -> Result<(u32, u32)> {
|
||||||
let mut adb_command = Adb::cmd_base(res_dir);
|
let mut adb_command = Adb::cmd_base(res_dir);
|
||||||
let output = adb_command
|
let output = adb_command
|
||||||
.args(&["-s", id, "shell", "wm", "size"])
|
.args(&["-s", id, "shell", "wm", "size"])
|
||||||
.output()
|
.output()
|
||||||
.context("Failed to execute 'adb shell wm size'")?;
|
.context("Failed to execute 'adb shell wm size'")?;
|
||||||
let lines = output.stdout.lines();
|
|
||||||
let mut size = (0, 0);
|
for line in output.stdout.lines() {
|
||||||
for line in lines {
|
if let std::result::Result::Ok(line) = line {
|
||||||
if let std::result::Result::Ok(s) = line {
|
if line.starts_with("Physical size: ") {
|
||||||
println!("{}", s);
|
let size_str = line.trim_start_matches("Physical size: ").split('x');
|
||||||
if s.starts_with("Physical size:") {
|
let width = size_str.clone().next().unwrap().parse::<u32>().unwrap();
|
||||||
let mut iter = s.split_whitespace();
|
let height = size_str.clone().last().unwrap().parse::<u32>().unwrap();
|
||||||
iter.next();
|
return Ok((width, height));
|
||||||
iter.next();
|
|
||||||
let mut size_str = iter.next().unwrap().split('x');
|
|
||||||
let width = size_str.next().unwrap().parse::<u16>().unwrap();
|
|
||||||
let height = size_str.next().unwrap().parse::<u16>().unwrap();
|
|
||||||
size = (width, height);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(size)
|
Err(anyhow::anyhow!("Failed to get screen size"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,12 +87,14 @@ 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")]{
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
let mut cmd = Command::new(ResHelper::get_file_path(res_dir, ResourceName::Adb));
|
let mut cmd = Command::new(ResHelper::get_file_path(res_dir, ResourceName::Adb));
|
||||||
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
|
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
|
||||||
cmd
|
cmd
|
||||||
}
|
}
|
||||||
#[cfg(not(target_os = "windows"))]{
|
#[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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -169,4 +166,22 @@ impl Adb {
|
|||||||
.context("Failed to execute 'adb start-server'")?;
|
.context("Failed to execute 'adb start-server'")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn cmd_connect(res_dir: &PathBuf, address: &str) -> Result<String> {
|
||||||
|
let mut adb_command = Adb::cmd_base(res_dir);
|
||||||
|
let output = adb_command
|
||||||
|
.args(&["connect", address])
|
||||||
|
.output()
|
||||||
|
.context(format!("Failed to execute 'adb connect {}'", address))?;
|
||||||
|
|
||||||
|
let res = String::from_utf8(output.stdout)?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn t() {
|
||||||
|
let res_dir = PathBuf::from("/Users/akichase/Projects/github/scrcpy-mask/src-tauri/resource/");
|
||||||
|
let res = Adb::cmd_connect(&res_dir, "127.0.0.1:1234").unwrap();
|
||||||
|
println!("{}", res)
|
||||||
}
|
}
|
||||||
|
@ -40,8 +40,8 @@ impl ScrcpyClient {
|
|||||||
Adb::cmd_forward_remove(res_dir)
|
Adb::cmd_forward_remove(res_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// get the screen size of the device
|
// get the screen size of the device
|
||||||
pub fn get_screen_size(res_dir: &PathBuf, id: &str) -> Result<(u16, u16)> {
|
pub fn get_device_screen_size(res_dir: &PathBuf, id: &str) -> Result<(u32, u32)> {
|
||||||
Device::cmd_screen_size(res_dir, id)
|
Device::cmd_screen_size(res_dir, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
pub mod adb;
|
pub mod adb;
|
||||||
pub mod resource;
|
|
||||||
pub mod client;
|
|
||||||
pub mod socket;
|
|
||||||
pub mod binary;
|
pub mod binary;
|
||||||
|
pub mod client;
|
||||||
pub mod control_msg;
|
pub mod control_msg;
|
||||||
|
pub mod resource;
|
||||||
pub mod scrcpy_mask_cmd;
|
pub mod scrcpy_mask_cmd;
|
||||||
|
pub mod socket;
|
||||||
|
@ -4,10 +4,10 @@
|
|||||||
use scrcpy_mask::{
|
use scrcpy_mask::{
|
||||||
adb::{Adb, Device},
|
adb::{Adb, Device},
|
||||||
client::ScrcpyClient,
|
client::ScrcpyClient,
|
||||||
resource::ResHelper,
|
resource::{ResHelper, ResourceName},
|
||||||
socket::connect_socket,
|
socket::connect_socket,
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::{fs::read_to_string, sync::Arc};
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@ -20,16 +20,6 @@ fn adb_devices(app: tauri::AppHandle) -> Result<Vec<Device>, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
/// get screen size of the device
|
|
||||||
fn get_screen_size(id: String, app: tauri::AppHandle) -> Result<(u16, u16), String> {
|
|
||||||
let dir = app.path().resource_dir().unwrap().join("resource");
|
|
||||||
match ScrcpyClient::get_screen_size(&dir, &id) {
|
|
||||||
Ok(size) => Ok(size),
|
|
||||||
Err(e) => Err(e.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
/// forward local port to the device port
|
/// forward local port to the device port
|
||||||
fn forward_server_port(
|
fn forward_server_port(
|
||||||
@ -118,10 +108,99 @@ fn start_scrcpy_server(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
/// get device screen size
|
||||||
|
fn get_device_screen_size(id: String, app: tauri::AppHandle) -> Result<(u32, u32), String> {
|
||||||
|
let dir = app.path().resource_dir().unwrap().join("resource");
|
||||||
|
match ScrcpyClient::get_device_screen_size(&dir, &id) {
|
||||||
|
Ok(size) => Ok(size),
|
||||||
|
Err(e) => Err(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
/// connect to wireless device
|
||||||
|
fn adb_connect(address: String, app: tauri::AppHandle) -> Result<String, String> {
|
||||||
|
let dir = app.path().resource_dir().unwrap().join("resource");
|
||||||
|
match Adb::cmd_connect(&dir, &address) {
|
||||||
|
Ok(res) => Ok(res),
|
||||||
|
Err(e) => Err(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
/// load default key mapping config file
|
||||||
|
fn load_default_keyconfig(app: tauri::AppHandle) -> Result<String, String> {
|
||||||
|
let dir = app.path().resource_dir().unwrap().join("resource");
|
||||||
|
let file = ResHelper::get_file_path(&dir, ResourceName::DefaultKeyConfig);
|
||||||
|
match read_to_string(file) {
|
||||||
|
Ok(content) => Ok(content),
|
||||||
|
Err(e) => Err(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_process::init())
|
||||||
|
.plugin(tauri_plugin_clipboard_manager::init())
|
||||||
|
.plugin(tauri_plugin_store::Builder::new().build())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
let stores = app
|
||||||
|
.app_handle()
|
||||||
|
.state::<tauri_plugin_store::StoreCollection<tauri::Wry>>();
|
||||||
|
let path = std::path::PathBuf::from("store.bin");
|
||||||
|
tauri_plugin_store::with_store(app.app_handle().clone(), stores, path, |store| {
|
||||||
|
// restore window position and size
|
||||||
|
match store.get("maskArea") {
|
||||||
|
Some(value) => {
|
||||||
|
let pos_x = value["posX"].as_i64().unwrap_or(100);
|
||||||
|
let pos_y = value["posY"].as_i64().unwrap_or(100);
|
||||||
|
let size_w = value["sizeW"].as_i64().unwrap_or(800);
|
||||||
|
let size_h = value["sizeH"].as_i64().unwrap_or(600);
|
||||||
|
let main_window: tauri::WebviewWindow =
|
||||||
|
app.get_webview_window("main").unwrap();
|
||||||
|
main_window.set_zoom(1.).unwrap();
|
||||||
|
main_window
|
||||||
|
.set_position(tauri::Position::Logical(tauri::LogicalPosition {
|
||||||
|
x: (pos_x - 70) as f64,
|
||||||
|
y: (pos_y - 30) as f64,
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
main_window
|
||||||
|
.set_size(tauri::Size::Logical(tauri::LogicalSize {
|
||||||
|
width: (size_w + 70) as f64,
|
||||||
|
height: (size_h + 30) as f64,
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let main_window: tauri::WebviewWindow =
|
||||||
|
app.get_webview_window("main").unwrap();
|
||||||
|
main_window
|
||||||
|
.set_size(tauri::Size::Logical(tauri::LogicalSize {
|
||||||
|
width: (800 + 70) as f64,
|
||||||
|
height: (600 + 30) as f64,
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
store
|
||||||
|
.insert(
|
||||||
|
"maskArea".to_string(),
|
||||||
|
serde_json::json!({
|
||||||
|
"posX": 0,
|
||||||
|
"posY": 0,
|
||||||
|
"sizeW": 800,
|
||||||
|
"sizeH": 600
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// check resource files
|
// check resource files
|
||||||
ResHelper::res_init(
|
ResHelper::res_init(
|
||||||
&app.path()
|
&app.path()
|
||||||
@ -130,50 +209,16 @@ 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_os::init())
|
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
adb_devices,
|
adb_devices,
|
||||||
get_screen_size,
|
|
||||||
forward_server_port,
|
forward_server_port,
|
||||||
push_server_file,
|
push_server_file,
|
||||||
start_scrcpy_server
|
start_scrcpy_server,
|
||||||
|
get_device_screen_size,
|
||||||
|
adb_connect,
|
||||||
|
load_default_keyconfig
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
@ -4,6 +4,7 @@ use std::path::PathBuf;
|
|||||||
pub enum ResourceName {
|
pub enum ResourceName {
|
||||||
Adb,
|
Adb,
|
||||||
ScrcpyServer,
|
ScrcpyServer,
|
||||||
|
DefaultKeyConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ResHelper {
|
pub struct ResHelper {
|
||||||
@ -32,6 +33,7 @@ impl ResHelper {
|
|||||||
ResourceName::Adb => dir.join("adb"),
|
ResourceName::Adb => dir.join("adb"),
|
||||||
|
|
||||||
ResourceName::ScrcpyServer => dir.join("scrcpy-server-v2.4"),
|
ResourceName::ScrcpyServer => dir.join("scrcpy-server-v2.4"),
|
||||||
|
ResourceName::DefaultKeyConfig => dir.join("default-key-config.json"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,12 +77,13 @@ pub async fn handle_sm_cmd(
|
|||||||
let h = payload["screen"]["h"].as_u64().unwrap() as u16;
|
let h = payload["screen"]["h"].as_u64().unwrap() as u16;
|
||||||
let x = payload["pos"]["x"].as_i64().unwrap() as i32;
|
let x = payload["pos"]["x"].as_i64().unwrap() as i32;
|
||||||
let y = payload["pos"]["y"].as_i64().unwrap() as i32;
|
let y = payload["pos"]["y"].as_i64().unwrap() as i32;
|
||||||
|
let time = payload["time"].as_u64().unwrap();
|
||||||
match payload["action"].as_u64().unwrap() {
|
match payload["action"].as_u64().unwrap() {
|
||||||
// default
|
// default
|
||||||
0 => {
|
0 => {
|
||||||
// down
|
// down
|
||||||
touch(ctrl_msg_type, pointer_id, x, y, w, h, 0, writer).await;
|
touch(ctrl_msg_type, pointer_id, x, y, w, h, 0, writer).await;
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
tokio::time::sleep(tokio::time::Duration::from_millis(time)).await;
|
||||||
// up
|
// up
|
||||||
touch(ctrl_msg_type, pointer_id, x, y, w, h, 1, writer).await;
|
touch(ctrl_msg_type, pointer_id, x, y, w, h, 1, writer).await;
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,6 @@ pub async fn connect_socket(
|
|||||||
listen_handler: u32,
|
listen_handler: u32,
|
||||||
app: Arc<tauri::AppHandle>,
|
app: Arc<tauri::AppHandle>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
|
|
||||||
let client = TcpStream::connect(address)
|
let client = TcpStream::connect(address)
|
||||||
.await
|
.await
|
||||||
.context("Socket connect failed")?;
|
.context("Socket connect failed")?;
|
||||||
@ -193,8 +192,7 @@ async fn recv_front_msg(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else{
|
|
||||||
eprintln!("fc-command非法");
|
eprintln!("fc-command非法");
|
||||||
eprintln!("{:?}", payload);
|
eprintln!("{:?}", payload);
|
||||||
}
|
}
|
||||||
|
74
src/App.vue
74
src/App.vue
@ -7,6 +7,71 @@ import {
|
|||||||
NMessageProvider,
|
NMessageProvider,
|
||||||
NDialogProvider,
|
NDialogProvider,
|
||||||
} from "naive-ui";
|
} from "naive-ui";
|
||||||
|
import { Store } from "@tauri-apps/plugin-store";
|
||||||
|
import { KeyMappingConfig } from "./keyMappingConfig";
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
import { useGlobalStore } from "./store/global";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
const store = useGlobalStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
router.replace({ name: "mask" });
|
||||||
|
|
||||||
|
const localStore = new Store("store.bin");
|
||||||
|
// loading screenSize from local store
|
||||||
|
const screenSize = await localStore.get<{ sizeW: number; sizeH: number }>(
|
||||||
|
"screenSize"
|
||||||
|
);
|
||||||
|
if (screenSize !== null) {
|
||||||
|
store.screenSizeW = screenSize.sizeW;
|
||||||
|
store.screenSizeH = screenSize.sizeH;
|
||||||
|
}
|
||||||
|
|
||||||
|
// loading keyMappingConfigList from local store
|
||||||
|
let keyMappingConfigList = await localStore.get<KeyMappingConfig[]>(
|
||||||
|
"keyMappingConfigList"
|
||||||
|
);
|
||||||
|
if (keyMappingConfigList === null || keyMappingConfigList.length === 0) {
|
||||||
|
// add empty key mapping config
|
||||||
|
// unable to get mask element when app is not ready
|
||||||
|
// so we use the stored mask area to get relative size
|
||||||
|
const maskArea = await localStore.get<{
|
||||||
|
posX: number;
|
||||||
|
posY: number;
|
||||||
|
sizeW: number;
|
||||||
|
sizeH: number;
|
||||||
|
}>("maskArea");
|
||||||
|
let relativeSize = { w: 800, h: 600 };
|
||||||
|
if (maskArea !== null) {
|
||||||
|
relativeSize = {
|
||||||
|
w: maskArea.sizeW,
|
||||||
|
h: maskArea.sizeH,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
keyMappingConfigList = [
|
||||||
|
{
|
||||||
|
relativeSize,
|
||||||
|
title: "空白方案",
|
||||||
|
list: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
await localStore.set("keyMappingConfigList", keyMappingConfigList);
|
||||||
|
}
|
||||||
|
store.keyMappingConfigList = keyMappingConfigList;
|
||||||
|
|
||||||
|
// loading curKeyMappingIndex from local store
|
||||||
|
let curKeyMappingIndex = await localStore.get<number>("curKeyMappingIndex");
|
||||||
|
if (
|
||||||
|
curKeyMappingIndex === null ||
|
||||||
|
curKeyMappingIndex >= keyMappingConfigList.length
|
||||||
|
) {
|
||||||
|
curKeyMappingIndex = 0;
|
||||||
|
localStore.set("curKeyMappingIndex", curKeyMappingIndex);
|
||||||
|
}
|
||||||
|
store.curKeyMappingIndex = curKeyMappingIndex;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -29,7 +94,6 @@ import {
|
|||||||
.container {
|
.container {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 70px 1fr;
|
grid-template-columns: 70px 1fr;
|
||||||
grid-template-rows: 30px 1fr;
|
grid-template-rows: 30px 1fr;
|
||||||
@ -37,4 +101,12 @@ import {
|
|||||||
"sidebar header"
|
"sidebar header"
|
||||||
"sidebar content";
|
"sidebar content";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.n-scrollbar-container {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-scrollbar-content {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -15,10 +15,13 @@ import {
|
|||||||
pushServerFile,
|
pushServerFile,
|
||||||
forwardServerPort,
|
forwardServerPort,
|
||||||
startScrcpyServer,
|
startScrcpyServer,
|
||||||
getScreenSize,
|
getDeviceScreenSize,
|
||||||
|
adbConnect,
|
||||||
} from "../invoke";
|
} from "../invoke";
|
||||||
import {
|
import {
|
||||||
NH4,
|
NH4,
|
||||||
|
NP,
|
||||||
|
NInput,
|
||||||
NInputNumber,
|
NInputNumber,
|
||||||
NButton,
|
NButton,
|
||||||
NDataTable,
|
NDataTable,
|
||||||
@ -26,25 +29,34 @@ import {
|
|||||||
NEmpty,
|
NEmpty,
|
||||||
NTooltip,
|
NTooltip,
|
||||||
NFlex,
|
NFlex,
|
||||||
|
NFormItem,
|
||||||
NIcon,
|
NIcon,
|
||||||
NSpin,
|
NSpin,
|
||||||
|
NScrollbar,
|
||||||
DataTableColumns,
|
DataTableColumns,
|
||||||
DropdownOption,
|
DropdownOption,
|
||||||
useDialog,
|
useDialog,
|
||||||
|
useMessage,
|
||||||
|
NInputGroup,
|
||||||
} from "naive-ui";
|
} from "naive-ui";
|
||||||
import { CloseCircle, InformationCircle } from "@vicons/ionicons5";
|
import { CloseCircle, InformationCircle } from "@vicons/ionicons5";
|
||||||
import { 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 { shutdown } from "../frontcommand/scrcpyMaskCmd";
|
import { shutdown } from "../frontcommand/scrcpyMaskCmd";
|
||||||
import { useGlobalStore } from "../store/global";
|
import { useGlobalStore } from "../store/global";
|
||||||
|
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
const store = useGlobalStore();
|
const store = useGlobalStore();
|
||||||
|
const message = useMessage();
|
||||||
|
|
||||||
const port = ref(27183);
|
const port = ref(27183);
|
||||||
|
const address = ref("");
|
||||||
|
|
||||||
|
const localStore = new Store("store.bin");
|
||||||
|
|
||||||
//#region listener
|
//#region listener
|
||||||
const deviceWaitForMetadataTask: ((deviceName: string) => void)[] = [];
|
let deviceWaitForMetadataTask: ((deviceName: string) => void) | null = null;
|
||||||
|
|
||||||
let unlisten: UnlistenFn | undefined;
|
let unlisten: UnlistenFn | undefined;
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@ -53,8 +65,7 @@ onMounted(async () => {
|
|||||||
let payload = JSON.parse(event.payload as string);
|
let payload = JSON.parse(event.payload as string);
|
||||||
switch (payload.type) {
|
switch (payload.type) {
|
||||||
case "MetaData":
|
case "MetaData":
|
||||||
let task = deviceWaitForMetadataTask.shift();
|
deviceWaitForMetadataTask?.(payload.deviceName);
|
||||||
task?.(payload.deviceName);
|
|
||||||
break;
|
break;
|
||||||
case "ClipboardChanged":
|
case "ClipboardChanged":
|
||||||
console.log("剪切板变动", payload.clipboard);
|
console.log("剪切板变动", payload.clipboard);
|
||||||
@ -154,44 +165,92 @@ const menuOptions: DropdownOption[] = [
|
|||||||
label: () => h("span", "控制此设备"),
|
label: () => h("span", "控制此设备"),
|
||||||
key: "control",
|
key: "control",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: () => h("span", "获取屏幕尺寸"),
|
||||||
|
key: "screen",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function onMenuClickoutside() {
|
function onMenuClickoutside() {
|
||||||
showMenu.value = false;
|
showMenu.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deviceControl() {
|
||||||
|
if (!port.value) {
|
||||||
|
port.value = 27183;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(store.screenSizeW > 0) || !(store.screenSizeH > 0)) {
|
||||||
|
message.error("请正确输入当前控制设备的屏幕尺寸");
|
||||||
|
store.screenSizeW = 0;
|
||||||
|
store.screenSizeH = 0;
|
||||||
|
store.hideLoading();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (store.controledDevice) {
|
||||||
|
message.error("请先关闭当前控制设备");
|
||||||
|
store.hideLoading();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStore.set("screenSize", {
|
||||||
|
sizeW: store.screenSizeW,
|
||||||
|
sizeH: store.screenSizeH,
|
||||||
|
});
|
||||||
|
message.info("屏幕尺寸已保存,正在启动控制服务,请保持设备亮屏");
|
||||||
|
|
||||||
|
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("设备连接超时");
|
||||||
|
}
|
||||||
|
}, 6000);
|
||||||
|
|
||||||
|
// add cb for metadata
|
||||||
|
deviceWaitForMetadataTask = (deviceName: string) => {
|
||||||
|
store.controledDevice = {
|
||||||
|
scid,
|
||||||
|
deviceName,
|
||||||
|
device,
|
||||||
|
};
|
||||||
|
nextTick(() => {
|
||||||
|
deviceWaitForMetadataTask = null;
|
||||||
|
clearTimeout(id);
|
||||||
|
store.hideLoading();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
async function onMenuSelect(key: string) {
|
||||||
showMenu.value = false;
|
showMenu.value = false;
|
||||||
store.showLoading();
|
store.showLoading();
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "control":
|
case "control":
|
||||||
if (!port.value) {
|
await deviceControl();
|
||||||
port.value = 27183;
|
break;
|
||||||
}
|
case "screen":
|
||||||
let device = devices.value[rowIndex];
|
await deviceGetScreenSize();
|
||||||
|
|
||||||
let scid = (
|
|
||||||
"00000000" + Math.floor(Math.random() * 100000).toString(16)
|
|
||||||
).slice(-8);
|
|
||||||
|
|
||||||
let screenSize = await getScreenSize(device.id);
|
|
||||||
|
|
||||||
await pushServerFile(device.id);
|
|
||||||
await forwardServerPort(device.id, scid, port.value);
|
|
||||||
await startScrcpyServer(device.id, scid, `127.0.0.1:${port.value}`);
|
|
||||||
|
|
||||||
// add cb for metadata
|
|
||||||
deviceWaitForMetadataTask.push((deviceName: string) => {
|
|
||||||
store.controledDevice = {
|
|
||||||
scid,
|
|
||||||
deviceName,
|
|
||||||
device,
|
|
||||||
screenSize,
|
|
||||||
};
|
|
||||||
nextTick(() => {
|
|
||||||
store.hideLoading();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -203,15 +262,20 @@ async function refreshDevices() {
|
|||||||
store.hideLoading();
|
store.hideLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
const screenSizeInfo = computed(() => {
|
async function connectDevice() {
|
||||||
if (store.controledDevice) {
|
if (!address.value) {
|
||||||
return `${store.controledDevice.screenSize[0]} x ${store.controledDevice.screenSize[1]}`;
|
message.error("请输入无线调试地址");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return "";
|
|
||||||
});
|
store.showLoading();
|
||||||
|
message.info(await adbConnect(address.value));
|
||||||
|
await refreshDevices();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<NScrollbar>
|
||||||
<div class="device">
|
<div class="device">
|
||||||
<NSpin :show="store.showLoadingRef">
|
<NSpin :show="store.showLoadingRef">
|
||||||
<NH4 prefix="bar">本地端口</NH4>
|
<NH4 prefix="bar">本地端口</NH4>
|
||||||
@ -220,8 +284,40 @@ const screenSizeInfo = computed(() => {
|
|||||||
:show-button="false"
|
:show-button="false"
|
||||||
:min="16384"
|
:min="16384"
|
||||||
:max="49151"
|
:max="49151"
|
||||||
|
placeholder="Scrcpy 本地端口"
|
||||||
style="max-width: 300px"
|
style="max-width: 300px"
|
||||||
/>
|
/>
|
||||||
|
<NH4 prefix="bar">无线连接</NH4>
|
||||||
|
<NInputGroup style="max-width: 300px">
|
||||||
|
<NInput
|
||||||
|
v-model:value="address"
|
||||||
|
clearable
|
||||||
|
placeholder="无线调试地址"
|
||||||
|
/>
|
||||||
|
<NButton type="primary" @click="connectDevice">连接</NButton>
|
||||||
|
</NInputGroup>
|
||||||
|
<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>
|
<NH4 prefix="bar">受控设备</NH4>
|
||||||
<div class="controled-device-list">
|
<div class="controled-device-list">
|
||||||
<NEmpty
|
<NEmpty
|
||||||
@ -245,8 +341,7 @@ const screenSizeInfo = computed(() => {
|
|||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
scid: {{ store.controledDevice.scid }} <br />status:
|
scid: {{ store.controledDevice.scid }} <br />status:
|
||||||
{{ store.controledDevice.device.status }} <br />screen:
|
{{ store.controledDevice.device.status }}
|
||||||
{{ screenSizeInfo }}
|
|
||||||
</NTooltip>
|
</NTooltip>
|
||||||
<NButton quaternary circle type="error" @click="shutdownSC()">
|
<NButton quaternary circle type="error" @click="shutdownSC()">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
@ -257,7 +352,7 @@ const screenSizeInfo = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NFlex justify="space-between" align="center">
|
<NFlex justify="space-between" align="center">
|
||||||
<NH4 prefix="bar">可用设备</NH4>
|
<NH4 style="margin: 20px 0" prefix="bar">可用设备</NH4>
|
||||||
<NButton
|
<NButton
|
||||||
tertiary
|
tertiary
|
||||||
circle
|
circle
|
||||||
@ -290,17 +385,15 @@ const screenSizeInfo = computed(() => {
|
|||||||
/>
|
/>
|
||||||
</NSpin>
|
</NSpin>
|
||||||
</div>
|
</div>
|
||||||
|
</NScrollbar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.device {
|
.device {
|
||||||
color: var(--light-color);
|
color: var(--light-color);
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
padding: 0 25px;
|
padding: 0 20px;
|
||||||
}
|
height: 100%;
|
||||||
|
|
||||||
.n-h4 {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.controled-device-list {
|
.controled-device-list {
|
||||||
|
@ -1,41 +1,46 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onActivated, ref } from "vue";
|
import { onActivated } from "vue";
|
||||||
import { NDialog } from "naive-ui";
|
import { NDialog, useMessage } from "naive-ui";
|
||||||
import { useGlobalStore } from "../store/global";
|
import { useGlobalStore } from "../store/global";
|
||||||
import { onBeforeRouteLeave, useRouter } from "vue-router";
|
import { onBeforeRouteLeave, useRouter } from "vue-router";
|
||||||
import {
|
import {
|
||||||
initShortcuts,
|
applyShortcuts,
|
||||||
|
clearShortcuts,
|
||||||
listenToKeyEvent,
|
listenToKeyEvent,
|
||||||
unlistenToKeyEvent,
|
unlistenToKeyEvent,
|
||||||
|
updateScreenSizeAndMaskArea,
|
||||||
} from "../hotkey";
|
} from "../hotkey";
|
||||||
|
import { KeySteeringWheel } from "../keyMappingConfig";
|
||||||
const maskRef = ref<HTMLElement | null>(null);
|
|
||||||
|
|
||||||
const store = useGlobalStore();
|
const store = useGlobalStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const message = useMessage();
|
||||||
let isShortcutInited = false;
|
|
||||||
|
|
||||||
onBeforeRouteLeave(() => {
|
onBeforeRouteLeave(() => {
|
||||||
if (isShortcutInited) {
|
if (store.controledDevice) {
|
||||||
if (maskRef.value) {
|
|
||||||
unlistenToKeyEvent();
|
unlistenToKeyEvent();
|
||||||
}
|
clearShortcuts();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onActivated(async () => {
|
onActivated(async () => {
|
||||||
if (isShortcutInited) {
|
const maskElement = document.getElementById("maskElement") as HTMLElement;
|
||||||
if (maskRef.value) {
|
|
||||||
listenToKeyEvent();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (store.controledDevice) {
|
if (store.controledDevice) {
|
||||||
if (maskRef.value) {
|
updateScreenSizeAndMaskArea(
|
||||||
initShortcuts(store.controledDevice.screenSize, maskRef.value);
|
[store.screenSizeW, store.screenSizeH],
|
||||||
|
[maskElement.clientWidth, maskElement.clientHeight]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
applyShortcuts(
|
||||||
|
maskElement,
|
||||||
|
store.keyMappingConfigList[store.curKeyMappingIndex]
|
||||||
|
)
|
||||||
|
) {
|
||||||
listenToKeyEvent();
|
listenToKeyEvent();
|
||||||
isShortcutInited = true;
|
} else {
|
||||||
|
message.error("按键方案异常,请删除此方案");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -43,9 +48,6 @@ onActivated(async () => {
|
|||||||
function toStartServer() {
|
function toStartServer() {
|
||||||
router.replace({ name: "device" });
|
router.replace({ name: "device" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO 按键设置
|
|
||||||
// TODO 渲染按钮
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -61,25 +63,113 @@ function toStartServer() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<template v-if="store.keyMappingConfigList.length">
|
||||||
|
<div @contextmenu.prevent class="mask" id="maskElement"></div>
|
||||||
|
<div class="button-layer">
|
||||||
|
<template
|
||||||
|
v-for="button in store.keyMappingConfigList[store.curKeyMappingIndex]
|
||||||
|
.list"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-show="store.controledDevice"
|
v-if="button.type === 'SteeringWheel'"
|
||||||
@contextmenu.prevent
|
class="mask-steering-wheel"
|
||||||
class="mask"
|
:style="{
|
||||||
ref="maskRef"
|
left: button.posX - 75 + 'px',
|
||||||
></div>
|
top: button.posY - 75 + 'px',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="wheel-container">
|
||||||
|
<i />
|
||||||
|
<span>{{ (button as KeySteeringWheel).key.up }}</span>
|
||||||
|
<i />
|
||||||
|
<span>{{ (button as KeySteeringWheel).key.left }}</span>
|
||||||
|
<i />
|
||||||
|
<span>{{ (button as KeySteeringWheel).key.right }}</span>
|
||||||
|
<i />
|
||||||
|
<span>{{ (button as KeySteeringWheel).key.down }}</span>
|
||||||
|
<i />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="mask-button"
|
||||||
|
:style="{
|
||||||
|
left: button.posX + 'px',
|
||||||
|
top: button.posY - 14 + 'px',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ button.key }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.mask {
|
.mask {
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
background-color: transparent;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
border-right: 1px solid var(--bg-color);
|
||||||
|
border-bottom: 1px solid var(--bg-color);
|
||||||
|
border-radius: 0 0 5px 0;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-layer {
|
||||||
|
position: absolute;
|
||||||
|
left: 70px;
|
||||||
|
top: 30px;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
& > .mask-button {
|
||||||
|
position: absolute;
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .mask-steering-wheel {
|
||||||
|
position: absolute;
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
.wheel-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 50px);
|
||||||
|
grid-template-rows: repeat(3, 50px);
|
||||||
|
justify-items: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.notice {
|
.notice {
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
z-index: 1;
|
||||||
|
position: absolute;
|
||||||
|
left: 70px;
|
||||||
|
top: 30px;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 3;
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
|
@ -5,22 +5,33 @@ import {
|
|||||||
LogoAndroid,
|
LogoAndroid,
|
||||||
SettingsOutline,
|
SettingsOutline,
|
||||||
ReturnDownBackOutline,
|
ReturnDownBackOutline,
|
||||||
|
VolumeHighOutline,
|
||||||
|
VolumeLowOutline,
|
||||||
StopOutline,
|
StopOutline,
|
||||||
ListOutline,
|
ListOutline,
|
||||||
|
BulbOutline,
|
||||||
|
Bulb,
|
||||||
} from "@vicons/ionicons5";
|
} from "@vicons/ionicons5";
|
||||||
import { Keyboard24Regular } from "@vicons/fluent";
|
import { Keyboard24Regular } from "@vicons/fluent";
|
||||||
import { NIcon } from "naive-ui";
|
import { NIcon, useMessage } from "naive-ui";
|
||||||
import { useGlobalStore } from "../store/global";
|
import { useGlobalStore } from "../store/global";
|
||||||
import { sendInjectKeycode } from "../frontcommand/controlMsg";
|
import {
|
||||||
|
sendInjectKeycode,
|
||||||
|
sendSetScreenPowerMode,
|
||||||
|
} from "../frontcommand/controlMsg";
|
||||||
import {
|
import {
|
||||||
AndroidKeyEventAction,
|
AndroidKeyEventAction,
|
||||||
AndroidKeycode,
|
AndroidKeycode,
|
||||||
AndroidMetastate,
|
AndroidMetastate,
|
||||||
} from "../frontcommand/android";
|
} from "../frontcommand/android";
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const store = useGlobalStore();
|
const store = useGlobalStore();
|
||||||
|
const message = useMessage();
|
||||||
|
|
||||||
|
const nextScreenPowerMode = ref(0);
|
||||||
|
|
||||||
function nav(name: string) {
|
function nav(name: string) {
|
||||||
router.replace({ name });
|
router.replace({ name });
|
||||||
@ -49,6 +60,8 @@ async function sendKeyCodeToDevice(code: AndroidKeycode) {
|
|||||||
repeat: 0,
|
repeat: 0,
|
||||||
metastate: AndroidMetastate.AMETA_NONE,
|
metastate: AndroidMetastate.AMETA_NONE,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
message.error("未连接设备");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -83,6 +96,27 @@ async function sendKeyCodeToDevice(code: AndroidKeycode) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="nav">
|
<div class="nav">
|
||||||
|
<div
|
||||||
|
@click="
|
||||||
|
sendSetScreenPowerMode({ mode: nextScreenPowerMode });
|
||||||
|
nextScreenPowerMode = nextScreenPowerMode ? 0 : 2;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<NIcon>
|
||||||
|
<Bulb v-if="nextScreenPowerMode" />
|
||||||
|
<BulbOutline v-else />
|
||||||
|
</NIcon>
|
||||||
|
</div>
|
||||||
|
<div @click="sendKeyCodeToDevice(AndroidKeycode.AKEYCODE_VOLUME_UP)">
|
||||||
|
<NIcon>
|
||||||
|
<VolumeHighOutline />
|
||||||
|
</NIcon>
|
||||||
|
</div>
|
||||||
|
<div @click="sendKeyCodeToDevice(AndroidKeycode.AKEYCODE_VOLUME_DOWN)">
|
||||||
|
<NIcon>
|
||||||
|
<VolumeLowOutline />
|
||||||
|
</NIcon>
|
||||||
|
</div>
|
||||||
<div @click="sendKeyCodeToDevice(AndroidKeycode.AKEYCODE_BACK)">
|
<div @click="sendKeyCodeToDevice(AndroidKeycode.AKEYCODE_BACK)">
|
||||||
<NIcon>
|
<NIcon>
|
||||||
<ReturnDownBackOutline />
|
<ReturnDownBackOutline />
|
||||||
@ -173,16 +207,14 @@ async function sendKeyCodeToDevice(code: AndroidKeycode) {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.NIcon {
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
}
|
|
||||||
|
|
||||||
.NIcon:hover {
|
&:hover {
|
||||||
color: var(--primary-hover-color);
|
color: var(--primary-hover-color);
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
.NIcon:active {
|
&:active {
|
||||||
color: var(--primary-pressed-color);
|
color: var(--primary-pressed-color);
|
||||||
transform: scale(0.9);
|
transform: scale(0.9);
|
||||||
}
|
}
|
||||||
|
@ -1,63 +1,317 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Ref, onActivated, ref } from "vue";
|
import { nextTick, onActivated, ref } from "vue";
|
||||||
|
import KeyInfo from "./KeyInfo.vue";
|
||||||
|
import KeySetting from "./KeySetting.vue";
|
||||||
|
import KeyCommon from "./KeyCommon.vue";
|
||||||
|
import KeySteeringWheel from "./KeySteeringWheel.vue";
|
||||||
|
import KeySkill from "./KeySkill.vue";
|
||||||
|
import KeyObservation from "./KeyObservation.vue";
|
||||||
|
import {
|
||||||
|
KeyDirectionalSkill,
|
||||||
|
KeySteeringWheel as KeyMappingSteeringWheel,
|
||||||
|
KeyObservation as KeyMappingObservation,
|
||||||
|
KeyTap,
|
||||||
|
KeyMacro,
|
||||||
|
} from "../../keyMappingConfig";
|
||||||
|
import { useGlobalStore } from "../../store/global";
|
||||||
|
import { DropdownOption, NDropdown, useDialog, useMessage } from "naive-ui";
|
||||||
import { onBeforeRouteLeave } from "vue-router";
|
import { onBeforeRouteLeave } from "vue-router";
|
||||||
|
import { useKeyboardStore } from "../../store/keyboard";
|
||||||
|
|
||||||
// TODO 添加右侧按键列表用于拖放
|
const store = useGlobalStore();
|
||||||
// TODO 在进入此页面时扩宽窗口,离开时恢复窗口大小
|
const keyboardStore = useKeyboardStore();
|
||||||
|
const dialog = useDialog();
|
||||||
|
const message = useMessage();
|
||||||
|
|
||||||
const keyboardElement = ref<HTMLElement | null>(null);
|
const addButtonPos = ref({ x: 0, y: 0 });
|
||||||
const mouseX = ref(0);
|
const addButtonOptions: DropdownOption[] = [
|
||||||
const mouseY = ref(0);
|
{
|
||||||
|
label: "普通点击",
|
||||||
|
key: "Tap",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "键盘行走",
|
||||||
|
key: "SteeringWheel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "技能",
|
||||||
|
key: "DirectionalSkill",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "技能取消",
|
||||||
|
key: "CancelSkill",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "观察视角",
|
||||||
|
key: "Observation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "宏",
|
||||||
|
key: "Macro",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
function clientxToPosx(clientx: number) {
|
function onAddButtonSelect(
|
||||||
return clientx < 70 ? 0 : Math.floor(clientx - 70);
|
type:
|
||||||
|
| "Tap"
|
||||||
|
| "SteeringWheel"
|
||||||
|
| "DirectionalSkill"
|
||||||
|
| "CancelSkill"
|
||||||
|
| "Observation"
|
||||||
|
| "Macro"
|
||||||
|
) {
|
||||||
|
keyboardStore.showButtonAddFlag = false;
|
||||||
|
const keyMapping = {
|
||||||
|
type,
|
||||||
|
key: "NONE",
|
||||||
|
note: "",
|
||||||
|
posX: addButtonPos.value.x - 70,
|
||||||
|
posY: addButtonPos.value.y - 30,
|
||||||
|
pointerId: 2, // default skill pointerId
|
||||||
|
};
|
||||||
|
if (type === "Tap") {
|
||||||
|
(keyMapping as KeyTap).time = 80;
|
||||||
|
} else if (type === "SteeringWheel") {
|
||||||
|
(keyMapping as unknown as KeyMappingSteeringWheel).key = {
|
||||||
|
left: "NONE",
|
||||||
|
right: "NONE",
|
||||||
|
up: "NONE",
|
||||||
|
down: "NONE",
|
||||||
|
};
|
||||||
|
} else if (type === "DirectionalSkill") {
|
||||||
|
(keyMapping as unknown as KeyDirectionalSkill).range = 30;
|
||||||
|
} else if (type === "CancelSkill") {
|
||||||
|
keyMapping.note = "取消技能";
|
||||||
|
} else if (type === "Observation") {
|
||||||
|
(keyMapping as unknown as KeyMappingObservation).scale = 0.6;
|
||||||
|
} else if (type === "Macro") {
|
||||||
|
(keyMapping as unknown as KeyMacro).macro = {
|
||||||
|
down: null,
|
||||||
|
loop: null,
|
||||||
|
up: null,
|
||||||
|
};
|
||||||
|
} else return;
|
||||||
|
keyboardStore.edited = true;
|
||||||
|
store.editKeyMappingList.push(keyMapping);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clientyToPosy(clienty: number) {
|
function isKeyUnique(curKey: string): boolean {
|
||||||
return clienty < 30 ? 0 : Math.floor(clienty - 30);
|
const set = new Set<string>();
|
||||||
|
for (const keyMapping of store.editKeyMappingList) {
|
||||||
|
if (keyMapping.type === "SteeringWheel") {
|
||||||
|
const nameList: ["up", "down", "left", "right"] = [
|
||||||
|
"up",
|
||||||
|
"down",
|
||||||
|
"left",
|
||||||
|
"right",
|
||||||
|
];
|
||||||
|
for (const name of nameList) {
|
||||||
|
if (set.has((keyMapping as KeyMappingSteeringWheel).key[name]))
|
||||||
|
return false;
|
||||||
|
set.add((keyMapping as KeyMappingSteeringWheel).key[name]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (set.has(keyMapping.key as string)) return false;
|
||||||
|
set.add(keyMapping.key as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (set.has(curKey)) return false;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ignoreMousemove = true;
|
function setCurButtonKey(curKey: string) {
|
||||||
function mousemoveHandler(event: MouseEvent) {
|
if (
|
||||||
ignoreMousemove = !ignoreMousemove;
|
keyboardStore.activeButtonIndex === -1 ||
|
||||||
if (ignoreMousemove) return;
|
keyboardStore.activeButtonIndex >= store.editKeyMappingList.length
|
||||||
mouseX.value = clientxToPosx(event.clientX);
|
)
|
||||||
mouseY.value = clientyToPosy(event.clientY);
|
return;
|
||||||
|
|
||||||
|
const keyMapping = store.editKeyMappingList[keyboardStore.activeButtonIndex];
|
||||||
|
if (
|
||||||
|
keyMapping.type === "SteeringWheel" &&
|
||||||
|
keyboardStore.activeSteeringWheelButtonKeyIndex === -1
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!isKeyUnique(curKey)) {
|
||||||
|
message.error("按键重复:" + curKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyMapping.type === "SteeringWheel") {
|
||||||
|
const keyObject = keyMapping.key as {
|
||||||
|
left: string;
|
||||||
|
right: string;
|
||||||
|
up: string;
|
||||||
|
down: string;
|
||||||
|
};
|
||||||
|
const nameList: ["up", "down", "left", "right"] = [
|
||||||
|
"up",
|
||||||
|
"down",
|
||||||
|
"left",
|
||||||
|
"right",
|
||||||
|
];
|
||||||
|
const activeSteeringWheelButtonKeyIndex =
|
||||||
|
keyboardStore.activeSteeringWheelButtonKeyIndex;
|
||||||
|
if (
|
||||||
|
activeSteeringWheelButtonKeyIndex >= 0 &&
|
||||||
|
activeSteeringWheelButtonKeyIndex <= 3
|
||||||
|
) {
|
||||||
|
const curName = nameList[activeSteeringWheelButtonKeyIndex];
|
||||||
|
keyObject[curName] = curKey;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
keyMapping.key = curKey;
|
||||||
|
}
|
||||||
|
keyboardStore.edited = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyboardCodeList: Ref<string[]> = ref([]);
|
function handleClick(event: MouseEvent) {
|
||||||
function keyupHandler(event: KeyboardEvent) {
|
if (event.button === 0) {
|
||||||
|
// left click
|
||||||
|
if (event.target === document.getElementById("keyboardElement")) {
|
||||||
|
if (keyboardStore.showSettingFlag) {
|
||||||
|
keyboardStore.showSettingFlag = false;
|
||||||
|
} else {
|
||||||
|
keyboardStore.activeButtonIndex = -1;
|
||||||
|
keyboardStore.activeSteeringWheelButtonKeyIndex = -1;
|
||||||
|
keyboardStore.showButtonSettingFlag = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (event.button === 2) {
|
||||||
|
// right click
|
||||||
|
if (event.target === document.getElementById("keyboardElement")) {
|
||||||
|
// add button
|
||||||
|
keyboardStore.showSettingFlag = false;
|
||||||
|
keyboardStore.activeButtonIndex = -1;
|
||||||
|
keyboardStore.activeSteeringWheelButtonKeyIndex = -1;
|
||||||
|
keyboardStore.showButtonAddFlag = true;
|
||||||
|
|
||||||
|
keyboardStore.showButtonAddFlag = false;
|
||||||
|
nextTick().then(() => {
|
||||||
|
keyboardStore.showButtonAddFlag = true;
|
||||||
|
addButtonPos.value.x = event.clientX;
|
||||||
|
addButtonPos.value.y = event.clientY;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setCurButtonKey(`M${event.button}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// other click
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (keyboardCodeList.value.length > 10) {
|
setCurButtonKey(`M${event.button}`);
|
||||||
keyboardCodeList.value.shift();
|
}
|
||||||
keyboardCodeList.value.push(event.code);
|
}
|
||||||
} else keyboardCodeList.value.push(event.code);
|
|
||||||
|
function handleKeyUp(event: KeyboardEvent) {
|
||||||
|
setCurButtonKey(event.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseWheel(event: WheelEvent) {
|
||||||
|
if (event.deltaY > 0) {
|
||||||
|
// WheelDown
|
||||||
|
setCurButtonKey("WheelDown");
|
||||||
|
} else if (event.deltaY < 0) {
|
||||||
|
// WheelUp
|
||||||
|
setCurButtonKey("WheelUp");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetKeyMappingConfig() {
|
||||||
|
keyboardStore.activeButtonIndex = -1;
|
||||||
|
keyboardStore.activeSteeringWheelButtonKeyIndex = -1;
|
||||||
|
keyboardStore.showSettingFlag = false;
|
||||||
|
store.resetEditKeyMappingList();
|
||||||
|
keyboardStore.edited = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
keyboardElement.value?.addEventListener("mousemove", mousemoveHandler);
|
document.addEventListener("keyup", handleKeyUp);
|
||||||
document.addEventListener("keyup", keyupHandler);
|
document.addEventListener("wheel", handleMouseWheel);
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeRouteLeave(() => {
|
onBeforeRouteLeave(() => {
|
||||||
keyboardElement.value?.removeEventListener("mousemove", mousemoveHandler);
|
document.removeEventListener("keyup", handleKeyUp);
|
||||||
document.removeEventListener("keyup", keyupHandler);
|
document.removeEventListener("wheel", handleMouseWheel);
|
||||||
|
if (keyboardStore.edited) {
|
||||||
|
dialog.warning({
|
||||||
|
title: "Warning",
|
||||||
|
content: "当前方案尚未保存,是否保存?",
|
||||||
|
positiveText: "保存",
|
||||||
|
negativeText: "取消",
|
||||||
|
onPositiveClick: () => {
|
||||||
|
if (store.applyEditKeyMappingList()) {
|
||||||
|
keyboardStore.edited = false;
|
||||||
|
} else {
|
||||||
|
message.error("存在重复按键,无法保存");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNegativeClick: () => {
|
||||||
|
resetKeyMappingConfig();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="keyboardElement" class="keyboard">
|
<div
|
||||||
此处最好用其他颜色的蒙版,和右侧的按键列表区同色
|
v-if="store.keyMappingConfigList.length"
|
||||||
<div>{{ mouseX }}, {{ mouseY }}</div>
|
id="keyboardElement"
|
||||||
<div v-for="code in keyboardCodeList">
|
class="keyboard"
|
||||||
{{ code }}
|
@mousedown="handleClick"
|
||||||
</div>
|
@contextmenu.prevent
|
||||||
|
>
|
||||||
|
<KeySetting />
|
||||||
|
<KeyInfo />
|
||||||
|
<NDropdown
|
||||||
|
:options="addButtonOptions"
|
||||||
|
:show="keyboardStore.showButtonAddFlag"
|
||||||
|
placement="bottom-start"
|
||||||
|
trigger="manual"
|
||||||
|
:x="addButtonPos.x"
|
||||||
|
:y="addButtonPos.y"
|
||||||
|
@clickoutside="keyboardStore.showButtonAddFlag = false"
|
||||||
|
@select="onAddButtonSelect"
|
||||||
|
/>
|
||||||
|
<template v-for="(_, index) in store.editKeyMappingList">
|
||||||
|
<KeySteeringWheel
|
||||||
|
v-if="store.editKeyMappingList[index].type === 'SteeringWheel'"
|
||||||
|
:index="index"
|
||||||
|
/>
|
||||||
|
<KeySkill
|
||||||
|
v-else-if="
|
||||||
|
store.editKeyMappingList[index].type === 'DirectionalSkill' ||
|
||||||
|
store.editKeyMappingList[index].type === 'DirectionlessSkill' ||
|
||||||
|
store.editKeyMappingList[index].type === 'TriggerWhenPressedSkill'
|
||||||
|
"
|
||||||
|
:index="index"
|
||||||
|
/>
|
||||||
|
<KeyObservation
|
||||||
|
v-else-if="store.editKeyMappingList[index].type === 'Observation'"
|
||||||
|
:index="index"
|
||||||
|
/>
|
||||||
|
<KeyCommon v-else :index="index" />
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="scss">
|
||||||
.keyboard {
|
.keyboard {
|
||||||
background-color: rgba(255, 255, 255, 0.5);
|
color: var(--light-color);
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
|
||||||
|
.keyboard-button {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 1px solid red;
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
330
src/components/keyboard/KeyCommon.vue
Normal file
330
src/components/keyboard/KeyCommon.vue
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import { useGlobalStore } from "../../store/global";
|
||||||
|
import {
|
||||||
|
NButton,
|
||||||
|
NFormItem,
|
||||||
|
NH4,
|
||||||
|
NIcon,
|
||||||
|
NInput,
|
||||||
|
NModal,
|
||||||
|
NCard,
|
||||||
|
useMessage,
|
||||||
|
NFlex,
|
||||||
|
NInputNumber,
|
||||||
|
} from "naive-ui";
|
||||||
|
import { CloseCircle, Settings } from "@vicons/ionicons5";
|
||||||
|
import { KeyMacro, KeyMacroList, KeyTap } from "../../keyMappingConfig";
|
||||||
|
import { useKeyboardStore } from "../../store/keyboard";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
index: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const keyboardStore = useKeyboardStore();
|
||||||
|
|
||||||
|
const store = useGlobalStore();
|
||||||
|
const message = useMessage();
|
||||||
|
const elementRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const isActive = computed(
|
||||||
|
() => props.index === keyboardStore.activeButtonIndex
|
||||||
|
);
|
||||||
|
const keyMapping = computed(() => store.editKeyMappingList[props.index]);
|
||||||
|
|
||||||
|
const showMacroModal = ref(false);
|
||||||
|
const editedMacroRaw = ref({
|
||||||
|
down: "",
|
||||||
|
loop: "",
|
||||||
|
up: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
function dragHandler(downEvent: MouseEvent) {
|
||||||
|
keyboardStore.activeButtonIndex = props.index;
|
||||||
|
keyboardStore.showButtonSettingFlag = false;
|
||||||
|
const oldX = keyMapping.value.posX;
|
||||||
|
const oldY = keyMapping.value.posY;
|
||||||
|
const element = elementRef.value;
|
||||||
|
if (element) {
|
||||||
|
const keyboardElement = document.getElementById(
|
||||||
|
"keyboardElement"
|
||||||
|
) as HTMLElement;
|
||||||
|
const maxX = keyboardElement.clientWidth - 20;
|
||||||
|
const maxY = keyboardElement.clientHeight - 20;
|
||||||
|
|
||||||
|
const x = downEvent.clientX;
|
||||||
|
const y = downEvent.clientY;
|
||||||
|
const moveHandler = (moveEvent: MouseEvent) => {
|
||||||
|
let newX = oldX + moveEvent.clientX - x;
|
||||||
|
let newY = oldY + moveEvent.clientY - y;
|
||||||
|
newX = Math.max(20, Math.min(newX, maxX));
|
||||||
|
newY = Math.max(20, Math.min(newY, maxY));
|
||||||
|
keyMapping.value.posX = newX;
|
||||||
|
keyMapping.value.posY = newY;
|
||||||
|
};
|
||||||
|
window.addEventListener("mousemove", moveHandler);
|
||||||
|
const upHandler = () => {
|
||||||
|
window.removeEventListener("mousemove", moveHandler);
|
||||||
|
window.removeEventListener("mouseup", upHandler);
|
||||||
|
if (oldX !== keyMapping.value.posX || oldY !== keyMapping.value.posY) {
|
||||||
|
keyboardStore.edited = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("mouseup", upHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function delCurKeyMapping() {
|
||||||
|
keyboardStore.edited = true;
|
||||||
|
keyboardStore.activeButtonIndex = -1;
|
||||||
|
store.editKeyMappingList.splice(props.index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMacro(macroRaw: string): KeyMacroList {
|
||||||
|
// simple parsing and possible to let the wrong code pass
|
||||||
|
if (macroRaw === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const macro: KeyMacroList = JSON.parse(macroRaw);
|
||||||
|
if (macro === null) return macro;
|
||||||
|
for (const macroItem of macro) {
|
||||||
|
if (typeof macroItem !== "object") {
|
||||||
|
throw ["macroItem is not object", macroItem];
|
||||||
|
}
|
||||||
|
if (!("type" in macroItem)) {
|
||||||
|
throw ["macroItem has no type attribute", macroItem];
|
||||||
|
}
|
||||||
|
if (!("args" in macroItem)) {
|
||||||
|
throw ["macroItem has no args attribute", macroItem];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return macro;
|
||||||
|
}
|
||||||
|
|
||||||
|
let editedFlag = false;
|
||||||
|
function editMacro() {
|
||||||
|
editedFlag = false;
|
||||||
|
const macro = (keyMapping.value as KeyMacro).macro;
|
||||||
|
editedMacroRaw.value = {
|
||||||
|
down: macro.down === null ? "" : JSON.stringify(macro.down, null, 2),
|
||||||
|
loop: macro.loop === null ? "" : JSON.stringify(macro.loop, null, 2),
|
||||||
|
up: macro.up === null ? "" : JSON.stringify(macro.up, null, 2),
|
||||||
|
};
|
||||||
|
showMacroModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveMacro() {
|
||||||
|
if (!editedFlag) return;
|
||||||
|
try {
|
||||||
|
const macro: {
|
||||||
|
down: KeyMacroList;
|
||||||
|
loop: KeyMacroList;
|
||||||
|
up: KeyMacroList;
|
||||||
|
} = {
|
||||||
|
down: null,
|
||||||
|
loop: null,
|
||||||
|
up: null,
|
||||||
|
};
|
||||||
|
const keyList: ["down", "loop", "up"] = ["down", "loop", "up"];
|
||||||
|
for (const key of keyList) {
|
||||||
|
const macroRaw = editedMacroRaw.value[key];
|
||||||
|
macro[key] = parseMacro(macroRaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
(keyMapping.value as KeyMacro).macro = macro;
|
||||||
|
showMacroModal.value = false;
|
||||||
|
keyboardStore.edited = true;
|
||||||
|
message.success("宏代码解析成功,但不保证代码正确性,请自行测试");
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
message.error("宏代码保存失败,请检查代码格式是否正确");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingPosX = ref(0);
|
||||||
|
const settingPosY = ref(0);
|
||||||
|
function showSetting() {
|
||||||
|
const keyboardElement = document.getElementById(
|
||||||
|
"keyboardElement"
|
||||||
|
) as HTMLElement;
|
||||||
|
const maxWidth = keyboardElement.clientWidth - 150;
|
||||||
|
const maxHeight = keyboardElement.clientHeight - 300;
|
||||||
|
|
||||||
|
settingPosX.value = Math.min(keyMapping.value.posX + 25, maxWidth);
|
||||||
|
settingPosY.value = Math.min(keyMapping.value.posY - 25, maxHeight);
|
||||||
|
keyboardStore.showButtonSettingFlag = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="{ active: isActive }"
|
||||||
|
:style="{
|
||||||
|
left: `${keyMapping.posX - 20}px`,
|
||||||
|
top: `${keyMapping.posY - 20}px`,
|
||||||
|
}"
|
||||||
|
@mousedown="dragHandler"
|
||||||
|
class="key-common"
|
||||||
|
ref="elementRef"
|
||||||
|
>
|
||||||
|
<span>{{ keyMapping.key }}</span>
|
||||||
|
<NButton
|
||||||
|
class="key-close-btn"
|
||||||
|
text
|
||||||
|
@click="delCurKeyMapping"
|
||||||
|
:type="isActive ? 'primary' : 'info'"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon size="15">
|
||||||
|
<CloseCircle />
|
||||||
|
</NIcon>
|
||||||
|
</template>
|
||||||
|
</NButton>
|
||||||
|
<NButton
|
||||||
|
class="key-setting-btn"
|
||||||
|
text
|
||||||
|
@click="showSetting"
|
||||||
|
:type="isActive ? 'primary' : 'info'"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon size="15">
|
||||||
|
<Settings />
|
||||||
|
</NIcon>
|
||||||
|
</template>
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="key-setting"
|
||||||
|
v-if="isActive && keyboardStore.showButtonSettingFlag"
|
||||||
|
:style="{
|
||||||
|
left: `${settingPosX}px`,
|
||||||
|
top: `${settingPosY}px`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<NH4 prefix="bar">{{
|
||||||
|
keyMapping.type === "CancelSkill"
|
||||||
|
? "技能取消"
|
||||||
|
: keyMapping.type === "Tap"
|
||||||
|
? "普通点击"
|
||||||
|
: "宏"
|
||||||
|
}}</NH4>
|
||||||
|
<NFormItem v-if="keyMapping.type === 'Macro'" label="宏代码">
|
||||||
|
<NButton type="success" @click="editMacro"> 编辑代码 </NButton>
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem v-if="keyMapping.type === 'Tap'" label="触摸时长">
|
||||||
|
<NInputNumber
|
||||||
|
v-model:value="(keyMapping as KeyTap).time"
|
||||||
|
:min="0"
|
||||||
|
placeholder="请输入触摸时长(ms)"
|
||||||
|
@update:value="keyboardStore.edited = true"
|
||||||
|
/>
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="触点ID">
|
||||||
|
<NInputNumber
|
||||||
|
v-model:value="keyMapping.pointerId"
|
||||||
|
:min="0"
|
||||||
|
placeholder="请输入触点ID"
|
||||||
|
@update:value="keyboardStore.edited = true"
|
||||||
|
/>
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="备注">
|
||||||
|
<NInput
|
||||||
|
v-model:value="keyMapping.note"
|
||||||
|
placeholder="请输入备注"
|
||||||
|
@update:value="keyboardStore.edited = true"
|
||||||
|
/>
|
||||||
|
</NFormItem>
|
||||||
|
<NModal
|
||||||
|
v-if="keyMapping.type === 'Macro'"
|
||||||
|
v-model:show="showMacroModal"
|
||||||
|
@before-leave="saveMacro"
|
||||||
|
>
|
||||||
|
<NCard style="width: 50%; height: 80%" title="宏编辑">
|
||||||
|
<NFlex vertical style="height: 100%">
|
||||||
|
<div>按下按键执行</div>
|
||||||
|
<NInput
|
||||||
|
type="textarea"
|
||||||
|
style="flex-grow: 1"
|
||||||
|
placeholder="JSON宏代码, 可为空"
|
||||||
|
v-model:value="editedMacroRaw.down"
|
||||||
|
@update:value="editedFlag = true"
|
||||||
|
round
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
<div>按住执行</div>
|
||||||
|
<NInput
|
||||||
|
type="textarea"
|
||||||
|
style="flex-grow: 1"
|
||||||
|
placeholder="JSON宏代码, 可为空"
|
||||||
|
v-model:value="editedMacroRaw.loop"
|
||||||
|
@update:value="editedFlag = true"
|
||||||
|
round
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
<div>抬起执行</div>
|
||||||
|
<NInput
|
||||||
|
type="textarea"
|
||||||
|
style="flex-grow: 1"
|
||||||
|
placeholder="JSON宏代码, 可为空"
|
||||||
|
v-model:value="editedMacroRaw.up"
|
||||||
|
@update:value="editedFlag = true"
|
||||||
|
round
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</NFlex>
|
||||||
|
</NCard>
|
||||||
|
</NModal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.key-setting {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 10px 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 150px;
|
||||||
|
height: 300px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 2px solid var(--light-color);
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-common {
|
||||||
|
position: absolute;
|
||||||
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--blue-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:not(.active):hover {
|
||||||
|
border: 2px solid var(--light-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-close-btn {
|
||||||
|
position: absolute;
|
||||||
|
left: 45px;
|
||||||
|
bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-setting-btn {
|
||||||
|
position: absolute;
|
||||||
|
left: 45px;
|
||||||
|
top: 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.active {
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
</style>
|
180
src/components/keyboard/KeyInfo.vue
Normal file
180
src/components/keyboard/KeyInfo.vue
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { NIcon } from "naive-ui";
|
||||||
|
import { CloseCircle } from "@vicons/ionicons5";
|
||||||
|
import { Ref, ref, watch } from "vue";
|
||||||
|
import { useKeyboardStore } from "../../store/keyboard";
|
||||||
|
|
||||||
|
const keyboardStore = useKeyboardStore();
|
||||||
|
|
||||||
|
const mouseX = ref(0);
|
||||||
|
const mouseY = ref(0);
|
||||||
|
|
||||||
|
function clientxToPosx(clientx: number) {
|
||||||
|
return clientx < 70 ? 0 : Math.floor(clientx - 70);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clientyToPosy(clienty: number) {
|
||||||
|
return clienty < 30 ? 0 : Math.floor(clienty - 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ignoreMousemove = true;
|
||||||
|
function mousemoveHandler(event: MouseEvent) {
|
||||||
|
ignoreMousemove = !ignoreMousemove;
|
||||||
|
if (ignoreMousemove) return;
|
||||||
|
mouseX.value = clientxToPosx(event.clientX);
|
||||||
|
mouseY.value = clientyToPosy(event.clientY);
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyboardCodeList: Ref<string[]> = ref([]);
|
||||||
|
function keyupHandler(event: KeyboardEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (keyboardCodeList.value.length > 5) {
|
||||||
|
keyboardCodeList.value.shift();
|
||||||
|
keyboardCodeList.value.push(event.code);
|
||||||
|
} else keyboardCodeList.value.push(event.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mousedownHandler(event: MouseEvent) {
|
||||||
|
const key = `M${event.button}`;
|
||||||
|
if (keyboardCodeList.value.length > 5) {
|
||||||
|
keyboardCodeList.value.shift();
|
||||||
|
keyboardCodeList.value.push(key);
|
||||||
|
} else keyboardCodeList.value.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => keyboardStore.showKeyInfoFlag,
|
||||||
|
(value) => {
|
||||||
|
const keyboardElement = document.getElementById(
|
||||||
|
"keyboardElement"
|
||||||
|
) as HTMLElement;
|
||||||
|
if (value) {
|
||||||
|
keyboardElement.addEventListener("mousemove", mousemoveHandler);
|
||||||
|
keyboardElement.addEventListener("mousedown", mousedownHandler);
|
||||||
|
document.addEventListener("keyup", keyupHandler);
|
||||||
|
} else {
|
||||||
|
keyboardElement.removeEventListener("mousemove", mousemoveHandler);
|
||||||
|
keyboardElement.removeEventListener("mousedown", mousedownHandler);
|
||||||
|
document.removeEventListener("keyup", keyupHandler);
|
||||||
|
keyboardCodeList.value.splice(0, keyboardCodeList.value.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let lastPosX = 0;
|
||||||
|
let lastPosY = 0;
|
||||||
|
function dragHandler(downEvent: MouseEvent) {
|
||||||
|
if (
|
||||||
|
downEvent.target instanceof HTMLElement &&
|
||||||
|
downEvent.target.className === "key-info-header"
|
||||||
|
) {
|
||||||
|
const target = downEvent.target;
|
||||||
|
downEvent.preventDefault();
|
||||||
|
target.style.setProperty("cursor", "grabbing");
|
||||||
|
|
||||||
|
const element = downEvent.target.parentElement;
|
||||||
|
const keyboardElement = document.getElementById(
|
||||||
|
"keyboardElement"
|
||||||
|
) as HTMLElement;
|
||||||
|
const maxWidth = keyboardElement.clientWidth - 120;
|
||||||
|
const maxHeight = keyboardElement.clientHeight - 200;
|
||||||
|
|
||||||
|
const x = downEvent.clientX;
|
||||||
|
const y = downEvent.clientY;
|
||||||
|
const moveHandler = (moveEvent: MouseEvent) => {
|
||||||
|
const newX = lastPosX + moveEvent.clientX - x;
|
||||||
|
const newY = lastPosY + moveEvent.clientY - y;
|
||||||
|
element?.style.setProperty(
|
||||||
|
"left",
|
||||||
|
`${Math.max(0, Math.min(newX, maxWidth))}px`
|
||||||
|
);
|
||||||
|
element?.style.setProperty(
|
||||||
|
"top",
|
||||||
|
`${Math.max(0, Math.min(newY, maxHeight))}px`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
window.addEventListener("mousemove", moveHandler);
|
||||||
|
const upHandler = (upEvent: MouseEvent) => {
|
||||||
|
lastPosX += upEvent.clientX - x;
|
||||||
|
lastPosY += upEvent.clientY - y;
|
||||||
|
window.removeEventListener("mousemove", moveHandler);
|
||||||
|
window.removeEventListener("mouseup", upHandler);
|
||||||
|
target.style.setProperty("cursor", "grab");
|
||||||
|
};
|
||||||
|
window.addEventListener("mouseup", upHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-show="keyboardStore.showKeyInfoFlag" class="key-info" @contextmenu.prevent>
|
||||||
|
<div class="key-info-header" @mousedown="dragHandler">
|
||||||
|
Key Info
|
||||||
|
<div
|
||||||
|
class="key-info-close"
|
||||||
|
@click="keyboardStore.showKeyInfoFlag = false"
|
||||||
|
>
|
||||||
|
<NIcon><CloseCircle></CloseCircle></NIcon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="key-info-content">
|
||||||
|
<div style="border-bottom: 1px solid var(--light-color)">
|
||||||
|
{{ mouseX }}, {{ mouseY }}
|
||||||
|
</div>
|
||||||
|
<div v-if="keyboardCodeList.length === 0">Press any key</div>
|
||||||
|
<div v-for="code in keyboardCodeList">
|
||||||
|
{{ code }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.key-info {
|
||||||
|
color: var(--light-color);
|
||||||
|
background-color: var(--content-bg-color);
|
||||||
|
width: 120px;
|
||||||
|
height: 200px;
|
||||||
|
border-radius: 10px;
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
z-index: 8;
|
||||||
|
|
||||||
|
.key-info-header {
|
||||||
|
background-color: var(--gray-color);
|
||||||
|
color: var(--bg-color);
|
||||||
|
font-weight: bold;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 10px 10px 0 0;
|
||||||
|
cursor: grab;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.key-info-close {
|
||||||
|
position: absolute;
|
||||||
|
transition: color 0.3s;
|
||||||
|
right: 5px;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--content-bg-color);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
color: var(--red-color);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
color: var(--red-pressed-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-info-content {
|
||||||
|
height: 180px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid var(--gray-color);
|
||||||
|
border-radius: 0 0 10px 10px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
219
src/components/keyboard/KeyObservation.vue
Normal file
219
src/components/keyboard/KeyObservation.vue
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import { useGlobalStore } from "../../store/global";
|
||||||
|
import { NIcon, NButton, NFormItem, NInput, NH4, NInputNumber } from "naive-ui";
|
||||||
|
import { Eye, CloseCircle, Settings } from "@vicons/ionicons5";
|
||||||
|
import { KeyObservation } from "../../keyMappingConfig";
|
||||||
|
import { useKeyboardStore } from "../../store/keyboard";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
index: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const keyboardStore = useKeyboardStore();
|
||||||
|
|
||||||
|
const store = useGlobalStore();
|
||||||
|
const elementRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const isActive = computed(
|
||||||
|
() => props.index === keyboardStore.activeButtonIndex
|
||||||
|
);
|
||||||
|
const keyMapping = computed(
|
||||||
|
() => store.editKeyMappingList[props.index] as KeyObservation
|
||||||
|
);
|
||||||
|
|
||||||
|
function dragHandler(downEvent: MouseEvent) {
|
||||||
|
keyboardStore.activeButtonIndex = props.index;
|
||||||
|
keyboardStore.showButtonSettingFlag = false;
|
||||||
|
const oldX = keyMapping.value.posX;
|
||||||
|
const oldY = keyMapping.value.posY;
|
||||||
|
const element = elementRef.value;
|
||||||
|
if (element) {
|
||||||
|
const keyboardElement = document.getElementById(
|
||||||
|
"keyboardElement"
|
||||||
|
) as HTMLElement;
|
||||||
|
const maxX = keyboardElement.clientWidth - 30;
|
||||||
|
const maxY = keyboardElement.clientHeight - 30;
|
||||||
|
|
||||||
|
const x = downEvent.clientX;
|
||||||
|
const y = downEvent.clientY;
|
||||||
|
const moveHandler = (moveEvent: MouseEvent) => {
|
||||||
|
let newX = oldX + moveEvent.clientX - x;
|
||||||
|
let newY = oldY + moveEvent.clientY - y;
|
||||||
|
newX = Math.max(30, Math.min(newX, maxX));
|
||||||
|
newY = Math.max(30, Math.min(newY, maxY));
|
||||||
|
keyMapping.value.posX = newX;
|
||||||
|
keyMapping.value.posY = newY;
|
||||||
|
};
|
||||||
|
window.addEventListener("mousemove", moveHandler);
|
||||||
|
const upHandler = () => {
|
||||||
|
window.removeEventListener("mousemove", moveHandler);
|
||||||
|
window.removeEventListener("mouseup", upHandler);
|
||||||
|
if (oldX !== keyMapping.value.posX || oldY !== keyMapping.value.posY) {
|
||||||
|
keyboardStore.edited = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("mouseup", upHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function delCurKeyMapping() {
|
||||||
|
keyboardStore.edited = true;
|
||||||
|
keyboardStore.activeButtonIndex = -1;
|
||||||
|
store.editKeyMappingList.splice(props.index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingPosX = ref(0);
|
||||||
|
const settingPosY = ref(0);
|
||||||
|
function showSetting() {
|
||||||
|
const keyboardElement = document.getElementById(
|
||||||
|
"keyboardElement"
|
||||||
|
) as HTMLElement;
|
||||||
|
const maxWidth = keyboardElement.clientWidth - 150;
|
||||||
|
const maxHeight = keyboardElement.clientHeight - 300;
|
||||||
|
|
||||||
|
settingPosX.value = Math.min(keyMapping.value.posX + 40, maxWidth);
|
||||||
|
settingPosY.value = Math.min(keyMapping.value.posY - 30, maxHeight);
|
||||||
|
keyboardStore.showButtonSettingFlag = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="{ active: isActive }"
|
||||||
|
:style="{
|
||||||
|
left: `${keyMapping.posX - 30}px`,
|
||||||
|
top: `${keyMapping.posY - 30}px`,
|
||||||
|
}"
|
||||||
|
@mousedown="dragHandler"
|
||||||
|
class="key-observation"
|
||||||
|
ref="elementRef"
|
||||||
|
>
|
||||||
|
<NIcon size="25"><Eye /></NIcon>
|
||||||
|
<span>{{ keyMapping.key }}</span>
|
||||||
|
<NButton
|
||||||
|
class="key-close-btn"
|
||||||
|
text
|
||||||
|
@click="delCurKeyMapping"
|
||||||
|
:type="isActive ? 'primary' : 'info'"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon size="15">
|
||||||
|
<CloseCircle />
|
||||||
|
</NIcon>
|
||||||
|
</template>
|
||||||
|
</NButton>
|
||||||
|
<NButton
|
||||||
|
class="key-setting-btn"
|
||||||
|
text
|
||||||
|
@click="showSetting"
|
||||||
|
:type="isActive ? 'primary' : 'info'"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon size="15">
|
||||||
|
<Settings />
|
||||||
|
</NIcon>
|
||||||
|
</template>
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="key-setting"
|
||||||
|
v-if="isActive && keyboardStore.showButtonSettingFlag"
|
||||||
|
:style="{
|
||||||
|
left: `${settingPosX}px`,
|
||||||
|
top: `${settingPosY}px`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<NH4 prefix="bar">观察视角</NH4>
|
||||||
|
<NFormItem label="灵敏度">
|
||||||
|
<NInputNumber
|
||||||
|
v-model:value="keyMapping.scale"
|
||||||
|
placeholder="请输入灵敏度"
|
||||||
|
:step="0.1"
|
||||||
|
@update:value="keyboardStore.edited = true"
|
||||||
|
/>
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="触点ID">
|
||||||
|
<NInputNumber
|
||||||
|
v-model:value="keyMapping.pointerId"
|
||||||
|
:min="0"
|
||||||
|
placeholder="请输入触点ID"
|
||||||
|
@update:value="keyboardStore.edited = true"
|
||||||
|
/>
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="备注">
|
||||||
|
<NInput
|
||||||
|
v-model:value="keyMapping.note"
|
||||||
|
placeholder="请输入备注"
|
||||||
|
@update:value="keyboardStore.edited = true"
|
||||||
|
/>
|
||||||
|
</NFormItem>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.key-setting {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 10px 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 150px;
|
||||||
|
height: 300px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 2px solid var(--light-color);
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-observation {
|
||||||
|
position: absolute;
|
||||||
|
height: 60px;
|
||||||
|
width: 60px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--blue-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.n-icon {
|
||||||
|
color: var(--blue-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.active):hover {
|
||||||
|
border: 2px solid var(--light-color);
|
||||||
|
color: var(--light-color);
|
||||||
|
|
||||||
|
.n-icon {
|
||||||
|
color: var(--light-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-close-btn {
|
||||||
|
position: absolute;
|
||||||
|
left: 65px;
|
||||||
|
bottom: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-setting-btn {
|
||||||
|
position: absolute;
|
||||||
|
left: 65px;
|
||||||
|
top: 45px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
.n-icon {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
480
src/components/keyboard/KeySetting.vue
Normal file
480
src/components/keyboard/KeySetting.vue
Normal file
@ -0,0 +1,480 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Settings, CloseCircle } from "@vicons/ionicons5";
|
||||||
|
import {
|
||||||
|
NButton,
|
||||||
|
NIcon,
|
||||||
|
NH4,
|
||||||
|
NSelect,
|
||||||
|
NFlex,
|
||||||
|
NP,
|
||||||
|
NModal,
|
||||||
|
NCard,
|
||||||
|
NInput,
|
||||||
|
useMessage,
|
||||||
|
} from "naive-ui";
|
||||||
|
import { computed, onActivated, onMounted, ref, watch } from "vue";
|
||||||
|
import { useGlobalStore } from "../../store/global";
|
||||||
|
import { Store } from "@tauri-apps/plugin-store";
|
||||||
|
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||||
|
import { loadDefaultKeyconfig } from "../../invoke";
|
||||||
|
import { KeyMappingConfig } from "../../keyMappingConfig";
|
||||||
|
import { useKeyboardStore } from "../../store/keyboard";
|
||||||
|
|
||||||
|
const store = useGlobalStore();
|
||||||
|
const keyboardStore = useKeyboardStore();
|
||||||
|
const localStore = new Store("store.bin");
|
||||||
|
const message = useMessage();
|
||||||
|
|
||||||
|
const showImportModal = ref(false);
|
||||||
|
const showRenameModal = ref(false);
|
||||||
|
const importModalInputValue = ref("");
|
||||||
|
const renameModalInputValue = ref("");
|
||||||
|
|
||||||
|
const keyMappingNameOptions = computed(() => {
|
||||||
|
return store.keyMappingConfigList.map((item, index) => {
|
||||||
|
return {
|
||||||
|
label: item.title,
|
||||||
|
value: index,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const curRelativeSize = computed(() => {
|
||||||
|
if (store.keyMappingConfigList.length === 0) {
|
||||||
|
return { w: 800, h: 600 };
|
||||||
|
}
|
||||||
|
return store.keyMappingConfigList[store.curKeyMappingIndex].relativeSize;
|
||||||
|
});
|
||||||
|
|
||||||
|
const keySettingPos = ref({ x: 100, y: 100 });
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// loading keySettingPos from local store
|
||||||
|
let storedPos = await localStore.get<{ x: number; y: number }>(
|
||||||
|
"keySettingPos"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (storedPos === null) {
|
||||||
|
await localStore.set("keySettingPos", keySettingPos.value);
|
||||||
|
storedPos = { x: 100, y: 100 };
|
||||||
|
}
|
||||||
|
// apply keySettingPos
|
||||||
|
const keyboardElement = document.getElementById(
|
||||||
|
"keyboardElement"
|
||||||
|
) as HTMLElement;
|
||||||
|
const maxWidth = keyboardElement.clientWidth - 40;
|
||||||
|
const maxHeight = keyboardElement.clientHeight - 40;
|
||||||
|
keySettingPos.value.x = Math.max(0, Math.min(storedPos.x, maxWidth));
|
||||||
|
keySettingPos.value.y = Math.max(0, Math.min(storedPos.y, maxHeight));
|
||||||
|
});
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
// reset editKeyMappingList as the same as keyMappingList
|
||||||
|
resetKeyMappingConfig();
|
||||||
|
// check config relative size
|
||||||
|
checkConfigSize();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => store.curKeyMappingIndex,
|
||||||
|
() => {
|
||||||
|
// check config relative size
|
||||||
|
checkConfigSize();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function dragHandler(downEvent: MouseEvent) {
|
||||||
|
const target = document.getElementById("keySettingBtn") as HTMLElement;
|
||||||
|
const keyboardElement = document.getElementById(
|
||||||
|
"keyboardElement"
|
||||||
|
) as HTMLElement;
|
||||||
|
const maxWidth = keyboardElement.clientWidth - 40;
|
||||||
|
const maxHeight = keyboardElement.clientHeight - 40;
|
||||||
|
|
||||||
|
const oldX = keySettingPos.value.x;
|
||||||
|
const oldY = keySettingPos.value.y;
|
||||||
|
const x = downEvent.clientX;
|
||||||
|
const y = downEvent.clientY;
|
||||||
|
|
||||||
|
let moveFlag = false;
|
||||||
|
const moveHandler = (moveEvent: MouseEvent) => {
|
||||||
|
const newX = oldX + moveEvent.clientX - x;
|
||||||
|
const newY = oldY + moveEvent.clientY - y;
|
||||||
|
keySettingPos.value.x = Math.max(0, Math.min(newX, maxWidth));
|
||||||
|
keySettingPos.value.y = Math.max(0, Math.min(newY, maxHeight));
|
||||||
|
};
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
moveFlag = true;
|
||||||
|
target.style.setProperty("cursor", "grabbing");
|
||||||
|
window.addEventListener("mousemove", moveHandler);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
const upHandler = () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
window.removeEventListener("mousemove", moveHandler);
|
||||||
|
window.removeEventListener("mouseup", upHandler);
|
||||||
|
if (moveFlag) {
|
||||||
|
// move up
|
||||||
|
target.style.setProperty("cursor", "pointer");
|
||||||
|
localStore.set("keySettingPos", keySettingPos.value);
|
||||||
|
} else {
|
||||||
|
// click up
|
||||||
|
keyboardStore.activeButtonIndex = -1;
|
||||||
|
keyboardStore.activeSteeringWheelButtonKeyIndex = -1;
|
||||||
|
keyboardStore.showSettingFlag = !keyboardStore.showSettingFlag;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("mouseup", upHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
function importKeyMappingConfig() {
|
||||||
|
let keyMappingConfig;
|
||||||
|
try {
|
||||||
|
keyMappingConfig = JSON.parse(importModalInputValue.value);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
message.error("导入失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
store.keyMappingConfigList.push(keyMappingConfig);
|
||||||
|
store.setKeyMappingIndex(store.keyMappingConfigList.length - 1);
|
||||||
|
showImportModal.value = false;
|
||||||
|
localStore.set("keyMappingConfigList", store.keyMappingConfigList);
|
||||||
|
message.success("按键方案已导入");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importDefaultKeyMappingConfig() {
|
||||||
|
const data = await loadDefaultKeyconfig();
|
||||||
|
let defaultConfigs: KeyMappingConfig[];
|
||||||
|
let count = 0;
|
||||||
|
try {
|
||||||
|
defaultConfigs = JSON.parse(data);
|
||||||
|
for (const config of defaultConfigs) {
|
||||||
|
store.keyMappingConfigList.push(config);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
message.error("默认按键方案导入失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStore.set("keyMappingConfigList", store.keyMappingConfigList);
|
||||||
|
message.success(`已导入${count}个默认方案`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createKeyMappingConfig() {
|
||||||
|
if (keyboardStore.edited) {
|
||||||
|
message.error("请先保存或还原当前方案");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyboardElement = document.getElementById(
|
||||||
|
"keyboardElement"
|
||||||
|
) as HTMLElement;
|
||||||
|
const newConfig: KeyMappingConfig = {
|
||||||
|
title: "新方案",
|
||||||
|
relativeSize: {
|
||||||
|
w: keyboardElement.clientWidth,
|
||||||
|
h: keyboardElement.clientHeight,
|
||||||
|
},
|
||||||
|
list: [],
|
||||||
|
};
|
||||||
|
store.keyMappingConfigList.push(newConfig);
|
||||||
|
store.setKeyMappingIndex(store.keyMappingConfigList.length - 1);
|
||||||
|
localStore.set("keyMappingConfigList", store.keyMappingConfigList);
|
||||||
|
message.success("新方案已创建");
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyCurKeyMappingConfig() {
|
||||||
|
if (keyboardStore.edited) {
|
||||||
|
message.error("请先保存或还原当前方案");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const curConfig = store.keyMappingConfigList[store.curKeyMappingIndex];
|
||||||
|
const newConfig: KeyMappingConfig = {
|
||||||
|
title: curConfig.title + "-副本",
|
||||||
|
relativeSize: curConfig.relativeSize,
|
||||||
|
list: curConfig.list,
|
||||||
|
};
|
||||||
|
store.keyMappingConfigList.push(newConfig);
|
||||||
|
keyboardStore.activeButtonIndex = -1;
|
||||||
|
keyboardStore.activeSteeringWheelButtonKeyIndex = -1;
|
||||||
|
store.setKeyMappingIndex(store.keyMappingConfigList.length - 1);
|
||||||
|
localStore.set("keyMappingConfigList", store.keyMappingConfigList);
|
||||||
|
message.success("方案已复制为:" + curConfig.title + "-副本");
|
||||||
|
}
|
||||||
|
|
||||||
|
function delCurKeyMappingConfig() {
|
||||||
|
if (store.keyMappingConfigList.length <= 1) {
|
||||||
|
message.error("至少保留一个方案");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const title = store.keyMappingConfigList[store.curKeyMappingIndex].title;
|
||||||
|
store.keyMappingConfigList.splice(store.curKeyMappingIndex, 1);
|
||||||
|
|
||||||
|
// reset active and edit status
|
||||||
|
keyboardStore.activeButtonIndex = -1;
|
||||||
|
keyboardStore.activeSteeringWheelButtonKeyIndex = -1;
|
||||||
|
keyboardStore.edited = false;
|
||||||
|
store.setKeyMappingIndex(
|
||||||
|
store.curKeyMappingIndex > 0 ? store.curKeyMappingIndex - 1 : 0
|
||||||
|
);
|
||||||
|
localStore.set("keyMappingConfigList", store.keyMappingConfigList);
|
||||||
|
message.success("方案已删除:" + title);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renameKeyMappingConfig() {
|
||||||
|
const newTitle = renameModalInputValue.value;
|
||||||
|
showRenameModal.value = false;
|
||||||
|
if (newTitle !== "") {
|
||||||
|
store.keyMappingConfigList[store.curKeyMappingIndex].title = newTitle;
|
||||||
|
localStore.set("keyMappingConfigList", store.keyMappingConfigList);
|
||||||
|
message.success("方案已重命名为:" + newTitle);
|
||||||
|
} else {
|
||||||
|
message.error("方案名不能为空");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportKeyMappingConfig() {
|
||||||
|
const config = store.keyMappingConfigList[store.curKeyMappingIndex];
|
||||||
|
const data = JSON.stringify(config, null, 2);
|
||||||
|
writeText(data)
|
||||||
|
.then(() => {
|
||||||
|
message.success("当前按键方案已导出到剪切板");
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
message.error("按键方案导出失败");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveKeyMappingConfig() {
|
||||||
|
if (store.applyEditKeyMappingList()) {
|
||||||
|
keyboardStore.edited = false;
|
||||||
|
} else {
|
||||||
|
message.error("存在重复按键,无法保存");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkConfigSize() {
|
||||||
|
const keyboardElement = document.getElementById(
|
||||||
|
"keyboardElement"
|
||||||
|
) as HTMLElement;
|
||||||
|
const curKeyMappingConfig =
|
||||||
|
store.keyMappingConfigList[store.curKeyMappingIndex];
|
||||||
|
const relativeSize = curKeyMappingConfig.relativeSize;
|
||||||
|
|
||||||
|
if (
|
||||||
|
keyboardElement.clientWidth !== relativeSize.w ||
|
||||||
|
keyboardElement.clientHeight !== relativeSize.h
|
||||||
|
) {
|
||||||
|
message.warning(
|
||||||
|
`请注意当前按键方案"${curKeyMappingConfig.title}"与蒙版尺寸不一致,若有需要可进行迁移`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateKeyMappingConfig() {
|
||||||
|
if (keyboardStore.edited) {
|
||||||
|
message.error("请先保存或还原当前按键方案");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyboardElement = document.getElementById(
|
||||||
|
"keyboardElement"
|
||||||
|
) as HTMLElement;
|
||||||
|
const curKeyMappingConfig =
|
||||||
|
store.keyMappingConfigList[store.curKeyMappingIndex];
|
||||||
|
|
||||||
|
const relativeSize = curKeyMappingConfig.relativeSize;
|
||||||
|
const sizeW = keyboardElement.clientWidth;
|
||||||
|
const sizeH = keyboardElement.clientHeight;
|
||||||
|
|
||||||
|
if (sizeW !== relativeSize.w || sizeH !== relativeSize.h) {
|
||||||
|
// deep clone
|
||||||
|
const newConfig = JSON.parse(JSON.stringify(curKeyMappingConfig));
|
||||||
|
// migrate relativeSize
|
||||||
|
newConfig.relativeSize = {
|
||||||
|
w: sizeW,
|
||||||
|
h: sizeH,
|
||||||
|
};
|
||||||
|
// migrate key pos
|
||||||
|
for (const keyMapping of newConfig.list) {
|
||||||
|
keyMapping.posX = Math.round((keyMapping.posX / relativeSize.w) * sizeW);
|
||||||
|
keyMapping.posY = Math.round((keyMapping.posY / relativeSize.h) * sizeH);
|
||||||
|
}
|
||||||
|
// migrate title
|
||||||
|
newConfig.title += "-迁移";
|
||||||
|
|
||||||
|
store.keyMappingConfigList.splice(
|
||||||
|
store.curKeyMappingIndex + 1,
|
||||||
|
0,
|
||||||
|
newConfig
|
||||||
|
);
|
||||||
|
message.success("已迁移到新方案:" + newConfig.title);
|
||||||
|
keyboardStore.activeButtonIndex = -1;
|
||||||
|
keyboardStore.activeSteeringWheelButtonKeyIndex = -1;
|
||||||
|
store.setKeyMappingIndex(store.curKeyMappingIndex + 1);
|
||||||
|
} else {
|
||||||
|
message.info("当前方案符合蒙版尺寸,无需迁移");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectKeyMappingConfig(index: number) {
|
||||||
|
if (keyboardStore.edited) {
|
||||||
|
message.error("请先保存或还原当前按键方案");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
keyboardStore.activeButtonIndex = -1;
|
||||||
|
keyboardStore.activeSteeringWheelButtonKeyIndex = -1;
|
||||||
|
store.setKeyMappingIndex(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetKeyMappingConfig() {
|
||||||
|
keyboardStore.activeButtonIndex = -1;
|
||||||
|
keyboardStore.activeSteeringWheelButtonKeyIndex = -1;
|
||||||
|
store.resetEditKeyMappingList();
|
||||||
|
keyboardStore.edited = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NButton
|
||||||
|
circle
|
||||||
|
type="info"
|
||||||
|
size="large"
|
||||||
|
class="key-setting-btn"
|
||||||
|
id="keySettingBtn"
|
||||||
|
title="长按可拖动"
|
||||||
|
@mousedown="dragHandler"
|
||||||
|
:style="{
|
||||||
|
left: keySettingPos.x + 'px',
|
||||||
|
top: keySettingPos.y + 'px',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon><Settings /></NIcon>
|
||||||
|
</template>
|
||||||
|
</NButton>
|
||||||
|
<div
|
||||||
|
class="key-setting"
|
||||||
|
v-show="keyboardStore.showSettingFlag"
|
||||||
|
@mousedown="
|
||||||
|
keyboardStore.activeButtonIndex = -1;
|
||||||
|
keyboardStore.activeSteeringWheelButtonKeyIndex = -1;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<NButton
|
||||||
|
text
|
||||||
|
class="key-setting-close"
|
||||||
|
@click="keyboardStore.showSettingFlag = false"
|
||||||
|
>
|
||||||
|
<NIcon><CloseCircle></CloseCircle></NIcon>
|
||||||
|
</NButton>
|
||||||
|
<NH4 prefix="bar">按键方案</NH4>
|
||||||
|
<NSelect
|
||||||
|
:value="store.curKeyMappingIndex"
|
||||||
|
@update:value="selectKeyMappingConfig"
|
||||||
|
:options="keyMappingNameOptions"
|
||||||
|
/>
|
||||||
|
<NP style="margin-top: 20px">
|
||||||
|
Relative Size:{{ curRelativeSize.w }}x{{ curRelativeSize.h }}
|
||||||
|
</NP>
|
||||||
|
<NFlex style="margin-top: 20px">
|
||||||
|
<template v-if="keyboardStore.edited">
|
||||||
|
<NButton type="success" @click="saveKeyMappingConfig">保存方案</NButton>
|
||||||
|
<NButton type="error" @click="resetKeyMappingConfig">还原方案</NButton>
|
||||||
|
</template>
|
||||||
|
<NButton @click="createKeyMappingConfig">新建方案</NButton>
|
||||||
|
<NButton @click="copyCurKeyMappingConfig">复制方案</NButton>
|
||||||
|
<NButton @click="migrateKeyMappingConfig">迁移方案</NButton>
|
||||||
|
<NButton @click="delCurKeyMappingConfig">删除方案</NButton>
|
||||||
|
<NButton
|
||||||
|
@click="
|
||||||
|
showRenameModal = true;
|
||||||
|
renameModalInputValue =
|
||||||
|
store.keyMappingConfigList[store.curKeyMappingIndex].title;
|
||||||
|
"
|
||||||
|
>重命名</NButton
|
||||||
|
>
|
||||||
|
</NFlex>
|
||||||
|
<NH4 prefix="bar">其他</NH4>
|
||||||
|
<NFlex>
|
||||||
|
<NButton
|
||||||
|
@click="
|
||||||
|
showImportModal = true;
|
||||||
|
importModalInputValue = '';
|
||||||
|
"
|
||||||
|
>导入方案</NButton
|
||||||
|
>
|
||||||
|
<NButton @click="exportKeyMappingConfig">导出方案</NButton>
|
||||||
|
<NButton @click="importDefaultKeyMappingConfig">导入默认</NButton>
|
||||||
|
<NButton
|
||||||
|
@click="keyboardStore.showKeyInfoFlag = !keyboardStore.showKeyInfoFlag"
|
||||||
|
>按键信息</NButton
|
||||||
|
>
|
||||||
|
</NFlex>
|
||||||
|
<NP style="margin-top: 40px">提示:右键空白区域可添加按键</NP>
|
||||||
|
</div>
|
||||||
|
<NModal v-model:show="showImportModal">
|
||||||
|
<NCard style="width: 40%; height: 50%">
|
||||||
|
<NFlex vertical style="height: 100%">
|
||||||
|
<NInput
|
||||||
|
type="textarea"
|
||||||
|
style="flex-grow: 1"
|
||||||
|
placeholder="粘贴单个按键方案的JSON文本 (此处无法对按键方案的合法性进行判断, 请确保JSON内容正确)"
|
||||||
|
v-model:value="importModalInputValue"
|
||||||
|
round
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
<NButton type="success" round @click="importKeyMappingConfig"
|
||||||
|
>导入</NButton
|
||||||
|
>
|
||||||
|
</NFlex>
|
||||||
|
</NCard>
|
||||||
|
</NModal>
|
||||||
|
<NModal v-model:show="showRenameModal">
|
||||||
|
<NCard style="width: 40%" title="重命名按键方案">
|
||||||
|
<NFlex vertical>
|
||||||
|
<NInput v-model:value="renameModalInputValue" clearable />
|
||||||
|
<NButton type="success" round @click="renameKeyMappingConfig"
|
||||||
|
>重命名</NButton
|
||||||
|
>
|
||||||
|
</NFlex>
|
||||||
|
</NCard>
|
||||||
|
</NModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.key-setting-btn {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-setting {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 70%;
|
||||||
|
height: 70%;
|
||||||
|
margin: auto;
|
||||||
|
background-color: var(--content-bg-color);
|
||||||
|
padding: 0 50px;
|
||||||
|
border: 1px solid var(--gray-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
.key-setting-close {
|
||||||
|
font-size: 24px;
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
368
src/components/keyboard/KeySkill.vue
Normal file
368
src/components/keyboard/KeySkill.vue
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import { useGlobalStore } from "../../store/global";
|
||||||
|
import { Flash, CloseCircle, Settings } from "@vicons/ionicons5";
|
||||||
|
import {
|
||||||
|
NIcon,
|
||||||
|
NButton,
|
||||||
|
NH4,
|
||||||
|
NFormItem,
|
||||||
|
NInput,
|
||||||
|
NInputNumber,
|
||||||
|
NCheckbox,
|
||||||
|
NFlex,
|
||||||
|
} from "naive-ui";
|
||||||
|
import {
|
||||||
|
KeyDirectionalSkill,
|
||||||
|
KeyTriggerWhenPressedSkill,
|
||||||
|
} from "../../keyMappingConfig";
|
||||||
|
import { useKeyboardStore } from "../../store/keyboard";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
index: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const keyboardStore = useKeyboardStore();
|
||||||
|
|
||||||
|
const store = useGlobalStore();
|
||||||
|
const elementRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const isActive = computed(
|
||||||
|
() => props.index === keyboardStore.activeButtonIndex
|
||||||
|
);
|
||||||
|
const keyMapping = computed(() => store.editKeyMappingList[props.index]);
|
||||||
|
|
||||||
|
function dragHandler(downEvent: MouseEvent) {
|
||||||
|
keyboardStore.activeButtonIndex = props.index;
|
||||||
|
keyboardStore.showButtonSettingFlag = false;
|
||||||
|
const oldX = keyMapping.value.posX;
|
||||||
|
const oldY = keyMapping.value.posY;
|
||||||
|
const element = elementRef.value;
|
||||||
|
if (element) {
|
||||||
|
const keyboardElement = document.getElementById(
|
||||||
|
"keyboardElement"
|
||||||
|
) as HTMLElement;
|
||||||
|
const maxX = keyboardElement.clientWidth - 30;
|
||||||
|
const maxY = keyboardElement.clientHeight - 30;
|
||||||
|
|
||||||
|
const x = downEvent.clientX;
|
||||||
|
const y = downEvent.clientY;
|
||||||
|
const moveHandler = (moveEvent: MouseEvent) => {
|
||||||
|
let newX = oldX + moveEvent.clientX - x;
|
||||||
|
let newY = oldY + moveEvent.clientY - y;
|
||||||
|
newX = Math.max(30, Math.min(newX, maxX));
|
||||||
|
newY = Math.max(30, Math.min(newY, maxY));
|
||||||
|
keyMapping.value.posX = newX;
|
||||||
|
keyMapping.value.posY = newY;
|
||||||
|
};
|
||||||
|
window.addEventListener("mousemove", moveHandler);
|
||||||
|
const upHandler = () => {
|
||||||
|
window.removeEventListener("mousemove", moveHandler);
|
||||||
|
window.removeEventListener("mouseup", upHandler);
|
||||||
|
if (oldX !== keyMapping.value.posX || oldY !== keyMapping.value.posY) {
|
||||||
|
keyboardStore.edited = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("mouseup", upHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function delCurKeyMapping() {
|
||||||
|
keyboardStore.edited = true;
|
||||||
|
|
||||||
|
keyboardStore.activeButtonIndex = -1;
|
||||||
|
store.editKeyMappingList.splice(props.index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDirectionless = computed(
|
||||||
|
() =>
|
||||||
|
keyMapping.value.type === "DirectionlessSkill" ||
|
||||||
|
(keyMapping.value.type === "TriggerWhenPressedSkill" &&
|
||||||
|
!(keyMapping.value as KeyTriggerWhenPressedSkill).directional)
|
||||||
|
);
|
||||||
|
|
||||||
|
const isTriggerWhenPressed = computed(
|
||||||
|
() => keyMapping.value.type === "TriggerWhenPressedSkill"
|
||||||
|
);
|
||||||
|
|
||||||
|
function changeSkillType(flag: string) {
|
||||||
|
// the design of skill keymapping type is not good
|
||||||
|
const t = keyMapping.value.type;
|
||||||
|
if (flag === "direction") {
|
||||||
|
keyboardStore.edited = true;
|
||||||
|
if (t === "DirectionalSkill") {
|
||||||
|
delete (keyMapping.value as any).range;
|
||||||
|
keyMapping.value.type = "DirectionlessSkill";
|
||||||
|
} else if (t === "DirectionlessSkill") {
|
||||||
|
(keyMapping.value as any).range = 0;
|
||||||
|
keyMapping.value.type = "DirectionalSkill";
|
||||||
|
} else {
|
||||||
|
const k = keyMapping.value as KeyTriggerWhenPressedSkill;
|
||||||
|
k.directional = !k.directional;
|
||||||
|
k.rangeOrTime = k.directional ? 0 : 80;
|
||||||
|
}
|
||||||
|
} else if (flag === "trigger") {
|
||||||
|
keyboardStore.edited = true;
|
||||||
|
if (t === "DirectionalSkill") {
|
||||||
|
const k = keyMapping.value as any;
|
||||||
|
k.directional = true;
|
||||||
|
k.rangeOrTime = k.range;
|
||||||
|
delete k.range;
|
||||||
|
k.type = "TriggerWhenPressedSkill";
|
||||||
|
} else if (t === "DirectionlessSkill") {
|
||||||
|
const k = keyMapping.value as any;
|
||||||
|
k.directional = false;
|
||||||
|
k.rangeOrTime = 80; // touch time
|
||||||
|
k.type = "TriggerWhenPressedSkill";
|
||||||
|
} else {
|
||||||
|
const k = keyMapping.value as any;
|
||||||
|
if (k.directional) {
|
||||||
|
k.range = k.rangeOrTime;
|
||||||
|
delete k.rangeOrTime;
|
||||||
|
k.type = "DirectionalSkill";
|
||||||
|
} else {
|
||||||
|
delete k.rangeOrTime;
|
||||||
|
k.type = "DirectionlessSkill";
|
||||||
|
}
|
||||||
|
delete k.directional;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingPosX = ref(0);
|
||||||
|
const settingPosY = ref(0);
|
||||||
|
|
||||||
|
function showSetting() {
|
||||||
|
const keyboardElement = document.getElementById(
|
||||||
|
"keyboardElement"
|
||||||
|
) as HTMLElement;
|
||||||
|
// setting
|
||||||
|
const maxWidth = keyboardElement.clientWidth - 200;
|
||||||
|
const maxHeight = keyboardElement.clientHeight - 420;
|
||||||
|
settingPosX.value = Math.min(keyMapping.value.posX + 40, maxWidth);
|
||||||
|
settingPosY.value = Math.min(keyMapping.value.posY - 30, maxHeight);
|
||||||
|
updateRangeIndicator(keyboardElement);
|
||||||
|
keyboardStore.showButtonSettingFlag = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rangeIndicatorTop = ref(0);
|
||||||
|
const indicatorLength = ref(0);
|
||||||
|
function updateRangeIndicator(element?: HTMLElement) {
|
||||||
|
if (!element)
|
||||||
|
element = document.getElementById("keyboardElement") as HTMLElement;
|
||||||
|
|
||||||
|
if (!isDirectionless.value) {
|
||||||
|
// indicator
|
||||||
|
const range =
|
||||||
|
keyMapping.value.type === "DirectionalSkill"
|
||||||
|
? (keyMapping.value as KeyDirectionalSkill).range
|
||||||
|
: (keyMapping.value as KeyTriggerWhenPressedSkill).rangeOrTime;
|
||||||
|
indicatorLength.value = Math.round(
|
||||||
|
((element.clientHeight * range) / 100) * 2
|
||||||
|
);
|
||||||
|
rangeIndicatorTop.value = Math.round(
|
||||||
|
element.clientHeight / 2 - indicatorLength.value / 4
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="{ active: isActive }"
|
||||||
|
:style="{
|
||||||
|
left: `${keyMapping.posX - 30}px`,
|
||||||
|
top: `${keyMapping.posY - 30}px`,
|
||||||
|
}"
|
||||||
|
@mousedown="dragHandler"
|
||||||
|
class="key-skill"
|
||||||
|
ref="elementRef"
|
||||||
|
>
|
||||||
|
<NIcon size="25"><Flash /></NIcon>
|
||||||
|
<span>{{ keyMapping.key }}</span>
|
||||||
|
<NButton
|
||||||
|
class="key-close-btn"
|
||||||
|
text
|
||||||
|
@click="delCurKeyMapping"
|
||||||
|
:type="isActive ? 'primary' : 'info'"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon size="15">
|
||||||
|
<CloseCircle />
|
||||||
|
</NIcon>
|
||||||
|
</template>
|
||||||
|
</NButton>
|
||||||
|
<NButton
|
||||||
|
class="key-setting-btn"
|
||||||
|
text
|
||||||
|
@click="showSetting"
|
||||||
|
:type="isActive ? 'primary' : 'info'"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon size="15">
|
||||||
|
<Settings />
|
||||||
|
</NIcon>
|
||||||
|
</template>
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="key-setting"
|
||||||
|
v-if="isActive && keyboardStore.showButtonSettingFlag"
|
||||||
|
:style="{
|
||||||
|
left: `${settingPosX}px`,
|
||||||
|
top: `${settingPosY}px`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<NH4 prefix="bar">技能</NH4>
|
||||||
|
<NFormItem label="选项">
|
||||||
|
<NFlex vertical>
|
||||||
|
<NCheckbox
|
||||||
|
@click="changeSkillType('direction')"
|
||||||
|
:checked="isDirectionless"
|
||||||
|
>无方向技能</NCheckbox
|
||||||
|
>
|
||||||
|
<NCheckbox
|
||||||
|
@click="changeSkillType('trigger')"
|
||||||
|
:checked="isTriggerWhenPressed"
|
||||||
|
>按下时触发</NCheckbox
|
||||||
|
>
|
||||||
|
</NFlex>
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem v-if="!isDirectionless" label="范围">
|
||||||
|
<NInputNumber
|
||||||
|
v-if="keyMapping.type === 'DirectionalSkill'"
|
||||||
|
v-model:value="(keyMapping as KeyDirectionalSkill).range"
|
||||||
|
placeholder="请输入技能范围"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
@update:value="
|
||||||
|
keyboardStore.edited = true;
|
||||||
|
updateRangeIndicator();
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<NInputNumber
|
||||||
|
v-else
|
||||||
|
v-model:value="(keyMapping as KeyTriggerWhenPressedSkill).rangeOrTime"
|
||||||
|
placeholder="请输入技能范围"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
@update:value="
|
||||||
|
keyboardStore.edited = true;
|
||||||
|
updateRangeIndicator();
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem
|
||||||
|
v-if="(keyMapping.type==='TriggerWhenPressedSkill'&&!(keyMapping as KeyTriggerWhenPressedSkill).directional)"
|
||||||
|
label="触摸时长"
|
||||||
|
>
|
||||||
|
<NInputNumber
|
||||||
|
v-model:value="(keyMapping as KeyTriggerWhenPressedSkill).rangeOrTime"
|
||||||
|
:min="0"
|
||||||
|
placeholder="请输入触摸时长(ms)"
|
||||||
|
@update:value="keyboardStore.edited = true"
|
||||||
|
/>
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="触点ID">
|
||||||
|
<NInputNumber
|
||||||
|
v-model:value="keyMapping.pointerId"
|
||||||
|
:min="0"
|
||||||
|
placeholder="请输入触点ID"
|
||||||
|
@update:value="keyboardStore.edited = true"
|
||||||
|
/>
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="备注">
|
||||||
|
<NInput
|
||||||
|
v-model:value="keyMapping.note"
|
||||||
|
placeholder="请输入备注"
|
||||||
|
@update:value="keyboardStore.edited = true"
|
||||||
|
/>
|
||||||
|
</NFormItem>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isActive && keyboardStore.showButtonSettingFlag"
|
||||||
|
class="range-indicator"
|
||||||
|
:style="{
|
||||||
|
top: `${rangeIndicatorTop}px`,
|
||||||
|
width: `${indicatorLength}px`,
|
||||||
|
height: `${indicatorLength}px`,
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.range-indicator {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: auto;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--blue-color);
|
||||||
|
clip-path: polygon(0 0, 100% 0, 50% 25%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-setting {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 10px 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 200px;
|
||||||
|
height: 420px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 2px solid var(--light-color);
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-skill {
|
||||||
|
position: absolute;
|
||||||
|
height: 60px;
|
||||||
|
width: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 2px solid var(--blue-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.n-icon {
|
||||||
|
color: var(--blue-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.active):hover {
|
||||||
|
border: 2px solid var(--light-color);
|
||||||
|
color: var(--light-color);
|
||||||
|
|
||||||
|
.n-icon {
|
||||||
|
color: var(--light-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-close-btn {
|
||||||
|
position: absolute;
|
||||||
|
left: 65px;
|
||||||
|
bottom: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-setting-btn {
|
||||||
|
position: absolute;
|
||||||
|
left: 65px;
|
||||||
|
top: 45px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
.n-icon {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
282
src/components/keyboard/KeySteeringWheel.vue
Normal file
282
src/components/keyboard/KeySteeringWheel.vue
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import { useGlobalStore } from "../../store/global";
|
||||||
|
import { KeySteeringWheel } from "../../keyMappingConfig";
|
||||||
|
import { NButton, NFormItem, NH4, NIcon, NInput, NInputNumber } from "naive-ui";
|
||||||
|
import { CloseCircle, Move, Settings } from "@vicons/ionicons5";
|
||||||
|
import { useKeyboardStore } from "../../store/keyboard";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
index: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const keyboardStore = useKeyboardStore();
|
||||||
|
|
||||||
|
const store = useGlobalStore();
|
||||||
|
const elementRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const isActive = computed(
|
||||||
|
() => props.index === keyboardStore.activeButtonIndex
|
||||||
|
);
|
||||||
|
const keyMapping = computed(
|
||||||
|
() => store.editKeyMappingList[props.index] as KeySteeringWheel
|
||||||
|
);
|
||||||
|
|
||||||
|
const offset = computed(() => {
|
||||||
|
const keyboardElement = document.getElementById("keyboardElement");
|
||||||
|
if (keyboardElement) {
|
||||||
|
const clientWidth = keyboardElement.clientWidth;
|
||||||
|
const screenSizeW =
|
||||||
|
store.screenSizeW === 0 ? clientWidth : store.screenSizeW;
|
||||||
|
return (
|
||||||
|
((keyMapping.value as KeySteeringWheel).offset * clientWidth) /
|
||||||
|
screenSizeW
|
||||||
|
);
|
||||||
|
} else return (keyMapping.value as KeySteeringWheel).offset;
|
||||||
|
});
|
||||||
|
|
||||||
|
function dragHandler(downEvent: MouseEvent) {
|
||||||
|
keyboardStore.activeButtonIndex = props.index;
|
||||||
|
keyboardStore.showButtonSettingFlag = false;
|
||||||
|
const oldX = keyMapping.value.posX;
|
||||||
|
const oldY = keyMapping.value.posY;
|
||||||
|
const element = elementRef.value;
|
||||||
|
if (element) {
|
||||||
|
const keyboardElement = document.getElementById(
|
||||||
|
"keyboardElement"
|
||||||
|
) as HTMLElement;
|
||||||
|
const maxX = keyboardElement.clientWidth - offset.value;
|
||||||
|
const maxY = keyboardElement.clientHeight - offset.value;
|
||||||
|
|
||||||
|
const x = downEvent.clientX;
|
||||||
|
const y = downEvent.clientY;
|
||||||
|
const moveHandler = (moveEvent: MouseEvent) => {
|
||||||
|
let newX = oldX + moveEvent.clientX - x;
|
||||||
|
let newY = oldY + moveEvent.clientY - y;
|
||||||
|
newX = Math.max(offset.value, Math.min(newX, maxX));
|
||||||
|
newY = Math.max(offset.value, Math.min(newY, maxY));
|
||||||
|
keyMapping.value.posX = newX;
|
||||||
|
keyMapping.value.posY = newY;
|
||||||
|
};
|
||||||
|
window.addEventListener("mousemove", moveHandler);
|
||||||
|
const upHandler = () => {
|
||||||
|
window.removeEventListener("mousemove", moveHandler);
|
||||||
|
window.removeEventListener("mouseup", upHandler);
|
||||||
|
if (oldX !== keyMapping.value.posX || oldY !== keyMapping.value.posY) {
|
||||||
|
keyboardStore.edited = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("mouseup", upHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function delCurKeyMapping() {
|
||||||
|
keyboardStore.edited = true;
|
||||||
|
keyboardStore.activeButtonIndex = -1;
|
||||||
|
store.editKeyMappingList.splice(props.index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingPosX = ref(0);
|
||||||
|
const settingPosY = ref(0);
|
||||||
|
function showSetting() {
|
||||||
|
const keyboardElement = document.getElementById(
|
||||||
|
"keyboardElement"
|
||||||
|
) as HTMLElement;
|
||||||
|
const maxWidth = keyboardElement.clientWidth - 150;
|
||||||
|
const maxHeight = keyboardElement.clientHeight - 300;
|
||||||
|
|
||||||
|
settingPosX.value = Math.min(
|
||||||
|
keyMapping.value.posX + offset.value + 10,
|
||||||
|
maxWidth
|
||||||
|
);
|
||||||
|
settingPosY.value = Math.min(keyMapping.value.posY - offset.value, maxHeight);
|
||||||
|
keyboardStore.showButtonSettingFlag = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="{ active: isActive }"
|
||||||
|
:style="{
|
||||||
|
left: `${keyMapping.posX - offset}px`,
|
||||||
|
top: `${keyMapping.posY - offset}px`,
|
||||||
|
width: `${offset * 2}px`,
|
||||||
|
height: `${offset * 2}px`,
|
||||||
|
}"
|
||||||
|
@mousedown="dragHandler"
|
||||||
|
class="key-steering-wheel"
|
||||||
|
ref="elementRef"
|
||||||
|
>
|
||||||
|
<i />
|
||||||
|
<span
|
||||||
|
@mousedown="keyboardStore.activeSteeringWheelButtonKeyIndex = 0"
|
||||||
|
:class="{
|
||||||
|
'active-wheel':
|
||||||
|
isActive && keyboardStore.activeSteeringWheelButtonKeyIndex == 0,
|
||||||
|
}"
|
||||||
|
>{{ keyMapping.key.up }}</span
|
||||||
|
>
|
||||||
|
<i />
|
||||||
|
<span
|
||||||
|
@mousedown="keyboardStore.activeSteeringWheelButtonKeyIndex = 2"
|
||||||
|
:class="{
|
||||||
|
'active-wheel':
|
||||||
|
isActive && keyboardStore.activeSteeringWheelButtonKeyIndex == 2,
|
||||||
|
}"
|
||||||
|
>{{ keyMapping.key.left }}</span
|
||||||
|
>
|
||||||
|
<NIcon size="20">
|
||||||
|
<Move />
|
||||||
|
</NIcon>
|
||||||
|
<span
|
||||||
|
@mousedown="keyboardStore.activeSteeringWheelButtonKeyIndex = 3"
|
||||||
|
:class="{
|
||||||
|
'active-wheel':
|
||||||
|
isActive && keyboardStore.activeSteeringWheelButtonKeyIndex == 3,
|
||||||
|
}"
|
||||||
|
>{{ keyMapping.key.right }}</span
|
||||||
|
>
|
||||||
|
<i />
|
||||||
|
<span
|
||||||
|
@mousedown="keyboardStore.activeSteeringWheelButtonKeyIndex = 1"
|
||||||
|
:class="{
|
||||||
|
'active-wheel':
|
||||||
|
isActive && keyboardStore.activeSteeringWheelButtonKeyIndex == 1,
|
||||||
|
}"
|
||||||
|
>{{ keyMapping.key.down }}</span
|
||||||
|
>
|
||||||
|
<i />
|
||||||
|
<NButton
|
||||||
|
class="key-close-btn"
|
||||||
|
text
|
||||||
|
@click="delCurKeyMapping"
|
||||||
|
:type="isActive ? 'primary' : 'info'"
|
||||||
|
:style="{
|
||||||
|
left: `${offset * 2 + 10}px`,
|
||||||
|
bottom: `${offset * 2 - 20}px`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon size="15">
|
||||||
|
<CloseCircle />
|
||||||
|
</NIcon>
|
||||||
|
</template>
|
||||||
|
</NButton>
|
||||||
|
<NButton
|
||||||
|
class="key-setting-btn"
|
||||||
|
text
|
||||||
|
@click="showSetting"
|
||||||
|
:type="isActive ? 'primary' : 'info'"
|
||||||
|
:style="{
|
||||||
|
left: `${offset * 2 + 10}px`,
|
||||||
|
top: `${offset * 2 - 20}px`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon size="15">
|
||||||
|
<Settings />
|
||||||
|
</NIcon>
|
||||||
|
</template>
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="key-setting"
|
||||||
|
v-if="isActive && keyboardStore.showButtonSettingFlag"
|
||||||
|
:style="{
|
||||||
|
left: `${settingPosX}px`,
|
||||||
|
top: `${settingPosY}px`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<NH4 prefix="bar">键盘行走</NH4>
|
||||||
|
<NFormItem label="偏移">
|
||||||
|
<NInputNumber
|
||||||
|
v-model:value="keyMapping.offset"
|
||||||
|
:min="1"
|
||||||
|
@update:value="keyboardStore.edited = true"
|
||||||
|
/>
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="触点ID">
|
||||||
|
<NInputNumber
|
||||||
|
v-model:value="keyMapping.pointerId"
|
||||||
|
:min="0"
|
||||||
|
placeholder="请输入触点ID"
|
||||||
|
@update:value="keyboardStore.edited = true"
|
||||||
|
/>
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="备注">
|
||||||
|
<NInput
|
||||||
|
v-model:value="keyMapping.note"
|
||||||
|
placeholder="请输入备注"
|
||||||
|
@update:value="keyboardStore.edited = true"
|
||||||
|
/>
|
||||||
|
</NFormItem>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.key-setting {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 10px 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 150px;
|
||||||
|
height: 300px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 2px solid var(--light-color);
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-steering-wheel {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 2px solid var(--blue-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 33%);
|
||||||
|
grid-template-rows: repeat(3, 33%);
|
||||||
|
justify-items: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:not(.active):hover {
|
||||||
|
border: 2px solid var(--light-color);
|
||||||
|
color: var(--light-color);
|
||||||
|
|
||||||
|
.n-icon {
|
||||||
|
color: var(--light-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
color: var(--primary-hover-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-setting-btn,
|
||||||
|
.key-close-btn {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
.n-icon {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-wheel {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,17 +1,143 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NH4, NForm, FormInst } from "naive-ui";
|
import { Store } from "@tauri-apps/plugin-store";
|
||||||
|
import { Refresh, TrashBinOutline } from "@vicons/ionicons5";
|
||||||
|
import {
|
||||||
|
NH4,
|
||||||
|
NP,
|
||||||
|
NButton,
|
||||||
|
NFlex,
|
||||||
|
NList,
|
||||||
|
NListItem,
|
||||||
|
NModal,
|
||||||
|
NInput,
|
||||||
|
useDialog,
|
||||||
|
NCard,
|
||||||
|
NIcon,
|
||||||
|
} from "naive-ui";
|
||||||
|
import { relaunch } from "@tauri-apps/plugin-process";
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
|
||||||
import { ref } from "vue";
|
const localStore = new Store("store.bin");
|
||||||
|
const dialog = useDialog();
|
||||||
|
|
||||||
const formRef = ref<FormInst | null>(null);
|
const localStoreEntries = ref<[string, unknown][]>([]);
|
||||||
|
const showDataModal = ref(false);
|
||||||
|
const dataModalInputVal = ref("");
|
||||||
|
let curDataIndex = -1;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
refreshLocalData();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function refreshLocalData() {
|
||||||
|
localStoreEntries.value = await localStore.entries();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLocalStore(index: number) {
|
||||||
|
curDataIndex = index;
|
||||||
|
dataModalInputVal.value = JSON.stringify(
|
||||||
|
localStoreEntries.value[index][1],
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
showDataModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function delLocalStore(key?: string) {
|
||||||
|
if (key) {
|
||||||
|
dialog.warning({
|
||||||
|
title: "Warning",
|
||||||
|
content: `即将删除数据"${key}",删除操作不可撤回,是否继续?`,
|
||||||
|
positiveText: "删除",
|
||||||
|
negativeText: "取消",
|
||||||
|
onPositiveClick: () => {
|
||||||
|
localStore.delete(key);
|
||||||
|
localStoreEntries.value.splice(curDataIndex, 1);
|
||||||
|
showDataModal.value = false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dialog.warning({
|
||||||
|
title: "Warning",
|
||||||
|
content: "即将清空数据,操作不可撤回,且清空后将重启软件,是否继续?",
|
||||||
|
positiveText: "删除",
|
||||||
|
negativeText: "取消",
|
||||||
|
onPositiveClick: () => {
|
||||||
|
// localStore.clear();
|
||||||
|
relaunch();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="setting-page">
|
<div class="setting-page">
|
||||||
<NForm ref="formRef" label-placement="left">
|
<NFlex justify="space-between">
|
||||||
<NH4 prefix="bar">客户端相关</NH4>
|
<NH4 prefix="bar">本地数据</NH4>
|
||||||
</NForm>
|
<NFlex>
|
||||||
|
<NButton
|
||||||
|
tertiary
|
||||||
|
circle
|
||||||
|
type="primary"
|
||||||
|
@click="delLocalStore()"
|
||||||
|
style="margin-right: 20px"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon><TrashBinOutline /></NIcon>
|
||||||
|
</template>
|
||||||
|
</NButton>
|
||||||
|
<NButton
|
||||||
|
tertiary
|
||||||
|
circle
|
||||||
|
type="primary"
|
||||||
|
@click="refreshLocalData()"
|
||||||
|
style="margin-right: 20px"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon><Refresh /></NIcon>
|
||||||
|
</template>
|
||||||
|
</NButton>
|
||||||
|
</NFlex>
|
||||||
|
</NFlex>
|
||||||
|
<NP
|
||||||
|
>删除数据可能导致无法预料的后果,请慎重操作。若出现异常请尝试清空数据并重启软件。</NP
|
||||||
|
>
|
||||||
|
<NList class="data-list" hoverable clickable>
|
||||||
|
<NListItem v-for="(entrie, index) in localStoreEntries">
|
||||||
|
<div @click="showLocalStore(index)">
|
||||||
|
{{ entrie[0] }}
|
||||||
</div>
|
</div>
|
||||||
|
</NListItem>
|
||||||
|
</NList>
|
||||||
|
</div>
|
||||||
|
<NModal v-model:show="showDataModal">
|
||||||
|
<NCard style="width: 50%; height: 80%" title="卡片">
|
||||||
|
<NFlex vertical style="height: 100%">
|
||||||
|
<NInput
|
||||||
|
type="textarea"
|
||||||
|
style="flex-grow: 1"
|
||||||
|
:value="dataModalInputVal"
|
||||||
|
round
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
<NButton
|
||||||
|
type="success"
|
||||||
|
round
|
||||||
|
@click="delLocalStore(localStoreEntries[curDataIndex][0])"
|
||||||
|
>删除当前数据</NButton
|
||||||
|
>
|
||||||
|
</NFlex>
|
||||||
|
</NCard>
|
||||||
|
</NModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.setting-page {
|
||||||
|
padding: 10px 25px;
|
||||||
|
|
||||||
|
.data-list {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import { onMounted, onUnmounted, ref } from "vue";
|
import { onMounted, onUnmounted, ref } from "vue";
|
||||||
import {
|
import {
|
||||||
NH4,
|
NH4,
|
||||||
|
NP,
|
||||||
NForm,
|
NForm,
|
||||||
NGrid,
|
NGrid,
|
||||||
NFormItemGi,
|
NFormItemGi,
|
||||||
@ -12,57 +13,33 @@ import {
|
|||||||
NIcon,
|
NIcon,
|
||||||
FormInst,
|
FormInst,
|
||||||
useMessage,
|
useMessage,
|
||||||
NP,
|
|
||||||
} from "naive-ui";
|
} from "naive-ui";
|
||||||
import {
|
import {
|
||||||
|
LogicalPosition,
|
||||||
|
LogicalSize,
|
||||||
PhysicalPosition,
|
PhysicalPosition,
|
||||||
PhysicalSize,
|
PhysicalSize,
|
||||||
getCurrent,
|
getCurrent,
|
||||||
} from "@tauri-apps/api/window";
|
} from "@tauri-apps/api/window";
|
||||||
import { platform } from "@tauri-apps/plugin-os";
|
import { Store } from "@tauri-apps/plugin-store";
|
||||||
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 = () => {};
|
||||||
|
|
||||||
let factor = 1;
|
let factor = 1;
|
||||||
let platformName = "";
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
areaModel.value.posX = Math.floor(pos.x + ml);
|
|
||||||
areaModel.value.posY = Math.floor(pos.y + mt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const localStore = new Store("store.bin");
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
|
||||||
const formRef = ref<FormInst | null>(null);
|
const formRef = ref<FormInst | null>(null);
|
||||||
|
|
||||||
// logical pos and size of the mask area
|
// logical pos and size of the mask area
|
||||||
|
interface MaskArea {
|
||||||
|
posX: number;
|
||||||
|
posY: number;
|
||||||
|
sizeW: number;
|
||||||
|
sizeH: number;
|
||||||
|
}
|
||||||
const areaModel = ref({
|
const areaModel = ref({
|
||||||
posX: 0,
|
posX: 0,
|
||||||
posY: 0,
|
posY: 0,
|
||||||
@ -97,12 +74,32 @@ const areaFormRules: FormRules = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function refreshAreaModel(size?: PhysicalSize, pos?: PhysicalPosition) {
|
||||||
|
const lSize = size?.toLogical(factor);
|
||||||
|
const lPos = pos?.toLogical(factor);
|
||||||
|
|
||||||
|
// header size and sidebar size
|
||||||
|
const mt = 30;
|
||||||
|
const ml = 70;
|
||||||
|
|
||||||
|
// use logical position and size
|
||||||
|
if (lSize !== undefined) {
|
||||||
|
areaModel.value.sizeW = Math.round(lSize.width) - ml;
|
||||||
|
areaModel.value.sizeH = Math.round(lSize.height) - mt;
|
||||||
|
}
|
||||||
|
if (lPos !== undefined) {
|
||||||
|
areaModel.value.posX = Math.round(lPos.x) + ml;
|
||||||
|
areaModel.value.posY = Math.round(lPos.y) + mt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleAdjustClick(e: MouseEvent) {
|
function handleAdjustClick(e: MouseEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
formRef.value?.validate((errors) => {
|
formRef.value?.validate((errors) => {
|
||||||
if (!errors) {
|
if (!errors) {
|
||||||
adjustMaskArea().then(() => {
|
adjustMaskArea().then(() => {
|
||||||
message.success("调整完成");
|
localStore.set("maskArea", areaModel.value);
|
||||||
|
message.success("蒙版区域已保存");
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
message.error("请正确输入蒙版的坐标和尺寸");
|
message.error("请正确输入蒙版的坐标和尺寸");
|
||||||
@ -110,7 +107,6 @@ 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
|
||||||
@ -119,30 +115,28 @@ async function adjustMaskArea() {
|
|||||||
|
|
||||||
const appWindow = getCurrent();
|
const appWindow = getCurrent();
|
||||||
|
|
||||||
const pos = new PhysicalPosition(
|
const pos = new LogicalPosition(
|
||||||
areaModel.value.posX - ml,
|
areaModel.value.posX - ml,
|
||||||
areaModel.value.posY - mt
|
areaModel.value.posY - mt
|
||||||
);
|
);
|
||||||
|
|
||||||
const size = new PhysicalSize(
|
const size = new LogicalSize(
|
||||||
areaModel.value.sizeW + ml,
|
areaModel.value.sizeW + ml,
|
||||||
areaModel.value.sizeH + mt
|
areaModel.value.sizeH + 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();
|
factor = await appWindow.scaleFactor();
|
||||||
platformName = await platform();
|
|
||||||
|
let maskArea = await localStore.get<MaskArea>("maskArea");
|
||||||
|
if (maskArea !== null) {
|
||||||
|
areaModel.value = maskArea;
|
||||||
|
}
|
||||||
|
|
||||||
unlistenResize = await appWindow.onResized(({ payload: size }) => {
|
unlistenResize = await appWindow.onResized(({ payload: size }) => {
|
||||||
refreshAreaModel(size, undefined);
|
refreshAreaModel(size, undefined);
|
||||||
@ -165,7 +159,7 @@ onUnmounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="setting-page">
|
<div class="setting-page">
|
||||||
<NFlex justify="space-between" align="center">
|
<NFlex justify="space-between" align="center">
|
||||||
<NH4 prefix="bar">手动调整</NH4>
|
<NH4 prefix="bar">蒙版调整</NH4>
|
||||||
<NButton
|
<NButton
|
||||||
tertiary
|
tertiary
|
||||||
circle
|
circle
|
||||||
@ -213,7 +207,7 @@ onUnmounted(() => {
|
|||||||
/>
|
/>
|
||||||
</NFormItemGi>
|
</NFormItemGi>
|
||||||
</NGrid>
|
</NGrid>
|
||||||
<NP>提示:使用物理坐标、尺寸</NP>
|
<NP>提示:蒙版尺寸与设备尺寸将用于坐标转换,请保证尺寸的准确性</NP>
|
||||||
</NForm>
|
</NForm>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { NH4 } from "naive-ui";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="setting-page">
|
<div class="setting-page">
|
||||||
脚本设置
|
<NH4 prefix="bar">敬请期待</NH4>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.setting-page {
|
||||||
|
padding: 10px 25px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -31,10 +31,11 @@ import { NTabs, NTabPane, NScrollbar } from "naive-ui";
|
|||||||
.setting {
|
.setting {
|
||||||
background-color: var(--content-bg-color);
|
background-color: var(--content-bg-color);
|
||||||
color: var(--light-color);
|
color: var(--light-color);
|
||||||
overflow: hidden;
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.NTabPane {
|
.n-tab-pane {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ export async function sendKey(payload: CmdDataSendKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function touch(payload: CmdDataTouch) {
|
export async function touch(payload: CmdDataTouch) {
|
||||||
|
if (!("time" in payload) || payload.time === undefined) payload.time = 80;
|
||||||
await sendScrcpyMaskCmd(ScrcpyMaskCmdType.Touch, payload);
|
await sendScrcpyMaskCmd(ScrcpyMaskCmdType.Touch, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,11 +33,7 @@ export enum ScrcpyMaskCmdType {
|
|||||||
Shutdown = 18,
|
Shutdown = 18,
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScrcpyMaskCmdData =
|
type ScrcpyMaskCmdData = CmdDataSendKey | CmdDataTouch | CmdDataSwipe | String;
|
||||||
| CmdDataSendKey
|
|
||||||
| CmdDataTouch
|
|
||||||
| CmdDataSwipe
|
|
||||||
| String;
|
|
||||||
|
|
||||||
enum SendKeyAction {
|
enum SendKeyAction {
|
||||||
Default = 0,
|
Default = 0,
|
||||||
@ -62,6 +59,7 @@ interface CmdDataTouch {
|
|||||||
pointerId: number;
|
pointerId: number;
|
||||||
screen: { w: number; h: number };
|
screen: { w: number; h: number };
|
||||||
pos: { x: number; y: number };
|
pos: { x: number; y: number };
|
||||||
|
time?: number; // valid only when action is Default, default 80 milliseconds
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SwipeAction {
|
export enum SwipeAction {
|
||||||
|
481
src/hotkey.ts
481
src/hotkey.ts
@ -5,67 +5,114 @@ import {
|
|||||||
swipe,
|
swipe,
|
||||||
touch,
|
touch,
|
||||||
} from "./frontcommand/scrcpyMaskCmd";
|
} from "./frontcommand/scrcpyMaskCmd";
|
||||||
|
import {
|
||||||
|
KeyCancelSkill,
|
||||||
|
KeyDirectionalSkill,
|
||||||
|
KeyDirectionlessSkill,
|
||||||
|
KeyMacro,
|
||||||
|
KeyMacroList,
|
||||||
|
KeyMappingConfig,
|
||||||
|
KeyObservation,
|
||||||
|
KeySteeringWheel,
|
||||||
|
KeyTap,
|
||||||
|
KeyTriggerWhenPressedSkill,
|
||||||
|
} from "./keyMappingConfig";
|
||||||
|
|
||||||
function clientxToPosx(clientx: number) {
|
function clientxToPosx(clientx: number) {
|
||||||
return clientx < 70 ? 0 : Math.floor(clientx - 70);
|
return clientx < 70
|
||||||
|
? 0
|
||||||
|
: Math.floor((clientx - 70) * (screenSizeW / maskSizeW));
|
||||||
}
|
}
|
||||||
|
|
||||||
function clientyToPosy(clienty: number) {
|
function clientyToPosy(clienty: number) {
|
||||||
return clienty < 30 ? 0 : Math.floor(clienty - 30);
|
return clienty < 30
|
||||||
|
? 0
|
||||||
|
: Math.floor((clienty - 30) * (screenSizeH / maskSizeH));
|
||||||
}
|
}
|
||||||
|
|
||||||
function clientxToPosOffsetx(clientx: number, posx: number, scale: number) {
|
function clientxToPosOffsetx(clientx: number, posx: number, scale = 1) {
|
||||||
let offsetX = clientxToPosx(clientx) - posx;
|
let offsetX = clientxToPosx(clientx) - posx;
|
||||||
return Math.round(offsetX * scale);
|
return Math.round(offsetX * scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clientyToPosOffsety(clienty: number, posy: number, scale: number) {
|
function clientyToPosOffsety(clienty: number, posy: number, scale = 1) {
|
||||||
let offsetY = clientyToPosy(clienty) - posy;
|
let offsetY = clientyToPosy(clienty) - posy;
|
||||||
return Math.round(offsetY * scale);
|
return Math.round(offsetY * scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clientxToCenterOffsetx(clientx: number, range: number, scale = 0.5) {
|
function clientPosToSkillOffset(
|
||||||
return Math.max(
|
clientPos: { x: number; y: number },
|
||||||
-range,
|
range: number
|
||||||
Math.min(range, clientxToPosOffsetx(clientx, screenSizeW / 2, scale))
|
): { offsetX: number; offsetY: number } {
|
||||||
);
|
const maxLength = (100 / maskSizeH) * screenSizeH;
|
||||||
}
|
const centerX = maskSizeW * 0.5;
|
||||||
|
const centerY = maskSizeH * 0.55;
|
||||||
|
const cOffsetX = clientPos.x - 70 - centerX;
|
||||||
|
const cOffsetY = clientPos.y - 30 - centerY;
|
||||||
|
const offsetD = Math.sqrt(cOffsetX ** 2 + cOffsetY ** 2);
|
||||||
|
if (offsetD == 0) {
|
||||||
|
return {
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function clientyToCenterOffsety(clienty: number, range: number, scale = 0.5) {
|
const rangeD = (maskSizeH - centerY) * range * 0.01;
|
||||||
return Math.max(
|
if (offsetD >= rangeD) {
|
||||||
-range,
|
// include the case of rangeD == 0
|
||||||
Math.min(range, clientyToPosOffsety(clienty, screenSizeH * 0.55, scale))
|
return {
|
||||||
);
|
offsetX: Math.round((maxLength / offsetD) * cOffsetX),
|
||||||
|
offsetY: Math.round((maxLength / offsetD) * cOffsetY),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const factor = offsetD / rangeD;
|
||||||
|
return {
|
||||||
|
offsetX: Math.round((cOffsetX / rangeD) * maxLength * factor),
|
||||||
|
offsetY: Math.round((cOffsetY / rangeD) * maxLength * factor),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sleep(ms: number) {
|
async function sleep(ms: number) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateMacroPosX(pos: [string, number] | number): number {
|
function calculateMacroPosX(
|
||||||
if (typeof pos === "number") {
|
posX: [string, number] | number,
|
||||||
return pos;
|
relativeSizeW: number
|
||||||
|
): number {
|
||||||
|
if (typeof posX === "number") {
|
||||||
|
return Math.round(posX * (screenSizeW / relativeSizeW));
|
||||||
}
|
}
|
||||||
if (typeof pos === "string") {
|
if (typeof posX === "string") {
|
||||||
return clientxToPosx(mouseX);
|
return clientxToPosx(mouseX);
|
||||||
} else {
|
} else {
|
||||||
if (pos[0] === "mouse") {
|
if (posX[0] === "mouse") {
|
||||||
return clientxToPosx(mouseX) + pos[1];
|
return (
|
||||||
|
clientxToPosx(mouseX) +
|
||||||
|
Math.round(posX[1] * (screenSizeW / relativeSizeW))
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Invalid pos");
|
throw new Error("Invalid pos");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateMacroPosY(pos: [string, number] | number): number {
|
function calculateMacroPosY(
|
||||||
if (typeof pos === "number") {
|
posY: [string, number] | number,
|
||||||
return pos;
|
relativeSizeH: number
|
||||||
|
): number {
|
||||||
|
if (typeof posY === "number") {
|
||||||
|
return Math.round(posY * (screenSizeH / relativeSizeH));
|
||||||
}
|
}
|
||||||
if (typeof pos === "string") {
|
if (typeof posY === "string") {
|
||||||
return clientyToPosy(mouseY);
|
return clientyToPosy(mouseY);
|
||||||
} else {
|
} else {
|
||||||
if (pos[0] === "mouse") {
|
if (posY[0] === "mouse") {
|
||||||
return clientyToPosy(mouseY) + pos[1];
|
return (
|
||||||
|
clientyToPosy(mouseY) +
|
||||||
|
Math.round(posY[1] * (screenSizeH / relativeSizeH))
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Invalid pos");
|
throw new Error("Invalid pos");
|
||||||
}
|
}
|
||||||
@ -73,22 +120,23 @@ function calculateMacroPosY(pos: [string, number] | number): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function calculateMacroPosList(
|
function calculateMacroPosList(
|
||||||
posList: [[string, number] | number, [string, number] | number][]
|
posList: [[string, number] | number, [string, number] | number][],
|
||||||
|
relativeSize: { w: number; h: number }
|
||||||
): { x: number; y: number }[] {
|
): { x: number; y: number }[] {
|
||||||
return posList.map((posPair) => {
|
return posList.map((posPair) => {
|
||||||
return {
|
return {
|
||||||
x: calculateMacroPosX(posPair[0]),
|
x: calculateMacroPosX(posPair[0], relativeSize.w),
|
||||||
y: calculateMacroPosY(posPair[1]),
|
y: calculateMacroPosY(posPair[1], relativeSize.h),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO 偶尔不定时抽风(切换一下程序就能恢复),表现为setinterval中的回调函数未执行
|
// TODO ? 技能界面实际上是有投影变换的,需要一定的算法,不能仅仅相对坐标 (640,400)
|
||||||
// TODO 技能界面实际上是有投影变换的,需要一定的算法,不能仅仅相对坐标 (640,400)
|
|
||||||
|
|
||||||
// add shortcuts for observation
|
// add shortcuts for observation
|
||||||
function addObservationShortcuts(
|
function addObservationShortcuts(
|
||||||
key: string,
|
key: string,
|
||||||
|
relativeSize: { w: number; h: number },
|
||||||
posX: number,
|
posX: number,
|
||||||
posY: number,
|
posY: number,
|
||||||
scale: number,
|
scale: number,
|
||||||
@ -96,6 +144,8 @@ function addObservationShortcuts(
|
|||||||
) {
|
) {
|
||||||
let observationMouseX = 0;
|
let observationMouseX = 0;
|
||||||
let observationMouseY = 0;
|
let observationMouseY = 0;
|
||||||
|
posX = Math.round((posX / relativeSize.w) * screenSizeW);
|
||||||
|
posY = Math.round((posY / relativeSize.h) * screenSizeH);
|
||||||
addShortcut(
|
addShortcut(
|
||||||
key,
|
key,
|
||||||
async () => {
|
async () => {
|
||||||
@ -145,13 +195,17 @@ function addObservationShortcuts(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// add shortcuts for simple tap (touch for 100 ms when pressed)
|
// add shortcuts for simple tap (touch when press down)
|
||||||
function addTapShortcuts(
|
function addTapShortcuts(
|
||||||
key: string,
|
key: string,
|
||||||
|
relativeSize: { w: number; h: number },
|
||||||
|
time: number,
|
||||||
posX: number,
|
posX: number,
|
||||||
posY: number,
|
posY: number,
|
||||||
pointerId: number
|
pointerId: number
|
||||||
) {
|
) {
|
||||||
|
posX = Math.round((posX / relativeSize.w) * screenSizeW);
|
||||||
|
posY = Math.round((posY / relativeSize.h) * screenSizeH);
|
||||||
addShortcut(
|
addShortcut(
|
||||||
key,
|
key,
|
||||||
async () => {
|
async () => {
|
||||||
@ -166,6 +220,7 @@ function addTapShortcuts(
|
|||||||
x: posX,
|
x: posX,
|
||||||
y: posY,
|
y: posY,
|
||||||
},
|
},
|
||||||
|
time,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
@ -176,10 +231,13 @@ function addTapShortcuts(
|
|||||||
// add shortcuts for cancel skill
|
// add shortcuts for cancel skill
|
||||||
function addCancelSkillShortcuts(
|
function addCancelSkillShortcuts(
|
||||||
key: string,
|
key: string,
|
||||||
|
relativeSize: { w: number; h: number },
|
||||||
posX: number,
|
posX: number,
|
||||||
posY: number,
|
posY: number,
|
||||||
pointerId: number
|
pointerId: number
|
||||||
) {
|
) {
|
||||||
|
posX = Math.round((posX / relativeSize.w) * screenSizeW);
|
||||||
|
posY = Math.round((posY / relativeSize.h) * screenSizeH);
|
||||||
addShortcut(
|
addShortcut(
|
||||||
key,
|
key,
|
||||||
async () => {
|
async () => {
|
||||||
@ -227,19 +285,27 @@ function addCancelSkillShortcuts(
|
|||||||
// add shortcuts for trigger when pressed skill
|
// add shortcuts for trigger when pressed skill
|
||||||
function addTriggerWhenPressedSkillShortcuts(
|
function addTriggerWhenPressedSkillShortcuts(
|
||||||
key: string,
|
key: string,
|
||||||
|
relativeSize: { w: number; h: number },
|
||||||
// pos relative to the device
|
// pos relative to the device
|
||||||
posX: number,
|
posX: number,
|
||||||
posY: number,
|
posY: number,
|
||||||
directional: boolean,
|
directional: boolean,
|
||||||
// range is needed when directional is true
|
// range: when directional is true
|
||||||
range: number,
|
// time: when directional is false
|
||||||
|
rangeOrTime: number,
|
||||||
pointerId: number
|
pointerId: number
|
||||||
) {
|
) {
|
||||||
if (directional) {
|
if (directional) {
|
||||||
|
posX = Math.round((posX / relativeSize.w) * screenSizeW);
|
||||||
|
posY = Math.round((posY / relativeSize.h) * screenSizeH);
|
||||||
addShortcut(
|
addShortcut(
|
||||||
key,
|
key,
|
||||||
// down
|
// down
|
||||||
async () => {
|
async () => {
|
||||||
|
const skillOffset = clientPosToSkillOffset(
|
||||||
|
{ x: mouseX, y: mouseY },
|
||||||
|
rangeOrTime
|
||||||
|
);
|
||||||
await swipe({
|
await swipe({
|
||||||
action: SwipeAction.Default,
|
action: SwipeAction.Default,
|
||||||
pointerId,
|
pointerId,
|
||||||
@ -250,8 +316,8 @@ function addTriggerWhenPressedSkillShortcuts(
|
|||||||
pos: [
|
pos: [
|
||||||
{ x: posX, y: posY },
|
{ x: posX, y: posY },
|
||||||
{
|
{
|
||||||
x: posX + clientxToCenterOffsetx(mouseX, range),
|
x: posX + skillOffset.offsetX,
|
||||||
y: posY + clientyToCenterOffsety(mouseY, range),
|
y: posY + skillOffset.offsetY,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
intervalBetweenPos: 0,
|
intervalBetweenPos: 0,
|
||||||
@ -261,18 +327,21 @@ function addTriggerWhenPressedSkillShortcuts(
|
|||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
addTapShortcuts(key, posX, posY, pointerId);
|
addTapShortcuts(key, relativeSize, rangeOrTime, posX, posY, pointerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add shortcuts for directionless skill (cancelable)
|
// add shortcuts for directionless skill (cancelable)
|
||||||
function addDirectionlessSkillShortcuts(
|
function addDirectionlessSkillShortcuts(
|
||||||
key: string,
|
key: string,
|
||||||
|
relativeSize: { w: number; h: number },
|
||||||
// pos relative to the device
|
// pos relative to the device
|
||||||
posX: number,
|
posX: number,
|
||||||
posY: number,
|
posY: number,
|
||||||
pointerId: number
|
pointerId: number
|
||||||
) {
|
) {
|
||||||
|
posX = Math.round((posX / relativeSize.w) * screenSizeW);
|
||||||
|
posY = Math.round((posY / relativeSize.h) * screenSizeH);
|
||||||
addShortcut(
|
addShortcut(
|
||||||
key,
|
key,
|
||||||
// down
|
// down
|
||||||
@ -314,16 +383,23 @@ function addDirectionlessSkillShortcuts(
|
|||||||
// add shortcuts for directional skill (cancelable)
|
// add shortcuts for directional skill (cancelable)
|
||||||
function addDirectionalSkillShortcuts(
|
function addDirectionalSkillShortcuts(
|
||||||
key: string,
|
key: string,
|
||||||
|
relativeSize: { w: number; h: number },
|
||||||
// pos relative to the device
|
// pos relative to the device
|
||||||
posX: number,
|
posX: number,
|
||||||
posY: number,
|
posY: number,
|
||||||
range: number,
|
range: number,
|
||||||
pointerId: number
|
pointerId: number
|
||||||
) {
|
) {
|
||||||
|
posX = Math.round((posX / relativeSize.w) * screenSizeW);
|
||||||
|
posY = Math.round((posY / relativeSize.h) * screenSizeH);
|
||||||
addShortcut(
|
addShortcut(
|
||||||
key,
|
key,
|
||||||
// down
|
// down
|
||||||
async () => {
|
async () => {
|
||||||
|
const skillOffset = clientPosToSkillOffset(
|
||||||
|
{ x: mouseX, y: mouseY },
|
||||||
|
range
|
||||||
|
);
|
||||||
await swipe({
|
await swipe({
|
||||||
action: SwipeAction.NoUp,
|
action: SwipeAction.NoUp,
|
||||||
pointerId,
|
pointerId,
|
||||||
@ -334,8 +410,8 @@ function addDirectionalSkillShortcuts(
|
|||||||
pos: [
|
pos: [
|
||||||
{ x: posX, y: posY },
|
{ x: posX, y: posY },
|
||||||
{
|
{
|
||||||
x: posX + clientxToCenterOffsetx(mouseX, range),
|
x: posX + skillOffset.offsetX,
|
||||||
y: posY + clientyToCenterOffsety(mouseY, range),
|
y: posY + skillOffset.offsetY,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
intervalBetweenPos: 0,
|
intervalBetweenPos: 0,
|
||||||
@ -343,6 +419,10 @@ function addDirectionalSkillShortcuts(
|
|||||||
},
|
},
|
||||||
// loop
|
// loop
|
||||||
async () => {
|
async () => {
|
||||||
|
const skillOffset = clientPosToSkillOffset(
|
||||||
|
{ x: mouseX, y: mouseY },
|
||||||
|
range
|
||||||
|
);
|
||||||
await touch({
|
await touch({
|
||||||
action: TouchAction.Move,
|
action: TouchAction.Move,
|
||||||
pointerId,
|
pointerId,
|
||||||
@ -351,13 +431,17 @@ function addDirectionalSkillShortcuts(
|
|||||||
h: screenSizeH,
|
h: screenSizeH,
|
||||||
},
|
},
|
||||||
pos: {
|
pos: {
|
||||||
x: posX + clientxToCenterOffsetx(mouseX, range),
|
x: posX + skillOffset.offsetX,
|
||||||
y: posY + clientyToCenterOffsety(mouseY, range),
|
y: posY + skillOffset.offsetY,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// up
|
// up
|
||||||
async () => {
|
async () => {
|
||||||
|
const skillOffset = clientPosToSkillOffset(
|
||||||
|
{ x: mouseX, y: mouseY },
|
||||||
|
range
|
||||||
|
);
|
||||||
await touch({
|
await touch({
|
||||||
action: TouchAction.Up,
|
action: TouchAction.Up,
|
||||||
pointerId,
|
pointerId,
|
||||||
@ -366,8 +450,8 @@ function addDirectionalSkillShortcuts(
|
|||||||
h: screenSizeH,
|
h: screenSizeH,
|
||||||
},
|
},
|
||||||
pos: {
|
pos: {
|
||||||
x: posX + clientxToCenterOffsetx(mouseX, range),
|
x: posX + skillOffset.offsetX,
|
||||||
y: posY + clientyToCenterOffsety(mouseY, range),
|
y: posY + skillOffset.offsetY,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -378,6 +462,7 @@ function addDirectionalSkillShortcuts(
|
|||||||
// add shortcuts for steering wheel
|
// add shortcuts for steering wheel
|
||||||
function addSteeringWheelKeyboardShortcuts(
|
function addSteeringWheelKeyboardShortcuts(
|
||||||
key: wheelKey,
|
key: wheelKey,
|
||||||
|
relativeSize: { w: number; h: number },
|
||||||
// pos relative to the device
|
// pos relative to the device
|
||||||
posX: number,
|
posX: number,
|
||||||
posY: number,
|
posY: number,
|
||||||
@ -387,6 +472,8 @@ function addSteeringWheelKeyboardShortcuts(
|
|||||||
let loopFlag = false;
|
let loopFlag = false;
|
||||||
let curPosX = 0;
|
let curPosX = 0;
|
||||||
let curPosY = 0;
|
let curPosY = 0;
|
||||||
|
posX = Math.round((posX / relativeSize.w) * screenSizeW);
|
||||||
|
posY = Math.round((posY / relativeSize.h) * screenSizeH);
|
||||||
|
|
||||||
// calculate the end coordinates of the eight directions of the direction wheel
|
// calculate the end coordinates of the eight directions of the direction wheel
|
||||||
let offsetHalf = Math.round(offset / 1.414);
|
let offsetHalf = Math.round(offset / 1.414);
|
||||||
@ -566,8 +653,11 @@ function addClickShortcuts(key: string, pointerId: number) {
|
|||||||
|
|
||||||
let screenSizeW: number;
|
let screenSizeW: number;
|
||||||
let screenSizeH: number;
|
let screenSizeH: number;
|
||||||
|
let maskSizeW: number;
|
||||||
|
let maskSizeH: number;
|
||||||
let mouseX = 0;
|
let mouseX = 0;
|
||||||
let mouseY = 0;
|
let mouseY = 0;
|
||||||
|
let maskElement: HTMLElement;
|
||||||
|
|
||||||
const downKeyMap: Map<string, boolean> = new Map();
|
const downKeyMap: Map<string, boolean> = new Map();
|
||||||
const downKeyCBMap: Map<string, () => Promise<void>> = new Map();
|
const downKeyCBMap: Map<string, () => Promise<void>> = new Map();
|
||||||
@ -597,6 +687,7 @@ function keyupHandler(event: KeyboardEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleMouseDown(event: MouseEvent) {
|
function handleMouseDown(event: MouseEvent) {
|
||||||
|
if (event.target !== maskElement) return;
|
||||||
mouseX = event.clientX;
|
mouseX = event.clientX;
|
||||||
mouseY = event.clientY;
|
mouseY = event.clientY;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -614,7 +705,7 @@ function handleMouseUp(event: MouseEvent) {
|
|||||||
mouseY = event.clientY;
|
mouseY = event.clientY;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let key = "M" + event.button.toString();
|
let key = "M" + event.button.toString();
|
||||||
if (downKeyMap.has(key)) {
|
if (downKeyMap.has(key) && downKeyMap.get(key)) {
|
||||||
downKeyMap.set(key, false);
|
downKeyMap.set(key, false);
|
||||||
// execute the up callback asyncily
|
// execute the up callback asyncily
|
||||||
let cb = upKeyCBMap.get(key);
|
let cb = upKeyCBMap.get(key);
|
||||||
@ -627,9 +718,25 @@ function handleMouseMove(event: MouseEvent) {
|
|||||||
mouseY = event.clientY;
|
mouseY = event.clientY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let lastWheelDownTime: number = 0;
|
||||||
|
let lastWheelUpTime: number = 0;
|
||||||
|
function handleMouseWheel(event: WheelEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
// trigger interval is 50ms
|
||||||
|
if (event.deltaY > 0 && event.timeStamp - lastWheelDownTime > 50) {
|
||||||
|
lastWheelDownTime = event.timeStamp;
|
||||||
|
// WheelDown
|
||||||
|
downKeyCBMap.get("WheelDown")?.();
|
||||||
|
} else if (event.deltaY < 0 && event.timeStamp - lastWheelUpTime > 50) {
|
||||||
|
lastWheelUpTime = event.timeStamp;
|
||||||
|
// WheelUp
|
||||||
|
downKeyCBMap.get("WheelUp")?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function addShortcut(
|
function addShortcut(
|
||||||
key: string,
|
key: string,
|
||||||
downCB: () => Promise<void>,
|
downCB?: () => Promise<void>,
|
||||||
loopCB?: () => Promise<void>,
|
loopCB?: () => Promise<void>,
|
||||||
upCB?: () => Promise<void>,
|
upCB?: () => Promise<void>,
|
||||||
cancelAble = false // only work with downCB && upCB
|
cancelAble = false // only work with downCB && upCB
|
||||||
@ -681,20 +788,29 @@ function addShortcut(
|
|||||||
* @param macro
|
* @param macro
|
||||||
* @example
|
* @example
|
||||||
* await execMacro([
|
* await execMacro([
|
||||||
|
* // touch down
|
||||||
* {
|
* {
|
||||||
* type: "touch",
|
* type: "touch",
|
||||||
* // op, pointerId, posX, posY
|
* // op, pointerId, posX, posY
|
||||||
* args: ["down", 5, ["mouse", -10], 600],
|
* args: ["down", 5, ["mouse", -10], 600],
|
||||||
* },
|
* },
|
||||||
|
* // sleep 1000ms
|
||||||
* {
|
* {
|
||||||
* type: "sleep",
|
* type: "sleep",
|
||||||
* // time(ms)
|
* // time(ms)
|
||||||
* args: [1000],
|
* args: [1000],
|
||||||
* },
|
* },
|
||||||
|
* // touch up
|
||||||
* {
|
* {
|
||||||
* type: "touch",
|
* type: "touch",
|
||||||
* args: ["up", 5, ["mouse", 10], 600],
|
* args: ["up", 5, ["mouse", 10], 600],
|
||||||
* },
|
* },
|
||||||
|
* // touch 1000ms
|
||||||
|
* {
|
||||||
|
* type: "touch",
|
||||||
|
* args: ["default", 5, ["mouse", 10], 600, 1000],
|
||||||
|
* },
|
||||||
|
* // swipe
|
||||||
* {
|
* {
|
||||||
* type: "swipe",
|
* type: "swipe",
|
||||||
* // op, pointerId, posList, intervalBetweenPos
|
* // op, pointerId, posList, intervalBetweenPos
|
||||||
@ -712,7 +828,11 @@ function addShortcut(
|
|||||||
* },
|
* },
|
||||||
* ]);
|
* ]);
|
||||||
*/
|
*/
|
||||||
async function execMacro(macro: any[]) {
|
async function execMacro(
|
||||||
|
relativeSize: { w: number; h: number },
|
||||||
|
macro: KeyMacroList
|
||||||
|
) {
|
||||||
|
if (macro === null) return;
|
||||||
for (const cmd of macro) {
|
for (const cmd of macro) {
|
||||||
if (!cmd.hasOwnProperty("type") || !cmd.hasOwnProperty("args")) {
|
if (!cmd.hasOwnProperty("type") || !cmd.hasOwnProperty("args")) {
|
||||||
console.error("Invalid command: ", cmd);
|
console.error("Invalid command: ", cmd);
|
||||||
@ -750,9 +870,10 @@ async function execMacro(macro: any[]) {
|
|||||||
h: screenSizeH,
|
h: screenSizeH,
|
||||||
},
|
},
|
||||||
pos: {
|
pos: {
|
||||||
x: calculateMacroPosX(cmd.args[2]),
|
x: calculateMacroPosX(cmd.args[2], relativeSize.w),
|
||||||
y: calculateMacroPosY(cmd.args[3]),
|
y: calculateMacroPosY(cmd.args[3], relativeSize.h),
|
||||||
},
|
},
|
||||||
|
time: cmd.args.length > 4 ? cmd.args[4] : undefined,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "swipe":
|
case "swipe":
|
||||||
@ -778,7 +899,7 @@ async function execMacro(macro: any[]) {
|
|||||||
w: screenSizeW,
|
w: screenSizeW,
|
||||||
h: screenSizeH,
|
h: screenSizeH,
|
||||||
},
|
},
|
||||||
pos: calculateMacroPosList(cmd.args[2]),
|
pos: calculateMacroPosList(cmd.args[2], relativeSize),
|
||||||
intervalBetweenPos: cmd.args[3],
|
intervalBetweenPos: cmd.args[3],
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@ -801,128 +922,172 @@ function execLoopCB() {
|
|||||||
if (loopFlag) requestAnimationFrame(execLoopCB);
|
if (loopFlag) requestAnimationFrame(execLoopCB);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// change ts type
|
||||||
|
function asType<T>(_val: any): asserts _val is T {}
|
||||||
|
|
||||||
|
function applyKeyMappingConfigShortcuts(
|
||||||
|
keyMappingConfig: KeyMappingConfig
|
||||||
|
): boolean {
|
||||||
|
try {
|
||||||
|
const relativeSize = keyMappingConfig.relativeSize;
|
||||||
|
for (const item of keyMappingConfig.list) {
|
||||||
|
switch (item.type) {
|
||||||
|
case "SteeringWheel":
|
||||||
|
asType<KeySteeringWheel>(item);
|
||||||
|
addSteeringWheelKeyboardShortcuts(
|
||||||
|
item.key,
|
||||||
|
relativeSize,
|
||||||
|
item.posX,
|
||||||
|
item.posY,
|
||||||
|
item.offset,
|
||||||
|
item.pointerId
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "DirectionalSkill":
|
||||||
|
asType<KeyDirectionalSkill>(item);
|
||||||
|
addDirectionalSkillShortcuts(
|
||||||
|
item.key,
|
||||||
|
relativeSize,
|
||||||
|
item.posX,
|
||||||
|
item.posY,
|
||||||
|
item.range,
|
||||||
|
item.pointerId
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "DirectionlessSkill":
|
||||||
|
asType<KeyDirectionlessSkill>(item);
|
||||||
|
addDirectionlessSkillShortcuts(
|
||||||
|
item.key,
|
||||||
|
relativeSize,
|
||||||
|
item.posX,
|
||||||
|
item.posY,
|
||||||
|
item.pointerId
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "CancelSkill":
|
||||||
|
asType<KeyCancelSkill>(item);
|
||||||
|
addCancelSkillShortcuts(
|
||||||
|
item.key,
|
||||||
|
relativeSize,
|
||||||
|
item.posX,
|
||||||
|
item.posY,
|
||||||
|
item.pointerId
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "Tap":
|
||||||
|
asType<KeyTap>(item);
|
||||||
|
addTapShortcuts(
|
||||||
|
item.key,
|
||||||
|
relativeSize,
|
||||||
|
item.time,
|
||||||
|
item.posX,
|
||||||
|
item.posY,
|
||||||
|
item.pointerId
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "TriggerWhenPressedSkill":
|
||||||
|
asType<KeyTriggerWhenPressedSkill>(item);
|
||||||
|
addTriggerWhenPressedSkillShortcuts(
|
||||||
|
item.key,
|
||||||
|
relativeSize,
|
||||||
|
item.posX,
|
||||||
|
item.posY,
|
||||||
|
item.directional,
|
||||||
|
item.rangeOrTime,
|
||||||
|
item.pointerId
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "Observation":
|
||||||
|
asType<KeyObservation>(item);
|
||||||
|
addObservationShortcuts(
|
||||||
|
item.key,
|
||||||
|
relativeSize,
|
||||||
|
item.posX,
|
||||||
|
item.posY,
|
||||||
|
item.scale,
|
||||||
|
item.pointerId
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "Macro":
|
||||||
|
asType<KeyMacro>(item);
|
||||||
|
addShortcut(
|
||||||
|
item.key,
|
||||||
|
item.macro.down === null
|
||||||
|
? undefined
|
||||||
|
: async () => {
|
||||||
|
await execMacro(relativeSize, item.macro.down);
|
||||||
|
},
|
||||||
|
item.macro.loop === null
|
||||||
|
? undefined
|
||||||
|
: async () => {
|
||||||
|
await execMacro(relativeSize, item.macro.loop);
|
||||||
|
},
|
||||||
|
item.macro.up === null
|
||||||
|
? undefined
|
||||||
|
: async () => {
|
||||||
|
await execMacro(relativeSize, item.macro.up);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error("Invalid item type: ", item.type);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Invalid keyMappingConfig: ", keyMappingConfig, e);
|
||||||
|
clearShortcuts();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function listenToKeyEvent() {
|
export function listenToKeyEvent() {
|
||||||
document.addEventListener("keydown", keydownHandler);
|
window.addEventListener("keydown", keydownHandler);
|
||||||
document.addEventListener("keyup", keyupHandler);
|
window.addEventListener("keyup", keyupHandler);
|
||||||
loopFlag = true;
|
loopFlag = true;
|
||||||
execLoopCB();
|
execLoopCB();
|
||||||
// setInterval(()=>console.log(loopDownKeyCBMap), 3000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unlistenToKeyEvent() {
|
export function unlistenToKeyEvent() {
|
||||||
document.removeEventListener("keydown", keydownHandler);
|
window.removeEventListener("keydown", keydownHandler);
|
||||||
document.removeEventListener("keyup", keyupHandler);
|
window.removeEventListener("keyup", keyupHandler);
|
||||||
loopFlag = false;
|
loopFlag = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initShortcuts(
|
export function clearShortcuts() {
|
||||||
|
window.removeEventListener("mousedown", handleMouseDown);
|
||||||
|
window.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
window.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
window.removeEventListener("wheel", handleMouseWheel);
|
||||||
|
|
||||||
|
downKeyMap.clear();
|
||||||
|
downKeyCBMap.clear();
|
||||||
|
loopDownKeyCBMap.clear();
|
||||||
|
upKeyCBMap.clear();
|
||||||
|
cancelAbleKeyList.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateScreenSizeAndMaskArea(
|
||||||
screenSize: [number, number],
|
screenSize: [number, number],
|
||||||
element: HTMLElement
|
maskArea: [number, number]
|
||||||
) {
|
) {
|
||||||
screenSizeW = screenSize[0];
|
screenSizeW = screenSize[0];
|
||||||
screenSizeH = screenSize[1];
|
screenSizeH = screenSize[1];
|
||||||
|
maskSizeW = maskArea[0];
|
||||||
|
maskSizeH = maskArea[1];
|
||||||
|
}
|
||||||
|
|
||||||
element.addEventListener("mousedown", handleMouseDown);
|
export function applyShortcuts(
|
||||||
element.addEventListener("mousemove", handleMouseMove);
|
element: HTMLElement,
|
||||||
element.addEventListener("mouseup", handleMouseUp);
|
keyMappingConfig: KeyMappingConfig
|
||||||
element.addEventListener("mouseout", handleMouseUp); // mouse out of the element as mouse up
|
) {
|
||||||
|
maskElement = element;
|
||||||
|
window.addEventListener("mousedown", handleMouseDown);
|
||||||
|
window.addEventListener("mousemove", handleMouseMove);
|
||||||
|
window.addEventListener("mouseup", handleMouseUp);
|
||||||
|
window.addEventListener("wheel", handleMouseWheel);
|
||||||
|
|
||||||
addClickShortcuts("M0", 0);
|
addClickShortcuts("M0", 0);
|
||||||
addSteeringWheelKeyboardShortcuts(
|
return applyKeyMappingConfigShortcuts(keyMappingConfig);
|
||||||
{
|
|
||||||
left: "KeyA",
|
|
||||||
right: "KeyD",
|
|
||||||
up: "KeyW",
|
|
||||||
down: "KeyS",
|
|
||||||
},
|
|
||||||
180,
|
|
||||||
560,
|
|
||||||
100,
|
|
||||||
1
|
|
||||||
);
|
|
||||||
addDirectionalSkillShortcuts("KeyQ", 950, 610, 200, 2); // skill 1
|
|
||||||
addDirectionalSkillShortcuts("AltLeft", 1025, 500, 200, 2); // skill 2
|
|
||||||
addDirectionalSkillShortcuts("KeyE", 1160, 420, 200, 2); // skill 3
|
|
||||||
addTriggerWhenPressedSkillShortcuts("M4", 1160, 420, false, 0, 2); // skill 3 (no direction and trigger when pressed)
|
|
||||||
addDirectionlessSkillShortcuts("M1", 1150, 280, 2); // equipment skill (middle mouse click)
|
|
||||||
addCancelSkillShortcuts("Space", 1160, 140, 2); // cancel skill
|
|
||||||
|
|
||||||
addTapShortcuts("KeyB", 650, 650, 3); // home
|
|
||||||
addTapShortcuts("KeyC", 740, 650, 3); // recover
|
|
||||||
addDirectionalSkillShortcuts("KeyF", 840, 650, 200, 2); // summoner skills
|
|
||||||
addTriggerWhenPressedSkillShortcuts("ControlLeft", 840, 650, false, 0, 3); // summoner skills (no direction and trigger when pressed)
|
|
||||||
addTapShortcuts("M2", 1165, 620, 3); // attack (right click)
|
|
||||||
addTapShortcuts("Digit1", 880, 560, 3); // skill 1 upgrade
|
|
||||||
addTapShortcuts("Digit2", 960, 430, 3); // skill 2 upgrade
|
|
||||||
addTapShortcuts("Digit3", 1090, 350, 3); // skill 3 upgrade
|
|
||||||
addTapShortcuts("Digit5", 130, 300, 3); // quick buy 1
|
|
||||||
addTapShortcuts("Digit6", 130, 370, 3); // quick buy 2
|
|
||||||
|
|
||||||
addObservationShortcuts("M3", 1000, 200, 0.5, 4); // observation
|
|
||||||
|
|
||||||
// panel
|
|
||||||
addShortcut(
|
|
||||||
"Tab",
|
|
||||||
async () => {
|
|
||||||
await execMacro([
|
|
||||||
{
|
|
||||||
type: "touch",
|
|
||||||
args: ["default", 5, 1185, 40],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
async () => {
|
|
||||||
await execMacro([
|
|
||||||
{
|
|
||||||
type: "touch",
|
|
||||||
args: ["default", 5, 1220, 100],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// shop
|
|
||||||
addShortcut(
|
|
||||||
"ShiftLeft",
|
|
||||||
async () => {
|
|
||||||
await execMacro([
|
|
||||||
{
|
|
||||||
type: "touch",
|
|
||||||
args: ["default", 5, 40, 300],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
async () => {
|
|
||||||
await execMacro([
|
|
||||||
{
|
|
||||||
type: "touch",
|
|
||||||
args: ["default", 5, 1200, 60],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// map
|
|
||||||
addShortcut(
|
|
||||||
"KeyZ",
|
|
||||||
async () => {
|
|
||||||
await execMacro([
|
|
||||||
{
|
|
||||||
type: "touch",
|
|
||||||
args: ["default", 5, 250, 230],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
async () => {
|
|
||||||
await execMacro([
|
|
||||||
{
|
|
||||||
type: "touch",
|
|
||||||
args: ["default", 5, 640, 150],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
interface Device {
|
interface Device {
|
||||||
id: string;
|
id: string;
|
||||||
@ -9,10 +9,6 @@ export async function adbDevices(): Promise<Device[]> {
|
|||||||
return await invoke("adb_devices");
|
return await invoke("adb_devices");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getScreenSize(id: string): Promise<[number, number]> {
|
|
||||||
return await invoke("get_screen_size", { id });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function forwardServerPort(
|
export async function forwardServerPort(
|
||||||
id: string,
|
id: string,
|
||||||
scid: string,
|
scid: string,
|
||||||
@ -33,4 +29,18 @@ 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 getDeviceScreenSize(
|
||||||
|
id: string
|
||||||
|
): Promise<[number, number]> {
|
||||||
|
return await invoke("get_device_screen_size", { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function adbConnect(address: string): Promise<string> {
|
||||||
|
return await invoke("adb_connect", { address });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadDefaultKeyconfig(): Promise<string> {
|
||||||
|
return await invoke("load_default_keyconfig");
|
||||||
|
}
|
||||||
|
|
||||||
export type { Device };
|
export type { Device };
|
||||||
|
100
src/keyMappingConfig.ts
Normal file
100
src/keyMappingConfig.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
interface Key {
|
||||||
|
type:
|
||||||
|
| "SteeringWheel"
|
||||||
|
| "DirectionalSkill"
|
||||||
|
| "DirectionlessSkill"
|
||||||
|
| "CancelSkill"
|
||||||
|
| "Tap"
|
||||||
|
| "TriggerWhenPressedSkill"
|
||||||
|
| "Observation"
|
||||||
|
| "Macro";
|
||||||
|
note: string;
|
||||||
|
posX: number;
|
||||||
|
posY: number;
|
||||||
|
pointerId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeySteeringWheel extends Key {
|
||||||
|
key: {
|
||||||
|
left: string;
|
||||||
|
right: string;
|
||||||
|
up: string;
|
||||||
|
down: string;
|
||||||
|
};
|
||||||
|
offset: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeyDirectionalSkill extends Key {
|
||||||
|
key: string;
|
||||||
|
range: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeyDirectionlessSkill extends Key {
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeyCancelSkill extends Key {
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeyTriggerWhenPressedSkill extends Key {
|
||||||
|
key: string;
|
||||||
|
directional: boolean;
|
||||||
|
rangeOrTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeyObservation extends Key {
|
||||||
|
key: string;
|
||||||
|
scale: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeyTap extends Key {
|
||||||
|
key: string;
|
||||||
|
time: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type KeyMacroType = "touch" | "sleep" | "swipe";
|
||||||
|
type KeyMacroArgs = any[];
|
||||||
|
|
||||||
|
type KeyMacroList = Array<{
|
||||||
|
type: KeyMacroType;
|
||||||
|
args: KeyMacroArgs;
|
||||||
|
}> | null;
|
||||||
|
interface KeyMacro extends Key {
|
||||||
|
key: string;
|
||||||
|
macro: {
|
||||||
|
down: KeyMacroList;
|
||||||
|
loop: KeyMacroList;
|
||||||
|
up: KeyMacroList;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type KeyMapping =
|
||||||
|
| KeySteeringWheel
|
||||||
|
| KeyDirectionalSkill
|
||||||
|
| KeyDirectionlessSkill
|
||||||
|
| KeyTriggerWhenPressedSkill
|
||||||
|
| KeyObservation
|
||||||
|
| KeyMacro
|
||||||
|
| KeyCancelSkill
|
||||||
|
| KeyTap;
|
||||||
|
|
||||||
|
interface KeyMappingConfig {
|
||||||
|
relativeSize: { w: number; h: number };
|
||||||
|
title: string;
|
||||||
|
list: KeyMapping[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type {
|
||||||
|
KeyMacroList,
|
||||||
|
KeySteeringWheel,
|
||||||
|
KeyDirectionalSkill,
|
||||||
|
KeyDirectionlessSkill,
|
||||||
|
KeyCancelSkill,
|
||||||
|
KeyTap,
|
||||||
|
KeyTriggerWhenPressedSkill,
|
||||||
|
KeyObservation,
|
||||||
|
KeyMacro,
|
||||||
|
KeyMapping,
|
||||||
|
KeyMappingConfig,
|
||||||
|
};
|
@ -1,8 +1,16 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { Ref, ref } from "vue";
|
import { Ref, ref } from "vue";
|
||||||
import { Device } from "../invoke";
|
import { Device } from "../invoke";
|
||||||
|
import {
|
||||||
|
KeyMapping,
|
||||||
|
KeyMappingConfig,
|
||||||
|
KeySteeringWheel,
|
||||||
|
} from "../keyMappingConfig";
|
||||||
|
import { Store } from "@tauri-apps/plugin-store";
|
||||||
|
|
||||||
export const useGlobalStore = defineStore("counter", () => {
|
const localStore = new Store("store.bin");
|
||||||
|
|
||||||
|
export const useGlobalStore = defineStore("global", () => {
|
||||||
const showLoadingRef = ref(false);
|
const showLoadingRef = ref(false);
|
||||||
function showLoading() {
|
function showLoading() {
|
||||||
showLoadingRef.value = true;
|
showLoadingRef.value = true;
|
||||||
@ -15,15 +23,67 @@ 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 screenSizeW: Ref<number> = ref(0);
|
||||||
|
const screenSizeH: Ref<number> = ref(0);
|
||||||
|
|
||||||
|
const controledDevice: Ref<ControledDevice | null> = ref(null);
|
||||||
|
|
||||||
|
const keyMappingConfigList: Ref<KeyMappingConfig[]> = ref([]);
|
||||||
|
const curKeyMappingIndex = ref(0);
|
||||||
|
const editKeyMappingList: Ref<KeyMapping[]> = ref([]);
|
||||||
|
|
||||||
|
function applyEditKeyMappingList(): boolean {
|
||||||
|
const set = new Set<string>();
|
||||||
|
for (const keyMapping of editKeyMappingList.value) {
|
||||||
|
if (keyMapping.type === "SteeringWheel") {
|
||||||
|
const nameList: ["up", "down", "left", "right"] = [
|
||||||
|
"up",
|
||||||
|
"down",
|
||||||
|
"left",
|
||||||
|
"right",
|
||||||
|
];
|
||||||
|
for (const name of nameList) {
|
||||||
|
if (set.has((keyMapping as KeySteeringWheel).key[name])) return false;
|
||||||
|
set.add((keyMapping as KeySteeringWheel).key[name]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (set.has(keyMapping.key as string)) return false;
|
||||||
|
set.add(keyMapping.key as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keyMappingConfigList.value[curKeyMappingIndex.value].list =
|
||||||
|
editKeyMappingList.value;
|
||||||
|
localStore.set("keyMappingConfigList", keyMappingConfigList.value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetEditKeyMappingList() {
|
||||||
|
editKeyMappingList.value = JSON.parse(
|
||||||
|
JSON.stringify(keyMappingConfigList.value[curKeyMappingIndex.value].list)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setKeyMappingIndex(index: number) {
|
||||||
|
curKeyMappingIndex.value = index;
|
||||||
|
resetEditKeyMappingList();
|
||||||
|
localStore.set("curKeyMappingIndex", index);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showLoading,
|
showLoading,
|
||||||
hideLoading,
|
hideLoading,
|
||||||
showLoadingRef,
|
showLoadingRef,
|
||||||
controledDevice,
|
controledDevice,
|
||||||
|
screenSizeW,
|
||||||
|
screenSizeH,
|
||||||
|
keyMappingConfigList,
|
||||||
|
curKeyMappingIndex,
|
||||||
|
editKeyMappingList,
|
||||||
|
applyEditKeyMappingList,
|
||||||
|
resetEditKeyMappingList,
|
||||||
|
setKeyMappingIndex,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
22
src/store/keyboard.ts
Normal file
22
src/store/keyboard.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
export const useKeyboardStore = defineStore("keyboard", () => {
|
||||||
|
const showKeyInfoFlag = ref(false);
|
||||||
|
const showSettingFlag = ref(false);
|
||||||
|
const showButtonSettingFlag = ref(false);
|
||||||
|
const showButtonAddFlag = ref(false);
|
||||||
|
const activeButtonIndex = ref(-1);
|
||||||
|
const activeSteeringWheelButtonKeyIndex = ref(-1);
|
||||||
|
const edited = ref(false);
|
||||||
|
|
||||||
|
return {
|
||||||
|
showKeyInfoFlag,
|
||||||
|
showSettingFlag,
|
||||||
|
showButtonSettingFlag,
|
||||||
|
showButtonAddFlag,
|
||||||
|
activeButtonIndex,
|
||||||
|
activeSteeringWheelButtonKeyIndex,
|
||||||
|
edited,
|
||||||
|
};
|
||||||
|
});
|
@ -1,15 +1,17 @@
|
|||||||
:root {
|
:root {
|
||||||
--primary-color: #63E2B7;
|
--primary-color: #63e2b7;
|
||||||
--primary-hover-color: #7FE7C4;
|
--primary-hover-color: #7fe7c4;
|
||||||
--primary-pressed-color: #5ACEA7;
|
--primary-pressed-color: #5acea7;
|
||||||
--bg-color: #101014;
|
|
||||||
--content-bg-color: #18181C;
|
|
||||||
--content-hl-color: #26262A;
|
|
||||||
|
|
||||||
--light-color: rgba(255, 255, 255, 0.9);
|
--bg-color: #101014;
|
||||||
|
--content-bg-color: #18181c;
|
||||||
|
--content-hl-color: #26262a;
|
||||||
|
--light-color: rgba(255, 255, 255, 0.82);
|
||||||
--gray-color: #6b6e76;
|
--gray-color: #6b6e76;
|
||||||
--red-color: #fc5185;
|
--red-color: #fc5185;
|
||||||
--red-pressed-color: #f4336d;
|
--red-pressed-color: #f4336d;
|
||||||
|
--blue-color: #70C0E8;
|
||||||
|
--blue-pressed-color: #66AFD3;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
@ -24,4 +26,3 @@ div#app {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user