diff --git a/apps/readest-app/.claude/memory/MEMORY.md b/apps/readest-app/.claude/memory/MEMORY.md index 6741cb20..8f50e525 100644 --- a/apps/readest-app/.claude/memory/MEMORY.md +++ b/apps/readest-app/.claude/memory/MEMORY.md @@ -13,6 +13,7 @@ - [Reading ruler line/column-aware](reading-ruler-line-aware.md) — ruler snaps to real lines; multi-column band spans one column; Range.getClientRects() returns tall block boxes that must be dropped; iframe frame-offset mapping; synthetic-key throttling - [TOC expand + auto-scroll](toc-expand-and-autoscroll.md) — #4059 collapse-by-default policy in `tocTree.ts`; pinned-sidebar mounts before progress → dynamic expansion breaks scroll-to-current via (1) spurious onScroll clearing pending and (2) Virtuoso scrollToIndex landing short after row growth (re-assert on rAF) - [BooknoteView auto-scroll (#4352)](booknote-view-autoscroll-4352.md) — virtualizing the annotation/bookmark list dropped auto-scroll-to-nearest; two paths (reload: OverlayScrollbars resets scrollTop → re-apply in `initialized` via ref; tab-switch: synchronous scrollToIndex on fresh-mounted list wedges Virtuoso → use `initialTopMostItemIndex` + skip-gate). Mirrors TOCView. Includes dev-server/Fast-Refresh/screenshot-vs-DOM verification gotchas +- [TOC current-position row](toc-current-position-row.md) — synthetic "Current position" row (open-book icon + live `progress.page`) injected one level deeper under the active TOC item via `buildTOCDisplayItems` in `TOCItem.tsx`. INVARIANT: insert AFTER the active item so its `flatItems` index stays valid for the auto-scroll effects - [Swipe page-turn bg flash](paginator-swipe-bg-flash.md) — white↔black flash on swipe+animation only; `#background` was static screen-space and didn't track content during drag/snap; fix = sliding per-view full-bleed segments (`computeBackgroundSegments`) rebuilt on scroll + per-rAF synced to the view transform during snap - [Duokan fullscreen cover hidden in scroll mode](duokan-fullscreen-cover-scroll.md) — #4379 `data-duokan-page-fullscreen` cover pinned `position:absolute height:100%` collapses against auto-height scroll container; gate fullscreen on `this.#column` + reset stale absolute props on toggle (`setImageSize` in paginator.js) - [Paginated texture occlusion](paginated-texture-occlusion-4399.md) — #4399 host `.foliate-viewer::before` texture absent in paginated (shown in scrolled); opaque `#background` container (`= fallbackBg`) from the swipe-flash fix occludes it; shared `textureAwareBackground` helper + `hasTexture ? '' : fallbackBg` container @@ -85,3 +86,4 @@ - [en/translation.json holds ONLY plural variants + proper nouns](feedback_en_plurals_manual.md) — non-plural strings stay out (defaultValue: key is the en source); plural strings (`_('...', { count })`) need hand-added `_one`/`_other` entries or the singular renders as "1 days" - [Never push on every change](feedback_dont_push_every_change.md) — hold pushes during active bug iteration; commit locally only until user confirms or work hits a clean done-state - [No test seams in production code](feedback_no_test_seams_in_prod.md) — production must never import or call `__reset*ForTests`; cross-module test resets belong in the test file's beforeEach/afterEach +- [Dependabot transitive fixes](dependabot-pnpm-overrides.md) — pin patched min-version in `pnpm-workspace.yaml` `overrides:` (NOT package.json `pnpm.overrides`, which pnpm 9+ ignores); watch for existing too-low pins; alert#≠issue# so no `Closes #` (PR #4523) diff --git a/apps/readest-app/.claude/memory/dependabot-pnpm-overrides.md b/apps/readest-app/.claude/memory/dependabot-pnpm-overrides.md new file mode 100644 index 00000000..03553bfa --- /dev/null +++ b/apps/readest-app/.claude/memory/dependabot-pnpm-overrides.md @@ -0,0 +1,21 @@ +--- +name: dependabot-pnpm-overrides +description: How to fix transitive-dependency Dependabot/CVE alerts in the readest monorepo (pnpm overrides location + style) +metadata: + node_type: memory + type: project + originSessionId: cdc9c728-a2c7-4a9e-b87a-44046560a4fa +--- + +Transitive npm security advisories (Dependabot alerts against `pnpm-lock.yaml`) are fixed by pinning a **minimum patched version** in the `overrides:` block of **`pnpm-workspace.yaml`** at the monorepo root — NOT `package.json`'s `pnpm.overrides`. + +**Why:** the repo uses pnpm 9+ (`pnpm@11.x`), which reads `overrides`/`patchedDependencies`/`catalog` from `pnpm-workspace.yaml`. A `pnpm.overrides` block added to root `package.json` is silently ignored — `pnpm install` runs fast and the lockfile doesn't change. There is already a long list of security pins in that `overrides:` block (glob, undici, qs, body-parser, etc.). + +**How to apply:** +1. `gh api repos/readest/readest/dependabot/alerts/` → get package + `first_patched_version` + vulnerable range. +2. Confirm parent ranges allow the patch (`cat node_modules/.pnpm/@*/.../package.json | grep '""'`). +3. Add/raise the entry in `pnpm-workspace.yaml` `overrides:` in the existing style: `: '>='` (e.g. `shell-quote: '>=1.8.4'`). **Check for an existing too-low pin** — e.g. `qs: '>=6.14.2'` still allowed the vulnerable 6.15.1; had to raise to `'>=6.15.2'`. +4. `pnpm install --lockfile-only` then `pnpm install`. Verify with `pnpm why -r ` (should show only the patched version). Stale dirs may linger in `node_modules/.pnpm` but are harmless if the lockfile has zero refs to the old version. +5. Dependabot **alert numbers are not GitHub issue numbers** — don't use `Closes #N`; alerts auto-dismiss when the vulnerable version leaves the default-branch lockfile. Reference the `/security/dependabot/` URLs in the PR body instead. + +First done in PR #4523 (shell-quote 1.8.4 / GHSA-w7jw-789q-3m8p, qs 6.15.2 / GHSA-q8mj-m7cp-5q26). Diff stays scoped to just the bumped packages; prefer this over `pnpm update ` which incidentally refreshes unrelated in-range patches (e.g. react-is). See [[feedback_pr_new_branch]] [[feedback_use_worktree]]. diff --git a/apps/readest-app/.claude/memory/toc-current-position-row.md b/apps/readest-app/.claude/memory/toc-current-position-row.md new file mode 100644 index 00000000..64e7977a --- /dev/null +++ b/apps/readest-app/.claude/memory/toc-current-position-row.md @@ -0,0 +1,25 @@ +--- +name: toc-current-position-row +description: "TOC \"Current position\" row showing the live reading page under the active item" +metadata: + node_type: memory + type: project + originSessionId: 7c3a3d3c-86fe-4f6a-b539-8bf8052d68bd +--- + +The TOC sidebar shows a synthetic "Current position" row (open-book `FiBookOpen` icon + `_('Current position')` label + live page number) directly under the highlighted/active TOC item, indented one level deeper. The row is **clickable** → navigates to `progress.location` (the exact current reading CFI, more precise than the section header which goes to section start); passes `onClick` from TOCView's `handleCurrentPositionClick` which dispatches `navigate` + `view.goTo(location)` (mirrors `BooknoteItem`). Page number uses the ordinary muted `text-base-content/50` (NOT the blue highlight — explicit user ask); icon + label stay highlighted. + +`'Current position'` was ALREADY an existing i18n key (used by `src/app/reader/hooks/kosyncPreview.ts`), translated in all 33 locales — reusing it needed no new translation. + +Key files: +- `src/app/reader/components/sidebar/TOCItem.tsx` — `CurrentPositionRow` component + pure helpers `buildTOCDisplayItems(flatItems, activeHref, currentPage)`, `isCurrentPositionItem`, types `CurrentPositionItem`/`TOCDisplayItem`. +- `src/app/reader/components/sidebar/TOCView.tsx` — `displayItems = useMemo(buildTOCDisplayItems(flatItems, activeHref, progress?.page))`; Virtuoso renders `displayItems` (totalCount + itemContent discriminates on `isCurrentPositionItem`). + +Design facts: +- Page number = `progress.page` (already `pageInfo.current + 1`, resolves fixed-layout vs reflowable). Same scale as the per-item page numbers (`item.location.current + 1` / `item.index + 1`). The active section header shows its START page; the current-position row shows the LIVE page, so the gap = how far into the section you are. +- Indent: synthetic row depth = `activeItem.depth + 1`, rendered with the same `(depth + 1) * 12` formula → matches a child item's indent. +- Highlight reuses the active-item classes (`text-bold-in-eink sm:bg-base-300/65 sm:text-base-content text-blue-500`) so eink behavior is already covered. Icon inherits `currentColor`. + +**INVARIANT (do not break):** the row is inserted *after* the active item, so the active item's index in `displayItems` equals its index in `flatItems`. This is why the auto-scroll effects (which still `flatItems.findIndex(...).scrollToIndex`) keep working untouched. If you ever insert anything BEFORE the active item, the scroll-to-active index math breaks. See [[toc-expand-and-autoscroll]] and [[booknote-view-autoscroll-4352]]. + +Tests: `src/__tests__/components/TOCItem.test.tsx` (`CurrentPositionRow` + `buildTOCDisplayItems` suites). 'Current position' is a non-plural string → no en/translation.json entry needed ([[feedback_en_plurals_manual]]). diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index 1e2e31b7..79228e91 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -1831,5 +1831,6 @@ "Book file is not available locally": "ملف الكتاب غير متوفر محليًا", "Failed to send book": "فشل إرسال الكتاب", "Send": "إرسال", - "Highlight Current Sentence": "تمييز الجملة الحالية" + "Highlight Current Sentence": "تمييز الجملة الحالية", + "File Path": "مسار الملف" } diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json index 775027fc..5d4b168d 100644 --- a/apps/readest-app/public/locales/bn/translation.json +++ b/apps/readest-app/public/locales/bn/translation.json @@ -1699,5 +1699,6 @@ "Book file is not available locally": "বইয়ের ফাইলটি স্থানীয়ভাবে উপলব্ধ নয়", "Failed to send book": "বই পাঠাতে ব্যর্থ হয়েছে", "Send": "পাঠান", - "Highlight Current Sentence": "বর্তমান বাক্য হাইলাইট করুন" + "Highlight Current Sentence": "বর্তমান বাক্য হাইলাইট করুন", + "File Path": "ফাইলের পথ" } diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json index 6d9805fa..2cd073f9 100644 --- a/apps/readest-app/public/locales/bo/translation.json +++ b/apps/readest-app/public/locales/bo/translation.json @@ -1666,5 +1666,6 @@ "Book file is not available locally": "དེབ་ཀྱི་ཡིག་ཆ་དེ་ས་གནས་སུ་མི་འདུག", "Failed to send book": "དེབ་སྐུར་གཏོང་མ་ཐུབ།", "Send": "སྐུར་གཏོང་།", - "Highlight Current Sentence": "ད་ལྟའི་ཚིག་གྲུབ་ལ་འོག་ཐིག་གཏོང་།" + "Highlight Current Sentence": "ད་ལྟའི་ཚིག་གྲུབ་ལ་འོག་ཐིག་གཏོང་།", + "File Path": "ཡིག་ཆའི་ལམ་ཐིག" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index 0d745095..be63055c 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -1699,5 +1699,6 @@ "Book file is not available locally": "Buchdatei ist nicht lokal verfügbar", "Failed to send book": "Senden des Buchs fehlgeschlagen", "Send": "Senden", - "Highlight Current Sentence": "Aktuellen Satz hervorheben" + "Highlight Current Sentence": "Aktuellen Satz hervorheben", + "File Path": "Dateipfad" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index 68799d54..0cdcf73f 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -1699,5 +1699,6 @@ "Book file is not available locally": "Το αρχείο του βιβλίου δεν είναι διαθέσιμο τοπικά", "Failed to send book": "Αποτυχία αποστολής βιβλίου", "Send": "Αποστολή", - "Highlight Current Sentence": "Επισήμανση τρέχουσας πρότασης" + "Highlight Current Sentence": "Επισήμανση τρέχουσας πρότασης", + "File Path": "Διαδρομή αρχείου" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index bf5951f7..30031a69 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -1732,5 +1732,6 @@ "Book file is not available locally": "El archivo del libro no está disponible localmente", "Failed to send book": "No se pudo enviar el libro", "Send": "Enviar", - "Highlight Current Sentence": "Resaltar la oración actual" + "Highlight Current Sentence": "Resaltar la oración actual", + "File Path": "Ruta del archivo" } diff --git a/apps/readest-app/public/locales/fa/translation.json b/apps/readest-app/public/locales/fa/translation.json index 9213644a..8903eed2 100644 --- a/apps/readest-app/public/locales/fa/translation.json +++ b/apps/readest-app/public/locales/fa/translation.json @@ -1699,5 +1699,6 @@ "Book file is not available locally": "فایل کتاب به صورت محلی در دسترس نیست", "Failed to send book": "ارسال کتاب ناموفق بود", "Send": "ارسال", - "Highlight Current Sentence": "برجسته‌سازی جملهٔ فعلی" + "Highlight Current Sentence": "برجسته‌سازی جملهٔ فعلی", + "File Path": "مسیر فایل" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index 15b495a0..3619afd7 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -1732,5 +1732,6 @@ "Book file is not available locally": "Le fichier du livre n'est pas disponible localement", "Failed to send book": "Échec de l'envoi du livre", "Send": "Envoyer", - "Highlight Current Sentence": "Surligner la phrase actuelle" + "Highlight Current Sentence": "Surligner la phrase actuelle", + "File Path": "Chemin du fichier" } diff --git a/apps/readest-app/public/locales/he/translation.json b/apps/readest-app/public/locales/he/translation.json index 505c5c5f..232f39f0 100644 --- a/apps/readest-app/public/locales/he/translation.json +++ b/apps/readest-app/public/locales/he/translation.json @@ -1732,5 +1732,6 @@ "Book file is not available locally": "קובץ הספר אינו זמין באופן מקומי", "Failed to send book": "שליחת הספר נכשלה", "Send": "שליחה", - "Highlight Current Sentence": "הדגשת המשפט הנוכחי" + "Highlight Current Sentence": "הדגשת המשפט הנוכחי", + "File Path": "נתיב הקובץ" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index 9a1bcfe1..ec8babbe 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -1699,5 +1699,6 @@ "Book file is not available locally": "पुस्तक फ़ाइल स्थानीय रूप से उपलब्ध नहीं है", "Failed to send book": "पुस्तक भेजने में विफल", "Send": "भेजें", - "Highlight Current Sentence": "वर्तमान वाक्य को हाइलाइट करें" + "Highlight Current Sentence": "वर्तमान वाक्य को हाइलाइट करें", + "File Path": "फाइल पथ" } diff --git a/apps/readest-app/public/locales/hu/translation.json b/apps/readest-app/public/locales/hu/translation.json index 80446ac7..1c075308 100644 --- a/apps/readest-app/public/locales/hu/translation.json +++ b/apps/readest-app/public/locales/hu/translation.json @@ -1699,5 +1699,6 @@ "Book file is not available locally": "A könyvfájl nem érhető el helyileg", "Failed to send book": "A könyv küldése sikertelen", "Send": "Küldés", - "Highlight Current Sentence": "Aktuális mondat kiemelése" + "Highlight Current Sentence": "Aktuális mondat kiemelése", + "File Path": "Fájl elérési útja" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index 6a8cf2b3..ad7aa46b 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -1666,5 +1666,6 @@ "Book file is not available locally": "Berkas buku tidak tersedia secara lokal", "Failed to send book": "Gagal mengirim buku", "Send": "Kirim", - "Highlight Current Sentence": "Sorot kalimat saat ini" + "Highlight Current Sentence": "Sorot kalimat saat ini", + "File Path": "Jalur File" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index f7cb01e1..02e6e3e0 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -1732,5 +1732,6 @@ "Book file is not available locally": "Il file del libro non è disponibile localmente", "Failed to send book": "Invio del libro non riuscito", "Send": "Invia", - "Highlight Current Sentence": "Evidenzia la frase corrente" + "Highlight Current Sentence": "Evidenzia la frase corrente", + "File Path": "Percorso del file" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index 595f6048..a41fe8cd 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -1666,5 +1666,6 @@ "Book file is not available locally": "書籍ファイルがローカルに存在しません", "Failed to send book": "書籍の送信に失敗しました", "Send": "送信", - "Highlight Current Sentence": "現在の文をハイライト" + "Highlight Current Sentence": "現在の文をハイライト", + "File Path": "ファイルパス" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index ee849e5f..eeeff46c 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -1666,5 +1666,6 @@ "Book file is not available locally": "책 파일을 로컬에서 사용할 수 없습니다", "Failed to send book": "책 보내기에 실패했습니다", "Send": "보내기", - "Highlight Current Sentence": "현재 문장 강조" + "Highlight Current Sentence": "현재 문장 강조", + "File Path": "파일 경로" } diff --git a/apps/readest-app/public/locales/ms/translation.json b/apps/readest-app/public/locales/ms/translation.json index 59eb94b7..b82ba499 100644 --- a/apps/readest-app/public/locales/ms/translation.json +++ b/apps/readest-app/public/locales/ms/translation.json @@ -1666,5 +1666,6 @@ "Book file is not available locally": "Fail buku tidak tersedia secara setempat", "Failed to send book": "Gagal menghantar buku", "Send": "Hantar", - "Highlight Current Sentence": "Serlahkan ayat semasa" + "Highlight Current Sentence": "Serlahkan ayat semasa", + "File Path": "Laluan Fail" } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index 4f2b4b33..35511f6e 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -1699,5 +1699,6 @@ "Book file is not available locally": "Boekbestand is niet lokaal beschikbaar", "Failed to send book": "Verzenden van boek mislukt", "Send": "Verzenden", - "Highlight Current Sentence": "Huidige zin markeren" + "Highlight Current Sentence": "Huidige zin markeren", + "File Path": "Bestandspad" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index c40c632b..c35b67b3 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -1765,5 +1765,6 @@ "Book file is not available locally": "Plik książki nie jest dostępny lokalnie", "Failed to send book": "Nie udało się wysłać książki", "Send": "Wyślij", - "Highlight Current Sentence": "Podświetl bieżące zdanie" + "Highlight Current Sentence": "Podświetl bieżące zdanie", + "File Path": "Ścieżka pliku" } diff --git a/apps/readest-app/public/locales/pt-BR/translation.json b/apps/readest-app/public/locales/pt-BR/translation.json index 92b6e681..e69567d3 100644 --- a/apps/readest-app/public/locales/pt-BR/translation.json +++ b/apps/readest-app/public/locales/pt-BR/translation.json @@ -1732,5 +1732,6 @@ "Book file is not available locally": "O arquivo do livro não está disponível localmente", "Failed to send book": "Falha ao enviar o livro", "Send": "Enviar", - "Highlight Current Sentence": "Destacar a frase atual" + "Highlight Current Sentence": "Destacar a frase atual", + "File Path": "Caminho do arquivo" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index f642ee57..c66d5eaf 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -1732,5 +1732,6 @@ "Book file is not available locally": "O ficheiro do livro não está disponível localmente", "Failed to send book": "Falha ao enviar o livro", "Send": "Enviar", - "Highlight Current Sentence": "Destacar a frase atual" + "Highlight Current Sentence": "Destacar a frase atual", + "File Path": "Caminho do Arquivo" } diff --git a/apps/readest-app/public/locales/ro/translation.json b/apps/readest-app/public/locales/ro/translation.json index 062f4659..b74fecca 100644 --- a/apps/readest-app/public/locales/ro/translation.json +++ b/apps/readest-app/public/locales/ro/translation.json @@ -1732,5 +1732,6 @@ "Book file is not available locally": "Fișierul cărții nu este disponibil local", "Failed to send book": "Trimiterea cărții a eșuat", "Send": "Trimite", - "Highlight Current Sentence": "Evidențiază propoziția curentă" + "Highlight Current Sentence": "Evidențiază propoziția curentă", + "File Path": "Calea fișierului" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index cdc8775a..9814b3cf 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -1765,5 +1765,6 @@ "Book file is not available locally": "Файл книги недоступен локально", "Failed to send book": "Не удалось отправить книгу", "Send": "Отправить", - "Highlight Current Sentence": "Выделять текущее предложение" + "Highlight Current Sentence": "Выделять текущее предложение", + "File Path": "Путь к файлу" } diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json index 9e407695..9015831b 100644 --- a/apps/readest-app/public/locales/si/translation.json +++ b/apps/readest-app/public/locales/si/translation.json @@ -1699,5 +1699,6 @@ "Book file is not available locally": "පොත් ගොනුව දේශීයව ලබා ගත නොහැක", "Failed to send book": "පොත යැවීමට අසමත් විය", "Send": "යවන්න", - "Highlight Current Sentence": "වර්තමාන වාක්‍යය ඉස්මතු කරන්න" + "Highlight Current Sentence": "වර්තමාන වාක්‍යය ඉස්මතු කරන්න", + "File Path": "ගොනු මාර්ගය" } diff --git a/apps/readest-app/public/locales/sl/translation.json b/apps/readest-app/public/locales/sl/translation.json index be165932..0c0a09a8 100644 --- a/apps/readest-app/public/locales/sl/translation.json +++ b/apps/readest-app/public/locales/sl/translation.json @@ -1765,5 +1765,6 @@ "Book file is not available locally": "Datoteka knjige ni na voljo lokalno", "Failed to send book": "Pošiljanje knjige ni uspelo", "Send": "Pošlji", - "Highlight Current Sentence": "Označi trenutni stavek" + "Highlight Current Sentence": "Označi trenutni stavek", + "File Path": "Pot do datoteke" } diff --git a/apps/readest-app/public/locales/sv/translation.json b/apps/readest-app/public/locales/sv/translation.json index 31b1fd4d..56eff1d8 100644 --- a/apps/readest-app/public/locales/sv/translation.json +++ b/apps/readest-app/public/locales/sv/translation.json @@ -1699,5 +1699,6 @@ "Book file is not available locally": "Bokfilen är inte tillgänglig lokalt", "Failed to send book": "Det gick inte att skicka boken", "Send": "Skicka", - "Highlight Current Sentence": "Markera aktuell mening" + "Highlight Current Sentence": "Markera aktuell mening", + "File Path": "Filsökväg" } diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json index c0db9498..f28a74ec 100644 --- a/apps/readest-app/public/locales/ta/translation.json +++ b/apps/readest-app/public/locales/ta/translation.json @@ -1699,5 +1699,6 @@ "Book file is not available locally": "புத்தக கோப்பு உள்ளூரில் கிடைக்கவில்லை", "Failed to send book": "புத்தகத்தை அனுப்ப முடியவில்லை", "Send": "அனுப்பு", - "Highlight Current Sentence": "தற்போதைய வாக்கியத்தை தனிப்படுத்து" + "Highlight Current Sentence": "தற்போதைய வாக்கியத்தை தனிப்படுத்து", + "File Path": "கோப்பு பாதை" } diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json index 1dd097a8..ee8689f2 100644 --- a/apps/readest-app/public/locales/th/translation.json +++ b/apps/readest-app/public/locales/th/translation.json @@ -1666,5 +1666,6 @@ "Book file is not available locally": "ไม่มีไฟล์หนังสือในเครื่อง", "Failed to send book": "ส่งหนังสือไม่สำเร็จ", "Send": "ส่ง", - "Highlight Current Sentence": "ไฮไลต์ประโยคปัจจุบัน" + "Highlight Current Sentence": "ไฮไลต์ประโยคปัจจุบัน", + "File Path": "เส้นทางไฟล์" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index 509d9675..d9d5c6ce 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -1699,5 +1699,6 @@ "Book file is not available locally": "Kitap dosyası yerel olarak mevcut değil", "Failed to send book": "Kitap gönderilemedi", "Send": "Gönder", - "Highlight Current Sentence": "Geçerli cümleyi vurgula" + "Highlight Current Sentence": "Geçerli cümleyi vurgula", + "File Path": "Dosya Yolu" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index c38518b1..7b5d6b5b 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -1765,5 +1765,6 @@ "Book file is not available locally": "Файл книги недоступний локально", "Failed to send book": "Не вдалося надіслати книгу", "Send": "Надіслати", - "Highlight Current Sentence": "Виділяти поточне речення" + "Highlight Current Sentence": "Виділяти поточне речення", + "File Path": "Шлях до файлу" } diff --git a/apps/readest-app/public/locales/uz/translation.json b/apps/readest-app/public/locales/uz/translation.json index b4742e33..1b7e1949 100644 --- a/apps/readest-app/public/locales/uz/translation.json +++ b/apps/readest-app/public/locales/uz/translation.json @@ -1699,5 +1699,6 @@ "Book file is not available locally": "Kitob fayli mahalliy tarzda mavjud emas", "Failed to send book": "Kitobni yuborib bo'lmadi", "Send": "Yuborish", - "Highlight Current Sentence": "Joriy jumlani ajratib ko'rsatish" + "Highlight Current Sentence": "Joriy jumlani ajratib ko'rsatish", + "File Path": "Fayl yo'li" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index 32051b66..62d20512 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -1666,5 +1666,6 @@ "Book file is not available locally": "Tệp sách không có sẵn trên thiết bị", "Failed to send book": "Gửi sách thất bại", "Send": "Gửi", - "Highlight Current Sentence": "Tô sáng câu hiện tại" + "Highlight Current Sentence": "Tô sáng câu hiện tại", + "File Path": "Đường dẫn tệp" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index aebf33c2..5c1fd367 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -1666,5 +1666,6 @@ "Book file is not available locally": "本地没有该图书文件", "Failed to send book": "发送图书失败", "Send": "发送", - "Highlight Current Sentence": "高亮当前句子" + "Highlight Current Sentence": "高亮当前句子", + "File Path": "文件路径" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index 4f102e7d..eda56a88 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -1666,5 +1666,6 @@ "Book file is not available locally": "本機沒有此書籍檔案", "Failed to send book": "傳送書籍失敗", "Send": "傳送", - "Highlight Current Sentence": "醒目標示目前句子" + "Highlight Current Sentence": "醒目標示目前句子", + "File Path": "檔案路徑" } diff --git a/apps/readest-app/src/__tests__/components/TOCItem.test.tsx b/apps/readest-app/src/__tests__/components/TOCItem.test.tsx index ee4ad241..814f8fd2 100644 --- a/apps/readest-app/src/__tests__/components/TOCItem.test.tsx +++ b/apps/readest-app/src/__tests__/components/TOCItem.test.tsx @@ -1,13 +1,23 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; -import { render, screen, cleanup } from '@testing-library/react'; +import { render, screen, cleanup, fireEvent } from '@testing-library/react'; -import { StaticListRow } from '@/app/reader/components/sidebar/TOCItem'; +import { + StaticListRow, + CurrentPositionRow, + buildTOCDisplayItems, + isCurrentPositionItem, + type FlatTOCItem, +} from '@/app/reader/components/sidebar/TOCItem'; import { TOCItem } from '@/libs/document'; vi.mock('@/utils/misc', () => ({ getContentMd5: (s: string) => s, })); +vi.mock('@/hooks/useTranslation', () => ({ + useTranslation: () => (key: string) => key, +})); + const makeLeafItem = (overrides?: Partial): TOCItem => ({ id: 1, label: 'Chapter 1', @@ -146,6 +156,108 @@ describe('aria-current on active treeitem', () => { }); }); +describe('CurrentPositionRow', () => { + it('renders the "Current position" label and the current page number', () => { + render( +
+ +
, + ); + expect(screen.getByText('Current position')).toBeTruthy(); + expect(screen.getByText('42')).toBeTruthy(); + }); + + it('renders an open-book icon', () => { + const { container } = render(); + expect(container.querySelector('svg')).toBeTruthy(); + }); + + it('indents according to depth (matching a child of the active item)', () => { + const { container } = render(); + const row = container.querySelector('[role="treeitem"]') as HTMLElement; + expect(row.style.paddingInlineStart).toBe(`${(2 + 1) * 12}px`); + }); + + it('exposes the page number in its aria-label', () => { + render(); + const row = screen.getByRole('treeitem'); + expect(row.getAttribute('aria-label')).toContain('Current position'); + expect(row.getAttribute('aria-label')).toContain('7'); + }); + + it('calls onClick when clicked', () => { + const onClick = vi.fn(); + render(); + fireEvent.click(screen.getByRole('treeitem')); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('calls onClick when Enter is pressed', () => { + const onClick = vi.fn(); + render(); + fireEvent.keyDown(screen.getByRole('treeitem'), { key: 'Enter' }); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('is focusable (tabIndex=0) when an onClick is provided', () => { + render(); + expect(screen.getByRole('treeitem').getAttribute('tabindex')).toBe('0'); + }); +}); + +describe('buildTOCDisplayItems', () => { + const makeFlat = (count: number, depth = 0): FlatTOCItem[] => + Array.from({ length: count }, (_, i) => ({ + item: { id: i + 1, label: `Chapter ${i}`, href: `ch${i}.html`, index: i }, + depth, + index: i, + })); + + it('inserts a current-position row right after the active item', () => { + const flat = makeFlat(4); + const result = buildTOCDisplayItems(flat, 'ch1.html', 10); + expect(result).toHaveLength(5); + const inserted = result[2]!; + expect(isCurrentPositionItem(inserted)).toBe(true); + if (isCurrentPositionItem(inserted)) expect(inserted.page).toBe(10); + }); + + it('indents the current-position row one level deeper than the active item', () => { + const flat = makeFlat(3, 1); + const result = buildTOCDisplayItems(flat, 'ch0.html', 5); + const inserted = result[1]!; + expect(isCurrentPositionItem(inserted)).toBe(true); + if (isCurrentPositionItem(inserted)) expect(inserted.depth).toBe(2); + }); + + it('keeps the active item index unchanged so auto-scroll still targets it', () => { + const flat = makeFlat(4); + const result = buildTOCDisplayItems(flat, 'ch2.html', 9); + const activeIdx = result.findIndex( + (r) => !isCurrentPositionItem(r) && r.item.href === 'ch2.html', + ); + expect(activeIdx).toBe(2); + expect(isCurrentPositionItem(result[3]!)).toBe(true); + }); + + it('returns the original list when no item matches the active href', () => { + const flat = makeFlat(3); + const result = buildTOCDisplayItems(flat, 'missing.html', 5); + expect(result).toBe(flat); + }); + + it('returns the original list when the current page is unknown', () => { + const flat = makeFlat(3); + expect(buildTOCDisplayItems(flat, 'ch1.html', null)).toBe(flat); + expect(buildTOCDisplayItems(flat, 'ch1.html', undefined)).toBe(flat); + }); + + it('returns the original list when there is no active href', () => { + const flat = makeFlat(3); + expect(buildTOCDisplayItems(flat, null, 5)).toBe(flat); + }); +}); + describe('TOCView tree role', () => { it('static list container has role="tree"', async () => { // This is tested indirectly via TOCView, but we verify the container role diff --git a/apps/readest-app/src/app/reader/components/sidebar/TOCItem.tsx b/apps/readest-app/src/app/reader/components/sidebar/TOCItem.tsx index a1dd34d7..491af624 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/TOCItem.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/TOCItem.tsx @@ -1,6 +1,8 @@ import clsx from 'clsx'; import React, { useCallback } from 'react'; +import { FiBookOpen } from 'react-icons/fi'; import { TOCItem } from '@/libs/document'; +import { useTranslation } from '@/hooks/useTranslation'; import { getContentMd5 } from '@/utils/misc'; const createExpanderIcon = (isExpanded: boolean) => { @@ -30,6 +32,42 @@ export interface FlatTOCItem { isExpanded?: boolean; } +// Synthetic row injected right under the active TOC item to surface the +// current reading page (see buildTOCDisplayItems). +export interface CurrentPositionItem { + isCurrentPosition: true; + depth: number; + page: number; +} + +export type TOCDisplayItem = FlatTOCItem | CurrentPositionItem; + +export const isCurrentPositionItem = (item: TOCDisplayItem): item is CurrentPositionItem => + 'isCurrentPosition' in item; + +// Insert a "current position" row immediately after the active TOC item so the +// reader can see exactly how far they've progressed within the highlighted +// section. The row sits one level deeper than the active item. Inserting it +// *after* the active item leaves that item's index untouched, so the auto-scroll +// logic in TOCView keeps targeting the right row. +export const buildTOCDisplayItems = ( + flatItems: FlatTOCItem[], + activeHref: string | null, + currentPage: number | null | undefined, +): TOCDisplayItem[] => { + if (!activeHref || currentPage == null) return flatItems; + const activeIndex = flatItems.findIndex((f) => f.item.href === activeHref); + if (activeIndex === -1) return flatItems; + const currentRow: CurrentPositionItem = { + isCurrentPosition: true, + depth: flatItems[activeIndex]!.depth + 1, + page: currentPage, + }; + const result: TOCDisplayItem[] = flatItems.slice(); + result.splice(activeIndex + 1, 0, currentRow); + return result; +}; + const TOCItemView = React.memo<{ bookKey: string; flatItem: FlatTOCItem; @@ -165,3 +203,56 @@ export const StaticListRow: React.FC = ({ ); }; + +export const CurrentPositionRow: React.FC<{ + depth: number; + page: number; + onClick?: () => void; +}> = ({ depth, page, onClick }) => { + const _ = useTranslation(); + const label = _('Current position'); + + const handleClick = useCallback( + (event: React.MouseEvent | React.KeyboardEvent) => { + event.preventDefault(); + onClick?.(); + }, + [onClick], + ); + + return ( +
+
e.key === 'Enter' && handleClick(e) : undefined} + className={clsx( + 'flex w-full items-center rounded-md py-4 sm:py-2', + 'text-bold-in-eink sm:bg-base-300/65 sm:text-base-content text-blue-500', + onClick && 'cursor-pointer sm:hover:bg-base-300/75', + )} + style={{ paddingInlineStart: `${(depth + 1) * 12}px` }} + > +
+
+ ); +}; diff --git a/apps/readest-app/src/app/reader/components/sidebar/TOCView.tsx b/apps/readest-app/src/app/reader/components/sidebar/TOCView.tsx index 77a04d5f..4d084487 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/TOCView.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/TOCView.tsx @@ -8,7 +8,13 @@ import { useReaderStore } from '@/store/readerStore'; import { useSidebarStore } from '@/store/sidebarStore'; import { eventDispatcher } from '@/utils/event'; import { useTextTranslation } from '../../hooks/useTextTranslation'; -import { FlatTOCItem, StaticListRow } from './TOCItem'; +import { + buildTOCDisplayItems, + CurrentPositionRow, + FlatTOCItem, + isCurrentPositionItem, + StaticListRow, +} from './TOCItem'; import { computeExpandedSet, getItemIdentifier } from './tocTree'; const flattenTOC = (items: TOCItem[], expandedItems: Set, depth = 0): FlatTOCItem[] => { @@ -177,6 +183,13 @@ const TOCView: React.FC<{ const activeHref = progress?.sectionHref ?? null; const flatItems = useMemo(() => flattenTOC(toc, expandedItems), [toc, expandedItems]); + // Inject a "current position" row under the active item showing the current + // reading page. It sits after the active item, so flatItems indices (used by + // the auto-scroll effects) stay valid against this rendered list. + const displayItems = useMemo( + () => buildTOCDisplayItems(flatItems, activeHref, progress?.page), + [flatItems, activeHref, progress?.page], + ); // Keep the refs read by the OverlayScrollbars `initialized` callback current. activeHrefRef.current = activeHref; flatItemsRef.current = flatItems; @@ -204,6 +217,13 @@ const TOCView: React.FC<{ [bookKey, getView], ); + const handleCurrentPositionClick = useCallback(() => { + const location = getProgress(bookKey)?.location; + if (!location) return; + eventDispatcher.dispatch('navigate', { bookKey, cfi: location }); + getView(bookKey)?.goTo(location); + }, [bookKey, getView, getProgress]); + useEffect(() => { if (!isSideBarVisible || sideBarBookKey !== bookKey) { userScrolledRef.current = false; @@ -287,16 +307,28 @@ const TOCView: React.FC<{ }, 10000); }} style={{ height: containerHeight }} - totalCount={flatItems.length} - itemContent={(index) => ( - - )} + totalCount={displayItems.length} + itemContent={(index) => { + const row = displayItems[index]!; + if (isCurrentPositionItem(row)) { + return ( + + ); + } + return ( + + ); + }} overscan={500} />