diff --git a/src-tauri/capabilities/properties.json b/src-tauri/capabilities/properties.json index 7701e20..8968b07 100644 --- a/src-tauri/capabilities/properties.json +++ b/src-tauri/capabilities/properties.json @@ -5,7 +5,10 @@ "windows": ["properties-*"], "permissions": [ "core:window:allow-close", + "core:window:allow-minimize", "core:window:allow-set-title", + "core:window:allow-start-dragging", + "core:window:allow-toggle-maximize", "core:event:allow-listen", "core:event:allow-unlisten", "dialog:default", diff --git a/src-tauri/src/properties_window.rs b/src-tauri/src/properties_window.rs index 17926f7..23f0325 100644 --- a/src-tauri/src/properties_window.rs +++ b/src-tauri/src/properties_window.rs @@ -186,6 +186,7 @@ impl PropertiesWindowRegistry { Ok(self.session_for_window(label)?.as_deref() == Some(session_id)) } + #[cfg(test)] pub fn is_ready(&self, label: &str) -> Result { Ok(self .state @@ -238,6 +239,17 @@ pub fn ensure_main_window(caller: &tauri::WebviewWindow) -> Result<(), String> { .ok_or_else(|| "This command is available only to the main window".to_string()) } +fn emit_to_main( + app: &tauri::AppHandle, + event: &str, + payload: T, +) -> Result<(), String> { + let main_window = app + .get_webview_window(MAIN_WINDOW_LABEL) + .ok_or_else(|| "Firelink main window is unavailable".to_string())?; + main_window.emit(event, payload).map_err(|error| error.to_string()) +} + fn registered_download_for_caller( caller: &tauri::WebviewWindow, registry: &PropertiesWindowRegistry, @@ -319,9 +331,10 @@ pub fn open_download_properties_window( let label = registry.allocate(&id)?; if let Some(window) = app.get_webview_window(&label) { - if !registry.is_ready(&label)? { - return Ok(label); - } + // Visibility belongs to the native window owner, not to the renderer + // handshake. A delayed or lost snapshot must leave a usable loading + // window on screen instead of making the open request appear to do + // nothing. let _ = window.unminimize(); let _ = window.show(); let _ = window.set_focus(); @@ -337,21 +350,25 @@ pub fn open_download_properties_window( .min_inner_size(760.0, 560.0) .resizable(true) .always_on_top(false) - .visible(false); + .visible(true); #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] let builder = builder.decorations(false); let build_result = builder.build(); + if let Ok(window) = &build_result { + // Keep the initial loading/error shell visible even if the child + // renderer has not completed its bridge handshake yet. + let _ = window.show(); + let _ = window.set_focus(); + } if let Err(error) = build_result { // Two rapid main-window requests can race between the native lookup // above and builder creation. If the first request won, retain the // registry entry and focus its window instead of treating the second // request as a failed open. if let Some(window) = app.get_webview_window(&label) { - if registry.is_ready(&label)? { - let _ = window.unminimize(); - let _ = window.show(); - let _ = window.set_focus(); - } + let _ = window.unminimize(); + let _ = window.show(); + let _ = window.set_focus(); return Ok(label); } let _ = registry.remove_window(&label); @@ -385,8 +402,8 @@ pub fn properties_window_send_ready( } return Err(error); } - app.emit_to( - MAIN_WINDOW_LABEL, + emit_to_main( + &app, PROPERTIES_WINDOW_READY_EVENT, PropertiesWindowReadyEvent { window_label: caller.label().to_string(), @@ -394,7 +411,6 @@ pub fn properties_window_send_ready( session_id, }, ) - .map_err(|error| error.to_string()) } #[tauri::command] @@ -438,8 +454,8 @@ pub fn properties_window_send_action( if !registry.session_matches(caller.label(), &session_id)? { return Err("Properties window session is no longer current".to_string()); } - app.emit_to( - MAIN_WINDOW_LABEL, + emit_to_main( + &app, PROPERTIES_WINDOW_ACTION_REQUEST_EVENT, PropertiesWindowActionEvent { window_label: caller.label().to_string(), @@ -450,7 +466,6 @@ pub fn properties_window_send_action( payload, }, ) - .map_err(|error| error.to_string()) } #[tauri::command] diff --git a/src/components/PropertiesWindowApp.tsx b/src/components/PropertiesWindowApp.tsx index 6a8903f..2e78c64 100644 --- a/src/components/PropertiesWindowApp.tsx +++ b/src/components/PropertiesWindowApp.tsx @@ -416,6 +416,11 @@ export const PropertiesWindowApp = () => { window.clearInterval(readyRetryTimer); readyRetryTimer = undefined; } + } catch (error) { + // Native visibility is established by the opener. Keep the + // loading shell usable when the optional ready-state update is + // delayed or rejected, and let the next snapshot retry it. + setErrorMessage(errorText(error)); } finally { revealInFlightRef.current = false; } diff --git a/src/main.tsx b/src/main.tsx index 4042a9e..3f1ca1d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -7,13 +7,11 @@ import "@fontsource-variable/outfit/wght.css"; import "@fontsource-variable/roboto/wght.css"; import "@fontsource-variable/vazirmatn/wght.css"; import "./index.css"; -import App from "./App"; import { i18nReady } from "./i18n"; import { ErrorBoundary } from "./components/ErrorBoundary"; import { ToastProvider } from "./contexts/ToastContext"; import { error as logError, warn as logWarn, initLogger } from "./utils/logger"; import { getCurrentWindow } from '@tauri-apps/api/window'; -import { PropertiesWindowApp } from './components/PropertiesWindowApp'; const isPropertiesWindow = getCurrentWindow().label.startsWith('properties-'); @@ -45,14 +43,22 @@ console.warn = (...values: unknown[]) => { }; const rootElement = document.getElementById("root"); -const renderApp = () => { +const renderApp = async () => { if (!rootElement) return; + // Keep the child entrypoint isolated from the main application module. App + // imports the persistent Zustand stores, whose module initialization issues + // main-window-only IPC commands. Loading it in a Properties child creates a + // second persistence owner and can race the bridge handshake. + const RootComponent = isPropertiesWindow + ? (await import('./components/PropertiesWindowApp')).PropertiesWindowApp + : (await import('./App')).default; + createRoot(rootElement).render( - {isPropertiesWindow ? : } + , @@ -61,7 +67,7 @@ const renderApp = () => { void i18nReady.then(renderApp).catch(error => { console.error('Failed to initialize localization:', error); - renderApp(); + void renderApp(); }); // Prevent the webview's default context menu ("Reload", etc.) on right-click.