mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-18 09:29:24 +00:00
Merge branch 'V2' into urls
This commit is contained in:
commit
343d7b5b8a
@ -24,7 +24,7 @@ indent_size = 2
|
|||||||
insert_final_newline = false
|
insert_final_newline = false
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
[{*.js,*.jsx,*.ts,*.tsx}]
|
[{*.js,*.jsx,*.mjs,*.ts,*.tsx}]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
[*.css]
|
[*.css]
|
||||||
|
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -147,6 +147,8 @@ jobs:
|
|||||||
cache-dependency-path: frontend/package-lock.json
|
cache-dependency-path: frontend/package-lock.json
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: cd frontend && npm ci
|
run: cd frontend && npm ci
|
||||||
|
- name: Lint frontend
|
||||||
|
run: cd frontend && npm run lint
|
||||||
- name: Build frontend
|
- name: Build frontend
|
||||||
run: cd frontend && npm run build
|
run: cd frontend && npm run build
|
||||||
- name: Run frontend tests
|
- name: Run frontend tests
|
||||||
|
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
@ -19,5 +19,6 @@
|
|||||||
"yzhang.markdown-all-in-one", // Markdown All-in-One extension for enhanced Markdown editing
|
"yzhang.markdown-all-in-one", // Markdown All-in-One extension for enhanced Markdown editing
|
||||||
"stylelint.vscode-stylelint", // Stylelint extension for CSS and SCSS linting
|
"stylelint.vscode-stylelint", // Stylelint extension for CSS and SCSS linting
|
||||||
"redhat.vscode-yaml", // YAML extension for Visual Studio Code
|
"redhat.vscode-yaml", // YAML extension for Visual Studio Code
|
||||||
|
"dbaeumer.vscode-eslint", // ESLint extension for TypeScript linting
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
42
frontend/eslint.config.mjs
Normal file
42
frontend/eslint.config.mjs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import { defineConfig } from 'eslint/config';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default defineConfig(
|
||||||
|
eslint.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
"dist", // Contains 3rd party code
|
||||||
|
"public", // Contains 3rd party code
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
"no-undef": "off", // Temporarily disabled until codebase conformant
|
||||||
|
"@typescript-eslint/no-empty-object-type": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
// Allow empty extending interfaces because there's no real reason not to, and it makes it obvious where to put extra attributes in the future
|
||||||
|
allowInterfaces: 'with-single-extends',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-explicit-any": "off", // Temporarily disabled until codebase conformant
|
||||||
|
"@typescript-eslint/no-require-imports": "off", // Temporarily disabled until codebase conformant
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"args": "all", // All function args must be used (or explicitly ignored)
|
||||||
|
"argsIgnorePattern": "^_", // Allow unused variables beginning with an underscore
|
||||||
|
"caughtErrors": "all", // Caught errors must be used (or explicitly ignored)
|
||||||
|
"caughtErrorsIgnorePattern": "^_", // Allow unused variables beginning with an underscore
|
||||||
|
"destructuredArrayIgnorePattern": "^_", // Allow unused variables beginning with an underscore
|
||||||
|
"varsIgnorePattern": "^_", // Allow unused variables beginning with an underscore
|
||||||
|
"ignoreRestSiblings": true, // Allow unused variables when removing attributes from objects (otherwise this requires explicit renaming like `({ x: _x, ...y }) => y`, which is clunky)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
934
frontend/package-lock.json
generated
934
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -40,6 +40,7 @@
|
|||||||
"predev": "npm run generate-icons",
|
"predev": "npm run generate-icons",
|
||||||
"dev": "npx tsc --noEmit && vite",
|
"dev": "npx tsc --noEmit && vite",
|
||||||
"prebuild": "npm run generate-icons",
|
"prebuild": "npm run generate-icons",
|
||||||
|
"lint": "npx eslint",
|
||||||
"build": "npx tsc --noEmit && vite build",
|
"build": "npx tsc --noEmit && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
@ -72,6 +73,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.34.0",
|
||||||
"@iconify-json/material-symbols": "^1.2.33",
|
"@iconify-json/material-symbols": "^1.2.33",
|
||||||
"@iconify/utils": "^3.0.1",
|
"@iconify/utils": "^3.0.1",
|
||||||
"@playwright/test": "^1.40.0",
|
"@playwright/test": "^1.40.0",
|
||||||
@ -80,6 +82,7 @@
|
|||||||
"@types/react-dom": "^19.1.5",
|
"@types/react-dom": "^19.1.5",
|
||||||
"@vitejs/plugin-react": "^4.5.0",
|
"@vitejs/plugin-react": "^4.5.0",
|
||||||
"@vitest/coverage-v8": "^1.0.0",
|
"@vitest/coverage-v8": "^1.0.0",
|
||||||
|
"eslint": "^9.34.0",
|
||||||
"jsdom": "^23.0.0",
|
"jsdom": "^23.0.0",
|
||||||
"license-checker": "^25.0.1",
|
"license-checker": "^25.0.1",
|
||||||
"madge": "^8.0.0",
|
"madge": "^8.0.0",
|
||||||
@ -87,7 +90,8 @@
|
|||||||
"postcss-cli": "^11.0.1",
|
"postcss-cli": "^11.0.1",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.9.2",
|
||||||
|
"typescript-eslint": "^8.42.0",
|
||||||
"vite": "^6.3.5",
|
"vite": "^6.3.5",
|
||||||
"vitest": "^1.0.0"
|
"vitest": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,7 @@ async function main() {
|
|||||||
needsRegeneration = false;
|
needsRegeneration = false;
|
||||||
info(`✅ Icon set already up-to-date (${usedIcons.length} icons, ${Math.round(fs.statSync(outputPath).size / 1024)}KB)`);
|
info(`✅ Icon set already up-to-date (${usedIcons.length} icons, ${Math.round(fs.statSync(outputPath).size / 1024)}KB)`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
// If we can't parse existing file, regenerate
|
// If we can't parse existing file, regenerate
|
||||||
needsRegeneration = true;
|
needsRegeneration = true;
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ try {
|
|||||||
// Install license-checker if not present
|
// Install license-checker if not present
|
||||||
try {
|
try {
|
||||||
require.resolve('license-checker');
|
require.resolve('license-checker');
|
||||||
} catch (e) {
|
} catch {
|
||||||
console.log('📦 Installing license-checker...');
|
console.log('📦 Installing license-checker...');
|
||||||
execSync('npm install --save-dev license-checker', { stdio: 'inherit' });
|
execSync('npm install --save-dev license-checker', { stdio: 'inherit' });
|
||||||
}
|
}
|
||||||
@ -224,7 +224,7 @@ function getLicenseUrl(licenseType) {
|
|||||||
// Handle complex SPDX expressions like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)"
|
// Handle complex SPDX expressions like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)"
|
||||||
if (licenseType.includes('AND') || licenseType.includes('OR')) {
|
if (licenseType.includes('AND') || licenseType.includes('OR')) {
|
||||||
// Extract the first license from compound expressions for URL
|
// Extract the first license from compound expressions for URL
|
||||||
const match = licenseType.match(/\(?\s*([A-Za-z0-9\-\.]+)/);
|
const match = licenseType.match(/\(?\s*([A-Za-z0-9\-.]+)/);
|
||||||
if (match && licenseUrls[match[1]]) {
|
if (match && licenseUrls[match[1]]) {
|
||||||
return licenseUrls[match[1]];
|
return licenseUrls[match[1]];
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,9 @@ import "./styles/cookieconsent.css";
|
|||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { RightRailProvider } from "./contexts/RightRailContext";
|
import { RightRailProvider } from "./contexts/RightRailContext";
|
||||||
|
|
||||||
|
// Import file ID debugging helpers (development only)
|
||||||
|
import "./utils/fileIdSafety";
|
||||||
|
|
||||||
// Loading component for i18next suspense
|
// Loading component for i18next suspense
|
||||||
const LoadingFallback = () => (
|
const LoadingFallback = () => (
|
||||||
<div
|
<div
|
||||||
|
@ -4,7 +4,6 @@ import { Dropzone } from '@mantine/dropzone';
|
|||||||
import { FileMetadata } from '../types/file';
|
import { FileMetadata } from '../types/file';
|
||||||
import { useFileManager } from '../hooks/useFileManager';
|
import { useFileManager } from '../hooks/useFileManager';
|
||||||
import { useFilesModalContext } from '../contexts/FilesModalContext';
|
import { useFilesModalContext } from '../contexts/FilesModalContext';
|
||||||
import { createFileId } from '../types/fileContext';
|
|
||||||
import { Tool } from '../types/tool';
|
import { Tool } from '../types/tool';
|
||||||
import MobileLayout from './fileManager/MobileLayout';
|
import MobileLayout from './fileManager/MobileLayout';
|
||||||
import DesktopLayout from './fileManager/DesktopLayout';
|
import DesktopLayout from './fileManager/DesktopLayout';
|
||||||
@ -21,13 +20,7 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
|||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
|
|
||||||
const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile } = useFileManager();
|
const { loadRecentFiles, handleRemoveFile, convertToFile } = useFileManager();
|
||||||
|
|
||||||
// Wrapper for storeFile that generates UUID
|
|
||||||
const storeFileWithId = useCallback(async (file: File) => {
|
|
||||||
const fileId = createFileId(); // Generate UUID for storage
|
|
||||||
return await storeFile(file, fileId);
|
|
||||||
}, [storeFile]);
|
|
||||||
|
|
||||||
// File management handlers
|
// File management handlers
|
||||||
const isFileSupported = useCallback((fileName: string) => {
|
const isFileSupported = useCallback((fileName: string) => {
|
||||||
|
@ -1,42 +1,28 @@
|
|||||||
import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
import React, { useState, useCallback, useRef, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
Text, Center, Box, Notification, LoadingOverlay, Stack, Group, Portal
|
Text, Center, Box, Notification, LoadingOverlay, Stack, Group, Portal
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useFileSelection, useFileState, useFileManagement } from '../../contexts/FileContext';
|
||||||
import UploadFileIcon from '@mui/icons-material/UploadFile';
|
|
||||||
import { useFileSelection, useFileState, useFileManagement, useFileActions } from '../../contexts/FileContext';
|
|
||||||
import { useNavigationActions } from '../../contexts/NavigationContext';
|
import { useNavigationActions } from '../../contexts/NavigationContext';
|
||||||
import { FileOperation } from '../../types/fileContext';
|
|
||||||
import { fileStorage } from '../../services/fileStorage';
|
|
||||||
import { generateThumbnailForFile } from '../../utils/thumbnailUtils';
|
|
||||||
import { zipFileService } from '../../services/zipFileService';
|
import { zipFileService } from '../../services/zipFileService';
|
||||||
import { detectFileExtension } from '../../utils/fileUtils';
|
import { detectFileExtension } from '../../utils/fileUtils';
|
||||||
import styles from './FileEditor.module.css';
|
|
||||||
import FileEditorThumbnail from './FileEditorThumbnail';
|
import FileEditorThumbnail from './FileEditorThumbnail';
|
||||||
import FilePickerModal from '../shared/FilePickerModal';
|
import FilePickerModal from '../shared/FilePickerModal';
|
||||||
import SkeletonLoader from '../shared/SkeletonLoader';
|
import SkeletonLoader from '../shared/SkeletonLoader';
|
||||||
import { FileId } from '../../types/file';
|
import { FileId, StirlingFile } from '../../types/fileContext';
|
||||||
|
|
||||||
|
|
||||||
interface FileEditorProps {
|
interface FileEditorProps {
|
||||||
onOpenPageEditor?: (file: File) => void;
|
onOpenPageEditor?: () => void;
|
||||||
onMergeFiles?: (files: File[]) => void;
|
onMergeFiles?: (files: StirlingFile[]) => void;
|
||||||
toolMode?: boolean;
|
toolMode?: boolean;
|
||||||
showUpload?: boolean;
|
|
||||||
showBulkActions?: boolean;
|
|
||||||
supportedExtensions?: string[];
|
supportedExtensions?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileEditor = ({
|
const FileEditor = ({
|
||||||
onOpenPageEditor,
|
|
||||||
onMergeFiles,
|
|
||||||
toolMode = false,
|
toolMode = false,
|
||||||
showUpload = true,
|
|
||||||
showBulkActions = true,
|
|
||||||
supportedExtensions = ["pdf"]
|
supportedExtensions = ["pdf"]
|
||||||
}: FileEditorProps) => {
|
}: FileEditorProps) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
// Utility function to check if a file extension is supported
|
// Utility function to check if a file extension is supported
|
||||||
const isFileSupported = useCallback((fileName: string): boolean => {
|
const isFileSupported = useCallback((fileName: string): boolean => {
|
||||||
@ -49,13 +35,10 @@ const FileEditor = ({
|
|||||||
const { addFiles, removeFiles, reorderFiles } = useFileManagement();
|
const { addFiles, removeFiles, reorderFiles } = useFileManagement();
|
||||||
|
|
||||||
// Extract needed values from state (memoized to prevent infinite loops)
|
// Extract needed values from state (memoized to prevent infinite loops)
|
||||||
const activeFiles = useMemo(() => selectors.getFiles(), [selectors.getFilesSignature()]);
|
const activeStirlingFileStubs = useMemo(() => selectors.getStirlingFileStubs(), [selectors.getFilesSignature()]);
|
||||||
const activeFileRecords = useMemo(() => selectors.getFileRecords(), [selectors.getFilesSignature()]);
|
|
||||||
const selectedFileIds = state.ui.selectedFileIds;
|
const selectedFileIds = state.ui.selectedFileIds;
|
||||||
const isProcessing = state.ui.isProcessing;
|
|
||||||
|
|
||||||
// Get the real context actions
|
// Get navigation actions
|
||||||
const { actions } = useFileActions();
|
|
||||||
const { actions: navActions } = useNavigationActions();
|
const { actions: navActions } = useNavigationActions();
|
||||||
|
|
||||||
// Get file selection context
|
// Get file selection context
|
||||||
@ -92,10 +75,10 @@ const FileEditor = ({
|
|||||||
const contextSelectedIdsRef = useRef<FileId[]>([]);
|
const contextSelectedIdsRef = useRef<FileId[]>([]);
|
||||||
contextSelectedIdsRef.current = contextSelectedIds;
|
contextSelectedIdsRef.current = contextSelectedIds;
|
||||||
|
|
||||||
// Use activeFileRecords directly - no conversion needed
|
// Use activeStirlingFileStubs directly - no conversion needed
|
||||||
const localSelectedIds = contextSelectedIds;
|
const localSelectedIds = contextSelectedIds;
|
||||||
|
|
||||||
// Helper to convert FileRecord to FileThumbnail format
|
// Helper to convert StirlingFileStub to FileThumbnail format
|
||||||
const recordToFileItem = useCallback((record: any) => {
|
const recordToFileItem = useCallback((record: any) => {
|
||||||
const file = selectors.getFile(record.id);
|
const file = selectors.getFile(record.id);
|
||||||
if (!file) return null;
|
if (!file) return null;
|
||||||
@ -161,29 +144,9 @@ const FileEditor = ({
|
|||||||
if (extractionResult.success) {
|
if (extractionResult.success) {
|
||||||
allExtractedFiles.push(...extractionResult.extractedFiles);
|
allExtractedFiles.push(...extractionResult.extractedFiles);
|
||||||
|
|
||||||
// Record ZIP extraction operation
|
if (extractionResult.errors.length > 0) {
|
||||||
const operationId = `zip-extract-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
errors.push(...extractionResult.errors);
|
||||||
const operation: FileOperation = {
|
|
||||||
id: operationId,
|
|
||||||
type: 'convert',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
fileIds: extractionResult.extractedFiles.map(f => f.name) as FileId[] /* FIX ME: This doesn't seem right */,
|
|
||||||
status: 'pending',
|
|
||||||
metadata: {
|
|
||||||
originalFileName: file.name,
|
|
||||||
outputFileNames: extractionResult.extractedFiles.map(f => f.name),
|
|
||||||
fileSize: file.size,
|
|
||||||
parameters: {
|
|
||||||
extractionType: 'zip',
|
|
||||||
extractedCount: extractionResult.extractedCount,
|
|
||||||
totalFiles: extractionResult.totalFiles
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if (extractionResult.errors.length > 0) {
|
|
||||||
errors.push(...extractionResult.errors);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
errors.push(`Failed to extract ZIP file "${file.name}": ${extractionResult.errors.join(', ')}`);
|
errors.push(`Failed to extract ZIP file "${file.name}": ${extractionResult.errors.join(', ')}`);
|
||||||
}
|
}
|
||||||
@ -213,25 +176,6 @@ const FileEditor = ({
|
|||||||
|
|
||||||
// Process all extracted files
|
// Process all extracted files
|
||||||
if (allExtractedFiles.length > 0) {
|
if (allExtractedFiles.length > 0) {
|
||||||
// Record upload operations for PDF files
|
|
||||||
for (const file of allExtractedFiles) {
|
|
||||||
const operationId = `upload-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
||||||
const operation: FileOperation = {
|
|
||||||
id: operationId,
|
|
||||||
type: 'upload',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
fileIds: [file.name as FileId /* This doesn't seem right */],
|
|
||||||
status: 'pending',
|
|
||||||
metadata: {
|
|
||||||
originalFileName: file.name,
|
|
||||||
fileSize: file.size,
|
|
||||||
parameters: {
|
|
||||||
uploadMethod: 'drag-drop'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add files to context (they will be processed automatically)
|
// Add files to context (they will be processed automatically)
|
||||||
await addFiles(allExtractedFiles);
|
await addFiles(allExtractedFiles);
|
||||||
setStatus(`Added ${allExtractedFiles.length} files`);
|
setStatus(`Added ${allExtractedFiles.length} files`);
|
||||||
@ -252,27 +196,10 @@ const FileEditor = ({
|
|||||||
}
|
}
|
||||||
}, [addFiles]);
|
}, [addFiles]);
|
||||||
|
|
||||||
const selectAll = useCallback(() => {
|
|
||||||
setSelectedFiles(activeFileRecords.map(r => r.id)); // Use FileRecord IDs directly
|
|
||||||
}, [activeFileRecords, setSelectedFiles]);
|
|
||||||
|
|
||||||
const deselectAll = useCallback(() => setSelectedFiles([]), [setSelectedFiles]);
|
|
||||||
|
|
||||||
const closeAllFiles = useCallback(() => {
|
|
||||||
if (activeFileRecords.length === 0) return;
|
|
||||||
|
|
||||||
// Remove all files from context but keep in storage
|
|
||||||
const allFileIds = activeFileRecords.map(record => record.id);
|
|
||||||
removeFiles(allFileIds, false); // false = keep in storage
|
|
||||||
|
|
||||||
// Clear selections
|
|
||||||
setSelectedFiles([]);
|
|
||||||
}, [activeFileRecords, removeFiles, setSelectedFiles]);
|
|
||||||
|
|
||||||
const toggleFile = useCallback((fileId: FileId) => {
|
const toggleFile = useCallback((fileId: FileId) => {
|
||||||
const currentSelectedIds = contextSelectedIdsRef.current;
|
const currentSelectedIds = contextSelectedIdsRef.current;
|
||||||
|
|
||||||
const targetRecord = activeFileRecords.find(r => r.id === fileId);
|
const targetRecord = activeStirlingFileStubs.find(r => r.id === fileId);
|
||||||
if (!targetRecord) return;
|
if (!targetRecord) return;
|
||||||
|
|
||||||
const contextFileId = fileId; // No need to create a new ID
|
const contextFileId = fileId; // No need to create a new ID
|
||||||
@ -302,21 +229,12 @@ const FileEditor = ({
|
|||||||
|
|
||||||
// Update context (this automatically updates tool selection since they use the same action)
|
// Update context (this automatically updates tool selection since they use the same action)
|
||||||
setSelectedFiles(newSelection);
|
setSelectedFiles(newSelection);
|
||||||
}, [setSelectedFiles, toolMode, setStatus, activeFileRecords]);
|
}, [setSelectedFiles, toolMode, setStatus, activeStirlingFileStubs]);
|
||||||
|
|
||||||
const toggleSelectionMode = useCallback(() => {
|
|
||||||
setSelectionMode(prev => {
|
|
||||||
const newMode = !prev;
|
|
||||||
if (!newMode) {
|
|
||||||
setSelectedFiles([]);
|
|
||||||
}
|
|
||||||
return newMode;
|
|
||||||
});
|
|
||||||
}, [setSelectedFiles]);
|
|
||||||
|
|
||||||
// File reordering handler for drag and drop
|
// File reordering handler for drag and drop
|
||||||
const handleReorderFiles = useCallback((sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => {
|
const handleReorderFiles = useCallback((sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => {
|
||||||
const currentIds = activeFileRecords.map(r => r.id);
|
const currentIds = activeStirlingFileStubs.map(r => r.id);
|
||||||
|
|
||||||
// Find indices
|
// Find indices
|
||||||
const sourceIndex = currentIds.findIndex(id => id === sourceFileId);
|
const sourceIndex = currentIds.findIndex(id => id === sourceFileId);
|
||||||
@ -368,71 +286,34 @@ const FileEditor = ({
|
|||||||
// Update status
|
// Update status
|
||||||
const moveCount = filesToMove.length;
|
const moveCount = filesToMove.length;
|
||||||
setStatus(`${moveCount > 1 ? `${moveCount} files` : 'File'} reordered`);
|
setStatus(`${moveCount > 1 ? `${moveCount} files` : 'File'} reordered`);
|
||||||
}, [activeFileRecords, reorderFiles, setStatus]);
|
}, [activeStirlingFileStubs, reorderFiles, setStatus]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// File operations using context
|
// File operations using context
|
||||||
const handleDeleteFile = useCallback((fileId: FileId) => {
|
const handleDeleteFile = useCallback((fileId: FileId) => {
|
||||||
const record = activeFileRecords.find(r => r.id === fileId);
|
const record = activeStirlingFileStubs.find(r => r.id === fileId);
|
||||||
const file = record ? selectors.getFile(record.id) : null;
|
const file = record ? selectors.getFile(record.id) : null;
|
||||||
|
|
||||||
if (record && file) {
|
if (record && file) {
|
||||||
// Record close operation
|
|
||||||
const fileName = file.name;
|
|
||||||
const contextFileId = record.id;
|
|
||||||
const operationId = `close-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
||||||
const operation: FileOperation = {
|
|
||||||
id: operationId,
|
|
||||||
type: 'remove',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
fileIds: [fileName as FileId /* FIX ME: This doesn't seem right */],
|
|
||||||
status: 'pending',
|
|
||||||
metadata: {
|
|
||||||
originalFileName: fileName,
|
|
||||||
fileSize: record.size,
|
|
||||||
parameters: {
|
|
||||||
action: 'close',
|
|
||||||
reason: 'user_request'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Remove file from context but keep in storage (close, don't delete)
|
// Remove file from context but keep in storage (close, don't delete)
|
||||||
|
const contextFileId = record.id;
|
||||||
removeFiles([contextFileId], false);
|
removeFiles([contextFileId], false);
|
||||||
|
|
||||||
// Remove from context selections
|
// Remove from context selections
|
||||||
const currentSelected = selectedFileIds.filter(id => id !== contextFileId);
|
const currentSelected = selectedFileIds.filter(id => id !== contextFileId);
|
||||||
setSelectedFiles(currentSelected);
|
setSelectedFiles(currentSelected);
|
||||||
}
|
}
|
||||||
}, [activeFileRecords, selectors, removeFiles, setSelectedFiles, selectedFileIds]);
|
}, [activeStirlingFileStubs, selectors, removeFiles, setSelectedFiles, selectedFileIds]);
|
||||||
|
|
||||||
const handleViewFile = useCallback((fileId: FileId) => {
|
const handleViewFile = useCallback((fileId: FileId) => {
|
||||||
const record = activeFileRecords.find(r => r.id === fileId);
|
const record = activeStirlingFileStubs.find(r => r.id === fileId);
|
||||||
if (record) {
|
if (record) {
|
||||||
// Set the file as selected in context and switch to viewer for preview
|
// Set the file as selected in context and switch to viewer for preview
|
||||||
setSelectedFiles([fileId]);
|
setSelectedFiles([fileId]);
|
||||||
navActions.setWorkbench('viewer');
|
navActions.setWorkbench('viewer');
|
||||||
}
|
}
|
||||||
}, [activeFileRecords, setSelectedFiles, navActions.setWorkbench]);
|
}, [activeStirlingFileStubs, setSelectedFiles, navActions.setWorkbench]);
|
||||||
|
|
||||||
const handleMergeFromHere = useCallback((fileId: FileId) => {
|
|
||||||
const startIndex = activeFileRecords.findIndex(r => r.id === fileId);
|
|
||||||
if (startIndex === -1) return;
|
|
||||||
|
|
||||||
const recordsToMerge = activeFileRecords.slice(startIndex);
|
|
||||||
const filesToMerge = recordsToMerge.map(r => selectors.getFile(r.id)).filter(Boolean) as File[];
|
|
||||||
if (onMergeFiles) {
|
|
||||||
onMergeFiles(filesToMerge);
|
|
||||||
}
|
|
||||||
}, [activeFileRecords, selectors, onMergeFiles]);
|
|
||||||
|
|
||||||
const handleSplitFile = useCallback((fileId: FileId) => {
|
|
||||||
const file = selectors.getFile(fileId);
|
|
||||||
if (file && onOpenPageEditor) {
|
|
||||||
onOpenPageEditor(file);
|
|
||||||
}
|
|
||||||
}, [selectors, onOpenPageEditor]);
|
|
||||||
|
|
||||||
const handleLoadFromStorage = useCallback(async (selectedFiles: File[]) => {
|
const handleLoadFromStorage = useCallback(async (selectedFiles: File[]) => {
|
||||||
if (selectedFiles.length === 0) return;
|
if (selectedFiles.length === 0) return;
|
||||||
@ -467,7 +348,7 @@ const FileEditor = ({
|
|||||||
<Box p="md" pt="xl">
|
<Box p="md" pt="xl">
|
||||||
|
|
||||||
|
|
||||||
{activeFileRecords.length === 0 && !zipExtractionProgress.isExtracting ? (
|
{activeStirlingFileStubs.length === 0 && !zipExtractionProgress.isExtracting ? (
|
||||||
<Center h="60vh">
|
<Center h="60vh">
|
||||||
<Stack align="center" gap="md">
|
<Stack align="center" gap="md">
|
||||||
<Text size="lg" c="dimmed">📁</Text>
|
<Text size="lg" c="dimmed">📁</Text>
|
||||||
@ -475,7 +356,7 @@ const FileEditor = ({
|
|||||||
<Text size="sm" c="dimmed">Upload PDF files, ZIP archives, or load from storage to get started</Text>
|
<Text size="sm" c="dimmed">Upload PDF files, ZIP archives, or load from storage to get started</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Center>
|
</Center>
|
||||||
) : activeFileRecords.length === 0 && zipExtractionProgress.isExtracting ? (
|
) : activeStirlingFileStubs.length === 0 && zipExtractionProgress.isExtracting ? (
|
||||||
<Box>
|
<Box>
|
||||||
<SkeletonLoader type="controls" />
|
<SkeletonLoader type="controls" />
|
||||||
|
|
||||||
@ -522,7 +403,7 @@ const FileEditor = ({
|
|||||||
pointerEvents: 'auto'
|
pointerEvents: 'auto'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{activeFileRecords.map((record, index) => {
|
{activeStirlingFileStubs.map((record, index) => {
|
||||||
const fileItem = recordToFileItem(record);
|
const fileItem = recordToFileItem(record);
|
||||||
if (!fileItem) return null;
|
if (!fileItem) return null;
|
||||||
|
|
||||||
@ -531,7 +412,7 @@ const FileEditor = ({
|
|||||||
key={record.id}
|
key={record.id}
|
||||||
file={fileItem}
|
file={fileItem}
|
||||||
index={index}
|
index={index}
|
||||||
totalFiles={activeFileRecords.length}
|
totalFiles={activeStirlingFileStubs.length}
|
||||||
selectedFiles={localSelectedIds}
|
selectedFiles={localSelectedIds}
|
||||||
selectionMode={selectionMode}
|
selectionMode={selectionMode}
|
||||||
onToggleFile={toggleFile}
|
onToggleFile={toggleFile}
|
||||||
|
@ -44,7 +44,6 @@ const FileEditorThumbnail = ({
|
|||||||
selectedFiles,
|
selectedFiles,
|
||||||
onToggleFile,
|
onToggleFile,
|
||||||
onDeleteFile,
|
onDeleteFile,
|
||||||
onViewFile,
|
|
||||||
onSetStatus,
|
onSetStatus,
|
||||||
onReorderFiles,
|
onReorderFiles,
|
||||||
onDownloadFile,
|
onDownloadFile,
|
||||||
@ -61,8 +60,8 @@ const FileEditorThumbnail = ({
|
|||||||
|
|
||||||
// Resolve the actual File object for pin/unpin operations
|
// Resolve the actual File object for pin/unpin operations
|
||||||
const actualFile = useMemo(() => {
|
const actualFile = useMemo(() => {
|
||||||
return activeFiles.find((f: File) => f.name === file.name && f.size === file.size);
|
return activeFiles.find(f => f.fileId === file.id);
|
||||||
}, [activeFiles, file.name, file.size]);
|
}, [activeFiles, file.id]);
|
||||||
const isPinned = actualFile ? isFilePinned(actualFile) : false;
|
const isPinned = actualFile ? isFilePinned(actualFile) : false;
|
||||||
|
|
||||||
const downloadSelectedFile = useCallback(() => {
|
const downloadSelectedFile = useCallback(() => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Stack, Button, Box } from '@mantine/core';
|
import { Stack, Button, Box } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useIndexedDBThumbnail } from '../../hooks/useIndexedDBThumbnail';
|
import { useIndexedDBThumbnail } from '../../hooks/useIndexedDBThumbnail';
|
||||||
@ -22,7 +22,6 @@ const FileDetails: React.FC<FileDetailsProps> = ({
|
|||||||
// Get the currently displayed file
|
// Get the currently displayed file
|
||||||
const currentFile = selectedFiles.length > 0 ? selectedFiles[currentFileIndex] : null;
|
const currentFile = selectedFiles.length > 0 ? selectedFiles[currentFileIndex] : null;
|
||||||
const hasSelection = selectedFiles.length > 0;
|
const hasSelection = selectedFiles.length > 0;
|
||||||
const hasMultipleFiles = selectedFiles.length > 1;
|
|
||||||
|
|
||||||
// Use IndexedDB hook for the current file
|
// Use IndexedDB hook for the current file
|
||||||
const { thumbnail: currentThumbnail } = useIndexedDBThumbnail(currentFile);
|
const { thumbnail: currentThumbnail } = useIndexedDBThumbnail(currentFile);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Stack, Box } from '@mantine/core';
|
import { Box } from '@mantine/core';
|
||||||
import FileSourceButtons from './FileSourceButtons';
|
import FileSourceButtons from './FileSourceButtons';
|
||||||
import FileDetails from './FileDetails';
|
import FileDetails from './FileDetails';
|
||||||
import SearchInput from './SearchInput';
|
import SearchInput from './SearchInput';
|
||||||
|
@ -1,181 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import {
|
|
||||||
Stack,
|
|
||||||
Paper,
|
|
||||||
Text,
|
|
||||||
Badge,
|
|
||||||
Group,
|
|
||||||
Collapse,
|
|
||||||
Box,
|
|
||||||
ScrollArea,
|
|
||||||
Code,
|
|
||||||
Divider
|
|
||||||
} from '@mantine/core';
|
|
||||||
// FileContext no longer needed - these were stub functions anyway
|
|
||||||
import { FileOperation, FileOperationHistory as FileOperationHistoryType } from '../../types/fileContext';
|
|
||||||
import { PageOperation } from '../../types/pageEditor';
|
|
||||||
import { FileId } from '../../types/file';
|
|
||||||
|
|
||||||
interface FileOperationHistoryProps {
|
|
||||||
fileId: FileId;
|
|
||||||
showOnlyApplied?: boolean;
|
|
||||||
maxHeight?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FileOperationHistory: React.FC<FileOperationHistoryProps> = ({
|
|
||||||
fileId,
|
|
||||||
showOnlyApplied = false,
|
|
||||||
maxHeight = 400
|
|
||||||
}) => {
|
|
||||||
// These were stub functions in the old context - replace with empty stubs
|
|
||||||
const getFileHistory = (fileId: FileId) => ({ operations: [], createdAt: Date.now(), lastModified: Date.now() });
|
|
||||||
const getAppliedOperations = (fileId: FileId) => [];
|
|
||||||
|
|
||||||
const history = getFileHistory(fileId);
|
|
||||||
const allOperations = showOnlyApplied ? getAppliedOperations(fileId) : history?.operations || [];
|
|
||||||
const operations = allOperations.filter((op: any) => 'fileIds' in op) as FileOperation[];
|
|
||||||
|
|
||||||
const formatTimestamp = (timestamp: number) => {
|
|
||||||
return new Date(timestamp).toLocaleString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getOperationIcon = (type: string) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'split': return '✂️';
|
|
||||||
case 'merge': return '🔗';
|
|
||||||
case 'compress': return '🗜️';
|
|
||||||
case 'rotate': return '🔄';
|
|
||||||
case 'delete': return '🗑️';
|
|
||||||
case 'move': return '↕️';
|
|
||||||
case 'insert': return '📄';
|
|
||||||
case 'upload': return '⬆️';
|
|
||||||
case 'add': return '➕';
|
|
||||||
case 'remove': return '➖';
|
|
||||||
case 'replace': return '🔄';
|
|
||||||
case 'convert': return '🔄';
|
|
||||||
default: return '⚙️';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'applied': return 'green';
|
|
||||||
case 'failed': return 'red';
|
|
||||||
case 'pending': return 'yellow';
|
|
||||||
default: return 'gray';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderOperationDetails = (operation: FileOperation) => {
|
|
||||||
if ('metadata' in operation && operation.metadata) {
|
|
||||||
const { metadata } = operation;
|
|
||||||
return (
|
|
||||||
<Box mt="xs">
|
|
||||||
{metadata.parameters && (
|
|
||||||
<Text size="xs" c="dimmed">
|
|
||||||
Parameters: <Code>{JSON.stringify(metadata.parameters, null, 2)}</Code>
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{metadata.originalFileName && (
|
|
||||||
<Text size="xs" c="dimmed">
|
|
||||||
Original file: {metadata.originalFileName}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{metadata.outputFileNames && (
|
|
||||||
<Text size="xs" c="dimmed">
|
|
||||||
Output files: {metadata.outputFileNames.join(', ')}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{metadata.fileSize && (
|
|
||||||
<Text size="xs" c="dimmed">
|
|
||||||
File size: {(metadata.fileSize / 1024 / 1024).toFixed(2)} MB
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{metadata.pageCount && (
|
|
||||||
<Text size="xs" c="dimmed">
|
|
||||||
Pages: {metadata.pageCount}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{metadata.error && (
|
|
||||||
<Text size="xs" c="red">
|
|
||||||
Error: {metadata.error}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!history || operations.length === 0) {
|
|
||||||
return (
|
|
||||||
<Paper p="md" withBorder>
|
|
||||||
<Text c="dimmed" ta="center">
|
|
||||||
{showOnlyApplied ? 'No applied operations found' : 'No operation history available'}
|
|
||||||
</Text>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper p="md" withBorder>
|
|
||||||
<Group justify="space-between" mb="md">
|
|
||||||
<Text fw={500}>
|
|
||||||
{showOnlyApplied ? 'Applied Operations' : 'Operation History'}
|
|
||||||
</Text>
|
|
||||||
<Badge variant="light" color="blue">
|
|
||||||
{operations.length} operations
|
|
||||||
</Badge>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<ScrollArea h={maxHeight}>
|
|
||||||
<Stack gap="sm">
|
|
||||||
{operations.map((operation, index) => (
|
|
||||||
<Paper key={operation.id} p="sm" withBorder radius="sm" bg="gray.0">
|
|
||||||
<Group justify="space-between" align="start">
|
|
||||||
<Group gap="xs">
|
|
||||||
<Text span size="lg">
|
|
||||||
{getOperationIcon(operation.type)}
|
|
||||||
</Text>
|
|
||||||
<Box>
|
|
||||||
<Text fw={500} size="sm">
|
|
||||||
{operation.type.charAt(0).toUpperCase() + operation.type.slice(1)}
|
|
||||||
</Text>
|
|
||||||
<Text size="xs" c="dimmed">
|
|
||||||
{formatTimestamp(operation.timestamp)}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Badge
|
|
||||||
variant="filled"
|
|
||||||
color={getStatusColor(operation.status)}
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
{operation.status}
|
|
||||||
</Badge>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
{renderOperationDetails(operation)}
|
|
||||||
|
|
||||||
{index < operations.length - 1 && <Divider mt="sm" />}
|
|
||||||
</Paper>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</ScrollArea>
|
|
||||||
|
|
||||||
{history && (
|
|
||||||
<Group justify="space-between" mt="sm" pt="sm" style={{ borderTop: '1px solid var(--mantine-color-gray-3)' }}>
|
|
||||||
<Text size="xs" c="dimmed">
|
|
||||||
Created: {formatTimestamp(history.createdAt)}
|
|
||||||
</Text>
|
|
||||||
<Text size="xs" c="dimmed">
|
|
||||||
Last modified: {formatTimestamp(history.lastModified)}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
)}
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FileOperationHistory;
|
|
@ -1,6 +1,4 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Box } from '@mantine/core';
|
import { Box } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
|
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
|
||||||
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
|
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
|
||||||
import { useFileHandler } from '../../hooks/useFileHandler';
|
import { useFileHandler } from '../../hooks/useFileHandler';
|
||||||
@ -19,7 +17,6 @@ import Footer from '../shared/Footer';
|
|||||||
|
|
||||||
// No props needed - component uses contexts directly
|
// No props needed - component uses contexts directly
|
||||||
export default function Workbench() {
|
export default function Workbench() {
|
||||||
const { t } = useTranslation();
|
|
||||||
const { isRainbowMode } = useRainbowThemeContext();
|
const { isRainbowMode } = useRainbowThemeContext();
|
||||||
|
|
||||||
// Use context-based hooks to eliminate all prop drilling
|
// Use context-based hooks to eliminate all prop drilling
|
||||||
@ -78,11 +75,9 @@ export default function Workbench() {
|
|||||||
return (
|
return (
|
||||||
<FileEditor
|
<FileEditor
|
||||||
toolMode={!!selectedToolId}
|
toolMode={!!selectedToolId}
|
||||||
showUpload={true}
|
|
||||||
showBulkActions={!selectedToolId}
|
|
||||||
supportedExtensions={selectedTool?.supportedFormats || ["pdf"]}
|
supportedExtensions={selectedTool?.supportedFormats || ["pdf"]}
|
||||||
{...(!selectedToolId && {
|
{...(!selectedToolId && {
|
||||||
onOpenPageEditor: (file) => {
|
onOpenPageEditor: () => {
|
||||||
setCurrentView("pageEditor");
|
setCurrentView("pageEditor");
|
||||||
},
|
},
|
||||||
onMergeFiles: (filesToMerge) => {
|
onMergeFiles: (filesToMerge) => {
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
||||||
import { Box } from '@mantine/core';
|
import { Box } from '@mantine/core';
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
||||||
import styles from './PageEditor.module.css';
|
|
||||||
import { GRID_CONSTANTS } from './constants';
|
import { GRID_CONSTANTS } from './constants';
|
||||||
|
|
||||||
interface DragDropItem {
|
interface DragDropItem {
|
||||||
@ -22,12 +20,7 @@ interface DragDropGridProps<T extends DragDropItem> {
|
|||||||
|
|
||||||
const DragDropGrid = <T extends DragDropItem>({
|
const DragDropGrid = <T extends DragDropItem>({
|
||||||
items,
|
items,
|
||||||
selectedItems,
|
|
||||||
selectionMode,
|
|
||||||
isAnimating,
|
|
||||||
onReorderPages,
|
|
||||||
renderItem,
|
renderItem,
|
||||||
renderSplitMarker,
|
|
||||||
}: DragDropGridProps<T>) => {
|
}: DragDropGridProps<T>) => {
|
||||||
const itemRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
const itemRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@ -92,8 +85,6 @@ const DragDropGrid = <T extends DragDropItem>({
|
|||||||
overscan: OVERSCAN,
|
overscan: OVERSCAN,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Calculate optimal width for centering
|
// Calculate optimal width for centering
|
||||||
const remToPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
const remToPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
const itemWidth = parseFloat(GRID_CONSTANTS.ITEM_WIDTH) * remToPx;
|
const itemWidth = parseFloat(GRID_CONSTANTS.ITEM_WIDTH) * remToPx;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useCallback, useRef, useMemo, useEffect } from 'react';
|
import React, { useState, useCallback, useRef, useMemo, useEffect } from 'react';
|
||||||
import { Text, ActionIcon, CheckboxIndicator } from '@mantine/core';
|
import { ActionIcon, CheckboxIndicator } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||||
import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined';
|
import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined';
|
||||||
@ -44,7 +44,6 @@ const FileThumbnail = ({
|
|||||||
selectedFiles,
|
selectedFiles,
|
||||||
onToggleFile,
|
onToggleFile,
|
||||||
onDeleteFile,
|
onDeleteFile,
|
||||||
onViewFile,
|
|
||||||
onSetStatus,
|
onSetStatus,
|
||||||
onReorderFiles,
|
onReorderFiles,
|
||||||
onDownloadFile,
|
onDownloadFile,
|
||||||
@ -61,8 +60,8 @@ const FileThumbnail = ({
|
|||||||
|
|
||||||
// Resolve the actual File object for pin/unpin operations
|
// Resolve the actual File object for pin/unpin operations
|
||||||
const actualFile = useMemo(() => {
|
const actualFile = useMemo(() => {
|
||||||
return activeFiles.find((f: File) => f.name === file.name && f.size === file.size);
|
return activeFiles.find(f => f.fileId === file.id);
|
||||||
}, [activeFiles, file.name, file.size]);
|
}, [activeFiles, file.id]);
|
||||||
const isPinned = actualFile ? isFilePinned(actualFile) : false;
|
const isPinned = actualFile ? isFilePinned(actualFile) : false;
|
||||||
|
|
||||||
const downloadSelectedFile = useCallback(() => {
|
const downloadSelectedFile = useCallback(() => {
|
||||||
@ -93,40 +92,6 @@ const FileThumbnail = ({
|
|||||||
// ---- Selection ----
|
// ---- Selection ----
|
||||||
const isSelected = selectedFiles.includes(file.id);
|
const isSelected = selectedFiles.includes(file.id);
|
||||||
|
|
||||||
// ---- Meta formatting ----
|
|
||||||
const prettySize = useMemo(() => {
|
|
||||||
const bytes = file.size ?? 0;
|
|
||||||
if (bytes === 0) return '0 B';
|
|
||||||
const k = 1024;
|
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
|
|
||||||
}, [file.size]);
|
|
||||||
|
|
||||||
const extUpper = useMemo(() => {
|
|
||||||
const m = /\.([a-z0-9]+)$/i.exec(file.name ?? '');
|
|
||||||
return (m?.[1] || '').toUpperCase();
|
|
||||||
}, [file.name]);
|
|
||||||
|
|
||||||
const pageLabel = useMemo(
|
|
||||||
() =>
|
|
||||||
file.pageCount > 0
|
|
||||||
? `${file.pageCount} ${file.pageCount === 1 ? 'Page' : 'Pages'}`
|
|
||||||
: '',
|
|
||||||
[file.pageCount]
|
|
||||||
);
|
|
||||||
|
|
||||||
const dateLabel = useMemo(() => {
|
|
||||||
const d =
|
|
||||||
file.modifiedAt != null ? new Date(file.modifiedAt) : new Date(); // fallback
|
|
||||||
if (Number.isNaN(d.getTime())) return '';
|
|
||||||
return new Intl.DateTimeFormat(undefined, {
|
|
||||||
month: 'short',
|
|
||||||
day: '2-digit',
|
|
||||||
year: 'numeric',
|
|
||||||
}).format(d);
|
|
||||||
}, [file.modifiedAt]);
|
|
||||||
|
|
||||||
// ---- Drag & drop wiring ----
|
// ---- Drag & drop wiring ----
|
||||||
const fileElementRef = useCallback((element: HTMLDivElement | null) => {
|
const fileElementRef = useCallback((element: HTMLDivElement | null) => {
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
|
@ -1,13 +1,7 @@
|
|||||||
import React, { useState, useCallback, useRef, useEffect, useMemo } from "react";
|
import { useState, useCallback, useRef, useEffect } from "react";
|
||||||
import {
|
import { Text, Center, Box, LoadingOverlay, Stack } from "@mantine/core";
|
||||||
Button, Text, Center, Box,
|
import { useFileState, useFileActions } from "../../contexts/FileContext";
|
||||||
Notification, TextInput, LoadingOverlay, Modal, Alert,
|
import { PDFDocument, PageEditorFunctions } from "../../types/pageEditor";
|
||||||
Stack, Group, Portal
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useFileState, useFileActions, useCurrentFile, useFileSelection } from "../../contexts/FileContext";
|
|
||||||
import { PDFDocument, PDFPage, PageEditorFunctions } from "../../types/pageEditor";
|
|
||||||
import { ProcessedFile as EnhancedProcessedFile } from "../../types/processing";
|
|
||||||
import { pdfExportService } from "../../services/pdfExportService";
|
import { pdfExportService } from "../../services/pdfExportService";
|
||||||
import { documentManipulationService } from "../../services/documentManipulationService";
|
import { documentManipulationService } from "../../services/documentManipulationService";
|
||||||
// Thumbnail generation is now handled by individual PageThumbnail components
|
// Thumbnail generation is now handled by individual PageThumbnail components
|
||||||
@ -19,16 +13,11 @@ import NavigationWarningModal from '../shared/NavigationWarningModal';
|
|||||||
import { FileId } from "../../types/file";
|
import { FileId } from "../../types/file";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DOMCommand,
|
|
||||||
RotatePageCommand,
|
|
||||||
DeletePagesCommand,
|
DeletePagesCommand,
|
||||||
ReorderPagesCommand,
|
ReorderPagesCommand,
|
||||||
SplitCommand,
|
SplitCommand,
|
||||||
BulkRotateCommand,
|
BulkRotateCommand,
|
||||||
BulkSplitCommand,
|
|
||||||
SplitAllCommand,
|
|
||||||
PageBreakCommand,
|
PageBreakCommand,
|
||||||
BulkPageBreakCommand,
|
|
||||||
UndoManager
|
UndoManager
|
||||||
} from './commands/pageCommands';
|
} from './commands/pageCommands';
|
||||||
import { GRID_CONSTANTS } from './constants';
|
import { GRID_CONSTANTS } from './constants';
|
||||||
@ -49,35 +38,24 @@ const PageEditor = ({
|
|||||||
|
|
||||||
// Prefer IDs + selectors to avoid array identity churn
|
// Prefer IDs + selectors to avoid array identity churn
|
||||||
const activeFileIds = state.files.ids;
|
const activeFileIds = state.files.ids;
|
||||||
const primaryFileId = activeFileIds[0] ?? null;
|
|
||||||
const selectedFiles = selectors.getSelectedFiles();
|
|
||||||
|
|
||||||
// Stable signature for effects (prevents loops)
|
|
||||||
const filesSignature = selectors.getFilesSignature();
|
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
const globalProcessing = state.ui.isProcessing;
|
const globalProcessing = state.ui.isProcessing;
|
||||||
const processingProgress = state.ui.processingProgress;
|
|
||||||
const hasUnsavedChanges = state.ui.hasUnsavedChanges;
|
|
||||||
|
|
||||||
// Edit state management
|
// Edit state management
|
||||||
const [editedDocument, setEditedDocument] = useState<PDFDocument | null>(null);
|
const [editedDocument, setEditedDocument] = useState<PDFDocument | null>(null);
|
||||||
const [hasUnsavedDraft, setHasUnsavedDraft] = useState(false);
|
|
||||||
const [showResumeModal, setShowResumeModal] = useState(false);
|
|
||||||
const [foundDraft, setFoundDraft] = useState<any>(null);
|
|
||||||
const autoSaveTimer = useRef<number | null>(null);
|
|
||||||
|
|
||||||
// DOM-first undo manager (replaces the old React state undo system)
|
// DOM-first undo manager (replaces the old React state undo system)
|
||||||
const undoManagerRef = useRef(new UndoManager());
|
const undoManagerRef = useRef(new UndoManager());
|
||||||
|
|
||||||
// Document state management
|
// Document state management
|
||||||
const { document: mergedPdfDocument, isVeryLargeDocument, isLoading: documentLoading } = usePageDocument();
|
const { document: mergedPdfDocument } = usePageDocument();
|
||||||
|
|
||||||
|
|
||||||
// UI state management
|
// UI state management
|
||||||
const {
|
const {
|
||||||
selectionMode, selectedPageIds, movingPage, isAnimating, splitPositions, exportLoading,
|
selectionMode, selectedPageIds, movingPage, isAnimating, splitPositions, exportLoading,
|
||||||
setSelectionMode, setSelectedPageIds, setMovingPage, setIsAnimating, setSplitPositions, setExportLoading,
|
setSelectionMode, setSelectedPageIds, setMovingPage, setSplitPositions, setExportLoading,
|
||||||
togglePage, toggleSelectAll, animateReorder
|
togglePage, toggleSelectAll, animateReorder
|
||||||
} = usePageEditorState();
|
} = usePageEditorState();
|
||||||
|
|
||||||
@ -146,12 +124,6 @@ const PageEditor = ({
|
|||||||
}).filter(id => id !== '');
|
}).filter(id => id !== '');
|
||||||
}, [displayDocument]);
|
}, [displayDocument]);
|
||||||
|
|
||||||
// Convert selectedPageIds to numbers for components that still need numbers
|
|
||||||
const selectedPageNumbers = useMemo(() =>
|
|
||||||
getPageNumbersFromIds(selectedPageIds),
|
|
||||||
[selectedPageIds, getPageNumbersFromIds]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Select all pages by default when document initially loads
|
// Select all pages by default when document initially loads
|
||||||
const hasInitializedSelection = useRef(false);
|
const hasInitializedSelection = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
@ -9,9 +8,7 @@ import ContentCutIcon from "@mui/icons-material/ContentCut";
|
|||||||
import RotateLeftIcon from "@mui/icons-material/RotateLeft";
|
import RotateLeftIcon from "@mui/icons-material/RotateLeft";
|
||||||
import RotateRightIcon from "@mui/icons-material/RotateRight";
|
import RotateRightIcon from "@mui/icons-material/RotateRight";
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
|
||||||
import InsertPageBreakIcon from "@mui/icons-material/InsertPageBreak";
|
import InsertPageBreakIcon from "@mui/icons-material/InsertPageBreak";
|
||||||
import DownloadIcon from "@mui/icons-material/Download";
|
|
||||||
|
|
||||||
interface PageEditorControlsProps {
|
interface PageEditorControlsProps {
|
||||||
// Close/Reset functions
|
// Close/Reset functions
|
||||||
@ -46,7 +43,6 @@ interface PageEditorControlsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PageEditorControls = ({
|
const PageEditorControls = ({
|
||||||
onClosePdf,
|
|
||||||
onUndo,
|
onUndo,
|
||||||
onRedo,
|
onRedo,
|
||||||
canUndo,
|
canUndo,
|
||||||
@ -54,12 +50,7 @@ const PageEditorControls = ({
|
|||||||
onRotate,
|
onRotate,
|
||||||
onDelete,
|
onDelete,
|
||||||
onSplit,
|
onSplit,
|
||||||
onSplitAll,
|
|
||||||
onPageBreak,
|
onPageBreak,
|
||||||
onPageBreakAll,
|
|
||||||
onExportAll,
|
|
||||||
exportLoading,
|
|
||||||
selectionMode,
|
|
||||||
selectedPageIds,
|
selectedPageIds,
|
||||||
displayDocument,
|
displayDocument,
|
||||||
splitPositions,
|
splitPositions,
|
||||||
|
@ -52,16 +52,13 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
pageRefs,
|
pageRefs,
|
||||||
onReorderPages,
|
onReorderPages,
|
||||||
onTogglePage,
|
onTogglePage,
|
||||||
onAnimateReorder,
|
|
||||||
onExecuteCommand,
|
onExecuteCommand,
|
||||||
onSetStatus,
|
onSetStatus,
|
||||||
onSetMovingPage,
|
onSetMovingPage,
|
||||||
onDeletePage,
|
onDeletePage,
|
||||||
createRotateCommand,
|
createRotateCommand,
|
||||||
createDeleteCommand,
|
|
||||||
createSplitCommand,
|
createSplitCommand,
|
||||||
pdfDocument,
|
pdfDocument,
|
||||||
setPdfDocument,
|
|
||||||
splitPositions,
|
splitPositions,
|
||||||
onInsertFiles,
|
onInsertFiles,
|
||||||
}: PageThumbnailProps) => {
|
}: PageThumbnailProps) => {
|
||||||
@ -172,7 +169,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
type: 'page',
|
type: 'page',
|
||||||
pageNumber: page.pageNumber
|
pageNumber: page.pageNumber
|
||||||
}),
|
}),
|
||||||
onDrop: ({ source }) => {}
|
onDrop: (_) => {}
|
||||||
});
|
});
|
||||||
|
|
||||||
(element as any).__dragCleanup = () => {
|
(element as any).__dragCleanup = () => {
|
||||||
|
@ -27,9 +27,9 @@ export function usePageDocument(): PageDocumentHook {
|
|||||||
const globalProcessing = state.ui.isProcessing;
|
const globalProcessing = state.ui.isProcessing;
|
||||||
|
|
||||||
// Get primary file record outside useMemo to track processedFile changes
|
// Get primary file record outside useMemo to track processedFile changes
|
||||||
const primaryFileRecord = primaryFileId ? selectors.getFileRecord(primaryFileId) : null;
|
const primaryStirlingFileStub = primaryFileId ? selectors.getStirlingFileStub(primaryFileId) : null;
|
||||||
const processedFilePages = primaryFileRecord?.processedFile?.pages;
|
const processedFilePages = primaryStirlingFileStub?.processedFile?.pages;
|
||||||
const processedFileTotalPages = primaryFileRecord?.processedFile?.totalPages;
|
const processedFileTotalPages = primaryStirlingFileStub?.processedFile?.totalPages;
|
||||||
|
|
||||||
// Compute merged document with stable signature (prevents infinite loops)
|
// Compute merged document with stable signature (prevents infinite loops)
|
||||||
const mergedPdfDocument = useMemo((): PDFDocument | null => {
|
const mergedPdfDocument = useMemo((): PDFDocument | null => {
|
||||||
@ -38,16 +38,16 @@ export function usePageDocument(): PageDocumentHook {
|
|||||||
const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null;
|
const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null;
|
||||||
|
|
||||||
// If we have file IDs but no file record, something is wrong - return null to show loading
|
// If we have file IDs but no file record, something is wrong - return null to show loading
|
||||||
if (!primaryFileRecord) {
|
if (!primaryStirlingFileStub) {
|
||||||
console.log('🎬 PageEditor: No primary file record found, showing loading');
|
console.log('🎬 PageEditor: No primary file record found, showing loading');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name =
|
const name =
|
||||||
activeFileIds.length === 1
|
activeFileIds.length === 1
|
||||||
? (primaryFileRecord.name ?? 'document.pdf')
|
? (primaryStirlingFileStub.name ?? 'document.pdf')
|
||||||
: activeFileIds
|
: activeFileIds
|
||||||
.map(id => (selectors.getFileRecord(id)?.name ?? 'file').replace(/\.pdf$/i, ''))
|
.map(id => (selectors.getStirlingFileStub(id)?.name ?? 'file').replace(/\.pdf$/i, ''))
|
||||||
.join(' + ');
|
.join(' + ');
|
||||||
|
|
||||||
// Build page insertion map from files with insertion positions
|
// Build page insertion map from files with insertion positions
|
||||||
@ -55,7 +55,7 @@ export function usePageDocument(): PageDocumentHook {
|
|||||||
const originalFileIds: FileId[] = [];
|
const originalFileIds: FileId[] = [];
|
||||||
|
|
||||||
activeFileIds.forEach(fileId => {
|
activeFileIds.forEach(fileId => {
|
||||||
const record = selectors.getFileRecord(fileId);
|
const record = selectors.getStirlingFileStub(fileId);
|
||||||
if (record?.insertAfterPageId !== undefined) {
|
if (record?.insertAfterPageId !== undefined) {
|
||||||
if (!insertionMap.has(record.insertAfterPageId)) {
|
if (!insertionMap.has(record.insertAfterPageId)) {
|
||||||
insertionMap.set(record.insertAfterPageId, []);
|
insertionMap.set(record.insertAfterPageId, []);
|
||||||
@ -68,16 +68,15 @@ export function usePageDocument(): PageDocumentHook {
|
|||||||
|
|
||||||
// Build pages by interleaving original pages with insertions
|
// Build pages by interleaving original pages with insertions
|
||||||
let pages: PDFPage[] = [];
|
let pages: PDFPage[] = [];
|
||||||
let totalPageCount = 0;
|
|
||||||
|
|
||||||
// Helper function to create pages from a file
|
// Helper function to create pages from a file
|
||||||
const createPagesFromFile = (fileId: FileId, startPageNumber: number): PDFPage[] => {
|
const createPagesFromFile = (fileId: FileId, startPageNumber: number): PDFPage[] => {
|
||||||
const fileRecord = selectors.getFileRecord(fileId);
|
const stirlingFileStub = selectors.getStirlingFileStub(fileId);
|
||||||
if (!fileRecord) {
|
if (!stirlingFileStub) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const processedFile = fileRecord.processedFile;
|
const processedFile = stirlingFileStub.processedFile;
|
||||||
let filePages: PDFPage[] = [];
|
let filePages: PDFPage[] = [];
|
||||||
|
|
||||||
if (processedFile?.pages && processedFile.pages.length > 0) {
|
if (processedFile?.pages && processedFile.pages.length > 0) {
|
||||||
@ -144,8 +143,6 @@ export function usePageDocument(): PageDocumentHook {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
totalPageCount = pages.length;
|
|
||||||
|
|
||||||
if (pages.length === 0) {
|
if (pages.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -159,7 +156,7 @@ export function usePageDocument(): PageDocumentHook {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return mergedDoc;
|
return mergedDoc;
|
||||||
}, [activeFileIds, primaryFileId, primaryFileRecord, processedFilePages, processedFileTotalPages, selectors, filesSignature]);
|
}, [activeFileIds, primaryFileId, primaryStirlingFileStub, processedFilePages, processedFileTotalPages, selectors, filesSignature]);
|
||||||
|
|
||||||
// Large document detection for smart loading
|
// Large document detection for smart loading
|
||||||
const isVeryLargeDocument = useMemo(() => {
|
const isVeryLargeDocument = useMemo(() => {
|
||||||
|
@ -6,13 +6,13 @@ import StorageIcon from "@mui/icons-material/Storage";
|
|||||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||||
import EditIcon from "@mui/icons-material/Edit";
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
|
|
||||||
import { FileRecord } from "../../types/fileContext";
|
import { StirlingFileStub } from "../../types/fileContext";
|
||||||
import { getFileSize, getFileDate } from "../../utils/fileUtils";
|
import { getFileSize, getFileDate } from "../../utils/fileUtils";
|
||||||
import { useIndexedDBThumbnail } from "../../hooks/useIndexedDBThumbnail";
|
import { useIndexedDBThumbnail } from "../../hooks/useIndexedDBThumbnail";
|
||||||
|
|
||||||
interface FileCardProps {
|
interface FileCardProps {
|
||||||
file: File;
|
file: File;
|
||||||
record?: FileRecord;
|
record?: StirlingFileStub;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
onDoubleClick?: () => void;
|
onDoubleClick?: () => void;
|
||||||
onView?: () => void;
|
onView?: () => void;
|
||||||
@ -25,7 +25,7 @@ interface FileCardProps {
|
|||||||
const FileCard = ({ file, record, onRemove, onDoubleClick, onView, onEdit, isSelected, onSelect, isSupported = true }: FileCardProps) => {
|
const FileCard = ({ file, record, onRemove, onDoubleClick, onView, onEdit, isSelected, onSelect, isSupported = true }: FileCardProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// Use record thumbnail if available, otherwise fall back to IndexedDB lookup
|
// Use record thumbnail if available, otherwise fall back to IndexedDB lookup
|
||||||
const fileMetadata = record ? { id: record.id, name: file.name, type: file.type, size: file.size, lastModified: file.lastModified } : null;
|
const fileMetadata = record ? { id: record.id, name: record.name, type: record.type, size: record.size, lastModified: record.lastModified } : null;
|
||||||
const { thumbnail: indexedDBThumb, isGenerating } = useIndexedDBThumbnail(fileMetadata);
|
const { thumbnail: indexedDBThumb, isGenerating } = useIndexedDBThumbnail(fileMetadata);
|
||||||
const thumb = record?.thumbnailUrl || indexedDBThumb;
|
const thumb = record?.thumbnailUrl || indexedDBThumb;
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Box, Flex, Group, Text, Button, TextInput, Select, Badge } from "@mantine/core";
|
import { Box, Flex, Group, Text, Button, TextInput, Select } from "@mantine/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import SearchIcon from "@mui/icons-material/Search";
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
import SortIcon from "@mui/icons-material/Sort";
|
import SortIcon from "@mui/icons-material/Sort";
|
||||||
import FileCard from "./FileCard";
|
import FileCard from "./FileCard";
|
||||||
import { FileRecord } from "../../types/fileContext";
|
import { StirlingFileStub } from "../../types/fileContext";
|
||||||
import { FileId } from "../../types/file";
|
import { FileId } from "../../types/file";
|
||||||
|
|
||||||
interface FileGridProps {
|
interface FileGridProps {
|
||||||
files: Array<{ file: File; record?: FileRecord }>;
|
files: Array<{ file: File; record?: StirlingFileStub }>;
|
||||||
onRemove?: (index: number) => void;
|
onRemove?: (index: number) => void;
|
||||||
onDoubleClick?: (item: { file: File; record?: FileRecord }) => void;
|
onDoubleClick?: (item: { file: File; record?: StirlingFileStub }) => void;
|
||||||
onView?: (item: { file: File; record?: FileRecord }) => void;
|
onView?: (item: { file: File; record?: StirlingFileStub }) => void;
|
||||||
onEdit?: (item: { file: File; record?: FileRecord }) => void;
|
onEdit?: (item: { file: File; record?: StirlingFileStub }) => void;
|
||||||
onSelect?: (fileId: FileId) => void;
|
onSelect?: (fileId: FileId) => void;
|
||||||
selectedFiles?: FileId[];
|
selectedFiles?: FileId[];
|
||||||
showSearch?: boolean;
|
showSearch?: boolean;
|
||||||
@ -123,9 +123,17 @@ const FileGrid = ({
|
|||||||
h="30rem"
|
h="30rem"
|
||||||
style={{ overflowY: "auto", width: "100%" }}
|
style={{ overflowY: "auto", width: "100%" }}
|
||||||
>
|
>
|
||||||
{displayFiles.map((item, idx) => {
|
{displayFiles
|
||||||
const fileId = item.record?.id || item.file.name as FileId /* FIX ME: This doesn't seem right */;
|
.filter(item => {
|
||||||
const originalIdx = files.findIndex(f => (f.record?.id || f.file.name) === fileId);
|
if (!item.record?.id) {
|
||||||
|
console.error('FileGrid: File missing StirlingFileStub with proper ID:', item.file.name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((item, idx) => {
|
||||||
|
const fileId = item.record!.id; // Safe to assert after filter
|
||||||
|
const originalIdx = files.findIndex(f => f.record?.id === fileId);
|
||||||
const supported = isFileSupported ? isFileSupported(item.file.name) : true;
|
const supported = isFileSupported ? isFileSupported(item.file.name) : true;
|
||||||
return (
|
return (
|
||||||
<FileCard
|
<FileCard
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Container, Text, Button, Checkbox, Group, useMantineColorScheme } from '@mantine/core';
|
import { Container, Button, Group, useMantineColorScheme } from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import LocalIcon from './LocalIcon';
|
import LocalIcon from './LocalIcon';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
@ -15,7 +15,6 @@ const LanguageSelector = ({ position = 'bottom-start', offset = 8, compact = fal
|
|||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const [opened, setOpened] = useState(false);
|
const [opened, setOpened] = useState(false);
|
||||||
const [animationTriggered, setAnimationTriggered] = useState(false);
|
const [animationTriggered, setAnimationTriggered] = useState(false);
|
||||||
const [isChanging, setIsChanging] = useState(false);
|
|
||||||
const [pendingLanguage, setPendingLanguage] = useState<string | null>(null);
|
const [pendingLanguage, setPendingLanguage] = useState<string | null>(null);
|
||||||
const [rippleEffect, setRippleEffect] = useState<{x: number, y: number, key: number} | null>(null);
|
const [rippleEffect, setRippleEffect] = useState<{x: number, y: number, key: number} | null>(null);
|
||||||
|
|
||||||
@ -36,7 +35,6 @@ const LanguageSelector = ({ position = 'bottom-start', offset = 8, compact = fal
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start transition animation
|
// Start transition animation
|
||||||
setIsChanging(true);
|
|
||||||
setPendingLanguage(value);
|
setPendingLanguage(value);
|
||||||
|
|
||||||
// Simulate processing time for smooth transition
|
// Simulate processing time for smooth transition
|
||||||
@ -44,7 +42,6 @@ const LanguageSelector = ({ position = 'bottom-start', offset = 8, compact = fal
|
|||||||
i18n.changeLanguage(value);
|
i18n.changeLanguage(value);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsChanging(false);
|
|
||||||
setPendingLanguage(null);
|
setPendingLanguage(null);
|
||||||
setOpened(false);
|
setOpened(false);
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ try {
|
|||||||
localIconCount = Object.keys(iconSet.icons || {}).length;
|
localIconCount = Object.keys(iconSet.icons || {}).length;
|
||||||
console.info(`✅ Local icons loaded: ${localIconCount} icons (${Math.round(JSON.stringify(iconSet).length / 1024)}KB)`);
|
console.info(`✅ Local icons loaded: ${localIconCount} icons (${Math.round(JSON.stringify(iconSet).length / 1024)}KB)`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.info('ℹ️ Local icons not available - using CDN fallback');
|
console.info('ℹ️ Local icons not available - using CDN fallback');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ import { ActionIcon, Stack, Divider } from "@mantine/core";
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import LocalIcon from './LocalIcon';
|
import LocalIcon from './LocalIcon';
|
||||||
import { useRainbowThemeContext } from "./RainbowThemeProvider";
|
import { useRainbowThemeContext } from "./RainbowThemeProvider";
|
||||||
import AppConfigModal from './AppConfigModal';
|
|
||||||
import { useIsOverflowing } from '../../hooks/useIsOverflowing';
|
import { useIsOverflowing } from '../../hooks/useIsOverflowing';
|
||||||
import { useFilesModalContext } from '../../contexts/FilesModalContext';
|
import { useFilesModalContext } from '../../contexts/FilesModalContext';
|
||||||
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
|
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
|
||||||
@ -19,8 +18,7 @@ import {
|
|||||||
getActiveNavButton,
|
getActiveNavButton,
|
||||||
} from './quickAccessBar/QuickAccessBar';
|
} from './quickAccessBar/QuickAccessBar';
|
||||||
|
|
||||||
const QuickAccessBar = forwardRef<HTMLDivElement>(({
|
const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||||
}, ref) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isRainbowMode } = useRainbowThemeContext();
|
const { isRainbowMode } = useRainbowThemeContext();
|
||||||
const { openFilesModal, isFilesModalOpen } = useFilesModalContext();
|
const { openFilesModal, isFilesModalOpen } = useFilesModalContext();
|
||||||
|
@ -29,12 +29,11 @@ export default function RightRail() {
|
|||||||
|
|
||||||
// File state and selection
|
// File state and selection
|
||||||
const { state, selectors } = useFileState();
|
const { state, selectors } = useFileState();
|
||||||
const { selectedFiles, selectedFileIds, selectedPageNumbers, setSelectedFiles, setSelectedPages } = useFileSelection();
|
const { selectedFiles, selectedFileIds, setSelectedFiles } = useFileSelection();
|
||||||
const { removeFiles } = useFileManagement();
|
const { removeFiles } = useFileManagement();
|
||||||
|
|
||||||
const activeFiles = selectors.getFiles();
|
const activeFiles = selectors.getFiles();
|
||||||
const filesSignature = selectors.getFilesSignature();
|
const filesSignature = selectors.getFilesSignature();
|
||||||
const fileRecords = selectors.getFileRecords();
|
|
||||||
|
|
||||||
// Compute selection state and total items
|
// Compute selection state and total items
|
||||||
const getSelectionState = useCallback(() => {
|
const getSelectionState = useCallback(() => {
|
||||||
|
@ -82,8 +82,8 @@ export function adjustFontSizeToFit(
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelAnimationFrame(raf);
|
cancelAnimationFrame(raf);
|
||||||
try { ro.disconnect(); } catch {}
|
try { ro.disconnect(); } catch { /* Ignore errors */ }
|
||||||
try { mo.disconnect(); } catch {}
|
try { mo.disconnect(); } catch { /* Ignore errors */ }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ interface ActiveToolButtonProps {
|
|||||||
|
|
||||||
const NAV_IDS = ['read', 'sign', 'automate'];
|
const NAV_IDS = ['read', 'sign', 'automate'];
|
||||||
|
|
||||||
const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ activeButton, setActiveButton }) => {
|
const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton }) => {
|
||||||
const { selectedTool, selectedToolKey, leftPanelView, handleBackToTools } = useToolWorkflow();
|
const { selectedTool, selectedToolKey, leftPanelView, handleBackToTools } = useToolWorkflow();
|
||||||
const { getHomeNavigation } = useSidebarNavigation();
|
const { getHomeNavigation } = useSidebarNavigation();
|
||||||
|
|
||||||
@ -41,7 +41,6 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ activeButton, setAc
|
|||||||
const [indicatorTool, setIndicatorTool] = useState<typeof selectedTool | null>(null);
|
const [indicatorTool, setIndicatorTool] = useState<typeof selectedTool | null>(null);
|
||||||
const [indicatorVisible, setIndicatorVisible] = useState<boolean>(false);
|
const [indicatorVisible, setIndicatorVisible] = useState<boolean>(false);
|
||||||
const [replayAnim, setReplayAnim] = useState<boolean>(false);
|
const [replayAnim, setReplayAnim] = useState<boolean>(false);
|
||||||
const [isAnimating, setIsAnimating] = useState<boolean>(false);
|
|
||||||
const [isBackHover, setIsBackHover] = useState<boolean>(false);
|
const [isBackHover, setIsBackHover] = useState<boolean>(false);
|
||||||
const prevKeyRef = useRef<string | null>(null);
|
const prevKeyRef = useRef<string | null>(null);
|
||||||
const collapseTimeoutRef = useRef<number | null>(null);
|
const collapseTimeoutRef = useRef<number | null>(null);
|
||||||
@ -74,11 +73,9 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ activeButton, setAc
|
|||||||
replayRafRef.current = requestAnimationFrame(() => {
|
replayRafRef.current = requestAnimationFrame(() => {
|
||||||
setReplayAnim(true);
|
setReplayAnim(true);
|
||||||
});
|
});
|
||||||
setIsAnimating(true);
|
|
||||||
prevKeyRef.current = (selectedToolKey as string) || null;
|
prevKeyRef.current = (selectedToolKey as string) || null;
|
||||||
animTimeoutRef.current = window.setTimeout(() => {
|
animTimeoutRef.current = window.setTimeout(() => {
|
||||||
setReplayAnim(false);
|
setReplayAnim(false);
|
||||||
setIsAnimating(false);
|
|
||||||
animTimeoutRef.current = null;
|
animTimeoutRef.current = null;
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
@ -87,10 +84,8 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ activeButton, setAc
|
|||||||
clearTimers();
|
clearTimers();
|
||||||
setIndicatorTool(selectedTool);
|
setIndicatorTool(selectedTool);
|
||||||
setIndicatorVisible(true);
|
setIndicatorVisible(true);
|
||||||
setIsAnimating(true);
|
|
||||||
prevKeyRef.current = (selectedToolKey as string) || null;
|
prevKeyRef.current = (selectedToolKey as string) || null;
|
||||||
animTimeoutRef.current = window.setTimeout(() => {
|
animTimeoutRef.current = window.setTimeout(() => {
|
||||||
setIsAnimating(false);
|
|
||||||
animTimeoutRef.current = null;
|
animTimeoutRef.current = null;
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
@ -98,11 +93,9 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ activeButton, setAc
|
|||||||
const triggerCollapse = () => {
|
const triggerCollapse = () => {
|
||||||
clearTimers();
|
clearTimers();
|
||||||
setIndicatorVisible(false);
|
setIndicatorVisible(false);
|
||||||
setIsAnimating(true);
|
|
||||||
collapseTimeoutRef.current = window.setTimeout(() => {
|
collapseTimeoutRef.current = window.setTimeout(() => {
|
||||||
setIndicatorTool(null);
|
setIndicatorTool(null);
|
||||||
prevKeyRef.current = null;
|
prevKeyRef.current = null;
|
||||||
setIsAnimating(false);
|
|
||||||
collapseTimeoutRef.current = null;
|
collapseTimeoutRef.current = null;
|
||||||
}, 500); // match CSS transition duration
|
}, 500); // match CSS transition duration
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React from 'react';
|
||||||
import { Box, Stack, Text } from '@mantine/core';
|
import { Box, Stack } from '@mantine/core';
|
||||||
import { getSubcategoryLabel, ToolRegistryEntry } from '../../data/toolsTaxonomy';
|
import { getSubcategoryLabel, ToolRegistryEntry } from '../../data/toolsTaxonomy';
|
||||||
import ToolButton from './toolPicker/ToolButton';
|
import ToolButton from './toolPicker/ToolButton';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -40,12 +40,10 @@ const SearchResults: React.FC<SearchResultsProps> = ({ filteredTools, onSelect }
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
{/* global spacer to allow scrolling past last row in search mode */}
|
{/* Global spacer to allow scrolling past last row in search mode */}
|
||||||
<div aria-hidden style={{ height: 200 }} />
|
<div aria-hidden style={{ height: 200 }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SearchResults;
|
export default SearchResults;
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
|
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
|
||||||
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
|
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
|
||||||
import ToolPicker from './ToolPicker';
|
import ToolPicker from './ToolPicker';
|
||||||
@ -8,12 +6,11 @@ import ToolRenderer from './ToolRenderer';
|
|||||||
import ToolSearch from './toolPicker/ToolSearch';
|
import ToolSearch from './toolPicker/ToolSearch';
|
||||||
import { useSidebarContext } from "../../contexts/SidebarContext";
|
import { useSidebarContext } from "../../contexts/SidebarContext";
|
||||||
import rainbowStyles from '../../styles/rainbow.module.css';
|
import rainbowStyles from '../../styles/rainbow.module.css';
|
||||||
import { Stack, ScrollArea } from '@mantine/core';
|
import { ScrollArea } from '@mantine/core';
|
||||||
|
|
||||||
// No props needed - component uses context
|
// No props needed - component uses context
|
||||||
|
|
||||||
export default function ToolPanel() {
|
export default function ToolPanel() {
|
||||||
const { t } = useTranslation();
|
|
||||||
const { isRainbowMode } = useRainbowThemeContext();
|
const { isRainbowMode } = useRainbowThemeContext();
|
||||||
const { sidebarRefs } = useSidebarContext();
|
const { sidebarRefs } = useSidebarContext();
|
||||||
const { toolPanelRef } = sidebarRefs;
|
const { toolPanelRef } = sidebarRefs;
|
||||||
@ -27,7 +24,6 @@ export default function ToolPanel() {
|
|||||||
filteredTools,
|
filteredTools,
|
||||||
toolRegistry,
|
toolRegistry,
|
||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
handleBackToTools
|
|
||||||
} = useToolWorkflow();
|
} = useToolWorkflow();
|
||||||
|
|
||||||
const { selectedToolKey, handleToolSelect } = useToolWorkflow();
|
const { selectedToolKey, handleToolSelect } = useToolWorkflow();
|
||||||
|
@ -3,7 +3,6 @@ import { render, screen, fireEvent } from '@testing-library/react';
|
|||||||
import { MantineProvider } from '@mantine/core';
|
import { MantineProvider } from '@mantine/core';
|
||||||
import AddPasswordSettings from './AddPasswordSettings';
|
import AddPasswordSettings from './AddPasswordSettings';
|
||||||
import { defaultParameters } from '../../../hooks/tools/addPassword/useAddPasswordParameters';
|
import { defaultParameters } from '../../../hooks/tools/addPassword/useAddPasswordParameters';
|
||||||
import type { AddPasswordParameters } from '../../../hooks/tools/addPassword/useAddPasswordParameters';
|
|
||||||
|
|
||||||
// Mock useTranslation with predictable return values
|
// Mock useTranslation with predictable return values
|
||||||
const mockT = vi.fn((key: string) => `mock-${key}`);
|
const mockT = vi.fn((key: string) => `mock-${key}`);
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React from "react";
|
import { Stack, PasswordInput, Select } from "@mantine/core";
|
||||||
import { Stack, Text, PasswordInput, Select } from "@mantine/core";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { AddPasswordParameters } from "../../../hooks/tools/addPassword/useAddPasswordParameters";
|
import { AddPasswordParameters } from "../../../hooks/tools/addPassword/useAddPasswordParameters";
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React from "react";
|
import { Button, Stack } from "@mantine/core";
|
||||||
import { Button, Stack, Text } from "@mantine/core";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface WatermarkTypeSettingsProps {
|
interface WatermarkTypeSettingsProps {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Stack, Text, TextInput } from "@mantine/core";
|
import { Stack, TextInput } from "@mantine/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
|
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
|
||||||
import { removeEmojis } from "../../../utils/textUtils";
|
import { removeEmojis } from "../../../utils/textUtils";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@ -38,10 +38,8 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
automationIcon,
|
automationIcon,
|
||||||
setAutomationIcon,
|
setAutomationIcon,
|
||||||
selectedTools,
|
selectedTools,
|
||||||
addTool,
|
|
||||||
removeTool,
|
removeTool,
|
||||||
updateTool,
|
updateTool,
|
||||||
hasUnsavedChanges,
|
|
||||||
canSaveAutomation,
|
canSaveAutomation,
|
||||||
getToolName,
|
getToolName,
|
||||||
getToolDefaultParameters
|
getToolDefaultParameters
|
||||||
@ -84,14 +82,6 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
updateTool(selectedTools.length, newTool);
|
updateTool(selectedTools.length, newTool);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBackClick = () => {
|
|
||||||
if (hasUnsavedChanges()) {
|
|
||||||
setUnsavedWarningOpen(true);
|
|
||||||
} else {
|
|
||||||
onBack();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConfirmBack = () => {
|
const handleConfirmBack = () => {
|
||||||
setUnsavedWarningOpen(false);
|
setUnsavedWarningOpen(false);
|
||||||
onBack();
|
onBack();
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Group, Text, ActionIcon, Menu, Box } from '@mantine/core';
|
import { Group, Text, ActionIcon, Menu, Box } from '@mantine/core';
|
||||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
@ -76,13 +76,13 @@ export default function AutomationRun({ automation, onComplete, automateOperatio
|
|||||||
await automateOperation.executeOperation(
|
await automateOperation.executeOperation(
|
||||||
{
|
{
|
||||||
automationConfig: automation,
|
automationConfig: automation,
|
||||||
onStepStart: (stepIndex: number, operationName: string) => {
|
onStepStart: (stepIndex: number, _operationName: string) => {
|
||||||
setCurrentStepIndex(stepIndex);
|
setCurrentStepIndex(stepIndex);
|
||||||
setExecutionSteps(prev => prev.map((step, idx) =>
|
setExecutionSteps(prev => prev.map((step, idx) =>
|
||||||
idx === stepIndex ? { ...step, status: EXECUTION_STATUS.RUNNING } : step
|
idx === stepIndex ? { ...step, status: EXECUTION_STATUS.RUNNING } : step
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
onStepComplete: (stepIndex: number, resultFiles: File[]) => {
|
onStepComplete: (stepIndex: number, _resultFiles: File[]) => {
|
||||||
setExecutionSteps(prev => prev.map((step, idx) =>
|
setExecutionSteps(prev => prev.map((step, idx) =>
|
||||||
idx === stepIndex ? { ...step, status: EXECUTION_STATUS.COMPLETED } : step
|
idx === stepIndex ? { ...step, status: EXECUTION_STATUS.COMPLETED } : step
|
||||||
));
|
));
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
@ -32,7 +32,6 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel,
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [parameters, setParameters] = useState<any>({});
|
const [parameters, setParameters] = useState<any>({});
|
||||||
const [isValid, setIsValid] = useState(true);
|
|
||||||
|
|
||||||
// Get tool info from registry
|
// Get tool info from registry
|
||||||
const toolInfo = toolRegistry[tool.operation as keyof ToolRegistry];
|
const toolInfo = toolRegistry[tool.operation as keyof ToolRegistry];
|
||||||
@ -87,9 +86,7 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel,
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
if (isValid) {
|
onSave(parameters);
|
||||||
onSave(parameters);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -127,7 +124,6 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel,
|
|||||||
<Button
|
<Button
|
||||||
leftSection={<CheckIcon />}
|
leftSection={<CheckIcon />}
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={!isValid}
|
|
||||||
>
|
>
|
||||||
{t('automate.config.save', 'Save Configuration')}
|
{t('automate.config.save', 'Save Configuration')}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Stack, Text, Checkbox } from "@mantine/core";
|
import { Stack, Checkbox } from "@mantine/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ChangePermissionsParameters } from "../../../hooks/tools/changePermissions/useChangePermissionsParameters";
|
import { ChangePermissionsParameters } from "../../../hooks/tools/changePermissions/useChangePermissionsParameters";
|
||||||
|
|
||||||
|
@ -22,13 +22,13 @@ import {
|
|||||||
OUTPUT_OPTIONS,
|
OUTPUT_OPTIONS,
|
||||||
FIT_OPTIONS
|
FIT_OPTIONS
|
||||||
} from "../../../constants/convertConstants";
|
} from "../../../constants/convertConstants";
|
||||||
import { FileId } from "../../../types/file";
|
import { StirlingFile } from "../../../types/fileContext";
|
||||||
|
|
||||||
interface ConvertSettingsProps {
|
interface ConvertSettingsProps {
|
||||||
parameters: ConvertParameters;
|
parameters: ConvertParameters;
|
||||||
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||||
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
|
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
|
||||||
selectedFiles: File[];
|
selectedFiles: StirlingFile[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,7 +129,7 @@ const ConvertSettings = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const filterFilesByExtension = (extension: string) => {
|
const filterFilesByExtension = (extension: string) => {
|
||||||
const files = activeFiles.map(fileId => selectors.getFile(fileId)).filter(Boolean) as File[];
|
const files = activeFiles.map(fileId => selectors.getFile(fileId)).filter(Boolean) as StirlingFile[];
|
||||||
return files.filter(file => {
|
return files.filter(file => {
|
||||||
const fileExtension = detectFileExtension(file.name);
|
const fileExtension = detectFileExtension(file.name);
|
||||||
|
|
||||||
@ -143,21 +143,8 @@ const ConvertSettings = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateFileSelection = (files: File[]) => {
|
const updateFileSelection = (files: StirlingFile[]) => {
|
||||||
// Map File objects to their actual IDs in FileContext
|
const fileIds = files.map(file => file.fileId);
|
||||||
const fileIds = files.map(file => {
|
|
||||||
// Find the file ID by matching file properties
|
|
||||||
const fileRecord = state.files.ids
|
|
||||||
.map(id => selectors.getFileRecord(id))
|
|
||||||
.find(record =>
|
|
||||||
record &&
|
|
||||||
record.name === file.name &&
|
|
||||||
record.size === file.size &&
|
|
||||||
record.lastModified === file.lastModified
|
|
||||||
);
|
|
||||||
return fileRecord?.id;
|
|
||||||
}).filter((id): id is FileId => id !== undefined); // Type guard to ensure only strings
|
|
||||||
|
|
||||||
setSelectedFiles(fileIds);
|
setSelectedFiles(fileIds);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,11 +3,12 @@ import { Stack, Text, Select, Alert } from '@mantine/core';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParameters';
|
import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParameters';
|
||||||
import { usePdfSignatureDetection } from '../../../hooks/usePdfSignatureDetection';
|
import { usePdfSignatureDetection } from '../../../hooks/usePdfSignatureDetection';
|
||||||
|
import { StirlingFile } from '../../../types/fileContext';
|
||||||
|
|
||||||
interface ConvertToPdfaSettingsProps {
|
interface ConvertToPdfaSettingsProps {
|
||||||
parameters: ConvertParameters;
|
parameters: ConvertParameters;
|
||||||
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||||
selectedFiles: File[];
|
selectedFiles: StirlingFile[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Stack, Select, Text, Divider } from '@mantine/core';
|
import { Stack, Select, Divider } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import LanguagePicker from './LanguagePicker';
|
import LanguagePicker from './LanguagePicker';
|
||||||
import { OCRParameters } from '../../../hooks/tools/ocr/useOCRParameters';
|
import { OCRParameters } from '../../../hooks/tools/ocr/useOCRParameters';
|
||||||
|
@ -8,11 +8,7 @@ interface RemoveCertificateSignSettingsProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RemoveCertificateSignSettings: React.FC<RemoveCertificateSignSettingsProps> = ({
|
const RemoveCertificateSignSettings: React.FC<RemoveCertificateSignSettingsProps> = (_) => {
|
||||||
parameters,
|
|
||||||
onParameterChange, // Unused - kept for interface consistency and future extensibility
|
|
||||||
disabled = false
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Stack, Text, PasswordInput } from "@mantine/core";
|
import { Stack, PasswordInput } from "@mantine/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { RemovePasswordParameters } from "../../../hooks/tools/removePassword/useRemovePasswordParameters";
|
import { RemovePasswordParameters } from "../../../hooks/tools/removePassword/useRemovePasswordParameters";
|
||||||
|
|
||||||
|
@ -8,11 +8,7 @@ interface RepairSettingsProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RepairSettings: React.FC<RepairSettingsProps> = ({
|
const RepairSettings: React.FC<RepairSettingsProps> = (_) => {
|
||||||
parameters,
|
|
||||||
onParameterChange,
|
|
||||||
disabled = false
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Text, Anchor } from "@mantine/core";
|
import { Text, Anchor } from "@mantine/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import FolderIcon from '@mui/icons-material/Folder';
|
import FolderIcon from '@mui/icons-material/Folder';
|
||||||
@ -6,9 +6,10 @@ import UploadIcon from '@mui/icons-material/Upload';
|
|||||||
import { useFilesModalContext } from "../../../contexts/FilesModalContext";
|
import { useFilesModalContext } from "../../../contexts/FilesModalContext";
|
||||||
import { useAllFiles } from "../../../contexts/FileContext";
|
import { useAllFiles } from "../../../contexts/FileContext";
|
||||||
import { useFileManager } from "../../../hooks/useFileManager";
|
import { useFileManager } from "../../../hooks/useFileManager";
|
||||||
|
import { StirlingFile } from "../../../types/fileContext";
|
||||||
|
|
||||||
export interface FileStatusIndicatorProps {
|
export interface FileStatusIndicatorProps {
|
||||||
selectedFiles?: File[];
|
selectedFiles?: StirlingFile[];
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,7 +18,7 @@ const FileStatusIndicator = ({
|
|||||||
}: FileStatusIndicatorProps) => {
|
}: FileStatusIndicatorProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { openFilesModal, onFilesSelect } = useFilesModalContext();
|
const { openFilesModal, onFilesSelect } = useFilesModalContext();
|
||||||
const { files: workbenchFiles } = useAllFiles();
|
const { files: stirlingFileStubs } = useAllFiles();
|
||||||
const { loadRecentFiles } = useFileManager();
|
const { loadRecentFiles } = useFileManager();
|
||||||
const [hasRecentFiles, setHasRecentFiles] = useState<boolean | null>(null);
|
const [hasRecentFiles, setHasRecentFiles] = useState<boolean | null>(null);
|
||||||
|
|
||||||
@ -27,7 +28,7 @@ const FileStatusIndicator = ({
|
|||||||
try {
|
try {
|
||||||
const recentFiles = await loadRecentFiles();
|
const recentFiles = await loadRecentFiles();
|
||||||
setHasRecentFiles(recentFiles.length > 0);
|
setHasRecentFiles(recentFiles.length > 0);
|
||||||
} catch (error) {
|
} catch {
|
||||||
setHasRecentFiles(false);
|
setHasRecentFiles(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -55,7 +56,7 @@ const FileStatusIndicator = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if there are no files in the workbench
|
// Check if there are no files in the workbench
|
||||||
if (workbenchFiles.length === 0) {
|
if (stirlingFileStubs.length === 0) {
|
||||||
// If no recent files, show upload button
|
// If no recent files, show upload button
|
||||||
if (!hasRecentFiles) {
|
if (!hasRecentFiles) {
|
||||||
return (
|
return (
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import FileStatusIndicator from './FileStatusIndicator';
|
import FileStatusIndicator from './FileStatusIndicator';
|
||||||
|
import { StirlingFile } from '../../../types/fileContext';
|
||||||
|
|
||||||
export interface FilesToolStepProps {
|
export interface FilesToolStepProps {
|
||||||
selectedFiles: File[];
|
selectedFiles: StirlingFile[];
|
||||||
isCollapsed?: boolean;
|
isCollapsed?: boolean;
|
||||||
onCollapsedClick?: () => void;
|
onCollapsedClick?: () => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { Button, Group, Stack } from "@mantine/core";
|
import { Button, Stack } from "@mantine/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import DownloadIcon from "@mui/icons-material/Download";
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
import UndoIcon from "@mui/icons-material/Undo";
|
import UndoIcon from "@mui/icons-material/Undo";
|
||||||
|
@ -3,8 +3,6 @@ import { Stack, Text, Divider, Card, Group, Anchor } from '@mantine/core';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSuggestedTools } from '../../../hooks/useSuggestedTools';
|
import { useSuggestedTools } from '../../../hooks/useSuggestedTools';
|
||||||
|
|
||||||
export interface SuggestedToolsSectionProps {}
|
|
||||||
|
|
||||||
export function SuggestedToolsSection(): React.ReactElement {
|
export function SuggestedToolsSection(): React.ReactElement {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const suggestedTools = useSuggestedTools();
|
const suggestedTools = useSuggestedTools();
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { createContext, useContext, useMemo, useRef } from 'react';
|
import React, { createContext, useContext, useMemo } from 'react';
|
||||||
import { Text, Stack, Box, Flex, Divider } from '@mantine/core';
|
import { Text, Stack, Flex, Divider } from '@mantine/core';
|
||||||
import LocalIcon from '../../shared/LocalIcon';
|
import LocalIcon from '../../shared/LocalIcon';
|
||||||
import { Tooltip } from '../../shared/Tooltip';
|
import { Tooltip } from '../../shared/Tooltip';
|
||||||
import { TooltipTip } from '../../../types/tips';
|
import { TooltipTip } from '../../../types/tips';
|
||||||
|
@ -4,9 +4,10 @@ import { createToolSteps, ToolStepProvider } from './ToolStep';
|
|||||||
import OperationButton from './OperationButton';
|
import OperationButton from './OperationButton';
|
||||||
import { ToolOperationHook } from '../../../hooks/tools/shared/useToolOperation';
|
import { ToolOperationHook } from '../../../hooks/tools/shared/useToolOperation';
|
||||||
import { ToolWorkflowTitle, ToolWorkflowTitleProps } from './ToolWorkflowTitle';
|
import { ToolWorkflowTitle, ToolWorkflowTitleProps } from './ToolWorkflowTitle';
|
||||||
|
import { StirlingFile } from '../../../types/fileContext';
|
||||||
|
|
||||||
export interface FilesStepConfig {
|
export interface FilesStepConfig {
|
||||||
selectedFiles: File[];
|
selectedFiles: StirlingFile[];
|
||||||
isCollapsed?: boolean;
|
isCollapsed?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
onCollapsedClick?: () => void;
|
onCollapsedClick?: () => void;
|
||||||
@ -80,7 +81,7 @@ export function createToolFlow(config: ToolFlowConfig) {
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Middle Steps */}
|
{/* Middle Steps */}
|
||||||
{config.steps.map((stepConfig, index) =>
|
{config.steps.map((stepConfig) =>
|
||||||
steps.create(stepConfig.title, {
|
steps.create(stepConfig.title, {
|
||||||
isVisible: stepConfig.isVisible,
|
isVisible: stepConfig.isVisible,
|
||||||
isCollapsed: stepConfig.isCollapsed,
|
isCollapsed: stepConfig.isCollapsed,
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React from 'react';
|
import { Box } from '@mantine/core';
|
||||||
import { Box, Stack } from '@mantine/core';
|
|
||||||
import ToolButton from '../toolPicker/ToolButton';
|
import ToolButton from '../toolPicker/ToolButton';
|
||||||
import SubcategoryHeader from './SubcategoryHeader';
|
import SubcategoryHeader from './SubcategoryHeader';
|
||||||
|
|
||||||
|
@ -8,11 +8,7 @@ interface SingleLargePageSettingsProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SingleLargePageSettings: React.FC<SingleLargePageSettingsProps> = ({
|
const SingleLargePageSettings: React.FC<SingleLargePageSettingsProps> = (_) => {
|
||||||
parameters,
|
|
||||||
onParameterChange,
|
|
||||||
disabled = false
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -126,7 +126,7 @@ const ToolSearch = ({
|
|||||||
key={id}
|
key={id}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onToolSelect && onToolSelect(id);
|
onToolSelect?.(id);
|
||||||
setDropdownOpen(false);
|
setDropdownOpen(false);
|
||||||
}}
|
}}
|
||||||
leftSection={<div style={{ color: "var(--tools-text-and-icon-color)" }}>{tool.icon}</div>}
|
leftSection={<div style={{ color: "var(--tools-text-and-icon-color)" }}>{tool.icon}</div>}
|
||||||
|
@ -8,11 +8,7 @@ interface UnlockPdfFormsSettingsProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UnlockPdfFormsSettings: React.FC<UnlockPdfFormsSettingsProps> = ({
|
const UnlockPdfFormsSettings: React.FC<UnlockPdfFormsSettingsProps> = (_) => {
|
||||||
parameters,
|
|
||||||
onParameterChange, // Unused - kept for interface consistency and future extensibility
|
|
||||||
disabled = false
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
import React, { useEffect, useState, useRef, useCallback } from "react";
|
import React, { useEffect, useState, useRef, useCallback } from "react";
|
||||||
import { Paper, Stack, Text, ScrollArea, Loader, Center, Button, Group, NumberInput, useMantineTheme, ActionIcon, Box, Tabs } from "@mantine/core";
|
import { Paper, Stack, Text, ScrollArea, Center, Button, Group, NumberInput, useMantineTheme, ActionIcon, Box, Tabs } from "@mantine/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { pdfWorkerManager } from "../../services/pdfWorkerManager";
|
import { pdfWorkerManager } from "../../services/pdfWorkerManager";
|
||||||
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
|
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
|
||||||
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
|
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
|
||||||
import FirstPageIcon from "@mui/icons-material/FirstPage";
|
import FirstPageIcon from "@mui/icons-material/FirstPage";
|
||||||
import LastPageIcon from "@mui/icons-material/LastPage";
|
import LastPageIcon from "@mui/icons-material/LastPage";
|
||||||
import ViewSidebarIcon from "@mui/icons-material/ViewSidebar";
|
|
||||||
import ViewWeekIcon from "@mui/icons-material/ViewWeek"; // for dual page (book)
|
import ViewWeekIcon from "@mui/icons-material/ViewWeek"; // for dual page (book)
|
||||||
import DescriptionIcon from "@mui/icons-material/Description"; // for single page
|
import DescriptionIcon from "@mui/icons-material/Description"; // for single page
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import { useLocalStorage } from "@mantine/hooks";
|
|
||||||
import { fileStorage } from "../../services/fileStorage";
|
import { fileStorage } from "../../services/fileStorage";
|
||||||
import SkeletonLoader from '../shared/SkeletonLoader';
|
import SkeletonLoader from '../shared/SkeletonLoader';
|
||||||
import { useFileState, useFileActions, useCurrentFile } from "../../contexts/FileContext";
|
import { useFileState } from "../../contexts/FileContext";
|
||||||
import { useFileWithUrl } from "../../hooks/useFileWithUrl";
|
import { useFileWithUrl } from "../../hooks/useFileWithUrl";
|
||||||
|
import { isFileObject } from "../../types/fileContext";
|
||||||
import { FileId } from "../../types/file";
|
import { FileId } from "../../types/file";
|
||||||
|
|
||||||
|
|
||||||
@ -141,8 +140,6 @@ export interface ViewerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Viewer = ({
|
const Viewer = ({
|
||||||
sidebarsVisible,
|
|
||||||
setSidebarsVisible,
|
|
||||||
onClose,
|
onClose,
|
||||||
previewFile,
|
previewFile,
|
||||||
}: ViewerProps) => {
|
}: ViewerProps) => {
|
||||||
@ -151,13 +148,7 @@ const Viewer = ({
|
|||||||
|
|
||||||
// Get current file from FileContext
|
// Get current file from FileContext
|
||||||
const { selectors } = useFileState();
|
const { selectors } = useFileState();
|
||||||
const { actions } = useFileActions();
|
|
||||||
const currentFile = useCurrentFile();
|
|
||||||
|
|
||||||
const getCurrentFile = () => currentFile.file;
|
|
||||||
const getCurrentProcessedFile = () => currentFile.record?.processedFile || undefined;
|
|
||||||
const clearAllFiles = actions.clearAllFiles;
|
|
||||||
const addFiles = actions.addFiles;
|
|
||||||
const activeFiles = selectors.getFiles();
|
const activeFiles = selectors.getFiles();
|
||||||
|
|
||||||
// Tab management for multiple files
|
// Tab management for multiple files
|
||||||
@ -201,7 +192,7 @@ const Viewer = ({
|
|||||||
const effectiveFile = React.useMemo(() => {
|
const effectiveFile = React.useMemo(() => {
|
||||||
if (previewFile) {
|
if (previewFile) {
|
||||||
// Validate the preview file
|
// Validate the preview file
|
||||||
if (!(previewFile instanceof File)) {
|
if (!isFileObject(previewFile)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -405,7 +396,7 @@ const Viewer = ({
|
|||||||
// Start progressive preloading after a short delay
|
// Start progressive preloading after a short delay
|
||||||
setTimeout(() => startProgressivePreload(), 100);
|
setTimeout(() => startProgressivePreload(), 100);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setPageImages([]);
|
setPageImages([]);
|
||||||
setNumPages(0);
|
setNumPages(0);
|
||||||
|
@ -19,7 +19,10 @@ import {
|
|||||||
FileContextStateValue,
|
FileContextStateValue,
|
||||||
FileContextActionsValue,
|
FileContextActionsValue,
|
||||||
FileContextActions,
|
FileContextActions,
|
||||||
FileRecord
|
FileId,
|
||||||
|
StirlingFileStub,
|
||||||
|
StirlingFile,
|
||||||
|
createStirlingFile
|
||||||
} from '../types/fileContext';
|
} from '../types/fileContext';
|
||||||
|
|
||||||
// Import modular components
|
// Import modular components
|
||||||
@ -29,7 +32,6 @@ import { AddedFile, addFiles, consumeFiles, undoConsumeFiles, createFileActions
|
|||||||
import { FileLifecycleManager } from './file/lifecycle';
|
import { FileLifecycleManager } from './file/lifecycle';
|
||||||
import { FileStateContext, FileActionsContext } from './file/contexts';
|
import { FileStateContext, FileActionsContext } from './file/contexts';
|
||||||
import { IndexedDBProvider, useIndexedDB } from './IndexedDBContext';
|
import { IndexedDBProvider, useIndexedDB } from './IndexedDBContext';
|
||||||
import { FileId } from '../types/file';
|
|
||||||
|
|
||||||
const DEBUG = process.env.NODE_ENV === 'development';
|
const DEBUG = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
@ -37,7 +39,6 @@ const DEBUG = process.env.NODE_ENV === 'development';
|
|||||||
// Inner provider component that has access to IndexedDB
|
// Inner provider component that has access to IndexedDB
|
||||||
function FileContextInner({
|
function FileContextInner({
|
||||||
children,
|
children,
|
||||||
enableUrlSync = true,
|
|
||||||
enablePersistence = true
|
enablePersistence = true
|
||||||
}: FileContextProviderProps) {
|
}: FileContextProviderProps) {
|
||||||
const [state, dispatch] = useReducer(fileContextReducer, initialFileContextState);
|
const [state, dispatch] = useReducer(fileContextReducer, initialFileContextState);
|
||||||
@ -79,7 +80,7 @@ function FileContextInner({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// File operations using unified addFiles helper with persistence
|
// File operations using unified addFiles helper with persistence
|
||||||
const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise<File[]> => {
|
const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise<StirlingFile[]> => {
|
||||||
const addedFilesWithIds = await addFiles('raw', { files, ...options }, stateRef, filesRef, dispatch, lifecycleManager);
|
const addedFilesWithIds = await addFiles('raw', { files, ...options }, stateRef, filesRef, dispatch, lifecycleManager);
|
||||||
|
|
||||||
// Auto-select the newly added files if requested
|
// Auto-select the newly added files if requested
|
||||||
@ -98,15 +99,15 @@ function FileContextInner({
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return addedFilesWithIds.map(({ file }) => file);
|
return addedFilesWithIds.map(({ file, id }) => createStirlingFile(file, id));
|
||||||
}, [indexedDB, enablePersistence]);
|
}, [indexedDB, enablePersistence]);
|
||||||
|
|
||||||
const addProcessedFiles = useCallback(async (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>): Promise<File[]> => {
|
const addProcessedFiles = useCallback(async (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>): Promise<StirlingFile[]> => {
|
||||||
const result = await addFiles('processed', { filesWithThumbnails }, stateRef, filesRef, dispatch, lifecycleManager);
|
const result = await addFiles('processed', { filesWithThumbnails }, stateRef, filesRef, dispatch, lifecycleManager);
|
||||||
return result.map(({ file }) => file);
|
return result.map(({ file, id }) => createStirlingFile(file, id));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: any }>, options?: { selectFiles?: boolean }): Promise<File[]> => {
|
const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: any }>, options?: { selectFiles?: boolean }): Promise<StirlingFile[]> => {
|
||||||
const result = await addFiles('stored', { filesWithMetadata }, stateRef, filesRef, dispatch, lifecycleManager);
|
const result = await addFiles('stored', { filesWithMetadata }, stateRef, filesRef, dispatch, lifecycleManager);
|
||||||
|
|
||||||
// Auto-select the newly added files if requested
|
// Auto-select the newly added files if requested
|
||||||
@ -114,7 +115,7 @@ function FileContextInner({
|
|||||||
selectFiles(result);
|
selectFiles(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.map(({ file }) => file);
|
return result.map(({ file, id }) => createStirlingFile(file, id));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Action creators
|
// Action creators
|
||||||
@ -122,42 +123,21 @@ function FileContextInner({
|
|||||||
|
|
||||||
// Helper functions for pinned files
|
// Helper functions for pinned files
|
||||||
const consumeFilesWrapper = useCallback(async (inputFileIds: FileId[], outputFiles: File[]): Promise<FileId[]> => {
|
const consumeFilesWrapper = useCallback(async (inputFileIds: FileId[], outputFiles: File[]): Promise<FileId[]> => {
|
||||||
return consumeFiles(inputFileIds, outputFiles, stateRef, filesRef, dispatch, indexedDB);
|
return consumeFiles(inputFileIds, outputFiles, filesRef, dispatch, indexedDB);
|
||||||
}, [indexedDB]);
|
}, [indexedDB]);
|
||||||
|
|
||||||
const undoConsumeFilesWrapper = useCallback(async (inputFiles: File[], inputFileRecords: FileRecord[], outputFileIds: FileId[]): Promise<void> => {
|
const undoConsumeFilesWrapper = useCallback(async (inputFiles: File[], inputStirlingFileStubs: StirlingFileStub[], outputFileIds: FileId[]): Promise<void> => {
|
||||||
return undoConsumeFiles(inputFiles, inputFileRecords, outputFileIds, stateRef, filesRef, dispatch, indexedDB);
|
return undoConsumeFiles(inputFiles, inputStirlingFileStubs, outputFileIds, filesRef, dispatch, indexedDB);
|
||||||
}, [indexedDB]);
|
}, [indexedDB]);
|
||||||
|
|
||||||
// Helper to find FileId from File object
|
// File pinning functions - use StirlingFile directly
|
||||||
const findFileId = useCallback((file: File): FileId | undefined => {
|
const pinFileWrapper = useCallback((file: StirlingFile) => {
|
||||||
return (Object.keys(stateRef.current.files.byId) as FileId[]).find(id => {
|
baseActions.pinFile(file.fileId);
|
||||||
const storedFile = filesRef.current.get(id);
|
}, [baseActions]);
|
||||||
return storedFile &&
|
|
||||||
storedFile.name === file.name &&
|
|
||||||
storedFile.size === file.size &&
|
|
||||||
storedFile.lastModified === file.lastModified;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// File-to-ID wrapper functions for pinning
|
const unpinFileWrapper = useCallback((file: StirlingFile) => {
|
||||||
const pinFileWrapper = useCallback((file: File) => {
|
baseActions.unpinFile(file.fileId);
|
||||||
const fileId = findFileId(file);
|
}, [baseActions]);
|
||||||
if (fileId) {
|
|
||||||
baseActions.pinFile(fileId);
|
|
||||||
} else {
|
|
||||||
console.warn('File not found for pinning:', file.name);
|
|
||||||
}
|
|
||||||
}, [baseActions, findFileId]);
|
|
||||||
|
|
||||||
const unpinFileWrapper = useCallback((file: File) => {
|
|
||||||
const fileId = findFileId(file);
|
|
||||||
if (fileId) {
|
|
||||||
baseActions.unpinFile(fileId);
|
|
||||||
} else {
|
|
||||||
console.warn('File not found for unpinning:', file.name);
|
|
||||||
}
|
|
||||||
}, [baseActions, findFileId]);
|
|
||||||
|
|
||||||
// Complete actions object
|
// Complete actions object
|
||||||
const actions = useMemo<FileContextActions>(() => ({
|
const actions = useMemo<FileContextActions>(() => ({
|
||||||
@ -178,8 +158,8 @@ function FileContextInner({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateFileRecord: (fileId: FileId, updates: Partial<FileRecord>) =>
|
updateStirlingFileStub: (fileId: FileId, updates: Partial<StirlingFileStub>) =>
|
||||||
lifecycleManager.updateFileRecord(fileId, updates, stateRef),
|
lifecycleManager.updateStirlingFileStub(fileId, updates, stateRef),
|
||||||
reorderFiles: (orderedFileIds: FileId[]) => {
|
reorderFiles: (orderedFileIds: FileId[]) => {
|
||||||
dispatch({ type: 'REORDER_FILES', payload: { orderedFileIds } });
|
dispatch({ type: 'REORDER_FILES', payload: { orderedFileIds } });
|
||||||
},
|
},
|
||||||
@ -303,7 +283,7 @@ export {
|
|||||||
useFileSelection,
|
useFileSelection,
|
||||||
useFileManagement,
|
useFileManagement,
|
||||||
useFileUI,
|
useFileUI,
|
||||||
useFileRecord,
|
useStirlingFileStub,
|
||||||
useAllFiles,
|
useAllFiles,
|
||||||
useSelectedFiles,
|
useSelectedFiles,
|
||||||
// Primary API hooks for tools
|
// Primary API hooks for tools
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { createContext, useContext, useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
import React, { createContext, useContext, useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
||||||
import { FileMetadata } from '../types/file';
|
import { FileMetadata } from '../types/file';
|
||||||
import { StoredFile, fileStorage } from '../services/fileStorage';
|
import { fileStorage } from '../services/fileStorage';
|
||||||
import { downloadFiles } from '../utils/downloadUtils';
|
import { downloadFiles } from '../utils/downloadUtils';
|
||||||
import { FileId } from '../types/file';
|
import { FileId } from '../types/file';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
import React, { createContext, useContext, useCallback, useRef } from 'react';
|
import React, { createContext, useContext, useCallback, useRef } from 'react';
|
||||||
|
|
||||||
const DEBUG = process.env.NODE_ENV === 'development';
|
const DEBUG = process.env.NODE_ENV === 'development';
|
||||||
import { fileStorage, StoredFile } from '../services/fileStorage';
|
import { fileStorage } from '../services/fileStorage';
|
||||||
import { FileId } from '../types/file';
|
import { FileId } from '../types/file';
|
||||||
import { FileMetadata } from '../types/file';
|
import { FileMetadata } from '../types/file';
|
||||||
import { generateThumbnailForFile } from '../utils/thumbnailUtils';
|
import { generateThumbnailForFile } from '../utils/thumbnailUtils';
|
||||||
@ -61,7 +61,7 @@ export function IndexedDBProvider({ children }: IndexedDBProviderProps) {
|
|||||||
const thumbnail = existingThumbnail || await generateThumbnailForFile(file);
|
const thumbnail = existingThumbnail || await generateThumbnailForFile(file);
|
||||||
|
|
||||||
// Store in IndexedDB
|
// Store in IndexedDB
|
||||||
const storedFile = await fileStorage.storeFile(file, fileId, thumbnail);
|
await fileStorage.storeFile(file, fileId, thumbnail);
|
||||||
|
|
||||||
// Cache the file object for immediate reuse
|
// Cache the file object for immediate reuse
|
||||||
fileCache.current.set(fileId, { file, lastAccessed: Date.now() });
|
fileCache.current.set(fileId, { file, lastAccessed: Date.now() });
|
||||||
|
@ -103,7 +103,7 @@ const NavigationActionsContext = createContext<NavigationContextActionsValue | u
|
|||||||
export const NavigationProvider: React.FC<{
|
export const NavigationProvider: React.FC<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
enableUrlSync?: boolean;
|
enableUrlSync?: boolean;
|
||||||
}> = ({ children, enableUrlSync = true }) => {
|
}> = ({ children }) => {
|
||||||
const [state, dispatch] = useReducer(navigationReducer, initialState);
|
const [state, dispatch] = useReducer(navigationReducer, initialState);
|
||||||
const toolRegistry = useFlatToolRegistry();
|
const toolRegistry = useFlatToolRegistry();
|
||||||
|
|
||||||
|
@ -89,6 +89,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
|
|||||||
clearToolSelection: () => void;
|
clearToolSelection: () => void;
|
||||||
|
|
||||||
// Tool Reset Actions
|
// Tool Reset Actions
|
||||||
|
toolResetFunctions: Record<string, () => void>;
|
||||||
registerToolReset: (toolId: string, resetFunction: () => void) => void;
|
registerToolReset: (toolId: string, resetFunction: () => void) => void;
|
||||||
resetTool: (toolId: string) => void;
|
resetTool: (toolId: string) => void;
|
||||||
|
|
||||||
@ -258,6 +259,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
clearToolSelection: () => actions.setSelectedTool(null),
|
clearToolSelection: () => actions.setSelectedTool(null),
|
||||||
|
|
||||||
// Tool Reset Actions
|
// Tool Reset Actions
|
||||||
|
toolResetFunctions,
|
||||||
registerToolReset,
|
registerToolReset,
|
||||||
resetTool,
|
resetTool,
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import { FileId } from '../../types/file';
|
|||||||
import {
|
import {
|
||||||
FileContextState,
|
FileContextState,
|
||||||
FileContextAction,
|
FileContextAction,
|
||||||
FileRecord
|
StirlingFileStub
|
||||||
} from '../../types/fileContext';
|
} from '../../types/fileContext';
|
||||||
|
|
||||||
// Initial state
|
// Initial state
|
||||||
@ -29,7 +29,7 @@ export const initialFileContextState: FileContextState = {
|
|||||||
function processFileSwap(
|
function processFileSwap(
|
||||||
state: FileContextState,
|
state: FileContextState,
|
||||||
filesToRemove: FileId[],
|
filesToRemove: FileId[],
|
||||||
filesToAdd: FileRecord[]
|
filesToAdd: StirlingFileStub[]
|
||||||
): FileContextState {
|
): FileContextState {
|
||||||
// Only remove unpinned files
|
// Only remove unpinned files
|
||||||
const unpinnedRemoveIds = filesToRemove.filter(id => !state.pinnedFiles.has(id));
|
const unpinnedRemoveIds = filesToRemove.filter(id => !state.pinnedFiles.has(id));
|
||||||
@ -70,11 +70,11 @@ function processFileSwap(
|
|||||||
export function fileContextReducer(state: FileContextState, action: FileContextAction): FileContextState {
|
export function fileContextReducer(state: FileContextState, action: FileContextAction): FileContextState {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'ADD_FILES': {
|
case 'ADD_FILES': {
|
||||||
const { fileRecords } = action.payload;
|
const { stirlingFileStubs } = action.payload;
|
||||||
const newIds: FileId[] = [];
|
const newIds: FileId[] = [];
|
||||||
const newById: Record<FileId, FileRecord> = { ...state.files.byId };
|
const newById: Record<FileId, StirlingFileStub> = { ...state.files.byId };
|
||||||
|
|
||||||
fileRecords.forEach(record => {
|
stirlingFileStubs.forEach(record => {
|
||||||
// Only add if not already present (dedupe by stable ID)
|
// Only add if not already present (dedupe by stable ID)
|
||||||
if (!newById[record.id]) {
|
if (!newById[record.id]) {
|
||||||
newIds.push(record.id);
|
newIds.push(record.id);
|
||||||
@ -233,13 +233,13 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'CONSUME_FILES': {
|
case 'CONSUME_FILES': {
|
||||||
const { inputFileIds, outputFileRecords } = action.payload;
|
const { inputFileIds, outputStirlingFileStubs } = action.payload;
|
||||||
return processFileSwap(state, inputFileIds, outputFileRecords);
|
return processFileSwap(state, inputFileIds, outputStirlingFileStubs);
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'UNDO_CONSUME_FILES': {
|
case 'UNDO_CONSUME_FILES': {
|
||||||
const { inputFileRecords, outputFileIds } = action.payload;
|
const { inputStirlingFileStubs, outputFileIds } = action.payload;
|
||||||
return processFileSwap(state, outputFileIds, inputFileRecords);
|
return processFileSwap(state, outputFileIds, inputStirlingFileStubs);
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'RESET_CONTEXT': {
|
case 'RESET_CONTEXT': {
|
||||||
|
@ -3,18 +3,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FileRecord,
|
StirlingFileStub,
|
||||||
FileContextAction,
|
FileContextAction,
|
||||||
FileContextState,
|
FileContextState,
|
||||||
toFileRecord,
|
toStirlingFileStub,
|
||||||
createFileId,
|
createFileId,
|
||||||
createQuickKey
|
createQuickKey
|
||||||
} from '../../types/fileContext';
|
} from '../../types/fileContext';
|
||||||
import { FileId, FileMetadata } from '../../types/file';
|
import { FileId, FileMetadata } from '../../types/file';
|
||||||
import { generateThumbnailWithMetadata } from '../../utils/thumbnailUtils';
|
import { generateThumbnailWithMetadata } from '../../utils/thumbnailUtils';
|
||||||
import { FileLifecycleManager } from './lifecycle';
|
import { FileLifecycleManager } from './lifecycle';
|
||||||
import { fileProcessingService } from '../../services/fileProcessingService';
|
import { buildQuickKeySet } from './fileSelectors';
|
||||||
import { buildQuickKeySet, buildQuickKeySetFromMetadata } from './fileSelectors';
|
|
||||||
|
|
||||||
const DEBUG = process.env.NODE_ENV === 'development';
|
const DEBUG = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
@ -109,8 +108,8 @@ export async function addFiles(
|
|||||||
await addFilesMutex.lock();
|
await addFilesMutex.lock();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fileRecords: FileRecord[] = [];
|
const stirlingFileStubs: StirlingFileStub[] = [];
|
||||||
const addedFiles: AddedFile[] = [];
|
const addedFiles: AddedFile[] = [];
|
||||||
|
|
||||||
// Build quickKey lookup from existing files for deduplication
|
// Build quickKey lookup from existing files for deduplication
|
||||||
const existingQuickKeys = buildQuickKeySet(stateRef.current.files.byId);
|
const existingQuickKeys = buildQuickKeySet(stateRef.current.files.byId);
|
||||||
@ -163,7 +162,7 @@ export async function addFiles(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create record with immediate thumbnail and page metadata
|
// Create record with immediate thumbnail and page metadata
|
||||||
const record = toFileRecord(file, fileId);
|
const record = toStirlingFileStub(file, fileId);
|
||||||
if (thumbnail) {
|
if (thumbnail) {
|
||||||
record.thumbnailUrl = thumbnail;
|
record.thumbnailUrl = thumbnail;
|
||||||
// Track blob URLs for cleanup (images return blob URLs that need revocation)
|
// Track blob URLs for cleanup (images return blob URLs that need revocation)
|
||||||
@ -184,7 +183,7 @@ export async function addFiles(
|
|||||||
}
|
}
|
||||||
|
|
||||||
existingQuickKeys.add(quickKey);
|
existingQuickKeys.add(quickKey);
|
||||||
fileRecords.push(record);
|
stirlingFileStubs.push(record);
|
||||||
addedFiles.push({ file, id: fileId, thumbnail });
|
addedFiles.push({ file, id: fileId, thumbnail });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -205,7 +204,7 @@ export async function addFiles(
|
|||||||
const fileId = createFileId();
|
const fileId = createFileId();
|
||||||
filesRef.current.set(fileId, file);
|
filesRef.current.set(fileId, file);
|
||||||
|
|
||||||
const record = toFileRecord(file, fileId);
|
const record = toStirlingFileStub(file, fileId);
|
||||||
if (thumbnail) {
|
if (thumbnail) {
|
||||||
record.thumbnailUrl = thumbnail;
|
record.thumbnailUrl = thumbnail;
|
||||||
// Track blob URLs for cleanup (images return blob URLs that need revocation)
|
// Track blob URLs for cleanup (images return blob URLs that need revocation)
|
||||||
@ -226,7 +225,7 @@ export async function addFiles(
|
|||||||
}
|
}
|
||||||
|
|
||||||
existingQuickKeys.add(quickKey);
|
existingQuickKeys.add(quickKey);
|
||||||
fileRecords.push(record);
|
stirlingFileStubs.push(record);
|
||||||
addedFiles.push({ file, id: fileId, thumbnail });
|
addedFiles.push({ file, id: fileId, thumbnail });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -254,7 +253,7 @@ export async function addFiles(
|
|||||||
|
|
||||||
filesRef.current.set(fileId, file);
|
filesRef.current.set(fileId, file);
|
||||||
|
|
||||||
const record = toFileRecord(file, fileId);
|
const record = toStirlingFileStub(file, fileId);
|
||||||
|
|
||||||
// Generate processedFile metadata for stored files
|
// Generate processedFile metadata for stored files
|
||||||
let pageCount: number = 1;
|
let pageCount: number = 1;
|
||||||
@ -301,7 +300,7 @@ export async function addFiles(
|
|||||||
}
|
}
|
||||||
|
|
||||||
existingQuickKeys.add(quickKey);
|
existingQuickKeys.add(quickKey);
|
||||||
fileRecords.push(record);
|
stirlingFileStubs.push(record);
|
||||||
addedFiles.push({ file, id: fileId, thumbnail: metadata.thumbnail });
|
addedFiles.push({ file, id: fileId, thumbnail: metadata.thumbnail });
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -310,9 +309,9 @@ export async function addFiles(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch ADD_FILES action if we have new files
|
// Dispatch ADD_FILES action if we have new files
|
||||||
if (fileRecords.length > 0) {
|
if (stirlingFileStubs.length > 0) {
|
||||||
dispatch({ type: 'ADD_FILES', payload: { fileRecords } });
|
dispatch({ type: 'ADD_FILES', payload: { stirlingFileStubs } });
|
||||||
if (DEBUG) console.log(`📄 addFiles(${kind}): Successfully added ${fileRecords.length} files`);
|
if (DEBUG) console.log(`📄 addFiles(${kind}): Successfully added ${stirlingFileStubs.length} files`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return addedFiles;
|
return addedFiles;
|
||||||
@ -328,7 +327,7 @@ export async function addFiles(
|
|||||||
async function processFilesIntoRecords(
|
async function processFilesIntoRecords(
|
||||||
files: File[],
|
files: File[],
|
||||||
filesRef: React.MutableRefObject<Map<FileId, File>>
|
filesRef: React.MutableRefObject<Map<FileId, File>>
|
||||||
): Promise<Array<{ record: FileRecord; file: File; fileId: FileId; thumbnail?: string }>> {
|
): Promise<Array<{ record: StirlingFileStub; file: File; fileId: FileId; thumbnail?: string }>> {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
files.map(async (file) => {
|
files.map(async (file) => {
|
||||||
const fileId = createFileId();
|
const fileId = createFileId();
|
||||||
@ -347,7 +346,7 @@ async function processFilesIntoRecords(
|
|||||||
if (DEBUG) console.warn(`📄 Failed to generate thumbnail for file ${file.name}:`, error);
|
if (DEBUG) console.warn(`📄 Failed to generate thumbnail for file ${file.name}:`, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const record = toFileRecord(file, fileId);
|
const record = toStirlingFileStub(file, fileId);
|
||||||
if (thumbnail) {
|
if (thumbnail) {
|
||||||
record.thumbnailUrl = thumbnail;
|
record.thumbnailUrl = thumbnail;
|
||||||
}
|
}
|
||||||
@ -365,10 +364,10 @@ async function processFilesIntoRecords(
|
|||||||
* Helper function to persist files to IndexedDB
|
* Helper function to persist files to IndexedDB
|
||||||
*/
|
*/
|
||||||
async function persistFilesToIndexedDB(
|
async function persistFilesToIndexedDB(
|
||||||
fileRecords: Array<{ file: File; fileId: FileId; thumbnail?: string }>,
|
stirlingFileStubs: Array<{ file: File; fileId: FileId; thumbnail?: string }>,
|
||||||
indexedDB: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<any> }
|
indexedDB: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<any> }
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await Promise.all(fileRecords.map(async ({ file, fileId, thumbnail }) => {
|
await Promise.all(stirlingFileStubs.map(async ({ file, fileId, thumbnail }) => {
|
||||||
try {
|
try {
|
||||||
await indexedDB.saveFile(file, fileId, thumbnail);
|
await indexedDB.saveFile(file, fileId, thumbnail);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -383,7 +382,6 @@ async function persistFilesToIndexedDB(
|
|||||||
export async function consumeFiles(
|
export async function consumeFiles(
|
||||||
inputFileIds: FileId[],
|
inputFileIds: FileId[],
|
||||||
outputFiles: File[],
|
outputFiles: File[],
|
||||||
stateRef: React.MutableRefObject<FileContextState>,
|
|
||||||
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
||||||
dispatch: React.Dispatch<FileContextAction>,
|
dispatch: React.Dispatch<FileContextAction>,
|
||||||
indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<any> } | null
|
indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<any> } | null
|
||||||
@ -391,11 +389,11 @@ export async function consumeFiles(
|
|||||||
if (DEBUG) console.log(`📄 consumeFiles: Processing ${inputFileIds.length} input files, ${outputFiles.length} output files`);
|
if (DEBUG) console.log(`📄 consumeFiles: Processing ${inputFileIds.length} input files, ${outputFiles.length} output files`);
|
||||||
|
|
||||||
// Process output files with thumbnails and metadata
|
// Process output files with thumbnails and metadata
|
||||||
const outputFileRecords = await processFilesIntoRecords(outputFiles, filesRef);
|
const outputStirlingFileStubs = await processFilesIntoRecords(outputFiles, filesRef);
|
||||||
|
|
||||||
// Persist output files to IndexedDB if available
|
// Persist output files to IndexedDB if available
|
||||||
if (indexedDB) {
|
if (indexedDB) {
|
||||||
await persistFilesToIndexedDB(outputFileRecords, indexedDB);
|
await persistFilesToIndexedDB(outputStirlingFileStubs, indexedDB);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch the consume action
|
// Dispatch the consume action
|
||||||
@ -403,21 +401,20 @@ export async function consumeFiles(
|
|||||||
type: 'CONSUME_FILES',
|
type: 'CONSUME_FILES',
|
||||||
payload: {
|
payload: {
|
||||||
inputFileIds,
|
inputFileIds,
|
||||||
outputFileRecords: outputFileRecords.map(({ record }) => record)
|
outputStirlingFileStubs: outputStirlingFileStubs.map(({ record }) => record)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (DEBUG) console.log(`📄 consumeFiles: Successfully consumed files - removed ${inputFileIds.length} inputs, added ${outputFileRecords.length} outputs`);
|
if (DEBUG) console.log(`📄 consumeFiles: Successfully consumed files - removed ${inputFileIds.length} inputs, added ${outputStirlingFileStubs.length} outputs`);
|
||||||
|
|
||||||
// Return the output file IDs for undo tracking
|
// Return the output file IDs for undo tracking
|
||||||
return outputFileRecords.map(({ fileId }) => fileId);
|
return outputStirlingFileStubs.map(({ fileId }) => fileId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to restore files to filesRef and manage IndexedDB cleanup
|
* Helper function to restore files to filesRef and manage IndexedDB cleanup
|
||||||
*/
|
*/
|
||||||
async function restoreFilesAndCleanup(
|
async function restoreFilesAndCleanup(
|
||||||
filesToRestore: Array<{ file: File; record: FileRecord }>,
|
filesToRestore: Array<{ file: File; record: StirlingFileStub }>,
|
||||||
fileIdsToRemove: FileId[],
|
fileIdsToRemove: FileId[],
|
||||||
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
||||||
indexedDB?: { deleteFile: (fileId: FileId) => Promise<void> } | null
|
indexedDB?: { deleteFile: (fileId: FileId) => Promise<void> } | null
|
||||||
@ -466,18 +463,17 @@ async function restoreFilesAndCleanup(
|
|||||||
*/
|
*/
|
||||||
export async function undoConsumeFiles(
|
export async function undoConsumeFiles(
|
||||||
inputFiles: File[],
|
inputFiles: File[],
|
||||||
inputFileRecords: FileRecord[],
|
inputStirlingFileStubs: StirlingFileStub[],
|
||||||
outputFileIds: FileId[],
|
outputFileIds: FileId[],
|
||||||
stateRef: React.MutableRefObject<FileContextState>,
|
|
||||||
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
||||||
dispatch: React.Dispatch<FileContextAction>,
|
dispatch: React.Dispatch<FileContextAction>,
|
||||||
indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<any>; deleteFile: (fileId: FileId) => Promise<void> } | null
|
indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<any>; deleteFile: (fileId: FileId) => Promise<void> } | null
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (DEBUG) console.log(`📄 undoConsumeFiles: Restoring ${inputFileRecords.length} input files, removing ${outputFileIds.length} output files`);
|
if (DEBUG) console.log(`📄 undoConsumeFiles: Restoring ${inputStirlingFileStubs.length} input files, removing ${outputFileIds.length} output files`);
|
||||||
|
|
||||||
// Validate inputs
|
// Validate inputs
|
||||||
if (inputFiles.length !== inputFileRecords.length) {
|
if (inputFiles.length !== inputStirlingFileStubs.length) {
|
||||||
throw new Error(`Mismatch between input files (${inputFiles.length}) and records (${inputFileRecords.length})`);
|
throw new Error(`Mismatch between input files (${inputFiles.length}) and records (${inputStirlingFileStubs.length})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a backup of current filesRef state for rollback
|
// Create a backup of current filesRef state for rollback
|
||||||
@ -487,7 +483,7 @@ export async function undoConsumeFiles(
|
|||||||
// Prepare files to restore
|
// Prepare files to restore
|
||||||
const filesToRestore = inputFiles.map((file, index) => ({
|
const filesToRestore = inputFiles.map((file, index) => ({
|
||||||
file,
|
file,
|
||||||
record: inputFileRecords[index]
|
record: inputStirlingFileStubs[index]
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Restore input files and clean up output files
|
// Restore input files and clean up output files
|
||||||
@ -502,13 +498,12 @@ export async function undoConsumeFiles(
|
|||||||
dispatch({
|
dispatch({
|
||||||
type: 'UNDO_CONSUME_FILES',
|
type: 'UNDO_CONSUME_FILES',
|
||||||
payload: {
|
payload: {
|
||||||
inputFileRecords,
|
inputStirlingFileStubs,
|
||||||
outputFileIds
|
outputFileIds
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (DEBUG) console.log(`📄 undoConsumeFiles: Successfully undone consume operation - restored ${inputFileRecords.length} inputs, removed ${outputFileIds.length} outputs`);
|
if (DEBUG) console.log(`📄 undoConsumeFiles: Successfully undone consume operation - restored ${inputStirlingFileStubs.length} inputs, removed ${outputFileIds.length} outputs`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Rollback filesRef to previous state
|
// Rollback filesRef to previous state
|
||||||
if (DEBUG) console.error('📄 undoConsumeFiles: Error during undo, rolling back filesRef', error);
|
if (DEBUG) console.error('📄 undoConsumeFiles: Error during undo, rolling back filesRef', error);
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
FileContextStateValue,
|
FileContextStateValue,
|
||||||
FileContextActionsValue
|
FileContextActionsValue
|
||||||
} from './contexts';
|
} from './contexts';
|
||||||
import { FileRecord } from '../../types/fileContext';
|
import { StirlingFileStub, StirlingFile } from '../../types/fileContext';
|
||||||
import { FileId } from '../../types/file';
|
import { FileId } from '../../types/file';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,13 +38,13 @@ export function useFileActions(): FileContextActionsValue {
|
|||||||
/**
|
/**
|
||||||
* Hook for current/primary file (first in list)
|
* Hook for current/primary file (first in list)
|
||||||
*/
|
*/
|
||||||
export function useCurrentFile(): { file?: File; record?: FileRecord } {
|
export function useCurrentFile(): { file?: File; record?: StirlingFileStub } {
|
||||||
const { state, selectors } = useFileState();
|
const { state, selectors } = useFileState();
|
||||||
|
|
||||||
const primaryFileId = state.files.ids[0];
|
const primaryFileId = state.files.ids[0];
|
||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
file: primaryFileId ? selectors.getFile(primaryFileId) : undefined,
|
file: primaryFileId ? selectors.getFile(primaryFileId) : undefined,
|
||||||
record: primaryFileId ? selectors.getFileRecord(primaryFileId) : undefined
|
record: primaryFileId ? selectors.getStirlingFileStub(primaryFileId) : undefined
|
||||||
}), [primaryFileId, selectors]);
|
}), [primaryFileId, selectors]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ export function useFileManagement() {
|
|||||||
addFiles: actions.addFiles,
|
addFiles: actions.addFiles,
|
||||||
removeFiles: actions.removeFiles,
|
removeFiles: actions.removeFiles,
|
||||||
clearAllFiles: actions.clearAllFiles,
|
clearAllFiles: actions.clearAllFiles,
|
||||||
updateFileRecord: actions.updateFileRecord,
|
updateStirlingFileStub: actions.updateStirlingFileStub,
|
||||||
reorderFiles: actions.reorderFiles
|
reorderFiles: actions.reorderFiles
|
||||||
}), [actions]);
|
}), [actions]);
|
||||||
}
|
}
|
||||||
@ -111,24 +111,24 @@ export function useFileUI() {
|
|||||||
/**
|
/**
|
||||||
* Hook for specific file by ID (optimized for individual file access)
|
* Hook for specific file by ID (optimized for individual file access)
|
||||||
*/
|
*/
|
||||||
export function useFileRecord(fileId: FileId): { file?: File; record?: FileRecord } {
|
export function useStirlingFileStub(fileId: FileId): { file?: File; record?: StirlingFileStub } {
|
||||||
const { selectors } = useFileState();
|
const { selectors } = useFileState();
|
||||||
|
|
||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
file: selectors.getFile(fileId),
|
file: selectors.getFile(fileId),
|
||||||
record: selectors.getFileRecord(fileId)
|
record: selectors.getStirlingFileStub(fileId)
|
||||||
}), [fileId, selectors]);
|
}), [fileId, selectors]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook for all files (use sparingly - causes re-renders on file list changes)
|
* Hook for all files (use sparingly - causes re-renders on file list changes)
|
||||||
*/
|
*/
|
||||||
export function useAllFiles(): { files: File[]; records: FileRecord[]; fileIds: FileId[] } {
|
export function useAllFiles(): { files: StirlingFile[]; records: StirlingFileStub[]; fileIds: FileId[] } {
|
||||||
const { state, selectors } = useFileState();
|
const { state, selectors } = useFileState();
|
||||||
|
|
||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
files: selectors.getFiles(),
|
files: selectors.getFiles(),
|
||||||
records: selectors.getFileRecords(),
|
records: selectors.getStirlingFileStubs(),
|
||||||
fileIds: state.files.ids
|
fileIds: state.files.ids
|
||||||
}), [state.files.ids, selectors]);
|
}), [state.files.ids, selectors]);
|
||||||
}
|
}
|
||||||
@ -136,12 +136,12 @@ export function useAllFiles(): { files: File[]; records: FileRecord[]; fileIds:
|
|||||||
/**
|
/**
|
||||||
* Hook for selected files (optimized for selection-based UI)
|
* Hook for selected files (optimized for selection-based UI)
|
||||||
*/
|
*/
|
||||||
export function useSelectedFiles(): { files: File[]; records: FileRecord[]; fileIds: FileId[] } {
|
export function useSelectedFiles(): { files: StirlingFile[]; records: StirlingFileStub[]; fileIds: FileId[] } {
|
||||||
const { state, selectors } = useFileState();
|
const { state, selectors } = useFileState();
|
||||||
|
|
||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
files: selectors.getSelectedFiles(),
|
files: selectors.getSelectedFiles(),
|
||||||
records: selectors.getSelectedFileRecords(),
|
records: selectors.getSelectedStirlingFileStubs(),
|
||||||
fileIds: state.ui.selectedFileIds
|
fileIds: state.ui.selectedFileIds
|
||||||
}), [state.ui.selectedFileIds, selectors]);
|
}), [state.ui.selectedFileIds, selectors]);
|
||||||
}
|
}
|
||||||
@ -166,9 +166,9 @@ export function useFileContext() {
|
|||||||
addFiles: actions.addFiles,
|
addFiles: actions.addFiles,
|
||||||
consumeFiles: actions.consumeFiles,
|
consumeFiles: actions.consumeFiles,
|
||||||
undoConsumeFiles: actions.undoConsumeFiles,
|
undoConsumeFiles: actions.undoConsumeFiles,
|
||||||
recordOperation: (fileId: FileId, operation: any) => {}, // Operation tracking not implemented
|
recordOperation: (_fileId: FileId, _operation: any) => {}, // Operation tracking not implemented
|
||||||
markOperationApplied: (fileId: FileId, operationId: string) => {}, // Operation tracking not implemented
|
markOperationApplied: (_fileId: FileId, _operationId: string) => {}, // Operation tracking not implemented
|
||||||
markOperationFailed: (fileId: FileId, operationId: string, error: string) => {}, // Operation tracking not implemented
|
markOperationFailed: (_fileId: FileId, _operationId: string, _error: string) => {}, // Operation tracking not implemented
|
||||||
|
|
||||||
// File ID lookup
|
// File ID lookup
|
||||||
findFileId: (file: File) => {
|
findFileId: (file: File) => {
|
||||||
|
@ -4,9 +4,11 @@
|
|||||||
|
|
||||||
import { FileId } from '../../types/file';
|
import { FileId } from '../../types/file';
|
||||||
import {
|
import {
|
||||||
FileRecord,
|
StirlingFileStub,
|
||||||
FileContextState,
|
FileContextState,
|
||||||
FileContextSelectors
|
FileContextSelectors,
|
||||||
|
StirlingFile,
|
||||||
|
createStirlingFile
|
||||||
} from '../../types/fileContext';
|
} from '../../types/fileContext';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,16 +19,24 @@ export function createFileSelectors(
|
|||||||
filesRef: React.MutableRefObject<Map<FileId, File>>
|
filesRef: React.MutableRefObject<Map<FileId, File>>
|
||||||
): FileContextSelectors {
|
): FileContextSelectors {
|
||||||
return {
|
return {
|
||||||
getFile: (id: FileId) => filesRef.current.get(id),
|
getFile: (id: FileId) => {
|
||||||
|
const file = filesRef.current.get(id);
|
||||||
|
return file ? createStirlingFile(file, id) : undefined;
|
||||||
|
},
|
||||||
|
|
||||||
getFiles: (ids?: FileId[]) => {
|
getFiles: (ids?: FileId[]) => {
|
||||||
const currentIds = ids || stateRef.current.files.ids;
|
const currentIds = ids || stateRef.current.files.ids;
|
||||||
return currentIds.map(id => filesRef.current.get(id)).filter(Boolean) as File[];
|
return currentIds
|
||||||
|
.map(id => {
|
||||||
|
const file = filesRef.current.get(id);
|
||||||
|
return file ? createStirlingFile(file, id) : undefined;
|
||||||
|
})
|
||||||
|
.filter(Boolean) as StirlingFile[];
|
||||||
},
|
},
|
||||||
|
|
||||||
getFileRecord: (id: FileId) => stateRef.current.files.byId[id],
|
getStirlingFileStub: (id: FileId) => stateRef.current.files.byId[id],
|
||||||
|
|
||||||
getFileRecords: (ids?: FileId[]) => {
|
getStirlingFileStubs: (ids?: FileId[]) => {
|
||||||
const currentIds = ids || stateRef.current.files.ids;
|
const currentIds = ids || stateRef.current.files.ids;
|
||||||
return currentIds.map(id => stateRef.current.files.byId[id]).filter(Boolean);
|
return currentIds.map(id => stateRef.current.files.byId[id]).filter(Boolean);
|
||||||
},
|
},
|
||||||
@ -35,11 +45,14 @@ export function createFileSelectors(
|
|||||||
|
|
||||||
getSelectedFiles: () => {
|
getSelectedFiles: () => {
|
||||||
return stateRef.current.ui.selectedFileIds
|
return stateRef.current.ui.selectedFileIds
|
||||||
.map(id => filesRef.current.get(id))
|
.map(id => {
|
||||||
.filter(Boolean) as File[];
|
const file = filesRef.current.get(id);
|
||||||
|
return file ? createStirlingFile(file, id) : undefined;
|
||||||
|
})
|
||||||
|
.filter(Boolean) as StirlingFile[];
|
||||||
},
|
},
|
||||||
|
|
||||||
getSelectedFileRecords: () => {
|
getSelectedStirlingFileStubs: () => {
|
||||||
return stateRef.current.ui.selectedFileIds
|
return stateRef.current.ui.selectedFileIds
|
||||||
.map(id => stateRef.current.files.byId[id])
|
.map(id => stateRef.current.files.byId[id])
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
@ -52,26 +65,21 @@ export function createFileSelectors(
|
|||||||
|
|
||||||
getPinnedFiles: () => {
|
getPinnedFiles: () => {
|
||||||
return Array.from(stateRef.current.pinnedFiles)
|
return Array.from(stateRef.current.pinnedFiles)
|
||||||
.map(id => filesRef.current.get(id))
|
.map(id => {
|
||||||
.filter(Boolean) as File[];
|
const file = filesRef.current.get(id);
|
||||||
|
return file ? createStirlingFile(file, id) : undefined;
|
||||||
|
})
|
||||||
|
.filter(Boolean) as StirlingFile[];
|
||||||
},
|
},
|
||||||
|
|
||||||
getPinnedFileRecords: () => {
|
getPinnedStirlingFileStubs: () => {
|
||||||
return Array.from(stateRef.current.pinnedFiles)
|
return Array.from(stateRef.current.pinnedFiles)
|
||||||
.map(id => stateRef.current.files.byId[id])
|
.map(id => stateRef.current.files.byId[id])
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
},
|
},
|
||||||
|
|
||||||
isFilePinned: (file: File) => {
|
isFilePinned: (file: StirlingFile) => {
|
||||||
// Find FileId by matching File object properties
|
return stateRef.current.pinnedFiles.has(file.fileId);
|
||||||
const fileId = (Object.keys(stateRef.current.files.byId) as FileId[]).find(id => {
|
|
||||||
const storedFile = filesRef.current.get(id);
|
|
||||||
return storedFile &&
|
|
||||||
storedFile.name === file.name &&
|
|
||||||
storedFile.size === file.size &&
|
|
||||||
storedFile.lastModified === file.lastModified;
|
|
||||||
});
|
|
||||||
return fileId ? stateRef.current.pinnedFiles.has(fileId) : false;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Stable signature for effects - prevents unnecessary re-renders
|
// Stable signature for effects - prevents unnecessary re-renders
|
||||||
@ -90,9 +98,9 @@ export function createFileSelectors(
|
|||||||
/**
|
/**
|
||||||
* Helper for building quickKey sets for deduplication
|
* Helper for building quickKey sets for deduplication
|
||||||
*/
|
*/
|
||||||
export function buildQuickKeySet(fileRecords: Record<FileId, FileRecord>): Set<string> {
|
export function buildQuickKeySet(stirlingFileStubs: Record<FileId, StirlingFileStub>): Set<string> {
|
||||||
const quickKeys = new Set<string>();
|
const quickKeys = new Set<string>();
|
||||||
Object.values(fileRecords).forEach(record => {
|
Object.values(stirlingFileStubs).forEach(record => {
|
||||||
if (record.quickKey) {
|
if (record.quickKey) {
|
||||||
quickKeys.add(record.quickKey);
|
quickKeys.add(record.quickKey);
|
||||||
}
|
}
|
||||||
@ -119,7 +127,7 @@ export function buildQuickKeySetFromMetadata(metadata: Array<{ name: string; siz
|
|||||||
export function getPrimaryFile(
|
export function getPrimaryFile(
|
||||||
stateRef: React.MutableRefObject<FileContextState>,
|
stateRef: React.MutableRefObject<FileContextState>,
|
||||||
filesRef: React.MutableRefObject<Map<FileId, File>>
|
filesRef: React.MutableRefObject<Map<FileId, File>>
|
||||||
): { file?: File; record?: FileRecord } {
|
): { file?: File; record?: StirlingFileStub } {
|
||||||
const primaryFileId = stateRef.current.files.ids[0];
|
const primaryFileId = stateRef.current.files.ids[0];
|
||||||
if (!primaryFileId) return {};
|
if (!primaryFileId) return {};
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { FileId } from '../../types/file';
|
import { FileId } from '../../types/file';
|
||||||
import { FileContextAction, FileRecord, ProcessedFilePage } from '../../types/fileContext';
|
import { FileContextAction, StirlingFileStub, ProcessedFilePage } from '../../types/fileContext';
|
||||||
|
|
||||||
const DEBUG = process.env.NODE_ENV === 'development';
|
const DEBUG = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ export class FileLifecycleManager {
|
|||||||
this.blobUrls.forEach(url => {
|
this.blobUrls.forEach(url => {
|
||||||
try {
|
try {
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Ignore revocation errors
|
// Ignore revocation errors
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -134,7 +134,7 @@ export class FileLifecycleManager {
|
|||||||
if (record.thumbnailUrl && record.thumbnailUrl.startsWith('blob:')) {
|
if (record.thumbnailUrl && record.thumbnailUrl.startsWith('blob:')) {
|
||||||
try {
|
try {
|
||||||
URL.revokeObjectURL(record.thumbnailUrl);
|
URL.revokeObjectURL(record.thumbnailUrl);
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Ignore revocation errors
|
// Ignore revocation errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -142,18 +142,18 @@ export class FileLifecycleManager {
|
|||||||
if (record.blobUrl && record.blobUrl.startsWith('blob:')) {
|
if (record.blobUrl && record.blobUrl.startsWith('blob:')) {
|
||||||
try {
|
try {
|
||||||
URL.revokeObjectURL(record.blobUrl);
|
URL.revokeObjectURL(record.blobUrl);
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Ignore revocation errors
|
// Ignore revocation errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up processed file thumbnails
|
// Clean up processed file thumbnails
|
||||||
if (record.processedFile?.pages) {
|
if (record.processedFile?.pages) {
|
||||||
record.processedFile.pages.forEach((page: ProcessedFilePage, index: number) => {
|
record.processedFile.pages.forEach((page: ProcessedFilePage) => {
|
||||||
if (page.thumbnail && page.thumbnail.startsWith('blob:')) {
|
if (page.thumbnail && page.thumbnail.startsWith('blob:')) {
|
||||||
try {
|
try {
|
||||||
URL.revokeObjectURL(page.thumbnail);
|
URL.revokeObjectURL(page.thumbnail);
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Ignore revocation errors
|
// Ignore revocation errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -166,7 +166,7 @@ export class FileLifecycleManager {
|
|||||||
/**
|
/**
|
||||||
* Update file record with race condition guards
|
* Update file record with race condition guards
|
||||||
*/
|
*/
|
||||||
updateFileRecord = (fileId: FileId, updates: Partial<FileRecord>, stateRef?: React.MutableRefObject<any>): void => {
|
updateStirlingFileStub = (fileId: FileId, updates: Partial<StirlingFileStub>, stateRef?: React.MutableRefObject<any>): void => {
|
||||||
// Guard against updating removed files (race condition protection)
|
// Guard against updating removed files (race condition protection)
|
||||||
if (!this.filesRef.current.has(fileId)) {
|
if (!this.filesRef.current.has(fileId)) {
|
||||||
if (DEBUG) console.warn(`🗂️ Attempted to update removed file (filesRef): ${fileId}`);
|
if (DEBUG) console.warn(`🗂️ Attempted to update removed file (filesRef): ${fileId}`);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, test, vi, beforeEach, MockedFunction } from 'vitest';
|
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||||
import { renderHook } from '@testing-library/react';
|
import { renderHook } from '@testing-library/react';
|
||||||
import { useAddPasswordOperation } from './useAddPasswordOperation';
|
import { useAddPasswordOperation } from './useAddPasswordOperation';
|
||||||
import type { AddPasswordFullParameters, AddPasswordParameters } from './useAddPasswordParameters';
|
import type { AddPasswordFullParameters } from './useAddPasswordParameters';
|
||||||
|
|
||||||
// Mock the useToolOperation hook
|
// Mock the useToolOperation hook
|
||||||
vi.mock('../shared/useToolOperation', async () => {
|
vi.mock('../shared/useToolOperation', async () => {
|
||||||
|
@ -3,7 +3,6 @@ import { useCallback } from 'react';
|
|||||||
import { executeAutomationSequence } from '../../../utils/automationExecutor';
|
import { executeAutomationSequence } from '../../../utils/automationExecutor';
|
||||||
import { useFlatToolRegistry } from '../../../data/useTranslatedToolRegistry';
|
import { useFlatToolRegistry } from '../../../data/useTranslatedToolRegistry';
|
||||||
import { AutomateParameters } from '../../../types/automation';
|
import { AutomateParameters } from '../../../types/automation';
|
||||||
import { AUTOMATION_CONSTANTS } from '../../../constants/automation';
|
|
||||||
|
|
||||||
export function useAutomateOperation() {
|
export function useAutomateOperation() {
|
||||||
const toolRegistry = useFlatToolRegistry();
|
const toolRegistry = useFlatToolRegistry();
|
||||||
|
@ -46,7 +46,7 @@ export function useSavedAutomations() {
|
|||||||
const { automationStorage } = await import('../../../services/automationStorage');
|
const { automationStorage } = await import('../../../services/automationStorage');
|
||||||
|
|
||||||
// Map suggested automation icons to MUI icon keys
|
// Map suggested automation icons to MUI icon keys
|
||||||
const getIconKey = (suggestedIcon: {id: string}): string => {
|
const getIconKey = (_suggestedIcon: {id: string}): string => {
|
||||||
// Check the automation ID or name to determine the appropriate icon
|
// Check the automation ID or name to determine the appropriate icon
|
||||||
switch (suggestedAutomation.id) {
|
switch (suggestedAutomation.id) {
|
||||||
case 'secure-pdf-ingestion':
|
case 'secure-pdf-ingestion':
|
||||||
|
@ -6,7 +6,6 @@ import { SuggestedAutomation } from '../../../types/automation';
|
|||||||
|
|
||||||
// Create icon components
|
// Create icon components
|
||||||
const CompressIcon = () => React.createElement(LocalIcon, { icon: 'compress', width: '1.5rem', height: '1.5rem' });
|
const CompressIcon = () => React.createElement(LocalIcon, { icon: 'compress', width: '1.5rem', height: '1.5rem' });
|
||||||
const TextFieldsIcon = () => React.createElement(LocalIcon, { icon: 'text-fields', width: '1.5rem', height: '1.5rem' });
|
|
||||||
const SecurityIcon = () => React.createElement(LocalIcon, { icon: 'security', width: '1.5rem', height: '1.5rem' });
|
const SecurityIcon = () => React.createElement(LocalIcon, { icon: 'security', width: '1.5rem', height: '1.5rem' });
|
||||||
const StarIcon = () => React.createElement(LocalIcon, { icon: 'star', width: '1.5rem', height: '1.5rem' });
|
const StarIcon = () => React.createElement(LocalIcon, { icon: 'star', width: '1.5rem', height: '1.5rem' });
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useToolOperation, ToolOperationConfig, ToolType } from '../shared/useToolOperation';
|
import { useToolOperation, ToolType } from '../shared/useToolOperation';
|
||||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
import { CompressParameters, defaultParameters } from './useCompressParameters';
|
import { CompressParameters, defaultParameters } from './useCompressParameters';
|
||||||
|
|
||||||
|
@ -2,9 +2,8 @@ import { useCallback } from 'react';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ConvertParameters, defaultParameters } from './useConvertParameters';
|
import { ConvertParameters, defaultParameters } from './useConvertParameters';
|
||||||
import { detectFileExtension } from '../../../utils/fileUtils';
|
|
||||||
import { createFileFromApiResponse } from '../../../utils/fileResponseUtils';
|
import { createFileFromApiResponse } from '../../../utils/fileResponseUtils';
|
||||||
import { useToolOperation, ToolOperationConfig, ToolType } from '../shared/useToolOperation';
|
import { useToolOperation, ToolType } from '../shared/useToolOperation';
|
||||||
import { getEndpointUrl, isImageFormat, isWebFormat } from '../../../utils/convertUtils';
|
import { getEndpointUrl, isImageFormat, isWebFormat } from '../../../utils/convertUtils';
|
||||||
|
|
||||||
// Static function that can be used by both the hook and automation executor
|
// Static function that can be used by both the hook and automation executor
|
||||||
|
@ -2,7 +2,6 @@ import {
|
|||||||
COLOR_TYPES,
|
COLOR_TYPES,
|
||||||
OUTPUT_OPTIONS,
|
OUTPUT_OPTIONS,
|
||||||
FIT_OPTIONS,
|
FIT_OPTIONS,
|
||||||
TO_FORMAT_OPTIONS,
|
|
||||||
CONVERSION_MATRIX,
|
CONVERSION_MATRIX,
|
||||||
type ColorType,
|
type ColorType,
|
||||||
type OutputOption,
|
type OutputOption,
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, test, expect } from 'vitest';
|
import { describe, test, expect } from 'vitest';
|
||||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
import { renderHook, act } from '@testing-library/react';
|
||||||
import { useConvertParameters } from './useConvertParameters';
|
import { useConvertParameters } from './useConvertParameters';
|
||||||
|
|
||||||
describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
|
describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
|
||||||
@ -347,9 +347,9 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
|
|||||||
|
|
||||||
const malformedFiles: Array<{name: string}> = [
|
const malformedFiles: Array<{name: string}> = [
|
||||||
{ name: 'valid.pdf' },
|
{ name: 'valid.pdf' },
|
||||||
// @ts-ignore - Testing runtime resilience
|
// @ts-expect-error - Testing runtime resilience
|
||||||
{ name: null },
|
{ name: null },
|
||||||
// @ts-ignore
|
// @ts-expect-error - Testing runtime resilience
|
||||||
{ name: undefined }
|
{ name: undefined }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
|||||||
import { RemoveCertificateSignParameters, defaultParameters } from './useRemoveCertificateSignParameters';
|
import { RemoveCertificateSignParameters, defaultParameters } from './useRemoveCertificateSignParameters';
|
||||||
|
|
||||||
// Static function that can be used by both the hook and automation executor
|
// Static function that can be used by both the hook and automation executor
|
||||||
export const buildRemoveCertificateSignFormData = (parameters: RemoveCertificateSignParameters, file: File): FormData => {
|
export const buildRemoveCertificateSignFormData = (_parameters: RemoveCertificateSignParameters, file: File): FormData => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("fileInput", file);
|
formData.append("fileInput", file);
|
||||||
return formData;
|
return formData;
|
||||||
|
@ -4,7 +4,7 @@ import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
|||||||
import { RepairParameters, defaultParameters } from './useRepairParameters';
|
import { RepairParameters, defaultParameters } from './useRepairParameters';
|
||||||
|
|
||||||
// Static function that can be used by both the hook and automation executor
|
// Static function that can be used by both the hook and automation executor
|
||||||
export const buildRepairFormData = (parameters: RepairParameters, file: File): FormData => {
|
export const buildRepairFormData = (_parameters: RepairParameters, file: File): FormData => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("fileInput", file);
|
formData.append("fileInput", file);
|
||||||
return formData;
|
return formData;
|
||||||
|
@ -4,10 +4,11 @@ import { useEndpointEnabled } from '../../useEndpointConfig';
|
|||||||
import { BaseToolProps } from '../../../types/tool';
|
import { BaseToolProps } from '../../../types/tool';
|
||||||
import { ToolOperationHook } from './useToolOperation';
|
import { ToolOperationHook } from './useToolOperation';
|
||||||
import { BaseParametersHook } from './useBaseParameters';
|
import { BaseParametersHook } from './useBaseParameters';
|
||||||
|
import { StirlingFile } from '../../../types/fileContext';
|
||||||
|
|
||||||
interface BaseToolReturn<TParams> {
|
interface BaseToolReturn<TParams> {
|
||||||
// File management
|
// File management
|
||||||
selectedFiles: File[];
|
selectedFiles: StirlingFile[];
|
||||||
|
|
||||||
// Tool-specific hooks
|
// Tool-specific hooks
|
||||||
params: BaseParametersHook<TParams>;
|
params: BaseParametersHook<TParams>;
|
||||||
|
@ -6,10 +6,8 @@ import { useToolState, type ProcessingProgress } from './useToolState';
|
|||||||
import { useToolApiCalls, type ApiCallsConfig } from './useToolApiCalls';
|
import { useToolApiCalls, type ApiCallsConfig } from './useToolApiCalls';
|
||||||
import { useToolResources } from './useToolResources';
|
import { useToolResources } from './useToolResources';
|
||||||
import { extractErrorMessage } from '../../../utils/toolErrorHandler';
|
import { extractErrorMessage } from '../../../utils/toolErrorHandler';
|
||||||
import { createOperation } from '../../../utils/toolOperationTracker';
|
import { StirlingFile, extractFiles, FileId, StirlingFileStub } from '../../../types/fileContext';
|
||||||
import { ResponseHandler } from '../../../utils/toolResponseProcessor';
|
import { ResponseHandler } from '../../../utils/toolResponseProcessor';
|
||||||
import { FileId } from '../../../types/file';
|
|
||||||
import { FileRecord } from '../../../types/fileContext';
|
|
||||||
|
|
||||||
// Re-export for backwards compatibility
|
// Re-export for backwards compatibility
|
||||||
export type { ProcessingProgress, ResponseHandler };
|
export type { ProcessingProgress, ResponseHandler };
|
||||||
@ -104,7 +102,7 @@ export interface ToolOperationHook<TParams = void> {
|
|||||||
progress: ProcessingProgress | null;
|
progress: ProcessingProgress | null;
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
executeOperation: (params: TParams, selectedFiles: File[]) => Promise<void>;
|
executeOperation: (params: TParams, selectedFiles: StirlingFile[]) => Promise<void>;
|
||||||
resetResults: () => void;
|
resetResults: () => void;
|
||||||
clearError: () => void;
|
clearError: () => void;
|
||||||
cancelOperation: () => void;
|
cancelOperation: () => void;
|
||||||
@ -130,7 +128,7 @@ export const useToolOperation = <TParams>(
|
|||||||
config: ToolOperationConfig<TParams>
|
config: ToolOperationConfig<TParams>
|
||||||
): ToolOperationHook<TParams> => {
|
): ToolOperationHook<TParams> => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { recordOperation, markOperationApplied, markOperationFailed, addFiles, consumeFiles, undoConsumeFiles, findFileId, actions: fileActions, selectors } = useFileContext();
|
const { addFiles, consumeFiles, undoConsumeFiles, selectors } = useFileContext();
|
||||||
|
|
||||||
// Composed hooks
|
// Composed hooks
|
||||||
const { state, actions } = useToolState();
|
const { state, actions } = useToolState();
|
||||||
@ -140,13 +138,13 @@ export const useToolOperation = <TParams>(
|
|||||||
// Track last operation for undo functionality
|
// Track last operation for undo functionality
|
||||||
const lastOperationRef = useRef<{
|
const lastOperationRef = useRef<{
|
||||||
inputFiles: File[];
|
inputFiles: File[];
|
||||||
inputFileRecords: FileRecord[];
|
inputStirlingFileStubs: StirlingFileStub[];
|
||||||
outputFileIds: FileId[];
|
outputFileIds: FileId[];
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const executeOperation = useCallback(async (
|
const executeOperation = useCallback(async (
|
||||||
params: TParams,
|
params: TParams,
|
||||||
selectedFiles: File[]
|
selectedFiles: StirlingFile[]
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
// Validation
|
// Validation
|
||||||
if (selectedFiles.length === 0) {
|
if (selectedFiles.length === 0) {
|
||||||
@ -160,9 +158,6 @@ export const useToolOperation = <TParams>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup operation tracking
|
|
||||||
const { operation, operationId, fileId } = createOperation(config.operationType, params, selectedFiles);
|
|
||||||
recordOperation(fileId, operation);
|
|
||||||
|
|
||||||
// Reset state
|
// Reset state
|
||||||
actions.setLoading(true);
|
actions.setLoading(true);
|
||||||
@ -173,8 +168,11 @@ export const useToolOperation = <TParams>(
|
|||||||
try {
|
try {
|
||||||
let processedFiles: File[];
|
let processedFiles: File[];
|
||||||
|
|
||||||
|
// Convert StirlingFile to regular File objects for API processing
|
||||||
|
const validRegularFiles = extractFiles(validFiles);
|
||||||
|
|
||||||
switch (config.toolType) {
|
switch (config.toolType) {
|
||||||
case ToolType.singleFile:
|
case ToolType.singleFile: {
|
||||||
// Individual file processing - separate API call per file
|
// Individual file processing - separate API call per file
|
||||||
const apiCallsConfig: ApiCallsConfig<TParams> = {
|
const apiCallsConfig: ApiCallsConfig<TParams> = {
|
||||||
endpoint: config.endpoint,
|
endpoint: config.endpoint,
|
||||||
@ -184,17 +182,18 @@ export const useToolOperation = <TParams>(
|
|||||||
};
|
};
|
||||||
processedFiles = await processFiles(
|
processedFiles = await processFiles(
|
||||||
params,
|
params,
|
||||||
validFiles,
|
validRegularFiles,
|
||||||
apiCallsConfig,
|
apiCallsConfig,
|
||||||
actions.setProgress,
|
actions.setProgress,
|
||||||
actions.setStatus
|
actions.setStatus
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case ToolType.multiFile:
|
case ToolType.multiFile: {
|
||||||
// Multi-file processing - single API call with all files
|
// Multi-file processing - single API call with all files
|
||||||
actions.setStatus('Processing files...');
|
actions.setStatus('Processing files...');
|
||||||
const formData = config.buildFormData(params, validFiles);
|
const formData = config.buildFormData(params, validRegularFiles);
|
||||||
const endpoint = typeof config.endpoint === 'function' ? config.endpoint(params) : config.endpoint;
|
const endpoint = typeof config.endpoint === 'function' ? config.endpoint(params) : config.endpoint;
|
||||||
|
|
||||||
const response = await axios.post(endpoint, formData, { responseType: 'blob' });
|
const response = await axios.post(endpoint, formData, { responseType: 'blob' });
|
||||||
@ -202,11 +201,11 @@ export const useToolOperation = <TParams>(
|
|||||||
// Multi-file responses are typically ZIP files that need extraction, but some may return single PDFs
|
// Multi-file responses are typically ZIP files that need extraction, but some may return single PDFs
|
||||||
if (config.responseHandler) {
|
if (config.responseHandler) {
|
||||||
// Use custom responseHandler for multi-file (handles ZIP extraction)
|
// Use custom responseHandler for multi-file (handles ZIP extraction)
|
||||||
processedFiles = await config.responseHandler(response.data, validFiles);
|
processedFiles = await config.responseHandler(response.data, validRegularFiles);
|
||||||
} else if (response.data.type === 'application/pdf' ||
|
} else if (response.data.type === 'application/pdf' ||
|
||||||
(response.headers && response.headers['content-type'] === 'application/pdf')) {
|
(response.headers && response.headers['content-type'] === 'application/pdf')) {
|
||||||
// Single PDF response (e.g. split with merge option) - use original filename
|
// Single PDF response (e.g. split with merge option) - use original filename
|
||||||
const originalFileName = validFiles[0]?.name || 'document.pdf';
|
const originalFileName = validRegularFiles[0]?.name || 'document.pdf';
|
||||||
const singleFile = new File([response.data], originalFileName, { type: 'application/pdf' });
|
const singleFile = new File([response.data], originalFileName, { type: 'application/pdf' });
|
||||||
processedFiles = [singleFile];
|
processedFiles = [singleFile];
|
||||||
} else {
|
} else {
|
||||||
@ -219,10 +218,11 @@ export const useToolOperation = <TParams>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case ToolType.custom:
|
case ToolType.custom:
|
||||||
actions.setStatus('Processing files...');
|
actions.setStatus('Processing files...');
|
||||||
processedFiles = await config.customProcessor(params, validFiles);
|
processedFiles = await config.customProcessor(params, validRegularFiles);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,21 +242,17 @@ export const useToolOperation = <TParams>(
|
|||||||
|
|
||||||
// Replace input files with processed files (consumeFiles handles pinning)
|
// Replace input files with processed files (consumeFiles handles pinning)
|
||||||
const inputFileIds: FileId[] = [];
|
const inputFileIds: FileId[] = [];
|
||||||
const inputFileRecords: FileRecord[] = [];
|
const inputStirlingFileStubs: StirlingFileStub[] = [];
|
||||||
|
|
||||||
// Build parallel arrays of IDs and records for undo tracking
|
// Build parallel arrays of IDs and records for undo tracking
|
||||||
for (const file of validFiles) {
|
for (const file of validFiles) {
|
||||||
const fileId = findFileId(file);
|
const fileId = file.fileId;
|
||||||
if (fileId) {
|
const record = selectors.getStirlingFileStub(fileId);
|
||||||
const record = selectors.getFileRecord(fileId);
|
if (record) {
|
||||||
if (record) {
|
inputFileIds.push(fileId);
|
||||||
inputFileIds.push(fileId);
|
inputStirlingFileStubs.push(record);
|
||||||
inputFileRecords.push(record);
|
|
||||||
} else {
|
|
||||||
console.warn(`No file record found for file: ${file.name}`);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.warn(`No file ID found for file: ${file.name}`);
|
console.warn(`No file stub found for file: ${file.name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,24 +260,22 @@ export const useToolOperation = <TParams>(
|
|||||||
|
|
||||||
// Store operation data for undo (only store what we need to avoid memory bloat)
|
// Store operation data for undo (only store what we need to avoid memory bloat)
|
||||||
lastOperationRef.current = {
|
lastOperationRef.current = {
|
||||||
inputFiles: validFiles, // Keep original File objects for undo
|
inputFiles: extractFiles(validFiles), // Convert to File objects for undo
|
||||||
inputFileRecords: inputFileRecords.map(record => ({ ...record })), // Deep copy to avoid reference issues
|
inputStirlingFileStubs: inputStirlingFileStubs.map(record => ({ ...record })), // Deep copy to avoid reference issues
|
||||||
outputFileIds
|
outputFileIds
|
||||||
};
|
};
|
||||||
|
|
||||||
markOperationApplied(fileId, operationId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const errorMessage = config.getErrorMessage?.(error) || extractErrorMessage(error);
|
const errorMessage = config.getErrorMessage?.(error) || extractErrorMessage(error);
|
||||||
actions.setError(errorMessage);
|
actions.setError(errorMessage);
|
||||||
actions.setStatus('');
|
actions.setStatus('');
|
||||||
markOperationFailed(fileId, operationId, errorMessage);
|
|
||||||
} finally {
|
} finally {
|
||||||
actions.setLoading(false);
|
actions.setLoading(false);
|
||||||
actions.setProgress(null);
|
actions.setProgress(null);
|
||||||
}
|
}
|
||||||
}, [t, config, actions, recordOperation, markOperationApplied, markOperationFailed, addFiles, consumeFiles, findFileId, processFiles, generateThumbnails, createDownloadInfo, cleanupBlobUrls, extractZipFiles, extractAllZipFiles]);
|
}, [t, config, actions, addFiles, consumeFiles, processFiles, generateThumbnails, createDownloadInfo, cleanupBlobUrls, extractZipFiles, extractAllZipFiles]);
|
||||||
|
|
||||||
const cancelOperation = useCallback(() => {
|
const cancelOperation = useCallback(() => {
|
||||||
cancelApiCalls();
|
cancelApiCalls();
|
||||||
@ -310,10 +304,10 @@ export const useToolOperation = <TParams>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { inputFiles, inputFileRecords, outputFileIds } = lastOperationRef.current;
|
const { inputFiles, inputStirlingFileStubs, outputFileIds } = lastOperationRef.current;
|
||||||
|
|
||||||
// Validate that we have data to undo
|
// Validate that we have data to undo
|
||||||
if (inputFiles.length === 0 || inputFileRecords.length === 0) {
|
if (inputFiles.length === 0 || inputStirlingFileStubs.length === 0) {
|
||||||
actions.setError(t('invalidUndoData', 'Cannot undo: invalid operation data'));
|
actions.setError(t('invalidUndoData', 'Cannot undo: invalid operation data'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -325,7 +319,8 @@ export const useToolOperation = <TParams>(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Undo the consume operation
|
// Undo the consume operation
|
||||||
await undoConsumeFiles(inputFiles, inputFileRecords, outputFileIds);
|
await undoConsumeFiles(inputFiles, inputStirlingFileStubs, outputFileIds);
|
||||||
|
|
||||||
|
|
||||||
// Clear results and operation tracking
|
// Clear results and operation tracking
|
||||||
resetResults();
|
resetResults();
|
||||||
|
@ -4,7 +4,7 @@ import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
|||||||
import { SingleLargePageParameters, defaultParameters } from './useSingleLargePageParameters';
|
import { SingleLargePageParameters, defaultParameters } from './useSingleLargePageParameters';
|
||||||
|
|
||||||
// Static function that can be used by both the hook and automation executor
|
// Static function that can be used by both the hook and automation executor
|
||||||
export const buildSingleLargePageFormData = (parameters: SingleLargePageParameters, file: File): FormData => {
|
export const buildSingleLargePageFormData = (_parameters: SingleLargePageParameters, file: File): FormData => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("fileInput", file);
|
formData.append("fileInput", file);
|
||||||
return formData;
|
return formData;
|
||||||
|
@ -71,7 +71,7 @@ export const useSplitOperation = () => {
|
|||||||
|
|
||||||
// Custom response handler that extracts ZIP files
|
// Custom response handler that extracts ZIP files
|
||||||
// Can't add to exported config because it requires access to the zip code so must be part of the hook
|
// Can't add to exported config because it requires access to the zip code so must be part of the hook
|
||||||
const responseHandler = useCallback(async (blob: Blob, originalFiles: File[]): Promise<File[]> => {
|
const responseHandler = useCallback(async (blob: Blob, _originalFiles: File[]): Promise<File[]> => {
|
||||||
// Split operations return ZIP files with multiple PDF pages
|
// Split operations return ZIP files with multiple PDF pages
|
||||||
return await extractZipFiles(blob);
|
return await extractZipFiles(blob);
|
||||||
}, [extractZipFiles]);
|
}, [extractZipFiles]);
|
||||||
|
@ -4,7 +4,7 @@ import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
|||||||
import { UnlockPdfFormsParameters, defaultParameters } from './useUnlockPdfFormsParameters';
|
import { UnlockPdfFormsParameters, defaultParameters } from './useUnlockPdfFormsParameters';
|
||||||
|
|
||||||
// Static function that can be used by both the hook and automation executor
|
// Static function that can be used by both the hook and automation executor
|
||||||
export const buildUnlockPdfFormsFormData = (parameters: UnlockPdfFormsParameters, file: File): FormData => {
|
export const buildUnlockPdfFormsFormData = (_parameters: UnlockPdfFormsParameters, file: File): FormData => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("fileInput", file);
|
formData.append("fileInput", file);
|
||||||
return formData;
|
return formData;
|
||||||
|
@ -184,11 +184,6 @@ export const useCookieConsent = ({ analyticsEnabled = false }: CookieConsentConf
|
|||||||
// Force show after initialization
|
// Force show after initialization
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.CookieConsent.show();
|
window.CookieConsent.show();
|
||||||
|
|
||||||
// Debug: Check if modal elements exist
|
|
||||||
const ccMain = document.getElementById('cc-main');
|
|
||||||
const consentModal = document.querySelector('.cm-wrapper');
|
|
||||||
|
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -105,7 +105,6 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const endpointsKey = endpoints.join(',');
|
|
||||||
fetchAllEndpointStatuses();
|
fetchAllEndpointStatuses();
|
||||||
}, [endpoints.join(',')]); // Re-run when endpoints array changes
|
}, [endpoints.join(',')]); // Re-run when endpoints array changes
|
||||||
|
|
||||||
|
@ -135,7 +135,7 @@ export function useEnhancedProcessedFiles(
|
|||||||
updatedFiles.set(file, processed);
|
updatedFiles.set(file, processed);
|
||||||
hasNewFiles = true;
|
hasNewFiles = true;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Ignore errors in completion check
|
// Ignore errors in completion check
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { useIndexedDB } from '../contexts/IndexedDBContext';
|
import { useIndexedDB } from '../contexts/IndexedDBContext';
|
||||||
import { FileMetadata } from '../types/file';
|
import { FileMetadata } from '../types/file';
|
||||||
import { generateThumbnailForFile } from '../utils/thumbnailUtils';
|
import { FileId } from '../types/fileContext';
|
||||||
import { FileId } from '../types/file';
|
|
||||||
|
|
||||||
export const useFileManager = () => {
|
export const useFileManager = () => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { isFileObject } from '../types/fileContext';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook to convert a File object to { file: File; url: string } format
|
* Hook to convert a File object to { file: File; url: string } format
|
||||||
@ -8,8 +9,8 @@ export function useFileWithUrl(file: File | Blob | null): { file: File | Blob; u
|
|||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (!file) return null;
|
if (!file) return null;
|
||||||
|
|
||||||
// Validate that file is a proper File or Blob object
|
// Validate that file is a proper File, StirlingFile, or Blob object
|
||||||
if (!(file instanceof File) && !(file instanceof Blob)) {
|
if (!isFileObject(file) && !(file instanceof Blob)) {
|
||||||
console.warn('useFileWithUrl: Expected File or Blob, got:', file);
|
console.warn('useFileWithUrl: Expected File or Blob, got:', file);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -2,21 +2,8 @@ import { useState, useEffect } from "react";
|
|||||||
import { FileMetadata } from "../types/file";
|
import { FileMetadata } from "../types/file";
|
||||||
import { useIndexedDB } from "../contexts/IndexedDBContext";
|
import { useIndexedDB } from "../contexts/IndexedDBContext";
|
||||||
import { generateThumbnailForFile } from "../utils/thumbnailUtils";
|
import { generateThumbnailForFile } from "../utils/thumbnailUtils";
|
||||||
|
import { FileId } from "../types/fileContext";
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate optimal scale for thumbnail generation
|
|
||||||
* Ensures high quality while preventing oversized renders
|
|
||||||
*/
|
|
||||||
function calculateThumbnailScale(pageViewport: { width: number; height: number }): number {
|
|
||||||
const maxWidth = 400; // Max thumbnail width
|
|
||||||
const maxHeight = 600; // Max thumbnail height
|
|
||||||
|
|
||||||
const scaleX = maxWidth / pageViewport.width;
|
|
||||||
const scaleY = maxHeight / pageViewport.height;
|
|
||||||
|
|
||||||
// Don't upscale, only downscale if needed
|
|
||||||
return Math.min(scaleX, scaleY, 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook for IndexedDB-aware thumbnail loading
|
* Hook for IndexedDB-aware thumbnail loading
|
||||||
@ -53,7 +40,7 @@ export function useIndexedDBThumbnail(file: FileMetadata | undefined | null): {
|
|||||||
|
|
||||||
// Try to load file from IndexedDB using new context
|
// Try to load file from IndexedDB using new context
|
||||||
if (file.id && indexedDB) {
|
if (file.id && indexedDB) {
|
||||||
const loadedFile = await indexedDB.loadFile(file.id);
|
const loadedFile = await indexedDB.loadFile(file.id as FileId);
|
||||||
if (!loadedFile) {
|
if (!loadedFile) {
|
||||||
throw new Error('File not found in IndexedDB');
|
throw new Error('File not found in IndexedDB');
|
||||||
}
|
}
|
||||||
@ -70,7 +57,7 @@ export function useIndexedDBThumbnail(file: FileMetadata | undefined | null): {
|
|||||||
// Save thumbnail to IndexedDB for persistence
|
// Save thumbnail to IndexedDB for persistence
|
||||||
if (file.id && indexedDB && thumbnail) {
|
if (file.id && indexedDB && thumbnail) {
|
||||||
try {
|
try {
|
||||||
await indexedDB.updateThumbnail(file.id, thumbnail);
|
await indexedDB.updateThumbnail(file.id as FileId, thumbnail);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to save thumbnail to IndexedDB:', error);
|
console.warn('Failed to save thumbnail to IndexedDB:', error);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { PDFDocument, PDFPage } from '../types/pageEditor';
|
import { PDFDocument, PDFPage } from '../types/pageEditor';
|
||||||
import { pdfWorkerManager } from '../services/pdfWorkerManager';
|
import { pdfWorkerManager } from '../services/pdfWorkerManager';
|
||||||
|
import { createQuickKey } from '../types/fileContext';
|
||||||
|
|
||||||
export function usePDFProcessor() {
|
export function usePDFProcessor() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@ -75,7 +76,7 @@ export function usePDFProcessor() {
|
|||||||
// Create pages without thumbnails initially - load them lazily
|
// Create pages without thumbnails initially - load them lazily
|
||||||
for (let i = 1; i <= totalPages; i++) {
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
pages.push({
|
pages.push({
|
||||||
id: `${file.name}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
originalPageNumber: i,
|
originalPageNumber: i,
|
||||||
thumbnail: null, // Will be loaded lazily
|
thumbnail: null, // Will be loaded lazily
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
|
||||||
import { pdfWorkerManager } from '../services/pdfWorkerManager';
|
import { pdfWorkerManager } from '../services/pdfWorkerManager';
|
||||||
|
import { StirlingFile } from '../types/fileContext';
|
||||||
|
|
||||||
export interface PdfSignatureDetectionResult {
|
export interface PdfSignatureDetectionResult {
|
||||||
hasDigitalSignatures: boolean;
|
hasDigitalSignatures: boolean;
|
||||||
isChecking: boolean;
|
isChecking: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const usePdfSignatureDetection = (files: File[]): PdfSignatureDetectionResult => {
|
export const usePdfSignatureDetection = (files: StirlingFile[]): PdfSignatureDetectionResult => {
|
||||||
const [hasDigitalSignatures, setHasDigitalSignatures] = useState(false);
|
const [hasDigitalSignatures, setHasDigitalSignatures] = useState(false);
|
||||||
const [isChecking, setIsChecking] = useState(false);
|
const [isChecking, setIsChecking] = useState(false);
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useRef } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { thumbnailGenerationService } from '../services/thumbnailGenerationService';
|
import { thumbnailGenerationService } from '../services/thumbnailGenerationService';
|
||||||
|
import { createQuickKey } from '../types/fileContext';
|
||||||
import { FileId } from '../types/file';
|
import { FileId } from '../types/file';
|
||||||
|
|
||||||
// Request queue to handle concurrent thumbnail requests
|
// Request queue to handle concurrent thumbnail requests
|
||||||
@ -71,8 +72,8 @@ async function processRequestQueue() {
|
|||||||
|
|
||||||
console.log(`📸 Batch generating ${requests.length} thumbnails for pages: ${pageNumbers.slice(0, 5).join(', ')}${pageNumbers.length > 5 ? '...' : ''}`);
|
console.log(`📸 Batch generating ${requests.length} thumbnails for pages: ${pageNumbers.slice(0, 5).join(', ')}${pageNumbers.length > 5 ? '...' : ''}`);
|
||||||
|
|
||||||
// Use file name as fileId for PDF document caching
|
// Use quickKey for PDF document caching (same metadata, consistent format)
|
||||||
const fileId = file.name + '_' + file.size + '_' + file.lastModified as FileId;
|
const fileId = createQuickKey(file) as FileId;
|
||||||
|
|
||||||
const results = await thumbnailGenerationService.generateThumbnails(
|
const results = await thumbnailGenerationService.generateThumbnails(
|
||||||
fileId,
|
fileId,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
import { useState, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
|
import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
|
||||||
import { getAllEndpoints, type ToolRegistryEntry } from "../data/toolsTaxonomy";
|
import { getAllEndpoints, type ToolRegistryEntry } from "../data/toolsTaxonomy";
|
||||||
@ -20,15 +20,6 @@ export const useToolManagement = (): ToolManagementResult => {
|
|||||||
|
|
||||||
// Build endpoints list from registry entries with fallback to legacy mapping
|
// Build endpoints list from registry entries with fallback to legacy mapping
|
||||||
const baseRegistry = useFlatToolRegistry();
|
const baseRegistry = useFlatToolRegistry();
|
||||||
const registryDerivedEndpoints = useMemo(() => {
|
|
||||||
const endpointsByTool: Record<string, string[]> = {};
|
|
||||||
Object.entries(baseRegistry).forEach(([key, entry]) => {
|
|
||||||
if (entry.endpoints && entry.endpoints.length > 0) {
|
|
||||||
endpointsByTool[key] = entry.endpoints;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return endpointsByTool;
|
|
||||||
}, [baseRegistry]);
|
|
||||||
|
|
||||||
const allEndpoints = useMemo(() => getAllEndpoints(baseRegistry), [baseRegistry]);
|
const allEndpoints = useMemo(() => getAllEndpoints(baseRegistry), [baseRegistry]);
|
||||||
const { endpointStatus, loading: endpointsLoading } = useMultipleEndpointsEnabled(allEndpoints);
|
const { endpointStatus, loading: endpointsLoading } = useMultipleEndpointsEnabled(allEndpoints);
|
||||||
|
@ -10,8 +10,8 @@ type ToolParameterValues = Record<string, any>;
|
|||||||
* Register tool parameters and get current values
|
* Register tool parameters and get current values
|
||||||
*/
|
*/
|
||||||
export function useToolParameters(
|
export function useToolParameters(
|
||||||
toolName: string,
|
_toolName: string,
|
||||||
parameters: Record<string, any>
|
_parameters: Record<string, any>
|
||||||
): [ToolParameterValues, (updates: Partial<ToolParameterValues>) => void] {
|
): [ToolParameterValues, (updates: Partial<ToolParameterValues>) => void] {
|
||||||
|
|
||||||
// Return empty values and noop updater
|
// Return empty values and noop updater
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { clamp } from '../utils/genericUtils';
|
import { clamp } from '../utils/genericUtils';
|
||||||
import { getSidebarInfo } from '../utils/sidebarUtils';
|
import { getSidebarInfo } from '../utils/sidebarUtils';
|
||||||
import { SidebarRefs, SidebarState } from '../types/sidebar';
|
import { SidebarRefs, SidebarState } from '../types/sidebar';
|
||||||
|
@ -35,8 +35,11 @@ function updatePosthogConsent(){
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const optIn = (window.CookieConsent as any).acceptedCategory('analytics');
|
const optIn = (window.CookieConsent as any).acceptedCategory('analytics');
|
||||||
optIn?
|
if (optIn) {
|
||||||
posthog.opt_in_capturing() : posthog.opt_out_capturing();
|
posthog.opt_in_capturing();
|
||||||
|
} else {
|
||||||
|
posthog.opt_out_capturing();
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Updated analytics consent: ", optIn? "opted in" : "opted out");
|
console.log("Updated analytics consent: ", optIn? "opted in" : "opted out");
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import React, { useEffect } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useToolWorkflow } from "../contexts/ToolWorkflowContext";
|
import { useToolWorkflow } from "../contexts/ToolWorkflowContext";
|
||||||
import { Group } from "@mantine/core";
|
import { Group } from "@mantine/core";
|
||||||
@ -11,7 +10,6 @@ import Workbench from "../components/layout/Workbench";
|
|||||||
import QuickAccessBar from "../components/shared/QuickAccessBar";
|
import QuickAccessBar from "../components/shared/QuickAccessBar";
|
||||||
import RightRail from "../components/shared/RightRail";
|
import RightRail from "../components/shared/RightRail";
|
||||||
import FileManager from "../components/FileManager";
|
import FileManager from "../components/FileManager";
|
||||||
import Footer from "../components/shared/Footer";
|
|
||||||
|
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import { ProcessedFile, ProcessingState, PDFPage, ProcessingConfig, ProcessingMetrics } from '../types/processing';
|
||||||
import { ProcessedFile, ProcessingState, PDFPage, ProcessingStrategy, ProcessingConfig, ProcessingMetrics } from '../types/processing';
|
|
||||||
import { ProcessingCache } from './processingCache';
|
import { ProcessingCache } from './processingCache';
|
||||||
import { FileHasher } from '../utils/fileHash';
|
import { FileHasher } from '../utils/fileHash';
|
||||||
import { FileAnalyzer } from './fileAnalyzer';
|
import { FileAnalyzer } from './fileAnalyzer';
|
||||||
import { ProcessingErrorHandler } from './processingErrorHandler';
|
import { ProcessingErrorHandler } from './processingErrorHandler';
|
||||||
import { pdfWorkerManager } from './pdfWorkerManager';
|
import { pdfWorkerManager } from './pdfWorkerManager';
|
||||||
|
import { createQuickKey } from '../types/fileContext';
|
||||||
|
|
||||||
export class EnhancedPDFProcessingService {
|
export class EnhancedPDFProcessingService {
|
||||||
private static instance: EnhancedPDFProcessingService;
|
private static instance: EnhancedPDFProcessingService;
|
||||||
@ -201,7 +201,7 @@ export class EnhancedPDFProcessingService {
|
|||||||
const thumbnail = await this.renderPageThumbnail(page, config.thumbnailQuality);
|
const thumbnail = await this.renderPageThumbnail(page, config.thumbnailQuality);
|
||||||
|
|
||||||
pages.push({
|
pages.push({
|
||||||
id: `${file.name}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
@ -251,7 +251,7 @@ export class EnhancedPDFProcessingService {
|
|||||||
const thumbnail = await this.renderPageThumbnail(page, config.thumbnailQuality);
|
const thumbnail = await this.renderPageThumbnail(page, config.thumbnailQuality);
|
||||||
|
|
||||||
pages.push({
|
pages.push({
|
||||||
id: `${file.name}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
@ -266,7 +266,7 @@ export class EnhancedPDFProcessingService {
|
|||||||
// Create placeholder pages for remaining pages
|
// Create placeholder pages for remaining pages
|
||||||
for (let i = priorityCount + 1; i <= totalPages; i++) {
|
for (let i = priorityCount + 1; i <= totalPages; i++) {
|
||||||
pages.push({
|
pages.push({
|
||||||
id: `${file.name}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
thumbnail: null, // Will be loaded lazily
|
thumbnail: null, // Will be loaded lazily
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
@ -313,7 +313,7 @@ export class EnhancedPDFProcessingService {
|
|||||||
const thumbnail = await this.renderPageThumbnail(page, config.thumbnailQuality);
|
const thumbnail = await this.renderPageThumbnail(page, config.thumbnailQuality);
|
||||||
|
|
||||||
pages.push({
|
pages.push({
|
||||||
id: `${file.name}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
@ -334,7 +334,7 @@ export class EnhancedPDFProcessingService {
|
|||||||
// Create placeholders for remaining pages
|
// Create placeholders for remaining pages
|
||||||
for (let i = firstChunkEnd + 1; i <= totalPages; i++) {
|
for (let i = firstChunkEnd + 1; i <= totalPages; i++) {
|
||||||
pages.push({
|
pages.push({
|
||||||
id: `${file.name}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
thumbnail: null,
|
thumbnail: null,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
@ -354,7 +354,7 @@ export class EnhancedPDFProcessingService {
|
|||||||
*/
|
*/
|
||||||
private async processMetadataOnly(
|
private async processMetadataOnly(
|
||||||
file: File,
|
file: File,
|
||||||
config: ProcessingConfig,
|
_config: ProcessingConfig,
|
||||||
state: ProcessingState
|
state: ProcessingState
|
||||||
): Promise<ProcessedFile> {
|
): Promise<ProcessedFile> {
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
@ -368,7 +368,7 @@ export class EnhancedPDFProcessingService {
|
|||||||
const pages: PDFPage[] = [];
|
const pages: PDFPage[] = [];
|
||||||
for (let i = 1; i <= totalPages; i++) {
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
pages.push({
|
pages.push({
|
||||||
id: `${file.name}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
thumbnail: null,
|
thumbnail: null,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
@ -459,11 +459,12 @@ export class EnhancedPDFProcessingService {
|
|||||||
case 'failed':
|
case 'failed':
|
||||||
this.metrics.failedFiles++;
|
this.metrics.failedFiles++;
|
||||||
break;
|
break;
|
||||||
case 'cacheHit':
|
case 'cacheHit': {
|
||||||
// Update cache hit rate
|
// Update cache hit rate
|
||||||
const totalAttempts = this.metrics.totalFiles + 1;
|
const totalAttempts = this.metrics.totalFiles + 1;
|
||||||
this.metrics.cacheHitRate = (this.metrics.cacheHitRate * this.metrics.totalFiles + 1) / totalAttempts;
|
this.metrics.cacheHitRate = (this.metrics.cacheHitRate * this.metrics.totalFiles + 1) / totalAttempts;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -508,7 +509,7 @@ export class EnhancedPDFProcessingService {
|
|||||||
*/
|
*/
|
||||||
clearAllProcessing(): void {
|
clearAllProcessing(): void {
|
||||||
// Cancel all ongoing processing
|
// Cancel all ongoing processing
|
||||||
this.processing.forEach((state, key) => {
|
this.processing.forEach((state) => {
|
||||||
if (state.cancellationToken) {
|
if (state.cancellationToken) {
|
||||||
state.cancellationToken.abort();
|
state.cancellationToken.abort();
|
||||||
}
|
}
|
||||||
@ -519,10 +520,7 @@ export class EnhancedPDFProcessingService {
|
|||||||
this.notifyListeners();
|
this.notifyListeners();
|
||||||
|
|
||||||
// Force memory cleanup hint
|
// Force memory cleanup hint
|
||||||
if (typeof window !== 'undefined' && window.gc) {
|
setTimeout(() => window.gc?.(), 100);
|
||||||
let gc = window.gc;
|
|
||||||
setTimeout(() => gc(), 100);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -128,7 +128,7 @@ export class FileAnalyzer {
|
|||||||
* Estimate processing time based on file characteristics and strategy
|
* Estimate processing time based on file characteristics and strategy
|
||||||
*/
|
*/
|
||||||
private static estimateProcessingTime(
|
private static estimateProcessingTime(
|
||||||
fileSize: number,
|
_fileSize: number,
|
||||||
pageCount: number = 0,
|
pageCount: number = 0,
|
||||||
strategy: ProcessingStrategy
|
strategy: ProcessingStrategy
|
||||||
): number {
|
): number {
|
||||||
@ -148,15 +148,17 @@ export class FileAnalyzer {
|
|||||||
case 'immediate_full':
|
case 'immediate_full':
|
||||||
return pageCount * baseTime;
|
return pageCount * baseTime;
|
||||||
|
|
||||||
case 'priority_pages':
|
case 'priority_pages': {
|
||||||
// Estimate time for priority pages (first 10)
|
// Estimate time for priority pages (first 10)
|
||||||
const priorityPages = Math.min(pageCount, 10);
|
const priorityPages = Math.min(pageCount, 10);
|
||||||
return priorityPages * baseTime;
|
return priorityPages * baseTime;
|
||||||
|
}
|
||||||
|
|
||||||
case 'progressive_chunked':
|
case 'progressive_chunked': {
|
||||||
// Estimate time for first chunk (20 pages)
|
// Estimate time for first chunk (20 pages)
|
||||||
const firstChunk = Math.min(pageCount, 20);
|
const firstChunk = Math.min(pageCount, 20);
|
||||||
return firstChunk * baseTime;
|
return firstChunk * baseTime;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return pageCount * baseTime;
|
return pageCount * baseTime;
|
||||||
@ -232,7 +234,7 @@ export class FileAnalyzer {
|
|||||||
const headerString = String.fromCharCode(...headerBytes);
|
const headerString = String.fromCharCode(...headerBytes);
|
||||||
|
|
||||||
return headerString.startsWith('%PDF-');
|
return headerString.startsWith('%PDF-');
|
||||||
} catch (error) {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user