mirror of
https://github.com/0PandaDEV/Qopy.git
synced 2025-04-22 05:34:04 +02:00
restructure
This commit is contained in:
parent
96f9f475df
commit
266b6ff3e1
12 changed files with 158 additions and 103 deletions
236
src-tauri/src/api/clipboard.rs
Normal file
236
src-tauri/src/api/clipboard.rs
Normal file
|
@ -0,0 +1,236 @@
|
|||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use tauri::{AppHandle, Manager, Emitter, Listener};
|
||||
use tauri_plugin_clipboard::Clipboard;
|
||||
use tokio::runtime::Runtime as TokioRuntime;
|
||||
use regex::Regex;
|
||||
use sqlx::SqlitePool;
|
||||
use std::{
|
||||
fs,
|
||||
sync::Mutex,
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
use rand::Rng;
|
||||
use sha2::{Sha256, Digest};
|
||||
use rdev::{simulate, Key, EventType};
|
||||
use lazy_static::lazy_static;
|
||||
use image::ImageFormat;
|
||||
use tauri::plugin::TauriPlugin;
|
||||
|
||||
pub fn init() -> TauriPlugin<tauri::Wry> {
|
||||
tauri::plugin::Builder::new("clipboard")
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
read_image,
|
||||
simulate_paste,
|
||||
get_image_path
|
||||
])
|
||||
.setup(|app_handle, _api| {
|
||||
setup(app_handle.clone());
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn read_image(filename: String) -> Result<Vec<u8>, String> {
|
||||
let app_data_dir = APP_DATA_DIR.lock().unwrap();
|
||||
let app_data_dir = app_data_dir.as_ref().expect("App data directory not set");
|
||||
let image_path = app_data_dir.join("images").join(filename);
|
||||
fs::read(image_path).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn simulate_paste() {
|
||||
let mut events = vec![
|
||||
EventType::KeyPress(Key::ControlLeft),
|
||||
EventType::KeyPress(Key::KeyV),
|
||||
EventType::KeyRelease(Key::KeyV),
|
||||
EventType::KeyRelease(Key::ControlLeft),
|
||||
];
|
||||
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
|
||||
for event in events.drain(..) {
|
||||
simulate(&event).unwrap();
|
||||
thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_image_path(app_handle: tauri::AppHandle, filename: String) -> String {
|
||||
let app_data_dir = app_handle.path().app_data_dir().expect("Failed to get app data directory");
|
||||
let image_path = app_data_dir.join("images").join(filename);
|
||||
image_path.to_str().unwrap_or("").to_string()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn start_monitor(app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
let clipboard = app_handle.state::<Clipboard>();
|
||||
clipboard.start_monitor(app_handle.clone()).map_err(|e| e.to_string())?;
|
||||
app_handle.emit("plugin:clipboard://clipboard-monitor/status", true).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup<R: tauri::Runtime>(app: AppHandle<R>) {
|
||||
let app = app.clone();
|
||||
let runtime = TokioRuntime::new().expect("Failed to create Tokio runtime");
|
||||
|
||||
app.clone().listen("plugin:clipboard://clipboard-monitor/update", move |_event| {
|
||||
let app = app.clone();
|
||||
runtime.block_on(async move {
|
||||
let clipboard = app.state::<Clipboard>();
|
||||
let available_types = clipboard.available_types().unwrap();
|
||||
|
||||
println!("Clipboard update detected");
|
||||
|
||||
match get_pool(&app).await {
|
||||
Ok(pool) => {
|
||||
if available_types.image {
|
||||
println!("Handling image change");
|
||||
if let Ok(image_data) = clipboard.read_image_base64() {
|
||||
let base64_image = STANDARD.encode(&image_data);
|
||||
insert_content_if_not_exists(app.clone(), pool.clone(), "image", base64_image).await;
|
||||
}
|
||||
let _ = app.emit("plugin:clipboard://image-changed", ());
|
||||
} else if available_types.rtf {
|
||||
println!("Handling RTF change");
|
||||
if let Ok(rtf) = clipboard.read_rtf() {
|
||||
insert_content_if_not_exists(app.clone(), pool.clone(), "rtf", rtf).await;
|
||||
}
|
||||
let _ = app.emit("plugin:clipboard://rtf-changed", ());
|
||||
} else if available_types.files {
|
||||
println!("Handling files change");
|
||||
if let Ok(files) = clipboard.read_files() {
|
||||
let files_str = files.join(", ");
|
||||
insert_content_if_not_exists(app.clone(), pool.clone(), "files", files_str).await;
|
||||
}
|
||||
let _ = app.emit("plugin:clipboard://files-changed", ());
|
||||
} else if available_types.text {
|
||||
println!("Handling text change");
|
||||
if let Ok(text) = clipboard.read_text() {
|
||||
insert_content_if_not_exists(app.clone(), pool.clone(), "text", text).await;
|
||||
}
|
||||
let _ = app.emit("plugin:clipboard://text-changed", ());
|
||||
} else {
|
||||
println!("Unknown clipboard content type");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to get database pool: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async fn get_pool<R: tauri::Runtime>(app_handle: &tauri::AppHandle<R>) -> Result<SqlitePool, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let app_data_dir = app_handle.path().app_data_dir().expect("Failed to get app data directory");
|
||||
let db_path = app_data_dir.join("data.db");
|
||||
let database_url = format!("sqlite:{}", db_path.to_str().unwrap());
|
||||
SqlitePool::connect(&database_url).await.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
|
||||
}
|
||||
|
||||
async fn insert_content_if_not_exists<R: tauri::Runtime>(app_handle: tauri::AppHandle<R>, pool: SqlitePool, content_type: &str, content: String) {
|
||||
let last_content: Option<String> = sqlx::query_scalar(
|
||||
"SELECT content FROM history WHERE content_type = ? ORDER BY timestamp DESC LIMIT 1",
|
||||
)
|
||||
.bind(content_type)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
|
||||
let content = if content_type == "image" {
|
||||
match save_image(&app_handle, &content).await {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
println!("Failed to save image: {}", e);
|
||||
content
|
||||
}
|
||||
}
|
||||
} else {
|
||||
content
|
||||
};
|
||||
|
||||
if last_content.as_deref() != Some(&content) {
|
||||
let id: String = rand::thread_rng()
|
||||
.sample_iter(&rand::distributions::Alphanumeric)
|
||||
.take(16)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
let url_regex = Regex::new(r"^https?://(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*)$").unwrap();
|
||||
let favicon_base64 = if content_type == "text" && url_regex.is_match(&content) {
|
||||
match url::Url::parse(&content) {
|
||||
Ok(url) => match fetch_favicon_as_base64(url).await {
|
||||
Ok(Some(favicon)) => Some(favicon),
|
||||
Ok(None) => None,
|
||||
Err(e) => {
|
||||
println!("Failed to fetch favicon: {}", e);
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Failed to parse URL: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let _ = sqlx::query(
|
||||
"INSERT INTO history (id, content_type, content, favicon) VALUES (?, ?, ?, ?)",
|
||||
)
|
||||
.bind(id)
|
||||
.bind(content_type)
|
||||
.bind(&content)
|
||||
.bind(favicon_base64)
|
||||
.execute(&pool)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn save_image<R: tauri::Runtime>(app_handle: &tauri::AppHandle<R>, base64_image: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let image_data = STANDARD.decode(base64_image)?;
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&image_data);
|
||||
let hash = hasher.finalize();
|
||||
let filename = format!("{:x}.png", hash);
|
||||
|
||||
let app_data_dir = app_handle.path().app_data_dir().expect("Failed to get app data directory");
|
||||
let images_dir = app_data_dir.join("images");
|
||||
let path = images_dir.join(&filename);
|
||||
|
||||
if !path.exists() {
|
||||
fs::create_dir_all(&images_dir)?;
|
||||
fs::write(&path, &image_data)?;
|
||||
}
|
||||
|
||||
Ok(path.to_str().unwrap().to_string())
|
||||
}
|
||||
|
||||
async fn fetch_favicon_as_base64(url: url::Url) -> Result<Option<String>, Box<dyn std::error::Error>> {
|
||||
let client = reqwest::Client::new();
|
||||
let favicon_url = format!("https://favicone.com/{}", url.host_str().unwrap());
|
||||
let response = client.get(&favicon_url).send().await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let bytes = response.bytes().await?;
|
||||
let img = image::load_from_memory(&bytes)?;
|
||||
let mut png_bytes: Vec<u8> = Vec::new();
|
||||
img.write_to(&mut std::io::Cursor::new(&mut png_bytes), ImageFormat::Png)?;
|
||||
Ok(Some(STANDARD.encode(&png_bytes)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref APP_DATA_DIR: Mutex<Option<std::path::PathBuf>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
pub fn set_app_data_dir(path: std::path::PathBuf) {
|
||||
let mut dir = APP_DATA_DIR.lock().unwrap();
|
||||
*dir = Some(path);
|
||||
}
|
82
src-tauri/src/api/database.rs
Normal file
82
src-tauri/src/api/database.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
use tauri::plugin::TauriPlugin;
|
||||
use tauri::{AppHandle, Manager, Emitter};
|
||||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
use std::fs;
|
||||
use tokio::runtime::Runtime;
|
||||
use rand::{thread_rng, Rng};
|
||||
use rand::distributions::Alphanumeric;
|
||||
|
||||
pub fn init() -> TauriPlugin<tauri::Wry> {
|
||||
tauri::plugin::Builder::new("database")
|
||||
.setup(|app_handle: &AppHandle, _api| {
|
||||
setup(app_handle)?;
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
fn setup(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let rt = Runtime::new().expect("Failed to create Tokio runtime");
|
||||
|
||||
let app_data_dir = app.path().app_data_dir().unwrap();
|
||||
fs::create_dir_all(&app_data_dir).expect("Failed to create app data directory");
|
||||
|
||||
let db_path = app_data_dir.join("data.db");
|
||||
let is_new_db = !db_path.exists();
|
||||
if is_new_db {
|
||||
fs::File::create(&db_path).expect("Failed to create database file");
|
||||
}
|
||||
|
||||
let db_url = format!("sqlite:{}", db_path.to_str().unwrap());
|
||||
let pool = rt.block_on(async {
|
||||
SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&db_url)
|
||||
.await
|
||||
.expect("Failed to create pool")
|
||||
});
|
||||
|
||||
rt.block_on(async {
|
||||
sqlx::query(
|
||||
"CREATE TABLE IF NOT EXISTS history (
|
||||
id TEXT PRIMARY KEY,
|
||||
content_type TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
favicon TEXT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)"
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Failed to create table");
|
||||
|
||||
sqlx::query(
|
||||
"CREATE INDEX IF NOT EXISTS idx_timestamp ON history (timestamp)"
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Failed to create index");
|
||||
|
||||
if is_new_db {
|
||||
let id: String = thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(16)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
sqlx::query("INSERT INTO history (id, content_type, content, timestamp) VALUES (?, ?, ?, CURRENT_TIMESTAMP)")
|
||||
.bind(id)
|
||||
.bind("text")
|
||||
.bind("Welcome to your clipboard history!")
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Failed to insert welcome message");
|
||||
}
|
||||
});
|
||||
|
||||
app.manage(pool);
|
||||
app.manage(rt);
|
||||
|
||||
app.emit("database_initialized", ()).expect("Failed to emit database_initialized event");
|
||||
|
||||
Ok(())
|
||||
}
|
40
src-tauri/src/api/hotkeys.rs
Normal file
40
src-tauri/src/api/hotkeys.rs
Normal file
|
@ -0,0 +1,40 @@
|
|||
use tauri::plugin::TauriPlugin;
|
||||
use tauri::Manager;
|
||||
use rdev::{listen, EventType, Key};
|
||||
use crate::utils::commands;
|
||||
|
||||
pub fn init() -> TauriPlugin<tauri::Wry> {
|
||||
tauri::plugin::Builder::new("hotkeys")
|
||||
.setup(|app, _| {
|
||||
setup(app.app_handle().clone());
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
fn setup(app_handle: tauri::AppHandle) {
|
||||
std::thread::spawn(move || {
|
||||
let mut meta_pressed = false;
|
||||
listen(move |event| {
|
||||
match event.event_type {
|
||||
EventType::KeyPress(Key::MetaLeft) | EventType::KeyPress(Key::MetaRight) => {
|
||||
meta_pressed = true;
|
||||
}
|
||||
EventType::KeyRelease(Key::MetaLeft) | EventType::KeyRelease(Key::MetaRight) => {
|
||||
meta_pressed = false;
|
||||
}
|
||||
EventType::KeyPress(Key::KeyV) => {
|
||||
if meta_pressed {
|
||||
meta_pressed = false;
|
||||
let window = app_handle.get_webview_window("main").unwrap();
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
commands::center_window_on_current_monitor(&window);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
}
|
5
src-tauri/src/api/mod.rs
Normal file
5
src-tauri/src/api/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub mod updater;
|
||||
pub mod clipboard;
|
||||
pub mod database;
|
||||
pub mod tray;
|
||||
pub mod hotkeys;
|
62
src-tauri/src/api/tray.rs
Normal file
62
src-tauri/src/api/tray.rs
Normal file
|
@ -0,0 +1,62 @@
|
|||
use tauri::AppHandle;
|
||||
use tauri::plugin::TauriPlugin;
|
||||
use tauri::Manager;
|
||||
use tauri::menu::{MenuBuilder, MenuItemBuilder};
|
||||
use tauri::tray::{MouseButton, TrayIconBuilder, TrayIconEvent};
|
||||
|
||||
pub fn init() -> TauriPlugin<tauri::Wry> {
|
||||
tauri::plugin::Builder::new("tray")
|
||||
.setup(|app, _api| {
|
||||
setup(app)?;
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
fn setup(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
let window_clone_for_tray = window.clone();
|
||||
let window_clone_for_click = window.clone();
|
||||
|
||||
let icon_bytes = include_bytes!("../../icons/Square71x71Logo.png");
|
||||
let icon = tauri::image::Image::from_bytes(icon_bytes).unwrap();
|
||||
|
||||
let _tray = TrayIconBuilder::new()
|
||||
.menu(
|
||||
&MenuBuilder::new(app)
|
||||
.items(&[&MenuItemBuilder::with_id("show", "Show/Hide").build(app)?])
|
||||
.items(&[&MenuItemBuilder::with_id("quit", "Quit").build(app)?])
|
||||
.build()?,
|
||||
)
|
||||
.on_menu_event(move |_app, event| match event.id().as_ref() {
|
||||
"quit" => {
|
||||
std::process::exit(0);
|
||||
}
|
||||
"show" => {
|
||||
let is_visible = window_clone_for_tray.is_visible().unwrap();
|
||||
if is_visible {
|
||||
window_clone_for_tray.hide().unwrap();
|
||||
} else {
|
||||
window_clone_for_tray.show().unwrap();
|
||||
window_clone_for_tray.set_focus().unwrap();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
})
|
||||
.on_tray_icon_event(move |_tray, event| {
|
||||
if let TrayIconEvent::Click { button, .. } = event {
|
||||
if button == MouseButton::Left {
|
||||
let is_visible = window_clone_for_click.is_visible().unwrap();
|
||||
if is_visible {
|
||||
window_clone_for_click.hide().unwrap();
|
||||
} else {
|
||||
window_clone_for_click.show().unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.icon(icon)
|
||||
.build(app)?;
|
||||
|
||||
Ok(())
|
||||
}
|
54
src-tauri/src/api/updater.rs
Normal file
54
src-tauri/src/api/updater.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
use tauri::plugin::TauriPlugin;
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
use tokio;
|
||||
|
||||
pub fn init() -> TauriPlugin<tauri::Wry> {
|
||||
tauri::plugin::Builder::new("updater")
|
||||
.invoke_handler(tauri::generate_handler![check_for_updates])
|
||||
.build()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_for_updates(app: AppHandle) {
|
||||
println!("Checking for updates...");
|
||||
|
||||
let updater = app.updater().unwrap();
|
||||
let response = updater.check().await;
|
||||
|
||||
match response {
|
||||
Ok(Some(update)) => {
|
||||
let cur_ver = &update.current_version;
|
||||
let new_ver = &update.version;
|
||||
let mut msg = String::new();
|
||||
msg.extend([
|
||||
&format!("{cur_ver} -> {new_ver}\n\n"),
|
||||
"Would you like to install it now?",
|
||||
]);
|
||||
|
||||
app.dialog()
|
||||
.message(msg)
|
||||
.title("Update Available")
|
||||
.ok_button_label("Install")
|
||||
.cancel_button_label("Cancel")
|
||||
.show(move |response| {
|
||||
if !response {
|
||||
return;
|
||||
}
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = update.download_and_install(|_, _| {}, || {}).await {
|
||||
println!("Error installing new update: {:?}", e);
|
||||
app.dialog().message(
|
||||
"Failed to install new update. The new update can be downloaded from Github"
|
||||
).kind(MessageDialogKind::Error).show(|_| {});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
Ok(None) => println!("No updates available."),
|
||||
Err(e) => {
|
||||
println!("Failed to check for updates: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue