Shared component of both the Petrarca Project and the Boccaccio Project — Android companion to Oculus
Oculus Mobile is the Android counterpart to the Electron desktop app Oculus: a document-scanning tool for turning photos of physical pages into usable images. It is built with React and Capacitor instead of Electron, targeting Android as a native app while sharing the same core editing pipeline (filters, crop, perspective straightening) as the desktop version.
This is a reduced-functionality counterpart to the desktop app, not a like-for-like port — see Differences from the desktop app for exactly what was left out and why.
- Architecture Overview
- Capture
- Manual Editor
- Export & Share
- Differences from the desktop app
- Tech Stack
- Project Structure
Oculus Mobile is a Capacitor app: a React single-page app running inside a native Android WebView shell, with a small set of native plugins bridging to platform APIs (camera/gallery, filesystem, share sheet). There is no separate main/renderer process split like on desktop — everything, including PDF/ZIP generation, runs client-side inside the WebView, and only file system access and native share/camera dialogs go through Capacitor's plugin bridge.
The app has the same three-view structure as the desktop version, switched directly in App.jsx with no router: Capture, Editor, and Export. State lives in the same kind of single Zustand store (useAppStore.js) — photo batch, selected photo, editor step, export selection — minus everything related to double-page splitting (see below).
The Capture page (CapturePage) combines two independent ways of getting a photo into the batch, plus an in-app rapid-fire mode:
- Native camera capture — via the
@capacitor/cameraplugin (useCamerahook), opening the device's own camera UI (full-screen, percapacitor.config.json) and returning a high-resolution photo (up to 4032×3024, with automatic orientation correction) straight into the batch. - Native gallery picker — the same plugin's
CameraSource.Photosmode, for adding existing photos from the device's gallery. - In-app batch camera — a
getUserMedia-based live camera view (useStreamCamera, shared with the desktop app's implementation) for rapidly capturing several pages in a row without leaving a single screen, with a quick flash feedback effect per shot and a running photo count badge. - Cache management — a "Clear cache" action wipes the app's native
CacheandDatastorage directories directly (via@capacitor/filesystem), for a real on-device cleanup rather than just clearing in-memory state. - Batch gallery — the same swipeable/paginated photo gallery component as desktop, with per-photo delete and entry points into the Editor and Export views.
The Editor (EditorPage) mirrors the desktop app's guided 4-step workflow — Filters → Crop → Straighten → Confirm — almost exactly, reusing the same underlying geometry engine (three-straighten.js) and the same step-by-step UX (auto-hiding header overlay, step indicator, prev/next photo navigation within the batch).
- Step 1 — Filters — brightness, contrast, saturation, sepia, invert, hue rotation, blur, and free rotation, baked into the image via canvas before moving on, identical to the desktop implementation.
- Step 2 — Crop — drag-to-resize crop rectangle with pixel-accurate mapping between displayed and natural-resolution pixels.
- Step 3 — Straighten — the same 8-point (4 corners + 4 edge midpoints) Three.js-based perspective correction as desktop, with the same page-size presets (A4/A5/Letter, portrait/landscape, or free/auto).
- Step 4 — Confirm — side-by-side original-vs-processed comparison, with discard or save-and-replace actions.
ExportPanel covers the same two export formats as desktop, but the whole export pipeline runs in the WebView and hands off to native APIs only for saving/sharing the result.
- Selective export — choose which batch photos to include, with select-all/deselect-all.
- Customizable file name — freely editable output file name, with automatic extension handling when switching between formats.
- ZIP export — sequentially numbered JPEG pages bundled into a ZIP, built client-side with
JSZip(rather than in a Node main process, as on desktop). - PDF export — one photo per page (A4/Letter, configurable margin, optional "fit to page"), built client-side with
jsPDF(rather thanpdfkit, as on desktop) — images only, no text/fonts involved. - Save to device storage — the exported file is written via
@capacitor/filesystemto external/public storage (falling back to the app's Documents directory), automatically finding a free numbered name (file (1).zip,file (2).zip, …) if a file with the same name already exists. - Native share sheet — after export, the saved file can be handed directly to Android's native share sheet via
@capacitor/share, using the file's nativefile://URI. - Progress feedback — a live progress indicator during export, mirroring the desktop app's UX.
Oculus Mobile intentionally ships a subset of the desktop app's functionality, favoring a simpler, phone-friendly workflow over full feature parity:
- No OpenCV auto-processing pipeline. The desktop app's automatic page-edge detection, one-tap perspective correction, and guided auto-process review flow are not present here — every photo goes through the manual Filters → Crop → Straighten workflow.
- No double-page-spread support. There is no "double page" capture toggle, no split-line detection, and no ability to turn one photo of an open book/spread into two separate corrected pages — the crop/straighten geometry engine (
three-straighten.js) ships without its splitting logic entirely. - No PDF import. Extracting images out of an existing PDF to seed the batch (available on desktop) isn't available; only the camera and the native gallery can add photos.
- No camera-device picker. Since Android exposes camera selection through its own native camera UI, there's no in-app equivalent of the desktop app's video-input-device selection modal.
- Client-side PDF/ZIP generation. Desktop delegates PDF (
pdfkit) and ZIP (JSZip) generation to the Electron main process; the mobile app runs the equivalent generation (jsPDF+JSZip) entirely inside the WebView, since there's no separate native process to hand the work to. - Native-storage-aware cache clearing. Unlike desktop, mobile storage is sandboxed per-app, so a dedicated "Clear cache" action was added to let users reclaim space directly from within the app.
- Application shell: Capacitor (
@capacitor/core,@capacitor/android), Vite - UI framework: React 19, Zustand (state management)
- Native device access:
@capacitor/camera(camera & gallery),@capacitor/filesystem(storage read/write, cache clearing),@capacitor/share(native share sheet) - 3D/graphics engine: Three.js — shared 8-point perspective-correction engine with the desktop app
- PDF generation:
jsPDF(client-side, WebView) - Archiving:
JSZip(client-side, WebView)
Oculus-mobile/
├── capacitor.config.json # App id, web dir, Android camera presentation style
├── index.html
├── src/
│ ├── App.jsx # View switcher: capture / editor / export
│ ├── main.jsx
│ ├── store/
│ │ └── useAppStore.js # Zustand store: photos, editor step, export selection
│ ├── hooks/
│ │ ├── useCamera.js # Capacitor Camera plugin: native capture + gallery picker
│ │ ├── useStreamCamera.js # getUserMedia in-app batch camera (shared logic w/ desktop)
│ │ ├── useFilters.js
│ │ └── useStraighten.js
│ ├── lib/
│ │ ├── three-straighten.js # Filters baking, crop, perspective warp, page presets (no split)
│ │ ├── pdf-export.js # Photo batch → PDF (jsPDF, client-side)
│ │ ├── zip-export.js # Photo batch → ZIP (JSZip, client-side)
│ │ └── share-utils.js # saveToDownloads / shareFile (Capacitor Filesystem + Share, w/ web fallback)
│ ├── pages/
│ │ ├── CapturePage.jsx # Camera + gallery + cache clearing
│ │ └── EditorPage.jsx # Guided 4-step manual editing workflow
│ └── components/
│ ├── CameraCapture/ # Native camera/gallery buttons, in-app batch camera trigger
│ ├── BatchCamera/ # Rapid-fire in-app batch capture modal
│ ├── PhotoGallery/ # Swipeable batch gallery
│ ├── FilterPanel/ # Step 1: image filters
│ ├── CropTool/ # Step 2: crop
│ ├── Straightener/ # Step 3: 8-point perspective correction
│ └── ExportPanel/ # ZIP/PDF export, save & share
└── package.json