diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 8d45ba3..5e1c1de 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6,6 +6,7 @@ version = 3 name = "Clippr" version = "0.1.0" dependencies = [ + "active-win-pos-rs", "base64 0.22.1", "chrono", "dirs", @@ -26,6 +27,20 @@ dependencies = [ "window-vibrancy", ] +[[package]] +name = "active-win-pos-rs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9c1d770875c536934a8e7150061b0dbddb919298f0ff762b0f8fc12c8928877" +dependencies = [ + "appkit-nsworkspace-bindings", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "objc", + "windows 0.48.0", + "xcb", +] + [[package]] name = "addr2line" version = "0.24.2" @@ -110,6 +125,16 @@ version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13" +[[package]] +name = "appkit-nsworkspace-bindings" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "062382938604cfa02c03689ab75af0e7eb79175ba0d0b2bcfad18f5190702dd7" +dependencies = [ + "bindgen", + "objc", +] + [[package]] name = "arbitrary" version = "1.4.0" @@ -376,6 +401,29 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bindgen" +version = "0.68.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.86", + "which", +] + [[package]] name = "bit_field" version = "0.10.2" @@ -584,6 +632,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfb" version = "0.7.3" @@ -632,6 +689,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading 0.8.5", +] + [[package]] name = "clipboard-rs" version = "0.2.1" @@ -665,8 +733,8 @@ dependencies = [ "bitflags 2.6.0", "block", "cocoa-foundation", - "core-foundation", - "core-graphics", + "core-foundation 0.10.0", + "core-graphics 0.24.0", "foreign-types", "libc", "objc", @@ -680,8 +748,8 @@ checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" dependencies = [ "bitflags 2.6.0", "block", - "core-foundation", - "core-graphics-types", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", "libc", "objc", ] @@ -723,6 +791,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.0" @@ -739,6 +817,19 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + [[package]] name = "core-graphics" version = "0.24.0" @@ -746,12 +837,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.6.0", - "core-foundation", - "core-graphics-types", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", "foreign-types", "libc", ] +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + [[package]] name = "core-graphics-types" version = "0.2.0" @@ -759,7 +861,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.6.0", - "core-foundation", + "core-foundation 0.10.0", "libc", ] @@ -2289,6 +2391,12 @@ dependencies = [ "spin", ] +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "lebe" version = "0.5.2" @@ -2315,7 +2423,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" dependencies = [ "gtk-sys", - "libloading", + "libloading 0.7.4", "once_cell", ] @@ -2346,6 +2454,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + [[package]] name = "libm" version = "0.2.11" @@ -3049,6 +3167,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361" +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -3256,7 +3380,7 @@ checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64 0.22.1", "indexmap 2.6.0", - "quick-xml", + "quick-xml 0.32.0", "serde", "time", ] @@ -3310,6 +3434,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "prettyplease" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" +dependencies = [ + "proc-macro2", + "syn 2.0.86", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -3403,6 +3537,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.32.0" @@ -3710,6 +3853,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.1" @@ -4071,7 +4220,7 @@ checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" dependencies = [ "bytemuck", "cfg_aliases", - "core-graphics", + "core-graphics 0.24.0", "foreign-types", "js-sys", "log", @@ -4462,8 +4611,8 @@ checksum = "63f1f6b2017cc33d7f6fc9c6186a2c0f5dfc985899a7b4fe9e64985c17533db3" dependencies = [ "bitflags 2.6.0", "cocoa", - "core-foundation", - "core-graphics", + "core-foundation 0.10.0", + "core-graphics 0.24.0", "crossbeam-channel", "dispatch", "dlopen2", @@ -5086,7 +5235,7 @@ version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c92af36a182b46206723bdf8a7942e20838cde1cf062e5b97854d57eb01763b" dependencies = [ - "core-graphics", + "core-graphics 0.24.0", "crossbeam-channel", "dirs", "libappindicator", @@ -5512,6 +5661,18 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "whoami" version = "1.5.2" @@ -5576,6 +5737,15 @@ dependencies = [ "windows-version", ] +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows" version = "0.51.1" @@ -6009,6 +6179,17 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" +[[package]] +name = "xcb" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e2f212bb1a92cd8caac8051b829a6582ede155ccb60b5d5908b81b100952be" +dependencies = [ + "bitflags 1.3.2", + "libc", + "quick-xml 0.30.0", +] + [[package]] name = "xdg-home" version = "1.3.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a583d2a..e07abf0 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -33,6 +33,7 @@ chrono = "0.4.38" dirs = "5.0" tauri-plugin-fs = "2" window-vibrancy = "0.5.2" +active-win-pos-rs = "0.8.3" [target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] tauri-plugin-global-shortcut = "2.0.0" diff --git a/src-tauri/src/api/db.rs b/src-tauri/src/api/db.rs index b304c10..adbd756 100644 --- a/src-tauri/src/api/db.rs +++ b/src-tauri/src/api/db.rs @@ -7,13 +7,15 @@ use super::image::extract_text_from_base64; #[derive(Serialize, Deserialize)] pub struct ClipboardHistory { content: String, - date: String, + first_copied_date: String, + last_copied_date: String, window_title: String, window_exe: String, #[serde(rename = "type")] type_: String, count: i32, image: String, + html: String, } #[derive(Deserialize)] @@ -30,66 +32,6 @@ pub struct SortOptions { order: String, } -#[tauri::command(rename_all = "snake_case")] -pub async fn initialize_database(db_path: String) -> Result<(), String> { - let conn = Connection::open(db_path).map_err(|e| e.to_string())?; - - // Enable FTS5 extension - conn.execute_batch("PRAGMA foreign_keys = ON;") - .map_err(|e| e.to_string())?; - - // Create main clipboard table - conn.execute( - "CREATE TABLE IF NOT EXISTS clipboard ( - id INTEGER PRIMARY KEY, - content TEXT, - date TEXT, - window_title TEXT, - window_exe TEXT, - type TEXT, - image TEXT - )", - [], - ) - .map_err(|e| e.to_string())?; - - // Create FTS5 virtual table - conn.execute( - "CREATE VIRTUAL TABLE IF NOT EXISTS clipboard_fts USING fts5( - content, - window_title, - window_exe, - content='clipboard', - content_rowid='id' - )", - [], - ) - .map_err(|e| e.to_string())?; - - // Create triggers to keep FTS index up to date - conn.execute_batch( - " - CREATE TRIGGER IF NOT EXISTS clipboard_ai AFTER INSERT ON clipboard BEGIN - INSERT INTO clipboard_fts(rowid, content, window_title, window_exe) - VALUES (new.id, new.content, new.window_title, new.window_exe); - END; - CREATE TRIGGER IF NOT EXISTS clipboard_ad AFTER DELETE ON clipboard BEGIN - INSERT INTO clipboard_fts(clipboard_fts, rowid, content, window_title, window_exe) - VALUES('delete', old.id, old.content, old.window_title, old.window_exe); - END; - CREATE TRIGGER IF NOT EXISTS clipboard_au AFTER UPDATE ON clipboard BEGIN - INSERT INTO clipboard_fts(clipboard_fts, rowid, content, window_title, window_exe) - VALUES('delete', old.id, old.content, old.window_title, old.window_exe); - INSERT INTO clipboard_fts(rowid, content, window_title, window_exe) - VALUES (new.id, new.content, new.window_title, new.window_exe); - END; - ", - ) - .map_err(|e| e.to_string())?; - - Ok(()) -} - #[tauri::command(rename_all = "snake_case")] pub async fn save_clipboard_to_db( db_path: String, @@ -97,29 +39,70 @@ pub async fn save_clipboard_to_db( window_title: String, window_exe: String, type_: String, + html: Option, image: Option, ) -> Result { let conn = Connection::open(db_path.clone()).map_err(|e| e.to_string())?; let date = Utc::now().to_rfc3339(); - conn.execute( - "INSERT INTO clipboard (content, date, window_title, window_exe, type, image) VALUES (?, ?, ?, ?, ?, ?)", - params![ - content, - date, - window_title, - window_exe, - type_, - image.clone().unwrap_or_default() - ], - ).map_err(|e| e.to_string())?; - let id = conn.last_insert_rowid(); - - if type_ == "image" { - let base64_str = image.unwrap_or_default(); - let text = extract_text_from_base64(base64_str).await?; - update_clipboard_in_db(db_path, id, text).await?; + + // Check if content already exists + let existing_id: Option = if type_ == "image" { + // For images, check the image column + if let Some(img) = &image { + let mut stmt = conn.prepare("SELECT id FROM clipboard WHERE image = ?") + .map_err(|e| e.to_string())?; + stmt.query_row(params![img], |row| row.get(0)) + .ok() + } else { + None + } + } else { + // For other types, check the content column + let mut stmt = conn.prepare("SELECT id FROM clipboard WHERE content = ?") + .map_err(|e| e.to_string())?; + stmt.query_row(params![content], |row| row.get(0)) + .ok() + }; + + println!("Existing ID: {:?}", existing_id); + + if let Some(id) = existing_id { + // Update existing entry + conn.execute( + "UPDATE clipboard SET count = count + 1, last_copied_date = ?, window_title = ?, window_exe = ? WHERE id = ?", + params![ + date, + window_title, + window_exe, + id + ], + ).map_err(|e| e.to_string())?; + Ok(id) + } else { + // Insert new entry + conn.execute( + "INSERT INTO clipboard (content, first_copied_date, last_copied_date, window_title, window_exe, type, image, html, count) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + params![ + content, + date, + date, + window_title, + window_exe, + type_, + image.clone().unwrap_or_default(), + html.clone().unwrap_or_default(), + 1 + ], + ).map_err(|e| e.to_string())?; + let id = conn.last_insert_rowid(); + + if type_ == "image" { + let base64_str = image.unwrap_or_default(); + let text = extract_text_from_base64(base64_str).await?; + update_clipboard_in_db(db_path, id, text).await?; + } + Ok(id) } - Ok(id) } #[tauri::command(rename_all = "snake_case")] @@ -149,14 +132,8 @@ pub async fn get_history( // Start with a base query that always uses the latest entries let mut query = String::from( - "WITH LatestEntries AS ( - SELECT content, MAX(id) as latest_id - FROM clipboard - GROUP BY content - ) - SELECT c.id, c.content, c.date, c.window_title, c.window_exe, c.type, c.image, 1 as count - FROM clipboard c - INNER JOIN LatestEntries le ON c.id = le.latest_id", + "SELECT c.id, c.content, c.first_copied_date, c.last_copied_date, c.window_title, c.window_exe, c.type, c.image, c.html, c.count + FROM clipboard c" ); let mut conditions = Vec::new(); @@ -198,7 +175,7 @@ pub async fn get_history( if let Some(s) = sort { query.push_str(&format!(" ORDER BY c.{} {}", s.column, s.order)); } else { - query.push_str(" ORDER BY c.date DESC"); + query.push_str(" ORDER BY c.last_copied_date DESC"); } // Apply pagination @@ -215,12 +192,14 @@ pub async fn get_history( .query_map(params.as_slice(), |row| { Ok(ClipboardHistory { content: row.get(1)?, - date: row.get(2)?, - window_title: row.get(3)?, - window_exe: row.get(4)?, - type_: row.get(5)?, - image: row.get(6)?, - count: row.get(7)?, + first_copied_date: row.get(2)?, + last_copied_date: row.get(3)?, + window_title: row.get(4)?, + window_exe: row.get(5)?, + type_: row.get(6)?, + image: row.get(7)?, + html: row.get(8)?, + count: row.get(9)?, }) }) .map_err(|e| e.to_string())?; diff --git a/src-tauri/src/api/mod.rs b/src-tauri/src/api/mod.rs index 185209b..c583b2c 100644 --- a/src-tauri/src/api/mod.rs +++ b/src-tauri/src/api/mod.rs @@ -1,4 +1,4 @@ pub mod clipboard; pub mod db; pub mod image; -pub mod position; +pub mod window; diff --git a/src-tauri/src/api/position.rs b/src-tauri/src/api/position.rs deleted file mode 100644 index 614c2d0..0000000 --- a/src-tauri/src/api/position.rs +++ /dev/null @@ -1,16 +0,0 @@ -use tauri::{LogicalPosition, WebviewWindow}; - -#[tauri::command] -pub fn center_window_on_current_monitor(window: &WebviewWindow) { - if let Some(monitor) = window.current_monitor().ok().flatten() { - let monitor_size = monitor.size(); - let window_size = window.outer_size().ok().unwrap_or_default(); - - let x = (monitor_size.width as f64 - window_size.width as f64) / 2.0; - let y = (monitor_size.height as f64 - window_size.height as f64) / 2.0; - - window - .set_position(LogicalPosition::new(x, y)) - .expect("Failed to set window position"); - } -} diff --git a/src-tauri/src/api/window.rs b/src-tauri/src/api/window.rs new file mode 100644 index 0000000..667ca3c --- /dev/null +++ b/src-tauri/src/api/window.rs @@ -0,0 +1,38 @@ +use serde::Serialize; +use tauri::{LogicalPosition, WebviewWindow}; +use active_win_pos_rs::get_active_window; + +#[tauri::command] +pub fn center_window_on_current_monitor(window: &WebviewWindow) { + if let Some(monitor) = window.current_monitor().ok().flatten() { + let monitor_size = monitor.size(); + let window_size = window.outer_size().ok().unwrap_or_default(); + + let x = (monitor_size.width as f64 - window_size.width as f64) / 2.0; + let y = (monitor_size.height as f64 - window_size.height as f64) / 2.0; + + window + .set_position(LogicalPosition::new(x, y)) + .expect("Failed to set window position"); + } +} + +#[derive(Serialize)] +pub struct WindowInfo { + pub title: String, + pub process_path: String, + pub app_name: String, + pub window_id: String, + pub process_id: u64, +} + +#[tauri::command] +pub fn get_current_window() -> Option { + get_active_window().ok().map(|window| WindowInfo { + title: window.title, + process_path: window.process_path.file_name().and_then(|n| n.to_str()).unwrap_or("").to_string(), + app_name: window.app_name, + window_id: window.window_id, + process_id: window.process_id + }) +} \ No newline at end of file diff --git a/src-tauri/src/db/migration.rs b/src-tauri/src/db/migration.rs index 9cfd153..bf23c7b 100644 --- a/src-tauri/src/db/migration.rs +++ b/src-tauri/src/db/migration.rs @@ -7,11 +7,14 @@ pub const MIGRATION: Migration = Migration { sql: "CREATE TABLE clipboard ( id INTEGER PRIMARY KEY, content TEXT, - date TEXT, + first_copied_date TEXT, + last_copied_date TEXT, window_title TEXT, window_exe TEXT, type TEXT, - image TEXT + image TEXT, + html TEXT, + count INTEGER ); CREATE VIRTUAL TABLE clipboard_fts USING fts5( content, diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 4133cd9..c248dc5 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -28,7 +28,7 @@ fn toggle_app_window(app: &AppHandle) { let _ = window.hide(); } else { let _ = window.show(); - api::position::center_window_on_current_monitor(&window); + api::window::center_window_on_current_monitor(&window); let _ = window.set_focus(); } } @@ -121,7 +121,7 @@ fn main() { Ok(()) }) .on_window_event(|app, event| { - // #[cfg(not(dev))] + #[cfg(not(dev))] if let tauri::WindowEvent::Focused(false) = event { if let Some(window) = app.get_webview_window("popup") { let _ = window.hide(); @@ -131,11 +131,11 @@ fn main() { .invoke_handler(tauri::generate_handler![ message, api::image::extract_text_from_base64, - api::db::initialize_database, api::db::save_clipboard_to_db, api::db::update_clipboard_in_db, api::db::get_history, - api::db::delete_clipboard_from_db + api::db::delete_clipboard_from_db, + api::window::get_current_window ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/App.tsx b/src/App.tsx index b9930d2..88b477b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,11 @@ import { createSignal, For, onCleanup, onMount } from "solid-js"; import { emit, listen } from "@tauri-apps/api/event"; import { getCurrentWindow } from "@tauri-apps/api/window"; -import { writeHtml, writeImageBase64, writeText } from "tauri-plugin-clipboard-api"; +import { + writeHtml, + writeImageBase64, + writeText, +} from "tauri-plugin-clipboard-api"; import { invoke } from "@tauri-apps/api/core"; import type { ClipboardHistory } from "./types/clipboard"; import { SearchInput } from "./components/search-input"; @@ -58,40 +62,49 @@ export const App = ({ db_path }: { db_path: string }) => { // }; const handleKeyDown = (event: KeyboardEvent) => { - const list = listRef?.children; - const totalLength = list?.length ?? 0; - if (event.key === "ArrowDown") { - setActiveIndex((prev) => Math.min(prev + 1, totalLength - 1)); - if (activeIndex() >= totalLength - 5) { - setOffset((prev) => prev + limit); - updateHistory(offset(), limit); - } - } else if (event.key === "ArrowUp") { - setActiveIndex((prev) => Math.max(prev - 1, 0)); - } else if (event.key === "Enter") { - const item = clipboardHistory()[activeIndex()]; - handleCopy(item); - getCurrentWindow().hide(); - } else if (event.key === "Escape") { - if (inputRef && inputRef.value.length > 0) { - inputRef.value = ""; - updateHistory(); - } else if (activeIndex() > 0) { - setActiveIndex(0); - list?.[0]?.scrollIntoView({ behavior: "smooth", block: "center" }); - } else { + try { + const list = listRef?.children; + const totalLength = clipboardHistory().length; + if (event.key === "ArrowDown") { + setActiveIndex((prev) => Math.min(prev + 1, totalLength - 1)); + event.preventDefault(); + list?.[activeIndex()]?.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + // Only load more if we have at least 20 items in the current history + if ( + activeIndex() >= totalLength - 5 + ) { + setOffset((prev) => prev + limit); + updateHistory(offset(), limit); + } + } else if (event.key === "ArrowUp") { + setActiveIndex((prev) => Math.max(prev - 1, 0)); + event.preventDefault(); + list?.[activeIndex()]?.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + } else if (event.key === "Enter") { + const item = clipboardHistory()[activeIndex()]; + handleCopy(item); getCurrentWindow().hide(); + } else if (event.key === "Escape") { + if (inputRef && inputRef.value.length > 0) { + inputRef.value = ""; + updateHistory(); + } else if (activeIndex() > 0) { + setActiveIndex(0); + list?.[0]?.scrollIntoView({ behavior: "smooth", block: "center" }); + } else { + getCurrentWindow().hide(); + } + } else { + inputRef?.focus(); } - } else { - inputRef?.focus(); - } - - if (event.key === "ArrowDown" || event.key === "ArrowUp") { - event.preventDefault(); - list?.[activeIndex()]?.scrollIntoView({ - behavior: "smooth", - block: "center", - }); + } catch (error) { + console.error(error); } }; @@ -106,9 +119,7 @@ export const App = ({ db_path }: { db_path: string }) => { }; const updateHistory = (offset = 0, limit = 20) => { - if (offset === 0) { - setIsInitialLoading(true); - } else { + if (offset !== 0) { setIsLoadingMore(true); } invoke("get_history", { @@ -133,12 +144,11 @@ export const App = ({ db_path }: { db_path: string }) => { writeImageBase64(item.image); } else if (item.type === "html") { writeHtml(item.content); - } - else { + } else { writeText(item.content); } getCurrentWindow().hide(); - } + }; // const handleDelete = async (item: ClipboardHistory) => { // await invoke("delete_clipboard_from_db", { db_path, id: item.id }); @@ -147,7 +157,6 @@ export const App = ({ db_path }: { db_path: string }) => { const handleInput = () => { setActiveIndex(0); - setIsInitialLoading(true); if (searchTimeout()) window.clearTimeout(searchTimeout()); const timeoutId = window.setTimeout(() => { invoke("get_history", { @@ -156,10 +165,9 @@ export const App = ({ db_path }: { db_path: string }) => { }) .then((items) => { setClipboardHistory(items); - setIsInitialLoading(false); }) .catch(() => { - setIsInitialLoading(false); + // Handle error if needed }); }, 300); setSearchTimeout(timeoutId); @@ -179,20 +187,26 @@ export const App = ({ db_path }: { db_path: string }) => { updateHistory(); listen("clipboard_saved", async () => { - console.log("clipboard saved"); - // Get the latest history in background - const newHistory = await invoke("get_history", { - db_path, - offset: 0, - limit: 20, - }); + try { + console.log("clipboard saved"); + // Get the latest history in background + const newHistory = await invoke("get_history", { + db_path, + offset: 0, + limit: 20, + }); + + console.log(newHistory); - // Compare with current history - const currentHistory = clipboardHistory().slice(0, 20); + // Compare with current history + const currentHistory = clipboardHistory().slice(0, 20); - // Only update if there are differences - if (JSON.stringify(newHistory) !== JSON.stringify(currentHistory)) { - setClipboardHistory(newHistory); + // Only update if there are differences + if (JSON.stringify(newHistory) !== JSON.stringify(currentHistory)) { + setClipboardHistory(newHistory); + } + } catch (error) { + console.error(error); } }); @@ -246,7 +260,10 @@ export const App = ({ db_path }: { db_path: string }) => { }); return ( -
e.preventDefault()}> +
e.preventDefault()} + >
@@ -257,19 +274,21 @@ export const App = ({ db_path }: { db_path: string }) => { class="h-full pb-2 overflow-y-auto invisible hover:visible max-h-[calc(100svh-4.5rem)] hover:overflow-y-auto select-none scroll-area" >
    - {isInitialLoading() ? ( - {() => } - ) : clipboardHistory().length === 0 ? ( + { clipboardHistory().length === 0 ? ( ) : ( <> {(item, index) => { - const currentDate = getRelativeTime(new Date(item.date)); + const currentDate = getRelativeTime( + new Date(item.last_copied_date) + ); const prevDate = index() > 0 ? getRelativeTime( - new Date(clipboardHistory()[index() - 1].date) + new Date( + clipboardHistory()[index() - 1].last_copied_date + ) ) : null; @@ -309,16 +328,7 @@ export const App = ({ db_path }: { db_path: string }) => { class="w-full h-full flex flex-col gap-2 mt-2 px-4 overflow-hidden" // onContextMenu={handleRightPanelContextMenu} > - {isInitialLoading() ? ( -
    -
    -
    -
    -
    -
    -
    -
    - ) : clipboardHistory().length > 0 ? ( + {clipboardHistory().length > 0 ? ( = (props) => {

    {highlightText( - props.item.content.replace(/<[^>]*>/g, '').trim().split("\n")[0], + props.item.content.trim().split("\n")[0], props.searchQuery )}

    diff --git a/src/components/clipboard-preview.tsx b/src/components/clipboard-preview.tsx index 5aa9d46..6adc948 100644 --- a/src/components/clipboard-preview.tsx +++ b/src/components/clipboard-preview.tsx @@ -10,28 +10,30 @@ interface ClipboardPreviewProps { } export const ClipboardPreview: Component = (props) => { + const formatDate = (dateStr: string) => { + return new Intl.DateTimeFormat("ja-JP", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }).format(new Date(dateStr)); + }; + return ( -
    -
    - {props.item?.date && ( +
    +
    + {props.item?.last_copied_date && ( )} -

    - {/* {props.item.window_title} ({props.item.type === "text" ? props.item.count : null}) */} -

    + {props?.item?.type === "image" ? (
    = (props) => {
    ) : props?.item?.type === "html" ? (
    -
    +
    ) : (
    {highlightText(props.item?.content, props.searchQuery)}
    )} + +
    +
    +

    Copy Count

    +

    {props.item.count} times

    +
    +
    +

    Content Type

    +

    {props.item.type}

    +
    +
    +

    First Copied

    +

    {formatDate(props.item.first_copied_date)}

    +
    +
    +

    Last Copied

    +

    {formatDate(props.item.last_copied_date)}

    +
    +
    +

    Window

    +

    + {props.item.window_title} +

    +
    +
    +

    Application

    +

    + {props.item.window_exe} +

    +
    +
    ); }; diff --git a/src/components/clipbpard-list.tsx b/src/components/clipbpard-list.tsx index d5d35e9..ecacb2b 100644 --- a/src/components/clipbpard-list.tsx +++ b/src/components/clipbpard-list.tsx @@ -42,10 +42,10 @@ export const ClipboardList: Component = (props) => { <> {(item, index) => { - const currentDate = getRelativeTime(new Date(item.date)); + const currentDate = getRelativeTime(new Date(item.last_copied_date)); const prevDate = index() > 0 - ? getRelativeTime(new Date(props.items[index() - 1].date)) + ? getRelativeTime(new Date(props.items[index() - 1].last_copied_date)) : null; return ( diff --git a/src/index.tsx b/src/index.tsx index 37ddaee..a8db795 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,7 +1,6 @@ import { render } from "solid-js/web"; import { createSignal } from "solid-js"; import { emit, listen } from "@tauri-apps/api/event"; -import { getCurrentWindow } from "@tauri-apps/api/window"; import { hasImage, hasText, @@ -18,6 +17,7 @@ import { invoke } from "@tauri-apps/api/core"; import { appConfigDir } from "@tauri-apps/api/path"; // import { writeFile, BaseDirectory } from "@tauri-apps/plugin-fs"; import { path } from "@tauri-apps/api"; +import { ActiveWindowProps } from "./types/clipboard"; const main = async () => { const app_config_dir = await appConfigDir(); @@ -25,13 +25,8 @@ const main = async () => { const root = document.getElementById("root"); - const appWindow = getCurrentWindow(); - - const [hasImageSignal, setHasImageSignal] = createSignal(false); - const [hasTextSignal, setHasTextSignal] = createSignal(false); - const [hasHtmlSignal, setHasHtmlSignal] = createSignal(false); - const [hasFileSignal, setHasFileSignal] = createSignal(false); const [prevImage, setPrevImage] = createSignal(""); + const [prevText, setPrevText] = createSignal(""); const [isCopyingFromApp, setIsCopyingFromApp] = createSignal(false); createSignal(false); @@ -42,18 +37,15 @@ const main = async () => { return; } + + const type = await hasFiles() ? "file" : await hasImage() ? "image" : await hasHTML() ? "html" : await hasText() ? "text" : null; + // Skip if clipboard has file - if (hasFileSignal()) return; + if (type === "file") return; - const windowTitle = (await appWindow.title()) || "unknown"; - const windowExe = "unknown"; - const type = hasImageSignal() - ? "image" - : hasHtmlSignal() - ? "html" - : hasTextSignal() - ? "text" - : "unknown"; + const window = await invoke("get_current_window") as ActiveWindowProps; + const windowTitle = window.title; + const windowExe = window.process_path.split(/[/\\]/).pop() || window.process_path; if (type === "image") { const image = await readImageBase64(); @@ -75,9 +67,13 @@ const main = async () => { window_exe: windowExe, type, image, + html: null, }); } else { - const content = type === "html" ? await readHtml() : await readText(); + const content = await readText(); + if (content !== prevText()) setPrevText(content); + else return; + const html = type === "html" ? await readHtml() : ""; await invoke("save_clipboard_to_db", { db_path, content, @@ -85,6 +81,7 @@ const main = async () => { window_exe: windowExe, type, image: null, + html, }); } }; @@ -96,11 +93,6 @@ const main = async () => { }); onClipboardUpdate(async () => { - setHasImageSignal(await hasImage()); - setHasTextSignal(await hasText()); - setHasHtmlSignal(await hasHTML()); - setHasFileSignal(await hasFiles()); - await saveClipboard(); emit("clipboard_saved"); }); diff --git a/src/types/clipboard.ts b/src/types/clipboard.ts index a4f305d..e5817b5 100644 --- a/src/types/clipboard.ts +++ b/src/types/clipboard.ts @@ -1,10 +1,26 @@ export type ClipboardHistory = { id: number; content: string; - date: string; + first_copied_date: string; + last_copied_date: string; window_title: string; window_exe: string; type: string; count: number; image: string; + html: string; }; + +export type ActiveWindowProps = { + title: string; + process_path: string; + app_name: string; + window_id: string; + process_id: number; + position: { + x: number; + y: number; + width: number; + height: number; + }; +}; \ No newline at end of file