diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b9e5323..52ec1be 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -8,6 +8,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.12" @@ -52,6 +63,23 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" +dependencies = [ + "android_log-sys", + "env_filter 0.1.4", + "log", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -378,6 +406,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -409,6 +449,30 @@ dependencies = [ "piper", ] +[[package]] +name = "borsh" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" +dependencies = [ + "once_cell", + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "brotli" version = "8.0.2" @@ -436,6 +500,40 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "byte-unit" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d" +dependencies = [ + "rust_decimal", + "schemars 1.2.1", + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytemuck" version = "1.25.0" @@ -1458,6 +1556,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_filter" version = "1.0.1" @@ -1482,7 +1590,7 @@ checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ "anstream", "anstyle", - "env_filter", + "env_filter 1.0.1", "jiff", "log", ] @@ -1616,6 +1724,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fern" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" +dependencies = [ + "log", +] + [[package]] name = "ffmpeg-sidecar" version = "2.5.0" @@ -1767,6 +1884,12 @@ dependencies = [ "libc", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futf" version = "0.1.5" @@ -2462,6 +2585,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] [[package]] name = "hashbrown" @@ -2469,7 +2595,7 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.12", ] [[package]] @@ -3345,6 +3471,9 @@ name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "value-bag", +] [[package]] name = "lzma-rust2" @@ -3896,6 +4025,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "objc2" version = "0.6.4" @@ -4471,6 +4609,7 @@ dependencies = [ "hnsw_rs", "image", "kamadak-exif", + "log", "memmap2", "mozjpeg", "notify", @@ -4487,10 +4626,13 @@ dependencies = [ "tauri-build", "tauri-plugin-dialog", "tauri-plugin-fs", + "tauri-plugin-log", "tauri-plugin-notification", "tauri-plugin-opener", "tauri-plugin-process", + "tauri-plugin-single-instance", "tauri-plugin-updater", + "tauri-plugin-window-state", "tokenizers", "tokio", "ureq", @@ -4704,6 +4846,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "pulp" version = "0.21.5" @@ -4814,6 +4976,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -5087,6 +5255,15 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "reqwest" version = "0.12.28" @@ -5216,6 +5393,35 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rusqlite" version = "0.32.1" @@ -5230,6 +5436,23 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust_decimal" +version = "1.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be2a24f50780bc85f09cc6ac299bdf1424302742d77221106859c9d8b102126a" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", + "wasm-bindgen", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -5449,6 +5672,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "security-framework" version = "3.7.0" @@ -5746,6 +5975,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "0.3.11" @@ -6097,6 +6332,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.45" @@ -6288,6 +6529,28 @@ dependencies = [ "url", ] +[[package]] +name = "tauri-plugin-log" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7545bd67f070a4500432c826e2e0682146a1d6712aee22a2786490156b574d93" +dependencies = [ + "android_logger", + "byte-unit", + "fern", + "log", + "objc2", + "objc2-foundation", + "serde", + "serde_json", + "serde_repr", + "swift-rs", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "time", +] + [[package]] name = "tauri-plugin-notification" version = "2.3.3" @@ -6339,6 +6602,21 @@ dependencies = [ "tauri-plugin", ] +[[package]] +name = "tauri-plugin-single-instance" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8f29386f5e9fdc699182388a33ee80a56de436d91b67459e86afef426282af" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.18", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + [[package]] name = "tauri-plugin-updater" version = "2.10.1" @@ -6372,6 +6650,21 @@ dependencies = [ "zip 4.6.1", ] +[[package]] +name = "tauri-plugin-window-state" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704" +dependencies = [ + "bitflags 2.11.0", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + [[package]] name = "tauri-runtime" version = "2.10.1" @@ -6580,7 +6873,9 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde_core", "time-core", @@ -6613,13 +6908,28 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokenizers" version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b238e22d44a15349529690fb07bd645cf58149a1b1e44d6cb5bd1641ff1a6223" dependencies = [ - "ahash", + "ahash 0.8.12", "aho-corasick", "compact_str", "dary_heap", @@ -7125,6 +7435,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" + [[package]] name = "utf8-zero" version = "0.8.1" @@ -7156,6 +7472,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" + [[package]] name = "vcpkg" version = "0.2.15" @@ -8264,6 +8586,15 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11" version = "2.21.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index eeb3967..be7b68f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -57,6 +57,10 @@ tauri-plugin-notification = "2" mozjpeg = "0.10.13" tauri-plugin-updater = "2" tauri-plugin-process = "2" +tauri-plugin-log = "2" +tauri-plugin-single-instance = "2" +tauri-plugin-window-state = "2" +log = "0.4" # ── Dev-mode performance ──────────────────────────────────────────────────── # opt-level=1 on the main crate keeps incremental compile short. diff --git a/src-tauri/src/captioner.rs b/src-tauri/src/captioner.rs index aea7833..967d02d 100644 --- a/src-tauri/src/captioner.rs +++ b/src-tauri/src/captioner.rs @@ -470,7 +470,7 @@ impl FlorenceCaptioner { acceleration, false, )?; - println!( + log::info!( "Florence sessions loaded in {:?} with {:?} acceleration (total init {:?})", sessions_started_at.elapsed(), acceleration, @@ -490,9 +490,9 @@ impl FlorenceCaptioner { pub fn generate(&mut self, image_path: &Path) -> Result { let started_at = Instant::now(); - println!("Florence caption started: {}", image_path.display()); + log::info!("Florence caption started: {}", image_path.display()); let image_features = run_vision_encoder(&mut self.vision_session, image_path)?; - println!("Florence vision encoder done in {:?}", started_at.elapsed()); + log::info!("Florence vision encoder done in {:?}", started_at.elapsed()); let prompt_ids = self .tokenizer .encode(self.caption_detail.prompt(), false) @@ -502,7 +502,7 @@ impl FlorenceCaptioner { .map(|id| i64::from(*id)) .collect::>(); let prompt_embeds = run_token_embedder(&mut self.embed_session, &prompt_ids)?; - println!( + log::info!( "Florence token embeddings done in {:?}", started_at.elapsed() ); @@ -513,7 +513,7 @@ impl FlorenceCaptioner { &encoder_embeds, &encoder_attention_mask, )?; - println!("Florence encoder done in {:?}", started_at.elapsed()); + log::info!("Florence encoder done in {:?}", started_at.elapsed()); let generated_ids = run_decoder( &mut self.decoder_prefill_session, @@ -523,7 +523,7 @@ impl FlorenceCaptioner { &encoder_attention_mask, self.caption_detail.max_new_tokens(), )?; - println!("Florence decoder done in {:?}", started_at.elapsed()); + log::info!("Florence decoder done in {:?}", started_at.elapsed()); let generated_u32 = generated_ids .into_iter() @@ -784,7 +784,7 @@ fn run_decoder( "inputs_embeds" => prefill_inputs_embeds_tensor }) .map_err(|error| anyhow::anyhow!("{error}"))?; - println!( + log::info!( "Florence decoder prefill done in {:?}", prefill_started_at.elapsed() ); @@ -848,7 +848,7 @@ fn run_decoder( decoder_kv = collect_present_key_values(&outputs, DECODER_LAYERS)?; } - println!("Florence decoder produced {} token(s)", generated.len()); + log::info!("Florence decoder produced {} token(s)", generated.len()); Ok(generated) } @@ -886,7 +886,7 @@ fn create_session( CaptionAcceleration::Auto | CaptionAcceleration::Directml => { // `allow_directml` is false for the 4 text/decoder sessions — // they intentionally run on CPU regardless of the setting. - println!( + log::info!( "Florence: using CPU for {} (DirectML disabled for this session type)", path.display() ); diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index ba33ad0..7a66e37 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -227,6 +227,14 @@ pub async fn add_folder( return Err("Path is not a valid directory".into()); } + // Let the webview load media from this folder via the asset protocol. + if let Err(error) = app + .asset_protocol_scope() + .allow_directory(&folder_path, true) + { + log::error!("Failed to allow asset scope for {}: {}", path, error); + } + let name = folder_path .file_name() .map(|n| n.to_string_lossy().to_string()) @@ -397,6 +405,15 @@ pub async fn update_folder_path( if !new_path_buf.is_dir() { return Err(format!("Path is not a valid directory: {}", new_path)); } + + // Let the webview load media from the relocated folder via the asset protocol. + if let Err(error) = app + .asset_protocol_scope() + .allow_directory(&new_path_buf, true) + { + log::error!("Failed to allow asset scope for {}: {}", new_path, error); + } + let new_name = new_path_buf .file_name() .map(|n| n.to_string_lossy().to_string()) diff --git a/src-tauri/src/embedder.rs b/src-tauri/src/embedder.rs index f2a7551..7c0b262 100644 --- a/src-tauri/src/embedder.rs +++ b/src-tauri/src/embedder.rs @@ -20,7 +20,7 @@ fn with_text_embedder(f: impl FnOnce(&ClipImageEmbedder) -> Result) -> Res .lock() .map_err(|_| anyhow::anyhow!("Text embedder lock poisoned"))?; if guard.is_none() { - println!("Initializing CLIP text embedder..."); + log::info!("Initializing CLIP text embedder..."); *guard = Some(ClipImageEmbedder::new()?); } f(guard.as_ref().unwrap()) @@ -35,13 +35,13 @@ pub struct ClipImageEmbedder { impl ClipImageEmbedder { pub fn new() -> Result { - println!("Initializing CLIP image embedder..."); + log::info!("Initializing CLIP image embedder..."); let api = Api::new()?; let repo = api.repo(Repo::new( "laion/CLIP-ViT-B-32-laion2B-s34B-b79K".to_string(), RepoType::Model, )); - println!("Resolving CLIP model weights from Hugging Face cache..."); + log::info!("Resolving CLIP model weights from Hugging Face cache..."); let model_path = repo.get("model.safetensors")?; let tokenizer_repo = api.repo(Repo::new( "openai/clip-vit-base-patch32".to_string(), @@ -60,7 +60,7 @@ impl ClipImageEmbedder { }; let model = ClipModel::new(vb, &config)?; let tokenizer = Tokenizer::from_file(tokenizer_path).map_err(anyhow::Error::msg)?; - println!("CLIP image embedder ready."); + log::info!("CLIP image embedder ready."); Ok(Self { model, @@ -177,14 +177,14 @@ impl ClipImageEmbedder { fn resolve_device() -> Result { match Device::cuda_if_available(0) { Ok(device) if device.is_cuda() => { - println!("CLIP embedder: using CUDA GPU (device 0)."); + log::info!("CLIP embedder: using CUDA GPU (device 0)."); return Ok(device); } Ok(_) => { - println!("CLIP embedder: no compatible CUDA GPU found — using CPU."); + log::info!("CLIP embedder: no compatible CUDA GPU found — using CPU."); } Err(e) => { - println!("CLIP embedder: CUDA init failed ({e}) — using CPU."); + log::info!("CLIP embedder: CUDA init failed ({e}) — using CPU."); } } Ok(Device::Cpu) diff --git a/src-tauri/src/indexer.rs b/src-tauri/src/indexer.rs index cb60901..a0d7d1d 100644 --- a/src-tauri/src/indexer.rs +++ b/src-tauri/src/indexer.rs @@ -200,7 +200,7 @@ pub fn index_folder(app: AppHandle, pool: DbPool, folder_id: i64, folder_path: P // not be silently destroyed. if !folder_path.is_dir() { let error_msg = format!("Folder not found: {}", folder_path.display()); - eprintln!("Indexing error for folder {}: {}", folder_id, error_msg); + log::error!("Indexing error for folder {}: {}", folder_id, error_msg); if let Ok(conn) = pool.get() { let _ = db::set_folder_scan_error(&conn, folder_id, &error_msg); } @@ -221,7 +221,7 @@ pub fn index_folder(app: AppHandle, pool: DbPool, folder_id: i64, folder_path: P let storage_profile = detect_storage_profile(&folder_path); set_folder_storage_profile(folder_id, RuntimeAdaptiveProfile::new(storage_profile)); if let Err(error) = do_index(app.clone(), &pool, folder_id, folder_path) { - eprintln!("Indexing error for folder {}: {}", folder_id, error); + log::error!("Indexing error for folder {}: {}", folder_id, error); if let Ok(conn) = pool.get() { let _ = db::set_folder_scan_error(&conn, folder_id, &error.to_string()); } @@ -254,7 +254,7 @@ pub fn start_thumbnail_worker( Ok(true) => {} Ok(false) => std::thread::sleep(std::time::Duration::from_millis(250)), Err(error) => { - eprintln!("Thumbnail worker error: {}", error); + log::error!("Thumbnail worker error: {}", error); std::thread::sleep(std::time::Duration::from_millis(250)); } } @@ -269,7 +269,7 @@ pub fn start_metadata_worker(app: AppHandle, pool: DbPool, media_tools: MediaToo Ok(true) => {} Ok(false) => std::thread::sleep(std::time::Duration::from_millis(250)), Err(error) => { - eprintln!("Metadata worker error: {}", error); + log::error!("Metadata worker error: {}", error); std::thread::sleep(std::time::Duration::from_millis(250)); } } @@ -279,7 +279,7 @@ pub fn start_metadata_worker(app: AppHandle, pool: DbPool, media_tools: MediaToo pub fn start_embedding_worker(app: AppHandle, pool: DbPool) { std::thread::spawn(move || { let mut embedder: Option = None; - println!("Embedding worker started."); + log::info!("Embedding worker started."); loop { // Only back off when the queue is empty (or errored); while jobs // are pending, claim the next batch immediately. @@ -287,7 +287,7 @@ pub fn start_embedding_worker(app: AppHandle, pool: DbPool) { Ok(true) => {} Ok(false) => std::thread::sleep(std::time::Duration::from_millis(500)), Err(error) => { - eprintln!("Embedding worker error: {}", error); + log::error!("Embedding worker error: {}", error); std::thread::sleep(std::time::Duration::from_millis(500)); } } @@ -298,16 +298,16 @@ pub fn start_embedding_worker(app: AppHandle, pool: DbPool) { pub fn start_caption_worker(app: AppHandle, pool: DbPool, app_data_dir: PathBuf) { std::thread::spawn(move || { let mut captioner: Option = None; - println!("Caption worker started."); + log::info!("Caption worker started."); loop { // If the acceleration setting changed, drop the cached session so // the next batch picks it up with the new execution provider. if captioner::CAPTION_SESSION_DIRTY.swap(false, std::sync::atomic::Ordering::Relaxed) { - println!("Caption worker: acceleration setting changed — resetting session."); + log::info!("Caption worker: acceleration setting changed — resetting session."); captioner = None; } if let Err(error) = process_caption_batch(&app, &pool, &app_data_dir, &mut captioner) { - eprintln!("Caption worker error: {}", error); + log::error!("Caption worker error: {}", error); captioner = None; } std::thread::sleep(std::time::Duration::from_millis(750)); @@ -318,12 +318,12 @@ pub fn start_caption_worker(app: AppHandle, pool: DbPool, app_data_dir: PathBuf) pub fn start_tagging_worker(app: AppHandle, pool: DbPool, app_data_dir: PathBuf) { std::thread::spawn(move || { let mut tagger_instance: Option = None; - println!("Tagging worker started."); + log::info!("Tagging worker started."); loop { // If the acceleration setting changed, drop the cached session so // the next batch picks it up with the new execution provider. if tagger::TAGGER_SESSION_DIRTY.swap(false, std::sync::atomic::Ordering::Relaxed) { - println!("Tagging worker: acceleration setting changed — resetting session."); + log::info!("Tagging worker: acceleration setting changed — resetting session."); tagger_instance = None; } // Only back off when the queue is empty (or errored); while jobs @@ -332,7 +332,7 @@ pub fn start_tagging_worker(app: AppHandle, pool: DbPool, app_data_dir: PathBuf) Ok(true) => {} Ok(false) => std::thread::sleep(std::time::Duration::from_millis(750)), Err(error) => { - eprintln!("Tagging worker error: {}", error); + log::error!("Tagging worker error: {}", error); tagger_instance = None; std::thread::sleep(std::time::Duration::from_millis(750)); } @@ -369,7 +369,7 @@ fn do_index(app: AppHandle, pool: &DbPool, folder_id: i64, folder_path: PathBuf) if let Some(path) = err.path() { unreadable_prefixes.push(path.to_string_lossy().into_owned()); } - eprintln!( + log::error!( "WalkDir error while scanning folder {}: {}", folder_path.display(), err @@ -649,7 +649,7 @@ fn process_thumbnail_batch( return Ok(false); } - println!("Thumbnail batch claimed: {} items", jobs.len()); + log::info!("Thumbnail batch claimed: {} items", jobs.len()); let (image_jobs, video_jobs): (Vec<_>, Vec<_>) = jobs.into_iter().partition(|job| job.media_kind == "image"); @@ -846,7 +846,7 @@ fn process_embedding_batch( *embedder = Some(ClipImageEmbedder::new()?); } - println!("Embedding batch claimed: {} items", jobs.len()); + log::info!("Embedding batch claimed: {} items", jobs.len()); let folder_ids = jobs.iter().map(|job| job.folder_id).collect::>(); emit_folder_job_progress( app, @@ -897,7 +897,7 @@ fn process_embedding_batch( } } Err(batch_error) => { - eprintln!( + log::error!( "Embedding batch fallback to per-image mode: {}", batch_error ); @@ -946,7 +946,7 @@ fn process_embedding_batch( })?; if !updated_images.is_empty() { - println!("Embedding batch completed: {} items", updated_images.len()); + log::info!("Embedding batch completed: {} items", updated_images.len()); let folder_ids = updated_images .iter() .map(|image| image.folder_id) @@ -962,9 +962,13 @@ fn process_embedding_batch( let write_elapsed = write_started_at.elapsed(); let batch_elapsed = batch_started_at.elapsed(); - println!( + log::info!( "Embedding batch timing: claimed {} in {:?}, infer {:?}, write {:?}, total {:?}", - EMBEDDING_BATCH_SIZE, claim_elapsed, infer_elapsed, write_elapsed, batch_elapsed + EMBEDDING_BATCH_SIZE, + claim_elapsed, + infer_elapsed, + write_elapsed, + batch_elapsed ); Ok(true) @@ -1397,7 +1401,7 @@ impl WatcherHandle { let mut w = self.inner.watcher.lock().unwrap(); if path.is_dir() { if let Err(e) = w.watch(&path, RecursiveMode::Recursive) { - eprintln!("Watcher: failed to watch {:?}: {}", path, e); + log::error!("Watcher: failed to watch {:?}: {}", path, e); } } } @@ -1422,7 +1426,7 @@ impl WatcherHandle { let _ = w.unwatch(old_path); if new_path.is_dir() { if let Err(e) = w.watch(&new_path, RecursiveMode::Recursive) { - eprintln!("Watcher: failed to watch {:?}: {}", new_path, e); + log::error!("Watcher: failed to watch {:?}: {}", new_path, e); } } } @@ -1467,7 +1471,7 @@ pub fn start_watcher(app: AppHandle, pool: DbPool, thumb_dir: PathBuf) -> Watche for path in map.keys() { if path.is_dir() { if let Err(e) = w.watch(path, RecursiveMode::Recursive) { - eprintln!("Watcher: failed to watch {:?}: {}", path, e); + log::error!("Watcher: failed to watch {:?}: {}", path, e); } } } @@ -1593,7 +1597,7 @@ fn process_watcher_path( let conn = match pool.get() { Ok(c) => c, Err(e) => { - eprintln!("Watcher: DB pool error: {}", e); + log::error!("Watcher: DB pool error: {}", e); return; } }; @@ -1628,7 +1632,7 @@ fn process_watcher_path( } } Ok(_) => {} - Err(e) => eprintln!("Watcher: commit error for {:?}: {}", path, e), + Err(e) => log::error!("Watcher: commit error for {:?}: {}", path, e), } } else { // File removed from disk — clean up DB row and thumbnail. @@ -1645,7 +1649,7 @@ fn process_watcher_path( } } Ok(None) => {} // never indexed or already removed - Err(e) => eprintln!("Watcher: lookup error for {:?}: {}", path, e), + Err(e) => log::error!("Watcher: lookup error for {:?}: {}", path, e), } } } @@ -1675,7 +1679,7 @@ fn process_watcher_rename( let conn = match pool.get() { Ok(c) => c, Err(e) => { - eprintln!("Watcher rename: DB pool error: {}", e); + log::error!("Watcher rename: DB pool error: {}", e); return; } }; @@ -1692,7 +1696,7 @@ fn process_watcher_rename( return; } Err(e) => { - eprintln!("Watcher rename: DB lookup error: {}", e); + log::error!("Watcher rename: DB lookup error: {}", e); return; } }; @@ -1717,7 +1721,7 @@ fn process_watcher_rename( new_filename, effective_thumb, ) { - eprintln!("Watcher rename: DB update error: {}", e); + log::error!("Watcher rename: DB update error: {}", e); return; } @@ -1730,6 +1734,6 @@ fn process_watcher_rename( images: vec![record], }, ), - Err(e) => eprintln!("Watcher rename: post-update fetch error: {}", e), + Err(e) => log::error!("Watcher rename: post-update fetch error: {}", e), } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index fe6832d..46b514c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -16,6 +16,28 @@ use tauri::Manager; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + // Must be the first plugin: a second launch hands its args to the + // running instance and exits before anything else initializes. + .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { + if let Some(window) = app.get_webview_window("main") { + let _ = window.unminimize(); + let _ = window.set_focus(); + } + })) + .plugin( + tauri_plugin_log::Builder::new() + .targets([ + tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout), + tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir { + file_name: Some("phokus".into()), + }), + ]) + .level(log::LevelFilter::Info) + .max_file_size(5 * 1024 * 1024) + .rotation_strategy(tauri_plugin_log::RotationStrategy::KeepOne) + .build(), + ) + .plugin(tauri_plugin_window_state::Builder::new().build()) .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_opener::init()) @@ -43,12 +65,12 @@ pub fn run() { let backfilled = db::backfill_embedding_jobs(&conn).expect("Failed to backfill embedding jobs"); if backfilled > 0 { - println!("Backfilled {} embedding jobs.", backfilled); + log::info!("Backfilled {} embedding jobs.", backfilled); } let (orphaned_vectors, missing_vectors) = db::repair_embedding_consistency(&conn) .expect("Failed to repair embedding consistency"); if orphaned_vectors > 0 || missing_vectors > 0 { - println!( + log::info!( "Repaired embedding consistency: removed {} orphaned vectors, requeued {} missing vectors.", orphaned_vectors, missing_vectors ); @@ -58,6 +80,19 @@ pub fn run() { let thumb_dir = app_dir.join("thumbnails"); std::fs::create_dir_all(&thumb_dir).expect("Failed to create thumbnail dir"); + // The asset protocol scope is no longer a blanket "**": thumbnails + // are allowed statically in tauri.conf.json, and each indexed + // folder is allowed here (and in add_folder/update_folder_path). + { + let scope = app.asset_protocol_scope(); + let conn = pool.get().expect("Failed to get connection for asset scope"); + for folder in db::get_folders(&conn).unwrap_or_default() { + if let Err(error) = scope.allow_directory(&folder.path, true) { + log::error!("Failed to allow asset scope for {}: {}", folder.path, error); + } + } + } + let thumbnail_worker_count = std::thread::available_parallelism() .map(|parallelism| StorageProfile::Balanced.thumbnail_workers(parallelism.get())) .unwrap_or(2); diff --git a/src-tauri/src/media.rs b/src-tauri/src/media.rs index d5a4340..a409eca 100644 --- a/src-tauri/src/media.rs +++ b/src-tauri/src/media.rs @@ -43,22 +43,23 @@ impl MediaTools { } auto_download_with_progress(|event| match event { FfmpegDownloadProgressEvent::Starting => { - println!("Downloading bundled FFmpeg..."); + log::info!("Downloading bundled FFmpeg..."); } FfmpegDownloadProgressEvent::Downloading { total_bytes, downloaded_bytes, } => { - println!( + log::info!( "Downloading bundled FFmpeg: {}/{} bytes", - downloaded_bytes, total_bytes + downloaded_bytes, + total_bytes ); } FfmpegDownloadProgressEvent::UnpackingArchive => { - println!("Unpacking bundled FFmpeg..."); + log::info!("Unpacking bundled FFmpeg..."); } FfmpegDownloadProgressEvent::Done => { - println!("Bundled FFmpeg ready."); + log::info!("Bundled FFmpeg ready."); } }) } diff --git a/src-tauri/src/tagger.rs b/src-tauri/src/tagger.rs index 4b0de34..47c5e4b 100644 --- a/src-tauri/src/tagger.rs +++ b/src-tauri/src/tagger.rs @@ -443,7 +443,7 @@ impl WdTagger { let labels = load_labels(&labels_path)?; - println!( + log::info!( "WD tagger loaded in {:?} ({} labels, input {}x{}, {:?} acceleration)", started_at.elapsed(), labels.len(), @@ -519,7 +519,7 @@ impl WdTagger { tags.sort_by(|a, b| b.confidence.total_cmp(&a.confidence)); tags.truncate(max_tags); - println!( + log::info!( "WD tagger: {} tags (threshold={}, rating={}) in {:?} for {}", tags.len(), self.threshold, @@ -559,13 +559,13 @@ fn create_tagger_session(path: &Path, acceleration: TaggerAcceleration) -> Resul let mut builder = match acceleration { TaggerAcceleration::Cpu => { - println!("WD tagger: using CPU execution provider"); + log::info!("WD tagger: using CPU execution provider"); builder } TaggerAcceleration::Auto => builder .with_execution_providers([ep::DirectML::default().build().fail_silently()]) .unwrap_or_else(|error| { - println!("WD tagger: DirectML unavailable, falling back to CPU"); + log::info!("WD tagger: DirectML unavailable, falling back to CPU"); error.recover() }), TaggerAcceleration::Directml => builder diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 70180f3..7a4f93f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -22,11 +22,12 @@ } ], "security": { - "csp": null, + "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' asset: http://asset.localhost data: blob:; media-src 'self' asset: http://asset.localhost; connect-src 'self' ipc: http://ipc.localhost; font-src 'self' data:; object-src 'none'", + "devCsp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' asset: http://asset.localhost data: blob:; media-src 'self' asset: http://asset.localhost; connect-src 'self' ipc: http://ipc.localhost ws://localhost:1420 http://localhost:1420; font-src 'self' data:; object-src 'none'", "assetProtocol": { "enable": true, "scope": [ - "**" + "$APPDATA/thumbnails/**" ] } }