From f5df80f1547a49d081ca127eb131441db1dc6133 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Sun, 22 Mar 2026 00:58:38 +0800 Subject: [PATCH] feat(koplugin): support sync bookmarks between Koreader and Readest devices (#3580) --- README.md | 52 +++++----- apps/readest.koplugin/syncannotations.lua | 113 ++++++++++++++-------- 2 files changed, 100 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 2137e732..ef5ab305 100644 --- a/README.md +++ b/README.md @@ -45,38 +45,38 @@
✅ Implemented
-| **Feature** | **Description** | **Status** | -| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------- | -| **Multi-Format Support** | Support EPUB, MOBI, KF8 (AZW3), FB2, CBZ, TXT, PDF | ✅ | -| **Scroll/Page View Modes** | Switch between scrolling or paginated reading modes. | ✅ | -| **Full-Text Search** | Search across the entire book to find relevant sections. | ✅ | -| **Annotations and Highlighting** | Add highlights, bookmarks, and notes to enhance your reading experience and use instant mode for quicker interactions. | ✅ | -| **Dictionary/Wikipedia Lookup** | Instantly look up words and terms when reading. | ✅ | -| **[Parallel Read][link-parallel-read]** | Read two books or documents simultaneously in a split-screen view. | ✅ | -| **Customize Font and Layout** | Adjust font, layout, theme mode, and theme colors for a personalized experience. | ✅ | -| **Code Syntax Highlighting** | Read software manuals with rich coloring of code examples. | ✅ | -| **File Association and Open With** | Quickly open files in Readest in your file browser with one-click. | ✅ | -| **Library Management** | Organize, sort, and manage your entire ebook library. | ✅ | -| **OPDS/Calibre Integration** | Integrate OPDS/Calibre to access online libraries and catalogs. | ✅ | -| **Translate with DeepL and Yandex** | From a single sentence to the entire book—translate instantly. | ✅ | -| **Text-to-Speech (TTS) Support** | Enjoy smooth, multilingual narration—even within a single book. | ✅ | -| **Sync across Platforms** | Synchronize book files, reading progress, notes, and bookmarks across all supported platforms. | ✅ | -| **Accessibility** | Provides full keyboard navigation and supports for screen readers such as VoiceOver, TalkBack, NVDA, and Orca. | ✅ | -| **Visual & Focus Aids** | Reading ruler, paragraph-by-paragraph reading mode, and speed reading features. | ✅ | +| **Feature** | **Description** | **Status** | +| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | ---------- | +| **Multi-Format Support** | Support EPUB, MOBI, KF8 (AZW3), FB2, CBZ, TXT, PDF | ✅ | +| **Scroll/Page View Modes** | Switch between scrolling or paginated reading modes. | ✅ | +| **Full-Text Search** | Search across the entire book to find relevant sections. | ✅ | +| **Annotations and Highlighting** | Add highlights, bookmarks, and notes to enhance your reading experience and use instant mode for quicker interactions. | ✅ | +| **Dictionary/Wikipedia Lookup** | Instantly look up words and terms when reading. | ✅ | +| **[Parallel Read][link-parallel-read]** | Read two books or documents simultaneously in a split-screen view. | ✅ | +| **Customize Font and Layout** | Adjust font, layout, theme mode, and theme colors for a personalized experience. | ✅ | +| **Code Syntax Highlighting** | Read software manuals with rich coloring of code examples. | ✅ | +| **File Association and Open With** | Quickly open files in Readest in your file browser with one-click. | ✅ | +| **Library Management** | Organize, sort, and manage your entire ebook library. | ✅ | +| **OPDS/Calibre Integration** | Integrate OPDS/Calibre to access online libraries and catalogs. | ✅ | +| **Translate with DeepL and Yandex** | From a single sentence to the entire book—translate instantly. | ✅ | +| **Text-to-Speech (TTS) Support** | Enjoy smooth, multilingual narration—even within a single book. | ✅ | +| **Sync across Platforms** | Synchronize book files, reading progress, notes, and bookmarks across all supported platforms. | ✅ | +| [**Sync with Koreader**][link-kosync-wiki] | Synchronize reading progress, notes, and bookmarks with [Koreader][link-koreader] devices. | ✅ | +| **Accessibility** | Provides full keyboard navigation and supports for screen readers such as VoiceOver, TalkBack, NVDA, and Orca. | ✅ | +| **Visual & Focus Aids** | Reading ruler, paragraph-by-paragraph reading mode, and speed reading features. | ✅ | ## Planned Features
🛠 Building
🔄 Planned
-| **Feature** | **Description** | **Priority** | -| ------------------------------------------ | ------------------------------------------------------------------------------------------ | ------------ | -| [**Sync with Koreader**][link-kosync-wiki] | Synchronize reading progress, notes, and bookmarks with [Koreader][link-koreader] devices. | 🛠 | -| **AI-Powered Summarization** | Generate summaries of books or chapters using AI for quick insights. | 🛠 | -| **Advanced Reading Stats** | Track reading time, pages read, and more for detailed insights. | 🛠 | -| **Audiobook Support** | Extend functionality to play and manage audiobooks. | 🔄 | -| **Handwriting Annotations** | Add support for handwriting annotations using a pen on compatible devices. | 🔄 | -| **In-Library Full-Text Search** | Search across your entire ebook library to find topics and quotes. | 🔄 | +| **Feature** | **Description** | **Priority** | +| ------------------------------- | -------------------------------------------------------------------------- | ------------ | +| **AI-Powered Summarization** | Generate summaries of books or chapters using AI for quick insights. | 🛠 | +| **Advanced Reading Stats** | Track reading time, pages read, and more for detailed insights. | 🛠 | +| **Audiobook Support** | Extend functionality to play and manage audiobooks. | 🔄 | +| **Handwriting Annotations** | Add support for handwriting annotations using a pen on compatible devices. | 🔄 | +| **In-Library Full-Text Search** | Search across your entire ebook library to find topics and quotes. | 🔄 | Stay tuned for continuous improvements and updates! Contributions and suggestions are always welcome—let's build the ultimate reading experience together. 😊 diff --git a/apps/readest.koplugin/syncannotations.lua b/apps/readest.koplugin/syncannotations.lua index 208a3024..c23969e0 100644 --- a/apps/readest.koplugin/syncannotations.lua +++ b/apps/readest.koplugin/syncannotations.lua @@ -60,18 +60,18 @@ function SyncAnnotations:getAnnotations(ui, settings, book_hash, meta_hash) local notes = {} for _, item in ipairs(annotations) do + local updated_at = self:parseDatetimeToMs(item.datetime_updated or item.datetime) + if updated_at <= last_sync then + goto skip + end + local pos0 = item.pos0 local pos1 = item.pos1 if type(pos0) == "table" then pos0 = nil end if type(pos1) == "table" then pos1 = nil end - if pos0 then - local updated_at = self:parseDatetimeToMs(item.datetime_updated or item.datetime) - if updated_at <= last_sync then - goto skip - end - local note_type = item.drawer and "annotation" or "bookmark" - local id = self:generateNoteId(book_hash, note_type, tostring(pos0), pos1 and tostring(pos1)) + if item.drawer and pos0 then + -- Annotation (highlight/underline/strikeout): has drawer and pos0/pos1 local style = "highlight" if item.drawer == "underscore" then style = "underline" @@ -79,23 +79,38 @@ function SyncAnnotations:getAnnotations(ui, settings, book_hash, meta_hash) style = "squiggly" end - local note = { + local id = self:generateNoteId(book_hash, "annotation", tostring(pos0), pos1 and tostring(pos1)) + table.insert(notes, { bookHash = book_hash, metaHash = meta_hash, id = id, - type = note_type, + type = "annotation", xpointer0 = tostring(pos0), xpointer1 = pos1 and tostring(pos1) or nil, text = item.text or "", note = item.note or "", - style = note_type == "annotation" and style or nil, - color = note_type == "annotation" and KO_TO_READEST_COLOR[item.color or "yellow"] or nil, + style = style, + color = KO_TO_READEST_COLOR[item.color or "yellow"], page = item.pageno, createdAt = self:parseDatetimeToMs(item.datetime), updatedAt = updated_at, - } - - table.insert(notes, note) + }) + elseif not item.drawer and type(item.page) == "string" then + -- Bookmark: no drawer, position in page field (xpointer string) + local page_xp = item.page + local id = self:generateNoteId(book_hash, "bookmark", page_xp) + table.insert(notes, { + bookHash = book_hash, + metaHash = meta_hash, + id = id, + type = "bookmark", + xpointer0 = page_xp, + text = item.text or "", + note = item.note or "", + page = item.pageno, + createdAt = self:parseDatetimeToMs(item.datetime), + updatedAt = updated_at, + }) end ::skip:: end @@ -208,10 +223,16 @@ function SyncAnnotations:pull(ui, settings, client, book_hash, meta_hash, dialog local annotation_mgr = ui.annotation if not annotation_mgr then return end - local existing_positions = {} + -- Build dedup sets: annotations by pos0|pos1, bookmarks by page xpointer + local existing_annotations = {} + local existing_bookmarks = {} for _, item in ipairs(annotation_mgr.annotations) do - local key = tostring(item.pos0) .. "|" .. tostring(item.pos1 or "") - existing_positions[key] = true + if item.drawer then + local key = tostring(item.pos0) .. "|" .. tostring(item.pos1 or "") + existing_annotations[key] = true + elseif type(item.page) == "string" then + existing_bookmarks[item.page] = true + end end local added = 0 @@ -221,37 +242,51 @@ function SyncAnnotations:pull(ui, settings, client, book_hash, meta_hash, dialog end local xp0 = note.xpointer0 - local xp1 = note.xpointer1 if not xp0 then goto continue end - local key = xp0 .. "|" .. (xp1 or "") - if existing_positions[key] then goto continue end - - local drawer = "lighten" local note_type = note.type + local item + if note_type == "bookmark" then - drawer = nil - elseif note.style == "underline" then - drawer = "underscore" - elseif note.style == "squiggly" then - drawer = "strikeout" + if existing_bookmarks[xp0] then goto continue end + + item = { + page = xp0, + text = note.text or "", + note = note.note or "", + pageno = note.page, + datetime = os.date("%Y-%m-%d %H:%M:%S"), + } + existing_bookmarks[xp0] = true + else + local xp1 = note.xpointer1 + local key = xp0 .. "|" .. (xp1 or "") + if existing_annotations[key] then goto continue end + + local drawer = "lighten" + if note.style == "underline" then + drawer = "underscore" + elseif note.style == "squiggly" then + drawer = "strikeout" + end + + item = { + pos0 = xp0, + pos1 = xp1 or xp0, + page = xp0, + text = note.text or "", + note = note.note or "", + drawer = drawer, + color = READEST_TO_KO_COLOR[note.color] or "yellow", + pageno = note.page, + datetime = os.date("%Y-%m-%d %H:%M:%S"), + } + existing_annotations[key] = true end - local item = { - pos0 = xp0, - pos1 = xp1 or xp0, - page = xp0, - text = note.text or "", - note = note.note or "", - drawer = drawer, - color = READEST_TO_KO_COLOR[note.color] or "yellow", - pageno = note.page, - datetime = os.date("%Y-%m-%d %H:%M:%S"), - } local index = annotation_mgr:addItem(item) ui:handleEvent(Event:new("AnnotationsModified", { item, index_modified = index })) logger.dbg("ReadestSync: Added annotation from sync:", item) - existing_positions[key] = true added = added + 1 ::continue::