mirror of
https://github.com/0PandaDEV/Qopy.git
synced 2025-04-22 05:34:04 +02:00
feat: move database logic to new structure
This commit is contained in:
parent
add822073e
commit
c60f173e29
10 changed files with 198 additions and 352 deletions
|
@ -1,37 +1,25 @@
|
|||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine;
|
||||
use image::ImageFormat;
|
||||
use lazy_static::lazy_static;
|
||||
use rand::Rng;
|
||||
use rdev::{simulate, EventType, Key};
|
||||
use regex::Regex;
|
||||
use sha2::{Digest, Sha256};
|
||||
use sqlx::SqlitePool;
|
||||
use uuid::Uuid;
|
||||
use std::fs;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::{fs, sync::Mutex, thread, time::Duration};
|
||||
use std::{thread, time::Duration};
|
||||
use tauri::{AppHandle, Emitter, Listener, Manager, Runtime};
|
||||
use tauri_plugin_clipboard::Clipboard;
|
||||
use tokio::runtime::Runtime as TokioRuntime;
|
||||
use regex::Regex;
|
||||
use url::Url;
|
||||
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||
|
||||
use crate::utils::favicon::fetch_favicon_as_base64;
|
||||
use crate::db;
|
||||
use crate::utils::types::{ContentType, HistoryItem};
|
||||
|
||||
lazy_static! {
|
||||
static ref APP_DATA_DIR: Mutex<Option<std::path::PathBuf>> = Mutex::new(None);
|
||||
static ref IS_PROGRAMMATIC_PASTE: AtomicBool = AtomicBool::new(false);
|
||||
}
|
||||
|
||||
pub fn set_app_data_dir(path: std::path::PathBuf) {
|
||||
let mut dir = APP_DATA_DIR.lock().unwrap();
|
||||
*dir = Some(path);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn read_image(filename: String) -> Result<String, String> {
|
||||
let app_data_dir = APP_DATA_DIR.lock().unwrap();
|
||||
let app_data_dir = app_data_dir.as_ref().expect("App data directory not set");
|
||||
let image_path = app_data_dir.join("images").join(filename);
|
||||
let image_data = fs::read(image_path).map_err(|e| e.to_string())?;
|
||||
Ok(STANDARD.encode(image_data))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn write_and_paste<R: Runtime>(
|
||||
app_handle: tauri::AppHandle<R>,
|
||||
|
@ -93,16 +81,6 @@ pub async fn write_and_paste<R: Runtime>(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_image_path(app_handle: tauri::AppHandle, filename: String) -> String {
|
||||
let app_data_dir = app_handle
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.expect("Failed to get app data directory");
|
||||
let image_path = app_data_dir.join("images").join(filename);
|
||||
image_path.to_str().unwrap_or("").to_string()
|
||||
}
|
||||
|
||||
pub fn setup<R: Runtime>(app: &AppHandle<R>) {
|
||||
let app = app.clone();
|
||||
let runtime = TokioRuntime::new().expect("Failed to create Tokio runtime");
|
||||
|
@ -124,38 +102,49 @@ pub fn setup<R: Runtime>(app: &AppHandle<R>) {
|
|||
if available_types.image {
|
||||
println!("Handling image change");
|
||||
if let Ok(image_data) = clipboard.read_image_base64() {
|
||||
insert_content_if_not_exists(
|
||||
app.clone(),
|
||||
pool.clone(),
|
||||
"image",
|
||||
image_data,
|
||||
)
|
||||
.await;
|
||||
let file_path = save_image_to_file(&app, &image_data)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
.unwrap_or_else(|e| e);
|
||||
let _ = db::history::add_history_item(
|
||||
pool,
|
||||
HistoryItem::new(ContentType::Image, file_path, None),
|
||||
).await;
|
||||
}
|
||||
let _ = app.emit("plugin:clipboard://image-changed", ());
|
||||
} else if available_types.files {
|
||||
println!("Handling files change");
|
||||
if let Ok(files) = clipboard.read_files() {
|
||||
let files_str = files.join(", ");
|
||||
insert_content_if_not_exists(
|
||||
app.clone(),
|
||||
pool.clone(),
|
||||
"files",
|
||||
files_str,
|
||||
)
|
||||
.await;
|
||||
let _ = db::history::add_history_item(
|
||||
pool,
|
||||
HistoryItem::new(ContentType::File, files_str, None),
|
||||
).await;
|
||||
}
|
||||
let _ = app.emit("plugin:clipboard://files-changed", ());
|
||||
} else if available_types.text {
|
||||
println!("Handling text change");
|
||||
if let Ok(text) = clipboard.read_text() {
|
||||
insert_content_if_not_exists(
|
||||
app.clone(),
|
||||
pool.clone(),
|
||||
"text",
|
||||
text,
|
||||
)
|
||||
.await;
|
||||
let 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(
|
||||
pool,
|
||||
HistoryItem::new(ContentType::Link, text, favicon)
|
||||
).await;
|
||||
}
|
||||
} else {
|
||||
let _ = db::history::add_history_item(
|
||||
pool,
|
||||
HistoryItem::new(ContentType::Text, text, None)
|
||||
).await;
|
||||
}
|
||||
}
|
||||
let _ = app.emit("plugin:clipboard://text-changed", ());
|
||||
} else {
|
||||
|
@ -173,130 +162,8 @@ pub fn setup<R: Runtime>(app: &AppHandle<R>) {
|
|||
|
||||
async fn get_pool<R: Runtime>(
|
||||
app_handle: &AppHandle<R>,
|
||||
) -> Result<SqlitePool, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let app_data_dir = app_handle
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.expect("Failed to get app data directory");
|
||||
let db_path = app_data_dir.join("data.db");
|
||||
let database_url = format!("sqlite:{}", db_path.to_str().unwrap());
|
||||
SqlitePool::connect(&database_url)
|
||||
.await
|
||||
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
|
||||
}
|
||||
|
||||
async fn insert_content_if_not_exists<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
pool: SqlitePool,
|
||||
content_type: &str,
|
||||
content: String,
|
||||
) {
|
||||
let last_content: Option<String> = sqlx::query_scalar(
|
||||
"SELECT content FROM history WHERE content_type = ? ORDER BY timestamp DESC LIMIT 1",
|
||||
)
|
||||
.bind(content_type)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
|
||||
let content = if content_type == "image" {
|
||||
match save_image(&app_handle, &content).await {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
println!("Failed to save image: {}", e);
|
||||
content
|
||||
}
|
||||
}
|
||||
} else {
|
||||
content
|
||||
};
|
||||
|
||||
if last_content.as_deref() != Some(&content) {
|
||||
let id: String = rand::thread_rng()
|
||||
.sample_iter(&rand::distributions::Alphanumeric)
|
||||
.take(16)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
let favicon_base64 = if content_type == "text" {
|
||||
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(&content) {
|
||||
match url::Url::parse(&content) {
|
||||
Ok(url) => match fetch_favicon_as_base64(url).await {
|
||||
Ok(Some(favicon)) => Some(favicon),
|
||||
Ok(None) => None,
|
||||
Err(e) => {
|
||||
println!("Failed to fetch favicon: {}", e);
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Failed to parse URL: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let _ = sqlx::query(
|
||||
"INSERT INTO history (id, content_type, content, favicon) VALUES (?, ?, ?, ?)",
|
||||
)
|
||||
.bind(id)
|
||||
.bind(content_type)
|
||||
.bind(&content)
|
||||
.bind(favicon_base64)
|
||||
.execute(&pool)
|
||||
.await;
|
||||
|
||||
let _ = app_handle.emit("clipboard-content-updated", ());
|
||||
}
|
||||
}
|
||||
|
||||
async fn save_image<R: Runtime>(
|
||||
app_handle: &AppHandle<R>,
|
||||
base64_image: &str,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let image_data = STANDARD.decode(base64_image)?;
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&image_data);
|
||||
let hash = hasher.finalize();
|
||||
let filename = format!("{:x}.png", hash);
|
||||
|
||||
let app_data_dir = app_handle
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.expect("Failed to get app data directory");
|
||||
let images_dir = app_data_dir.join("images");
|
||||
let path = images_dir.join(&filename);
|
||||
|
||||
if !path.exists() {
|
||||
fs::create_dir_all(&images_dir)?;
|
||||
fs::write(&path, &image_data)?;
|
||||
}
|
||||
|
||||
Ok(path.to_str().unwrap().to_string())
|
||||
}
|
||||
|
||||
async fn fetch_favicon_as_base64(
|
||||
url: url::Url,
|
||||
) -> Result<Option<String>, Box<dyn std::error::Error>> {
|
||||
let client = reqwest::Client::new();
|
||||
let favicon_url = format!("https://favicone.com/{}", url.host_str().unwrap());
|
||||
let response = client.get(&favicon_url).send().await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let bytes = response.bytes().await?;
|
||||
let img = image::load_from_memory(&bytes)?;
|
||||
let mut png_bytes: Vec<u8> = Vec::new();
|
||||
img.write_to(&mut std::io::Cursor::new(&mut png_bytes), ImageFormat::Png)?;
|
||||
Ok(Some(STANDARD.encode(&png_bytes)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
) -> Result<tauri::State<'_, SqlitePool>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
Ok(app_handle.state::<SqlitePool>())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
@ -310,3 +177,17 @@ pub fn start_monitor(app_handle: AppHandle) -> Result<(), String> {
|
|||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn save_image_to_file<R: Runtime>(app_handle: &AppHandle<R>, 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,161 +0,0 @@
|
|||
use rand::distributions::Alphanumeric;
|
||||
use rand::{thread_rng, Rng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use sqlx::sqlite::{SqlitePool, SqlitePoolOptions};
|
||||
use std::fs;
|
||||
use tauri::{Manager, Emitter};
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct KeybindSetting {
|
||||
keybind: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let rt = Runtime::new().expect("Failed to create Tokio runtime");
|
||||
|
||||
let app_data_dir = app.path().app_data_dir().unwrap();
|
||||
fs::create_dir_all(&app_data_dir).expect("Failed to create app data directory");
|
||||
|
||||
let db_path = app_data_dir.join("data.db");
|
||||
let is_new_db = !db_path.exists();
|
||||
if is_new_db {
|
||||
fs::File::create(&db_path).expect("Failed to create database file");
|
||||
}
|
||||
|
||||
let db_url = format!("sqlite:{}", db_path.to_str().unwrap());
|
||||
let pool = rt.block_on(async {
|
||||
SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&db_url)
|
||||
.await
|
||||
.expect("Failed to create pool")
|
||||
});
|
||||
|
||||
rt.block_on(async {
|
||||
// Setup settings table
|
||||
sqlx::query(
|
||||
"CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
)"
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Failed to create settings table");
|
||||
|
||||
let existing_keybind = sqlx::query_scalar::<_, Option<String>>(
|
||||
"SELECT value FROM settings WHERE key = 'keybind'"
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await;
|
||||
|
||||
match existing_keybind {
|
||||
Ok(Some(_)) => {
|
||||
},
|
||||
Ok(None) => {
|
||||
let default_keybind = KeybindSetting {
|
||||
keybind: vec!["Meta".to_string(), "V".to_string()],
|
||||
};
|
||||
let json = serde_json::to_string(&default_keybind).unwrap();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO settings (key, value) VALUES ('keybind', ?)"
|
||||
)
|
||||
.bind(json)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Failed to insert default keybind");
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to check existing keybind: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Setup history table
|
||||
sqlx::query(
|
||||
"CREATE TABLE IF NOT EXISTS history (
|
||||
id TEXT PRIMARY KEY,
|
||||
content_type TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
favicon TEXT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)"
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Failed to create history table");
|
||||
|
||||
sqlx::query(
|
||||
"CREATE INDEX IF NOT EXISTS idx_timestamp ON history (timestamp)"
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Failed to create index");
|
||||
|
||||
if is_new_db {
|
||||
let id: String = thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(16)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
sqlx::query("INSERT INTO history (id, content_type, content, timestamp) VALUES (?, ?, ?, CURRENT_TIMESTAMP)")
|
||||
.bind(id)
|
||||
.bind("text")
|
||||
.bind("Welcome to your clipboard history!")
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Failed to insert welcome message");
|
||||
}
|
||||
});
|
||||
|
||||
app.manage(pool);
|
||||
app.manage(rt);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_keybind(
|
||||
app_handle: tauri::AppHandle,
|
||||
keybind: Vec<String>,
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
) -> Result<(), String> {
|
||||
let json = serde_json::to_string(&keybind).map_err(|e| e.to_string())?;
|
||||
|
||||
sqlx::query("INSERT OR REPLACE INTO settings (key, value) VALUES ('keybind', ?)")
|
||||
.bind(json)
|
||||
.execute(&*pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let keybind_str = keybind.join("+");
|
||||
app_handle
|
||||
.emit("update-shortcut", keybind_str)
|
||||
.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 result =
|
||||
sqlx::query_scalar::<_, String>("SELECT value FROM settings WHERE key = 'keybind'")
|
||||
.fetch_optional(&*pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
match result {
|
||||
Some(json) => {
|
||||
let keybind: Vec<String> = serde_json::from_str(&json).map_err(|e| e.to_string())?;
|
||||
Ok(keybind)
|
||||
}
|
||||
None => {
|
||||
let default_keybind = vec!["Meta".to_string(), "V".to_string()];
|
||||
Ok(default_keybind)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,7 +18,7 @@ pub fn setup(app_handle: tauri::AppHandle) {
|
|||
HOTKEY_MANAGER.with(|m| *m.borrow_mut() = Some(manager));
|
||||
|
||||
let rt = app_handle.state::<tokio::runtime::Runtime>();
|
||||
let initial_keybind = rt.block_on(crate::api::database::get_keybind(app_handle_clone.clone()))
|
||||
let initial_keybind = rt.block_on(crate::db::settings::get_keybind(app_handle_clone.clone()))
|
||||
.expect("Failed to get initial keybind");
|
||||
let initial_shortcut = initial_keybind.join("+");
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
pub mod updater;
|
||||
pub mod clipboard;
|
||||
pub mod database;
|
||||
pub mod tray;
|
||||
pub mod hotkeys;
|
||||
|
|
63
src-tauri/src/db/database.rs
Normal file
63
src-tauri/src/db/database.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
use sqlx::sqlite::{SqlitePool, SqlitePoolOptions};
|
||||
use std::fs;
|
||||
use tauri::Manager;
|
||||
use tokio::runtime::Runtime as TokioRuntime;
|
||||
|
||||
pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let rt = TokioRuntime::new().expect("Failed to create Tokio runtime");
|
||||
|
||||
let app_data_dir = app.path().app_data_dir().unwrap();
|
||||
fs::create_dir_all(&app_data_dir).expect("Failed to create app data directory");
|
||||
|
||||
let db_path = app_data_dir.join("data.db");
|
||||
let is_new_db = !db_path.exists();
|
||||
if is_new_db {
|
||||
fs::File::create(&db_path).expect("Failed to create database file");
|
||||
}
|
||||
|
||||
let db_url = format!("sqlite:{}", db_path.to_str().unwrap());
|
||||
let pool = rt.block_on(async {
|
||||
SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&db_url)
|
||||
.await
|
||||
.expect("Failed to create pool")
|
||||
});
|
||||
|
||||
rt.block_on(async {
|
||||
apply_schema(&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>>(())
|
||||
})?;
|
||||
|
||||
app.manage(pool);
|
||||
app.manage(rt);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn apply_schema(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let schema = include_str!("scheme.sql");
|
||||
|
||||
let statements: Vec<&str> = schema
|
||||
.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 schema statement: {}", e))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
3
src-tauri/src/db/mod.rs
Normal file
3
src-tauri/src/db/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod database;
|
||||
pub mod history;
|
||||
pub mod settings;
|
12
src-tauri/src/db/scheme.sql
Normal file
12
src-tauri/src/db/scheme.sql
Normal file
|
@ -0,0 +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
|
||||
);
|
|
@ -4,8 +4,11 @@
|
|||
)]
|
||||
|
||||
mod api;
|
||||
mod db;
|
||||
mod utils;
|
||||
|
||||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
use std::fs;
|
||||
use tauri::Manager;
|
||||
use tauri::WebviewUrl;
|
||||
use tauri::WebviewWindow;
|
||||
|
@ -30,8 +33,30 @@ fn main() {
|
|||
.build(),
|
||||
)
|
||||
.setup(|app| {
|
||||
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 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 = if let Some(window) = app.get_webview_window("main") {
|
||||
window
|
||||
} else {
|
||||
|
@ -49,7 +74,7 @@ fn main() {
|
|||
.build()?
|
||||
};
|
||||
|
||||
let _ = api::database::setup(app);
|
||||
let _ = db::database::setup(app);
|
||||
api::hotkeys::setup(app_handle.clone());
|
||||
api::tray::setup(app)?;
|
||||
api::clipboard::setup(app.handle());
|
||||
|
@ -58,12 +83,6 @@ fn main() {
|
|||
utils::commands::center_window_on_current_monitor(&main_window);
|
||||
main_window.hide()?;
|
||||
|
||||
let app_data_dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.expect("Failed to get app data directory");
|
||||
api::clipboard::set_app_data_dir(app_data_dir);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
api::updater::check_for_updates(app_handle).await;
|
||||
});
|
||||
|
@ -79,11 +98,18 @@ fn main() {
|
|||
}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
api::clipboard::get_image_path,
|
||||
api::clipboard::write_and_paste,
|
||||
api::clipboard::read_image,
|
||||
api::database::save_keybind,
|
||||
api::database::get_keybind
|
||||
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,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
|
21
src-tauri/src/utils/favicon.rs
Normal file
21
src-tauri/src/utils/favicon.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
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 +1,3 @@
|
|||
pub mod commands;
|
||||
pub mod types;
|
||||
pub mod commands;
|
||||
pub mod favicon;
|
Loading…
Add table
Add a link
Reference in a new issue