Files
Stirling-PDF/frontend/src/desktop/components/AppProviders.tsx
T
b8ce4e47c1 Preserve local paths for desktop saves (#5543)
# Summary

- Adds desktop file tracking: local paths are preserved and save buttons
now work as expcted (doing Save/Save As as appropriate)
- Adds logic to track whether files are 'dirty' (they've been modified
by some tool, and not saved to disk yet).
- Improves file state UX (dirty vs saved) and close warnings
- Web behaviour should be unaffected by these changes

## Indicators
Files now have indicators in desktop mode to tell you their state.

### File up-to-date with disk

<img width="318" height="393" alt="image"
src="https://github.com/user-attachments/assets/06325f9a-afd7-4c2f-8a5b-6d11e3093115"
/>

### File modified by a tool but not saved to disk yet

<img width="357" height="385" alt="image"
src="https://github.com/user-attachments/assets/1a7716d9-c6f7-4d13-be0d-c1de6493954b"
/>

### File not tracked on disk

<img width="312" height="379" alt="image"
src="https://github.com/user-attachments/assets/9cffe300-bd9a-4e19-97c7-9b98bebefacc"
/>

# Limitations
- It's a bit weird that we still have files stored in indexeddb in the
app, which are still loadable. We might want to change this behaviour in
the future
- Viewer's Save doesn't persist to disk. I've left that out here because
it'd need a lot of testing to make sure the logic's right with making
sure you can leave the Viewer with applying the changes to the PDF
_without_ saving to disk
- There's no current way to do Save As on a file that has already been
persisted to disk - it's only ever Save. Similarly, there's no way to
duplicate a file.

---------

Co-authored-by: James Brunton <jbrunton96@gmail.com>
Co-authored-by: James Brunton <james@stirlingpdf.com>
2026-02-13 23:15:28 +00:00

158 lines
5.0 KiB
TypeScript

import { ReactNode, useEffect, useState } from "react";
import { AppProviders as ProprietaryAppProviders } from "@proprietary/components/AppProviders";
import { DesktopConfigSync } from '@app/components/DesktopConfigSync';
import { DesktopBannerInitializer } from '@app/components/DesktopBannerInitializer';
import { SaveShortcutListener } from '@app/components/SaveShortcutListener';
import { SetupWizard } from '@app/components/SetupWizard';
import { useFirstLaunchCheck } from '@app/hooks/useFirstLaunchCheck';
import { useBackendInitializer } from '@app/hooks/useBackendInitializer';
import { DESKTOP_DEFAULT_APP_CONFIG } from '@app/config/defaultAppConfig';
import { connectionModeService } from '@app/services/connectionModeService';
import { tauriBackendService } from '@app/services/tauriBackendService';
import { authService } from '@app/services/authService';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { isTauri } from '@tauri-apps/api/core';
/**
* Desktop application providers
* Wraps proprietary providers and adds desktop-specific configuration
* - Enables retry logic for app config (needed for Tauri mode when backend is starting)
* - Shows setup wizard on first launch
*/
export function AppProviders({ children }: { children: ReactNode }) {
const { isFirstLaunch, setupComplete } = useFirstLaunchCheck();
const [connectionMode, setConnectionMode] = useState<'saas' | 'selfhosted' | null>(null);
const [authChecked, setAuthChecked] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState(false);
// Load connection mode on mount
useEffect(() => {
void connectionModeService.getCurrentMode().then(setConnectionMode);
}, []);
useEffect(() => {
if (!isFirstLaunch && setupComplete) {
authService.isAuthenticated()
.then(setIsAuthenticated)
.catch(() => setIsAuthenticated(false))
.finally(() => setAuthChecked(true));
} else if (isFirstLaunch && !setupComplete) {
setAuthChecked(true);
setIsAuthenticated(false);
}
}, [isFirstLaunch, setupComplete]);
// Initialize backend health monitoring for self-hosted mode
useEffect(() => {
if (setupComplete && !isFirstLaunch && connectionMode === 'selfhosted') {
void tauriBackendService.initializeExternalBackend();
}
}, [setupComplete, isFirstLaunch, connectionMode]);
// Initialize monitoring for bundled backend (already started in Rust)
// This sets up port detection and health checks
const shouldMonitorBackend = setupComplete && !isFirstLaunch && connectionMode === 'saas';
useBackendInitializer(shouldMonitorBackend);
useEffect(() => {
if (!authChecked) {
return;
}
if (!isTauri()) {
return;
}
const currentWindow = getCurrentWindow();
currentWindow
.show()
.then(() => currentWindow.unminimize().catch(() => {}))
.then(() => currentWindow.setFocus().catch(() => {}))
.then(() => currentWindow.requestUserAttention(1).catch(() => {}))
.catch(() => {});
}, [authChecked]);
if (!authChecked) {
return (
<ProprietaryAppProviders
appConfigRetryOptions={{
maxRetries: 5,
initialDelay: 1000,
}}
appConfigProviderProps={{
initialConfig: DESKTOP_DEFAULT_APP_CONFIG,
bootstrapMode: 'non-blocking',
autoFetch: false,
}}
>
<div style={{ minHeight: '100vh' }} />
</ProprietaryAppProviders>
);
}
// Show setup wizard on first launch
if (isFirstLaunch && !setupComplete) {
return (
<ProprietaryAppProviders
appConfigRetryOptions={{
maxRetries: 5,
initialDelay: 1000,
}}
appConfigProviderProps={{
initialConfig: DESKTOP_DEFAULT_APP_CONFIG,
bootstrapMode: 'non-blocking',
autoFetch: false,
}}
>
<SetupWizard
onComplete={() => {
window.location.reload();
}}
/>
</ProprietaryAppProviders>
);
}
// Show setup wizard when not authenticated (desktop login flow).
if (authChecked && !isAuthenticated) {
return (
<ProprietaryAppProviders
appConfigRetryOptions={{
maxRetries: 5,
initialDelay: 1000,
}}
appConfigProviderProps={{
initialConfig: DESKTOP_DEFAULT_APP_CONFIG,
bootstrapMode: 'non-blocking',
autoFetch: false,
}}
>
<SetupWizard
onComplete={() => {
window.location.reload();
}}
/>
</ProprietaryAppProviders>
);
}
// Normal app flow
return (
<ProprietaryAppProviders
appConfigRetryOptions={{
maxRetries: 5,
initialDelay: 1000, // 1 second, with exponential backoff
}}
appConfigProviderProps={{
initialConfig: DESKTOP_DEFAULT_APP_CONFIG,
bootstrapMode: 'non-blocking',
autoFetch: false,
}}
>
<DesktopConfigSync />
<DesktopBannerInitializer />
<SaveShortcutListener />
{children}
</ProprietaryAppProviders>
);
}