Move to banner

This commit is contained in:
James Brunton 2025-11-13 16:40:56 +00:00
parent 8b9204ebba
commit ec89b77e95
8 changed files with 279 additions and 171 deletions

View File

@ -47,6 +47,11 @@
"description": "You can change this later in your system settings.", "description": "You can change this later in your system settings.",
"notNow": "Not Now", "notNow": "Not Now",
"setDefault": "Set Default", "setDefault": "Set Default",
"dismiss": "Dismiss",
"prompt": {
"title": "Set as Default PDF Editor",
"message": "Make Stirling PDF your default application for opening PDF files."
},
"success": { "success": {
"title": "Default App Set", "title": "Default App Set",
"message": "Stirling PDF is now your default PDF editor" "message": "Stirling PDF is now your default PDF editor"
@ -351,7 +356,13 @@
"mode": { "mode": {
"fullscreen": "Fullscreen", "fullscreen": "Fullscreen",
"sidebar": "Sidebar" "sidebar": "Sidebar"
} },
"defaultPdfEditor": "Default PDF editor",
"defaultPdfEditorActive": "Stirling PDF is your default PDF editor",
"defaultPdfEditorInactive": "Another application is set as default",
"defaultPdfEditorChecking": "Checking...",
"defaultPdfEditorSet": "Already Default",
"setAsDefault": "Set as Default"
}, },
"hotkeys": { "hotkeys": {
"title": "Keyboard Shortcuts", "title": "Keyboard Shortcuts",

View File

@ -0,0 +1,53 @@
import { Suspense } from "react";
import { Routes, Route } from "react-router-dom";
import { AppProviders } from "@app/components/AppProviders";
import { LoadingFallback } from "@app/components/shared/LoadingFallback";
import Landing from "@app/routes/Landing";
import Login from "@app/routes/Login";
import Signup from "@app/routes/Signup";
import AuthCallback from "@app/routes/AuthCallback";
import InviteAccept from "@app/routes/InviteAccept";
import OnboardingTour from "@app/components/onboarding/OnboardingTour";
import { DefaultAppBanner } from "@app/components/shared/DefaultAppBanner";
// Import global styles
import "@app/styles/tailwind.css";
import "@app/styles/cookieconsent.css";
import "@app/styles/index.css";
import "@app/styles/auth-theme.css";
// Import file ID debugging helpers (development only)
import "@app/utils/fileIdSafety";
export default function App() {
return (
<Suspense fallback={<LoadingFallback />}>
<AppProviders>
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<DefaultAppBanner />
<div
style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
className="desktop-app-content"
>
<style>{`
.desktop-app-content .h-screen {
height: 100% !important;
}
`}</style>
<Routes>
{/* Auth routes - no nested providers needed */}
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route path="/auth/callback" element={<AuthCallback />} />
<Route path="/invite/:token" element={<InviteAccept />} />
{/* Main app routes - Landing handles auth logic */}
<Route path="/*" element={<Landing />} />
</Routes>
<OnboardingTour />
</div>
</div>
</AppProviders>
</Suspense>
);
}

View File

@ -2,18 +2,13 @@ import { ReactNode } from "react";
import { AppProviders as ProprietaryAppProviders } from "@proprietary/components/AppProviders"; import { AppProviders as ProprietaryAppProviders } from "@proprietary/components/AppProviders";
import { DesktopConfigSync } from '@app/components/DesktopConfigSync'; import { DesktopConfigSync } from '@app/components/DesktopConfigSync';
import { DESKTOP_DEFAULT_APP_CONFIG } from '@app/config/defaultAppConfig'; import { DESKTOP_DEFAULT_APP_CONFIG } from '@app/config/defaultAppConfig';
import { DefaultAppPrompt } from '@app/components/DefaultAppPrompt';
import { useDefaultAppPrompt } from '@app/hooks/useDefaultAppPrompt';
/** /**
* Desktop application providers * Desktop application providers
* Wraps proprietary providers and adds desktop-specific configuration * Wraps proprietary providers and adds desktop-specific configuration
* - Enables retry logic for app config (needed for Tauri mode when backend is starting) * - 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 }) { export function AppProviders({ children }: { children: ReactNode }) {
const { promptOpened, handleSetDefault, handleDismiss } = useDefaultAppPrompt();
return ( return (
<ProprietaryAppProviders <ProprietaryAppProviders
appConfigRetryOptions={{ appConfigRetryOptions={{
@ -27,11 +22,6 @@ export function AppProviders({ children }: { children: ReactNode }) {
}} }}
> >
<DesktopConfigSync /> <DesktopConfigSync />
<DefaultAppPrompt
opened={promptOpened}
onSetDefault={handleSetDefault}
onDismiss={handleDismiss}
/>
{children} {children}
</ProprietaryAppProviders> </ProprietaryAppProviders>
); );

View File

@ -1,78 +0,0 @@
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,110 @@
import React, { useState, useEffect } from 'react';
import { Paper, Group, Text, Button, ActionIcon } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import LocalIcon from '@app/components/shared/LocalIcon';
import { defaultAppService } from '@app/services/defaultAppService';
import { alert } from '@app/components/toast';
const PROMPT_DISMISSED_KEY = 'stirlingpdf_default_app_prompt_dismissed';
export const DefaultAppBanner: React.FC = () => {
const { t } = useTranslation();
const [promptDismissed, setPromptDismissed] = useState(() => {
return localStorage.getItem(PROMPT_DISMISSED_KEY) === 'true';
});
const [isDefault, setIsDefault] = useState<boolean | null>(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
checkDefaultStatus();
}, []);
const checkDefaultStatus = async () => {
try {
const status = await defaultAppService.isDefaultPdfHandler();
setIsDefault(status);
} catch (error) {
console.error('Failed to check default status:', error);
}
};
const handleSetDefault = async () => {
setIsLoading(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'),
});
setIsDefault(true);
} 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'),
});
}
} catch (error) {
console.error('Failed to set default:', error);
alert({
alertType: 'error',
title: t('defaultApp.error.title', 'Error'),
body: t('defaultApp.error.message', 'Failed to set default PDF handler'),
});
} finally {
setIsLoading(false);
}
};
const handleDismissPrompt = () => {
setPromptDismissed(true);
localStorage.setItem(PROMPT_DISMISSED_KEY, 'true');
};
if (promptDismissed || isDefault !== false) {
return null;
}
return (
<Paper
p="sm"
radius={0}
style={{
background: 'var(--mantine-color-blue-0)',
borderBottom: '1px solid var(--mantine-color-blue-2)',
position: 'relative',
}}
>
<Group gap="sm" align="center" wrap="nowrap">
<LocalIcon icon="picture-as-pdf-rounded" width="1.2rem" height="1.2rem" style={{ color: 'var(--mantine-color-blue-6)', flexShrink: 0 }} />
<Text fw={500} size="sm" style={{ color: 'var(--mantine-color-blue-9)' }}>
{t('defaultApp.prompt.message', 'Make Stirling PDF your default application for opening PDF files.')}
</Text>
<Button
variant="light"
color="blue"
size="xs"
onClick={handleSetDefault}
loading={isLoading}
leftSection={<LocalIcon icon="check-circle-rounded" width="0.9rem" height="0.9rem" />}
style={{ flexShrink: 0 }}
>
{t('defaultApp.setDefault', 'Set Default')}
</Button>
</Group>
<ActionIcon
variant="subtle"
color="gray"
size="sm"
onClick={handleDismissPrompt}
aria-label={t('defaultApp.dismiss', 'Dismiss')}
style={{ position: 'absolute', top: '0.5rem', right: '0.5rem' }}
>
<LocalIcon icon="close-rounded" width="1rem" height="1rem" />
</ActionIcon>
</Paper>
);
};

View File

@ -0,0 +1,86 @@
import React, { useState, useEffect } from 'react';
import { Paper, Text, Button, Group } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { defaultAppService } from '@app/services/defaultAppService';
import { alert } from '@app/components/toast';
export const DefaultAppSettings: React.FC = () => {
const { t } = useTranslation();
const [isDefault, setIsDefault] = useState<boolean | null>(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
checkDefaultStatus();
}, []);
const checkDefaultStatus = async () => {
try {
const status = await defaultAppService.isDefaultPdfHandler();
setIsDefault(status);
} catch (error) {
console.error('Failed to check default status:', error);
}
};
const handleSetDefault = async () => {
setIsLoading(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'),
});
setIsDefault(true);
} 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'),
});
}
} catch (error) {
console.error('Failed to set default:', error);
alert({
alertType: 'error',
title: t('defaultApp.error.title', 'Error'),
body: t('defaultApp.error.message', 'Failed to set default PDF handler'),
});
} finally {
setIsLoading(false);
}
};
return (
<Paper withBorder p="md" radius="md">
<Group justify="space-between" align="center">
<div>
<Text fw={500} size="sm">
{t('settings.general.defaultPdfEditor', 'Default PDF editor')}
</Text>
<Text size="xs" c="dimmed" mt={4}>
{isDefault === true
? t('settings.general.defaultPdfEditorActive', 'Stirling PDF is your default PDF editor')
: isDefault === false
? t('settings.general.defaultPdfEditorInactive', 'Another application is set as default')
: t('settings.general.defaultPdfEditorChecking', 'Checking...')}
</Text>
</div>
<Button
variant={isDefault ? 'light' : 'filled'}
color="blue"
size="sm"
onClick={handleSetDefault}
loading={isLoading}
disabled={isDefault === true}
>
{isDefault
? t('settings.general.defaultPdfEditorSet', 'Already Default')
: t('settings.general.setAsDefault', 'Set as Default')}
</Button>
</Group>
</Paper>
);
};

View File

@ -0,0 +1,18 @@
import React from 'react';
import { Stack } from '@mantine/core';
import CoreGeneralSection from '@core/components/shared/config/configSections/GeneralSection';
import { DefaultAppSettings } from '@app/components/shared/config/configSections/DefaultAppSettings';
/**
* Desktop extension of GeneralSection that adds default PDF editor settings
*/
const GeneralSection: React.FC = () => {
return (
<Stack gap="lg">
<CoreGeneralSection />
<DefaultAppSettings />
</Stack>
);
};
export default GeneralSection;

View File

@ -1,82 +0,0 @@
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,
};
}