Merge pull request #34 from richardr1126/apple-webkit

Fix Apple WebKit (iOS) PDF Upload
This commit is contained in:
Richard Roberson 2025-03-01 23:46:53 -07:00 committed by GitHub
commit a704fcbec6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 56 additions and 68 deletions

View file

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

View file

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

View file

@ -29,7 +29,6 @@ import { useTTS } from '@/contexts/TTSContext';
import { useConfig } from '@/contexts/ConfigContext';
import {
extractTextFromPDF,
convertPDFDataToURL,
highlightPattern,
clearHighlights,
handleTextClick,
@ -43,7 +42,7 @@ import { combineAudioChunks } from '@/utils/audio';
*/
interface PDFContextType {
// Current document state
currDocURL: string | undefined;
currDocData: ArrayBuffer | undefined;
currDocName: string | undefined;
currDocPages: number | undefined;
currDocPage: number;
@ -99,7 +98,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
} = useConfig();
// Current document state
const [currDocURL, setCurrDocURL] = useState<string>();
const [currDocData, setCurrDocData] = useState<ArrayBuffer>();
const [currDocName, setCurrDocName] = useState<string>();
const [currDocText, setCurrDocText] = useState<string>();
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
*/
useEffect(() => {
if (currDocURL) {
if (currDocData) {
loadCurrDocText();
}
}, [currDocPage, currDocURL, loadCurrDocText]);
}, [currDocPage, currDocData, loadCurrDocText]);
/**
* 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
* @returns {Promise<void>}
@ -163,12 +162,11 @@ export function PDFProvider({ children }: { children: ReactNode }) {
try {
const doc = await indexedDBService.getDocument(id);
if (doc) {
const url = await convertPDFDataToURL(doc.data);
setCurrDocName(doc.name);
setCurrDocURL(url);
setCurrDocData(doc.data);
}
} 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(() => {
setCurrDocName(undefined);
setCurrDocURL(undefined);
setCurrDocData(undefined);
setCurrDocText(undefined);
setCurrDocPages(undefined);
setPdfDocument(undefined);
@ -306,7 +304,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
() => ({
onDocumentLoadSuccess,
setCurrentDocument,
currDocURL,
currDocData,
currDocName,
currDocPages,
currDocPage,
@ -322,7 +320,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
[
onDocumentLoadSuccess,
setCurrentDocument,
currDocURL,
currDocData,
currDocName,
currDocPages,
currDocPage,

View file

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

View file

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

View file

@ -43,7 +43,7 @@ class IndexedDBService {
request.onupgradeneeded = (event) => {
console.log('Upgrading IndexedDB schema...');
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains(PDF_STORE_NAME)) {
console.log('Creating PDF documents store...');
db.createObjectStore(PDF_STORE_NAME, { keyPath: 'id' });
@ -75,7 +75,12 @@ class IndexedDBService {
console.log('Adding document to IndexedDB:', document.name);
const transaction = this.db!.transaction([PDF_STORE_NAME], 'readwrite');
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) => {
const error = (event.target as IDBRequest).error;
@ -165,7 +170,7 @@ class IndexedDBService {
// Create two transactions - one for document deletion, one for location cleanup
const docTransaction = this.db!.transaction([PDF_STORE_NAME], 'readwrite');
const configTransaction = this.db!.transaction([CONFIG_STORE_NAME], 'readwrite');
const docStore = docTransaction.objectStore(PDF_STORE_NAME);
const configStore = configTransaction.objectStore(CONFIG_STORE_NAME);
@ -221,7 +226,7 @@ class IndexedDBService {
const transaction = this.db!.transaction([EPUB_STORE_NAME], 'readwrite');
const store = transaction.objectStore(EPUB_STORE_NAME);
// Create a structured clone of the document to ensure proper storage
const request = store.put({
...document,
@ -315,7 +320,7 @@ class IndexedDBService {
// Create two transactions - one for document deletion, one for location cleanup
const docTransaction = this.db!.transaction([EPUB_STORE_NAME], 'readwrite');
const configTransaction = this.db!.transaction([CONFIG_STORE_NAME], 'readwrite');
const docStore = docTransaction.objectStore(EPUB_STORE_NAME);
const configStore = configTransaction.objectStore(CONFIG_STORE_NAME);
@ -477,17 +482,15 @@ class IndexedDBService {
async syncToServer(): Promise<{ lastSync: number }> {
const pdfDocs = await this.getAllDocuments();
const epubDocs = await this.getAllEPUBDocuments();
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) {
const arrayBuffer = await doc.data.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
documents.push({
...doc,
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({
...doc,
type: 'epub',
data: Array.from(new Uint8Array(doc.data)) // Convert to regular array for JSON serialization
data: Array.from(new Uint8Array(doc.data))
});
}
@ -520,31 +523,26 @@ class IndexedDBService {
}
const { documents } = await response.json();
// Process each document
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') {
// Create a Blob from the raw binary data
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
});
await this.addDocument(documentData);
} else if (doc.type === 'epub') {
// Convert the numeric array back to ArrayBuffer for EPUB
const uint8Array = new Uint8Array(doc.data);
await this.addEPUBDocument({
id: doc.id,
type: doc.type,
name: doc.name,
size: doc.size,
lastModified: doc.lastModified,
data: uint8Array.buffer
});
await this.addEPUBDocument(documentData);
} else {
console.warn(`Unknown document type: ${doc.type}`);
}
}

View file

@ -15,16 +15,6 @@ interface TextMatch {
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
export async function extractTextFromPDF(
pdf: PDFDocumentProxy,

View file

@ -11,7 +11,7 @@ async function uploadFile(page: Page, filePath: string) {
test.describe('Document Upload and Display', () => {
test.beforeEach(async ({ page }) => {
// 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
await page.getByText('Done').click();
@ -23,7 +23,7 @@ test.describe('Document Upload and Display', () => {
await uploadFile(page, './public/sample.pdf');
// 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 }) => {
@ -31,11 +31,11 @@ test.describe('Document Upload and Display', () => {
await uploadFile(page, './public/sample.pdf');
// Click on the uploaded document
await page.getByText('sample.pdf').click();
await page.getByText('sample.pdf').click({ timeout: 10000 });
// Verify PDF viewer is displayed
await expect(page.locator('.react-pdf__Document')).toBeVisible();
await expect(page.locator('.react-pdf__Page')).toBeVisible();
await expect(page.locator('.react-pdf__Document')).toBeVisible({ timeout: 10000 });
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');
// 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 }) => {
@ -53,10 +53,10 @@ test.describe('Document Upload and Display', () => {
await uploadFile(page, './public/sample.epub');
// Click on the uploaded document
await page.getByText('sample.epub').click();
await page.getByText('sample.epub').click({ timeout: 10000 });
// Verify EPUB viewer is displayed
await expect(page.locator('.epub-container')).toBeVisible();
await expect(page.locator('.epub-container')).toBeVisible({ timeout: 10000 });
});
});
});