diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..5d4a513 --- /dev/null +++ b/TODO.md @@ -0,0 +1,6 @@ +# Project To-Do List + +* [ ] Improve the **clipboard** UI +* [ ] Handle **image data** within the clipboard functionality +* [ ] Improve the **file search** UI +* [ ] Searching on the home screen should also include **local file results** (i.e., combine file search and online search) \ No newline at end of file diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 73f6f91..316ecad 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -798,13 +798,34 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + [[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "dirs-sys", + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", ] [[package]] @@ -815,7 +836,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.5.2", "windows-sys 0.61.2", ] @@ -2763,6 +2784,7 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" name = "pathfinder" version = "0.1.0" dependencies = [ + "dirs 5.0.1", "enigo", "serde", "serde_json", @@ -2772,6 +2794,7 @@ dependencies = [ "tauri-plugin-global-shortcut", "tauri-plugin-opener", "uuid", + "walkdir", ] [[package]] @@ -3248,6 +3271,17 @@ dependencies = [ "bitflags 2.9.4", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -3949,7 +3983,7 @@ dependencies = [ "anyhow", "bytes", "cookie", - "dirs", + "dirs 6.0.0", "dunce", "embed_plist", "getrandom 0.3.3", @@ -4000,7 +4034,7 @@ checksum = "9c432ccc9ff661803dab74c6cd78de11026a578a9307610bbc39d3c55be7943f" dependencies = [ "anyhow", "cargo_toml", - "dirs", + "dirs 6.0.0", "glob", "heck 0.5.0", "json-patch", @@ -4552,7 +4586,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0d92153331e7d02ec09137538996a7786fe679c629c279e82a6be762b7e6fe2" dependencies = [ "crossbeam-channel", - "dirs", + "dirs 6.0.0", "libappindicator", "muda", "objc2 0.6.3", @@ -5288,6 +5322,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -5330,6 +5373,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -5387,6 +5445,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -5405,6 +5469,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -5423,6 +5493,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -5453,6 +5529,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -5471,6 +5553,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -5489,6 +5577,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -5507,6 +5601,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -5588,7 +5688,7 @@ dependencies = [ "block2 0.6.2", "cookie", "crossbeam-channel", - "dirs", + "dirs 6.0.0", "dpi", "dunce", "gdkx11", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c0fb62d..2f02fb8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -26,3 +26,5 @@ serde_json = "1" tauri-plugin-global-shortcut = "2.3.0" uuid = { version = "1.0", features = ["v4"] } enigo = "0.2.0" +walkdir = "2.4" +dirs = "5.0" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c8207a1..51a3195 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,6 +9,7 @@ use std::sync::{Arc, Mutex}; use std::time::{SystemTime, UNIX_EPOCH}; use std::fs; use std::path::PathBuf; +use walkdir::WalkDir; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ClipboardItem { @@ -28,6 +29,23 @@ pub struct ClipboardDatabase { pub max_items: usize, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FileItem { + pub name: String, + pub path: String, + pub file_type: String, + pub size: u64, + pub modified: u64, + pub is_app: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FileSearchDatabase { + pub files: Vec, + pub apps: Vec, + pub last_indexed: u64, +} + impl ClipboardDatabase { fn new(max_items: usize) -> Self { Self { @@ -76,6 +94,58 @@ impl ClipboardDatabase { } } +impl FileSearchDatabase { + fn new() -> Self { + Self { + files: Vec::new(), + apps: Vec::new(), + last_indexed: 0, + } + } + + fn add_file(&mut self, file: FileItem) { + if file.is_app { + self.apps.push(file); + } else { + self.files.push(file); + } + } + + fn search_files(&self, query: &str) -> Vec { + let mut results = Vec::new(); + let query_lower = query.to_lowercase(); + + // Search in apps first + for app in &self.apps { + if app.name.to_lowercase().contains(&query_lower) { + results.push(app.clone()); + } + } + + // Then search in files + for file in &self.files { + if file.name.to_lowercase().contains(&query_lower) { + results.push(file.clone()); + } + } + + // Limit results to prevent UI lag + results.truncate(50); + results + } + + fn get_apps(&self) -> Vec { + self.apps.clone() + } + + fn get_recent_files(&self) -> Vec { + let mut recent_files = self.files.clone(); + recent_files.sort_by(|a, b| b.modified.cmp(&a.modified)); + recent_files.truncate(20); + recent_files + } +} + fn get_db_path(app_handle: &tauri::AppHandle) -> PathBuf { app_handle .path() @@ -100,6 +170,152 @@ fn load_db(path: &PathBuf) -> Result { Ok(db) } +fn get_file_search_db_path(app_handle: &tauri::AppHandle) -> PathBuf { + app_handle + .path() + .app_data_dir() + .expect("Failed to get app data dir") + .join("file_search.json") +} + +fn save_file_db(db: &FileSearchDatabase, path: &PathBuf) -> Result<(), String> { + let json = serde_json::to_string_pretty(db).map_err(|e| e.to_string())?; + fs::write(path, json).map_err(|e| e.to_string())?; + Ok(()) +} + +fn load_file_db(path: &PathBuf) -> Result { + if !path.exists() { + return Ok(FileSearchDatabase::new()); + } + + let json = fs::read_to_string(path).map_err(|e| e.to_string())?; + let db: FileSearchDatabase = serde_json::from_str(&json).map_err(|e| e.to_string())?; + Ok(db) +} + +fn get_file_extension(path: &PathBuf) -> String { + path.extension() + .and_then(|ext| ext.to_str()) + .unwrap_or("") + .to_lowercase() +} + +fn is_app_file(path: &PathBuf) -> bool { + let extension = get_file_extension(path); + match extension.as_str() { + "app" => true, // macOS + "exe" | "msi" => true, // Windows + "deb" | "rpm" | "AppImage" => true, // Linux + _ => false, + } +} + +fn index_applications() -> Vec { + let mut apps = Vec::new(); + + // Common application directories + let app_dirs = if cfg!(target_os = "macos") { + vec![ + PathBuf::from("/Applications"), + PathBuf::from("/System/Applications"), + PathBuf::from("/System/Library/CoreServices"), + ] + } else if cfg!(target_os = "windows") { + vec![ + PathBuf::from("C:\\Program Files"), + PathBuf::from("C:\\Program Files (x86)"), + PathBuf::from("C:\\Users\\%USERNAME%\\AppData\\Local\\Programs"), + ] + } else { + vec![ + PathBuf::from("/usr/share/applications"), + PathBuf::from("/usr/local/share/applications"), + PathBuf::from("/var/lib/snapd/desktop/applications"), + ] + }; + + for app_dir in app_dirs { + if app_dir.exists() { + for entry in WalkDir::new(&app_dir) + .max_depth(3) + .into_iter() + .filter_map(|e| e.ok()) + { + let path = entry.path(); + if is_app_file(&path.to_path_buf()) { + if let (Ok(metadata), Some(name)) = (path.metadata(), path.file_name().and_then(|n| n.to_str())) { + let modified = metadata + .modified() + .unwrap_or(SystemTime::UNIX_EPOCH) + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + apps.push(FileItem { + name: name.to_string(), + path: path.to_string_lossy().to_string(), + file_type: get_file_extension(&path.to_path_buf()), + size: metadata.len(), + modified, + is_app: true, + }); + } + } + } + } + } + + apps +} + +fn index_user_files() -> Vec { + let mut files = Vec::new(); + + // Get user home directory + if let Some(home_dir) = dirs::home_dir() { + let common_dirs = vec![ + home_dir.join("Documents"), + home_dir.join("Downloads"), + home_dir.join("Desktop"), + home_dir.join("Pictures"), + ]; + + for dir in common_dirs { + if dir.exists() { + for entry in WalkDir::new(&dir) + .max_depth(4) + .into_iter() + .filter_map(|e| e.ok()) + { + let path = entry.path(); + if path.is_file() && !is_app_file(&path.to_path_buf()) { + if let (Ok(metadata), Some(name)) = (path.metadata(), path.file_name().and_then(|n| n.to_str())) { + let modified = metadata + .modified() + .unwrap_or(SystemTime::UNIX_EPOCH) + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + files.push(FileItem { + name: name.to_string(), + path: path.to_string_lossy().to_string(), + file_type: get_file_extension(&path.to_path_buf()), + size: metadata.len(), + modified, + is_app: false, + }); + } + } + } + } + } + } + + files +} + #[tauri::command] fn get_clipboard_history( state: tauri::State>>, @@ -186,6 +402,101 @@ fn paste_clipboard_item( Ok(()) } +#[tauri::command] +fn search_files( + state: tauri::State>>, + query: String, +) -> Result, String> { + let db = state.lock().map_err(|e| e.to_string())?; + Ok(db.search_files(&query)) +} + +#[tauri::command] +fn get_applications( + state: tauri::State>>, +) -> Result, String> { + let db = state.lock().map_err(|e| e.to_string())?; + Ok(db.get_apps()) +} + +#[tauri::command] +fn get_recent_files( + state: tauri::State>>, +) -> Result, String> { + let db = state.lock().map_err(|e| e.to_string())?; + Ok(db.get_recent_files()) +} + +#[tauri::command] +fn open_file( + _app_handle: tauri::AppHandle, + path: String, +) -> Result<(), String> { + use std::process::Command; + + #[cfg(target_os = "macos")] + { + Command::new("open") + .arg(&path) + .spawn() + .map_err(|e| e.to_string())?; + } + + #[cfg(target_os = "windows")] + { + Command::new("cmd") + .args(["/C", "start", "", &path]) + .spawn() + .map_err(|e| e.to_string())?; + } + + #[cfg(target_os = "linux")] + { + Command::new("xdg-open") + .arg(&path) + .spawn() + .map_err(|e| e.to_string())?; + } + + Ok(()) +} + +#[tauri::command] +fn refresh_file_index( + state: tauri::State>>, + app_handle: tauri::AppHandle, +) -> Result<(), String> { + let mut db = state.lock().map_err(|e| e.to_string())?; + + // Clear existing data + db.files.clear(); + db.apps.clear(); + + // Index applications + let apps = index_applications(); + for app in apps { + db.add_file(app); + } + + // Index user files + let files = index_user_files(); + for file in files { + db.add_file(file); + } + + // Update timestamp + db.last_indexed = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + // Save to file + let db_path = get_file_search_db_path(&app_handle); + save_file_db(&db, &db_path)?; + + Ok(()) +} + fn start_clipboard_monitor(app_handle: tauri::AppHandle, db: Arc>) { std::thread::spawn(move || { let mut last_content = String::new(); @@ -237,7 +548,7 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_clipboard_manager::init()) .setup(|app| { - // Initialize database + // Initialize clipboard database let db_path = get_db_path(&app.handle()); // Create app data directory if it doesn't exist @@ -250,6 +561,13 @@ pub fn run() { )); app.manage(db.clone()); + // Initialize file search database + let file_db_path = get_file_search_db_path(&app.handle()); + let file_db = Arc::new(Mutex::new( + load_file_db(&file_db_path).unwrap_or_else(|_| FileSearchDatabase::new()) + )); + app.manage(file_db.clone()); + // Start clipboard monitor start_clipboard_monitor(app.handle().clone(), db.clone()); @@ -286,6 +604,11 @@ pub fn run() { delete_clipboard_item, clear_clipboard_history, paste_clipboard_item, + search_files, + get_applications, + get_recent_files, + open_file, + refresh_file_index, ]) .run(tauri::generate_context!()) .expect("error while running tauri"); diff --git a/src/App.css b/src/App.css index 0aaad9d..f97e1a1 100644 --- a/src/App.css +++ b/src/App.css @@ -260,4 +260,124 @@ .clipboard-item { animation: slideIn 0.2s ease; +} + +/* File search specific styles */ +.file-container { + display: flex; + flex-direction: column; + gap: 0.75rem; + width: 100%; +} + +.file-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 0.5rem; + margin-bottom: 0.25rem; +} + +.file-count { + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.5); + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.refresh-btn { + padding: 0.4rem 0.9rem; + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 5px; + color: rgba(255, 255, 255, 0.7); + font-size: 0.85rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + letter-spacing: 0.3px; +} + +.refresh-btn:hover { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.25); + color: rgba(255, 255, 255, 0.9); +} + +.file-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.9rem; + gap: 1rem; + min-height: 60px; + border: 1px solid rgba(255, 255, 255, 0.08); +} + +.file-item:hover, +.file-item.selected { + border-color: rgba(255, 255, 255, 0.15); +} + +.file-main-content { + display: flex; + align-items: center; + gap: 0.75rem; + flex: 1; + min-width: 0; +} + +.file-icon { + font-size: 1.5rem; + flex-shrink: 0; +} + +.file-info { + display: flex; + flex-direction: column; + gap: 0.4rem; + flex: 1; + min-width: 0; +} + +.file-name { + font-size: 0.95rem; + color: rgba(255, 255, 255, 0.95); + font-weight: 500; + line-height: 1.3; + word-break: break-word; +} + +.file-metadata { + display: flex; + align-items: center; + gap: 0.6rem; + flex-wrap: wrap; +} + +.file-item .meta-item { + display: flex; + align-items: center; + gap: 0.3rem; + font-size: 0.8rem; +} + +.file-item .meta-label { + color: rgba(255, 255, 255, 0.4); + font-weight: 500; +} + +.file-item .meta-value { + color: rgba(255, 255, 255, 0.65); + font-weight: 400; +} + +.file-item .meta-divider { + color: rgba(255, 255, 255, 0.2); + font-size: 0.7rem; +} + +.file-item { + animation: slideIn 0.2s ease; } \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index fe01e89..0e3ea30 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -59,7 +59,7 @@ function App() { {currentPage === "online-search" && ( )} - {currentPage === "open-file" && } + {currentPage === "open-app" && } diff --git a/src/components/HomeOptions.jsx b/src/components/HomeOptions.jsx index ed25113..7dd3e27 100644 --- a/src/components/HomeOptions.jsx +++ b/src/components/HomeOptions.jsx @@ -4,7 +4,7 @@ import Fuse from "fuse.js"; const OPTIONS = [ { title: "Clipboard", icon: "📋", page: "clipboard" }, { title: "Online Search", icon: "🔍", page: "online-search" }, - { title: "Open File", icon: "📁", page: "open-file" }, + { title: "Open App", icon: "📁", page: "open-app" }, ]; const fuse = new Fuse(OPTIONS, { keys: ["title"], threshold: 0.4 }); diff --git a/src/components/OpenFilePage.jsx b/src/components/OpenFilePage.jsx index 7ec0bb1..f75d940 100644 --- a/src/components/OpenFilePage.jsx +++ b/src/components/OpenFilePage.jsx @@ -1,35 +1,227 @@ -import Fuse from "fuse.js"; +import { useState, useEffect } from 'react'; +import { invoke } from '@tauri-apps/api/core'; import { useKeyboardNavigation } from "../hooks/useKeyboardNavigation"; +import Fuse from "fuse.js"; +import { getCurrentWindow } from '@tauri-apps/api/window'; -const dummyFiles = [ - { text: "Document.pdf" }, - { text: "Resume.docx" }, - { text: "Presentation.pptx" }, -]; +export default function OpenFilePage({ query }) { + const [files, setFiles] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [isIndexed, setIsIndexed] = useState(false); -const fuse = new Fuse(dummyFiles, { keys: ["text"], threshold: 0.4 }); + useEffect(() => { + loadFiles(); + }, []); -export default function OpenFilePage({ query }) { - const filteredFiles = query - ? fuse.search(query).map((res) => res.item) - : dummyFiles; + useEffect(() => { + if (query && query.length > 0) { + searchFiles(query); + } else { + loadFiles(); + } + }, [query]); - const { getItemProps } = useKeyboardNavigation(filteredFiles, (item, idx) => { - console.log("Selected:", item.text); - }); + const loadFiles = async () => { + try { + setLoading(true); + const [apps, recentFiles] = await Promise.all([ + invoke('get_applications'), + invoke('get_recent_files') + ]); + + const allFiles = [...apps, ...recentFiles]; + setFiles(allFiles); + setError(null); + setIsIndexed(true); + } catch (err) { + console.error('Failed to load files:', err); + setError('Failed to load files'); + setIsIndexed(false); + } finally { + setLoading(false); + } + }; - return ( -
- {filteredFiles.map((file, idx) => ( -
- 📁 - {file.text} + const searchFiles = async (searchQuery) => { + try { + setLoading(true); + const results = await invoke('search_files', { query: searchQuery }); + setFiles(results); + setError(null); + } catch (err) { + console.error('Failed to search files:', err); + setError('Failed to search files'); + } finally { + setLoading(false); + } + }; + + const refreshIndex = async () => { + try { + setLoading(true); + await invoke('refresh_file_index'); + await loadFiles(); + } catch (err) { + console.error('Failed to refresh index:', err); + setError('Failed to refresh file index'); + } finally { + setLoading(false); + } + }; + + const handleSelect = async (item) => { + try { + // Hide window first + const window = getCurrentWindow(); + await window.hide(); + + // Wait for window to hide + await new Promise(resolve => setTimeout(resolve, 50)); + + // Open the file/app + await invoke('open_file', { path: item.path }); + + console.log('Opened:', item.name); + } catch (err) { + console.error('Failed to open file:', err); + setError('Failed to open file'); + } + }; + + const getFileIcon = (item) => { + if (item.is_app) { + return '🚀'; + } + + const extension = item.file_type.toLowerCase(); + switch (extension) { + case 'pdf': return '📄'; + case 'doc': + case 'docx': return '📝'; + case 'xls': + case 'xlsx': return '📊'; + case 'ppt': + case 'pptx': return '📊'; + case 'jpg': + case 'jpeg': + case 'png': + case 'gif': return '🖼️'; + case 'mp4': + case 'avi': + case 'mov': return '🎥'; + case 'mp3': + case 'wav': return '🎵'; + case 'zip': + case 'rar': return '📦'; + case 'txt': return '📄'; + default: return '📁'; + } + }; + + const formatFileSize = (bytes) => { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`; + if (bytes < 1073741824) return `${(bytes / 1048576).toFixed(1)} MB`; + return `${(bytes / 1073741824).toFixed(1)} GB`; + }; + + const formatDate = (timestamp) => { + const date = new Date(timestamp * 1000); + const now = new Date(); + const diff = now - date; + + const minutes = Math.floor(diff / 60000); + const hours = Math.floor(diff / 3600000); + const days = Math.floor(diff / 86400000); + + if (minutes < 1) return 'Just now'; + if (minutes < 60) return `${minutes}m ago`; + if (hours < 24) return `${hours}h ago`; + if (days < 7) return `${days}d ago`; + + return date.toLocaleDateString(); + }; + + const { getItemProps } = useKeyboardNavigation(files, handleSelect); + + if (loading) { + return ( +
+
Loading files...
+
+ ); + } + + if (error) { + return ( +
+
{error}
+ +
+ ); + } + + if (!isIndexed) { + return ( +
+
+

File index not found. Click to build index.

+
- ))} +
+ ); + } + + return ( +
+
+ {files.length} items + +
+ +
+ {files.length === 0 ? ( +
+ {query ? 'No matching files found' : 'No files found. Click "Refresh Index" to build the file index.'} +
+ ) : ( + files.map((file, idx) => ( +
+
+
{getFileIcon(file)}
+
+
{file.name}
+
+ + Type: + {file.file_type || 'Unknown'} + + + + Size: + {formatFileSize(file.size)} + + + + Modified: + {formatDate(file.modified)} + +
+
+
+
+ )) + )} +
); }