refactor: Clean up code formatting in utils module

This commit is contained in:
PandaDEV 2025-01-04 11:58:52 +10:00
parent 60b670c7a7
commit 22fcd84b8d
No known key found for this signature in database
GPG key ID: 13EFF9BAF70EE75C
14 changed files with 485 additions and 394 deletions

View file

@ -1,14 +1,14 @@
use tauri_plugin_aptabase::EventTracker;
use base64::{engine::general_purpose::STANDARD, Engine};
use base64::{ engine::general_purpose::STANDARD, Engine };
// use hyperpolyglot;
use lazy_static::lazy_static;
use rdev::{simulate, EventType, Key};
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 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;
@ -17,7 +17,7 @@ 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};
use crate::utils::types::{ ContentType, HistoryItem };
lazy_static! {
static ref IS_PROGRAMMATIC_PASTE: AtomicBool = AtomicBool::new(false);
@ -27,16 +27,14 @@ lazy_static! {
pub async fn write_and_paste(
app_handle: AppHandle,
content: String,
content_type: 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())?,
"image" => {
clipboard
.write_image_base64(content)
.map_err(|e| e.to_string())?;
clipboard.write_image_base64(content).map_err(|e| e.to_string())?;
}
"files" => {
clipboard
@ -44,11 +42,13 @@ pub async fn write_and_paste(
content
.split(", ")
.map(|file| file.to_string())
.collect::<Vec<String>>(),
.collect::<Vec<String>>()
)
.map_err(|e| e.to_string())?;
}
_ => return Err("Unsupported content type".to_string()),
_ => {
return Err("Unsupported content type".to_string());
}
}
IS_PROGRAMMATIC_PASTE.store(true, Ordering::SeqCst);
@ -65,7 +65,7 @@ pub async fn write_and_paste(
EventType::KeyPress(modifier_key),
EventType::KeyPress(Key::KeyV),
EventType::KeyRelease(Key::KeyV),
EventType::KeyRelease(modifier_key),
EventType::KeyRelease(modifier_key)
];
for event in events {
@ -81,9 +81,12 @@ pub async fn write_and_paste(
IS_PROGRAMMATIC_PASTE.store(false, Ordering::SeqCst);
});
let _ = app_handle.track_event("clipboard_paste", Some(serde_json::json!({
let _ = app_handle.track_event(
"clipboard_paste",
Some(serde_json::json!({
"content_type": content_type
})));
}))
);
Ok(())
}
@ -92,79 +95,92 @@ 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;
}
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 clipboard = app_handle.state::<Clipboard>();
let available_types = clipboard.available_types().unwrap();
let (app_name, app_icon) = get_app_info();
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);
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,
HistoryItem::new(app_name, ContentType::Image, file_path, None, app_icon, None)
pool.clone(),
HistoryItem::new(
app_name.clone(),
ContentType::File,
file,
None,
app_icon.clone(),
None
)
).await;
}
} else if available_types.files {
println!("Handling files change");
if let Ok(files) = clipboard.read_files() {
for file in files {
}
} 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.clone(),
pool,
HistoryItem::new(
app_name.clone(),
ContentType::File,
file,
None,
app_icon.clone(),
app_name,
ContentType::Link,
text,
favicon,
app_icon,
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();
} else {
if text.is_empty() {
return;
}
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) {
// 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(),
@ -175,43 +191,61 @@ pub fn setup(app: &AppHandle) {
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;
}
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);
} else {
println!("Unknown clipboard content type");
}
}
Err(e) => {
println!("Failed to get database pool: {}", e);
}
}
let _ = app_handle.emit("clipboard-content-updated", ());
let _ = app_handle.track_event("clipboard_copied", Some(serde_json::json!({
let _ = app_handle.emit("clipboard-content-updated", ());
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,
app_handle: &AppHandle
) -> Result<tauri::State<'_, SqlitePool>, Box<dyn std::error::Error + Send + Sync>> {
Ok(app_handle.state::<SqlitePool>())
}
@ -219,9 +253,7 @@ async fn get_pool(
#[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())?;
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())?;
@ -230,7 +262,7 @@ pub fn start_monitor(app_handle: AppHandle) -> Result<(), String> {
async fn save_image_to_file(
app_handle: &AppHandle,
base64_data: &str,
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");

View file

@ -1,13 +1,15 @@
use crate::utils::commands::center_window_on_current_monitor;
use crate::utils::keys::KeyCode;
use global_hotkey::{
hotkey::{Code, HotKey, Modifiers},
GlobalHotKeyEvent, GlobalHotKeyManager, HotKeyState,
hotkey::{ Code, HotKey, Modifiers },
GlobalHotKeyEvent,
GlobalHotKeyManager,
HotKeyState,
};
use lazy_static::lazy_static;
use std::str::FromStr;
use std::sync::Mutex;
use tauri::{AppHandle, Listener, Manager};
use tauri::{ AppHandle, Listener, Manager };
use tauri_plugin_aptabase::EventTracker;
lazy_static! {
@ -110,10 +112,18 @@ fn parse_hotkey(shortcut: &[String]) -> Result<HotKey, Box<dyn std::error::Error
for part in shortcut {
match part.as_str() {
"ControlLeft" => modifiers |= Modifiers::CONTROL,
"AltLeft" => modifiers |= Modifiers::ALT,
"ShiftLeft" => modifiers |= Modifiers::SHIFT,
"MetaLeft" => modifiers |= Modifiers::META,
"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)?));
}
@ -144,8 +154,10 @@ fn handle_hotkey_event(app_handle: &AppHandle) {
let _ = app_handle.track_event(
"hotkey_triggered",
Some(serde_json::json!({
Some(
serde_json::json!({
"action": if window.is_visible().unwrap() { "hide" } else { "show" }
})),
})
)
);
}

View file

@ -1,16 +1,15 @@
use tauri::{
menu::{MenuBuilder, MenuItemBuilder},
tray::TrayIconBuilder,
Emitter, Manager,
};
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!({
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();
@ -18,37 +17,42 @@ pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
let _tray = TrayIconBuilder::new()
.menu(
&MenuBuilder::new(app)
.items(&[&MenuItemBuilder::with_id("app_name", "Qopy")
.enabled(false)
.build(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()?,
.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();
.on_menu_event(move |_app, event| {
match event.id().as_ref() {
"quit" => {
let _ = _app.track_event("app_quit", None);
std::process::exit(0);
}
window.emit("main_route", ()).unwrap();
"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();
}
_ => (),
}
"settings" => {
let _ = _app.track_event("tray_settings", None);
window.emit("settings", ()).unwrap();
}
_ => (),
})
.icon(icon)
.build(app)?;

View file

@ -1,5 +1,5 @@
use tauri::{async_runtime, AppHandle};
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
use tauri::{ async_runtime, AppHandle };
use tauri_plugin_dialog::{ DialogExt, MessageDialogButtons, MessageDialogKind };
use tauri_plugin_updater::UpdaterExt;
pub async fn check_for_updates(app: AppHandle) {
@ -21,18 +21,35 @@ pub async fn check_for_updates(app: AppHandle) {
app.dialog()
.message(msg)
.title("Qopy Update Available")
.buttons(MessageDialogButtons::OkCancelCustom(String::from("Install"), String::from("Cancel")))
.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 {
match
update.download_and_install(
|_, _| {},
|| {}
).await
{
Ok(_) => {
app.dialog()
.message("Update installed successfully. The application needs to restart to apply the changes.")
.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")))
.buttons(
MessageDialogButtons::OkCancelCustom(
String::from("Restart"),
String::from("Cancel")
)
)
.show(move |response| {
if response {
app.restart();
@ -42,7 +59,9 @@ pub async fn check_for_updates(app: AppHandle) {
Err(e) => {
println!("Error installing new update: {:?}", e);
app.dialog()
.message("Failed to install new update. The new update can be downloaded from Github")
.message(
"Failed to install new update. The new update can be downloaded from Github"
)
.kind(MessageDialogKind::Error)
.show(|_| {});
}

View file

@ -1,5 +1,5 @@
use include_dir::{include_dir, Dir};
use sqlx::sqlite::{SqlitePool, SqlitePoolOptions};
use include_dir::{ include_dir, Dir };
use sqlx::sqlite::{ SqlitePool, SqlitePoolOptions };
use std::fs;
use tauri::Manager;
use tokio::runtime::Runtime as TokioRuntime;
@ -25,8 +25,7 @@ pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
let pool = rt.block_on(async {
SqlitePoolOptions::new()
.max_connections(5)
.connect(&db_url)
.await
.connect(&db_url).await
.expect("Failed to create pool")
});
@ -49,24 +48,22 @@ pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
}
async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
sqlx::query(
"CREATE TABLE IF NOT EXISTS schema_version (
sqlx
::query(
"CREATE TABLE IF NOT EXISTS schema_version (
version INTEGER PRIMARY KEY,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
);",
)
.execute(pool)
.await?;
);"
)
.execute(pool).await?;
let current_version: Option<i64> =
sqlx::query_scalar("SELECT MAX(version) FROM schema_version")
.fetch_one(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()
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") {
@ -93,16 +90,16 @@ async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box<dyn std::error::E
.collect();
for statement in statements {
sqlx::query(statement)
.execute(pool)
.await
sqlx
::query(statement)
.execute(pool).await
.map_err(|e| format!("Failed to execute migration {}: {}", version, e))?;
}
sqlx::query("INSERT INTO schema_version (version) VALUES (?)")
sqlx
::query("INSERT INTO schema_version (version) VALUES (?)")
.bind(version)
.execute(pool)
.await?;
.execute(pool).await?;
}
}

View file

@ -1,39 +1,35 @@
use crate::utils::types::{ContentType, HistoryItem};
use base64::{engine::general_purpose::STANDARD, Engine};
use crate::utils::types::{ ContentType, HistoryItem };
use base64::{ engine::general_purpose::STANDARD, Engine };
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use sqlx::{Row, SqlitePool};
use rand::{ thread_rng, Rng };
use sqlx::{ Row, SqlitePool };
use std::fs;
use tauri_plugin_aptabase::EventTracker;
pub async fn initialize_history(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
let id: String = thread_rng()
.sample_iter(&Alphanumeric)
.take(16)
.map(char::from)
.collect();
let id: String = thread_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?;
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 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()
@ -56,50 +52,53 @@ pub async fn get_history(pool: tauri::State<'_, SqlitePool>) -> Result<Vec<Histo
pub async fn add_history_item(
app_handle: tauri::AppHandle,
pool: tauri::State<'_, SqlitePool>,
item: HistoryItem,
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 = ?")
let existing = sqlx
::query("SELECT id FROM history WHERE content = ? AND content_type = ?")
.bind(&content)
.bind(&content_type)
.fetch_optional(&*pool)
.await
.fetch_optional(&*pool).await
.map_err(|e| e.to_string())?;
match existing {
Some(_) => {
sqlx::query(
"UPDATE history SET timestamp = strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now') WHERE content = ? AND content_type = ?"
)
.bind(&content)
.bind(&content_type)
.execute(&*pool)
.await
.map_err(|e| e.to_string())?;
sqlx
::query(
"UPDATE history SET timestamp = strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now') WHERE content = ? AND content_type = ?"
)
.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())?;
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!({
let _ = app_handle.track_event(
"history_item_added",
Some(serde_json::json!({
"content_type": item.content_type.to_string()
})));
}))
);
Ok(())
}
@ -107,16 +106,16 @@ pub async fn add_history_item(
#[tauri::command]
pub async fn search_history(
pool: tauri::State<'_, SqlitePool>,
query: String,
query: String
) -> Result<Vec<HistoryItem>, String> {
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"
)
.bind(query)
.fetch_all(&*pool)
.await
.map_err(|e| e.to_string())?;
let rows = sqlx
::query(
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history WHERE content LIKE ? ORDER BY timestamp DESC"
)
.bind(query)
.fetch_all(&*pool).await
.map_err(|e| e.to_string())?;
let items = rows
.iter()
@ -139,16 +138,16 @@ pub async fn search_history(
pub async fn load_history_chunk(
pool: tauri::State<'_, SqlitePool>,
offset: i64,
limit: 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 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()
@ -171,12 +170,12 @@ pub async fn load_history_chunk(
pub async fn delete_history_item(
app_handle: tauri::AppHandle,
pool: tauri::State<'_, SqlitePool>,
id: String,
id: String
) -> Result<(), String> {
sqlx::query("DELETE FROM history WHERE id = ?")
sqlx
::query("DELETE FROM history WHERE id = ?")
.bind(id)
.execute(&*pool)
.await
.execute(&*pool).await
.map_err(|e| e.to_string())?;
let _ = app_handle.track_event("history_item_deleted", None);
@ -189,9 +188,9 @@ pub async fn clear_history(
app_handle: tauri::AppHandle,
pool: tauri::State<'_, SqlitePool>
) -> Result<(), String> {
sqlx::query("DELETE FROM history")
.execute(&*pool)
.await
sqlx
::query("DELETE FROM history")
.execute(&*pool).await
.map_err(|e| e.to_string())?;
let _ = app_handle.track_event("history_cleared", None);

View file

@ -1,8 +1,8 @@
use serde::{Deserialize, Serialize};
use serde::{ Deserialize, Serialize };
use serde_json;
use sqlx::Row;
use sqlx::SqlitePool;
use tauri::{Emitter, Manager};
use tauri::{ Emitter, Manager };
use tauri_plugin_aptabase::EventTracker;
#[derive(Deserialize, Serialize)]
@ -16,10 +16,10 @@ pub async fn initialize_settings(pool: &SqlitePool) -> Result<(), Box<dyn std::e
};
let json = serde_json::to_string(&default_keybind)?;
sqlx::query("INSERT INTO settings (key, value) VALUES ('keybind', ?)")
sqlx
::query("INSERT INTO settings (key, value) VALUES ('keybind', ?)")
.bind(json)
.execute(pool)
.await?;
.execute(pool).await?;
Ok(())
}
@ -28,23 +28,24 @@ pub async fn initialize_settings(pool: &SqlitePool) -> Result<(), Box<dyn std::e
pub async fn save_keybind(
app_handle: tauri::AppHandle,
pool: tauri::State<'_, SqlitePool>,
keybind: Vec<String>,
keybind: Vec<String>
) -> Result<(), String> {
app_handle
.emit("update-shortcut", &keybind)
.map_err(|e| e.to_string())?;
app_handle.emit("update-shortcut", &keybind).map_err(|e| e.to_string())?;
let json = serde_json::to_string(&keybind).map_err(|e| e.to_string())?;
sqlx::query("INSERT OR REPLACE INTO settings (key, value) VALUES ('keybind', ?)")
sqlx
::query("INSERT OR REPLACE INTO settings (key, value) VALUES ('keybind', ?)")
.bind(json)
.execute(&*pool)
.await
.execute(&*pool).await
.map_err(|e| e.to_string())?;
let _ = app_handle.track_event("keybind_saved", Some(serde_json::json!({
let _ = app_handle.track_event(
"keybind_saved",
Some(serde_json::json!({
"keybind": keybind
})));
}))
);
Ok(())
}
@ -52,12 +53,12 @@ pub async fn save_keybind(
#[tauri::command]
pub async fn get_setting(
pool: tauri::State<'_, SqlitePool>,
key: String,
key: String
) -> Result<String, String> {
let row = sqlx::query("SELECT value FROM settings WHERE key = ?")
let row = sqlx
::query("SELECT value FROM settings WHERE key = ?")
.bind(key)
.fetch_optional(&*pool)
.await
.fetch_optional(&*pool).await
.map_err(|e| e.to_string())?;
Ok(row.map(|r| r.get("value")).unwrap_or_default())
@ -68,18 +69,21 @@ pub async fn save_setting(
app_handle: tauri::AppHandle,
pool: tauri::State<'_, SqlitePool>,
key: String,
value: String,
value: String
) -> Result<(), String> {
sqlx::query("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)")
sqlx
::query("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)")
.bind(key.clone())
.bind(value)
.execute(&*pool)
.await
.execute(&*pool).await
.map_err(|e| e.to_string())?;
let _ = app_handle.track_event("setting_saved", Some(serde_json::json!({
let _ = app_handle.track_event(
"setting_saved",
Some(serde_json::json!({
"key": key
})));
}))
);
Ok(())
}
@ -88,15 +92,18 @@ pub async fn save_setting(
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
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")
});
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())
}

View file

@ -1,7 +1,4 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
#![cfg_attr(all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows")]
mod api;
mod db;
@ -10,7 +7,7 @@ mod utils;
use sqlx::sqlite::SqlitePoolOptions;
use std::fs;
use tauri::Manager;
use tauri_plugin_aptabase::{EventTracker, InitOptions};
use tauri_plugin_aptabase::{ EventTracker, InitOptions };
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_prevent_default::Flags;
@ -18,7 +15,8 @@ fn main() {
let runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime");
let _guard = runtime.enter();
tauri::Builder::default()
tauri::Builder
::default()
.plugin(tauri_plugin_clipboard::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_sql::Builder::default().build())
@ -26,34 +24,37 @@ fn main() {
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_updater::Builder::default().build())
.plugin(
tauri_plugin_aptabase::Builder::new("A-SH-8937252746")
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());
.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!({
let _ = client.track_event(
"panic",
Some(
serde_json::json!({
"info": format!("{} ({})", msg, location),
})),
);
}))
.build(),
})
)
);
})
)
.build()
)
.plugin(tauri_plugin_autostart::init(
MacosLauncher::LaunchAgent,
Some(vec![]),
))
.plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, Some(vec![])))
.plugin(
tauri_plugin_prevent_default::Builder::new()
tauri_plugin_prevent_default::Builder
::new()
.with_flags(Flags::all().difference(Flags::CONTEXT_MENU))
.build(),
.build()
)
.setup(|app| {
let app_data_dir = app.path().app_data_dir().unwrap();
@ -75,8 +76,7 @@ fn main() {
tauri::async_runtime::spawn(async move {
let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect(&db_url)
.await
.connect(&db_url).await
.expect("Failed to create pool");
app_handle_clone.manage(pool);
@ -91,7 +91,10 @@ fn main() {
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(()))?;
main_window
.as_ref()
.map(|w| w.hide())
.unwrap_or(Ok(()))?;
let _ = app.track_event("app_started", None);
@ -109,21 +112,23 @@ fn main() {
}
}
})
.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,
db::settings::save_keybind,
db::settings::get_keybind,
utils::commands::fetch_page_meta,
])
.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,
db::settings::save_keybind,
db::settings::get_keybind,
utils::commands::fetch_page_meta
]
)
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View file

@ -1,34 +1,37 @@
use active_win_pos_rs::get_active_window;
use base64::{engine::general_purpose::STANDARD, Engine};
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()
if
let Some(monitor) = window
.available_monitors()
.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
}) {
.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;
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,
))
.set_position(PhysicalPosition::new(monitor.position().x + x, monitor.position().y + y))
.unwrap();
}
}
@ -51,59 +54,64 @@ fn _process_icon_to_base64(path: &str) -> Result<String, Box<dyn std::error::Err
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
_ => false,
};
}
// rgb/rgba
if (color.starts_with("rgb(") || color.starts_with("rgba(")) && color.ends_with(")") && !color[..color.len()-1].contains(")") {
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
_ => false,
};
}
// hsl/hsla
if (color.starts_with("hsl(") || color.starts_with("hsla(")) && color.ends_with(")") && !color[..color.len()-1].contains(")") {
// 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,
};
}
false
}
#[tauri::command]
pub async fn fetch_page_meta(url: String) -> Result<(String, Option<String>), String> {
let metadata = meta_fetcher::fetch_metadata(&url)
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
))
}
Ok((metadata.title.unwrap_or_else(|| "No title found".to_string()), metadata.image))
}

