diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2097443..1c5bbbf 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -48,6 +48,8 @@ pointy = "0.4" url = "2.5.7" filetime = "0.2" pmtiles = { version = "0.19", default-features = false, features = ["mmap-async-tokio", "http-async", "write", "reqwest-rustls"] } +[target.'cfg(target_os = "linux")'.dependencies] +gtk = "0.18.2" [dev-dependencies] tempfile = "3" diff --git a/src-tauri/src/commands/archive.rs b/src-tauri/src/commands/archive.rs index f8af56d..735130d 100644 --- a/src-tauri/src/commands/archive.rs +++ b/src-tauri/src/commands/archive.rs @@ -2,6 +2,10 @@ use std::backtrace::Backtrace; use std::path::{Path, PathBuf}; use serde::{Serialize}; use tauri::{Emitter, Manager}; +#[cfg(target_os = "linux")] +use gtk::prelude::{BinExt, Cast, GtkWindowExt, HeaderBarExt}; +#[cfg(target_os = "linux")] +use gtk::{EventBox, HeaderBar}; use crate::dwca::Archive; use crate::error::{ChuckError, Result}; @@ -99,7 +103,11 @@ pub(crate) fn get_archives_dir(app: tauri::AppHandle) -> R } #[tauri::command] -pub async fn open_archive(app: tauri::AppHandle, path: String) -> Result { +pub async fn open_archive( + app: tauri::AppHandle, + window: tauri::WebviewWindow, + path: String, +) -> Result { use std::sync::mpsc; let base_dir = get_archives_dir(app.clone())?; @@ -150,6 +158,10 @@ pub async fn open_archive(app: tauri::AppHandle, path: String) -> Result { @@ -191,16 +203,49 @@ pub fn get_opened_file(app: tauri::AppHandle) -> Option { state.0.lock().ok()?.take() } +fn set_archive_window_title(window: &tauri::WebviewWindow, info: &ArchiveInfo) { + let title = format!("{} \u{2013} {} occurrences", info.name, info.core_count); + if let Err(e) = window.set_title(&title) { + log::warn!("Failed to set window title: {e}"); + } + + // In GTK (default in Ubuntu), the above method of setting the window + // title doesn't change the HeaderBar the user actually sees, so we're + // using the approach described in + // https://github.com/tauri-apps/tauri/issues/13749#issuecomment-3027697386. + // This closure / ? approach is to avoid a ton of unwraps. + #[cfg(target_os = "linux")] + (|| -> Option<()> { + let header_bar = window.gtk_window().ok()? + .titlebar()? + .downcast::().ok()? + .child()? + .downcast::().ok()?; + header_bar.set_title(Some(&title)); + Some(()) + })(); +} + #[tauri::command] pub fn current_archive(app: tauri::AppHandle) -> Result { - Archive::current(&get_archives_dir(app)?).map_err(|e| { + let info = Archive::current(&get_archives_dir(app.clone())?).map_err(|e| { log::error!( "Failed to get current archive: {}, backtrace: {}", e, Backtrace::capture() ); e - })?.info() + })?.info()?; + // Set window title in a spawned task to avoid interfering with the command response. + // Using WebviewWindow as a command parameter breaks the return value in Tauri 2. + let app_clone = app; + let info_clone = info.clone(); + tauri::async_runtime::spawn(async move { + if let Some(window) = app_clone.get_webview_window("main") { + set_archive_window_title(&window, &info_clone); + } + }); + Ok(info) } #[tauri::command] diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 7a545f9..787cbd5 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -26,7 +26,6 @@ import { exportDwca, exportKml, getCurrentWebview, - getCurrentWindow, invoke, listen, showOpenDialog, @@ -316,12 +315,10 @@ async function handleSearchChange(params: SearchParams) { } } -// Set window title and initialize filtered total when archive loads +// Initialize filtered total and trigger initial search when archive loads. +// Window title is set from Rust (see open_archive / current_archive commands). $effect(() => { if (archive) { - getCurrentWindow().setTitle( - `${archive.name} – ${archive.coreCount} occurrences`, - ); filteredTotal = archive.coreCount; // Set default sort to Core ID diff --git a/tests/app.spec.ts b/tests/app.spec.ts index 69c825e..ea880e0 100644 --- a/tests/app.spec.ts +++ b/tests/app.spec.ts @@ -55,17 +55,6 @@ test.describe('Frontend', () => { expect(rows.length).toBeGreaterThan(0); }); - test('should display archive info in window title', async ({ page }) => { - // This tests that the mock window.setTitle is being called - // In a real app, we'd check the actual window title, but in our mock - // we just verify the function is called (checked via console logs) - - await openArchive(page); - - // Just verify the main content loaded, which triggers setTitle - await expect(page.locator('main')).toBeVisible(); - }); - test('should display sort controls in filters sidebar', async ({ page }) => { await openArchive(page);