- i18n loader at apps/readest.koplugin/i18n.lua: isolated callable,
falls back to KOReader's gettext when a string is untranslated
- Translation catalog at locales/<i18next-code>/translation.po for 31
languages, mirroring apps/readest-app/public/locales/
- scripts/extract-i18n.js (scan _("...") and _([[...]]); preserve
existing, drop obsolete, add new) and scripts/apply-translations.js
(bulk import from /tmp/koplugin-translations/<lang>.json)
- Mirror apps/readest-app SyncInfoDialog: rename showMetaHashInfo to
showSyncInfo, dialog title "Sync Info", new Last Synced row computed
as max(last_synced_at_config, last_synced_at_notes) from doc_settings
- syncconfig.lua / syncannotations.lua mark per-book sync timestamps
on push/pull success
- Rename "Meta Hash" -> "Book Fingerprint" in koplugin and
apps/readest-app SyncInfoDialog.tsx; translations propagated to
all readest-app locales
- "book config" -> "reading progress" wording across user-facing
strings (matches QiuYukang fork terminology)
- Replace "Log out as " / "Login failed: " concat prefixes with
T(_("...%1..."), arg) placeholder pattern (RTL / verb-final friendly)
- pnpm lint:lua: luajit -b syntax check across koplugin .lua files;
soft-skips when luajit is missing locally; CI installs luajit and
runs the check unconditionally
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends the dictionary system beyond StarDict/MDict with two more open
formats and a pluggable Web Search tier so users can fall back to online
sources when their offline bundles miss a word.
Formats:
- DICT (dictd, RFC 2229): .index + .dict.dz bundles. Shared DictZip
parsing with StarDict via new dictZip.ts helper.
- Slob (Aard 2): self-contained .slob containers, zlib-compressed
utf-8 entries; non-zlib/non-utf-8 bundles flagged unsupported at
import.
Web Search:
- Built-in templates for Google, Urban Dictionary, Merriam-Webster
(seeded into providerOrder, disabled by default).
- Custom URL templates via %WORD% placeholder, URL-encoded at
substitution; entries persist in settings.webSearches.
- V1 renders an "Open in {{name}}" external link (iframe embedding is
blocked by every major target site's X-Frame-Options).
UI:
- CustomDictionaries panel: flat outline-primary buttons for Import /
Add Web Search, end-aligned type badges for a uniform column,
hover states, compact tips block.
- Dictionary popup: bottom-right Manage icon (tooltip-only) deep-links
into Settings → Language → Dictionaries; rounded-corner clipping fix
on the tab strip.
File picker accepts .index and .slob; importer recognizes DICT and
Slob bundles and reads bundle metadata for friendly names.
Tests cover DICT/Slob readers and providers with real freedict-eng-nld
fixtures, web search substitution + provider rendering, and the new
store CRUD for web searches.
Closes#4038
Tapping the section info or progress bar overlays did not dismiss an
open annotation popup because the dismiss flow listens for
iframe-single-click, which only fires from inside the foliate iframe.
These overlays sit above the iframe and intercept taps before the
iframe sees them, so the popup stayed open.
Dispatch iframe-single-click first in their click handlers; if a popup
consumes it (existing useTextSelector handler dismisses popup/selection
and returns true), skip the original action.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ensureSharedBookLocal helper makes sure the local library has both the
Book entry and the bytes on disk after /import succeeds; navigating
into the reader before this lands on "Book not found"
- ShareLanding navigates via navigateToReader (path form on web) so the
reader actually renders instead of hitting the App Router stub and
going blank
- Loading + progress UI on the landing page while bytes stream in;
Open-in-app disabled mid-import to avoid races
- UserInfo header: vertically center avatar with name/email, tighter
mobile gap, and a fillContainer prop on UserAvatar so a parent can
size the box via classes without the inline style fighting back
- Rename "Share current page" -> "Share reading progress" (and matching
post-generation hint) and shrink the dialog from 480 to 460px
- Drop the unused Reload Page menu item from SettingsMenu
- Translate the two new i18n keys across 31 locales
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Direct file download from the public share landing carries rights /
abuse risk. Replace the Download button with the Open-in-app deep link
in both the logged-in flow (now: Add to library + Open in app) and the
anonymous flow (now: Open in app + Get Readest footnote).
The /api/share/[token]/download route is left intact so re-enabling
the button later is a one-file UI change.
Add a Share Book feature that generates an expiring HTTPS share URL plus a
parallel readest://share/{token} deep link. Recipients land on /s/{token},
where logged-in users can one-tap "Add to my library" (R2 server-side
byte-copy) and anonymous users download the book or open it in the app.
Sharers manage active links from a dedicated "Manage Shared Links" panel
under user settings.
Highlights:
- 9 new App Router endpoints under /api/share (create, [token], cover,
og.png, download, download/confirm, import, revoke, list).
- /s landing page with branded next/og chat unfurl image and SSR auth-cookie
detection so logged-in recipients see "Add to my library" as the primary
action without layout shift.
- Server-side R2 byte-copy for /import preserves the project's existing
invariant that every files.file_key is prefixed with its row's user_id;
stats / purge / delete / download routes work unchanged. URL-encodes the
copy source so titles with spaces or '&' don't break the copy.
- Universal 7-day expiry cap, no tier differentiation, no "never". DMCA-risk
reduction. Picker defaults to 3 days.
- Position-aware shares: "Share current page" toggle (off by default for
privacy) attaches the sharer's CFI; recipient lands at the same paragraph.
- Per-user 50-share cap, rate limiting via Cache-Control: no-store on
token-bearing responses, atomic SQL increment for download_count via a
SECURITY DEFINER function so the public confirm beacon stays safe under
concurrent fire.
- Soft revocation: presigned download URLs (5-min TTL) cannot be cancelled
before TTL; documented as accepted v1 behavior.
- token + token_hash hybrid storage: public endpoints look up by hash and
never select the raw token, so accidental SELECT-* leakage on a public
route can't expose the bearer credential.
- Mobile / desktop Tauri share via tauri-plugin-sharekit; web falls back to
navigator.share with a clipboard fallback when no native share method
exists. Share-sheet dismissal no longer silently copies.
UI:
- New Dialog with a settings-card group: iOS-style segmented duration picker
+ toggle slider for "Share current page", on a single row each.
- Reader top-bar Share button, library context-menu Share entry, manage-
shares list with cover thumbnails and overflow menu.
- New <SegmentedControl> primitive in src/components for reuse.
Coverage:
- Unit tests for token utils + URL parser (20 new tests, full suite at 3445).
- 31 locales translated for all new strings; en plurals hand-added per the
project's hand-curated en convention.
DB migration in docker/volumes/db/migrations/002_add_book_shares.sql adds
the book_shares table, RLS policies, and the increment_book_share_download
RPC. Migration is idempotent.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The footer NavigationBar and slide-up panels (Color, Progress, Font &
Layout) used `bg-base-200`, which is computed as a slightly-darker shade
of the theme's background. In eink mode this rendered as a visible grey
strip even when the user picked a pure white/black theme.
Switch to `bg-base-100` in eink mode and add a `border-base-content` top
border so the bar stays visually separated from the page area without
relying on a tinted background, matching the existing eink treatment
elsewhere (e.g. PageNavigationButtons, ImageViewer).
* fix(rsvp): seed local position from synced BookConfig on resume
* refactor(rsvp): simplify seedPosition
Consolidate the matched/mismatched write paths into one localStorage
write, extract stripCfiPath() to a module-level helper, and trim the
comments around it.
* fix(rsvp): make progress bar draggable on mobile
Three coordinated fixes so the RSVP overlay's seek bar works reliably
under touch:
- Add `touch-action: none` on the slider so the mobile browser stops
claiming the gesture for scroll/pan and firing pointercancel mid-drag.
- Hoist the `.rsvp-controls`/`.rsvp-header` exclusion to the top of the
overlay's touchend handler so a successful drag isn't immediately
re-interpreted as a speed-change swipe.
- Guard `releasePointerCapture` with `hasPointerCapture` so pointercancel
arriving after the browser has already released capture (multitouch,
app backgrounding) no longer throws NotFoundError.
* feat(annotations): preview mode for deep-link landings
When the reader opens at a deep-link CFI (e.g. clicking an exported
highlight from Obsidian), the position should not be persisted as the
user's reading progress until they actually start reading. Otherwise
the deep-link visit overwrites their last-read position and propagates
that across all sync targets.
Adds a per-book `previewMode` flag in the reader store that:
- Is set to true in FoliateViewer when the URL's `?cfi=` overrides the
saved last-position.
- Is cleared on the first user-initiated relocate (page turn / scroll),
reusing the existing reason filter in `docRelocateHandler`.
- Gates the auto progress writers:
- useProgressAutoSave — skip local config persist
- useProgressSync — skip auto-push and skip the remote-progress
view.goTo (so cloud pull doesn't yank the
user away from the previewed annotation)
- useKOSync — skip auto-push (manual pushes still respected)
Hardcover sync and Discord presence are unaffected: hardcover only
fires on explicit user button press, and Discord presence carries no
position information.
Also picks up the regenerated AndroidManifest.xml change from the
existing tauri.conf.json deep-link config (registers readest:// scheme
on Android so the smart landing page's intent:// launch resolves).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(annotations): jump in place when target book is already open
When an annotation deep link arrives while the user is already in the
reader (most common case on mobile App Links), navigateToReader was
pushing the same /reader path with a different cfi query param. The
reader's init useEffect has [] deps, so it doesn't re-run, and
FoliateViewer doesn't re-read the cfi — the view stayed put.
Detect a mounted view for the target book hash by walking
viewStates and matching the hash prefix on the bookKey. If found,
call view.goTo(cfi) directly and set previewMode so the existing
gates fire. Falls back to navigateToReader when no view is open.
Also adds a console.log on each parsed deep link to make this path
easier to debug from device logs in the future.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(annotations): deep links for highlight exports
Embed an HTTPS deep link in markdown export so clicking a highlight in
Obsidian / Notion / Mail launches Readest at the exact CFI position.
Mobile App Links / Universal Links open the native app silently when
installed; desktop attempts the readest:// scheme automatically with a
manual fallback.
- Markdown export wraps the page-number text in a per-annotation link:
https://web.readest.com/o/book/{hash}/annotation/{id}?cfi=...
- New /o/... smart landing page handles platform routing (intent:// on
Android Chrome, scheme + visibility-cancel on other Android, auto
scheme + 1 s fallback on desktop, manual button on iOS).
- Reader honors a ?cfi= query param on initial load (overrides the
saved last-position for the primary book only).
- New useOpenAnnotationLink hook handles incoming readest:// and
https://web.readest.com/o/... URLs, including cold-start (getCurrent)
and library-load deferral; supports the legacy flat shape
readest://annotation/{hash}/{id} from previous Readwise syncs.
- ReadwiseClient now emits the HTTPS deep link instead of the legacy
custom scheme.
- AASA extended with /o/* matcher; Android intent-filter for the host
has no pathPrefix so it already covers it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* i18n: translate annotation deep-link strings across all locales
Translates the 13 new keys introduced for the annotation deep-link
feature into all 31 supported locales. Replaces all 403
__STRING_NOT_TRANSLATED__ placeholders.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(reader): custom dictionaries (StarDict + MDict)
Adds a pluggable dictionary provider system. Built-in Wiktionary +
Wikipedia (extracted from the legacy single-popup model into a tabbed
shell) plus user-importable StarDict (.ifo/.idx/.dict.dz/.syn) and MDict
(.mdx/.mdd) bundles.
Settings → Language → Dictionaries: import / enable / drag-reorder /
delete (delete-mode toggle mirrors CustomFonts). Drag uses @dnd-kit with
pointer/touch/keyboard sensors. Reader popup: tabbed UI, per-tab lookup
history, scroll-aware back button, last-active tab persists. Tabs grow
to natural width up to a cap, truncate with ellipsis when crowded; phantom
bold layer prevents layout shift on focus.
StarDict reader is self-contained (replaces unused foliate-js/dict.js),
with lazy random-access binary search on .idx + .syn (~420 KB Int32Array
of byte offsets vs ~10 MB of parsed JS objects), lazy DictZip chunk
decompression via fflate streaming Inflate (cmudict/eng-nld both
chunked), and an optional .idx.offsets sidecar generated at import to
skip the init scan. Cmudict 105K-entry init drops from ~10 MB heap and
2 MB IO to ~1.7 MB heap and ~500 KB IO.
MDict uses the readest/js-mdict fork (added as a submodule, consumed via
tsconfig paths so deps stay out of readest's pnpm-lock) which adds a
browser-friendly BlobScanner reading via blob.slice(...).arrayBuffer()
— slices are lazy when the Blob is Readest's NativeFile / RemoteFile.
encrypt=2 (key-info-only) MDX is fully supported via ripemd128-based
mdxDecrypt; encrypt=1 (record-block, needs user passcode) surfaces as
unsupported.
Wikipedia annotation tool removed (Wikipedia is now a tab inside the
unified popup); legacy WiktionaryPopup / WikipediaPopup deleted. Stale
annotationQuickAction === 'wikipedia' coerced to 'dictionary' on settings
load. iOS-friendly external links: skip target="_blank" on Tauri to
avoid the WebView's "open externally" path triggering the shell scope
error; the popup's container click handler routes through openUrl.
i18n: 939 strings translated across 31 locales (30 base keys + CLDR
plural forms for ar/he/sl/pl/ru/uk/ro/it/pt/fr/es).
Test fixtures bundled: cmudict (StarDict, 105K entries), eng-nld
(StarDict, smaller), and a Longman Phrasal Verbs MDX (encrypt=2).
3396 unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(stardict): resolve fflate from js-mdict source for vitest + Next
js-mdict is consumed as TypeScript source via tsconfig paths from
packages/js-mdict/src/. Its sources `import 'fflate'` directly, but
fflate is only installed under apps/readest-app/node_modules — so
vite's import-analysis (and Next/Turbopack's resolver) can't find
fflate when it walks up from the redirected js-mdict source location.
CI's fresh checkout exposes this; locally a leftover
packages/js-mdict/node_modules/fflate from the old workspace setup
masked it.
Pin fflate resolution to apps/readest-app/node_modules/fflate in:
- vitest.config.mts (Vite alias)
- next.config.mjs (webpack alias + Turbopack resolveAlias — Turbopack
rejects absolute paths so use a project-relative form)
- tsconfig.json (paths entry so tsgo / Biome see it)
Verified by deleting packages/js-mdict/node_modules locally and
re-running pnpm test (3396 pass), pnpm lint (clean), and both
pnpm build-web and a tauri-platform Next build (clean).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The paginator's scrolled-mode scroll handler is debounced 250 ms, so
#anchor and #primaryIndex can lag behind the user's actual viewport.
Toggling out of scrolled mode within that window made
render() → scrollToAnchor(#anchor) restore the stale anchor, reverting
the position to a previously visible section.
Update foliate-js to flush the pending scroll state before flow leaves
'scrolled', and add regression coverage for the multi-section toggle path.
When a book's underlying file is missing, opening it in a dedicated
reader window showed an error toast then navigated that window to
/library, leaving a leftover library-in-reader-window the user had
to close manually. Route the recovery through a new
closeReaderWindowOrGoToLibrary() that closes the dedicated reader
window (after ensuring the main library window is visible) and only
falls back to /library navigation in the main window or on web.
Also fix a related macOS issue: the reader's CloseRequested handler
was running handleCloseBooks and calling currentWindow.destroy() on
the main window, which tore down the active book and bypassed the
Rust close-to-hide handler — making Cmd+W / traffic-light close
quit the app from the reader page (vs. correctly hiding from the
library page) and lose the active book even when the window did
hide. Skip both the cleanup and destroy on macOS for the main
window so the Rust handler hides it with the book intact, matching
the macOS minimize-to-dock convention.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On Android, long-press selects text via selectionchange while the finger
is still on the screen. The quick action handler was gated by
androidTouchEndRef and silently returned, so no popup ever opened. After
the user lifted, nothing re-ran the gated action.
Track the gated action in a small DeferredActionState ref and flush it
from the native touchend handler, so instant copy/dictionary/wikipedia/
search/translate/tts now fire on the first long-press release.
The browser delivers one large quantised delta per wheel notch, which
Chromium scrolls without interpolation — producing the jerky one-step
motion reported on Windows. Detect mouse-wheel-shaped events inside
the iframe (line-mode, or single-axis with |deltaY| ≥ 50), suppress
the native scroll, and replay the delta as an rAF exponential lerp on
the renderer's container. Trackpad / high-resolution input is left to
native scrolling so its momentum and 2-axis behaviour are preserved.
When the main window has been destroyed (Windows/Linux default close), the
reader's "go to library" button only closed the reader, leaving no library
visible. Add ensureMainLibraryWindow() that shows an existing main window
or recreates one with the 'main' label so the existing close-reader-window
wiring keeps working. Also grant the cross-window show/unminimize permissions
the call now needs.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(warichu): support warichu (割注) inline annotation layout
- Add warichu HTML transformer that converts <span class="warichu"> and
<warichu> elements into .warichu-pending placeholders during content load
- Implement runtime layout (layoutWarichu / relayoutWarichu) that measures
column position and splits text into small inline-block chunks (2 chars
each) so CSS column boundaries can break between them, preventing large
blank gaps in vertical-rl pagination
- Use column stride (column-width + column-gap) for accurate position
measurement across column boundaries
- Hook into stabilized event for initial layout and relayout on resize
- Add warichu CSS styles (inline-block chunks, half-size font, vertical align)
* fix(warichu): correct HTML slicing edge cases
- sliceHtml: re-emit tags that were already open before the slice start
so the result stays well-formed. Previously a slice past an opening
tag produced an orphan closing tag (e.g. "<b>Hello</b>"[3,5] →
"lo</b>" instead of "<b>lo</b>").
- sliceHtml / removeFirstVisibleChar / removeLastVisibleChar: treat HTML
entities (e.g. &) as one visible character so they aren't split or
truncated mid-entity (e.g. removeFirstVisibleChar("&rest") →
"amp;rest").
- buildNodes: drop a duplicate chunk.appendChild(l2).
- Add unit tests covering the above for the three pure helpers.
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
* Fix ZoomLabel not showing during pinch zoom
* fix: exponential wheel zoom and show ZoomLabel during pinch
- Use exponential wheel zoom (Math.exp) instead of additive for smoother PC scroll
- Show ZoomLabel during pinch start and pinch move
- Clean up unused setZoomSpeed state for wheel handler
The TOC occasionally flashed a scroll to the current item and then
snapped back to the top, and on slow mobile first-opens sometimes
stayed at the top entirely.
Root cause: `useOverlayScrollbars({ defer: true })` schedules OS
construction via `requestIdleCallback` with a ~2233 ms timeout. On a
busy first open the timeout fires before the browser goes idle, so OS
wraps the viewport late — and the wrap step resets the scroller's
`scrollTop` synchronously, undoing Virtuoso's earlier scroll to the
current item. Virtuoso's `rangeChanged` / `onScroll` don't propagate
the reset for another frame, so any guard based on tracked scroll
state reads stale.
* perf(store): decouple page turn from full library rewrite for large collections
Previously every page turn triggered setLibrary() which copied the entire
library array, ran refreshGroups() with MD5 hashing over all books, and
caused cascading re-renders. With ~2800 books this made reading unusable.
- Add hash-indexed Map to libraryStore for O(1) book lookups
- Add lightweight updateBookProgress() that skips array copy and refreshGroups
- Use hash index in setProgress, saveConfig, and initViewState
- Batch cover URL generation with concurrency limit on library load
Addresses #3714
* perf(import): replace filter()[0] with find() to short-circuit on first match
* fix(store): replace Object.assign state mutation with immutable spread in setConfig
* perf(persistence): remove JSON pretty-printing to reduce serialization overhead
* fix(reader): stabilize debounce reference in useIframeEvents to prevent timer reset on re-render
* perf(context): memoize provider values to prevent unnecessary consumer re-renders
* perf(store): cache visible library to avoid refiltering on every access
* perf(library): remove redundant refreshGroups call already triggered by setLibrary
* perf(import): replace O(n) splice(0,0) with O(1) push for new book insertion
* perf(import): defer library persistence to end of import batch instead of every 4 books
* perf(library): skip full library reload on reader close since store is already in sync
* fix: address PR review feedback for library perf optimizations
Correctness fixes for issues found in code review:
- fix(library): restore library reload on close-reader-window. Reader
windows are independent Tauri webviews with their own libraryStore
instance, so progress / readingStatus / move-to-front updates from
the reader do not propagate to the main window. Reload from disk
so the library reflects the changes the reader just persisted.
- perf(import): wire BookLookupIndex into importBooks. The lookupIndex
parameter on bookService.importBook had no caller, leaving the
Map-based dedup path dead. Build the index once per import batch
in app/library/page.tsx and thread it through appService.importBook
so the O(1) dedup path is actually exercised.
- perf(import): defer library save to end of batch. Add a skipSave
option to libraryStore.updateBooks and call appService.saveLibraryBooks
once after the entire import loop, instead of once per concurrency-4
sub-batch.
- fix(store): make updateBookProgress immutable. The previous in-place
mutation reused the same library array reference, bypassing Zustand
change detection AND leaving the visibleLibrary cache holding stale
Book references. Now slice the array, update the entry, and refresh
visibleLibrary. Also make readingStatus a required parameter so
future callers cannot accidentally clear it by omitting the argument.
- fix(store): make saveConfig immutable. It previously mutated the Book
object's progress / timestamps in place and used splice/unshift on
the shared library array. Now spread to a new book object and rebuild
via setLibrary. Also corrects the interface signature to return
Promise<void> (the implementation was already async).
- fix(store): make updateBook immutable for the same reason — it was
mutating the previous-state library array before spreading.
- fix(context): wrap AuthContext login/logout/refresh in useCallback.
Without this, the useMemo deps array changed every render and the
memo was a no-op, defeating the optimization the PR was trying to
add.
- fix(reader): use a ref for handlePageFlip in useMouseEvent's debounce.
The empty-deps useMemo froze the first-render handler; with the ref
the debounced wrapper always invokes the latest closure.
Test coverage added:
- library-store: immutable updateBookProgress, visibleLibrary cache
refresh, deleted-book filtering, updateBooks skipSave option
- book-data-store: immutable saveConfig, move-to-front correctness,
visibleLibrary order, persistence behavior
- import-metahash: BookLookupIndex update on new import, lookup-index
consultation before scanning books array
- auth-context (new file): context value identity stability across
re-renders, callback identity stability
- useIframeEvents (new file): debounced wheel handler dispatches to
the latest handlePageFlip after re-render
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(types): move BookLookupIndex to types/book.ts
Avoids the inline `import('@/services/bookService').BookLookupIndex`
type annotation in types/system.ts. Both the AppService interface and
the bookService implementation now import BookLookupIndex from the
canonical location alongside Book.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(import): convert importBook params to options object
Replace the long positional-argument list on appService.importBook
(saveBook, saveCover, overwrite, transient, lookupIndex) with a
single options object so callers no longer need to pad with
`undefined, undefined, undefined, undefined` to reach the parameter
they actually want to set.
Before:
await appService.importBook(file, library, undefined, undefined,
undefined, undefined, lookupIndex);
After:
await appService.importBook(file, library, { lookupIndex });
The underlying bookService.importBook is also refactored to take an
options object: required AppService callbacks (saveBookConfig,
generateCoverImageUrl) are bundled with the optional flags via an
ImportBookInternalOptions interface that extends the public
ImportBookOptions defined in types/book.ts.
All existing call sites updated to the new shape.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the inline touch-swipe event dispatching with a module-level
interceptor registry. This lets the reading ruler (priority 10) claim
drag gestures before the swipe-to-flip handler (priority 0), preventing
conflicts when dragging the ruler on touch devices.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: Add option to split words in RSVP mode
* fix(rsvp): replace lookbehind regex with lookahead-only split in getHyphenParts
* feat: Add option to split words in RSVP mode
* fix(theme): update data-theme and themeCode when system theme changes
* feat(rsvp): split on ellipsis between letters and preserve delimiter type
* fix(rsvp): fix incorrect merged line
* feat(rsvp): insert blank frame between consecutive identical words
* refactor(sidebar): replace react-window and OverlayScrollbars with react-virtuoso and CSS scrollbars
* feat(toc): smooth scroll to active chapter on sidebar open
* test(theme-store): expand dark theme palette fixture with full color tokens
* refactor: remove dead code and consolidate duplicate CSS scrollbar rules
* fix(toc): fix auto-scroll on sidebar open and improve scroll behavior
- Add isSideBarVisible to scroll effect deps so it fires on sidebar open
- Use setTimeout delay for Virtuoso to finish layout before scrolling
- Add 10s cooldown on user scroll to re-enable auto-scroll
- Use smooth scroll for short distances (<16 items), instant for longer
- Track visible center via rangeChanged for accurate distance calculation
- Fix tab navigation background opacity (bg-base-200 instead of /20)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Support HTTP Basic auth for kosync connections
* fix(kosync): use separate password field for HTTP Basic auth
- Add `password?` field to KOSyncSettings to store the plain password
alongside the existing `userkey` (md5 hash), preserving backward
compatibility for existing users
- Replace Buffer.from() with btoa() for browser-compatible Base64 encoding
- Simplify buildHeaders() to use config fields directly instead of
the useAuth union type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Wiktionary REST API does not support Chinese entries — simplified
characters return 404, traditional characters omit Chinese results, and
no pronunciation data is ever included. Switch Chinese lookups to the
Wiktionary Action API which returns full wikitext with pinyin and
definitions. Also add encodeURIComponent to the REST API URL for all
other languages.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>