Fix Apple WebKit PDF Upload

This commit is contained in:
Richard Roberson 2025-03-01 23:36:25 -07:00
parent 28a3804843
commit 136fdb9652
8 changed files with 56 additions and 68 deletions

View file

@ -5,7 +5,7 @@ on:
pull_request: pull_request:
branches: [ main, master ] branches: [ main, master ]
jobs: jobs:
test: e2e-testing:
timeout-minutes: 60 timeout-minutes: 60
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

View file

@ -36,7 +36,7 @@ export function PDFViewer({ zoomLevel }: PDFViewerProps) {
clearHighlights, clearHighlights,
handleTextClick, handleTextClick,
onDocumentLoadSuccess, onDocumentLoadSuccess,
currDocURL, currDocData,
currDocPages, currDocPages,
currDocText, currDocText,
currDocPage, currDocPage,
@ -163,7 +163,7 @@ export function PDFViewer({ zoomLevel }: PDFViewerProps) {
<Document <Document
loading={<DocumentSkeleton />} loading={<DocumentSkeleton />}
noData={<DocumentSkeleton />} noData={<DocumentSkeleton />}
file={currDocURL} file={currDocData}
onLoadSuccess={(pdf) => { onLoadSuccess={(pdf) => {
onDocumentLoadSuccess(pdf); onDocumentLoadSuccess(pdf);
}} }}

View file

@ -29,7 +29,6 @@ import { useTTS } from '@/contexts/TTSContext';
import { useConfig } from '@/contexts/ConfigContext'; import { useConfig } from '@/contexts/ConfigContext';
import { import {
extractTextFromPDF, extractTextFromPDF,
convertPDFDataToURL,
highlightPattern, highlightPattern,
clearHighlights, clearHighlights,
handleTextClick, handleTextClick,
@ -43,7 +42,7 @@ import { combineAudioChunks } from '@/utils/audio';
*/ */
interface PDFContextType { interface PDFContextType {
// Current document state // Current document state
currDocURL: string | undefined; currDocData: ArrayBuffer | undefined;
currDocName: string | undefined; currDocName: string | undefined;
currDocPages: number | undefined; currDocPages: number | undefined;
currDocPage: number; currDocPage: number;
@ -99,7 +98,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
} = useConfig(); } = useConfig();
// Current document state // Current document state
const [currDocURL, setCurrDocURL] = useState<string>(); const [currDocData, setCurrDocData] = useState<ArrayBuffer>();
const [currDocName, setCurrDocName] = useState<string>(); const [currDocName, setCurrDocName] = useState<string>();
const [currDocText, setCurrDocText] = useState<string>(); const [currDocText, setCurrDocText] = useState<string>();
const [pdfDocument, setPdfDocument] = useState<PDFDocumentProxy>(); const [pdfDocument, setPdfDocument] = useState<PDFDocumentProxy>();
@ -147,14 +146,14 @@ export function PDFProvider({ children }: { children: ReactNode }) {
* Triggers text extraction and processing when either the document URL or page changes * Triggers text extraction and processing when either the document URL or page changes
*/ */
useEffect(() => { useEffect(() => {
if (currDocURL) { if (currDocData) {
loadCurrDocText(); loadCurrDocText();
} }
}, [currDocPage, currDocURL, loadCurrDocText]); }, [currDocPage, currDocData, loadCurrDocText]);
/** /**
* Sets the current document based on its ID * Sets the current document based on its ID
* Retrieves document from IndexedDB and converts it to a viewable URL * Retrieves document from IndexedDB
* *
* @param {string} id - The unique identifier of the document to set * @param {string} id - The unique identifier of the document to set
* @returns {Promise<void>} * @returns {Promise<void>}
@ -163,12 +162,11 @@ export function PDFProvider({ children }: { children: ReactNode }) {
try { try {
const doc = await indexedDBService.getDocument(id); const doc = await indexedDBService.getDocument(id);
if (doc) { if (doc) {
const url = await convertPDFDataToURL(doc.data);
setCurrDocName(doc.name); setCurrDocName(doc.name);
setCurrDocURL(url); setCurrDocData(doc.data);
} }
} catch (error) { } catch (error) {
console.error('Failed to get document URL:', error); console.error('Failed to get document:', error);
} }
}, []); }, []);
@ -178,7 +176,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
*/ */
const clearCurrDoc = useCallback(() => { const clearCurrDoc = useCallback(() => {
setCurrDocName(undefined); setCurrDocName(undefined);
setCurrDocURL(undefined); setCurrDocData(undefined);
setCurrDocText(undefined); setCurrDocText(undefined);
setCurrDocPages(undefined); setCurrDocPages(undefined);
setPdfDocument(undefined); setPdfDocument(undefined);
@ -306,7 +304,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
() => ({ () => ({
onDocumentLoadSuccess, onDocumentLoadSuccess,
setCurrentDocument, setCurrentDocument,
currDocURL, currDocData,
currDocName, currDocName,
currDocPages, currDocPages,
currDocPage, currDocPage,
@ -322,7 +320,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
[ [
onDocumentLoadSuccess, onDocumentLoadSuccess,
setCurrentDocument, setCurrentDocument,
currDocURL, currDocData,
currDocName, currDocName,
currDocPages, currDocPages,
currDocPage, currDocPage,

View file

@ -39,13 +39,15 @@ export function usePDFDocuments() {
*/ */
const addDocument = useCallback(async (file: File): Promise<string> => { const addDocument = useCallback(async (file: File): Promise<string> => {
const id = uuidv4(); const id = uuidv4();
const arrayBuffer = await file.arrayBuffer();
const newDoc: PDFDocument = { const newDoc: PDFDocument = {
id, id,
type: 'pdf', type: 'pdf',
name: file.name, name: file.name,
size: file.size, size: file.size,
lastModified: file.lastModified, lastModified: file.lastModified,
data: new Blob([file], { type: file.type }), data: arrayBuffer,
}; };
try { try {

View file

@ -11,7 +11,7 @@ export interface BaseDocument {
export interface PDFDocument extends BaseDocument { export interface PDFDocument extends BaseDocument {
type: 'pdf'; type: 'pdf';
data: Blob; data: ArrayBuffer;
} }
export interface EPUBDocument extends BaseDocument { export interface EPUBDocument extends BaseDocument {

View file

@ -75,7 +75,12 @@ class IndexedDBService {
console.log('Adding document to IndexedDB:', document.name); console.log('Adding document to IndexedDB:', document.name);
const transaction = this.db!.transaction([PDF_STORE_NAME], 'readwrite'); const transaction = this.db!.transaction([PDF_STORE_NAME], 'readwrite');
const store = transaction.objectStore(PDF_STORE_NAME); const store = transaction.objectStore(PDF_STORE_NAME);
const request = store.put(document);
// Create a structured clone of the document to ensure proper storage
const request = store.put({
...document,
data: document.data // Store ArrayBuffer directly
});
request.onerror = (event) => { request.onerror = (event) => {
const error = (event.target as IDBRequest).error; const error = (event.target as IDBRequest).error;
@ -480,14 +485,12 @@ class IndexedDBService {
const documents = []; const documents = [];
// Process PDF documents - store the raw PDF data // Process PDF documents - convert ArrayBuffer to array for JSON serialization
for (const doc of pdfDocs) { for (const doc of pdfDocs) {
const arrayBuffer = await doc.data.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
documents.push({ documents.push({
...doc, ...doc,
type: 'pdf', type: 'pdf',
data: Array.from(uint8Array) // Convert to regular array for JSON serialization data: Array.from(new Uint8Array(doc.data))
}); });
} }
@ -496,7 +499,7 @@ class IndexedDBService {
documents.push({ documents.push({
...doc, ...doc,
type: 'epub', type: 'epub',
data: Array.from(new Uint8Array(doc.data)) // Convert to regular array for JSON serialization data: Array.from(new Uint8Array(doc.data))
}); });
} }
@ -523,28 +526,23 @@ class IndexedDBService {
// Process each document // Process each document
for (const doc of documents) { for (const doc of documents) {
// Convert the numeric array back to ArrayBuffer
const uint8Array = new Uint8Array(doc.data);
const documentData = {
id: doc.id,
type: doc.type,
name: doc.name,
size: doc.size,
lastModified: doc.lastModified,
data: uint8Array.buffer
};
if (doc.type === 'pdf') { if (doc.type === 'pdf') {
// Create a Blob from the raw binary data await this.addDocument(documentData);
const blob = new Blob([new Uint8Array(doc.data)], { type: 'application/pdf' });
await this.addDocument({
id: doc.id,
type: doc.type,
name: doc.name,
size: doc.size,
lastModified: doc.lastModified,
data: blob
});
} else if (doc.type === 'epub') { } else if (doc.type === 'epub') {
// Convert the numeric array back to ArrayBuffer for EPUB await this.addEPUBDocument(documentData);
const uint8Array = new Uint8Array(doc.data); } else {
await this.addEPUBDocument({ console.warn(`Unknown document type: ${doc.type}`);
id: doc.id,
type: doc.type,
name: doc.name,
size: doc.size,
lastModified: doc.lastModified,
data: uint8Array.buffer
});
} }
} }

View file

@ -15,16 +15,6 @@ interface TextMatch {
lengthDiff: number; lengthDiff: number;
} }
// URL Conversion functions
export function convertPDFDataToURL(pdfData: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(pdfData);
});
}
// Text Processing functions // Text Processing functions
export async function extractTextFromPDF( export async function extractTextFromPDF(
pdf: PDFDocumentProxy, pdf: PDFDocumentProxy,

View file

@ -11,7 +11,7 @@ async function uploadFile(page: Page, filePath: string) {
test.describe('Document Upload and Display', () => { test.describe('Document Upload and Display', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
// Navigate to the home page before each test // Navigate to the home page before each test
await page.goto('http://localhost:3003'); await page.goto('/');
// Click the "done" button to dismiss the welcome message // Click the "done" button to dismiss the welcome message
await page.getByText('Done').click(); await page.getByText('Done').click();
@ -23,7 +23,7 @@ test.describe('Document Upload and Display', () => {
await uploadFile(page, './public/sample.pdf'); await uploadFile(page, './public/sample.pdf');
// Verify upload success // Verify upload success
await expect(page.getByText('sample.pdf')).toBeVisible(); await expect(page.getByText('sample.pdf')).toBeVisible({ timeout: 10000 });
}); });
test('should display a PDF document', async ({ page }) => { test('should display a PDF document', async ({ page }) => {
@ -31,11 +31,11 @@ test.describe('Document Upload and Display', () => {
await uploadFile(page, './public/sample.pdf'); await uploadFile(page, './public/sample.pdf');
// Click on the uploaded document // Click on the uploaded document
await page.getByText('sample.pdf').click(); await page.getByText('sample.pdf').click({ timeout: 10000 });
// Verify PDF viewer is displayed // Verify PDF viewer is displayed
await expect(page.locator('.react-pdf__Document')).toBeVisible(); await expect(page.locator('.react-pdf__Document')).toBeVisible({ timeout: 10000 });
await expect(page.locator('.react-pdf__Page')).toBeVisible(); await expect(page.locator('.react-pdf__Page')).toBeVisible({ timeout: 10000 });
}); });
}); });
@ -45,7 +45,7 @@ test.describe('Document Upload and Display', () => {
await uploadFile(page, './public/sample.epub'); await uploadFile(page, './public/sample.epub');
// Verify upload success // Verify upload success
await expect(page.getByText('sample.epub')).toBeVisible(); await expect(page.getByText('sample.epub')).toBeVisible({ timeout: 10000 });
}); });
test('should display an EPUB document', async ({ page }) => { test('should display an EPUB document', async ({ page }) => {
@ -53,10 +53,10 @@ test.describe('Document Upload and Display', () => {
await uploadFile(page, './public/sample.epub'); await uploadFile(page, './public/sample.epub');
// Click on the uploaded document // Click on the uploaded document
await page.getByText('sample.epub').click(); await page.getByText('sample.epub').click({ timeout: 10000 });
// Verify EPUB viewer is displayed // Verify EPUB viewer is displayed
await expect(page.locator('.epub-container')).toBeVisible(); await expect(page.locator('.epub-container')).toBeVisible({ timeout: 10000 });
}); });
}); });
}); });