View file

@ -5,7 +5,7 @@ use reqwest;
use url::Url;
pub async fn fetch_favicon_as_base64(
url: Url,
url: Url
) -> Result<Option<String>, Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let favicon_url = format!("https://favicone.com/{}", url.host_str().unwrap());

View file

@ -105,7 +105,9 @@ impl FromStr for KeyCode {
"F10" => Code::F10,
"F11" => Code::F11,
"F12" => Code::F12,
_ => return Err(format!("Unknown key code: {}", s)),
_ => {
return Err(format!("Unknown key code: {}", s));
}
};
Ok(KeyCode(code))
}

View file

@ -1,6 +1,6 @@
use chrono;
use log::{LevelFilter, SetLoggerError};
use std::fs::{File, OpenOptions};
use log::{ LevelFilter, SetLoggerError };
use std::fs::{ File, OpenOptions };
use std::io::Write;
use std::panic;
@ -16,7 +16,7 @@ impl log::Log for FileLogger {
fn log(&self, record: &log::Record) {
if self.enabled(record.metadata()) {
let mut file = self.file.try_clone().expect("Failed to clone file handle");
// Format: timestamp [LEVEL] target: message (file:line)
writeln!(
file,
@ -50,32 +50,38 @@ pub fn init_logger(app_data_dir: &std::path::Path) -> Result<(), SetLoggerError>
// Set up panic hook
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());
panic::set_hook(
Box::new(move |panic_info| {
let mut file = panic_file.try_clone().expect("Failed to clone file handle");
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 location = panic_info
.location()
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
.unwrap_or_else(|| "unknown location".to_string());
let _ = writeln!(
file,
"{} [PANIC] rust_panic: {} ({})",
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
message,
location
);
}));
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))? };
unsafe {
log::set_logger_racy(Box::leak(logger))?;
}
log::set_max_level(LevelFilter::Debug);
Ok(())
}
}

View file

@ -2,4 +2,4 @@ pub mod commands;
pub mod favicon;
pub mod types;
pub mod logger;
pub mod keys;
pub mod keys;

View file

@ -1,5 +1,5 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use chrono::{ DateTime, Utc };
use serde::{ Deserialize, Serialize };
use std::fmt;
use uuid::Uuid;
@ -115,7 +115,7 @@ impl HistoryItem {
content: String,
favicon: Option<String>,
source_icon: Option<String>,
language: Option<String>,
language: Option<String>
) -> Self {
Self {
id: Uuid::new_v4().to_string(),
@ -130,7 +130,7 @@ impl HistoryItem {
}
pub fn to_row(
&self,
&self
) -> (
String,
String,