mirror of
https://github.com/0PandaDEV/Qopy.git
synced 2025-04-21 13:14:04 +02:00
feat: custom hotkey with global shortcut
This commit is contained in:
parent
02becca60d
commit
ba743f7961
7 changed files with 774 additions and 297 deletions
723
src-tauri/Cargo.lock
generated
723
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,13 +1,13 @@
|
|||
[package]
|
||||
name = "qopy"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
description = "Qopy"
|
||||
authors = ["pandadev"]
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.1", features = [] }
|
||||
tauri-build = { version = "2.0.3", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2.0.1", features = [
|
||||
|
@ -15,29 +15,30 @@ tauri = { version = "2.0.1", features = [
|
|||
"tray-icon",
|
||||
"image-png",
|
||||
] }
|
||||
tauri-plugin-sql = { version = "2.0.1", features = ["sqlite"] }
|
||||
tauri-plugin-sql = { version = "2.0.2", features = ["sqlite"] }
|
||||
tauri-plugin-autostart = "2.0.1"
|
||||
tauri-plugin-os = "2.0.1"
|
||||
tauri-plugin-updater = "2.0.2"
|
||||
tauri-plugin-dialog = "2.0.1"
|
||||
tauri-plugin-fs = "2.0.1"
|
||||
tauri-plugin-clipboard = "2.1.9"
|
||||
tauri-plugin-prevent-default = "0.6.1"
|
||||
tauri-plugin-dialog = "2.0.3"
|
||||
tauri-plugin-fs = "2.0.3"
|
||||
tauri-plugin-clipboard = "2.1.11"
|
||||
tauri-plugin-prevent-default = "0.7.5"
|
||||
tauri-plugin-global-shortcut = "2.0.1"
|
||||
sqlx = { version = "0.8.2", features = ["runtime-tokio-native-tls", "sqlite"] }
|
||||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
tokio = { version = "1.40.0", features = ["full"] }
|
||||
serde_json = "1.0.128"
|
||||
serde = { version = "1.0.215", features = ["derive"] }
|
||||
tokio = { version = "1.41.1", features = ["full"] }
|
||||
serde_json = "1.0.132"
|
||||
rdev = "0.5.3"
|
||||
rand = "0.8"
|
||||
base64 = "0.22.1"
|
||||
image = "0.25.2"
|
||||
reqwest = { version = "0.12.8", features = ["blocking"] }
|
||||
url = "2.5.2"
|
||||
regex = "1.11.0"
|
||||
image = "0.25.5"
|
||||
reqwest = { version = "0.12.9", features = ["blocking"] }
|
||||
url = "2.5.3"
|
||||
regex = "1.11.1"
|
||||
sha2 = "0.10.6"
|
||||
lazy_static = "1.4.0"
|
||||
time = "0.3"
|
||||
global-hotkey = "0.6.3"
|
||||
|
||||
[features]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use rand::distributions::Alphanumeric;
|
||||
use rand::{thread_rng, Rng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use sqlx::sqlite::{SqlitePool, SqlitePoolOptions};
|
||||
use std::fs;
|
||||
use tauri::Manager;
|
||||
use tauri::State;
|
||||
use tauri::{Manager, Emitter};
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
|
@ -118,18 +118,22 @@ pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
|||
|
||||
#[tauri::command]
|
||||
pub async fn save_keybind(
|
||||
app_handle: tauri::AppHandle,
|
||||
keybind: Vec<String>,
|
||||
pool: State<'_, SqlitePool>,
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
) -> Result<(), String> {
|
||||
let json = serde_json::to_string(&keybind).map_err(|e| e.to_string())?;
|
||||
|
||||
sqlx::query(
|
||||
"INSERT OR REPLACE INTO settings (key, value) VALUES ('keybind', ?)"
|
||||
)
|
||||
.bind(json)
|
||||
.execute(&*pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
sqlx::query("INSERT OR REPLACE INTO settings (key, value) VALUES ('keybind', ?)")
|
||||
.bind(json)
|
||||
.execute(&*pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let keybind_str = keybind.join("+");
|
||||
app_handle
|
||||
.emit("update-shortcut", keybind_str)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -138,18 +142,20 @@ pub async fn save_keybind(
|
|||
pub async fn get_keybind(app_handle: tauri::AppHandle) -> Result<Vec<String>, String> {
|
||||
let pool = app_handle.state::<SqlitePool>();
|
||||
|
||||
let result = sqlx::query_scalar::<_, String>(
|
||||
"SELECT value FROM settings WHERE key = 'keybind'"
|
||||
)
|
||||
.fetch_optional(&*pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let result =
|
||||
sqlx::query_scalar::<_, String>("SELECT value FROM settings WHERE key = 'keybind'")
|
||||
.fetch_optional(&*pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
match result {
|
||||
Some(json) => {
|
||||
let setting: KeybindSetting = serde_json::from_str(&json).map_err(|e| e.to_string())?;
|
||||
Ok(setting.keybind)
|
||||
},
|
||||
None => Ok(vec!["Meta".to_string(), "V".to_string()]),
|
||||
let keybind: Vec<String> = serde_json::from_str(&json).map_err(|e| e.to_string())?;
|
||||
Ok(keybind)
|
||||
}
|
||||
None => {
|
||||
let default_keybind = vec!["Meta".to_string(), "V".to_string()];
|
||||
Ok(default_keybind)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,44 +1,104 @@
|
|||
use crate::api::database::get_keybind;
|
||||
use crate::utils::commands::center_window_on_current_monitor;
|
||||
use rdev::{listen, EventType, Key};
|
||||
use tauri::Manager;
|
||||
use global_hotkey::{
|
||||
hotkey::{Code, HotKey, Modifiers},
|
||||
GlobalHotKeyEvent, GlobalHotKeyManager,
|
||||
};
|
||||
use std::str::FromStr;
|
||||
use tauri::{AppHandle, Listener, Manager};
|
||||
|
||||
fn key_to_string(key: &Key) -> String {
|
||||
format!("{:?}", key)
|
||||
pub fn setup(app_handle: tauri::AppHandle) {
|
||||
let app_handle_clone = app_handle.clone();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match get_keybind(app_handle_clone.clone()).await {
|
||||
Ok(keybind) => {
|
||||
if !keybind.is_empty() {
|
||||
let keybind_str = keybind.join("+");
|
||||
println!("Keybind: {:?}", keybind_str);
|
||||
if let Err(e) = register_shortcut(&app_handle_clone, &keybind_str) {
|
||||
eprintln!("Error registering shortcut: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error getting keybind: {:?}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let app_handle_for_listener = app_handle.clone();
|
||||
app_handle.listen("update-shortcut", move |event| {
|
||||
let payload_str = event.payload().to_string();
|
||||
if let Err(e) = register_shortcut(&app_handle_for_listener, &payload_str) {
|
||||
eprintln!("Error re-registering shortcut: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
let app_handle_for_hotkey = app_handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
loop {
|
||||
if let Ok(_) = GlobalHotKeyEvent::receiver().recv() {
|
||||
handle_hotkey_event(&app_handle_for_hotkey);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[warn(dead_code)]
|
||||
pub fn setup(app_handle: tauri::AppHandle) {
|
||||
std::thread::spawn(move || {
|
||||
let keybind = tauri::async_runtime::block_on(async { get_keybind(app_handle.clone()).await.unwrap_or_default() });
|
||||
fn register_shortcut(
|
||||
_app_handle: &tauri::AppHandle,
|
||||
shortcut: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let manager = GlobalHotKeyManager::new()?;
|
||||
let hotkey = parse_hotkey(shortcut)?;
|
||||
manager.register(hotkey)?;
|
||||
|
||||
println!("Listening for keybind: {:?}", keybind);
|
||||
println!("Listening for keybind: {}", shortcut);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let mut pressed_keys = vec![false; keybind.len()];
|
||||
fn parse_hotkey(shortcut: &str) -> Result<HotKey, Box<dyn std::error::Error>> {
|
||||
let mut modifiers = Modifiers::empty();
|
||||
let mut code = None;
|
||||
|
||||
listen(move |event| {
|
||||
match event.event_type {
|
||||
EventType::KeyPress(key) => {
|
||||
if let Some(index) = keybind.iter().position(|k| k == &key_to_string(&key)) {
|
||||
pressed_keys[index] = true;
|
||||
}
|
||||
}
|
||||
EventType::KeyRelease(key) => {
|
||||
if let Some(index) = keybind.iter().position(|k| k == &key_to_string(&key)) {
|
||||
pressed_keys[index] = false;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
for part in shortcut.split('+') {
|
||||
let part = part;
|
||||
if part.to_lowercase().starts_with("ctrl") || part.to_lowercase().starts_with("control") {
|
||||
modifiers |= Modifiers::CONTROL;
|
||||
} else if part.to_lowercase().starts_with("alt") {
|
||||
modifiers |= Modifiers::ALT;
|
||||
} else if part.to_lowercase().starts_with("shift") {
|
||||
modifiers |= Modifiers::SHIFT;
|
||||
} else if part.to_lowercase().starts_with("super") || part.to_lowercase().starts_with("meta") || part.to_lowercase().starts_with("cmd") {
|
||||
|
||||
modifiers |= Modifiers::META;
|
||||
} else {
|
||||
let pascal_case_key = part
|
||||
.split(|c: char| !c.is_alphanumeric())
|
||||
.map(|word| {
|
||||
let mut chars = word.chars();
|
||||
let first_char = chars.next().unwrap().to_uppercase().collect::<String>();
|
||||
let rest = chars.as_str();
|
||||
first_char + rest
|
||||
})
|
||||
.collect::<String>();
|
||||
code = Some(
|
||||
Code::from_str(&pascal_case_key)
|
||||
.map_err(|_| format!("Invalid key: {}", pascal_case_key))?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if pressed_keys.iter().all(|&k| k) {
|
||||
pressed_keys.iter_mut().for_each(|k| *k = false);
|
||||
let window = app_handle.get_webview_window("main").unwrap();
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
center_window_on_current_monitor(&window);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
Ok(HotKey::new(Some(modifiers), code.unwrap()))
|
||||
}
|
||||
|
||||
fn handle_hotkey_event(app_handle: &AppHandle) {
|
||||
let window = app_handle.get_webview_window("main").unwrap();
|
||||
if window.is_visible().unwrap() {
|
||||
window.hide().unwrap();
|
||||
} else {
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
center_window_on_current_monitor(&window);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"productName": "Qopy",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"identifier": "net.pandadev.qopy",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue