Add prompt to make Stirling your default PDF app

This commit is contained in:
James Brunton
2025-11-13 12:15:03 +00:00
parent 50760b5302
commit cb2446ae83
10 changed files with 507 additions and 2 deletions

View File

@@ -2,13 +2,18 @@ import { ReactNode } from "react";
import { AppProviders as ProprietaryAppProviders } from "@proprietary/components/AppProviders";
import { DesktopConfigSync } from '@app/components/DesktopConfigSync';
import { DESKTOP_DEFAULT_APP_CONFIG } from '@app/config/defaultAppConfig';
import { DefaultAppPrompt } from '@app/components/DefaultAppPrompt';
import { useDefaultAppPrompt } from '@app/hooks/useDefaultAppPrompt';
/**
* 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 default PDF handler prompt on first launch
*/
export function AppProviders({ children }: { children: ReactNode }) {
const { promptOpened, handleSetDefault, handleDismiss } = useDefaultAppPrompt();
return (
<ProprietaryAppProviders
appConfigRetryOptions={{
@@ -22,6 +27,11 @@ export function AppProviders({ children }: { children: ReactNode }) {
}}
>
<DesktopConfigSync />
<DefaultAppPrompt
opened={promptOpened}
onSetDefault={handleSetDefault}
onDismiss={handleDismiss}
/>
{children}
</ProprietaryAppProviders>
);

View File

@@ -0,0 +1,78 @@
import { Modal, Text, Button, Stack, Flex } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import CancelIcon from '@mui/icons-material/Cancel';
import { CSSProperties } from 'react';
interface DefaultAppPromptProps {
opened: boolean;
onSetDefault: () => void;
onDismiss: () => void;
}
const ICON_STYLE: CSSProperties = {
fontSize: 48,
display: 'block',
margin: '0 auto 12px',
color: 'var(--mantine-color-blue-6)',
};
export const DefaultAppPrompt = ({ opened, onSetDefault, onDismiss }: DefaultAppPromptProps) => {
const { t } = useTranslation();
return (
<Modal
opened={opened}
onClose={onDismiss}
title={t('defaultApp.title', 'Set as Default PDF App')}
centered
size="auto"
closeOnClickOutside={true}
closeOnEscape={true}
>
<Stack ta="center" p="md" gap="sm">
<PictureAsPdfIcon style={ICON_STYLE} />
<Text size="lg" fw={500}>
{t(
'defaultApp.message',
'Would you like to set Stirling PDF as your default PDF editor?'
)}
</Text>
<Text size="sm" c="dimmed">
{t(
'defaultApp.description',
'You can change this later in your system settings.'
)}
</Text>
</Stack>
<Flex
mt="md"
gap="sm"
justify="center"
align="center"
direction={{ base: 'column', md: 'row' }}
>
<Button
variant="light"
color="var(--mantine-color-gray-8)"
onClick={onDismiss}
leftSection={<CancelIcon fontSize="small" />}
w="10rem"
>
{t('defaultApp.notNow', 'Not Now')}
</Button>
<Button
variant="filled"
color="var(--mantine-color-blue-9)"
onClick={onSetDefault}
leftSection={<CheckCircleOutlineIcon fontSize="small" />}
w="10rem"
>
{t('defaultApp.setDefault', 'Set Default')}
</Button>
</Flex>
</Modal>
);
};

View File

@@ -0,0 +1,82 @@
import { useState, useEffect } from 'react';
import { defaultAppService } from '@app/services/defaultAppService';
import { alert } from '@app/components/toast';
import { useTranslation } from 'react-i18next';
export function useDefaultAppPrompt() {
const { t } = useTranslation();
const [promptOpened, setPromptOpened] = useState(false);
const [isSettingDefault, setIsSettingDefault] = useState(false);
// Check on mount if we should show the prompt
useEffect(() => {
const checkShouldPrompt = async () => {
try {
const shouldShow = await defaultAppService.shouldShowPrompt();
if (shouldShow) {
// Small delay so it doesn't show immediately on app launch
setTimeout(() => setPromptOpened(true), 2000);
}
} catch (error) {
console.error('[DefaultAppPrompt] Failed to check prompt status:', error);
}
};
checkShouldPrompt();
}, []);
const handleSetDefault = async () => {
setIsSettingDefault(true);
try {
const result = await defaultAppService.setAsDefaultPdfHandler();
if (result === 'set_successfully') {
alert({
alertType: 'success',
title: t('defaultApp.success.title', 'Default App Set'),
body: t(
'defaultApp.success.message',
'Stirling PDF is now your default PDF editor'
),
});
} else if (result === 'opened_settings') {
alert({
alertType: 'neutral',
title: t('defaultApp.settingsOpened.title', 'Settings Opened'),
body: t(
'defaultApp.settingsOpened.message',
'Please select Stirling PDF in your system settings'
),
});
}
// Mark as dismissed regardless of outcome
defaultAppService.setPromptDismissed(true);
setPromptOpened(false);
} catch (error) {
console.error('[DefaultAppPrompt] Failed to set default handler:', error);
alert({
alertType: 'error',
title: t('defaultApp.error.title', 'Error'),
body: t(
'defaultApp.error.message',
'Failed to set default PDF handler'
),
});
} finally {
setIsSettingDefault(false);
}
};
const handleDismiss = () => {
defaultAppService.setPromptDismissed(true);
setPromptOpened(false);
};
return {
promptOpened,
isSettingDefault,
handleSetDefault,
handleDismiss,
};
}

View File

@@ -0,0 +1,70 @@
import { invoke } from '@tauri-apps/api/core';
/**
* Service for managing default PDF handler settings
* Note: Uses localStorage for machine-specific preferences (not synced to server)
*/
export const defaultAppService = {
/**
* Check if Stirling PDF is the default PDF handler
*/
async isDefaultPdfHandler(): Promise<boolean> {
try {
const result = await invoke<boolean>('is_default_pdf_handler');
return result;
} catch (error) {
console.error('[DefaultApp] Failed to check default handler:', error);
return false;
}
},
/**
* Set or prompt to set Stirling PDF as default PDF handler
* Returns a status string indicating what happened
*/
async setAsDefaultPdfHandler(): Promise<'set_successfully' | 'opened_settings' | 'error'> {
try {
const result = await invoke<string>('set_as_default_pdf_handler');
return result as 'set_successfully' | 'opened_settings';
} catch (error) {
console.error('[DefaultApp] Failed to set default handler:', error);
return 'error';
}
},
/**
* Check if user has dismissed the default app prompt (machine-specific)
*/
hasUserDismissedPrompt(): boolean {
try {
const dismissed = localStorage.getItem('stirlingpdf_default_app_prompt_dismissed');
return dismissed === 'true';
} catch {
return false;
}
},
/**
* Mark that user has dismissed the default app prompt (machine-specific)
*/
setPromptDismissed(dismissed: boolean): void {
try {
localStorage.setItem('stirlingpdf_default_app_prompt_dismissed', dismissed ? 'true' : 'false');
} catch (error) {
console.error('[DefaultApp] Failed to save prompt preference:', error);
}
},
/**
* Check if we should show the default app prompt
* Returns true if: user hasn't dismissed it AND app is not default handler
*/
async shouldShowPrompt(): Promise<boolean> {
if (this.hasUserDismissedPrompt()) {
return false;
}
const isDefault = await this.isDefaultPdfHandler();
return !isDefault;
},
};