feat: implement mobile reader

- implement mobile epub reader
This commit is contained in:
2026-06-20 13:33:44 -05:00
parent 84c0497f21
commit 22ded87250
41 changed files with 3184 additions and 1111 deletions
+150
View File
@@ -0,0 +1,150 @@
# 010 — Mobile EPUB Reader
**Status:** Implemented
**Created:** 2026-06-04
## Objective
Mirror the web reading experience (`frontend/src/pages/ReadingPage.tsx` and
`frontend/src/components/reader/*`) inside the Expo app so users can open their
uploaded library, read EPUBs with persisted progress and typography settings,
and manage bookmarks/highlights — reusing the same Django REST API as the web
client.
The web EPUB renderer (`react-reader` / epub.js) is DOM-only, so mobile renders
EPUBs with `@epubjs-react-native/core`, which runs epub.js inside a
`react-native-webview`. This keeps behavior (CFI locations, themes, font
controls, TOC, annotations) close to web while staying inside the managed Expo
workflow.
## Scope
### Mirrored from web
- EPUB rendering with swipe pagination (`flow: "paginated"`).
- Resume position and debounced progress save (CFI + percentage).
- Table of contents drawer with jump-to-chapter.
- Reading settings: theme presets (light/sepia/paper/dark), font family, font
size, line spacing — persisted to `/api/reader/settings/`.
- Bookmarks: bookmark the current page, list/jump/delete, and create highlights
from a text selection. Highlights are re-applied on open (best-effort).
### Deferred (not in this pass)
- PDF reading. PDF books show a placeholder pointing to the web reader.
- Brightness and orientation-lock controls.
- Per-highlight color picker (highlights use a single default color).
- App-wide internationalization (web uses `react-i18n-lite`).
## Architecture
```
LibraryScreen (EBook list)
| navigate("BookDetail", { ebookId })
v
BookDetailScreen ----------------> ReaderScreen ({ ebookId })
|
GET /api/books/ebooks/:id/ (format guard)
|
epub? --------------------- pdf? -> placeholder
|
EpubReaderView (ReaderProvider)
|
expo-file-system downloadAsync(file/, Bearer token) -> file:// uri
|
<Reader src=file:// fileSystem=useFileSystem flow="paginated" />
|
onLocationChange -> debounce 800ms -> PATCH progress/
onSelected -> POST bookmarks/ (+ highlight annotation)
useReader().toc -> goToLocation(href)
settings change -> changeTheme / changeFontSize / changeFontFamily
+ PATCH /api/reader/settings/ (debounced)
```
The ebook file is downloaded to the app cache with an `Authorization` header
(the `/file/` endpoint is JWT-protected) and the local `file://` URI is handed
to the renderer — mirroring how the web client downloads a blob rather than
using a public/signed URL.
## Mobile changes
| File | Role |
|------|------|
| `mobile/src/api/client.ts` | Adds `apiClient`, `saveTokens`, `loadTokens`, `getApiBaseUrl` |
| `mobile/src/api/ebooks.ts` | `/api/books/ebooks/` list/detail/toc + `getFileUrl(id)` |
| `mobile/src/api/reader.ts` | Reader settings + per-book progress (mirrors web `api/reader.ts`) |
| `mobile/src/api/annotations.ts` | Bookmarks CRUD against `/api/annotations/bookmarks/` |
| `mobile/src/types/reader.ts` | `ReadingSettings`, `ReadingProgress` (full reader shapes) |
| `mobile/src/types/index.ts` | `AppStackParamList`, `Bookmark`, `CreateMarkerPayload` |
| `mobile/src/hooks/useReadingSettings.ts` | Loads + debounced-saves reader settings |
| `mobile/src/utils/epubTheme.ts` | Font stacks, theme palettes, `buildEpubTheme()` |
| `mobile/src/navigation/AppStack.tsx` | Native stack: Tabs / BookDetail / Reader |
| `mobile/src/screens/LibraryScreen.tsx` | Lists user EBooks (`ebooksApi.list`) |
| `mobile/src/screens/BookDetailScreen.tsx` | Metadata + start/resume button |
| `mobile/src/screens/ReaderScreen.tsx` | Format guard: EPUB view vs PDF placeholder |
| `mobile/src/components/reader/EpubReaderView.tsx` | Reader, progress, bookmarks, settings wiring |
| `mobile/src/components/reader/ReaderToolbar.tsx` | Title, chapter, progress bar, action buttons |
| `mobile/src/components/reader/TocModal.tsx` | Table of contents sheet |
| `mobile/src/components/reader/ReadingSettingsModal.tsx` | Theme/font/size/spacing controls |
| `mobile/src/components/reader/BookmarksModal.tsx` | Bookmarks & highlights list |
| `mobile/App.tsx` | Wraps the tree in `GestureHandlerRootView` |
Removed unused scaffolding: `mobile/src/navigation/AppNavigator.tsx`,
`mobile/src/navigation/MainTabs.tsx`.
## API contracts (consumed)
| Method | Path | Purpose |
|--------|------|---------|
| GET | `/api/books/ebooks/` | User library list |
| GET | `/api/books/ebooks/:id/` | EBook detail (format, progress, cover) |
| GET | `/api/books/ebooks/:id/file/` | Stream EPUB bytes (JWT, owner) |
| GET/PATCH | `/api/books/ebooks/:id/progress/` | Reading progress (`current_position`, `last_page`, `epub_location`) |
| GET/PATCH | `/api/reader/settings/` | Reader typography/theme settings |
| GET/POST/DELETE | `/api/annotations/bookmarks/` | Bookmarks & highlights (`ebook`, `epub_cfi`, `chapter_index`, ...) |
## Settings mapping (web -> mobile)
| Reader setting | Web (epub.js) | Mobile (`@epubjs-react-native/core`) |
|----------------|---------------|--------------------------------------|
| `theme` / colors | `themes.register/select` | `changeTheme(buildEpubTheme())` + `defaultTheme` |
| `font_size` | `themes.fontSize` | `changeFontSize("Npx")` |
| `font_family` | body font-family | `changeFontFamily(stack)` |
| `line_height` | body line-height | `buildEpubTheme()` CSS rule |
| `margin_width` | gap-based padding | not applied (deferred) |
| `brightness` / `orientation_lock` | applied on web | deferred |
## Dependencies added
- `@epubjs-react-native/core@1.4.7`
- `@epubjs-react-native/expo-file-system@1.1.4`
- `react-native-webview@13.12.5`
(`react-native-gesture-handler`, `react-native-reanimated`, and
`expo-file-system` were already present.)
## Configuration
| Env var | Purpose |
|---------|---------|
| `EXPO_PUBLIC_API_URL` | Backend base URL (e.g. `http://10.0.2.2:8000` on Android emulator, LAN IP on a device) |
## Compatibility notes
- The Expo file-system adapter (`@epubjs-react-native/expo-file-system`) depends
only on `expo-file-system`, so the reader runs in Expo Go. (The library's
bare adapter pulls native `@dr.pogodin/react-native-fs`; that path is not
used here.)
- React 19 / Expo SDK 52 may surface peer-dependency warnings for the
`@epubjs-react-native/*` packages.
## Verification
- [ ] Log in; Library lists the user's uploaded EBooks with covers/progress.
- [ ] Open an EPUB; it renders and paginates by swipe.
- [ ] Reopen a book; it resumes at the last position.
- [ ] Change theme/font/size/spacing; the page updates and persists across reopen.
- [ ] Open the TOC and jump to a chapter.
- [ ] Bookmark the current page; it appears in the bookmarks list and can be re-opened/deleted.
- [ ] Select text to create a highlight; it persists and re-renders on reopen.
- [ ] Open a PDF book; the placeholder is shown instead of a crash.