Annotations and bookmarks inserted into KOReader via the Readest sync
plugin were missing the chapter field, which native KOReader highlights
stamp at creation time. Downstream tools that group highlights by
chapter (e.g. obsidian-koreader-highlights, KOReader's own Markdown
exporter) treated these as orphans.
Resolve the chapter title from the xpointer using the same TOC call
that ReaderHighlight uses natively, and include it on both annotation
and bookmark item tables.
Closes#4133
* feat(koplugin/library): data layer + busted harness + design doc
- LibraryStore: per-user SQLite index merging cloud + local books by
partial-md5 hash. listBooks/listBookshelfBooks/listBookshelfGroups,
upsertBook with cloud_present/local_present OR-merge + _force/clear
sentinels, parseSyncRow, getChangedBooks for tombstone push.
- EXTS table mirroring web's document.ts.
- busted harness with KOReader stubs (G_reader_settings, DataStorage,
lua-ljsqlite3 against :memory:); spec_helper, exts_spec, smoke_spec,
librarystore_spec covering schema, sort, group nesting, dedupe.
- Library design doc + spec README.
- pnpm test:lua wired through root + app package.json; lint-koplugin
recurses into library/ + spec/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(koplugin/library): cloud sync — push, pull, upload, delete, downloads
- Spore methods: pullBooks (incremental /sync), getDownloadUrl,
getUploadUrl, listFiles, deleteFile.
- syncbooks.lua: pushBook + pushChangedBooks (advances watermark to
max(updated_at, deleted_at)), syncBooks(opts, mode) for push/pull/both,
downloadBook + downloadCover (sync socket.http with file sink; cover
download via fork+poll with single-slot queue + visible-page filter +
coalesced refresh), uploadBook (presigned-PUT flow + best-effort
cover), deleteCloudFiles (list-then-delete-each, mirrors
cloudService.deleteBook).
- SyncAuth.withFreshToken wrapper resolves the ensureClient race; 401/403
unified across syncconfig + syncannotations.
- Cloud + local book covers shared by partial-md5 hash; cover.png cached
at <settings>/readest_covers/<hash>.png with sentinel for known 404s.
- syncbooks_spec covers row-to-wire conversion + file_key shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(koplugin/library): local discovery — sidecar scanner + cover provider
- localscanner.lightScan iterates ReadHistory entries, reads
partial_md5_checksum from .sdr sidecars, and upserts local rows.
Slow filesystem walks deferred to fullSidecarWalk (24h-gated).
- coverprovider wraps BookInfoManager:getBookInfo for local books with
graceful FakeCover fallback when coverbrowser is absent.
- localscanner_spec + coverprovider_spec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(koplugin/library): UI — widget, item, view menu, FileManager hooks
- librarywidget: full-screen Menu mixed in with CoverMenu + Mosaic/List
per zen_ui's group_view pattern. Title-bar tap → view menu, search via
left icon, drill-in/back for grouping. Async cloud sync deferred via
scheduleIn so the menu paints before HTTP fires.
- libraryitem: BIM patch for cloud-only (readest-cloud://) and group
(readest-group://) URIs; group-cover composer (2x2 mosaic for grid,
same in list) with cache key derived from the actual first-N hashes
for content-based invalidation; ListMenuItem update + paintTo patches
for wider list-mode cover strip and cloud-up/down icon overlay.
- libraryviewmenu: ButtonDialog with View/Group by/Sort by/Actions.
Default Group by = Groups (parity with web), values authors/groups.
- librarypaint: partial-page e-ink repaint shim adapted from zen_ui.
- main.lua: Library menu entry, dispatcher actions (Open Library / Push
/ Pull as general; progress + annotations as reader-only),
"Add to Readest" button in FileManager's long-press file dialog
(dedupe by partial_md5; bumps updated_at when present, inserts a
fresh local-only row otherwise; un-tombstones via _clear_fields).
Push books on Library open when auto_sync is on, pull-only otherwise.
- Long-press action sheet with Readest BookDetailView parity:
Remove from Cloud & Device / Cloud Only / Device Only,
Upload to Cloud, Download Book / Cover / All.
- Cloud-down + cloud-up SVG icons (LiaCloudDownloadAltSolid /
LiaCloudUploadAltSolid) painted in the right-side wpageinfo slot.
- i18n catalog updated for new strings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(koplugin/library): split libraryitem into focused modules
libraryitem.lua had grown to 1018 lines mixing five unrelated
concerns. Split along the natural seams:
cloud_covers — readest-cloud:// URI scheme, on-disk <hash>.png
cache, single-slot async download queue, visible-page
filter
group_covers — readest-group:// URI scheme, 2x2 mosaic composer with
content-derived cache key (first-N hashes), cell
layout table
cloud_icons — bundled cloud-up/cloud-down SVG loader, IconWidget
cache, paint-overlay positioning
list_strip — list-mode group row builder (4-cover wider strip
replacing ListMenu's square cover slot)
bim_patch — BookInfoManager:getBookInfo router (cloud / group /
local) + ListMenuItem update + paintTo patches; owns
the _library_local_paths set and orig BIM reference
libraryitem.lua is now 141 lines: just the entry-table constructors
(entry_from_row, entry_from_group, entry_back) plus thin install /
set_visible_hashes delegates. Each new module is 88-216 lines.
No behavior change — same 113 specs pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(koplugin): co-locate dev tooling, ship-zip exclusions, CI job split
- apps/readest.koplugin/scripts/build-koplugin.mjs: local sideloading
build with the same exclusions the release workflow uses.
- Move lint-koplugin + test-koplugin from apps/readest-app/scripts/ to
apps/readest.koplugin/scripts/. All koplugin dev tooling now lives
with the koplugin and is excluded from the published release zip.
- Rename to .mjs so Node treats them as ESM without the reparse warning
(the i18n CommonJS scripts stay .js).
- Release workflow: zip -r exclusions for scripts/, docs/, spec/,
.busted so dev artifacts don't ship to end users.
- PR workflow: split build_web_app into build_web_app + test_web_app
for parallelism. The test job installs luarocks + busted +
lsqlite3complete and runs pnpm test:lua. test-koplugin.mjs now
hard-fails (instead of soft-skipping) when CI=true and a tool is
missing — a broken CI toolchain previously exited 0 silently.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
Add "Full sync all annotations" menu item that pushes and pulls all
annotations regardless of the last sync timestamp, enabling users to
sync old highlights that were created before the plugin was installed.
Changes:
- Add full_sync parameter to push/pull that bypasses timestamp filter
and uses since=0 for pulling all server annotations
- Deduplicate by annotation ID alongside position-based dedup
- Store server ID on pulled annotations and reuse it when pushing
- Parse ISO 8601 timestamps from server to preserve original
created/updated dates instead of using current time
- Resolve KOReader page numbers from xpointers via getPageFromXPointer
- Resolve Readest page numbers from CFI via getCFIProgress on pull
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add bidirectional annotation/highlight sync between KOReader and Readest:
- Add xpointer0/xpointer1 fields to BookNote and DBBookNote types for
KOReader XPointer positions alongside Readest's CFI format
- Extend transform layer to pass through xpointer fields to/from DB
- Convert CFI→XPointer on push and XPointer→CFI on pull in useNotesSync,
discarding notes that fail conversion
- Support KOReader's text()[K].N indexed text node format in xcfi.ts for
paragraphs with inline elements (e.g. <a> page anchors)
- Generate KOReader-compatible XPointers: text().N for single text nodes,
text()[K].N only when multiple direct text nodes exist
- Skip cfi-inert elements (injected by Readest at runtime) in XPointer
path building and resolution
- Map highlight colors between KOReader and Readest color systems
- Implement KOReader plugin annotation push/pull with deterministic IDs,
auto-sync on document open/close, and UIManager refresh on pull
- Refactor koplugin into focused modules: syncauth, syncconfig,
syncannotations, selfupdate
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>