Archived
feat: uv config other feats
- add uv configuration for the backend - update frontend to make auth work - add new auth endpoints - add bookmars feat - add reader feat
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
# 010 — Open Library metadata on import
|
||||
|
||||
**Status:** Implemented
|
||||
**Created:** 2026-06-03
|
||||
|
||||
## Objective
|
||||
|
||||
After a user uploads an EPUB/PDF (`POST /api/books/ebooks/`), enrich the `EBook` with metadata and a cover from [Open Library](https://openlibrary.org/developers/api), using only **title** and **author** from the upload form. Prefer **Spanish** editions when available; fall back to English/any.
|
||||
|
||||
Upload must **never fail** if Open Library is down or no match is found.
|
||||
|
||||
## Trigger
|
||||
|
||||
- **Automatic:** `EBookUploadSerializer.create()` calls `enrich_ebook_metadata(ebook)` after file save.
|
||||
- **Manual:** `POST /api/books/ebooks/{id}/enrich-metadata/` re-runs enrichment (owner only).
|
||||
|
||||
## Open Library usage
|
||||
|
||||
### Search
|
||||
|
||||
`GET https://openlibrary.org/search.json`
|
||||
|
||||
| Param | Value |
|
||||
|-------|--------|
|
||||
| `title` | User-provided title |
|
||||
| `author` | User-provided author |
|
||||
| `lang` | `es` (primary) or `en` (fallback) |
|
||||
| `limit` | `5` |
|
||||
| `fields` | `key,title,author_name,cover_i,first_publish_year,subject,language,edition_key,number_of_pages_median,publisher` |
|
||||
|
||||
Primary pass also uses query filter `language:spa`. Fallback omits language filter.
|
||||
|
||||
### Covers
|
||||
|
||||
`GET https://covers.openlibrary.org/b/id/{cover_i}-L.jpg` — downloaded and stored on `EBook.cover_image`.
|
||||
|
||||
## Match scoring
|
||||
|
||||
| Score | Behavior |
|
||||
|-------|----------|
|
||||
| ≥ 0.8 | Apply OL title/author + cover + full metadata |
|
||||
| 0.6 – 0.8 | Metadata + cover only; keep user title/author |
|
||||
| < 0.6 | `match_status: not_found`; no field changes except metadata stub |
|
||||
|
||||
Author overlap + title similarity (normalized strings, `difflib.SequenceMatcher`). Prefer hits with `cover_i`.
|
||||
|
||||
## Data model
|
||||
|
||||
No migration. Uses existing fields on `EBook`:
|
||||
|
||||
- `metadata_json` — full enrichment payload (see below)
|
||||
- `cover_image` — downloaded cover file
|
||||
- `title` / `author` — updated when match score ≥ 0.8
|
||||
|
||||
### `metadata_json` shape
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "openlibrary",
|
||||
"matched_at": "2026-06-03T12:00:00+00:00",
|
||||
"match_language": "es",
|
||||
"match_score": 0.92,
|
||||
"match_status": "matched",
|
||||
"user_input": { "title": "...", "author": "..." },
|
||||
"openlibrary": {
|
||||
"work_key": "/works/OL...",
|
||||
"edition_key": "...",
|
||||
"title": "...",
|
||||
"authors": ["..."],
|
||||
"cover_id": 12345,
|
||||
"cover_url": "https://covers.openlibrary.org/b/id/12345-L.jpg",
|
||||
"first_publish_year": 1605,
|
||||
"subjects": ["..."],
|
||||
"languages": ["spa"],
|
||||
"publishers": ["..."],
|
||||
"number_of_pages_median": 320
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API changes
|
||||
|
||||
### Upload response (unchanged path)
|
||||
|
||||
`POST /api/books/ebooks/` — response may include populated `cover_image` and updated `title`/`author` after sync enrichment.
|
||||
|
||||
### Detail
|
||||
|
||||
`GET /api/books/ebooks/{id}/` — adds read-only `metadata` (alias of `metadata_json`).
|
||||
|
||||
### Manual refresh
|
||||
|
||||
`POST /api/books/ebooks/{id}/enrich-metadata/` — returns updated `EBookDetailSerializer` payload.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Env var | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `OPENLIBRARY_ENABLED` | `true` | Kill switch |
|
||||
| `OPENLIBRARY_PREFERRED_LANG` | `es` | Primary `lang` param |
|
||||
| `OPENLIBRARY_FALLBACK_LANG` | `en` | Fallback `lang` param |
|
||||
| `OPENLIBRARY_TIMEOUT_SECONDS` | `5` | HTTP timeout |
|
||||
| `OPENLIBRARY_USER_AGENT` | `CloudReader/1.0` | User-Agent header |
|
||||
|
||||
## Code layout
|
||||
|
||||
```
|
||||
backend/apps/books/services/
|
||||
├── openlibrary.py # Search, scoring, cover download
|
||||
└── metadata.py # enrich_ebook_metadata orchestrator
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
1. Upload with title `Don Quijote`, author `Cervantes` → cover + Spanish-friendly metadata.
|
||||
2. Upload with nonsense title/author → 201, no cover, `match_status: not_found`.
|
||||
3. `POST .../enrich-metadata/` on existing ebook refreshes metadata.
|
||||
@@ -0,0 +1,73 @@
|
||||
# 011 — Web reader (react-reader)
|
||||
|
||||
**Status:** Implemented
|
||||
**Created:** 2026-06-03
|
||||
|
||||
## Objective
|
||||
|
||||
Replace the custom HTML chapter reader with [react-reader](https://github.com/gerhardsletten/react-reader) (epub.js) for paginated EPUB reading in the web app. Keep the existing toolbar and settings panel chrome. Block PDFs from the in-browser reader.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Library (EPUB only) → /read/:id → ReadingPage
|
||||
→ GET /api/books/ebooks/{id}/ (metadata, format guard)
|
||||
→ GET /api/books/ebooks/{id}/file/ (authenticated EPUB blob)
|
||||
→ ReactReader (blob URL + CFI location)
|
||||
→ PATCH /api/books/ebooks/{id}/progress/ (epub_location + percentage)
|
||||
```
|
||||
|
||||
EPUB files are fetched with JWT via the API, converted to a blob URL client-side, and passed to react-reader. This avoids unauthenticated `/media/` URLs and CORS issues in dev.
|
||||
|
||||
## Backend changes
|
||||
|
||||
### `GET /api/books/ebooks/{id}/file/`
|
||||
|
||||
- Authenticated, owner-only
|
||||
- Returns `FileResponse` with `Content-Type: application/epub+zip`
|
||||
- Returns `400` if format is not `epub`
|
||||
|
||||
### `ReadingProgress.epub_location`
|
||||
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| `epub_location` | CharField(2048) | EPUB CFI string for resume position |
|
||||
|
||||
Exposed on `GET/PATCH /api/books/ebooks/{id}/progress/` and in `EBookDetail.progress`.
|
||||
|
||||
`current_position` stores whole-book percentage (0–100) derived from epub.js locations.
|
||||
|
||||
## Frontend changes
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `pages/ReadingPage.tsx` | `ReactReader` replaces chapter HTML rendering |
|
||||
| `hooks/useEpubReader.ts` | Blob load, CFI state, debounced progress save |
|
||||
| `utils/epubRendition.ts` | Theme/font application via `getRendition` |
|
||||
| `api/books.ts` | `getEpubFile(id)` |
|
||||
| `pages/Library.tsx` | Block PDF open with notice |
|
||||
| `App.tsx` | Single route `/read/:id`; `/reader/:id` redirects |
|
||||
|
||||
### Removed (web-only)
|
||||
|
||||
- `useChapters.ts`, `TableOfContents.tsx`, `Reader.tsx`, `useReadingProgress.ts`
|
||||
|
||||
Backend `/toc/` and `/content/` endpoints remain for mobile/API consumers.
|
||||
|
||||
## EPUB-only policy
|
||||
|
||||
- Library: clicking a PDF shows a dismissible notice; no navigation to reader
|
||||
- ReadingPage: deep-link guard if `format !== 'epub'`
|
||||
- Upload still accepts PDF for storage; web reader is EPUB-only
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Frontend: `react-reader` (^2.0.15)
|
||||
|
||||
## Verification
|
||||
|
||||
1. Open an EPUB from library → paginated reading, swipe/tap page turns, built-in TOC
|
||||
2. Close and reopen → resumes at saved CFI
|
||||
3. Change theme/font in settings → applies inside epub iframe
|
||||
4. Click a PDF in library → notice shown, reader not opened
|
||||
5. `GET /api/books/ebooks/{id}/file/` without auth → 401
|
||||
@@ -0,0 +1,56 @@
|
||||
# 012 — Library book context menu
|
||||
|
||||
**Status:** Implemented
|
||||
**Created:** 2026-06-03
|
||||
|
||||
## Objective
|
||||
|
||||
Add a custom right-click context menu on each book card in the library with **Sync metadata** (Open Library refresh) and **Remove** (delete from library). Show toast notifications for success, warning, and error outcomes.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Library book card (contextmenu)
|
||||
→ BookContextMenu
|
||||
→ Sync metadata: POST /api/books/ebooks/{id}/enrich-metadata/
|
||||
→ Remove: confirm dialog → DELETE /api/books/ebooks/{id}/
|
||||
→ ToastProvider (app-wide) → success | warning | error toasts
|
||||
```
|
||||
|
||||
Backend endpoints already exist; no API changes required.
|
||||
|
||||
## Frontend changes
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `api/books.ts` | `enrichEBookMetadata(id)` → `POST .../enrich-metadata/` |
|
||||
| `types/book.ts` | `OpenLibraryMetadata` with `match_status` |
|
||||
| `hooks/useToast.tsx` | `ToastProvider`, `showToast({ message, variant })`, auto-dismiss ~4s |
|
||||
| `components/ToastContainer.tsx` | Fixed bottom-right toast stack |
|
||||
| `components/BookContextMenu.tsx` | Positioned menu; sync + remove actions |
|
||||
| `pages/Library.tsx` | `onContextMenu` on cards; local state updates |
|
||||
| `App.tsx` | Wrap routes with `ToastProvider` |
|
||||
|
||||
## Context menu behavior
|
||||
|
||||
- Opens on right-click (`contextmenu`); browser default menu suppressed
|
||||
- Position clamped to viewport; closes on outside click, Escape, or scroll
|
||||
- **Sync metadata:** updates card title/author/cover from response; toast by `metadata.match_status`:
|
||||
- `matched` → success
|
||||
- `not_found` → warning
|
||||
- other → neutral success
|
||||
- **Remove:** `window.confirm` before delete; removes card from list on success
|
||||
- Menu clicks do not trigger card navigation to reader
|
||||
|
||||
## Dependencies
|
||||
|
||||
None (no new npm packages).
|
||||
|
||||
## Verification
|
||||
|
||||
1. Right-click a book → menu appears at cursor
|
||||
2. Sync metadata on a matched book → cover/title may update, success toast
|
||||
3. Sync on unmatched book → warning toast, no crash
|
||||
4. Remove → confirm → book disappears, success toast
|
||||
5. Cancel remove → book stays, menu closes
|
||||
6. Left-click still opens reader (EPUB) or PDF notice
|
||||
@@ -0,0 +1,54 @@
|
||||
# 013 — Frontend i18n (react-i18n-lite)
|
||||
|
||||
**Status:** Implemented
|
||||
**Created:** 2026-06-04
|
||||
|
||||
## Objective
|
||||
|
||||
Internationalize the web UI in English (`en-US`) and Spanish (`es-ES`) using [react-i18n-lite](https://www.npmjs.com/package/react-i18n-lite).
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
App.tsx
|
||||
└── I18nProvider (TranslationContainer)
|
||||
├── resolveDefaultLanguage() — localStorage → navigator → en-US
|
||||
├── LanguagePersistence — sync setLanguage ↔ localStorage, html[lang]
|
||||
└── AuthProvider → ToastProvider → routes
|
||||
```
|
||||
|
||||
Components call `useTranslation()` and `t('namespace.key', { interpolation })`.
|
||||
|
||||
## Locale files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `frontend/src/locales/en-US.ts` | English dictionary |
|
||||
| `frontend/src/locales/es-ES.ts` | Spanish dictionary |
|
||||
| `frontend/src/locales/index.ts` | `locales` map, `SupportedLanguage`, helpers |
|
||||
|
||||
Key namespaces: `common`, `auth`, `library`, `contextMenu`, `toast`, `settings`, `reader`, `addBook`, `bookDetail`, `search`, `annotations`.
|
||||
|
||||
## Language selection
|
||||
|
||||
- **Settings page** — UI Language dropdown (English / Español)
|
||||
- **Persistence** — `localStorage` key `cloud-reader.locale`
|
||||
- **Default** — saved preference, else `navigator.language` (`es*` → `es-ES`), else `en-US`
|
||||
|
||||
## Scope
|
||||
|
||||
**Translated:** All user-facing chrome (library, auth, reader toolbar/settings/TOC, add book, book detail, annotations, context menu, toasts).
|
||||
|
||||
**Not translated:** EPUB body content, API error bodies, backend `reading_status_display`, Open Library metadata fields.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `react-i18n-lite` (^1.0.10)
|
||||
|
||||
## Verification
|
||||
|
||||
1. Open app — labels match browser or saved language
|
||||
2. Settings → switch to Español — library/auth update without reload
|
||||
3. Refresh — language persists
|
||||
4. Context menu and toasts show translated strings
|
||||
5. `yarn build` succeeds
|
||||
@@ -0,0 +1,46 @@
|
||||
# 014 — Library reading progress badges
|
||||
|
||||
**Status:** Implemented
|
||||
**Created:** 2026-06-04
|
||||
|
||||
## Objective
|
||||
|
||||
Show accurate reading state on library book cards using saved EPUB progress instead of always displaying "Want to Read" (`Quiero leer`).
|
||||
|
||||
## Data source
|
||||
|
||||
- `GET /api/books/ebooks/` returns `progress` (0–100) from `ReadingProgress.current_position` via `EBookListSerializer.get_progress`.
|
||||
- Progress is updated by the web reader (`useEpubReader` → `PATCH .../progress/`).
|
||||
|
||||
## Display rules
|
||||
|
||||
| Progress | `opened` | Badge | Cover extra |
|
||||
|----------|----------|-------|-------------|
|
||||
| `null` or `0` | false | Want to Read | — |
|
||||
| `0` | true | Opened | — |
|
||||
| `1–98` | — | Reading · N% | Green progress bar on cover |
|
||||
| `≥ 99` | — | Finished | — |
|
||||
|
||||
**Opened** is set when the reader first displays (CFI saved, 0% progress). Turning pages moves to **Reading**.
|
||||
|
||||
Threshold constant: `FINISHED_PROGRESS_THRESHOLD = 99` in `frontend/src/utils/libraryStatus.ts`.
|
||||
|
||||
## Frontend changes
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `utils/libraryStatus.ts` | `deriveReadingStatus`, `normalizeProgressPercent` |
|
||||
| `pages/Library.tsx` | Map API progress → status; badge labels; client-side status filter |
|
||||
| `locales/en-US.ts`, `es-ES.ts` | `library.readingStatus.readingWithProgress` |
|
||||
| `pages/Library.module.css` | Cover progress bar styles |
|
||||
|
||||
## Status filter
|
||||
|
||||
The reading-status filter in the library panel now filters client-side by derived status (`want_to_read`, `reading`, `finished`).
|
||||
|
||||
## Verification
|
||||
|
||||
1. New upload → **Want to Read** / **Quiero leer**
|
||||
2. Partial read → **Reading · N%** + progress bar
|
||||
3. Near end (≥ 99%) → **Finished** / **Terminado**
|
||||
4. Filters by Leyendo / Terminado work as expected
|
||||
@@ -0,0 +1,83 @@
|
||||
# 015 — EPUB bookmarks and notes (physical-book UX)
|
||||
|
||||
**Status:** Implemented
|
||||
**Created:** 2026-06-04
|
||||
|
||||
## Objective
|
||||
|
||||
Let users select passage text while reading an EPUB, save a bookmark (with optional thought), list markers in chapter order, and jump back to the exact location. Global view groups markers by book in a Reddit-style thread layout.
|
||||
|
||||
## Fix: AnnotationsProvider
|
||||
|
||||
`AnnotationsProvider` wraps all routes in [`App.tsx`](frontend/src/App.tsx) so `/bookmarks-notes` and the reader can use `useAnnotations()`.
|
||||
|
||||
## Data model
|
||||
|
||||
`Bookmark` (annotations app) references **`books.EBook`**, not catalog `Book`:
|
||||
|
||||
| Field | Purpose |
|
||||
|-------|---------|
|
||||
| `ebook` | FK to uploaded ebook |
|
||||
| `epub_cfi` | EPUB CFI anchor |
|
||||
| `chapter_index` | Spine index for sort order |
|
||||
| `chapter_title` | Display label |
|
||||
| `location_text` | Selected passage quote |
|
||||
| `content` | Optional user thought (empty = bookmark only) |
|
||||
| `page` | Legacy display field (`chapter_index + 1`) |
|
||||
|
||||
Unique: `(user, ebook, epub_cfi)`.
|
||||
|
||||
Default API ordering: `chapter_index`, `epub_cfi`.
|
||||
|
||||
Legacy `Note` model remains for old API; new UX uses `Bookmark.content` only.
|
||||
|
||||
## Reader flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant EpubView
|
||||
participant Popover
|
||||
participant API
|
||||
User->>EpubView: Select text
|
||||
EpubView->>Popover: Show near selection
|
||||
User->>Popover: Save optional thought
|
||||
Popover->>API: POST /annotations/bookmarks/
|
||||
```
|
||||
|
||||
1. Text selection via epub.js `selected` event and content `mouseup` hook.
|
||||
2. [`SelectionPopover`](frontend/src/components/reader/SelectionPopover.tsx) — floating UI, optional textarea.
|
||||
3. Toolbar bookmark icon opens [`BookMarkersPanel`](frontend/src/components/reader/BookMarkersPanel.tsx) (current ebook only).
|
||||
4. “Go to passage” sets reader `location` to stored CFI (`/read/:id` with router state).
|
||||
|
||||
## Global page (`/bookmarks-notes`)
|
||||
|
||||
[`MarkerThreadsView`](frontend/src/components/annotations/MarkerThreadsView.tsx):
|
||||
|
||||
- Groups markers by ebook (collapsible book rows).
|
||||
- Within each book: chapter order, passage as blockquote, thought indented below (Reddit-style).
|
||||
- Optional filter: `/bookmarks-notes/:ebookId`.
|
||||
|
||||
## API
|
||||
|
||||
- `GET /api/annotations/bookmarks/?ebook={id}`
|
||||
- `POST /api/annotations/bookmarks/` — body: `ebook`, `epub_cfi`, `chapter_index`, `chapter_title`, `location_text`, `content`
|
||||
|
||||
## i18n
|
||||
|
||||
New keys under `annotations.*` (EN/ES): `saveMarker`, `thoughtPlaceholder`, `bookmarkOnly`, `selectTextHint`, `goToPassage`, `inBookPanel`, `markerCount`, etc.
|
||||
|
||||
## Verification
|
||||
|
||||
1. `/bookmarks-notes` loads without provider error.
|
||||
2. Select text in reader → popover → save with/without thought.
|
||||
3. Markers appear in reader panel and global page under correct book, in chapter order.
|
||||
4. “Go to passage” opens the correct location.
|
||||
|
||||
See also [016 — Bookmark reading anchor](016-bookmark-reading-anchor.md) for preserving reading position while peeking at bookmarks.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- PDF selection
|
||||
- Multiple replies per passage
|
||||
- Migrating legacy `Note` rows into `Bookmark`
|
||||
@@ -0,0 +1,87 @@
|
||||
# 016 — Bookmark reading anchor
|
||||
|
||||
**Status:** Implemented
|
||||
**Created:** 2026-06-04
|
||||
**See also:** [015 — EPUB bookmarks and notes](015-epub-bookmarks-notes.md)
|
||||
|
||||
## Objective
|
||||
|
||||
Let users jump to a bookmark to review a passage without overwriting their true reading position. While peeking, show a high-visibility control to return to where they were reading.
|
||||
|
||||
## Definitions
|
||||
|
||||
| Term | Meaning |
|
||||
|------|---------|
|
||||
| **Reading anchor** | EPUB CFI (+ optional %) captured immediately before a bookmark peek |
|
||||
| **Peek mode** | Temporary view at a bookmark location; server `ReadingProgress` is not updated |
|
||||
| **Resume** | Jump back to the reading anchor and re-enable progress persistence |
|
||||
|
||||
## Problem (before)
|
||||
|
||||
“Go to passage” called `jumpToCfi`, which triggered `locationChanged` and debounced `PATCH /books/ebooks/{id}/progress/`, replacing `epub_location` and `current_position` with the bookmark. Library progress and the next reading session started at the bookmark instead of the real position.
|
||||
|
||||
## Triggers (enter peek mode)
|
||||
|
||||
- In-reader: **Go to passage** in [`BookMarkersPanel`](../frontend/src/components/reader/BookMarkersPanel.tsx)
|
||||
- Global: **Go to passage** on [`/bookmarks-notes`](../frontend/src/components/annotations/BookmarksNotesPage.tsx) → `/read/:id` with `state.epubLocation` (bookmark CFI)
|
||||
|
||||
## Non-triggers
|
||||
|
||||
- Table of contents navigation
|
||||
- Prev / next page buttons
|
||||
- Creating a new bookmark from text selection
|
||||
- Opening the book normally from the library (no `epubLocation` in router state)
|
||||
|
||||
## UX
|
||||
|
||||
### Resume control
|
||||
|
||||
- Component: [`ResumeReadingButton`](../frontend/src/components/reader/ResumeReadingButton.tsx)
|
||||
- Visible only when `isBookmarkPeekActive && readingAnchor != null`
|
||||
- Position: right edge, **above** the next-page chevron (`.reader-page-nav--next`)
|
||||
- Style: warm accent (`#ea580c` / `#f97316`), white label/icon; distinct from muted gray nav buttons
|
||||
- Action: `resumeReadingAnchor()` — hides control, returns to anchor CFI
|
||||
- i18n: `reader.resumeReading`, `reader.resumeReadingAria`
|
||||
|
||||
### Anchor policy
|
||||
|
||||
- First anchor is captured when peek starts; **additional “Go to passage” clicks while peeking do not replace the anchor** until the user resumes or leaves the reader.
|
||||
|
||||
## Progress rules
|
||||
|
||||
| Mode | `PATCH .../progress/` |
|
||||
|------|------------------------|
|
||||
| Normal reading | Yes (debounced on `locationChanged`, sync after locations ready) |
|
||||
| Bookmark peek | **No** — `flushProgress` / `scheduleProgress` no-op |
|
||||
| After resume | Yes — flush anchor CFI and percentage once |
|
||||
|
||||
### Load from bookmarks page
|
||||
|
||||
1. Fetch saved `ReadingProgress` from API.
|
||||
2. If `state.epubLocation` is set and saved `epub_location` exists → store saved location as **anchor**, set peek mode, open at bookmark CFI.
|
||||
3. Do not persist bookmark location as progress during peek.
|
||||
|
||||
### In-reader peek
|
||||
|
||||
1. Capture current `location` (CFI) as anchor (if valid).
|
||||
2. Enter peek mode, jump to bookmark CFI.
|
||||
|
||||
## API
|
||||
|
||||
No backend changes in v1. Anchor is session-only in [`useEpubReader`](../frontend/src/hooks/useEpubReader.ts).
|
||||
|
||||
## Verification
|
||||
|
||||
1. Read to ~30%, open markers, **Go to passage** on an early bookmark → jumps; after debounce, library/API progress still reflects ~30% (not bookmark).
|
||||
2. Orange **Back to reading** appears above the next chevron only during peek.
|
||||
3. Tap resume → returns to ~30%; button hides; progress saves resume.
|
||||
4. From `/bookmarks-notes`, **Go to passage** → peek + resume using saved progress as anchor.
|
||||
5. TOC / prev / next do not show the resume button.
|
||||
6. EN/ES strings present.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- PDF reader
|
||||
- Multiple anchor history stack
|
||||
- Backend `resume_epub_location` field
|
||||
- Peek mode for TOC jumps
|
||||
@@ -93,4 +93,4 @@ The monorepo structure supports a single pipeline that can:
|
||||
- `yarn install` at root (installs all workspaces)
|
||||
- `yarn workspace @cloud-reader/shared build`
|
||||
- `yarn workspace @cloud-reader/mobile build` (Expo EAS for mobile builds)
|
||||
- `yarn workspace @cloud-reader/frontend build` (Vite for web builds)
|
||||
- `yarn workspace @cloud-reader/frontend build` (Vite for web builds)
|
||||
Reference in New Issue
Block a user