fix(properties): restore standalone window startup

This commit is contained in:
NimBold
2026-08-05 22:57:58 +03:30
parent b36f55e5b5
commit 5d90b07fd7
4 changed files with 49 additions and 20 deletions
+3
View File
@@ -5,7 +5,10 @@
"windows": ["properties-*"], "windows": ["properties-*"],
"permissions": [ "permissions": [
"core:window:allow-close", "core:window:allow-close",
"core:window:allow-minimize",
"core:window:allow-set-title", "core:window:allow-set-title",
"core:window:allow-start-dragging",
"core:window:allow-toggle-maximize",
"core:event:allow-listen", "core:event:allow-listen",
"core:event:allow-unlisten", "core:event:allow-unlisten",
"dialog:default", "dialog:default",
+30 -15
View File
@@ -186,6 +186,7 @@ impl PropertiesWindowRegistry {
Ok(self.session_for_window(label)?.as_deref() == Some(session_id)) Ok(self.session_for_window(label)?.as_deref() == Some(session_id))
} }
#[cfg(test)]
pub fn is_ready(&self, label: &str) -> Result<bool, String> { pub fn is_ready(&self, label: &str) -> Result<bool, String> {
Ok(self Ok(self
.state .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()) .ok_or_else(|| "This command is available only to the main window".to_string())
} }
fn emit_to_main<T: Serialize + Clone>(
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( fn registered_download_for_caller(
caller: &tauri::WebviewWindow, caller: &tauri::WebviewWindow,
registry: &PropertiesWindowRegistry, registry: &PropertiesWindowRegistry,
@@ -319,9 +331,10 @@ pub fn open_download_properties_window(
let label = registry.allocate(&id)?; let label = registry.allocate(&id)?;
if let Some(window) = app.get_webview_window(&label) { if let Some(window) = app.get_webview_window(&label) {
if !registry.is_ready(&label)? { // Visibility belongs to the native window owner, not to the renderer
return Ok(label); // 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.unminimize();
let _ = window.show(); let _ = window.show();
let _ = window.set_focus(); let _ = window.set_focus();
@@ -337,21 +350,25 @@ pub fn open_download_properties_window(
.min_inner_size(760.0, 560.0) .min_inner_size(760.0, 560.0)
.resizable(true) .resizable(true)
.always_on_top(false) .always_on_top(false)
.visible(false); .visible(true);
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
let builder = builder.decorations(false); let builder = builder.decorations(false);
let build_result = builder.build(); 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 { if let Err(error) = build_result {
// Two rapid main-window requests can race between the native lookup // Two rapid main-window requests can race between the native lookup
// above and builder creation. If the first request won, retain the // above and builder creation. If the first request won, retain the
// registry entry and focus its window instead of treating the second // registry entry and focus its window instead of treating the second
// request as a failed open. // request as a failed open.
if let Some(window) = app.get_webview_window(&label) { if let Some(window) = app.get_webview_window(&label) {
if registry.is_ready(&label)? { let _ = window.unminimize();
let _ = window.unminimize(); let _ = window.show();
let _ = window.show(); let _ = window.set_focus();
let _ = window.set_focus();
}
return Ok(label); return Ok(label);
} }
let _ = registry.remove_window(&label); let _ = registry.remove_window(&label);
@@ -385,8 +402,8 @@ pub fn properties_window_send_ready(
} }
return Err(error); return Err(error);
} }
app.emit_to( emit_to_main(
MAIN_WINDOW_LABEL, &app,
PROPERTIES_WINDOW_READY_EVENT, PROPERTIES_WINDOW_READY_EVENT,
PropertiesWindowReadyEvent { PropertiesWindowReadyEvent {
window_label: caller.label().to_string(), window_label: caller.label().to_string(),
@@ -394,7 +411,6 @@ pub fn properties_window_send_ready(
session_id, session_id,
}, },
) )
.map_err(|error| error.to_string())
} }
#[tauri::command] #[tauri::command]
@@ -438,8 +454,8 @@ pub fn properties_window_send_action(
if !registry.session_matches(caller.label(), &session_id)? { if !registry.session_matches(caller.label(), &session_id)? {
return Err("Properties window session is no longer current".to_string()); return Err("Properties window session is no longer current".to_string());
} }
app.emit_to( emit_to_main(
MAIN_WINDOW_LABEL, &app,
PROPERTIES_WINDOW_ACTION_REQUEST_EVENT, PROPERTIES_WINDOW_ACTION_REQUEST_EVENT,
PropertiesWindowActionEvent { PropertiesWindowActionEvent {
window_label: caller.label().to_string(), window_label: caller.label().to_string(),
@@ -450,7 +466,6 @@ pub fn properties_window_send_action(
payload, payload,
}, },
) )
.map_err(|error| error.to_string())
} }
#[tauri::command] #[tauri::command]
+5
View File
@@ -416,6 +416,11 @@ export const PropertiesWindowApp = () => {
window.clearInterval(readyRetryTimer); window.clearInterval(readyRetryTimer);
readyRetryTimer = undefined; 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 { } finally {
revealInFlightRef.current = false; revealInFlightRef.current = false;
} }
+11 -5
View File
@@ -7,13 +7,11 @@ import "@fontsource-variable/outfit/wght.css";
import "@fontsource-variable/roboto/wght.css"; import "@fontsource-variable/roboto/wght.css";
import "@fontsource-variable/vazirmatn/wght.css"; import "@fontsource-variable/vazirmatn/wght.css";
import "./index.css"; import "./index.css";
import App from "./App";
import { i18nReady } from "./i18n"; import { i18nReady } from "./i18n";
import { ErrorBoundary } from "./components/ErrorBoundary"; import { ErrorBoundary } from "./components/ErrorBoundary";
import { ToastProvider } from "./contexts/ToastContext"; import { ToastProvider } from "./contexts/ToastContext";
import { error as logError, warn as logWarn, initLogger } from "./utils/logger"; import { error as logError, warn as logWarn, initLogger } from "./utils/logger";
import { getCurrentWindow } from '@tauri-apps/api/window'; import { getCurrentWindow } from '@tauri-apps/api/window';
import { PropertiesWindowApp } from './components/PropertiesWindowApp';
const isPropertiesWindow = getCurrentWindow().label.startsWith('properties-'); const isPropertiesWindow = getCurrentWindow().label.startsWith('properties-');
@@ -45,14 +43,22 @@ console.warn = (...values: unknown[]) => {
}; };
const rootElement = document.getElementById("root"); const rootElement = document.getElementById("root");
const renderApp = () => { const renderApp = async () => {
if (!rootElement) return; 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( createRoot(rootElement).render(
<StrictMode> <StrictMode>
<ErrorBoundary> <ErrorBoundary>
<ToastProvider> <ToastProvider>
{isPropertiesWindow ? <PropertiesWindowApp /> : <App />} <RootComponent />
</ToastProvider> </ToastProvider>
</ErrorBoundary> </ErrorBoundary>
</StrictMode>, </StrictMode>,
@@ -61,7 +67,7 @@ const renderApp = () => {
void i18nReady.then(renderApp).catch(error => { void i18nReady.then(renderApp).catch(error => {
console.error('Failed to initialize localization:', error); console.error('Failed to initialize localization:', error);
renderApp(); void renderApp();
}); });
// Prevent the webview's default context menu ("Reload", etc.) on right-click. // Prevent the webview's default context menu ("Reload", etc.) on right-click.