mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
tabs
This commit is contained in:
parent
458cb7fab4
commit
bd383fb1e7
@ -4533,6 +4533,32 @@
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Reset and Change Mode"
|
||||
},
|
||||
"welcomeBanner": {
|
||||
"title": "Welcome to PDF Text Editor (Early Access)",
|
||||
"experimental": "This is an experimental feature in active development. Expect some instability and issues during use.",
|
||||
"howItWorks": "This tool converts your PDF to an editable format where you can modify text content and reposition images. Changes are saved back as a new PDF.",
|
||||
"bestFor": "Works Best With:",
|
||||
"bestFor1": "Simple PDFs containing primarily text and images",
|
||||
"bestFor2": "Documents with standard paragraph formatting",
|
||||
"bestFor3": "Letters, essays, reports, and basic documents",
|
||||
"notIdealFor": "Not Ideal For:",
|
||||
"notIdealFor1": "PDFs with special formatting like bullet points, tables, or multi-column layouts",
|
||||
"notIdealFor2": "Magazines, brochures, or heavily designed documents",
|
||||
"notIdealFor3": "Instruction manuals with complex layouts",
|
||||
"limitations": "Current Limitations:",
|
||||
"limitation1": "Font rendering may differ slightly from the original PDF",
|
||||
"limitation2": "Complex graphics, form fields, and annotations are preserved but not editable",
|
||||
"limitation3": "Large files may take time to convert and process",
|
||||
"knownIssues": "Known Issues (Being Fixed):",
|
||||
"issue1": "Text colour is not currently preserved (will be added soon)",
|
||||
"issue2": "Paragraph mode has more alignment and spacing issues - Single Line mode recommended",
|
||||
"issue3": "The preview display differs from the exported PDF - exported PDFs are closer to the original",
|
||||
"issue4": "Rotated text alignment may need manual adjustment",
|
||||
"issue5": "Transparency and layering effects may vary from original",
|
||||
"feedback": "This is an early access feature. Please report any issues you encounter to help us improve!",
|
||||
"gotIt": "Got it",
|
||||
"dontShowAgain": "Don't show again"
|
||||
},
|
||||
"disclaimer": {
|
||||
"heading": "Preview limitations",
|
||||
"textFocus": "This workspace focuses on editing text and repositioning embedded images. Complex page artwork, form widgets, and layered graphics are preserved for export but are not fully editable here.",
|
||||
@ -4579,6 +4605,21 @@
|
||||
"standard14": "Standard PDF Font",
|
||||
"warnings": "Warnings",
|
||||
"suggestions": "Notes"
|
||||
},
|
||||
"manual": {
|
||||
"mergeTooltip": "Merge selected boxes into a single paragraph",
|
||||
"merge": "Merge selection",
|
||||
"ungroupTooltip": "Split paragraph back into separate lines",
|
||||
"ungroup": "Ungroup selection",
|
||||
"widthMenu": "Width options",
|
||||
"expandWidth": "Expand to page edge",
|
||||
"resetWidth": "Reset width",
|
||||
"resizeHandle": "Adjust text width"
|
||||
},
|
||||
"options": {
|
||||
"manualGrouping": {
|
||||
"descriptionInline": "Tip: Hold Ctrl (Cmd) or Shift to multi-select text boxes. A floating toolbar will appear above the selection so you can merge, ungroup, or adjust widths."
|
||||
}
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
|
||||
@ -121,10 +121,11 @@ export const NavigationProvider: React.FC<{
|
||||
hasUnsavedChanges
|
||||
});
|
||||
|
||||
// If we're leaving pageEditor or viewer workbench and have unsaved changes, request navigation
|
||||
// If we're leaving pageEditor, viewer, or custom workbench and have unsaved changes, request navigation
|
||||
const leavingWorkbenchWithChanges =
|
||||
(state.workbench === 'pageEditor' && workbench !== 'pageEditor' && hasUnsavedChanges) ||
|
||||
(state.workbench === 'viewer' && workbench !== 'viewer' && hasUnsavedChanges);
|
||||
(state.workbench === 'viewer' && workbench !== 'viewer' && hasUnsavedChanges) ||
|
||||
(state.workbench.startsWith('custom:') && workbench !== state.workbench && hasUnsavedChanges);
|
||||
|
||||
if (leavingWorkbenchWithChanges) {
|
||||
// Update state to reflect unsaved changes so modal knows
|
||||
@ -132,7 +133,19 @@ export const NavigationProvider: React.FC<{
|
||||
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges: true } });
|
||||
}
|
||||
const performWorkbenchChange = () => {
|
||||
dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
|
||||
// When leaving a custom workbench, clear the selected tool
|
||||
console.log('[NavigationContext] performWorkbenchChange executing', {
|
||||
from: state.workbench,
|
||||
to: workbench,
|
||||
isCustom: state.workbench.startsWith('custom:')
|
||||
});
|
||||
if (state.workbench.startsWith('custom:')) {
|
||||
console.log('[NavigationContext] Clearing tool and changing workbench to:', workbench);
|
||||
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench } });
|
||||
} else {
|
||||
console.log('[NavigationContext] Just changing workbench to:', workbench);
|
||||
dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
|
||||
}
|
||||
};
|
||||
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: performWorkbenchChange } });
|
||||
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } });
|
||||
@ -149,10 +162,11 @@ export const NavigationProvider: React.FC<{
|
||||
// Check for unsaved changes using registered checker or state
|
||||
const hasUnsavedChanges = unsavedChangesCheckerRef.current?.() || state.hasUnsavedChanges;
|
||||
|
||||
// If we're leaving pageEditor or viewer workbench and have unsaved changes, request navigation
|
||||
// If we're leaving pageEditor, viewer, or custom workbench and have unsaved changes, request navigation
|
||||
const leavingWorkbenchWithChanges =
|
||||
(state.workbench === 'pageEditor' && workbench !== 'pageEditor' && hasUnsavedChanges) ||
|
||||
(state.workbench === 'viewer' && workbench !== 'viewer' && hasUnsavedChanges);
|
||||
(state.workbench === 'viewer' && workbench !== 'viewer' && hasUnsavedChanges) ||
|
||||
(state.workbench.startsWith('custom:') && workbench !== state.workbench && hasUnsavedChanges);
|
||||
|
||||
if (leavingWorkbenchWithChanges) {
|
||||
const performWorkbenchChange = () => {
|
||||
@ -192,13 +206,19 @@ export const NavigationProvider: React.FC<{
|
||||
}, [state.hasUnsavedChanges]),
|
||||
|
||||
confirmNavigation: useCallback(() => {
|
||||
console.log('[NavigationContext] confirmNavigation called', {
|
||||
hasPendingNav: !!state.pendingNavigation,
|
||||
currentWorkbench: state.workbench,
|
||||
currentTool: state.selectedTool
|
||||
});
|
||||
if (state.pendingNavigation) {
|
||||
state.pendingNavigation();
|
||||
}
|
||||
|
||||
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } });
|
||||
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } });
|
||||
}, [state.pendingNavigation]),
|
||||
console.log('[NavigationContext] confirmNavigation completed');
|
||||
}, [state.pendingNavigation, state.workbench, state.selectedTool]),
|
||||
|
||||
cancelNavigation: useCallback(() => {
|
||||
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } });
|
||||
|
||||
@ -218,15 +218,25 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
||||
}, [customViewRegistry, customViewData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isBaseWorkbench(navigationState.workbench)) {
|
||||
const { workbench } = navigationState;
|
||||
if (isBaseWorkbench(workbench)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentCustomView = customWorkbenchViews.find(view => view.workbenchId === navigationState.workbench);
|
||||
const currentCustomView = customWorkbenchViews.find(view => view.workbenchId === workbench);
|
||||
const expectedWorkbench = selectedTool?.workbench;
|
||||
const workbenchOwnedBySelectedTool = expectedWorkbench === workbench;
|
||||
|
||||
if (!currentCustomView || currentCustomView.data == null) {
|
||||
// If the currently selected tool expects this custom workbench, allow it
|
||||
// some time to register/populate the view instead of immediately bouncing
|
||||
// the user back to Active Files.
|
||||
if (workbenchOwnedBySelectedTool) {
|
||||
return;
|
||||
}
|
||||
actions.setWorkbench(getDefaultWorkbench());
|
||||
}
|
||||
}, [actions, customWorkbenchViews, navigationState.workbench]);
|
||||
}, [actions, customWorkbenchViews, navigationState.workbench, selectedTool]);
|
||||
|
||||
// Persisted via PreferencesContext; no direct localStorage writes needed here
|
||||
|
||||
@ -421,4 +431,4 @@ export function useToolWorkflow(): ToolWorkflowContextValue {
|
||||
throw new Error('useToolWorkflow must be used within a ToolWorkflowProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,6 +33,7 @@ import MergeTypeIcon from '@mui/icons-material/MergeType';
|
||||
import CallSplitIcon from '@mui/icons-material/CallSplit';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import { Rnd } from 'react-rnd';
|
||||
import NavigationWarningModal from '@core/components/shared/NavigationWarningModal';
|
||||
|
||||
import {
|
||||
PdfTextEditorViewData,
|
||||
@ -347,6 +348,30 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => {
|
||||
maxWidth: number;
|
||||
} | null>(null);
|
||||
|
||||
// First-time banner state
|
||||
const [showWelcomeBanner, setShowWelcomeBanner] = useState(() => {
|
||||
try {
|
||||
return localStorage.getItem('pdfTextEditor.welcomeBannerDismissed') !== 'true';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
const handleDismissWelcomeBanner = useCallback(() => {
|
||||
// Just dismiss for this session, don't save to localStorage
|
||||
setShowWelcomeBanner(false);
|
||||
}, []);
|
||||
|
||||
const handleDontShowAgain = useCallback(() => {
|
||||
// Save to localStorage to never show again
|
||||
try {
|
||||
localStorage.setItem('pdfTextEditor.welcomeBannerDismissed', 'true');
|
||||
} catch {
|
||||
// Ignore localStorage errors
|
||||
}
|
||||
setShowWelcomeBanner(false);
|
||||
}, []);
|
||||
|
||||
const {
|
||||
document: pdfDocument,
|
||||
groupsByPage,
|
||||
@ -373,6 +398,7 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => {
|
||||
onReset,
|
||||
onDownloadJson,
|
||||
onGeneratePdf,
|
||||
onGeneratePdfForNavigation,
|
||||
onForceSingleTextElementChange,
|
||||
onGroupingModeChange,
|
||||
onMergeGroups,
|
||||
@ -1705,6 +1731,82 @@ const selectionToolbarPosition = useMemo(() => {
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Modal
|
||||
opened={showWelcomeBanner}
|
||||
onClose={handleDismissWelcomeBanner}
|
||||
title={
|
||||
<Group gap="xs">
|
||||
<InfoOutlinedIcon fontSize="small" />
|
||||
<Text fw={600}>{t('pdfTextEditor.welcomeBanner.title', 'Welcome to PDF Text Editor (Early Access)')}</Text>
|
||||
</Group>
|
||||
}
|
||||
centered
|
||||
size="lg"
|
||||
>
|
||||
<ScrollArea style={{ maxHeight: '70vh' }} offsetScrollbars>
|
||||
<Stack gap="sm">
|
||||
<Alert color="orange" variant="light" radius="md">
|
||||
<Text size="sm" fw={500}>
|
||||
{t('pdfTextEditor.welcomeBanner.experimental', 'This is an experimental feature in active development. Expect some instability and issues during use.')}
|
||||
</Text>
|
||||
</Alert>
|
||||
<Text size="sm">
|
||||
{t('pdfTextEditor.welcomeBanner.howItWorks', 'This tool converts your PDF to an editable format where you can modify text content and reposition images. Changes are saved back as a new PDF.')}
|
||||
</Text>
|
||||
<Divider />
|
||||
<Text size="sm" fw={500} c="green.7">
|
||||
{t('pdfTextEditor.welcomeBanner.bestFor', 'Works Best With:')}
|
||||
</Text>
|
||||
<Text size="sm" component="ul" style={{ marginLeft: '1rem', marginTop: '0.25rem' }}>
|
||||
<li>{t('pdfTextEditor.welcomeBanner.bestFor1', 'Simple PDFs containing primarily text and images')}</li>
|
||||
<li>{t('pdfTextEditor.welcomeBanner.bestFor2', 'Documents with standard paragraph formatting')}</li>
|
||||
<li>{t('pdfTextEditor.welcomeBanner.bestFor3', 'Letters, essays, reports, and basic documents')}</li>
|
||||
</Text>
|
||||
<Divider />
|
||||
<Text size="sm" fw={500} c="orange.7">
|
||||
{t('pdfTextEditor.welcomeBanner.notIdealFor', 'Not Ideal For:')}
|
||||
</Text>
|
||||
<Text size="sm" component="ul" style={{ marginLeft: '1rem', marginTop: '0.25rem' }}>
|
||||
<li>{t('pdfTextEditor.welcomeBanner.notIdealFor1', 'PDFs with special formatting like bullet points, tables, or multi-column layouts')}</li>
|
||||
<li>{t('pdfTextEditor.welcomeBanner.notIdealFor2', 'Magazines, brochures, or heavily designed documents')}</li>
|
||||
<li>{t('pdfTextEditor.welcomeBanner.notIdealFor3', 'Instruction manuals with complex layouts')}</li>
|
||||
</Text>
|
||||
<Divider />
|
||||
<Text size="sm" fw={500}>
|
||||
{t('pdfTextEditor.welcomeBanner.limitations', 'Current Limitations:')}
|
||||
</Text>
|
||||
<Text size="sm" component="ul" style={{ marginLeft: '1rem', marginTop: '0.25rem' }}>
|
||||
<li>{t('pdfTextEditor.welcomeBanner.limitation1', 'Font rendering may differ slightly from the original PDF')}</li>
|
||||
<li>{t('pdfTextEditor.welcomeBanner.limitation2', 'Complex graphics, form fields, and annotations are preserved but not editable')}</li>
|
||||
<li>{t('pdfTextEditor.welcomeBanner.limitation3', 'Large files may take time to convert and process')}</li>
|
||||
</Text>
|
||||
<Divider />
|
||||
<Text size="sm" fw={500}>
|
||||
{t('pdfTextEditor.welcomeBanner.knownIssues', 'Known Issues (Being Fixed):')}
|
||||
</Text>
|
||||
<Text size="sm" component="ul" style={{ marginLeft: '1rem', marginTop: '0.25rem' }}>
|
||||
<li>{t('pdfTextEditor.welcomeBanner.issue1', 'Text colour is not currently preserved (will be added soon)')}</li>
|
||||
<li>{t('pdfTextEditor.welcomeBanner.issue2', 'Paragraph mode has more alignment and spacing issues - Single Line mode recommended')}</li>
|
||||
<li>{t('pdfTextEditor.welcomeBanner.issue3', 'The preview display differs from the exported PDF - exported PDFs are closer to the original')}</li>
|
||||
<li>{t('pdfTextEditor.welcomeBanner.issue4', 'Rotated text alignment may need manual adjustment')}</li>
|
||||
<li>{t('pdfTextEditor.welcomeBanner.issue5', 'Transparency and layering effects may vary from original')}</li>
|
||||
</Text>
|
||||
<Divider />
|
||||
<Text size="xs" c="dimmed">
|
||||
{t('pdfTextEditor.welcomeBanner.feedback', 'This is an early access feature. Please report any issues you encounter to help us improve!')}
|
||||
</Text>
|
||||
<Group justify="flex-end" gap="sm" mt="md">
|
||||
<Button variant="default" onClick={handleDismissWelcomeBanner}>
|
||||
{t('pdfTextEditor.welcomeBanner.gotIt', 'Got it')}
|
||||
</Button>
|
||||
<Button onClick={handleDontShowAgain}>
|
||||
{t('pdfTextEditor.welcomeBanner.dontShowAgain', "Don't show again")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
</Modal>
|
||||
|
||||
<Card
|
||||
withBorder
|
||||
padding="md"
|
||||
@ -2169,7 +2271,7 @@ const selectionToolbarPosition = useMemo(() => {
|
||||
width: '100%',
|
||||
minHeight: '100%',
|
||||
height: 'auto',
|
||||
padding: 0,
|
||||
padding: '2px',
|
||||
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||
color: textColor,
|
||||
fontSize: `${fontSizePx}px`,
|
||||
@ -2212,7 +2314,7 @@ const selectionToolbarPosition = useMemo(() => {
|
||||
style={{
|
||||
width: '100%',
|
||||
minHeight: '100%',
|
||||
padding: 0,
|
||||
padding: '2px',
|
||||
whiteSpace,
|
||||
wordBreak,
|
||||
overflowWrap,
|
||||
@ -2342,6 +2444,11 @@ const selectionToolbarPosition = useMemo(() => {
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
{/* Navigation Warning Modal */}
|
||||
<NavigationWarningModal
|
||||
onApplyAndContinue={onGeneratePdfForNavigation}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@ -207,6 +207,7 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
||||
} = useToolWorkflow();
|
||||
const { actions: navigationActions } = useNavigationActions();
|
||||
const navigationState = useNavigationState();
|
||||
const { registerUnsavedChangesChecker, unregisterUnsavedChangesChecker } = navigationActions;
|
||||
|
||||
const [loadedDocument, setLoadedDocument] = useState<PdfJsonDocument | null>(null);
|
||||
const [groupsByPage, setGroupsByPage] = useState<TextGroup[][]>([]);
|
||||
@ -959,7 +960,7 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
||||
}
|
||||
}, [buildPayload, onComplete]);
|
||||
|
||||
const handleGeneratePdf = useCallback(async () => {
|
||||
const handleGeneratePdf = useCallback(async (skipComplete = false) => {
|
||||
try {
|
||||
setIsGeneratingPdf(true);
|
||||
|
||||
@ -1053,7 +1054,7 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
||||
|
||||
downloadBlob(response.data, downloadName);
|
||||
|
||||
if (onComplete) {
|
||||
if (onComplete && !skipComplete) {
|
||||
const pdfFile = new File([response.data], downloadName, { type: 'application/pdf' });
|
||||
onComplete([pdfFile]);
|
||||
}
|
||||
@ -1094,7 +1095,7 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
||||
|
||||
downloadBlob(response.data, downloadName);
|
||||
|
||||
if (onComplete) {
|
||||
if (onComplete && !skipComplete) {
|
||||
const pdfFile = new File([response.data], downloadName, { type: 'application/pdf' });
|
||||
onComplete([pdfFile]);
|
||||
}
|
||||
@ -1273,6 +1274,10 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
||||
onReset: handleResetEdits,
|
||||
onDownloadJson: handleDownloadJson,
|
||||
onGeneratePdf: handleGeneratePdf,
|
||||
onGeneratePdfForNavigation: async () => {
|
||||
// Generate PDF without triggering tool completion
|
||||
await handleGeneratePdf(true);
|
||||
},
|
||||
onForceSingleTextElementChange: setForceSingleTextElement,
|
||||
onGroupingModeChange: setGroupingMode,
|
||||
onMergeGroups: handleMergeGroups,
|
||||
@ -1370,14 +1375,30 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
||||
unregisterCustomWorkbenchView,
|
||||
]);
|
||||
|
||||
// Note: Compare tool doesn't auto-force workbench, and neither should we
|
||||
// The workbench should be set when the tool is selected via proper channels
|
||||
// (tool registry, tool picker, etc.) - not forced here
|
||||
|
||||
// Keep hasChanges in a ref for the checker to access
|
||||
const hasChangesRef = useRef(hasChanges);
|
||||
useEffect(() => {
|
||||
if (
|
||||
navigationState.selectedTool === 'pdfTextEditor' &&
|
||||
navigationState.workbench !== WORKBENCH_ID
|
||||
) {
|
||||
navigationActions.setWorkbench(WORKBENCH_ID);
|
||||
}
|
||||
}, [navigationActions, navigationState.selectedTool, navigationState.workbench]);
|
||||
hasChangesRef.current = hasChanges;
|
||||
console.log('[PdfTextEditor] hasChanges updated to:', hasChanges);
|
||||
}, [hasChanges]);
|
||||
|
||||
// Register unsaved changes checker for navigation guard
|
||||
useEffect(() => {
|
||||
const checker = () => {
|
||||
console.log('[PdfTextEditor] Checking unsaved changes:', hasChangesRef.current);
|
||||
return hasChangesRef.current;
|
||||
};
|
||||
registerUnsavedChangesChecker(checker);
|
||||
console.log('[PdfTextEditor] Registered unsaved changes checker');
|
||||
return () => {
|
||||
console.log('[PdfTextEditor] Unregistered unsaved changes checker');
|
||||
unregisterUnsavedChangesChecker();
|
||||
};
|
||||
}, [registerUnsavedChangesChecker, unregisterUnsavedChangesChecker]);
|
||||
|
||||
const lastSentViewDataRef = useRef<PdfTextEditorViewData | null>(null);
|
||||
|
||||
|
||||
@ -218,6 +218,7 @@ export interface PdfTextEditorViewData {
|
||||
onReset: () => void;
|
||||
onDownloadJson: () => void;
|
||||
onGeneratePdf: () => void;
|
||||
onGeneratePdfForNavigation: () => Promise<void>;
|
||||
onForceSingleTextElementChange: (value: boolean) => void;
|
||||
onGroupingModeChange: (value: 'auto' | 'paragraph' | 'singleLine') => void;
|
||||
onMergeGroups: (pageIndex: number, groupIds: string[]) => boolean;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user