diff --git a/frontend/package.json b/frontend/package.json index 05119e0..ff969af 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "haystack", "version": "0.1.0", - "description": "", + "description": "Screenshots that organize themselves", "type": "module", "scripts": { "start": "vite", diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index 75b2ce6..9a599d4 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "Haystack" +version = "0.1.0" +dependencies = [ + "base64 0.21.7", + "cocoa", + "notify", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-opener", + "tokio", +] + [[package]] name = "addr2line" version = "0.24.2" @@ -1518,22 +1534,6 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" -[[package]] -name = "haystack" -version = "0.1.0" -dependencies = [ - "base64 0.21.7", - "cocoa", - "notify", - "serde", - "serde_json", - "tauri", - "tauri-build", - "tauri-plugin-dialog", - "tauri-plugin-opener", - "tokio", -] - [[package]] name = "heck" version = "0.4.1" diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index 90f3c4d..df37364 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "haystack" +name = "Haystack" version = "0.1.0" -description = "A Tauri App" -authors = ["you"] +description = "Screenshots that organize themselves" +authors = ["Dmytro Kondakov", "John Costa"] edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/frontend/src-tauri/src/commands.rs b/frontend/src-tauri/src/commands.rs new file mode 100644 index 0000000..ec471e6 --- /dev/null +++ b/frontend/src-tauri/src/commands.rs @@ -0,0 +1,71 @@ +use crate::state::SharedWatcherState; +use crate::utils::process_png_file; +use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher}; +use std::path::PathBuf; +use std::sync::mpsc::channel; +use tauri::AppHandle; + +#[tauri::command] +pub async fn handle_selected_folder( + path: String, + state: tauri::State<'_, SharedWatcherState>, + app: AppHandle, +) -> Result { + let path_buf = PathBuf::from(&path); + + if !path_buf.exists() || !path_buf.is_dir() { + return Err("Invalid directory path".to_string()); + } + + // Stop existing watcher if any + let mut state = state + .lock() + .map_err(|_| "Failed to lock state".to_string())?; + state.clear_watcher(); + + // Create a channel to receive file system events + let (tx, rx) = channel(); + + // Create a new watcher + let mut watcher = RecommendedWatcher::new(tx, Config::default()) + .map_err(|e| format!("Failed to create watcher: {}", e))?; + + // Start watching the directory + watcher + .watch(path_buf.as_ref(), RecursiveMode::Recursive) + .map_err(|e| format!("Failed to watch directory: {}", e))?; + + // Store the watcher in state + state.set_watcher(watcher); + + let path_clone = path.clone(); + let app_clone = app.clone(); + tokio::spawn(async move { + println!("Starting to watch directory: {}", path_clone); + for res in rx { + match res { + Ok(event) => { + println!("Received event: {:?}", event); + match event.kind { + notify::EventKind::Create(_) | notify::EventKind::Modify(_) => { + for path in event.paths { + println!("Processing path: {}", path.display()); + if let Some(extension) = path.extension() { + if extension.to_string_lossy().to_lowercase() == "png" { + if let Err(e) = process_png_file(&path, app_clone.clone()) { + eprintln!("Error processing PNG file: {}", e); + } + } + } + } + } + _ => {} + } + } + Err(e) => eprintln!("Watch error: {:?}", e), + } + } + }); + + Ok(format!("Now watching directory: {}", path)) +} diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index 7a0decf..964adee 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -1,147 +1,22 @@ -use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; -use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher}; -use std::fs; -use std::path::PathBuf; -use std::sync::mpsc::channel; -use std::sync::Arc; -use std::sync::Mutex; -use tauri::AppHandle; -use tauri::Emitter; -use tauri::{WebviewUrl, WebviewWindowBuilder}; +mod commands; +mod state; +mod utils; +mod window; -struct WatcherState { - watcher: Option, -} - -impl WatcherState { - fn new() -> Self { - Self { watcher: None } - } -} - -// Handle PNG file processing -fn process_png_file(path: &PathBuf, app: AppHandle) -> Result<(), String> { - println!("Processing PNG file: {}", path.display()); - - // Read the file - let contents = fs::read(path).map_err(|e| format!("Failed to read file: {}", e))?; - - // Convert to base64 - let base64_string = BASE64.encode(&contents); - println!("Generated base64 string of length: {}", base64_string.len()); - - // Emit the base64 to frontend - app.emit("png-processed", base64_string) - .map_err(|e| format!("Failed to emit event: {}", e))?; - - println!("Successfully processed file: {}", path.display()); - Ok(()) -} - -#[tauri::command] -async fn handle_selected_folder( - path: String, - state: tauri::State<'_, Arc>>, - app: AppHandle, -) -> Result { - let path_buf = PathBuf::from(&path); - - if !path_buf.exists() || !path_buf.is_dir() { - return Err("Invalid directory path".to_string()); - } - - // Stop existing watcher if any - let mut state = state - .lock() - .map_err(|_| "Failed to lock state".to_string())?; - state.watcher = None; - - // Create a channel to receive file system events - let (tx, rx) = channel(); - - // Create a new watcher - let mut watcher = RecommendedWatcher::new(tx, Config::default()) - .map_err(|e| format!("Failed to create watcher: {}", e))?; - - // Start watching the directory - watcher - .watch(path_buf.as_ref(), RecursiveMode::Recursive) - .map_err(|e| format!("Failed to watch directory: {}", e))?; - - // Store the watcher in state - state.watcher = Some(watcher); - - let path_clone = path.clone(); - let app_clone = app.clone(); - tokio::spawn(async move { - println!("Starting to watch directory: {}", path_clone); - for res in rx { - match res { - Ok(event) => { - println!("Received event: {:?}", event); - match event.kind { - notify::EventKind::Create(_) | notify::EventKind::Modify(_) => { - for path in event.paths { - println!("Processing path: {}", path.display()); - if let Some(extension) = path.extension() { - if extension.to_string_lossy().to_lowercase() == "png" { - if let Err(e) = process_png_file(&path, app_clone.clone()) { - eprintln!("Error processing PNG file: {}", e); - } - } - } - } - } - _ => {} - } - } - Err(e) => eprintln!("Watch error: {:?}", e), - } - } - }); - - Ok(format!("Now watching directory: {}", path)) -} +use state::new_shared_watcher_state; +use window::setup_window; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - let watcher_state = Arc::new(Mutex::new(WatcherState::new())); + let watcher_state = new_shared_watcher_state(); tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_opener::init()) .manage(watcher_state) - .invoke_handler(tauri::generate_handler![handle_selected_folder]) + .invoke_handler(tauri::generate_handler![commands::handle_selected_folder]) .setup(|app| { - let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default()) - .inner_size(480.0, 360.0) - // .hidden_title(true) - .resizable(true); - // set transparent title bar only when building for macOS - #[cfg(target_os = "macos")] - let win_builder = win_builder.title_bar_style(TitleBarStyle::Transparent); - - let window = win_builder.build().unwrap(); - - // set background color only when building for macOS - #[cfg(target_os = "macos")] - { - use cocoa::appkit::{NSColor, NSWindow}; - use cocoa::base::{id, nil}; - - let ns_window = window.ns_window().unwrap() as id; - unsafe { - let bg_color = NSColor::colorWithRed_green_blue_alpha_( - nil, - 245.0 / 255.0, - 245.0 / 255.0, - 245.0 / 255.0, - 1.0, - ); - ns_window.setBackgroundColor_(bg_color); - } - } - + setup_window(app)?; Ok(()) }) .run(tauri::generate_context!()) diff --git a/frontend/src-tauri/src/state.rs b/frontend/src-tauri/src/state.rs new file mode 100644 index 0000000..df69f08 --- /dev/null +++ b/frontend/src-tauri/src/state.rs @@ -0,0 +1,27 @@ +use notify::RecommendedWatcher; +use std::sync::Arc; +use std::sync::Mutex; + +pub struct WatcherState { + watcher: Option, +} + +impl WatcherState { + pub fn new() -> Self { + Self { watcher: None } + } + + pub fn set_watcher(&mut self, watcher: RecommendedWatcher) { + self.watcher = Some(watcher); + } + + pub fn clear_watcher(&mut self) { + self.watcher = None; + } +} + +pub type SharedWatcherState = Arc>; + +pub fn new_shared_watcher_state() -> SharedWatcherState { + Arc::new(Mutex::new(WatcherState::new())) +} diff --git a/frontend/src-tauri/src/utils.rs b/frontend/src-tauri/src/utils.rs new file mode 100644 index 0000000..84a6000 --- /dev/null +++ b/frontend/src-tauri/src/utils.rs @@ -0,0 +1,22 @@ +use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; +use std::fs; +use std::path::PathBuf; +use tauri::{AppHandle, Emitter}; + +pub fn process_png_file(path: &PathBuf, app: AppHandle) -> Result<(), String> { + println!("Processing PNG file: {}", path.display()); + + // Read the file + let contents = fs::read(path).map_err(|e| format!("Failed to read file: {}", e))?; + + // Convert to base64 + let base64_string = BASE64.encode(&contents); + println!("Generated base64 string of length: {}", base64_string.len()); + + // Emit the base64 to frontend + app.emit("png-processed", base64_string) + .map_err(|e| format!("Failed to emit event: {}", e))?; + + println!("Successfully processed file: {}", path.display()); + Ok(()) +} diff --git a/frontend/src-tauri/src/window.rs b/frontend/src-tauri/src/window.rs new file mode 100644 index 0000000..77958c4 --- /dev/null +++ b/frontend/src-tauri/src/window.rs @@ -0,0 +1,37 @@ +use tauri::App; +use tauri::TitleBarStyle; +use tauri::{WebviewUrl, WebviewWindowBuilder}; + +pub fn setup_window(app: &mut App) -> Result<(), Box> { + let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default()) + .inner_size(480.0, 360.0) + .title("Haystack") + .hidden_title(true) + .resizable(false); + + // + #[cfg(target_os = "macos")] + let win_builder = win_builder.title_bar_style(TitleBarStyle::Transparent); + + let window = win_builder.build().unwrap(); + + #[cfg(target_os = "macos")] + { + use cocoa::appkit::{NSColor, NSWindow}; + use cocoa::base::{id, nil}; + + let ns_window = window.ns_window().unwrap() as id; + unsafe { + let bg_color = NSColor::colorWithRed_green_blue_alpha_( + nil, + 245.0 / 255.0, + 245.0 / 255.0, + 245.0 / 255.0, + 1.0, + ); + ns_window.setBackgroundColor_(bg_color); + } + } + + Ok(()) +} diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index fa1b1bc..711ee51 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -1,6 +1,6 @@ { "$schema": "https://schema.tauri.app/config/2", - "productName": "haystack", + "productName": "Haystack", "version": "0.1.0", "identifier": "com.haystack.app", "build": { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 844d7c1..05fb8fd 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,14 +1,22 @@ import { A } from "@solidjs/router"; import { IconSearch } from "@tabler/icons-solidjs"; +import { listen } from "@tauri-apps/api/event"; import clsx from "clsx"; import Fuse from "fuse.js"; -import { For, createEffect, createResource, createSignal } from "solid-js"; +import { + For, + createEffect, + createResource, + createSignal, + onCleanup, +} from "solid-js"; +import { ImageViewer } from "./components/ImageViewer"; +import { SearchCardContact } from "./components/search-card/SearchCardContact"; import { SearchCardEvent } from "./components/search-card/SearchCardEvent"; import { SearchCardLocation } from "./components/search-card/SearchCardLocation"; import { SearchCardNote } from "./components/search-card/SearchCardNote"; import { type UserImage, getUserImages } from "./network"; import { getCardSize } from "./utils/getCardSize"; -import { SearchCardContact } from "./components/search-card/SearchCardContact"; const getCardComponent = (item: UserImage) => { switch (item.type) { @@ -83,6 +91,7 @@ function App() { }); createEffect(() => { + setSearchResults(data() ?? []); fuze = new Fuse(data() ?? [], { keys: [ { name: "data.Name", weight: 2 }, @@ -98,10 +107,26 @@ function App() { setSearchResults(fuze.search(query).map((s) => s.item)); }; + let searchInputRef: HTMLInputElement | undefined; + + createEffect(() => { + // Listen for the focus-search event from Tauri + const unlisten = listen("focus-search", () => { + if (searchInputRef) { + searchInputRef.focus(); + } + }); + + onCleanup(() => { + unlisten.then((fn) => fn()); + }); + }); + return ( <>
login +
@@ -111,6 +136,7 @@ function App() { />