diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c071d77..f92f9da 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -83,8 +83,33 @@ fn apply_effects(app: &tauri::App) -> &'static str { "none" } +/// Detects if portable mode is active (a `data` folder or `portable.dat` file exists next to the executable). +fn detect_portable_dir() -> Option { + let exe = std::env::current_exe().ok()?; + let exe_dir = exe.parent()?; + let data_dir = exe_dir.join("data"); + let marker = exe_dir.join("portable.dat"); + if data_dir.is_dir() || marker.is_file() { + let _ = std::fs::create_dir_all(&data_dir); + Some(data_dir) + } else { + None + } +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + let portable_dir = detect_portable_dir(); + if let Some(ref p) = portable_dir { + // Redirect WebView2 data folder on Windows so no browser cache/state leaks to the host machine. + #[cfg(windows)] + { + let webview_dir = p.join("webview"); + let _ = std::fs::create_dir_all(&webview_dir); + std::env::set_var("WEBVIEW2_USER_DATA_FOLDER", webview_dir); + } + } + let app = tauri::Builder::default() .plugin(tauri_plugin_window_state::Builder::default().build()) .plugin(tauri_plugin_dialog::init()) @@ -109,16 +134,26 @@ pub fn run() { respond(responder, png.map(|p| (p.to_vec(), if p.starts_with(b"\x89PNG") { "image/png" } else { "image/svg+xml" }))); }); }) - .setup(|app| { + .setup(move |app| { let handle = app.handle().clone(); - let config_dir = app.path().app_config_dir()?; - let data_dir = app.path().app_data_dir()?; + let (config_dir, data_dir, cache_dir) = if let Some(ref p) = portable_dir { + let conf = p.join("config"); + let data = p.join("data"); + let cache = p.join("cache"); + let _ = std::fs::create_dir_all(&conf); + let _ = std::fs::create_dir_all(&data); + let _ = std::fs::create_dir_all(&cache); + (conf, data, Some(cache)) + } else { + (app.path().app_config_dir()?, app.path().app_data_dir()?, app.path().app_cache_dir().ok()) + }; + platform::set_data_dir(data_dir.clone()); let notify = handle.clone(); network::set_notify(move || { let _ = notify.emit("network-changed", ()); }); - if let Ok(cache) = app.path().app_cache_dir() { + if let Some(cache) = cache_dir { thumbs::set_cache_dir(cache.join("thumbs")); } let remote = Arc::new(remote::Registry::default()); diff --git a/src-tauri/src/search.rs b/src-tauri/src/search.rs index 9f65276..394fca3 100644 --- a/src-tauri/src/search.rs +++ b/src-tauri/src/search.rs @@ -106,6 +106,71 @@ struct Compiled { text: Option, } +/// Avoid contending on the shared result queue for every match. Branch view +/// can match nearly every file, unlike ordinary searches where hits are rare. +struct HitBatch { + pending: Arc>>, + items: Vec, +} + +impl HitBatch { + fn new(pending: Arc>>) -> Self { + Self { pending, items: Vec::with_capacity(128) } + } + + fn push(&mut self, hit: Hit) { + self.items.push(hit); + if self.items.len() >= 128 { + self.flush(); + } + } + + fn flush(&mut self) { + if !self.items.is_empty() { + self.pending.lock().append(&mut self.items); + } + } +} + +impl Drop for HitBatch { + fn drop(&mut self) { + self.flush(); + } +} + +/// Keep the progress counter mostly local to each walker thread as well. +struct ScanBatch { + total: Arc, + count: u64, +} + +impl ScanBatch { + fn new(total: Arc) -> Self { + Self { total, count: 0 } + } + + fn next(&mut self) -> u64 { + self.count += 1; + if self.count >= 256 { + self.flush(); + } + self.total.load(Ordering::Relaxed) + self.count + } + + fn flush(&mut self) { + if self.count > 0 { + self.total.fetch_add(self.count, Ordering::Relaxed); + self.count = 0; + } + } +} + +impl Drop for ScanBatch { + fn drop(&mut self) { + self.flush(); + } +} + fn compile(p: &SearchParams) -> AppResult { let name = if p.name.trim().is_empty() { NameMatch::Any @@ -315,6 +380,8 @@ pub fn run(params: SearchParams, channel: Channel, cancel: Arc, cancel: Arc, cancel: Arc= limit { truncated.store(true, Ordering::Relaxed); return ignore::WalkState::Quit; diff --git a/src/components/FavoritesBar.svelte b/src/components/FavoritesBar.svelte new file mode 100644 index 0000000..c808309 --- /dev/null +++ b/src/components/FavoritesBar.svelte @@ -0,0 +1,125 @@ + + + + + diff --git a/src/components/FileGrid.svelte b/src/components/FileGrid.svelte index af440a6..65f6c75 100644 --- a/src/components/FileGrid.svelte +++ b/src/components/FileGrid.svelte @@ -6,7 +6,7 @@ import { drag } from '../lib/dnd.svelte'; import { setMetrics } from '../lib/panelKeys.svelte'; import { isWindows } from '../lib/platform'; - import { backgroundPointerDown, itemPointerDown } from '../lib/pointer'; + import { backgroundPointerDown, canOpenContextMenu, itemPointerDown } from '../lib/pointer'; import type { SideName } from '../lib/state/app.svelte'; import { settings } from '../lib/state/settings.svelte'; import type { Tab } from '../lib/state/tab.svelte'; @@ -105,8 +105,10 @@ function onContext(e: MouseEvent, entry: Entry | null) { e.preventDefault(); - if (entry && !(entry.flags & F.UP)) void entryMenu(tab); - else void backgroundMenu(tab); + if (canOpenContextMenu(e, entry)) { + if (entry && !(entry.flags & F.UP)) void entryMenu(tab); + else void backgroundMenu(tab); + } } diff --git a/src/components/FileList.svelte b/src/components/FileList.svelte index 7b79c67..1dcb14f 100644 --- a/src/components/FileList.svelte +++ b/src/components/FileList.svelte @@ -11,7 +11,7 @@ import { localeTag, t } from '../lib/i18n/index.svelte'; import { setMetrics } from '../lib/panelKeys.svelte'; import { isWindows } from '../lib/platform'; - import { backgroundPointerDown, itemPointerDown } from '../lib/pointer'; + import { backgroundPointerDown, canOpenContextMenu, itemPointerDown } from '../lib/pointer'; import type { SideName } from '../lib/state/app.svelte'; import { settings } from '../lib/state/settings.svelte'; import type { Tab } from '../lib/state/tab.svelte'; @@ -135,8 +135,10 @@ function onContext(e: MouseEvent, entry: Entry | null) { e.preventDefault(); - if (entry && !(entry.flags & F.UP)) void entryMenu(tab); - else void backgroundMenu(tab); + if (canOpenContextMenu(e, entry)) { + if (entry && !(entry.flags & F.UP)) void entryMenu(tab); + else void backgroundMenu(tab); + } } diff --git a/src/components/Toolbar.svelte b/src/components/Toolbar.svelte index d2afd04..5156ec5 100644 --- a/src/components/Toolbar.svelte +++ b/src/components/Toolbar.svelte @@ -18,6 +18,7 @@ import Server from '@lucide/svelte/icons/server'; import Settings from '@lucide/svelte/icons/settings'; import SquareTerminal from '@lucide/svelte/icons/square-terminal'; + import FavoritesBar from './FavoritesBar.svelte'; import { run, shortcutOf } from '../lib/commands'; import { t } from '../lib/i18n/index.svelte'; import { isMac, shortcutLabel } from '../lib/platform'; @@ -108,7 +109,8 @@ - + +
@@ -134,6 +136,7 @@ align-items: center; gap: 10px; min-width: 0; + flex: 1; } .lead { display: none; @@ -147,17 +150,16 @@ } /* macOS: actions centred in the unified title bar, like Finder/Safari. */ .toolbar.mac { - display: grid; - grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + display: flex; padding: 2px 10px 0 0; } .toolbar.mac .lead { display: block; align-self: stretch; min-width: var(--chrome-left); + flex: none; } .toolbar.mac .trail { - justify-self: end; margin-left: 0; } .group { @@ -165,6 +167,12 @@ align-items: center; gap: 2px; } + .divider { + width: 1px; + height: 18px; + flex: none; + background: var(--border-strong); + } .segmented { display: flex; align-items: center; diff --git a/src/components/dialogs/SettingsDialog.svelte b/src/components/dialogs/SettingsDialog.svelte index 69acdd0..32de5cf 100644 --- a/src/components/dialogs/SettingsDialog.svelte +++ b/src/components/dialogs/SettingsDialog.svelte @@ -155,6 +155,7 @@ +