mirror of
https://github.com/0PandaDEV/Qopy.git
synced 2025-06-16 11:57:38 +02:00
chore: system
This commit is contained in:
parent
aa928f7094
commit
97c023df91
78 changed files with 15225 additions and 15225 deletions
8
src-tauri/.gitignore
vendored
8
src-tauri/.gitignore
vendored
|
@ -1,4 +1,4 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
/gen/schemas
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
/gen/schemas
|
||||
|
|
15618
src-tauri/Cargo.lock
generated
15618
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,3 +1,3 @@
|
|||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
|
|
|
@ -1,34 +1,34 @@
|
|||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "enables the default permissions",
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:path:default",
|
||||
"core:event:default",
|
||||
"core:window:default",
|
||||
"core:webview:default",
|
||||
"core:app:default",
|
||||
"core:resources:default",
|
||||
"core:image:default",
|
||||
"core:menu:default",
|
||||
"core:tray:default",
|
||||
"sql:allow-load",
|
||||
"sql:allow-select",
|
||||
"sql:allow-execute",
|
||||
"autostart:allow-enable",
|
||||
"autostart:allow-disable",
|
||||
"autostart:allow-is-enabled",
|
||||
"os:allow-os-type",
|
||||
"core:app:allow-app-hide",
|
||||
"core:app:allow-app-show",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-is-focused",
|
||||
"core:window:allow-is-visible",
|
||||
"fs:allow-read"
|
||||
]
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "enables the default permissions",
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:path:default",
|
||||
"core:event:default",
|
||||
"core:window:default",
|
||||
"core:webview:default",
|
||||
"core:app:default",
|
||||
"core:resources:default",
|
||||
"core:image:default",
|
||||
"core:menu:default",
|
||||
"core:tray:default",
|
||||
"sql:allow-load",
|
||||
"sql:allow-select",
|
||||
"sql:allow-execute",
|
||||
"autostart:allow-enable",
|
||||
"autostart:allow-disable",
|
||||
"autostart:allow-is-enabled",
|
||||
"os:allow-os-type",
|
||||
"core:app:allow-app-hide",
|
||||
"core:app:allow-app-show",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-is-focused",
|
||||
"core:window:allow-is-visible",
|
||||
"fs:allow-read"
|
||||
]
|
||||
}
|
|
@ -1,279 +1,279 @@
|
|||
use tauri_plugin_aptabase::EventTracker;
|
||||
use base64::{ engine::general_purpose::STANDARD, Engine };
|
||||
// use hyperpolyglot;
|
||||
use lazy_static::lazy_static;
|
||||
use rdev::{ simulate, EventType, Key };
|
||||
use regex::Regex;
|
||||
use sqlx::SqlitePool;
|
||||
use std::fs;
|
||||
use std::sync::atomic::{ AtomicBool, Ordering };
|
||||
use std::{ thread, time::Duration };
|
||||
use tauri::{ AppHandle, Emitter, Listener, Manager };
|
||||
use tauri_plugin_clipboard::Clipboard;
|
||||
use tokio::runtime::Runtime as TokioRuntime;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::db;
|
||||
use crate::utils::commands::get_app_info;
|
||||
use crate::utils::favicon::fetch_favicon_as_base64;
|
||||
use crate::utils::types::{ ContentType, HistoryItem };
|
||||
|
||||
lazy_static! {
|
||||
static ref IS_PROGRAMMATIC_PASTE: AtomicBool = AtomicBool::new(false);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn write_and_paste(
|
||||
app_handle: AppHandle,
|
||||
content: String,
|
||||
content_type: String
|
||||
) -> Result<(), String> {
|
||||
let clipboard = app_handle.state::<Clipboard>();
|
||||
|
||||
match content_type.as_str() {
|
||||
"text" => clipboard.write_text(content).map_err(|e| e.to_string())?,
|
||||
"link" => clipboard.write_text(content).map_err(|e| e.to_string())?,
|
||||
"color" => clipboard.write_text(content).map_err(|e| e.to_string())?,
|
||||
"image" => {
|
||||
clipboard.write_image_base64(content).map_err(|e| e.to_string())?;
|
||||
}
|
||||
"files" => {
|
||||
clipboard
|
||||
.write_files_uris(
|
||||
content
|
||||
.split(", ")
|
||||
.map(|file| file.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
_ => {
|
||||
return Err("Unsupported content type".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
IS_PROGRAMMATIC_PASTE.store(true, Ordering::SeqCst);
|
||||
|
||||
thread::spawn(|| {
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let modifier_key = Key::MetaLeft;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let modifier_key = Key::ControlLeft;
|
||||
|
||||
let events = vec![
|
||||
EventType::KeyPress(modifier_key),
|
||||
EventType::KeyPress(Key::KeyV),
|
||||
EventType::KeyRelease(Key::KeyV),
|
||||
EventType::KeyRelease(modifier_key)
|
||||
];
|
||||
|
||||
for event in events {
|
||||
if let Err(e) = simulate(&event) {
|
||||
println!("Simulation error: {:?}", e);
|
||||
}
|
||||
thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
});
|
||||
|
||||
tokio::spawn(async {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
IS_PROGRAMMATIC_PASTE.store(false, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
let _ = app_handle.track_event(
|
||||
"clipboard_paste",
|
||||
Some(serde_json::json!({
|
||||
"content_type": content_type
|
||||
}))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn setup(app: &AppHandle) {
|
||||
let app_handle = app.clone();
|
||||
let runtime = TokioRuntime::new().expect("Failed to create Tokio runtime");
|
||||
|
||||
app_handle.clone().listen("plugin:clipboard://clipboard-monitor/update", move |_event| {
|
||||
let app_handle = app_handle.clone();
|
||||
runtime.block_on(async move {
|
||||
if IS_PROGRAMMATIC_PASTE.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
let clipboard = app_handle.state::<Clipboard>();
|
||||
let available_types = clipboard.available_types().unwrap();
|
||||
|
||||
let (app_name, app_icon) = get_app_info();
|
||||
|
||||
match get_pool(&app_handle).await {
|
||||
Ok(pool) => {
|
||||
if available_types.image {
|
||||
println!("Handling image change");
|
||||
if let Ok(image_data) = clipboard.read_image_base64() {
|
||||
let file_path = save_image_to_file(&app_handle, &image_data).await
|
||||
.map_err(|e| e.to_string())
|
||||
.unwrap_or_else(|e| e);
|
||||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool,
|
||||
HistoryItem::new(
|
||||
app_name,
|
||||
ContentType::Image,
|
||||
file_path,
|
||||
None,
|
||||
app_icon,
|
||||
None
|
||||
)
|
||||
).await;
|
||||
}
|
||||
} else if available_types.files {
|
||||
println!("Handling files change");
|
||||
if let Ok(files) = clipboard.read_files() {
|
||||
for file in files {
|
||||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool.clone(),
|
||||
HistoryItem::new(
|
||||
app_name.clone(),
|
||||
ContentType::File,
|
||||
file,
|
||||
None,
|
||||
app_icon.clone(),
|
||||
None
|
||||
)
|
||||
).await;
|
||||
}
|
||||
}
|
||||
} else if available_types.text {
|
||||
println!("Handling text change");
|
||||
if let Ok(text) = clipboard.read_text() {
|
||||
let text = text.to_string();
|
||||
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();
|
||||
|
||||
if url_regex.is_match(&text) {
|
||||
if let Ok(url) = Url::parse(&text) {
|
||||
let favicon = match fetch_favicon_as_base64(url).await {
|
||||
Ok(Some(f)) => Some(f),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool,
|
||||
HistoryItem::new(
|
||||
app_name,
|
||||
ContentType::Link,
|
||||
text,
|
||||
favicon,
|
||||
app_icon,
|
||||
None
|
||||
)
|
||||
).await;
|
||||
}
|
||||
} else {
|
||||
if text.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Temporarily disabled code detection
|
||||
/*if let Some(detection) = hyperpolyglot::detect_from_text(&text) {
|
||||
let language = match detection {
|
||||
hyperpolyglot::Detection::Heuristics(lang) => lang.to_string(),
|
||||
_ => detection.language().to_string(),
|
||||
};
|
||||
|
||||
let _ = db::history::add_history_item(
|
||||
pool,
|
||||
HistoryItem::new(app_name, ContentType::Code, text, None, app_icon, Some(language))
|
||||
).await;
|
||||
} else*/ if crate::utils::commands::detect_color(&text) {
|
||||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool,
|
||||
HistoryItem::new(
|
||||
app_name,
|
||||
ContentType::Color,
|
||||
text,
|
||||
None,
|
||||
app_icon,
|
||||
None
|
||||
)
|
||||
).await;
|
||||
} else {
|
||||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool,
|
||||
HistoryItem::new(
|
||||
app_name,
|
||||
ContentType::Text,
|
||||
text.clone(),
|
||||
None,
|
||||
app_icon,
|
||||
None
|
||||
)
|
||||
).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Unknown clipboard content type");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to get database pool: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = app_handle.track_event(
|
||||
"clipboard_copied",
|
||||
Some(
|
||||
serde_json::json!({
|
||||
"content_type": if available_types.image { "image" }
|
||||
else if available_types.files { "files" }
|
||||
else if available_types.text { "text" }
|
||||
else { "unknown" }
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async fn get_pool(
|
||||
app_handle: &AppHandle
|
||||
) -> Result<tauri::State<'_, SqlitePool>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
Ok(app_handle.state::<SqlitePool>())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn start_monitor(app_handle: 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(())
|
||||
}
|
||||
|
||||
async fn save_image_to_file(
|
||||
app_handle: &AppHandle,
|
||||
base64_data: &str
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let app_data_dir = app_handle.path().app_data_dir().unwrap();
|
||||
let images_dir = app_data_dir.join("images");
|
||||
fs::create_dir_all(&images_dir)?;
|
||||
|
||||
let file_name = format!("{}.png", Uuid::new_v4());
|
||||
let file_path = images_dir.join(&file_name);
|
||||
|
||||
let bytes = STANDARD.decode(base64_data)?;
|
||||
fs::write(&file_path, bytes)?;
|
||||
|
||||
Ok(file_path.to_string_lossy().into_owned())
|
||||
}
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
use base64::{ engine::general_purpose::STANDARD, Engine };
|
||||
// use hyperpolyglot;
|
||||
use lazy_static::lazy_static;
|
||||
use rdev::{ simulate, EventType, Key };
|
||||
use regex::Regex;
|
||||
use sqlx::SqlitePool;
|
||||
use std::fs;
|
||||
use std::sync::atomic::{ AtomicBool, Ordering };
|
||||
use std::{ thread, time::Duration };
|
||||
use tauri::{ AppHandle, Emitter, Listener, Manager };
|
||||
use tauri_plugin_clipboard::Clipboard;
|
||||
use tokio::runtime::Runtime as TokioRuntime;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::db;
|
||||
use crate::utils::commands::get_app_info;
|
||||
use crate::utils::favicon::fetch_favicon_as_base64;
|
||||
use crate::utils::types::{ ContentType, HistoryItem };
|
||||
|
||||
lazy_static! {
|
||||
static ref IS_PROGRAMMATIC_PASTE: AtomicBool = AtomicBool::new(false);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn write_and_paste(
|
||||
app_handle: AppHandle,
|
||||
content: String,
|
||||
content_type: String
|
||||
) -> Result<(), String> {
|
||||
let clipboard = app_handle.state::<Clipboard>();
|
||||
|
||||
match content_type.as_str() {
|
||||
"text" => clipboard.write_text(content).map_err(|e| e.to_string())?,
|
||||
"link" => clipboard.write_text(content).map_err(|e| e.to_string())?,
|
||||
"color" => clipboard.write_text(content).map_err(|e| e.to_string())?,
|
||||
"image" => {
|
||||
clipboard.write_image_base64(content).map_err(|e| e.to_string())?;
|
||||
}
|
||||
"files" => {
|
||||
clipboard
|
||||
.write_files_uris(
|
||||
content
|
||||
.split(", ")
|
||||
.map(|file| file.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
_ => {
|
||||
return Err("Unsupported content type".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
IS_PROGRAMMATIC_PASTE.store(true, Ordering::SeqCst);
|
||||
|
||||
thread::spawn(|| {
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let modifier_key = Key::MetaLeft;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let modifier_key = Key::ControlLeft;
|
||||
|
||||
let events = vec![
|
||||
EventType::KeyPress(modifier_key),
|
||||
EventType::KeyPress(Key::KeyV),
|
||||
EventType::KeyRelease(Key::KeyV),
|
||||
EventType::KeyRelease(modifier_key)
|
||||
];
|
||||
|
||||
for event in events {
|
||||
if let Err(e) = simulate(&event) {
|
||||
println!("Simulation error: {:?}", e);
|
||||
}
|
||||
thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
});
|
||||
|
||||
tokio::spawn(async {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
IS_PROGRAMMATIC_PASTE.store(false, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
let _ = app_handle.track_event(
|
||||
"clipboard_paste",
|
||||
Some(serde_json::json!({
|
||||
"content_type": content_type
|
||||
}))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn setup(app: &AppHandle) {
|
||||
let app_handle = app.clone();
|
||||
let runtime = TokioRuntime::new().expect("Failed to create Tokio runtime");
|
||||
|
||||
app_handle.clone().listen("plugin:clipboard://clipboard-monitor/update", move |_event| {
|
||||
let app_handle = app_handle.clone();
|
||||
runtime.block_on(async move {
|
||||
if IS_PROGRAMMATIC_PASTE.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
let clipboard = app_handle.state::<Clipboard>();
|
||||
let available_types = clipboard.available_types().unwrap();
|
||||
|
||||
let (app_name, app_icon) = get_app_info();
|
||||
|
||||
match get_pool(&app_handle).await {
|
||||
Ok(pool) => {
|
||||
if available_types.image {
|
||||
println!("Handling image change");
|
||||
if let Ok(image_data) = clipboard.read_image_base64() {
|
||||
let file_path = save_image_to_file(&app_handle, &image_data).await
|
||||
.map_err(|e| e.to_string())
|
||||
.unwrap_or_else(|e| e);
|
||||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool,
|
||||
HistoryItem::new(
|
||||
app_name,
|
||||
ContentType::Image,
|
||||
file_path,
|
||||
None,
|
||||
app_icon,
|
||||
None
|
||||
)
|
||||
).await;
|
||||
}
|
||||
} else if available_types.files {
|
||||
println!("Handling files change");
|
||||
if let Ok(files) = clipboard.read_files() {
|
||||
for file in files {
|
||||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool.clone(),
|
||||
HistoryItem::new(
|
||||
app_name.clone(),
|
||||
ContentType::File,
|
||||
file,
|
||||
None,
|
||||
app_icon.clone(),
|
||||
None
|
||||
)
|
||||
).await;
|
||||
}
|
||||
}
|
||||
} else if available_types.text {
|
||||
println!("Handling text change");
|
||||
if let Ok(text) = clipboard.read_text() {
|
||||
let text = text.to_string();
|
||||
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();
|
||||
|
||||
if url_regex.is_match(&text) {
|
||||
if let Ok(url) = Url::parse(&text) {
|
||||
let favicon = match fetch_favicon_as_base64(url).await {
|
||||
Ok(Some(f)) => Some(f),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool,
|
||||
HistoryItem::new(
|
||||
app_name,
|
||||
ContentType::Link,
|
||||
text,
|
||||
favicon,
|
||||
app_icon,
|
||||
None
|
||||
)
|
||||
).await;
|
||||
}
|
||||
} else {
|
||||
if text.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Temporarily disabled code detection
|
||||
/*if let Some(detection) = hyperpolyglot::detect_from_text(&text) {
|
||||
let language = match detection {
|
||||
hyperpolyglot::Detection::Heuristics(lang) => lang.to_string(),
|
||||
_ => detection.language().to_string(),
|
||||
};
|
||||
|
||||
let _ = db::history::add_history_item(
|
||||
pool,
|
||||
HistoryItem::new(app_name, ContentType::Code, text, None, app_icon, Some(language))
|
||||
).await;
|
||||
} else*/ if crate::utils::commands::detect_color(&text) {
|
||||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool,
|
||||
HistoryItem::new(
|
||||
app_name,
|
||||
ContentType::Color,
|
||||
text,
|
||||
None,
|
||||
app_icon,
|
||||
None
|
||||
)
|
||||
).await;
|
||||
} else {
|
||||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool,
|
||||
HistoryItem::new(
|
||||
app_name,
|
||||
ContentType::Text,
|
||||
text.clone(),
|
||||
None,
|
||||
app_icon,
|
||||
None
|
||||
)
|
||||
).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Unknown clipboard content type");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to get database pool: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = app_handle.track_event(
|
||||
"clipboard_copied",
|
||||
Some(
|
||||
serde_json::json!({
|
||||
"content_type": if available_types.image { "image" }
|
||||
else if available_types.files { "files" }
|
||||
else if available_types.text { "text" }
|
||||
else { "unknown" }
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async fn get_pool(
|
||||
app_handle: &AppHandle
|
||||
) -> Result<tauri::State<'_, SqlitePool>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
Ok(app_handle.state::<SqlitePool>())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn start_monitor(app_handle: 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(())
|
||||
}
|
||||
|
||||
async fn save_image_to_file(
|
||||
app_handle: &AppHandle,
|
||||
base64_data: &str
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let app_data_dir = app_handle.path().app_data_dir().unwrap();
|
||||
let images_dir = app_data_dir.join("images");
|
||||
fs::create_dir_all(&images_dir)?;
|
||||
|
||||
let file_name = format!("{}.png", Uuid::new_v4());
|
||||
let file_path = images_dir.join(&file_name);
|
||||
|
||||
let bytes = STANDARD.decode(base64_data)?;
|
||||
fs::write(&file_path, bytes)?;
|
||||
|
||||
Ok(file_path.to_string_lossy().into_owned())
|
||||
}
|
||||
|
|
|
@ -1,155 +1,155 @@
|
|||
use crate::utils::commands::center_window_on_current_monitor;
|
||||
use crate::utils::keys::KeyCode;
|
||||
use global_hotkey::{
|
||||
hotkey::{ Code, HotKey, Modifiers },
|
||||
GlobalHotKeyEvent,
|
||||
GlobalHotKeyManager,
|
||||
HotKeyState,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tauri::{ AppHandle, Manager, Listener };
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
|
||||
#[derive(Default)]
|
||||
struct HotkeyState {
|
||||
manager: Option<GlobalHotKeyManager>,
|
||||
registered_hotkey: Option<HotKey>,
|
||||
}
|
||||
|
||||
unsafe impl Send for HotkeyState {}
|
||||
|
||||
pub fn setup(app_handle: tauri::AppHandle) {
|
||||
let state = Arc::new(Mutex::new(HotkeyState::default()));
|
||||
let manager = match GlobalHotKeyManager::new() {
|
||||
Ok(manager) => manager,
|
||||
Err(err) => {
|
||||
eprintln!("Failed to initialize hotkey manager: {:?}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
let mut hotkey_state = state.lock();
|
||||
hotkey_state.manager = Some(manager);
|
||||
}
|
||||
|
||||
let rt = app_handle.state::<tokio::runtime::Runtime>();
|
||||
let initial_keybind = rt
|
||||
.block_on(crate::db::settings::get_keybind(app_handle.clone()))
|
||||
.expect("Failed to get initial keybind");
|
||||
|
||||
if let Err(e) = register_shortcut(&state, &initial_keybind) {
|
||||
eprintln!("Error registering initial shortcut: {:?}", e);
|
||||
}
|
||||
|
||||
let state_clone = Arc::clone(&state);
|
||||
app_handle.listen("update-shortcut", move |event| {
|
||||
let payload_str = event.payload().replace("\\\"", "\"");
|
||||
let trimmed_str = payload_str.trim_matches('"');
|
||||
unregister_current_hotkey(&state_clone);
|
||||
|
||||
let payload: Vec<String> = serde_json::from_str(trimmed_str).unwrap_or_default();
|
||||
if let Err(e) = register_shortcut(&state_clone, &payload) {
|
||||
eprintln!("Error re-registering shortcut: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
let state_clone = Arc::clone(&state);
|
||||
app_handle.listen("save_keybind", move |event| {
|
||||
let payload_str = event.payload().to_string();
|
||||
unregister_current_hotkey(&state_clone);
|
||||
|
||||
let payload: Vec<String> = serde_json::from_str(&payload_str).unwrap_or_default();
|
||||
if let Err(e) = register_shortcut(&state_clone, &payload) {
|
||||
eprintln!("Error registering saved shortcut: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
setup_hotkey_receiver(app_handle);
|
||||
}
|
||||
|
||||
fn setup_hotkey_receiver(app_handle: AppHandle) {
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
match GlobalHotKeyEvent::receiver().recv() {
|
||||
Ok(event) => {
|
||||
if event.state == HotKeyState::Released {
|
||||
continue;
|
||||
}
|
||||
handle_hotkey_event(&app_handle);
|
||||
}
|
||||
Err(e) => eprintln!("Error receiving hotkey event: {:?}", e),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn unregister_current_hotkey(state: &Arc<Mutex<HotkeyState>>) {
|
||||
let mut hotkey_state = state.lock();
|
||||
if let Some(old_hotkey) = hotkey_state.registered_hotkey.take() {
|
||||
if let Some(manager) = &hotkey_state.manager {
|
||||
let _ = manager.unregister(old_hotkey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn register_shortcut(state: &Arc<Mutex<HotkeyState>>, shortcut: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let hotkey = parse_hotkey(shortcut)?;
|
||||
let mut hotkey_state = state.lock();
|
||||
|
||||
if let Some(manager) = &hotkey_state.manager {
|
||||
manager.register(hotkey.clone())?;
|
||||
hotkey_state.registered_hotkey = Some(hotkey);
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Hotkey manager not initialized".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_hotkey(shortcut: &[String]) -> Result<HotKey, Box<dyn std::error::Error>> {
|
||||
let mut modifiers = Modifiers::empty();
|
||||
let mut code = None;
|
||||
|
||||
for part in shortcut {
|
||||
match part.as_str() {
|
||||
"ControlLeft" => modifiers |= Modifiers::CONTROL,
|
||||
"AltLeft" => modifiers |= Modifiers::ALT,
|
||||
"ShiftLeft" => modifiers |= Modifiers::SHIFT,
|
||||
"MetaLeft" => modifiers |= Modifiers::META,
|
||||
key => code = Some(Code::from(KeyCode::from_str(key)?)),
|
||||
}
|
||||
}
|
||||
|
||||
let key_code = code.ok_or_else(|| "No valid key code found".to_string())?;
|
||||
Ok(HotKey::new(Some(modifiers), key_code))
|
||||
}
|
||||
|
||||
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.set_always_on_top(true).unwrap();
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
|
||||
let window_clone = window.clone();
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
window_clone.set_always_on_top(false).unwrap();
|
||||
});
|
||||
|
||||
center_window_on_current_monitor(&window);
|
||||
}
|
||||
|
||||
let _ = app_handle.track_event(
|
||||
"hotkey_triggered",
|
||||
Some(
|
||||
serde_json::json!({
|
||||
"action": if window.is_visible().unwrap() { "hide" } else { "show" }
|
||||
})
|
||||
)
|
||||
);
|
||||
use crate::utils::commands::center_window_on_current_monitor;
|
||||
use crate::utils::keys::KeyCode;
|
||||
use global_hotkey::{
|
||||
hotkey::{ Code, HotKey, Modifiers },
|
||||
GlobalHotKeyEvent,
|
||||
GlobalHotKeyManager,
|
||||
HotKeyState,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tauri::{ AppHandle, Manager, Listener };
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
|
||||
#[derive(Default)]
|
||||
struct HotkeyState {
|
||||
manager: Option<GlobalHotKeyManager>,
|
||||
registered_hotkey: Option<HotKey>,
|
||||
}
|
||||
|
||||
unsafe impl Send for HotkeyState {}
|
||||
|
||||
pub fn setup(app_handle: tauri::AppHandle) {
|
||||
let state = Arc::new(Mutex::new(HotkeyState::default()));
|
||||
let manager = match GlobalHotKeyManager::new() {
|
||||
Ok(manager) => manager,
|
||||
Err(err) => {
|
||||
eprintln!("Failed to initialize hotkey manager: {:?}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
let mut hotkey_state = state.lock();
|
||||
hotkey_state.manager = Some(manager);
|
||||
}
|
||||
|
||||
let rt = app_handle.state::<tokio::runtime::Runtime>();
|
||||
let initial_keybind = rt
|
||||
.block_on(crate::db::settings::get_keybind(app_handle.clone()))
|
||||
.expect("Failed to get initial keybind");
|
||||
|
||||
if let Err(e) = register_shortcut(&state, &initial_keybind) {
|
||||
eprintln!("Error registering initial shortcut: {:?}", e);
|
||||
}
|
||||
|
||||
let state_clone = Arc::clone(&state);
|
||||
app_handle.listen("update-shortcut", move |event| {
|
||||
let payload_str = event.payload().replace("\\\"", "\"");
|
||||
let trimmed_str = payload_str.trim_matches('"');
|
||||
unregister_current_hotkey(&state_clone);
|
||||
|
||||
let payload: Vec<String> = serde_json::from_str(trimmed_str).unwrap_or_default();
|
||||
if let Err(e) = register_shortcut(&state_clone, &payload) {
|
||||
eprintln!("Error re-registering shortcut: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
let state_clone = Arc::clone(&state);
|
||||
app_handle.listen("save_keybind", move |event| {
|
||||
let payload_str = event.payload().to_string();
|
||||
unregister_current_hotkey(&state_clone);
|
||||
|
||||
let payload: Vec<String> = serde_json::from_str(&payload_str).unwrap_or_default();
|
||||
if let Err(e) = register_shortcut(&state_clone, &payload) {
|
||||
eprintln!("Error registering saved shortcut: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
setup_hotkey_receiver(app_handle);
|
||||
}
|
||||
|
||||
fn setup_hotkey_receiver(app_handle: AppHandle) {
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
match GlobalHotKeyEvent::receiver().recv() {
|
||||
Ok(event) => {
|
||||
if event.state == HotKeyState::Released {
|
||||
continue;
|
||||
}
|
||||
handle_hotkey_event(&app_handle);
|
||||
}
|
||||
Err(e) => eprintln!("Error receiving hotkey event: {:?}", e),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn unregister_current_hotkey(state: &Arc<Mutex<HotkeyState>>) {
|
||||
let mut hotkey_state = state.lock();
|
||||
if let Some(old_hotkey) = hotkey_state.registered_hotkey.take() {
|
||||
if let Some(manager) = &hotkey_state.manager {
|
||||
let _ = manager.unregister(old_hotkey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn register_shortcut(state: &Arc<Mutex<HotkeyState>>, shortcut: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let hotkey = parse_hotkey(shortcut)?;
|
||||
let mut hotkey_state = state.lock();
|
||||
|
||||
if let Some(manager) = &hotkey_state.manager {
|
||||
manager.register(hotkey.clone())?;
|
||||
hotkey_state.registered_hotkey = Some(hotkey);
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Hotkey manager not initialized".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_hotkey(shortcut: &[String]) -> Result<HotKey, Box<dyn std::error::Error>> {
|
||||
let mut modifiers = Modifiers::empty();
|
||||
let mut code = None;
|
||||
|
||||
for part in shortcut {
|
||||
match part.as_str() {
|
||||
"ControlLeft" => modifiers |= Modifiers::CONTROL,
|
||||
"AltLeft" => modifiers |= Modifiers::ALT,
|
||||
"ShiftLeft" => modifiers |= Modifiers::SHIFT,
|
||||
"MetaLeft" => modifiers |= Modifiers::META,
|
||||
key => code = Some(Code::from(KeyCode::from_str(key)?)),
|
||||
}
|
||||
}
|
||||
|
||||
let key_code = code.ok_or_else(|| "No valid key code found".to_string())?;
|
||||
Ok(HotKey::new(Some(modifiers), key_code))
|
||||
}
|
||||
|
||||
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.set_always_on_top(true).unwrap();
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
|
||||
let window_clone = window.clone();
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
window_clone.set_always_on_top(false).unwrap();
|
||||
});
|
||||
|
||||
center_window_on_current_monitor(&window);
|
||||
}
|
||||
|
||||
let _ = app_handle.track_event(
|
||||
"hotkey_triggered",
|
||||
Some(
|
||||
serde_json::json!({
|
||||
"action": if window.is_visible().unwrap() { "hide" } else { "show" }
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
pub mod clipboard;
|
||||
pub mod hotkeys;
|
||||
pub mod tray;
|
||||
pub mod updater;
|
||||
pub mod clipboard;
|
||||
pub mod hotkeys;
|
||||
pub mod tray;
|
||||
pub mod updater;
|
||||
|
|
|
@ -1,61 +1,61 @@
|
|||
use tauri::{ menu::{ MenuBuilder, MenuItemBuilder }, tray::TrayIconBuilder, Emitter, Manager };
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
|
||||
pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
let is_visible = window.is_visible().unwrap();
|
||||
let _ = app.track_event(
|
||||
"tray_toggle",
|
||||
Some(serde_json::json!({
|
||||
"action": if is_visible { "hide" } else { "show" }
|
||||
}))
|
||||
);
|
||||
|
||||
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("app_name", "Qopy").enabled(false).build(app)?])
|
||||
.items(&[&MenuItemBuilder::with_id("show", "Show/Hide").build(app)?])
|
||||
.items(&[&MenuItemBuilder::with_id("settings", "Settings").build(app)?])
|
||||
.items(&[&MenuItemBuilder::with_id("quit", "Quit").build(app)?])
|
||||
.build()?
|
||||
)
|
||||
.on_menu_event(move |_app, event| {
|
||||
match event.id().as_ref() {
|
||||
"quit" => {
|
||||
let _ = _app.track_event("app_quit", None);
|
||||
std::process::exit(0);
|
||||
}
|
||||
"show" => {
|
||||
let _ = _app.track_event(
|
||||
"tray_toggle",
|
||||
Some(
|
||||
serde_json::json!({
|
||||
"action": if is_visible { "hide" } else { "show" }
|
||||
})
|
||||
)
|
||||
);
|
||||
let is_visible = window.is_visible().unwrap();
|
||||
if is_visible {
|
||||
window.hide().unwrap();
|
||||
} else {
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
}
|
||||
window.emit("main_route", ()).unwrap();
|
||||
}
|
||||
"settings" => {
|
||||
let _ = _app.track_event("tray_settings", None);
|
||||
window.emit("settings", ()).unwrap();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
})
|
||||
.icon(icon)
|
||||
.build(app)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
use tauri::{ menu::{ MenuBuilder, MenuItemBuilder }, tray::TrayIconBuilder, Emitter, Manager };
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
|
||||
pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
let is_visible = window.is_visible().unwrap();
|
||||
let _ = app.track_event(
|
||||
"tray_toggle",
|
||||
Some(serde_json::json!({
|
||||
"action": if is_visible { "hide" } else { "show" }
|
||||
}))
|
||||
);
|
||||
|
||||
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("app_name", "Qopy").enabled(false).build(app)?])
|
||||
.items(&[&MenuItemBuilder::with_id("show", "Show/Hide").build(app)?])
|
||||
.items(&[&MenuItemBuilder::with_id("settings", "Settings").build(app)?])
|
||||
.items(&[&MenuItemBuilder::with_id("quit", "Quit").build(app)?])
|
||||
.build()?
|
||||
)
|
||||
.on_menu_event(move |_app, event| {
|
||||
match event.id().as_ref() {
|
||||
"quit" => {
|
||||
let _ = _app.track_event("app_quit", None);
|
||||
std::process::exit(0);
|
||||
}
|
||||
"show" => {
|
||||
let _ = _app.track_event(
|
||||
"tray_toggle",
|
||||
Some(
|
||||
serde_json::json!({
|
||||
"action": if is_visible { "hide" } else { "show" }
|
||||
})
|
||||
)
|
||||
);
|
||||
let is_visible = window.is_visible().unwrap();
|
||||
if is_visible {
|
||||
window.hide().unwrap();
|
||||
} else {
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
}
|
||||
window.emit("main_route", ()).unwrap();
|
||||
}
|
||||
"settings" => {
|
||||
let _ = _app.track_event("tray_settings", None);
|
||||
window.emit("settings", ()).unwrap();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
})
|
||||
.icon(icon)
|
||||
.build(app)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,94 +1,94 @@
|
|||
use tauri::{ async_runtime, AppHandle, Manager };
|
||||
use tauri_plugin_dialog::{ DialogExt, MessageDialogButtons, MessageDialogKind };
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
pub async fn check_for_updates(app: AppHandle, prompted: bool) {
|
||||
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?",
|
||||
]);
|
||||
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
|
||||
app.dialog()
|
||||
.message(msg)
|
||||
.title("Qopy Update Available")
|
||||
.buttons(
|
||||
MessageDialogButtons::OkCancelCustom(
|
||||
String::from("Install"),
|
||||
String::from("Cancel")
|
||||
)
|
||||
)
|
||||
.show(move |response| {
|
||||
if !response {
|
||||
return;
|
||||
}
|
||||
async_runtime::spawn(async move {
|
||||
match
|
||||
update.download_and_install(
|
||||
|_, _| {},
|
||||
|| {}
|
||||
).await
|
||||
{
|
||||
Ok(_) => {
|
||||
app.dialog()
|
||||
.message(
|
||||
"Update installed successfully. The application needs to restart to apply the changes."
|
||||
)
|
||||
.title("Qopy Update Installed")
|
||||
.buttons(
|
||||
MessageDialogButtons::OkCancelCustom(
|
||||
String::from("Restart"),
|
||||
String::from("Cancel")
|
||||
)
|
||||
)
|
||||
.show(move |response| {
|
||||
if response {
|
||||
app.restart();
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
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) => {
|
||||
if prompted {
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
|
||||
app.dialog()
|
||||
.message("No updates available.")
|
||||
.title("Qopy Update Check")
|
||||
.show(|_| {});
|
||||
}
|
||||
|
||||
println!("No updates available. {}", e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
use tauri::{ async_runtime, AppHandle, Manager };
|
||||
use tauri_plugin_dialog::{ DialogExt, MessageDialogButtons, MessageDialogKind };
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
pub async fn check_for_updates(app: AppHandle, prompted: bool) {
|
||||
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?",
|
||||
]);
|
||||
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
|
||||
app.dialog()
|
||||
.message(msg)
|
||||
.title("Qopy Update Available")
|
||||
.buttons(
|
||||
MessageDialogButtons::OkCancelCustom(
|
||||
String::from("Install"),
|
||||
String::from("Cancel")
|
||||
)
|
||||
)
|
||||
.show(move |response| {
|
||||
if !response {
|
||||
return;
|
||||
}
|
||||
async_runtime::spawn(async move {
|
||||
match
|
||||
update.download_and_install(
|
||||
|_, _| {},
|
||||
|| {}
|
||||
).await
|
||||
{
|
||||
Ok(_) => {
|
||||
app.dialog()
|
||||
.message(
|
||||
"Update installed successfully. The application needs to restart to apply the changes."
|
||||
)
|
||||
.title("Qopy Update Installed")
|
||||
.buttons(
|
||||
MessageDialogButtons::OkCancelCustom(
|
||||
String::from("Restart"),
|
||||
String::from("Cancel")
|
||||
)
|
||||
)
|
||||
.show(move |response| {
|
||||
if response {
|
||||
app.restart();
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
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) => {
|
||||
if prompted {
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
|
||||
app.dialog()
|
||||
.message("No updates available.")
|
||||
.title("Qopy Update Check")
|
||||
.show(|_| {});
|
||||
}
|
||||
|
||||
println!("No updates available. {}", e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,107 +1,107 @@
|
|||
use include_dir::{ include_dir, Dir };
|
||||
use sqlx::sqlite::{ SqlitePool, SqlitePoolOptions };
|
||||
use std::fs;
|
||||
use tauri::Manager;
|
||||
use tokio::runtime::Runtime as TokioRuntime;
|
||||
|
||||
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/db/migrations");
|
||||
|
||||
pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let rt = TokioRuntime::new().expect("Failed to create Tokio runtime");
|
||||
app.manage(rt);
|
||||
|
||||
let rt = app.state::<TokioRuntime>();
|
||||
|
||||
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")
|
||||
});
|
||||
|
||||
app.manage(pool.clone());
|
||||
|
||||
rt.block_on(async {
|
||||
apply_migrations(&pool).await?;
|
||||
if is_new_db {
|
||||
if let Err(e) = super::history::initialize_history(&pool).await {
|
||||
eprintln!("Failed to initialize history: {}", e);
|
||||
}
|
||||
if let Err(e) = super::settings::initialize_settings(&pool).await {
|
||||
eprintln!("Failed to initialize settings: {}", e);
|
||||
}
|
||||
}
|
||||
Ok::<(), Box<dyn std::error::Error>>(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
sqlx
|
||||
::query(
|
||||
"CREATE TABLE IF NOT EXISTS schema_version (
|
||||
version INTEGER PRIMARY KEY,
|
||||
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);"
|
||||
)
|
||||
.execute(pool).await?;
|
||||
|
||||
let current_version: Option<i64> = sqlx
|
||||
::query_scalar("SELECT MAX(version) FROM schema_version")
|
||||
.fetch_one(pool).await?;
|
||||
|
||||
let current_version = current_version.unwrap_or(0);
|
||||
|
||||
let mut migration_files: Vec<(i64, &str)> = MIGRATIONS_DIR.files()
|
||||
.filter_map(|file| {
|
||||
let file_name = file.path().file_name()?.to_str()?;
|
||||
if file_name.ends_with(".sql") && file_name.starts_with("v") {
|
||||
let version: i64 = file_name
|
||||
.trim_start_matches("v")
|
||||
.trim_end_matches(".sql")
|
||||
.parse()
|
||||
.ok()?;
|
||||
Some((version, file.contents_utf8()?))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
migration_files.sort_by_key(|(version, _)| *version);
|
||||
|
||||
for (version, content) in migration_files {
|
||||
if version > current_version {
|
||||
let statements: Vec<&str> = content
|
||||
.split(';')
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
|
||||
for statement in statements {
|
||||
sqlx
|
||||
::query(statement)
|
||||
.execute(pool).await
|
||||
.map_err(|e| format!("Failed to execute migration {}: {}", version, e))?;
|
||||
}
|
||||
|
||||
sqlx
|
||||
::query("INSERT INTO schema_version (version) VALUES (?)")
|
||||
.bind(version)
|
||||
.execute(pool).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
use include_dir::{ include_dir, Dir };
|
||||
use sqlx::sqlite::{ SqlitePool, SqlitePoolOptions };
|
||||
use std::fs;
|
||||
use tauri::Manager;
|
||||
use tokio::runtime::Runtime as TokioRuntime;
|
||||
|
||||
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/db/migrations");
|
||||
|
||||
pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let rt = TokioRuntime::new().expect("Failed to create Tokio runtime");
|
||||
app.manage(rt);
|
||||
|
||||
let rt = app.state::<TokioRuntime>();
|
||||
|
||||
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")
|
||||
});
|
||||
|
||||
app.manage(pool.clone());
|
||||
|
||||
rt.block_on(async {
|
||||
apply_migrations(&pool).await?;
|
||||
if is_new_db {
|
||||
if let Err(e) = super::history::initialize_history(&pool).await {
|
||||
eprintln!("Failed to initialize history: {}", e);
|
||||
}
|
||||
if let Err(e) = super::settings::initialize_settings(&pool).await {
|
||||
eprintln!("Failed to initialize settings: {}", e);
|
||||
}
|
||||
}
|
||||
Ok::<(), Box<dyn std::error::Error>>(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
sqlx
|
||||
::query(
|
||||
"CREATE TABLE IF NOT EXISTS schema_version (
|
||||
version INTEGER PRIMARY KEY,
|
||||
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);"
|
||||
)
|
||||
.execute(pool).await?;
|
||||
|
||||
let current_version: Option<i64> = sqlx
|
||||
::query_scalar("SELECT MAX(version) FROM schema_version")
|
||||
.fetch_one(pool).await?;
|
||||
|
||||
let current_version = current_version.unwrap_or(0);
|
||||
|
||||
let mut migration_files: Vec<(i64, &str)> = MIGRATIONS_DIR.files()
|
||||
.filter_map(|file| {
|
||||
let file_name = file.path().file_name()?.to_str()?;
|
||||
if file_name.ends_with(".sql") && file_name.starts_with("v") {
|
||||
let version: i64 = file_name
|
||||
.trim_start_matches("v")
|
||||
.trim_end_matches(".sql")
|
||||
.parse()
|
||||
.ok()?;
|
||||
Some((version, file.contents_utf8()?))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
migration_files.sort_by_key(|(version, _)| *version);
|
||||
|
||||
for (version, content) in migration_files {
|
||||
if version > current_version {
|
||||
let statements: Vec<&str> = content
|
||||
.split(';')
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
|
||||
for statement in statements {
|
||||
sqlx
|
||||
::query(statement)
|
||||
.execute(pool).await
|
||||
.map_err(|e| format!("Failed to execute migration {}: {}", version, e))?;
|
||||
}
|
||||
|
||||
sqlx
|
||||
::query("INSERT INTO schema_version (version) VALUES (?)")
|
||||
.bind(version)
|
||||
.execute(pool).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,226 +1,226 @@
|
|||
use crate::utils::types::{ ContentType, HistoryItem };
|
||||
use base64::{ engine::general_purpose::STANDARD, Engine };
|
||||
use rand::{ rng, Rng };
|
||||
use rand::distr::Alphanumeric;
|
||||
use sqlx::{ Row, SqlitePool };
|
||||
use std::fs;
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
use tauri::Emitter;
|
||||
|
||||
pub async fn initialize_history(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let id: String = rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(16)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO history (id, source, content_type, content, timestamp) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)"
|
||||
)
|
||||
.bind(id)
|
||||
.bind("System")
|
||||
.bind("text")
|
||||
.bind("Welcome to your clipboard history!")
|
||||
.execute(pool).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_history(pool: tauri::State<'_, SqlitePool>) -> Result<Vec<HistoryItem>, String> {
|
||||
let rows = sqlx
|
||||
::query(
|
||||
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC"
|
||||
)
|
||||
.fetch_all(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let items = rows
|
||||
.iter()
|
||||
.map(|row| HistoryItem {
|
||||
id: row.get("id"),
|
||||
source: row.get("source"),
|
||||
source_icon: row.get("source_icon"),
|
||||
content_type: ContentType::from(row.get::<String, _>("content_type")),
|
||||
content: row.get("content"),
|
||||
favicon: row.get("favicon"),
|
||||
timestamp: row.get("timestamp"),
|
||||
language: row.get("language"),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn add_history_item(
|
||||
app_handle: tauri::AppHandle,
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
item: HistoryItem
|
||||
) -> Result<(), String> {
|
||||
let (id, source, source_icon, content_type, content, favicon, timestamp, language) =
|
||||
item.to_row();
|
||||
|
||||
let existing = sqlx
|
||||
::query("SELECT id FROM history WHERE content = ? AND content_type = ?")
|
||||
.bind(&content)
|
||||
.bind(&content_type)
|
||||
.fetch_optional(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
match existing {
|
||||
Some(_) => {
|
||||
sqlx
|
||||
::query(
|
||||
"UPDATE history SET source = ?, source_icon = ?, timestamp = strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now'), favicon = ?, language = ? WHERE content = ? AND content_type = ?"
|
||||
)
|
||||
.bind(&source)
|
||||
.bind(&source_icon)
|
||||
.bind(&favicon)
|
||||
.bind(&language)
|
||||
.bind(&content)
|
||||
.bind(&content_type)
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
None => {
|
||||
sqlx
|
||||
::query(
|
||||
"INSERT INTO history (id, source, source_icon, content_type, content, favicon, timestamp, language) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
)
|
||||
.bind(id)
|
||||
.bind(source)
|
||||
.bind(source_icon)
|
||||
.bind(content_type)
|
||||
.bind(content)
|
||||
.bind(favicon)
|
||||
.bind(timestamp)
|
||||
.bind(language)
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = app_handle.track_event(
|
||||
"history_item_added",
|
||||
Some(serde_json::json!({
|
||||
"content_type": item.content_type.to_string()
|
||||
}))
|
||||
);
|
||||
|
||||
let _ = app_handle.emit("clipboard-content-updated", ());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn search_history(
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
query: String
|
||||
) -> Result<Vec<HistoryItem>, String> {
|
||||
if query.trim().is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let query = format!("%{}%", query);
|
||||
|
||||
let rows = sqlx
|
||||
::query(
|
||||
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language
|
||||
FROM history
|
||||
WHERE content LIKE ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 100"
|
||||
)
|
||||
.bind(query)
|
||||
.fetch_all(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut items = Vec::with_capacity(rows.len());
|
||||
for row in rows.iter() {
|
||||
items.push(HistoryItem {
|
||||
id: row.get("id"),
|
||||
source: row.get("source"),
|
||||
source_icon: row.get("source_icon"),
|
||||
content_type: ContentType::from(row.get::<String, _>("content_type")),
|
||||
content: row.get("content"),
|
||||
favicon: row.get("favicon"),
|
||||
timestamp: row.get("timestamp"),
|
||||
language: row.get("language"),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_history_chunk(
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
offset: i64,
|
||||
limit: i64
|
||||
) -> Result<Vec<HistoryItem>, String> {
|
||||
let rows = sqlx
|
||||
::query(
|
||||
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC LIMIT ? OFFSET ?"
|
||||
)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let items = rows
|
||||
.iter()
|
||||
.map(|row| HistoryItem {
|
||||
id: row.get("id"),
|
||||
source: row.get("source"),
|
||||
source_icon: row.get("source_icon"),
|
||||
content_type: ContentType::from(row.get::<String, _>("content_type")),
|
||||
content: row.get("content"),
|
||||
favicon: row.get("favicon"),
|
||||
timestamp: row.get("timestamp"),
|
||||
language: row.get("language"),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_history_item(
|
||||
app_handle: tauri::AppHandle,
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
id: String
|
||||
) -> Result<(), String> {
|
||||
sqlx
|
||||
::query("DELETE FROM history WHERE id = ?")
|
||||
.bind(id)
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let _ = app_handle.track_event("history_item_deleted", None);
|
||||
let _ = app_handle.emit("clipboard-content-updated", ());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn clear_history(
|
||||
app_handle: tauri::AppHandle,
|
||||
pool: tauri::State<'_, SqlitePool>
|
||||
) -> Result<(), String> {
|
||||
sqlx
|
||||
::query("DELETE FROM history")
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let _ = app_handle.track_event("history_cleared", None);
|
||||
let _ = app_handle.emit("clipboard-content-updated", ());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn read_image(filename: String) -> Result<String, String> {
|
||||
let bytes = fs::read(filename).map_err(|e| e.to_string())?;
|
||||
Ok(STANDARD.encode(bytes))
|
||||
}
|
||||
use crate::utils::types::{ ContentType, HistoryItem };
|
||||
use base64::{ engine::general_purpose::STANDARD, Engine };
|
||||
use rand::{ rng, Rng };
|
||||
use rand::distr::Alphanumeric;
|
||||
use sqlx::{ Row, SqlitePool };
|
||||
use std::fs;
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
use tauri::Emitter;
|
||||
|
||||
pub async fn initialize_history(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let id: String = rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(16)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO history (id, source, content_type, content, timestamp) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)"
|
||||
)
|
||||
.bind(id)
|
||||
.bind("System")
|
||||
.bind("text")
|
||||
.bind("Welcome to your clipboard history!")
|
||||
.execute(pool).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_history(pool: tauri::State<'_, SqlitePool>) -> Result<Vec<HistoryItem>, String> {
|
||||
let rows = sqlx
|
||||
::query(
|
||||
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC"
|
||||
)
|
||||
.fetch_all(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let items = rows
|
||||
.iter()
|
||||
.map(|row| HistoryItem {
|
||||
id: row.get("id"),
|
||||
source: row.get("source"),
|
||||
source_icon: row.get("source_icon"),
|
||||
content_type: ContentType::from(row.get::<String, _>("content_type")),
|
||||
content: row.get("content"),
|
||||
favicon: row.get("favicon"),
|
||||
timestamp: row.get("timestamp"),
|
||||
language: row.get("language"),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn add_history_item(
|
||||
app_handle: tauri::AppHandle,
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
item: HistoryItem
|
||||
) -> Result<(), String> {
|
||||
let (id, source, source_icon, content_type, content, favicon, timestamp, language) =
|
||||
item.to_row();
|
||||
|
||||
let existing = sqlx
|
||||
::query("SELECT id FROM history WHERE content = ? AND content_type = ?")
|
||||
.bind(&content)
|
||||
.bind(&content_type)
|
||||
.fetch_optional(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
match existing {
|
||||
Some(_) => {
|
||||
sqlx
|
||||
::query(
|
||||
"UPDATE history SET source = ?, source_icon = ?, timestamp = strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now'), favicon = ?, language = ? WHERE content = ? AND content_type = ?"
|
||||
)
|
||||
.bind(&source)
|
||||
.bind(&source_icon)
|
||||
.bind(&favicon)
|
||||
.bind(&language)
|
||||
.bind(&content)
|
||||
.bind(&content_type)
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
None => {
|
||||
sqlx
|
||||
::query(
|
||||
"INSERT INTO history (id, source, source_icon, content_type, content, favicon, timestamp, language) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
)
|
||||
.bind(id)
|
||||
.bind(source)
|
||||
.bind(source_icon)
|
||||
.bind(content_type)
|
||||
.bind(content)
|
||||
.bind(favicon)
|
||||
.bind(timestamp)
|
||||
.bind(language)
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = app_handle.track_event(
|
||||
"history_item_added",
|
||||
Some(serde_json::json!({
|
||||
"content_type": item.content_type.to_string()
|
||||
}))
|
||||
);
|
||||
|
||||
let _ = app_handle.emit("clipboard-content-updated", ());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn search_history(
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
query: String
|
||||
) -> Result<Vec<HistoryItem>, String> {
|
||||
if query.trim().is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let query = format!("%{}%", query);
|
||||
|
||||
let rows = sqlx
|
||||
::query(
|
||||
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language
|
||||
FROM history
|
||||
WHERE content LIKE ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 100"
|
||||
)
|
||||
.bind(query)
|
||||
.fetch_all(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut items = Vec::with_capacity(rows.len());
|
||||
for row in rows.iter() {
|
||||
items.push(HistoryItem {
|
||||
id: row.get("id"),
|
||||
source: row.get("source"),
|
||||
source_icon: row.get("source_icon"),
|
||||
content_type: ContentType::from(row.get::<String, _>("content_type")),
|
||||
content: row.get("content"),
|
||||
favicon: row.get("favicon"),
|
||||
timestamp: row.get("timestamp"),
|
||||
language: row.get("language"),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_history_chunk(
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
offset: i64,
|
||||
limit: i64
|
||||
) -> Result<Vec<HistoryItem>, String> {
|
||||
let rows = sqlx
|
||||
::query(
|
||||
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC LIMIT ? OFFSET ?"
|
||||
)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let items = rows
|
||||
.iter()
|
||||
.map(|row| HistoryItem {
|
||||
id: row.get("id"),
|
||||
source: row.get("source"),
|
||||
source_icon: row.get("source_icon"),
|
||||
content_type: ContentType::from(row.get::<String, _>("content_type")),
|
||||
content: row.get("content"),
|
||||
favicon: row.get("favicon"),
|
||||
timestamp: row.get("timestamp"),
|
||||
language: row.get("language"),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_history_item(
|
||||
app_handle: tauri::AppHandle,
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
id: String
|
||||
) -> Result<(), String> {
|
||||
sqlx
|
||||
::query("DELETE FROM history WHERE id = ?")
|
||||
.bind(id)
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let _ = app_handle.track_event("history_item_deleted", None);
|
||||
let _ = app_handle.emit("clipboard-content-updated", ());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn clear_history(
|
||||
app_handle: tauri::AppHandle,
|
||||
pool: tauri::State<'_, SqlitePool>
|
||||
) -> Result<(), String> {
|
||||
sqlx
|
||||
::query("DELETE FROM history")
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let _ = app_handle.track_event("history_cleared", None);
|
||||
let _ = app_handle.emit("clipboard-content-updated", ());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn read_image(filename: String) -> Result<String, String> {
|
||||
let bytes = fs::read(filename).map_err(|e| e.to_string())?;
|
||||
Ok(STANDARD.encode(bytes))
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS history (
|
||||
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
||||
content_type TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
favicon TEXT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS history (
|
||||
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
||||
content_type TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
favicon TEXT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
|
@ -1,3 +1,3 @@
|
|||
ALTER TABLE history ADD COLUMN source TEXT DEFAULT 'System' NOT NULL;
|
||||
ALTER TABLE history ADD COLUMN source_icon TEXT;
|
||||
ALTER TABLE history ADD COLUMN language TEXT;
|
||||
ALTER TABLE history ADD COLUMN source TEXT DEFAULT 'System' NOT NULL;
|
||||
ALTER TABLE history ADD COLUMN source_icon TEXT;
|
||||
ALTER TABLE history ADD COLUMN language TEXT;
|
||||
|
|
|
@ -1 +1 @@
|
|||
INSERT INTO settings (key, value) VALUES ('autostart', 'true');
|
||||
INSERT INTO settings (key, value) VALUES ('autostart', 'true');
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
pub mod database;
|
||||
pub mod history;
|
||||
pub mod settings;
|
||||
pub mod database;
|
||||
pub mod history;
|
||||
pub mod settings;
|
||||
|
|
|
@ -1,87 +1,87 @@
|
|||
use serde::{ Deserialize, Serialize };
|
||||
use serde_json;
|
||||
use sqlx::Row;
|
||||
use sqlx::SqlitePool;
|
||||
use tauri::{ Emitter, Manager };
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct KeybindSetting {
|
||||
keybind: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn initialize_settings(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let default_keybind = KeybindSetting {
|
||||
keybind: vec!["Meta".to_string(), "V".to_string()],
|
||||
};
|
||||
let json = serde_json::to_string(&default_keybind)?;
|
||||
|
||||
sqlx
|
||||
::query("INSERT INTO settings (key, value) VALUES ('keybind', ?)")
|
||||
.bind(json)
|
||||
.execute(pool).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_setting(
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
key: String
|
||||
) -> Result<String, String> {
|
||||
let row = sqlx
|
||||
::query("SELECT value FROM settings WHERE key = ?")
|
||||
.bind(key)
|
||||
.fetch_optional(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(row.map(|r| r.get("value")).unwrap_or_default())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_setting(
|
||||
app_handle: tauri::AppHandle,
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
key: String,
|
||||
value: String
|
||||
) -> Result<(), String> {
|
||||
sqlx
|
||||
::query("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)")
|
||||
.bind(key.clone())
|
||||
.bind(value.clone())
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let _ = app_handle.track_event(
|
||||
"setting_saved",
|
||||
Some(serde_json::json!({
|
||||
"key": key
|
||||
}))
|
||||
);
|
||||
|
||||
if key == "keybind" {
|
||||
let _ = app_handle.emit("update-shortcut", &value).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_keybind(app_handle: tauri::AppHandle) -> Result<Vec<String>, String> {
|
||||
let pool = app_handle.state::<SqlitePool>();
|
||||
|
||||
let row = sqlx
|
||||
::query("SELECT value FROM settings WHERE key = 'keybind'")
|
||||
.fetch_optional(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let json = row
|
||||
.map(|r| r.get::<String, _>("value"))
|
||||
.unwrap_or_else(|| {
|
||||
serde_json
|
||||
::to_string(&vec!["MetaLeft".to_string(), "KeyV".to_string()])
|
||||
.expect("Failed to serialize default keybind")
|
||||
});
|
||||
|
||||
serde_json::from_str::<Vec<String>>(&json).map_err(|e| e.to_string())
|
||||
}
|
||||
use serde::{ Deserialize, Serialize };
|
||||
use serde_json;
|
||||
use sqlx::Row;
|
||||
use sqlx::SqlitePool;
|
||||
use tauri::{ Emitter, Manager };
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct KeybindSetting {
|
||||
keybind: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn initialize_settings(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let default_keybind = KeybindSetting {
|
||||
keybind: vec!["Meta".to_string(), "V".to_string()],
|
||||
};
|
||||
let json = serde_json::to_string(&default_keybind)?;
|
||||
|
||||
sqlx
|
||||
::query("INSERT INTO settings (key, value) VALUES ('keybind', ?)")
|
||||
.bind(json)
|
||||
.execute(pool).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_setting(
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
key: String
|
||||
) -> Result<String, String> {
|
||||
let row = sqlx
|
||||
::query("SELECT value FROM settings WHERE key = ?")
|
||||
.bind(key)
|
||||
.fetch_optional(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(row.map(|r| r.get("value")).unwrap_or_default())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_setting(
|
||||
app_handle: tauri::AppHandle,
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
key: String,
|
||||
value: String
|
||||
) -> Result<(), String> {
|
||||
sqlx
|
||||
::query("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)")
|
||||
.bind(key.clone())
|
||||
.bind(value.clone())
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let _ = app_handle.track_event(
|
||||
"setting_saved",
|
||||
Some(serde_json::json!({
|
||||
"key": key
|
||||
}))
|
||||
);
|
||||
|
||||
if key == "keybind" {
|
||||
let _ = app_handle.emit("update-shortcut", &value).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_keybind(app_handle: tauri::AppHandle) -> Result<Vec<String>, String> {
|
||||
let pool = app_handle.state::<SqlitePool>();
|
||||
|
||||
let row = sqlx
|
||||
::query("SELECT value FROM settings WHERE key = 'keybind'")
|
||||
.fetch_optional(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let json = row
|
||||
.map(|r| r.get::<String, _>("value"))
|
||||
.unwrap_or_else(|| {
|
||||
serde_json
|
||||
::to_string(&vec!["MetaLeft".to_string(), "KeyV".to_string()])
|
||||
.expect("Failed to serialize default keybind")
|
||||
});
|
||||
|
||||
serde_json::from_str::<Vec<String>>(&json).map_err(|e| e.to_string())
|
||||
}
|
||||
|
|
|
@ -1,136 +1,136 @@
|
|||
#![cfg_attr(all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows")]
|
||||
|
||||
mod api;
|
||||
mod db;
|
||||
mod utils;
|
||||
|
||||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
use std::fs;
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_aptabase::{ EventTracker, InitOptions };
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_prevent_default::Flags;
|
||||
|
||||
fn main() {
|
||||
let runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime");
|
||||
let _guard = runtime.enter();
|
||||
|
||||
tauri::Builder
|
||||
::default()
|
||||
.plugin(tauri_plugin_clipboard::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_sql::Builder::default().build())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_updater::Builder::default().build())
|
||||
.plugin(
|
||||
tauri_plugin_aptabase::Builder
|
||||
::new("A-SH-8937252746")
|
||||
.with_options(InitOptions {
|
||||
host: Some("https://aptabase.pandadev.net".to_string()),
|
||||
flush_interval: None,
|
||||
})
|
||||
.with_panic_hook(
|
||||
Box::new(|client, info, msg| {
|
||||
let location = info
|
||||
.location()
|
||||
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
|
||||
.unwrap_or_else(|| "".to_string());
|
||||
|
||||
let _ = client.track_event(
|
||||
"panic",
|
||||
Some(
|
||||
serde_json::json!({
|
||||
"info": format!("{} ({})", msg, location),
|
||||
})
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, Some(vec![])))
|
||||
.plugin(
|
||||
tauri_plugin_prevent_default::Builder
|
||||
::new()
|
||||
.with_flags(Flags::all().difference(Flags::CONTEXT_MENU))
|
||||
.build()
|
||||
)
|
||||
.setup(|app| {
|
||||
#[cfg(target_os = "macos")]
|
||||
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
|
||||
let app_data_dir = app.path().app_data_dir().unwrap();
|
||||
utils::logger::init_logger(&app_data_dir).expect("Failed to initialize logger");
|
||||
|
||||
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 app_handle = app.handle().clone();
|
||||
|
||||
let app_handle_clone = app_handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&db_url).await
|
||||
.expect("Failed to create pool");
|
||||
|
||||
app_handle_clone.manage(pool);
|
||||
});
|
||||
|
||||
let main_window = app.get_webview_window("main");
|
||||
|
||||
let _ = db::database::setup(app);
|
||||
api::hotkeys::setup(app_handle.clone());
|
||||
api::tray::setup(app)?;
|
||||
api::clipboard::setup(app.handle());
|
||||
let _ = api::clipboard::start_monitor(app_handle.clone());
|
||||
|
||||
utils::commands::center_window_on_current_monitor(main_window.as_ref().unwrap());
|
||||
main_window
|
||||
.as_ref()
|
||||
.map(|w| w.hide())
|
||||
.unwrap_or(Ok(()))?;
|
||||
|
||||
let _ = app.track_event("app_started", None);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
api::updater::check_for_updates(app_handle, false).await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|_app, _event| {
|
||||
#[cfg(not(dev))]
|
||||
if let tauri::WindowEvent::Focused(false) = _event {
|
||||
if let Some(window) = _app.get_webview_window("main") {
|
||||
let _ = window.hide();
|
||||
}
|
||||
}
|
||||
})
|
||||
.invoke_handler(
|
||||
tauri::generate_handler![
|
||||
api::clipboard::write_and_paste,
|
||||
db::history::get_history,
|
||||
db::history::add_history_item,
|
||||
db::history::search_history,
|
||||
db::history::load_history_chunk,
|
||||
db::history::delete_history_item,
|
||||
db::history::clear_history,
|
||||
db::history::read_image,
|
||||
db::settings::get_setting,
|
||||
db::settings::save_setting,
|
||||
utils::commands::fetch_page_meta,
|
||||
utils::commands::get_app_info
|
||||
]
|
||||
)
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
#![cfg_attr(all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows")]
|
||||
|
||||
mod api;
|
||||
mod db;
|
||||
mod utils;
|
||||
|
||||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
use std::fs;
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_aptabase::{ EventTracker, InitOptions };
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_prevent_default::Flags;
|
||||
|
||||
fn main() {
|
||||
let runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime");
|
||||
let _guard = runtime.enter();
|
||||
|
||||
tauri::Builder
|
||||
::default()
|
||||
.plugin(tauri_plugin_clipboard::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_sql::Builder::default().build())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_updater::Builder::default().build())
|
||||
.plugin(
|
||||
tauri_plugin_aptabase::Builder
|
||||
::new("A-SH-8937252746")
|
||||
.with_options(InitOptions {
|
||||
host: Some("https://aptabase.pandadev.net".to_string()),
|
||||
flush_interval: None,
|
||||
})
|
||||
.with_panic_hook(
|
||||
Box::new(|client, info, msg| {
|
||||
let location = info
|
||||
.location()
|
||||
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
|
||||
.unwrap_or_else(|| "".to_string());
|
||||
|
||||
let _ = client.track_event(
|
||||
"panic",
|
||||
Some(
|
||||
serde_json::json!({
|
||||
"info": format!("{} ({})", msg, location),
|
||||
})
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, Some(vec![])))
|
||||
.plugin(
|
||||
tauri_plugin_prevent_default::Builder
|
||||
::new()
|
||||
.with_flags(Flags::all().difference(Flags::CONTEXT_MENU))
|
||||
.build()
|
||||
)
|
||||
.setup(|app| {
|
||||
#[cfg(target_os = "macos")]
|
||||
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
|
||||
let app_data_dir = app.path().app_data_dir().unwrap();
|
||||
utils::logger::init_logger(&app_data_dir).expect("Failed to initialize logger");
|
||||
|
||||
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 app_handle = app.handle().clone();
|
||||
|
||||
let app_handle_clone = app_handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&db_url).await
|
||||
.expect("Failed to create pool");
|
||||
|
||||
app_handle_clone.manage(pool);
|
||||
});
|
||||
|
||||
let main_window = app.get_webview_window("main");
|
||||
|
||||
let _ = db::database::setup(app);
|
||||
api::hotkeys::setup(app_handle.clone());
|
||||
api::tray::setup(app)?;
|
||||
api::clipboard::setup(app.handle());
|
||||
let _ = api::clipboard::start_monitor(app_handle.clone());
|
||||
|
||||
utils::commands::center_window_on_current_monitor(main_window.as_ref().unwrap());
|
||||
main_window
|
||||
.as_ref()
|
||||
.map(|w| w.hide())
|
||||
.unwrap_or(Ok(()))?;
|
||||
|
||||
let _ = app.track_event("app_started", None);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
api::updater::check_for_updates(app_handle, false).await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|_app, _event| {
|
||||
#[cfg(not(dev))]
|
||||
if let tauri::WindowEvent::Focused(false) = _event {
|
||||
if let Some(window) = _app.get_webview_window("main") {
|
||||
let _ = window.hide();
|
||||
}
|
||||
}
|
||||
})
|
||||
.invoke_handler(
|
||||
tauri::generate_handler![
|
||||
api::clipboard::write_and_paste,
|
||||
db::history::get_history,
|
||||
db::history::add_history_item,
|
||||
db::history::search_history,
|
||||
db::history::load_history_chunk,
|
||||
db::history::delete_history_item,
|
||||
db::history::clear_history,
|
||||
db::history::read_image,
|
||||
db::settings::get_setting,
|
||||
db::settings::save_setting,
|
||||
utils::commands::fetch_page_meta,
|
||||
utils::commands::get_app_info
|
||||
]
|
||||
)
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
|
@ -1,155 +1,155 @@
|
|||
use applications::{AppInfoContext, AppInfo, AppTrait, utils::image::RustImage};
|
||||
use base64::{ engine::general_purpose::STANDARD, Engine };
|
||||
use image::codecs::png::PngEncoder;
|
||||
use tauri::PhysicalPosition;
|
||||
use meta_fetcher;
|
||||
|
||||
pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) {
|
||||
if
|
||||
let Some(monitor) = window
|
||||
.available_monitors()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|m| {
|
||||
let primary_monitor = window
|
||||
.primary_monitor()
|
||||
.unwrap()
|
||||
.expect("Failed to get primary monitor");
|
||||
let mouse_position = primary_monitor.position();
|
||||
let monitor_position = m.position();
|
||||
let monitor_size = m.size();
|
||||
mouse_position.x >= monitor_position.x &&
|
||||
mouse_position.x < monitor_position.x + (monitor_size.width as i32) &&
|
||||
mouse_position.y >= monitor_position.y &&
|
||||
mouse_position.y < monitor_position.y + (monitor_size.height as i32)
|
||||
})
|
||||
{
|
||||
let monitor_size = monitor.size();
|
||||
let window_size = window.outer_size().unwrap();
|
||||
|
||||
let x = ((monitor_size.width as i32) - (window_size.width as i32)) / 2;
|
||||
let y = ((monitor_size.height as i32) - (window_size.height as i32)) / 2;
|
||||
|
||||
window
|
||||
.set_position(PhysicalPosition::new(monitor.position().x + x, monitor.position().y + y))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_app_info() -> (String, Option<String>) {
|
||||
println!("Getting app info");
|
||||
let mut ctx = AppInfoContext::new(vec![]);
|
||||
println!("Created AppInfoContext");
|
||||
|
||||
if let Err(e) = ctx.refresh_apps() {
|
||||
println!("Failed to refresh apps: {:?}", e);
|
||||
return ("System".to_string(), None);
|
||||
}
|
||||
|
||||
println!("Refreshed apps");
|
||||
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
match ctx.get_frontmost_application() {
|
||||
Ok(window) => {
|
||||
println!("Found frontmost application: {}", window.name);
|
||||
let name = window.name.clone();
|
||||
let icon = window
|
||||
.load_icon()
|
||||
.ok()
|
||||
.and_then(|i| {
|
||||
println!("Loading icon for {}", name);
|
||||
i.to_png().ok().map(|png| {
|
||||
let encoded = STANDARD.encode(png.get_bytes());
|
||||
println!("Icon encoded successfully");
|
||||
encoded
|
||||
})
|
||||
});
|
||||
println!("Returning app info: {} with icon: {}", name, icon.is_some());
|
||||
(name, icon)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to get frontmost application: {:?}", e);
|
||||
("System".to_string(), None)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(info) => info,
|
||||
Err(_) => {
|
||||
println!("Panic occurred while getting app info");
|
||||
("System".to_string(), None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn _process_icon_to_base64(path: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let img = image::open(path)?;
|
||||
let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3);
|
||||
let mut png_buffer = Vec::new();
|
||||
resized.write_with_encoder(PngEncoder::new(&mut png_buffer))?;
|
||||
Ok(STANDARD.encode(png_buffer))
|
||||
}
|
||||
|
||||
pub fn detect_color(color: &str) -> bool {
|
||||
let color = color.trim().to_lowercase();
|
||||
|
||||
// hex
|
||||
if color.starts_with('#') && color.len() == color.trim_end_matches(char::is_whitespace).len() {
|
||||
let hex = &color[1..];
|
||||
return match hex.len() {
|
||||
3 | 6 | 8 => hex.chars().all(|c| c.is_ascii_hexdigit()),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
// rgb/rgba
|
||||
if
|
||||
(color.starts_with("rgb(") || color.starts_with("rgba(")) &&
|
||||
color.ends_with(")") &&
|
||||
!color[..color.len() - 1].contains(")")
|
||||
{
|
||||
let values = color
|
||||
.trim_start_matches("rgba(")
|
||||
.trim_start_matches("rgb(")
|
||||
.trim_end_matches(')')
|
||||
.split(',')
|
||||
.collect::<Vec<&str>>();
|
||||
|
||||
return match values.len() {
|
||||
3 | 4 => values.iter().all(|v| v.trim().parse::<f32>().is_ok()),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
// hsl/hsla
|
||||
if
|
||||
(color.starts_with("hsl(") || color.starts_with("hsla(")) &&
|
||||
color.ends_with(")") &&
|
||||
!color[..color.len() - 1].contains(")")
|
||||
{
|
||||
let values = color
|
||||
.trim_start_matches("hsla(")
|
||||
.trim_start_matches("hsl(")
|
||||
.trim_end_matches(')')
|
||||
.split(',')
|
||||
.collect::<Vec<&str>>();
|
||||
|
||||
return match values.len() {
|
||||
3 | 4 => values.iter().all(|v| v.trim().parse::<f32>().is_ok()),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch_page_meta(url: String) -> Result<(String, Option<String>), String> {
|
||||
let metadata = meta_fetcher
|
||||
::fetch_metadata(&url)
|
||||
.map_err(|e| format!("Failed to fetch metadata: {}", e))?;
|
||||
|
||||
Ok((metadata.title.unwrap_or_else(|| "No title found".to_string()), metadata.image))
|
||||
}
|
||||
use applications::{AppInfoContext, AppInfo, AppTrait, utils::image::RustImage};
|
||||
use base64::{ engine::general_purpose::STANDARD, Engine };
|
||||
use image::codecs::png::PngEncoder;
|
||||
use tauri::PhysicalPosition;
|
||||
use meta_fetcher;
|
||||
|
||||
pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) {
|
||||
if
|
||||
let Some(monitor) = window
|
||||
.available_monitors()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|m| {
|
||||
let primary_monitor = window
|
||||
.primary_monitor()
|
||||
.unwrap()
|
||||
.expect("Failed to get primary monitor");
|
||||
let mouse_position = primary_monitor.position();
|
||||
let monitor_position = m.position();
|
||||
let monitor_size = m.size();
|
||||
mouse_position.x >= monitor_position.x &&
|
||||
mouse_position.x < monitor_position.x + (monitor_size.width as i32) &&
|
||||
mouse_position.y >= monitor_position.y &&
|
||||
mouse_position.y < monitor_position.y + (monitor_size.height as i32)
|
||||
})
|
||||
{
|
||||
let monitor_size = monitor.size();
|
||||
let window_size = window.outer_size().unwrap();
|
||||
|
||||
let x = ((monitor_size.width as i32) - (window_size.width as i32)) / 2;
|
||||
let y = ((monitor_size.height as i32) - (window_size.height as i32)) / 2;
|
||||
|
||||
window
|
||||
.set_position(PhysicalPosition::new(monitor.position().x + x, monitor.position().y + y))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_app_info() -> (String, Option<String>) {
|
||||
println!("Getting app info");
|
||||
let mut ctx = AppInfoContext::new(vec![]);
|
||||
println!("Created AppInfoContext");
|
||||
|
||||
if let Err(e) = ctx.refresh_apps() {
|
||||
println!("Failed to refresh apps: {:?}", e);
|
||||
return ("System".to_string(), None);
|
||||
}
|
||||
|
||||
println!("Refreshed apps");
|
||||
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
match ctx.get_frontmost_application() {
|
||||
Ok(window) => {
|
||||
println!("Found frontmost application: {}", window.name);
|
||||
let name = window.name.clone();
|
||||
let icon = window
|
||||
.load_icon()
|
||||
.ok()
|
||||
.and_then(|i| {
|
||||
println!("Loading icon for {}", name);
|
||||
i.to_png().ok().map(|png| {
|
||||
let encoded = STANDARD.encode(png.get_bytes());
|
||||
println!("Icon encoded successfully");
|
||||
encoded
|
||||
})
|
||||
});
|
||||
println!("Returning app info: {} with icon: {}", name, icon.is_some());
|
||||
(name, icon)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to get frontmost application: {:?}", e);
|
||||
("System".to_string(), None)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(info) => info,
|
||||
Err(_) => {
|
||||
println!("Panic occurred while getting app info");
|
||||
("System".to_string(), None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn _process_icon_to_base64(path: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let img = image::open(path)?;
|
||||
let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3);
|
||||
let mut png_buffer = Vec::new();
|
||||
resized.write_with_encoder(PngEncoder::new(&mut png_buffer))?;
|
||||
Ok(STANDARD.encode(png_buffer))
|
||||
}
|
||||
|
||||
pub fn detect_color(color: &str) -> bool {
|
||||
let color = color.trim().to_lowercase();
|
||||
|
||||
// hex
|
||||
if color.starts_with('#') && color.len() == color.trim_end_matches(char::is_whitespace).len() {
|
||||
let hex = &color[1..];
|
||||
return match hex.len() {
|
||||
3 | 6 | 8 => hex.chars().all(|c| c.is_ascii_hexdigit()),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
// rgb/rgba
|
||||
if
|
||||
(color.starts_with("rgb(") || color.starts_with("rgba(")) &&
|
||||
color.ends_with(")") &&
|
||||
!color[..color.len() - 1].contains(")")
|
||||
{
|
||||
let values = color
|
||||
.trim_start_matches("rgba(")
|
||||
.trim_start_matches("rgb(")
|
||||
.trim_end_matches(')')
|
||||
.split(',')
|
||||
.collect::<Vec<&str>>();
|
||||
|
||||
return match values.len() {
|
||||
3 | 4 => values.iter().all(|v| v.trim().parse::<f32>().is_ok()),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
// hsl/hsla
|
||||
if
|
||||
(color.starts_with("hsl(") || color.starts_with("hsla(")) &&
|
||||
color.ends_with(")") &&
|
||||
!color[..color.len() - 1].contains(")")
|
||||
{
|
||||
let values = color
|
||||
.trim_start_matches("hsla(")
|
||||
.trim_start_matches("hsl(")
|
||||
.trim_end_matches(')')
|
||||
.split(',')
|
||||
.collect::<Vec<&str>>();
|
||||
|
||||
return match values.len() {
|
||||
3 | 4 => values.iter().all(|v| v.trim().parse::<f32>().is_ok()),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch_page_meta(url: String) -> Result<(String, Option<String>), String> {
|
||||
let metadata = meta_fetcher
|
||||
::fetch_metadata(&url)
|
||||
.map_err(|e| format!("Failed to fetch metadata: {}", e))?;
|
||||
|
||||
Ok((metadata.title.unwrap_or_else(|| "No title found".to_string()), metadata.image))
|
||||
}
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine;
|
||||
use image::ImageFormat;
|
||||
use reqwest;
|
||||
use url::Url;
|
||||
|
||||
pub async fn fetch_favicon_as_base64(
|
||||
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)
|
||||
}
|
||||
}
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine;
|
||||
use image::ImageFormat;
|
||||
use reqwest;
|
||||
use url::Url;
|
||||
|
||||
pub async fn fetch_favicon_as_base64(
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,120 +1,120 @@
|
|||
use global_hotkey::hotkey::Code;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub struct KeyCode(Code);
|
||||
|
||||
impl FromStr for KeyCode {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let code = match s {
|
||||
"Backquote" => Code::Backquote,
|
||||
"Backslash" => Code::Backslash,
|
||||
"BracketLeft" => Code::BracketLeft,
|
||||
"BracketRight" => Code::BracketRight,
|
||||
"Comma" => Code::Comma,
|
||||
"Digit0" => Code::Digit0,
|
||||
"Digit1" => Code::Digit1,
|
||||
"Digit2" => Code::Digit2,
|
||||
"Digit3" => Code::Digit3,
|
||||
"Digit4" => Code::Digit4,
|
||||
"Digit5" => Code::Digit5,
|
||||
"Digit6" => Code::Digit6,
|
||||
"Digit7" => Code::Digit7,
|
||||
"Digit8" => Code::Digit8,
|
||||
"Digit9" => Code::Digit9,
|
||||
"Equal" => Code::Equal,
|
||||
"KeyA" => Code::KeyA,
|
||||
"KeyB" => Code::KeyB,
|
||||
"KeyC" => Code::KeyC,
|
||||
"KeyD" => Code::KeyD,
|
||||
"KeyE" => Code::KeyE,
|
||||
"KeyF" => Code::KeyF,
|
||||
"KeyG" => Code::KeyG,
|
||||
"KeyH" => Code::KeyH,
|
||||
"KeyI" => Code::KeyI,
|
||||
"KeyJ" => Code::KeyJ,
|
||||
"KeyK" => Code::KeyK,
|
||||
"KeyL" => Code::KeyL,
|
||||
"KeyM" => Code::KeyM,
|
||||
"KeyN" => Code::KeyN,
|
||||
"KeyO" => Code::KeyO,
|
||||
"KeyP" => Code::KeyP,
|
||||
"KeyQ" => Code::KeyQ,
|
||||
"KeyR" => Code::KeyR,
|
||||
"KeyS" => Code::KeyS,
|
||||
"KeyT" => Code::KeyT,
|
||||
"KeyU" => Code::KeyU,
|
||||
"KeyV" => Code::KeyV,
|
||||
"KeyW" => Code::KeyW,
|
||||
"KeyX" => Code::KeyX,
|
||||
"KeyY" => Code::KeyY,
|
||||
"KeyZ" => Code::KeyZ,
|
||||
"Minus" => Code::Minus,
|
||||
"Period" => Code::Period,
|
||||
"Quote" => Code::Quote,
|
||||
"Semicolon" => Code::Semicolon,
|
||||
"Slash" => Code::Slash,
|
||||
"Backspace" => Code::Backspace,
|
||||
"CapsLock" => Code::CapsLock,
|
||||
"Delete" => Code::Delete,
|
||||
"Enter" => Code::Enter,
|
||||
"Space" => Code::Space,
|
||||
"Tab" => Code::Tab,
|
||||
"End" => Code::End,
|
||||
"Home" => Code::Home,
|
||||
"Insert" => Code::Insert,
|
||||
"PageDown" => Code::PageDown,
|
||||
"PageUp" => Code::PageUp,
|
||||
"ArrowDown" => Code::ArrowDown,
|
||||
"ArrowLeft" => Code::ArrowLeft,
|
||||
"ArrowRight" => Code::ArrowRight,
|
||||
"ArrowUp" => Code::ArrowUp,
|
||||
"NumLock" => Code::NumLock,
|
||||
"Numpad0" => Code::Numpad0,
|
||||
"Numpad1" => Code::Numpad1,
|
||||
"Numpad2" => Code::Numpad2,
|
||||
"Numpad3" => Code::Numpad3,
|
||||
"Numpad4" => Code::Numpad4,
|
||||
"Numpad5" => Code::Numpad5,
|
||||
"Numpad6" => Code::Numpad6,
|
||||
"Numpad7" => Code::Numpad7,
|
||||
"Numpad8" => Code::Numpad8,
|
||||
"Numpad9" => Code::Numpad9,
|
||||
"NumpadAdd" => Code::NumpadAdd,
|
||||
"NumpadDecimal" => Code::NumpadDecimal,
|
||||
"NumpadDivide" => Code::NumpadDivide,
|
||||
"NumpadMultiply" => Code::NumpadMultiply,
|
||||
"NumpadSubtract" => Code::NumpadSubtract,
|
||||
"Escape" => Code::Escape,
|
||||
"PrintScreen" => Code::PrintScreen,
|
||||
"ScrollLock" => Code::ScrollLock,
|
||||
"Pause" => Code::Pause,
|
||||
"AudioVolumeDown" => Code::AudioVolumeDown,
|
||||
"AudioVolumeMute" => Code::AudioVolumeMute,
|
||||
"AudioVolumeUp" => Code::AudioVolumeUp,
|
||||
"F1" => Code::F1,
|
||||
"F2" => Code::F2,
|
||||
"F3" => Code::F3,
|
||||
"F4" => Code::F4,
|
||||
"F5" => Code::F5,
|
||||
"F6" => Code::F6,
|
||||
"F7" => Code::F7,
|
||||
"F8" => Code::F8,
|
||||
"F9" => Code::F9,
|
||||
"F10" => Code::F10,
|
||||
"F11" => Code::F11,
|
||||
"F12" => Code::F12,
|
||||
_ => {
|
||||
return Err(format!("Unknown key code: {}", s));
|
||||
}
|
||||
};
|
||||
Ok(KeyCode(code))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeyCode> for Code {
|
||||
fn from(key_code: KeyCode) -> Self {
|
||||
key_code.0
|
||||
}
|
||||
}
|
||||
use global_hotkey::hotkey::Code;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub struct KeyCode(Code);
|
||||
|
||||
impl FromStr for KeyCode {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let code = match s {
|
||||
"Backquote" => Code::Backquote,
|
||||
"Backslash" => Code::Backslash,
|
||||
"BracketLeft" => Code::BracketLeft,
|
||||
"BracketRight" => Code::BracketRight,
|
||||
"Comma" => Code::Comma,
|
||||
"Digit0" => Code::Digit0,
|
||||
"Digit1" => Code::Digit1,
|
||||
"Digit2" => Code::Digit2,
|
||||
"Digit3" => Code::Digit3,
|
||||
"Digit4" => Code::Digit4,
|
||||
"Digit5" => Code::Digit5,
|
||||
"Digit6" => Code::Digit6,
|
||||
"Digit7" => Code::Digit7,
|
||||
"Digit8" => Code::Digit8,
|
||||
"Digit9" => Code::Digit9,
|
||||
"Equal" => Code::Equal,
|
||||
"KeyA" => Code::KeyA,
|
||||
"KeyB" => Code::KeyB,
|
||||
"KeyC" => Code::KeyC,
|
||||
"KeyD" => Code::KeyD,
|
||||
"KeyE" => Code::KeyE,
|
||||
"KeyF" => Code::KeyF,
|
||||
"KeyG" => Code::KeyG,
|
||||
"KeyH" => Code::KeyH,
|
||||
"KeyI" => Code::KeyI,
|
||||
"KeyJ" => Code::KeyJ,
|
||||
"KeyK" => Code::KeyK,
|
||||
"KeyL" => Code::KeyL,
|
||||
"KeyM" => Code::KeyM,
|
||||
"KeyN" => Code::KeyN,
|
||||
"KeyO" => Code::KeyO,
|
||||
"KeyP" => Code::KeyP,
|
||||
"KeyQ" => Code::KeyQ,
|
||||
"KeyR" => Code::KeyR,
|
||||
"KeyS" => Code::KeyS,
|
||||
"KeyT" => Code::KeyT,
|
||||
"KeyU" => Code::KeyU,
|
||||
"KeyV" => Code::KeyV,
|
||||
"KeyW" => Code::KeyW,
|
||||
"KeyX" => Code::KeyX,
|
||||
"KeyY" => Code::KeyY,
|
||||
"KeyZ" => Code::KeyZ,
|
||||
"Minus" => Code::Minus,
|
||||
"Period" => Code::Period,
|
||||
"Quote" => Code::Quote,
|
||||
"Semicolon" => Code::Semicolon,
|
||||
"Slash" => Code::Slash,
|
||||
"Backspace" => Code::Backspace,
|
||||
"CapsLock" => Code::CapsLock,
|
||||
"Delete" => Code::Delete,
|
||||
"Enter" => Code::Enter,
|
||||
"Space" => Code::Space,
|
||||
"Tab" => Code::Tab,
|
||||
"End" => Code::End,
|
||||
"Home" => Code::Home,
|
||||
"Insert" => Code::Insert,
|
||||
"PageDown" => Code::PageDown,
|
||||
"PageUp" => Code::PageUp,
|
||||
"ArrowDown" => Code::ArrowDown,
|
||||
"ArrowLeft" => Code::ArrowLeft,
|
||||
"ArrowRight" => Code::ArrowRight,
|
||||
"ArrowUp" => Code::ArrowUp,
|
||||
"NumLock" => Code::NumLock,
|
||||
"Numpad0" => Code::Numpad0,
|
||||
"Numpad1" => Code::Numpad1,
|
||||
"Numpad2" => Code::Numpad2,
|
||||
"Numpad3" => Code::Numpad3,
|
||||
"Numpad4" => Code::Numpad4,
|
||||
"Numpad5" => Code::Numpad5,
|
||||
"Numpad6" => Code::Numpad6,
|
||||
"Numpad7" => Code::Numpad7,
|
||||
"Numpad8" => Code::Numpad8,
|
||||
"Numpad9" => Code::Numpad9,
|
||||
"NumpadAdd" => Code::NumpadAdd,
|
||||
"NumpadDecimal" => Code::NumpadDecimal,
|
||||
"NumpadDivide" => Code::NumpadDivide,
|
||||
"NumpadMultiply" => Code::NumpadMultiply,
|
||||
"NumpadSubtract" => Code::NumpadSubtract,
|
||||
"Escape" => Code::Escape,
|
||||
"PrintScreen" => Code::PrintScreen,
|
||||
"ScrollLock" => Code::ScrollLock,
|
||||
"Pause" => Code::Pause,
|
||||
"AudioVolumeDown" => Code::AudioVolumeDown,
|
||||
"AudioVolumeMute" => Code::AudioVolumeMute,
|
||||
"AudioVolumeUp" => Code::AudioVolumeUp,
|
||||
"F1" => Code::F1,
|
||||
"F2" => Code::F2,
|
||||
"F3" => Code::F3,
|
||||
"F4" => Code::F4,
|
||||
"F5" => Code::F5,
|
||||
"F6" => Code::F6,
|
||||
"F7" => Code::F7,
|
||||
"F8" => Code::F8,
|
||||
"F9" => Code::F9,
|
||||
"F10" => Code::F10,
|
||||
"F11" => Code::F11,
|
||||
"F12" => Code::F12,
|
||||
_ => {
|
||||
return Err(format!("Unknown key code: {}", s));
|
||||
}
|
||||
};
|
||||
Ok(KeyCode(code))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeyCode> for Code {
|
||||
fn from(key_code: KeyCode) -> Self {
|
||||
key_code.0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,84 +1,84 @@
|
|||
use chrono;
|
||||
use log::{ LevelFilter, SetLoggerError };
|
||||
use std::fs::{ File, OpenOptions };
|
||||
use std::io::Write;
|
||||
use std::panic;
|
||||
|
||||
pub struct FileLogger {
|
||||
file: File,
|
||||
}
|
||||
|
||||
impl log::Log for FileLogger {
|
||||
fn enabled(&self, _metadata: &log::Metadata) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn log(&self, record: &log::Record) {
|
||||
if self.enabled(record.metadata()) {
|
||||
let mut file = self.file.try_clone().expect("Failed to clone file handle");
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
"{} [{:<5}] {}: {} ({}:{})",
|
||||
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
|
||||
record.level(),
|
||||
record.target(),
|
||||
record.args(),
|
||||
record.file().unwrap_or("unknown"),
|
||||
record.line().unwrap_or(0)
|
||||
).expect("Failed to write to log file");
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&self) {
|
||||
self.file.sync_all().expect("Failed to flush log file");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_logger(app_data_dir: &std::path::Path) -> Result<(), SetLoggerError> {
|
||||
let logs_dir = app_data_dir.join("logs");
|
||||
std::fs::create_dir_all(&logs_dir).expect("Failed to create logs directory");
|
||||
|
||||
let log_path = logs_dir.join("app.log");
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&log_path)
|
||||
.expect("Failed to open log file");
|
||||
|
||||
let panic_file = file.try_clone().expect("Failed to clone file handle");
|
||||
panic::set_hook(
|
||||
Box::new(move |panic_info| {
|
||||
let mut file = panic_file.try_clone().expect("Failed to clone file handle");
|
||||
|
||||
let location = panic_info
|
||||
.location()
|
||||
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
|
||||
.unwrap_or_else(|| "unknown location".to_string());
|
||||
|
||||
let message = match panic_info.payload().downcast_ref::<&str>() {
|
||||
Some(s) => *s,
|
||||
None =>
|
||||
match panic_info.payload().downcast_ref::<String>() {
|
||||
Some(s) => s.as_str(),
|
||||
None => "Unknown panic message",
|
||||
}
|
||||
};
|
||||
|
||||
let _ = writeln!(
|
||||
file,
|
||||
"{} [PANIC] rust_panic: {} ({})",
|
||||
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
|
||||
message,
|
||||
location
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
let logger = Box::new(FileLogger { file });
|
||||
unsafe {
|
||||
log::set_logger_racy(Box::leak(logger))?;
|
||||
}
|
||||
log::set_max_level(LevelFilter::Debug);
|
||||
Ok(())
|
||||
}
|
||||
use chrono;
|
||||
use log::{ LevelFilter, SetLoggerError };
|
||||
use std::fs::{ File, OpenOptions };
|
||||
use std::io::Write;
|
||||
use std::panic;
|
||||
|
||||
pub struct FileLogger {
|
||||
file: File,
|
||||
}
|
||||
|
||||
impl log::Log for FileLogger {
|
||||
fn enabled(&self, _metadata: &log::Metadata) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn log(&self, record: &log::Record) {
|
||||
if self.enabled(record.metadata()) {
|
||||
let mut file = self.file.try_clone().expect("Failed to clone file handle");
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
"{} [{:<5}] {}: {} ({}:{})",
|
||||
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
|
||||
record.level(),
|
||||
record.target(),
|
||||
record.args(),
|
||||
record.file().unwrap_or("unknown"),
|
||||
record.line().unwrap_or(0)
|
||||
).expect("Failed to write to log file");
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&self) {
|
||||
self.file.sync_all().expect("Failed to flush log file");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_logger(app_data_dir: &std::path::Path) -> Result<(), SetLoggerError> {
|
||||
let logs_dir = app_data_dir.join("logs");
|
||||
std::fs::create_dir_all(&logs_dir).expect("Failed to create logs directory");
|
||||
|
||||
let log_path = logs_dir.join("app.log");
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&log_path)
|
||||
.expect("Failed to open log file");
|
||||
|
||||
let panic_file = file.try_clone().expect("Failed to clone file handle");
|
||||
panic::set_hook(
|
||||
Box::new(move |panic_info| {
|
||||
let mut file = panic_file.try_clone().expect("Failed to clone file handle");
|
||||
|
||||
let location = panic_info
|
||||
.location()
|
||||
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
|
||||
.unwrap_or_else(|| "unknown location".to_string());
|
||||
|
||||
let message = match panic_info.payload().downcast_ref::<&str>() {
|
||||
Some(s) => *s,
|
||||
None =>
|
||||
match panic_info.payload().downcast_ref::<String>() {
|
||||
Some(s) => s.as_str(),
|
||||
None => "Unknown panic message",
|
||||
}
|
||||
};
|
||||
|
||||
let _ = writeln!(
|
||||
file,
|
||||
"{} [PANIC] rust_panic: {} ({})",
|
||||
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
|
||||
message,
|
||||
location
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
let logger = Box::new(FileLogger { file });
|
||||
unsafe {
|
||||
log::set_logger_racy(Box::leak(logger))?;
|
||||
}
|
||||
log::set_max_level(LevelFilter::Debug);
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
pub mod commands;
|
||||
pub mod favicon;
|
||||
pub mod types;
|
||||
pub mod logger;
|
||||
pub mod keys;
|
||||
pub mod commands;
|
||||
pub mod favicon;
|
||||
pub mod types;
|
||||
pub mod logger;
|
||||
pub mod keys;
|
||||
|
|
|
@ -1,155 +1,155 @@
|
|||
use chrono::{ DateTime, Utc };
|
||||
use serde::{ Deserialize, Serialize };
|
||||
use std::fmt;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
|
||||
pub struct HistoryItem {
|
||||
pub id: String,
|
||||
pub source: String,
|
||||
pub source_icon: Option<String>,
|
||||
pub content_type: ContentType,
|
||||
pub content: String,
|
||||
pub favicon: Option<String>,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub language: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ContentType {
|
||||
Text,
|
||||
Image,
|
||||
File,
|
||||
Link,
|
||||
Color,
|
||||
Code,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoText {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub characters: i32,
|
||||
pub words: i32,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoImage {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub dimensions: String,
|
||||
pub size: i64,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoFile {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub path: String,
|
||||
pub filesize: i64,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoLink {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub title: Option<String>,
|
||||
pub url: String,
|
||||
pub characters: i32,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoColor {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub hex: String,
|
||||
pub rgb: String,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoCode {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub language: String,
|
||||
pub lines: i32,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl fmt::Display for ContentType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
ContentType::Text => write!(f, "text"),
|
||||
ContentType::Image => write!(f, "image"),
|
||||
ContentType::File => write!(f, "file"),
|
||||
ContentType::Link => write!(f, "link"),
|
||||
ContentType::Color => write!(f, "color"),
|
||||
ContentType::Code => write!(f, "code"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ContentType {
|
||||
fn from(s: String) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"text" => ContentType::Text,
|
||||
"image" => ContentType::Image,
|
||||
"file" => ContentType::File,
|
||||
"link" => ContentType::Link,
|
||||
"color" => ContentType::Color,
|
||||
"code" => ContentType::Code,
|
||||
_ => ContentType::Text,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HistoryItem {
|
||||
pub fn new(
|
||||
source: String,
|
||||
content_type: ContentType,
|
||||
content: String,
|
||||
favicon: Option<String>,
|
||||
source_icon: Option<String>,
|
||||
language: Option<String>
|
||||
) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
source,
|
||||
source_icon,
|
||||
content_type,
|
||||
content,
|
||||
favicon,
|
||||
timestamp: Utc::now(),
|
||||
language,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_row(
|
||||
&self
|
||||
) -> (
|
||||
String,
|
||||
String,
|
||||
Option<String>,
|
||||
String,
|
||||
String,
|
||||
Option<String>,
|
||||
DateTime<Utc>,
|
||||
Option<String>,
|
||||
) {
|
||||
(
|
||||
self.id.clone(),
|
||||
self.source.clone(),
|
||||
self.source_icon.clone(),
|
||||
self.content_type.to_string(),
|
||||
self.content.clone(),
|
||||
self.favicon.clone(),
|
||||
self.timestamp,
|
||||
self.language.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
use chrono::{ DateTime, Utc };
|
||||
use serde::{ Deserialize, Serialize };
|
||||
use std::fmt;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
|
||||
pub struct HistoryItem {
|
||||
pub id: String,
|
||||
pub source: String,
|
||||
pub source_icon: Option<String>,
|
||||
pub content_type: ContentType,
|
||||
pub content: String,
|
||||
pub favicon: Option<String>,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub language: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ContentType {
|
||||
Text,
|
||||
Image,
|
||||
File,
|
||||
Link,
|
||||
Color,
|
||||
Code,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoText {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub characters: i32,
|
||||
pub words: i32,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoImage {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub dimensions: String,
|
||||
pub size: i64,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoFile {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub path: String,
|
||||
pub filesize: i64,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoLink {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub title: Option<String>,
|
||||
pub url: String,
|
||||
pub characters: i32,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoColor {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub hex: String,
|
||||
pub rgb: String,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoCode {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub language: String,
|
||||
pub lines: i32,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl fmt::Display for ContentType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
ContentType::Text => write!(f, "text"),
|
||||
ContentType::Image => write!(f, "image"),
|
||||
ContentType::File => write!(f, "file"),
|
||||
ContentType::Link => write!(f, "link"),
|
||||
ContentType::Color => write!(f, "color"),
|
||||
ContentType::Code => write!(f, "code"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ContentType {
|
||||
fn from(s: String) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"text" => ContentType::Text,
|
||||
"image" => ContentType::Image,
|
||||
"file" => ContentType::File,
|
||||
"link" => ContentType::Link,
|
||||
"color" => ContentType::Color,
|
||||
"code" => ContentType::Code,
|
||||
_ => ContentType::Text,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HistoryItem {
|
||||
pub fn new(
|
||||
source: String,
|
||||
content_type: ContentType,
|
||||
content: String,
|
||||
favicon: Option<String>,
|
||||
source_icon: Option<String>,
|
||||
language: Option<String>
|
||||
) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
source,
|
||||
source_icon,
|
||||
content_type,
|
||||
content,
|
||||
favicon,
|
||||
timestamp: Utc::now(),
|
||||
language,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_row(
|
||||
&self
|
||||
) -> (
|
||||
String,
|
||||
String,
|
||||
Option<String>,
|
||||
String,
|
||||
String,
|
||||
Option<String>,
|
||||
DateTime<Utc>,
|
||||
Option<String>,
|
||||
) {
|
||||
(
|
||||
self.id.clone(),
|
||||
self.source.clone(),
|
||||
self.source_icon.clone(),
|
||||
self.content_type.to_string(),
|
||||
self.content.clone(),
|
||||
self.favicon.clone(),
|
||||
self.timestamp,
|
||||
self.language.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,58 +1,58 @@
|
|||
{
|
||||
"productName": "Qopy",
|
||||
"version": "0.4.0",
|
||||
"identifier": "net.pandadev.qopy",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
"devUrl": "http://localhost:3000",
|
||||
"beforeDevCommand": "pnpm nuxt dev",
|
||||
"beforeBuildCommand": "pnpm nuxt generate"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Qopy",
|
||||
"titleBarStyle": "Overlay",
|
||||
"fullscreen": false,
|
||||
"resizable": false,
|
||||
"height": 474,
|
||||
"width": 750,
|
||||
"minHeight": 474,
|
||||
"maxHeight": 474,
|
||||
"minWidth": 750,
|
||||
"maxWidth": 750,
|
||||
"decorations": false,
|
||||
"center": true,
|
||||
"shadow": false,
|
||||
"transparent": true,
|
||||
"skipTaskbar": true,
|
||||
"alwaysOnTop": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
"withGlobalTauri": true,
|
||||
"macOSPrivateApi": true
|
||||
},
|
||||
"bundle": {
|
||||
"createUpdaterArtifacts": true,
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"category": "DeveloperTool"
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExNDIzNjA1QjE0NjU1OTkKUldTWlZVYXhCVFpDRWNvNmt0UE5lQmZkblEyZGZiZ2tHelJvT2YvNVpLU1RIM1RKZFQrb2tzWWwK",
|
||||
"endpoints": ["https://qopy.pandadev.net/"]
|
||||
}
|
||||
},
|
||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json"
|
||||
}
|
||||
{
|
||||
"productName": "Qopy",
|
||||
"version": "0.4.0",
|
||||
"identifier": "net.pandadev.qopy",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
"devUrl": "http://localhost:3000",
|
||||
"beforeDevCommand": "pnpm nuxt dev",
|
||||
"beforeBuildCommand": "pnpm nuxt generate"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Qopy",
|
||||
"titleBarStyle": "Overlay",
|
||||
"fullscreen": false,
|
||||
"resizable": false,
|
||||
"height": 474,
|
||||
"width": 750,
|
||||
"minHeight": 474,
|
||||
"maxHeight": 474,
|
||||
"minWidth": 750,
|
||||
"maxWidth": 750,
|
||||
"decorations": false,
|
||||
"center": true,
|
||||
"shadow": false,
|
||||
"transparent": true,
|
||||
"skipTaskbar": true,
|
||||
"alwaysOnTop": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
"withGlobalTauri": true,
|
||||
"macOSPrivateApi": true
|
||||
},
|
||||
"bundle": {
|
||||
"createUpdaterArtifacts": true,
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"category": "DeveloperTool"
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExNDIzNjA1QjE0NjU1OTkKUldTWlZVYXhCVFpDRWNvNmt0UE5lQmZkblEyZGZiZ2tHelJvT2YvNVpLU1RIM1RKZFQrb2tzWWwK",
|
||||
"endpoints": ["https://qopy.pandadev.net/"]
|
||||
}
|
||||
},
|
||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json"
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue