mirror of
https://github.com/nimbold/Firelink.git
synced 2026-09-10 09:45:44 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8931f14f72 |
@@ -18200,12 +18200,9 @@ fn toggle_log_pause(caller: tauri::WebviewWindow, pause: bool) -> Result<(), Str
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn is_log_paused() -> bool {
|
||||
// This read is needed during renderer bootstrap, including by standalone
|
||||
// Properties windows. Avoid extracting a WebviewWindow here: on Windows,
|
||||
// WebView2 can invoke the command while its host is still initializing.
|
||||
// Mutating logging commands remain restricted to the main window below.
|
||||
LOG_PAUSED.load(std::sync::atomic::Ordering::Relaxed)
|
||||
fn is_log_paused(caller: tauri::WebviewWindow) -> bool {
|
||||
properties_window::ensure_main_window(&caller).is_ok()
|
||||
&& LOG_PAUSED.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -19903,6 +19900,7 @@ pub fn run() {
|
||||
mark_main_window_startup_complete(app_handle);
|
||||
#[cfg(target_os = "windows")]
|
||||
reveal_main_window(app_handle);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
restore_pending_main_window(app_handle);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
+3
-37
@@ -15,11 +15,11 @@ import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import { initDownloadListener } from './store/downloadStore';
|
||||
import {
|
||||
subscribeToSettingsPersistenceErrors,
|
||||
useSettingsStore,
|
||||
waitForSettingsPersistence
|
||||
useSettingsStore
|
||||
} from "./store/useSettingsStore";
|
||||
import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/plugin-notification';
|
||||
import { WindowControls } from "./components/WindowControls";
|
||||
import { PropertiesWindowBridgeHost } from "./components/PropertiesWindowBridgeHost";
|
||||
import { useToast } from "./contexts/ToastContext";
|
||||
import { setLogStreamActive } from './utils/logger';
|
||||
import { updateDockBadge } from './utils/dockBadge';
|
||||
@@ -39,9 +39,7 @@ import { changeAppLocale, localeDirection, resolveAppLocale, syncDocumentLocale
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { formatDownloadBytes } from './utils/downloadProgress';
|
||||
import { synchronizeDocumentAppearance } from './utils/documentAppearance';
|
||||
import { createMainWindowSizePersistence } from './utils/mainWindowState';
|
||||
import { createSidebarResizeSession } from './utils/sidebarResize';
|
||||
import type { MainWindowSize } from './bindings/MainWindowSize';
|
||||
import {
|
||||
beginSchedulerControl,
|
||||
consumeSchedulerHandoffIds,
|
||||
@@ -493,44 +491,14 @@ function App() {
|
||||
useEffect(() => {
|
||||
const disposePersistence = initializeDownloadPersistence(getCurrentWindow().label);
|
||||
let active = true;
|
||||
let exitRequested = false;
|
||||
let exiting = false;
|
||||
let settingsHydrated = useSettingsStore.persist.hasHydrated();
|
||||
let latestSizeBeforeHydration: MainWindowSize | null = null;
|
||||
const unlistenSettingsHydration = settingsHydrated
|
||||
? null
|
||||
: useSettingsStore.persist.onFinishHydration(() => {
|
||||
settingsHydrated = true;
|
||||
const size = latestSizeBeforeHydration;
|
||||
latestSizeBeforeHydration = null;
|
||||
if (size && active && !exitRequested && !exiting) {
|
||||
useSettingsStore.getState().setMainWindowSize(size);
|
||||
}
|
||||
});
|
||||
const mainWindowSizePersistence = createMainWindowSizePersistence({
|
||||
appWindow: getCurrentWindow(),
|
||||
onSize: size => {
|
||||
if (!active || exiting) return;
|
||||
if (!settingsHydrated) {
|
||||
latestSizeBeforeHydration = size;
|
||||
return;
|
||||
}
|
||||
useSettingsStore.getState().setMainWindowSize(size);
|
||||
}
|
||||
});
|
||||
let cleanupListeners: (() => void) | null = null;
|
||||
let unlistenExit: (() => void) | null = null;
|
||||
const exitListener = listen('app-exit-requested', async () => {
|
||||
exitRequested = true;
|
||||
try {
|
||||
await mainWindowSizePersistence.flush();
|
||||
await waitForSettingsPersistence();
|
||||
await flushDownloadPersistence();
|
||||
} catch (error) {
|
||||
console.error('Failed to flush download state before exit:', error);
|
||||
} finally {
|
||||
exiting = true;
|
||||
latestSizeBeforeHydration = null;
|
||||
await invoke('ack_frontend_exit').catch(error => {
|
||||
console.error('Failed to acknowledge frontend exit flush:', error);
|
||||
});
|
||||
@@ -549,7 +517,6 @@ function App() {
|
||||
let unlistenDeepLink: (() => void) | null = null;
|
||||
const disposeListeners = () => {
|
||||
void queueFrontendReadyUpdate(false).catch(() => {});
|
||||
mainWindowSizePersistence.dispose();
|
||||
unlistenExit?.();
|
||||
unlistenExit = null;
|
||||
unlistenTerminalState?.();
|
||||
@@ -755,8 +722,6 @@ function App() {
|
||||
cleanupListeners = null;
|
||||
unlistenExit?.();
|
||||
unlistenExit = null;
|
||||
unlistenSettingsHydration?.();
|
||||
mainWindowSizePersistence.dispose();
|
||||
disposePersistence();
|
||||
};
|
||||
}, [addToast, enqueueAddInput, processExtensionDownload, queueFrontendReadyUpdate]);
|
||||
@@ -1286,6 +1251,7 @@ function App() {
|
||||
|
||||
{isAddModalOpen && <AddDownloadsModal />}
|
||||
|
||||
<PropertiesWindowBridgeHost />
|
||||
{isDeleteModalOpen && (
|
||||
<Suspense fallback={null}>
|
||||
<DeleteConfirmationModal />
|
||||
|
||||
+9
-59
@@ -16,29 +16,7 @@ import { invokeCommand as invoke } from './ipc';
|
||||
|
||||
const isPropertiesWindow = getCurrentWindow().label.startsWith('properties-');
|
||||
|
||||
// WebView2 can overflow its native call stack when the renderer sends IPC
|
||||
// during document bootstrapping. Keep all renderer-to-native startup work
|
||||
// behind the document load boundary, not just the first logger query. This is
|
||||
// also needed for Zustand persistence, whose module initialization reads the
|
||||
// native database before React mounts.
|
||||
const documentLoaded = new Promise<void>((resolve) => {
|
||||
const releaseAfterNativeLoad = () => {
|
||||
// The load event is dispatched from WebView2's navigation callback. Move
|
||||
// renderer startup to the next task so its first IPC cannot re-enter that
|
||||
// native callback stack.
|
||||
window.setTimeout(resolve, 0);
|
||||
};
|
||||
|
||||
if (document.readyState === 'complete') {
|
||||
releaseAfterNativeLoad();
|
||||
return;
|
||||
}
|
||||
window.addEventListener('load', releaseAfterNativeLoad, { once: true });
|
||||
});
|
||||
|
||||
void documentLoaded.then(() => {
|
||||
void initLogger();
|
||||
});
|
||||
void initLogger();
|
||||
|
||||
const serializeConsoleArguments = (values: unknown[]) => values.map(value => {
|
||||
if (value instanceof Error) return `${value.name}: ${value.message}\n${value.stack || ''}`;
|
||||
@@ -58,13 +36,11 @@ const originalConsoleError = console.error.bind(console);
|
||||
const originalConsoleWarn = console.warn.bind(console);
|
||||
console.error = (...values: unknown[]) => {
|
||||
originalConsoleError(...values);
|
||||
const message = redactConsoleMessage(serializeConsoleArguments(values));
|
||||
void documentLoaded.then(() => logError(message)).catch(() => undefined);
|
||||
void logError(redactConsoleMessage(serializeConsoleArguments(values))).catch(() => undefined);
|
||||
};
|
||||
console.warn = (...values: unknown[]) => {
|
||||
originalConsoleWarn(...values);
|
||||
const message = redactConsoleMessage(serializeConsoleArguments(values));
|
||||
void documentLoaded.then(() => logWarn(message)).catch(() => undefined);
|
||||
void logWarn(redactConsoleMessage(serializeConsoleArguments(values))).catch(() => undefined);
|
||||
};
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
@@ -99,46 +75,20 @@ const PropertiesStartupFailure = () => (
|
||||
</main>
|
||||
);
|
||||
|
||||
const MainStartupFailure = () => (
|
||||
<main className="flex h-screen min-h-0 flex-col items-center justify-center gap-4 bg-main-bg p-6 text-text-primary">
|
||||
<p role="alert">Firelink could not be loaded.</p>
|
||||
<button
|
||||
type="button"
|
||||
className="app-button app-button-primary px-3 text-xs"
|
||||
onClick={() => {
|
||||
void getCurrentWindow().close().catch(error => {
|
||||
console.error('[MainStartupFailure] close failed', error);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</main>
|
||||
);
|
||||
|
||||
const renderMainApp = async () => {
|
||||
if (!rootElement) return;
|
||||
|
||||
await documentLoaded;
|
||||
|
||||
try {
|
||||
// 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 = (await import('./App')).default;
|
||||
renderRoot(RootComponent);
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Firelink:', error);
|
||||
renderRoot(MainStartupFailure);
|
||||
}
|
||||
// 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 = (await import('./App')).default;
|
||||
renderRoot(RootComponent);
|
||||
};
|
||||
|
||||
const renderPropertiesApp = async () => {
|
||||
if (!rootElement) return;
|
||||
|
||||
await documentLoaded;
|
||||
|
||||
try {
|
||||
// Properties starts with the synchronous English catalog and changes locale
|
||||
// after its first paint. Waiting for a lazy locale chunk here delays the
|
||||
|
||||
Reference in New Issue
Block a user