mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-17 13:52:14 +01:00
Merge branch 'V2' into feature/AdjustColorsContrastTool
This commit is contained in:
commit
909654967f
@ -192,6 +192,11 @@ return useToolOperation({
|
||||
- **Preview System**: Tool results can be previewed without polluting file context (Split tool example)
|
||||
- **Performance**: Web Worker thumbnails, IndexedDB persistence, background processing
|
||||
|
||||
## Translation Rules
|
||||
|
||||
- **CRITICAL**: Always update translations in `en-GB` only, never `en-US`
|
||||
- Translation files are located in `frontend/public/locales/`
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Java Version**: Minimum JDK 17, supports and recommends JDK 21
|
||||
|
||||
@ -1916,6 +1916,7 @@ fileManager.storageError=Storage error occurred
|
||||
fileManager.storageLow=Storage is running low. Consider removing old files.
|
||||
fileManager.uploadError=Failed to upload some files.
|
||||
fileManager.supportMessage=Powered by browser database storage for unlimited capacity
|
||||
fileManager.loadingFiles=Loading files...
|
||||
|
||||
# Page Editor
|
||||
pageEditor.title=Page Editor
|
||||
|
||||
@ -3080,7 +3080,12 @@
|
||||
"panMode": "Pan Mode",
|
||||
"rotateLeft": "Rotate Left",
|
||||
"rotateRight": "Rotate Right",
|
||||
"toggleSidebar": "Toggle Sidebar"
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"exportSelected": "Export Selected Pages",
|
||||
"toggleAnnotations": "Toggle Annotations Visibility",
|
||||
"annotationMode": "Toggle Annotation Mode",
|
||||
"draw": "Draw",
|
||||
"save": "Save"
|
||||
},
|
||||
"search": {
|
||||
"title": "Search PDF",
|
||||
@ -3149,6 +3154,9 @@
|
||||
"addFiles": "Add Files",
|
||||
"dragFilesInOrClick": "Drag files in or click \"Add Files\" to browse"
|
||||
},
|
||||
"fileEditor": {
|
||||
"addFiles": "Add Files"
|
||||
},
|
||||
"fileManager": {
|
||||
"title": "Upload PDF Files",
|
||||
"subtitle": "Add files to your storage for easy access across tools",
|
||||
@ -3184,7 +3192,6 @@
|
||||
"googleDriveShort": "Drive",
|
||||
"myFiles": "My Files",
|
||||
"noRecentFiles": "No recent files found",
|
||||
"dropFilesHint": "Drop files here to upload",
|
||||
"googleDriveNotAvailable": "Google Drive integration not available",
|
||||
"openFiles": "Open Files",
|
||||
"openFile": "Open File",
|
||||
|
||||
@ -20,7 +20,7 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
const { loadRecentFiles, handleRemoveFile } = useFileManager();
|
||||
const { loadRecentFiles, handleRemoveFile, loading } = useFileManager();
|
||||
|
||||
// File management handlers
|
||||
const isFileSupported = useCallback((fileName: string) => {
|
||||
@ -123,7 +123,6 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
||||
onDrop={handleNewFileUpload}
|
||||
onDragEnter={() => setIsDragging(true)}
|
||||
onDragLeave={() => setIsDragging(false)}
|
||||
accept={{}}
|
||||
multiple={true}
|
||||
activateOnClick={false}
|
||||
style={{
|
||||
@ -147,6 +146,7 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
||||
onFileRemove={handleRemoveFileByIndex}
|
||||
modalHeight={modalHeight}
|
||||
refreshRecentFiles={refreshRecentFiles}
|
||||
isLoading={loading}
|
||||
>
|
||||
{isMobile ? <MobileLayout /> : <DesktopLayout />}
|
||||
</FileManagerProvider>
|
||||
|
||||
177
frontend/src/components/fileEditor/AddFileCard.tsx
Normal file
177
frontend/src/components/fileEditor/AddFileCard.tsx
Normal file
@ -0,0 +1,177 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Button, Group, useMantineColorScheme } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import { useFilesModalContext } from '../../contexts/FilesModalContext';
|
||||
import LocalIcon from '../shared/LocalIcon';
|
||||
import { BASE_PATH } from '../../constants/app';
|
||||
import styles from './FileEditor.module.css';
|
||||
|
||||
interface AddFileCardProps {
|
||||
onFileSelect: (files: File[]) => void;
|
||||
accept?: string;
|
||||
multiple?: boolean;
|
||||
}
|
||||
|
||||
const AddFileCard = ({
|
||||
onFileSelect,
|
||||
accept = "*/*",
|
||||
multiple = true
|
||||
}: AddFileCardProps) => {
|
||||
const { t } = useTranslation();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const { openFilesModal } = useFilesModalContext();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const [isUploadHover, setIsUploadHover] = useState(false);
|
||||
|
||||
const handleCardClick = () => {
|
||||
openFilesModal();
|
||||
};
|
||||
|
||||
const handleNativeUploadClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleOpenFilesModal = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
openFilesModal();
|
||||
};
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(event.target.files || []);
|
||||
if (files.length > 0) {
|
||||
onFileSelect(files);
|
||||
}
|
||||
// Reset input so same files can be selected again
|
||||
event.target.value = '';
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept={accept}
|
||||
multiple={multiple}
|
||||
onChange={handleFileChange}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={`${styles.addFileCard} w-[18rem] h-[22rem] select-none flex flex-col shadow-sm transition-all relative cursor-pointer`}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-label={t('fileEditor.addFiles', 'Add files')}
|
||||
onClick={handleCardClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleCardClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Header bar - matches FileEditorThumbnail structure */}
|
||||
<div className={`${styles.header} ${styles.addFileHeader}`}>
|
||||
<div className={styles.logoMark}>
|
||||
<AddIcon sx={{ color: 'inherit', fontSize: '1.5rem' }} />
|
||||
</div>
|
||||
<div className={styles.headerIndex}>
|
||||
{t('fileEditor.addFiles', 'Add Files')}
|
||||
</div>
|
||||
<div className={styles.kebab} />
|
||||
</div>
|
||||
|
||||
{/* Main content area */}
|
||||
<div className={styles.addFileContent}>
|
||||
{/* Stirling PDF Branding */}
|
||||
<Group gap="xs" align="center">
|
||||
<img
|
||||
src={colorScheme === 'dark' ? `${BASE_PATH}/branding/StirlingPDFLogoWhiteText.svg` : `${BASE_PATH}/branding/StirlingPDFLogoGreyText.svg`}
|
||||
alt="Stirling PDF"
|
||||
style={{ height: '2.2rem', width: 'auto' }}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{/* Add Files + Native Upload Buttons - styled like LandingPage */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '0.6rem',
|
||||
width: '100%',
|
||||
marginTop: '0.8rem',
|
||||
marginBottom: '0.8rem'
|
||||
}}
|
||||
onMouseLeave={() => setIsUploadHover(false)}
|
||||
>
|
||||
<Button
|
||||
style={{
|
||||
backgroundColor: 'var(--landing-button-bg)',
|
||||
color: 'var(--landing-button-color)',
|
||||
border: '1px solid var(--landing-button-border)',
|
||||
borderRadius: '2rem',
|
||||
height: '38px',
|
||||
paddingLeft: isUploadHover ? 0 : '1rem',
|
||||
paddingRight: isUploadHover ? 0 : '1rem',
|
||||
width: isUploadHover ? '58px' : 'calc(100% - 58px - 0.6rem)',
|
||||
minWidth: isUploadHover ? '58px' : undefined,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'width .5s ease, padding .5s ease'
|
||||
}}
|
||||
onClick={handleOpenFilesModal}
|
||||
onMouseEnter={() => setIsUploadHover(false)}
|
||||
>
|
||||
<LocalIcon icon="add" width="1.5rem" height="1.5rem" className="text-[var(--accent-interactive)]" />
|
||||
{!isUploadHover && (
|
||||
<span>
|
||||
{t('landing.addFiles', 'Add Files')}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Upload"
|
||||
style={{
|
||||
backgroundColor: 'var(--landing-button-bg)',
|
||||
color: 'var(--landing-button-color)',
|
||||
border: '1px solid var(--landing-button-border)',
|
||||
borderRadius: '1rem',
|
||||
height: '38px',
|
||||
width: isUploadHover ? 'calc(100% - 58px - 0.6rem)' : '58px',
|
||||
minWidth: '58px',
|
||||
paddingLeft: isUploadHover ? '1rem' : 0,
|
||||
paddingRight: isUploadHover ? '1rem' : 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'width .5s ease, padding .5s ease'
|
||||
}}
|
||||
onClick={handleNativeUploadClick}
|
||||
onMouseEnter={() => setIsUploadHover(true)}
|
||||
>
|
||||
<LocalIcon icon="upload" width="1.25rem" height="1.25rem" style={{ color: 'var(--accent-interactive)' }} />
|
||||
{isUploadHover && (
|
||||
<span style={{ marginLeft: '.5rem' }}>
|
||||
{t('landing.uploadFromComputer', 'Upload from computer')}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Instruction Text */}
|
||||
<span
|
||||
className="text-[var(--accent-interactive)]"
|
||||
style={{ fontSize: '.8rem', textAlign: 'center', marginTop: '0.5rem' }}
|
||||
>
|
||||
{t('fileUpload.dropFilesHere', 'Drop files here or click the upload button')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddFileCard;
|
||||
@ -304,4 +304,84 @@
|
||||
/* Light mode selected header stroke override */
|
||||
:global([data-mantine-color-scheme="light"]) .card[data-selected="true"] {
|
||||
outline-color: #3B4B6E;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Add File Card Styles
|
||||
========================= */
|
||||
|
||||
.addFileCard {
|
||||
background: var(--file-card-bg);
|
||||
border: 2px dashed var(--border-default);
|
||||
border-radius: 0.0625rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.18s ease;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.addFileCard:hover {
|
||||
opacity: 1;
|
||||
border-color: var(--color-blue-500);
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.addFileCard:focus {
|
||||
outline: 2px solid var(--color-blue-500);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.addFileHeader {
|
||||
background: var(--bg-subtle);
|
||||
color: var(--text-secondary);
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.addFileCard:hover .addFileHeader {
|
||||
background: var(--color-blue-500);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.addFileContent {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem 1rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.addFileIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-subtle);
|
||||
transition: background-color 0.18s ease;
|
||||
}
|
||||
|
||||
.addFileCard:hover .addFileIcon {
|
||||
background: var(--color-blue-50);
|
||||
}
|
||||
|
||||
.addFileText {
|
||||
font-weight: 500;
|
||||
transition: color 0.18s ease;
|
||||
}
|
||||
|
||||
.addFileCard:hover .addFileText {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.addFileSubtext {
|
||||
font-size: 0.875rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
@ -8,6 +8,7 @@ import { useNavigationActions } from '../../contexts/NavigationContext';
|
||||
import { zipFileService } from '../../services/zipFileService';
|
||||
import { detectFileExtension } from '../../utils/fileUtils';
|
||||
import FileEditorThumbnail from './FileEditorThumbnail';
|
||||
import AddFileCard from './AddFileCard';
|
||||
import FilePickerModal from '../shared/FilePickerModal';
|
||||
import SkeletonLoader from '../shared/SkeletonLoader';
|
||||
import { FileId, StirlingFile } from '../../types/fileContext';
|
||||
@ -171,8 +172,8 @@ const FileEditor = ({
|
||||
|
||||
// Process all extracted files
|
||||
if (allExtractedFiles.length > 0) {
|
||||
// Add files to context (they will be processed automatically)
|
||||
await addFiles(allExtractedFiles);
|
||||
// Add files to context and select them automatically
|
||||
await addFiles(allExtractedFiles, { selectFiles: true });
|
||||
showStatus(`Added ${allExtractedFiles.length} files`, 'success');
|
||||
}
|
||||
} catch (err) {
|
||||
@ -405,6 +406,14 @@ const FileEditor = ({
|
||||
pointerEvents: 'auto'
|
||||
}}
|
||||
>
|
||||
{/* Add File Card - only show when files exist */}
|
||||
{activeStirlingFileStubs.length > 0 && (
|
||||
<AddFileCard
|
||||
key="add-file-card"
|
||||
onFileSelect={handleFileUpload}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeStirlingFileStubs.map((record, index) => {
|
||||
return (
|
||||
<FileEditorThumbnail
|
||||
|
||||
115
frontend/src/components/fileManager/EmptyFilesState.tsx
Normal file
115
frontend/src/components/fileManager/EmptyFilesState.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Group, Text, Stack, useMantineColorScheme } from '@mantine/core';
|
||||
import HistoryIcon from '@mui/icons-material/History';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useFileManagerContext } from '../../contexts/FileManagerContext';
|
||||
import LocalIcon from '../shared/LocalIcon';
|
||||
import { BASE_PATH } from '../../constants/app';
|
||||
|
||||
const EmptyFilesState: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const { onLocalFileClick } = useFileManagerContext();
|
||||
const [isUploadHover, setIsUploadHover] = useState(false);
|
||||
|
||||
const handleUploadClick = () => {
|
||||
onLocalFileClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '2rem'
|
||||
}}
|
||||
>
|
||||
{/* Container */}
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
padding: '3rem 2rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '1.5rem',
|
||||
minWidth: '20rem',
|
||||
maxWidth: '28rem',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
{/* No Recent Files Message */}
|
||||
<Stack align="center" gap="sm">
|
||||
<HistoryIcon style={{ fontSize: '3rem', color: 'var(--mantine-color-gray-5)' }} />
|
||||
<Text c="dimmed" ta="center" size="lg">
|
||||
{t('fileManager.noRecentFiles', 'No recent files')}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
{/* Stirling PDF Logo */}
|
||||
<Group gap="xs" align="center">
|
||||
<img
|
||||
src={colorScheme === 'dark' ? `${BASE_PATH}/branding/StirlingPDFLogoWhiteText.svg` : `${BASE_PATH}/branding/StirlingPDFLogoGreyText.svg`}
|
||||
alt="Stirling PDF"
|
||||
style={{ height: '2.2rem', width: 'auto' }}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{/* Upload Button */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
marginTop: '0.5rem',
|
||||
marginBottom: '0.5rem'
|
||||
}}
|
||||
onMouseLeave={() => setIsUploadHover(false)}
|
||||
>
|
||||
<Button
|
||||
aria-label="Upload"
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-file-manager)',
|
||||
color: 'var(--landing-button-color)',
|
||||
border: '1px solid var(--landing-button-border)',
|
||||
borderRadius: isUploadHover ? '2rem' : '1rem',
|
||||
height: '38px',
|
||||
width: isUploadHover ? '100%' : '58px',
|
||||
minWidth: '58px',
|
||||
paddingLeft: isUploadHover ? '1rem' : 0,
|
||||
paddingRight: isUploadHover ? '1rem' : 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'width .5s ease, padding .5s ease, border-radius .5s ease'
|
||||
}}
|
||||
onClick={handleUploadClick}
|
||||
onMouseEnter={() => setIsUploadHover(true)}
|
||||
>
|
||||
<LocalIcon icon="upload" width="1.25rem" height="1.25rem" style={{ color: 'var(--accent-interactive)' }} />
|
||||
{isUploadHover && (
|
||||
<span style={{ marginLeft: '.5rem' }}>
|
||||
{t('landing.uploadFromComputer', 'Upload from computer')}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Instruction Text */}
|
||||
<span
|
||||
className="text-[var(--accent-interactive)]"
|
||||
style={{ fontSize: '.8rem', textAlign: 'center' }}
|
||||
>
|
||||
{t('fileUpload.dropFilesHere', 'Drop files here or click the upload button')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptyFilesState;
|
||||
@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Center, ScrollArea, Text, Stack } from '@mantine/core';
|
||||
import CloudIcon from '@mui/icons-material/Cloud';
|
||||
import HistoryIcon from '@mui/icons-material/History';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FileListItem from './FileListItem';
|
||||
import FileHistoryGroup from './FileHistoryGroup';
|
||||
import EmptyFilesState from './EmptyFilesState';
|
||||
import { useFileManagerContext } from '../../contexts/FileManagerContext';
|
||||
|
||||
interface FileListAreaProps {
|
||||
@ -29,6 +29,7 @@ const FileListArea: React.FC<FileListAreaProps> = ({
|
||||
onFileDoubleClick,
|
||||
onDownloadSingle,
|
||||
isFileSupported,
|
||||
isLoading,
|
||||
} = useFileManagerContext();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -43,15 +44,11 @@ const FileListArea: React.FC<FileListAreaProps> = ({
|
||||
scrollbarSize={8}
|
||||
>
|
||||
<Stack gap={0}>
|
||||
{recentFiles.length === 0 ? (
|
||||
{recentFiles.length === 0 && !isLoading ? (
|
||||
<EmptyFilesState />
|
||||
) : recentFiles.length === 0 && isLoading ? (
|
||||
<Center style={{ height: '12.5rem' }}>
|
||||
<Stack align="center" gap="sm">
|
||||
<HistoryIcon style={{ fontSize: '3rem', color: 'var(--mantine-color-gray-5)' }} />
|
||||
<Text c="dimmed" ta="center">{t('fileManager.noRecentFiles', 'No recent files')}</Text>
|
||||
<Text size="xs" c="dimmed" ta="center" style={{ opacity: 0.7 }}>
|
||||
{t('fileManager.dropFilesHint', 'Drop files anywhere to upload')}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Text c="dimmed" ta="center">{t('fileManager.loadingFiles', 'Loading files...')}</Text>
|
||||
</Center>
|
||||
) : (
|
||||
filteredFiles.map((file, index) => {
|
||||
|
||||
@ -9,7 +9,6 @@ const HiddenFileInput: React.FC = () => {
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple={true}
|
||||
accept={["*/*"] as any}
|
||||
onChange={onFileInputChange}
|
||||
style={{ display: 'none' }}
|
||||
data-testid="file-input"
|
||||
|
||||
@ -41,7 +41,7 @@ const LandingPage = () => {
|
||||
{/* White PDF Page Background */}
|
||||
<Dropzone
|
||||
onDrop={handleFileDrop}
|
||||
accept={["application/pdf", "application/zip", "application/x-zip-compressed"]}
|
||||
accept={["*/*"]}
|
||||
multiple={true}
|
||||
className="w-4/5 flex items-center justify-center h-[95%]"
|
||||
style={{
|
||||
@ -178,7 +178,7 @@ const LandingPage = () => {
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
accept=".pdf,.zip"
|
||||
accept="*/*"
|
||||
onChange={handleFileSelect}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
|
||||
@ -8,7 +8,7 @@ interface NavigationWarningModalProps {
|
||||
}
|
||||
|
||||
const NavigationWarningModal = ({
|
||||
onApplyAndContinue,
|
||||
onApplyAndContinue: _onApplyAndContinue,
|
||||
onExportAndContinue
|
||||
}: NavigationWarningModalProps) => {
|
||||
|
||||
@ -30,13 +30,6 @@ const NavigationWarningModal = ({
|
||||
confirmNavigation();
|
||||
};
|
||||
|
||||
const _handleApplyAndContinue = async () => {
|
||||
if (onApplyAndContinue) {
|
||||
await onApplyAndContinue();
|
||||
}
|
||||
setHasUnsavedChanges(false);
|
||||
confirmNavigation();
|
||||
};
|
||||
|
||||
const handleExportAndContinue = async () => {
|
||||
if (onExportAndContinue) {
|
||||
@ -85,7 +78,7 @@ const NavigationWarningModal = ({
|
||||
</Button>
|
||||
|
||||
{/* TODO:: Add this back in when it works */}
|
||||
{/* {onApplyAndContinue && (
|
||||
{/* {_onApplyAndContinue && (
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
|
||||
@ -15,6 +15,7 @@ import BulkSelectionPanel from '../pageEditor/BulkSelectionPanel';
|
||||
import { SearchInterface } from '../viewer/SearchInterface';
|
||||
import { ViewerContext } from '../../contexts/ViewerContext';
|
||||
import { useSignature } from '../../contexts/SignatureContext';
|
||||
import ViewerAnnotationControls from './rightRail/ViewerAnnotationControls';
|
||||
|
||||
import { parseSelection } from '../../utils/bulkselection/parseSelection';
|
||||
|
||||
@ -293,6 +294,9 @@ export default function RightRail() {
|
||||
<LocalIcon icon="view-list" width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
{/* Annotation Controls */}
|
||||
<ViewerAnnotationControls currentView={currentView} />
|
||||
</div>
|
||||
<Divider className="right-rail-divider" />
|
||||
</div>
|
||||
|
||||
@ -67,7 +67,7 @@
|
||||
}
|
||||
|
||||
.right-rail-slot.visible {
|
||||
max-height: 18rem; /* increased to fit additional controls + divider */
|
||||
max-height: 40rem; /* increased to fit additional controls + divider */
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@ -77,14 +77,14 @@
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
max-height: 18rem;
|
||||
max-height: 40rem;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rightRailShrinkUp {
|
||||
0% {
|
||||
max-height: 18rem;
|
||||
max-height: 40rem;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
|
||||
@ -0,0 +1,222 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ActionIcon, Popover } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import LocalIcon from '../LocalIcon';
|
||||
import { Tooltip } from '../Tooltip';
|
||||
import { ViewerContext } from '../../../contexts/ViewerContext';
|
||||
import { useSignature } from '../../../contexts/SignatureContext';
|
||||
import { ColorSwatchButton, ColorPicker } from '../../annotation/shared/ColorPicker';
|
||||
import { useFileState, useFileContext } from '../../../contexts/FileContext';
|
||||
import { generateThumbnailWithMetadata } from '../../../utils/thumbnailUtils';
|
||||
import { createProcessedFile } from '../../../contexts/file/fileActions';
|
||||
import { createStirlingFile, createNewStirlingFileStub } from '../../../types/fileContext';
|
||||
|
||||
interface ViewerAnnotationControlsProps {
|
||||
currentView: string;
|
||||
}
|
||||
|
||||
export default function ViewerAnnotationControls({ currentView }: ViewerAnnotationControlsProps) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedColor, setSelectedColor] = useState('#000000');
|
||||
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
|
||||
const [isHoverColorPickerOpen, setIsHoverColorPickerOpen] = useState(false);
|
||||
|
||||
// Viewer context for PDF controls - safely handle when not available
|
||||
const viewerContext = React.useContext(ViewerContext);
|
||||
|
||||
// Signature context for accessing drawing API
|
||||
const { signatureApiRef } = useSignature();
|
||||
|
||||
// File state for save functionality
|
||||
const { state, selectors } = useFileState();
|
||||
const { actions: fileActions } = useFileContext();
|
||||
const activeFiles = selectors.getFiles();
|
||||
|
||||
// Turn off annotation mode when switching away from viewer
|
||||
useEffect(() => {
|
||||
if (currentView !== 'viewer' && viewerContext?.isAnnotationMode) {
|
||||
viewerContext.setAnnotationMode(false);
|
||||
}
|
||||
}, [currentView, viewerContext]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Annotation Visibility Toggle */}
|
||||
<Tooltip content={t('rightRail.toggleAnnotations', 'Toggle Annotations Visibility')} position="left" offset={12} arrow>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={() => {
|
||||
viewerContext?.toggleAnnotationsVisibility();
|
||||
}}
|
||||
disabled={currentView !== 'viewer' || viewerContext?.isAnnotationMode}
|
||||
>
|
||||
<LocalIcon
|
||||
icon={viewerContext?.isAnnotationsVisible ? "visibility" : "visibility-off-rounded"}
|
||||
width="1.5rem"
|
||||
height="1.5rem"
|
||||
/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
{/* Annotation Mode Toggle with Drawing Controls */}
|
||||
{viewerContext?.isAnnotationMode ? (
|
||||
// When active: Show color picker on hover
|
||||
<div
|
||||
onMouseEnter={() => setIsHoverColorPickerOpen(true)}
|
||||
onMouseLeave={() => setIsHoverColorPickerOpen(false)}
|
||||
style={{ display: 'inline-flex' }}
|
||||
>
|
||||
<Popover
|
||||
opened={isHoverColorPickerOpen}
|
||||
onClose={() => setIsHoverColorPickerOpen(false)}
|
||||
position="left"
|
||||
withArrow
|
||||
shadow="md"
|
||||
offset={8}
|
||||
>
|
||||
<Popover.Target>
|
||||
<ActionIcon
|
||||
variant="filled"
|
||||
color="blue"
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={() => {
|
||||
viewerContext?.toggleAnnotationMode();
|
||||
setIsHoverColorPickerOpen(false); // Close hover color picker when toggling off
|
||||
// Deactivate drawing tool when exiting annotation mode
|
||||
if (signatureApiRef?.current) {
|
||||
try {
|
||||
signatureApiRef.current.deactivateTools();
|
||||
} catch (error) {
|
||||
console.log('Signature API not ready:', error);
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={currentView !== 'viewer'}
|
||||
aria-label="Drawing mode active"
|
||||
>
|
||||
<LocalIcon icon="edit" width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<div style={{ minWidth: '8rem' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem', padding: '0.5rem' }}>
|
||||
<div style={{ fontSize: '0.8rem', fontWeight: 500 }}>Drawing Color</div>
|
||||
<ColorSwatchButton
|
||||
color={selectedColor}
|
||||
size={32}
|
||||
onClick={() => {
|
||||
setIsHoverColorPickerOpen(false); // Close hover picker
|
||||
setIsColorPickerOpen(true); // Open main color picker modal
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
</div>
|
||||
) : (
|
||||
// When inactive: Show "Draw" tooltip
|
||||
<Tooltip content={t('rightRail.draw', 'Draw')} position="left" offset={12} arrow>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={() => {
|
||||
viewerContext?.toggleAnnotationMode();
|
||||
// Activate ink drawing tool when entering annotation mode
|
||||
if (signatureApiRef?.current && currentView === 'viewer') {
|
||||
try {
|
||||
signatureApiRef.current.activateDrawMode();
|
||||
signatureApiRef.current.updateDrawSettings(selectedColor, 2);
|
||||
} catch (error) {
|
||||
console.log('Signature API not ready:', error);
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={currentView !== 'viewer'}
|
||||
aria-label={typeof t === 'function' ? t('rightRail.draw', 'Draw') : 'Draw'}
|
||||
>
|
||||
<LocalIcon icon="edit" width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Save PDF with Annotations */}
|
||||
<Tooltip content={t('rightRail.save', 'Save')} position="left" offset={12} arrow>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={async () => {
|
||||
if (viewerContext?.exportActions?.saveAsCopy && currentView === 'viewer') {
|
||||
try {
|
||||
const pdfArrayBuffer = await viewerContext.exportActions.saveAsCopy();
|
||||
if (pdfArrayBuffer) {
|
||||
// Create new File object with flattened annotations
|
||||
const blob = new Blob([pdfArrayBuffer], { type: 'application/pdf' });
|
||||
|
||||
// Get the original file name or use a default
|
||||
const originalFileName = activeFiles.length > 0 ? activeFiles[0].name : 'document.pdf';
|
||||
const newFile = new File([blob], originalFileName, { type: 'application/pdf' });
|
||||
|
||||
// Replace the current file in context with the saved version (exact same logic as Sign tool)
|
||||
if (activeFiles.length > 0) {
|
||||
// Generate thumbnail and metadata for the saved file
|
||||
const thumbnailResult = await generateThumbnailWithMetadata(newFile);
|
||||
const processedFileMetadata = createProcessedFile(thumbnailResult.pageCount, thumbnailResult.thumbnail);
|
||||
|
||||
// Get current file info
|
||||
const currentFileIds = state.files.ids;
|
||||
if (currentFileIds.length > 0) {
|
||||
const currentFileId = currentFileIds[0];
|
||||
const currentRecord = selectors.getStirlingFileStub(currentFileId);
|
||||
|
||||
if (!currentRecord) {
|
||||
console.error('No file record found for:', currentFileId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create output stub and file (exact same as Sign tool)
|
||||
const outputStub = createNewStirlingFileStub(newFile, undefined, thumbnailResult.thumbnail, processedFileMetadata);
|
||||
const outputStirlingFile = createStirlingFile(newFile, outputStub.id);
|
||||
|
||||
// Replace the original file with the saved version
|
||||
await fileActions.consumeFiles([currentFileId], [outputStirlingFile], [outputStub]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving PDF:', error);
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={currentView !== 'viewer'}
|
||||
>
|
||||
<LocalIcon icon="save" width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
{/* Color Picker Modal */}
|
||||
<ColorPicker
|
||||
isOpen={isColorPickerOpen}
|
||||
onClose={() => setIsColorPickerOpen(false)}
|
||||
selectedColor={selectedColor}
|
||||
onColorChange={(color) => {
|
||||
setSelectedColor(color);
|
||||
// Update drawing tool color if annotation mode is active
|
||||
if (viewerContext?.isAnnotationMode && signatureApiRef?.current && currentView === 'viewer') {
|
||||
try {
|
||||
signatureApiRef.current.updateDrawSettings(color, 2);
|
||||
} catch (error) {
|
||||
console.log('Unable to update drawing settings:', error);
|
||||
}
|
||||
}
|
||||
}}
|
||||
title="Choose Drawing Color"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -29,16 +29,19 @@ const EmbedPdfViewerContent = ({
|
||||
const { colorScheme: _colorScheme } = useMantineColorScheme();
|
||||
const viewerRef = React.useRef<HTMLDivElement>(null);
|
||||
const [isViewerHovered, setIsViewerHovered] = React.useState(false);
|
||||
const { isThumbnailSidebarVisible, toggleThumbnailSidebar, zoomActions, spreadActions, panActions: _panActions, rotationActions: _rotationActions, getScrollState, getZoomState, getSpreadState } = useViewer();
|
||||
const { isThumbnailSidebarVisible, toggleThumbnailSidebar, zoomActions, spreadActions, panActions: _panActions, rotationActions: _rotationActions, getScrollState, getZoomState, getSpreadState, isAnnotationMode, isAnnotationsVisible } = useViewer();
|
||||
|
||||
const scrollState = getScrollState();
|
||||
const zoomState = getZoomState();
|
||||
const spreadState = getSpreadState();
|
||||
|
||||
// Check if we're in signature mode
|
||||
// Check if we're in signature mode OR viewer annotation mode
|
||||
const { selectedTool } = useNavigationState();
|
||||
const isSignatureMode = selectedTool === 'sign';
|
||||
|
||||
// Enable annotations when: in sign mode, OR annotation mode is active, OR we want to show existing annotations
|
||||
const shouldEnableAnnotations = isSignatureMode || isAnnotationMode || isAnnotationsVisible;
|
||||
|
||||
// Get signature context
|
||||
const { signatureApiRef, historyApiRef } = useSignature();
|
||||
|
||||
@ -186,7 +189,7 @@ const EmbedPdfViewerContent = ({
|
||||
<LocalEmbedPDF
|
||||
file={effectiveFile.file}
|
||||
url={effectiveFile.url}
|
||||
enableSignature={isSignatureMode}
|
||||
enableAnnotations={shouldEnableAnnotations}
|
||||
signatureApiRef={signatureApiRef as React.RefObject<any>}
|
||||
historyApiRef={historyApiRef as React.RefObject<any>}
|
||||
onSignatureAdded={() => {
|
||||
|
||||
@ -42,13 +42,13 @@ import { ExportAPIBridge } from './ExportAPIBridge';
|
||||
interface LocalEmbedPDFProps {
|
||||
file?: File | Blob;
|
||||
url?: string | null;
|
||||
enableSignature?: boolean;
|
||||
enableAnnotations?: boolean;
|
||||
onSignatureAdded?: (annotation: any) => void;
|
||||
signatureApiRef?: React.RefObject<SignatureAPI>;
|
||||
historyApiRef?: React.RefObject<HistoryAPI>;
|
||||
}
|
||||
|
||||
export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureAdded, signatureApiRef, historyApiRef }: LocalEmbedPDFProps) {
|
||||
export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatureAdded, signatureApiRef, historyApiRef }: LocalEmbedPDFProps) {
|
||||
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
|
||||
const [, setAnnotations] = useState<Array<{id: string, pageIndex: number, rect: any}>>([]);
|
||||
|
||||
@ -93,10 +93,10 @@ export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureA
|
||||
createPluginRegistration(SelectionPluginPackage),
|
||||
|
||||
// Register history plugin for undo/redo (recommended for annotations)
|
||||
...(enableSignature ? [createPluginRegistration(HistoryPluginPackage)] : []),
|
||||
...(enableAnnotations ? [createPluginRegistration(HistoryPluginPackage)] : []),
|
||||
|
||||
// Register annotation plugin (depends on InteractionManager, Selection, History)
|
||||
...(enableSignature ? [createPluginRegistration(AnnotationPluginPackage, {
|
||||
...(enableAnnotations ? [createPluginRegistration(AnnotationPluginPackage, {
|
||||
annotationAuthor: 'Digital Signature',
|
||||
autoCommit: true,
|
||||
deactivateToolAfterCreate: false,
|
||||
@ -194,7 +194,7 @@ export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureA
|
||||
<EmbedPDF
|
||||
engine={engine}
|
||||
plugins={plugins}
|
||||
onInitialized={enableSignature ? async (registry) => {
|
||||
onInitialized={enableAnnotations ? async (registry) => {
|
||||
const annotationPlugin = registry.getPlugin('annotation');
|
||||
if (!annotationPlugin || !annotationPlugin.provides) return;
|
||||
|
||||
@ -265,8 +265,8 @@ export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureA
|
||||
<SearchAPIBridge />
|
||||
<ThumbnailAPIBridge />
|
||||
<RotateAPIBridge />
|
||||
{enableSignature && <SignatureAPIBridge ref={signatureApiRef} />}
|
||||
{enableSignature && <HistoryAPIBridge ref={historyApiRef} />}
|
||||
{enableAnnotations && <SignatureAPIBridge ref={signatureApiRef} />}
|
||||
{enableAnnotations && <HistoryAPIBridge ref={historyApiRef} />}
|
||||
<ExportAPIBridge />
|
||||
<GlobalPointerProvider>
|
||||
<Viewport
|
||||
@ -312,7 +312,7 @@ export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureA
|
||||
{/* Selection layer for text interaction */}
|
||||
<SelectionLayer pageIndex={pageIndex} scale={scale} />
|
||||
{/* Annotation layer for signatures (only when enabled) */}
|
||||
{enableSignature && (
|
||||
{enableAnnotations && (
|
||||
<AnnotationLayer
|
||||
pageIndex={pageIndex}
|
||||
scale={scale}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { createPluginRegistration } from '@embedpdf/core';
|
||||
import { EmbedPDF } from '@embedpdf/core/react';
|
||||
import { usePdfiumEngine } from '@embedpdf/engines/react';
|
||||
@ -312,4 +312,4 @@ export function LocalEmbedPDFWithAnnotations({
|
||||
</EmbedPDF>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
|
||||
import { PdfAnnotationSubtype, PdfStandardFont, PdfTextAlignment, PdfVerticalAlignment, uuidV4 } from '@embedpdf/models';
|
||||
import { SignParameters } from '../../hooks/tools/sign/useSignParameters';
|
||||
import { useSignature } from '../../contexts/SignatureContext';
|
||||
import { useViewer } from '../../contexts/ViewerContext';
|
||||
|
||||
export interface SignatureAPI {
|
||||
addImageSignature: (signatureData: string, x: number, y: number, width: number, height: number, pageIndex: number) => void;
|
||||
@ -20,11 +21,12 @@ export interface SignatureAPI {
|
||||
export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPIBridge(_, ref) {
|
||||
const { provides: annotationApi } = useAnnotationCapability();
|
||||
const { signatureConfig, storeImageData, isPlacementMode } = useSignature();
|
||||
const { isAnnotationMode } = useViewer();
|
||||
|
||||
|
||||
// Enable keyboard deletion of selected annotations - only when in signature placement mode
|
||||
// Enable keyboard deletion of selected annotations - when in signature placement mode or viewer annotation mode
|
||||
useEffect(() => {
|
||||
if (!annotationApi || !isPlacementMode) return;
|
||||
if (!annotationApi || (!isPlacementMode && !isAnnotationMode)) return;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Delete' || event.key === 'Backspace') {
|
||||
@ -65,7 +67,7 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPI
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [annotationApi, storeImageData, isPlacementMode]);
|
||||
}, [annotationApi, storeImageData, isPlacementMode, isAnnotationMode]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
addImageSignature: (signatureData: string, x: number, y: number, width: number, height: number, pageIndex: number) => {
|
||||
|
||||
@ -15,7 +15,19 @@ export function ZoomAPIBridge() {
|
||||
if (zoom && !hasSetInitialZoom.current) {
|
||||
hasSetInitialZoom.current = true;
|
||||
setTimeout(() => {
|
||||
zoom.requestZoom(1.4);
|
||||
try {
|
||||
zoom.requestZoom(1.4);
|
||||
} catch (error) {
|
||||
console.log('Zoom initialization delayed, viewport not ready:', error);
|
||||
// Retry after a longer delay
|
||||
setTimeout(() => {
|
||||
try {
|
||||
zoom.requestZoom(1.4);
|
||||
} catch (retryError) {
|
||||
console.log('Zoom initialization failed:', retryError);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
}, [zoom]);
|
||||
|
||||
@ -18,6 +18,7 @@ interface FileManagerContextValue {
|
||||
expandedFileIds: Set<FileId>;
|
||||
fileGroups: Map<FileId, StirlingFileStub[]>;
|
||||
loadedHistoryFiles: Map<FileId, StirlingFileStub[]>;
|
||||
isLoading: boolean;
|
||||
|
||||
// Handlers
|
||||
onSourceChange: (source: 'recent' | 'local' | 'drive') => void;
|
||||
@ -58,6 +59,7 @@ interface FileManagerProviderProps {
|
||||
onFileRemove: (index: number) => void;
|
||||
modalHeight: string;
|
||||
refreshRecentFiles: () => Promise<void>;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
||||
@ -71,6 +73,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
||||
onFileRemove,
|
||||
modalHeight,
|
||||
refreshRecentFiles,
|
||||
isLoading,
|
||||
}) => {
|
||||
const [activeSource, setActiveSource] = useState<'recent' | 'local' | 'drive'>('recent');
|
||||
const [selectedFileIds, setSelectedFileIds] = useState<FileId[]>([]);
|
||||
@ -574,6 +577,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
||||
expandedFileIds,
|
||||
fileGroups,
|
||||
loadedHistoryFiles,
|
||||
isLoading,
|
||||
|
||||
// Handlers
|
||||
onSourceChange: handleSourceChange,
|
||||
@ -607,6 +611,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
||||
expandedFileIds,
|
||||
fileGroups,
|
||||
loadedHistoryFiles,
|
||||
isLoading,
|
||||
handleSourceChange,
|
||||
handleLocalFileClick,
|
||||
handleFileSelect,
|
||||
|
||||
@ -123,6 +123,15 @@ interface ViewerContextType {
|
||||
isThumbnailSidebarVisible: boolean;
|
||||
toggleThumbnailSidebar: () => void;
|
||||
|
||||
// Annotation visibility toggle
|
||||
isAnnotationsVisible: boolean;
|
||||
toggleAnnotationsVisibility: () => void;
|
||||
|
||||
// Annotation/drawing mode for viewer
|
||||
isAnnotationMode: boolean;
|
||||
setAnnotationMode: (enabled: boolean) => void;
|
||||
toggleAnnotationMode: () => void;
|
||||
|
||||
// State getters - read current state from bridges
|
||||
getScrollState: () => ScrollState;
|
||||
getZoomState: () => ZoomState;
|
||||
@ -208,6 +217,8 @@ interface ViewerProviderProps {
|
||||
export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
||||
// UI state - only state directly managed by this context
|
||||
const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = useState(false);
|
||||
const [isAnnotationsVisible, setIsAnnotationsVisible] = useState(true);
|
||||
const [isAnnotationMode, setIsAnnotationModeState] = useState(false);
|
||||
|
||||
// Get current navigation state to check if we're in sign mode
|
||||
useNavigation();
|
||||
@ -268,6 +279,18 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
||||
setIsThumbnailSidebarVisible(prev => !prev);
|
||||
};
|
||||
|
||||
const toggleAnnotationsVisibility = () => {
|
||||
setIsAnnotationsVisible(prev => !prev);
|
||||
};
|
||||
|
||||
const setAnnotationMode = (enabled: boolean) => {
|
||||
setIsAnnotationModeState(enabled);
|
||||
};
|
||||
|
||||
const toggleAnnotationMode = () => {
|
||||
setIsAnnotationModeState(prev => !prev);
|
||||
};
|
||||
|
||||
// State getters - read from bridge refs
|
||||
const getScrollState = (): ScrollState => {
|
||||
return bridgeRefs.current.scroll?.state || { currentPage: 1, totalPages: 0 };
|
||||
@ -547,6 +570,13 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
||||
isThumbnailSidebarVisible,
|
||||
toggleThumbnailSidebar,
|
||||
|
||||
// Annotation controls
|
||||
isAnnotationsVisible,
|
||||
toggleAnnotationsVisibility,
|
||||
isAnnotationMode,
|
||||
setAnnotationMode,
|
||||
toggleAnnotationMode,
|
||||
|
||||
// State getters
|
||||
getScrollState,
|
||||
getZoomState,
|
||||
|
||||
@ -476,7 +476,6 @@ export async function addStirlingFileStubs(
|
||||
await addFilesMutex.lock();
|
||||
|
||||
try {
|
||||
if (DEBUG) console.log(`📄 addStirlingFileStubs: Adding ${stirlingFileStubs.length} StirlingFileStubs preserving metadata`);
|
||||
|
||||
const existingQuickKeys = buildQuickKeySet(stateRef.current.files.byId);
|
||||
const validStubs: StirlingFileStub[] = [];
|
||||
@ -515,14 +514,12 @@ export async function addStirlingFileStubs(
|
||||
record.processedFile.totalPages !== record.processedFile.pages.length;
|
||||
|
||||
if (needsProcessing) {
|
||||
if (DEBUG) console.log(`📄 addStirlingFileStubs: Regenerating processedFile for ${record.name}`);
|
||||
|
||||
// Use centralized metadata generation function
|
||||
const processedFileMetadata = await generateProcessedFileMetadata(stirlingFile);
|
||||
if (processedFileMetadata) {
|
||||
record.processedFile = processedFileMetadata;
|
||||
record.thumbnailUrl = processedFileMetadata.thumbnailUrl; // Update thumbnail if needed
|
||||
if (DEBUG) console.log(`📄 addStirlingFileStubs: Regenerated processedFile for ${record.name} with ${processedFileMetadata.totalPages} pages`);
|
||||
} else {
|
||||
// Fallback for files that couldn't be processed
|
||||
if (DEBUG) console.warn(`📄 addStirlingFileStubs: Failed to regenerate processedFile for ${record.name}`);
|
||||
@ -541,7 +538,6 @@ export async function addStirlingFileStubs(
|
||||
// Dispatch ADD_FILES action if we have new files
|
||||
if (validStubs.length > 0) {
|
||||
dispatch({ type: 'ADD_FILES', payload: { stirlingFileStubs: validStubs } });
|
||||
if (DEBUG) console.log(`📄 addStirlingFileStubs: Successfully added ${validStubs.length} files with preserved metadata`);
|
||||
}
|
||||
|
||||
return loadedFiles;
|
||||
|
||||
@ -70,7 +70,6 @@ async function processRequestQueue() {
|
||||
const pageNumbers = requests.map(req => req.pageNumber);
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
|
||||
console.log(`📸 Batch generating ${requests.length} thumbnails for pages: ${pageNumbers.slice(0, 5).join(', ')}${pageNumbers.length > 5 ? '...' : ''}`);
|
||||
|
||||
// Use quickKey for PDF document caching (same metadata, consistent format)
|
||||
const fileId = createQuickKey(file) as FileId;
|
||||
@ -80,9 +79,8 @@ async function processRequestQueue() {
|
||||
arrayBuffer,
|
||||
pageNumbers,
|
||||
{ scale: 1.0, quality: 0.8, batchSize: BATCH_SIZE },
|
||||
(progress) => {
|
||||
(_progress) => {
|
||||
// Optional: Could emit progress events here for UI feedback
|
||||
console.log(`📸 Batch progress: ${progress.completed}/${progress.total} thumbnails generated`);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user