Support KOReader's v2 annotation model (2024.07+) — sync was collecting nothing
KOReader replaced the bookmark store: since 2024.07, highlights/notes/
bookmarks live in ui.annotation.annotations (sidecar key 'annotations')
with new field semantics — text = highlighted text, note = user note,
drawer/color styling, pos0/pos1 as xpointers (CRE) or {page=N} tables
(pdf). ui.bookmark.bookmarks no longer exists, so collectAnnotations
returned empty and applyServerAnnotations bailed on its first guard:
annotations NEVER crossed in either direction on current KOReader
(reproduced: device log showed highlights:[] with a highlight present,
and the book's sidecar showed annotations={} with no bookmarks key).
Both functions are now dual-model via getAnnotationStore():
- collect: v2 entries normalized to the wire format (text/note from
the new fields, pdf table positions stringified to their page,
classification text+note=note / text=highlight / else=bookmark with
the label traveling in the entry text, as before); v1 mapping
corrected to KOReader's own migration semantics (v1 notes held the
highlighted text, text held the note).
- apply: v2 entries are built with drawer='lighten' for highlights
(its absence marks a page bookmark), pdf positions as {page=N},
and inserted via ui.annotation:addItem() — the module's supported
API (sorted insertion, datetime/pageno defaults). v1 path preserved
for older devices. Tombstone removal works on either array; the
v1-only onSortBookmarks call is now guarded.
Verified both models in the stub harness against the live server:
v2 push collects the device highlight; v2 pull lands server
highlights (with notes), and bookmarks with correct field shapes;
v1 roundtrip unchanged.
This commit is contained in:
@@ -835,24 +835,57 @@ function Bookhoard:collectBookData()
|
|||||||
return book_data
|
return book_data
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- KOReader stores annotations in one of two models depending on version:
|
||||||
|
-- v2 (2024.07+): self.ui.annotation.annotations; entries carry
|
||||||
|
-- text = highlighted text, note = user note, drawer/color style,
|
||||||
|
-- pos0/pos1 = xpointer (CRE) or {page=N} table (pdf), page = xpointer
|
||||||
|
-- (CRE) or number (pdf). Sidecar key "annotations".
|
||||||
|
-- v1 (older): self.ui.bookmark.bookmarks; entries carry text = note
|
||||||
|
-- label / bookmark title, notes = highlighted text, pos0/pos1 strings.
|
||||||
|
-- Returns "v2"/"v1"/nil and the entries array.
|
||||||
|
function Bookhoard:getAnnotationStore()
|
||||||
|
if self.ui.annotation and type(self.ui.annotation.annotations) == "table" then
|
||||||
|
return "v2", self.ui.annotation.annotations
|
||||||
|
end
|
||||||
|
if self.ui.bookmark and type(self.ui.bookmark.bookmarks) == "table" then
|
||||||
|
return "v1", self.ui.bookmark.bookmarks
|
||||||
|
end
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
function Bookhoard:collectAnnotations()
|
function Bookhoard:collectAnnotations()
|
||||||
local bookmarks = json_util.InitArray({})
|
local bookmarks = json_util.InitArray({})
|
||||||
local highlights = json_util.InitArray({})
|
local highlights = json_util.InitArray({})
|
||||||
local notes = json_util.InitArray({})
|
local notes = json_util.InitArray({})
|
||||||
|
|
||||||
if not self.ui.bookmark then
|
local model, entries = self:getAnnotationStore()
|
||||||
|
if not model then
|
||||||
return bookmarks, highlights, notes
|
return bookmarks, highlights, notes
|
||||||
end
|
end
|
||||||
|
|
||||||
local all_bookmarks = self.ui.bookmark.bookmarks
|
|
||||||
if not all_bookmarks then
|
|
||||||
return bookmarks, highlights, notes
|
|
||||||
end
|
|
||||||
local file_sha256 = self:getFileSHA256()
|
local file_sha256 = self:getFileSHA256()
|
||||||
local total_pages = self.ui.document:getPageCount()
|
local total_pages = self.ui.document:getPageCount()
|
||||||
local has_pages = self.ui.document.info.has_pages
|
local has_pages = self.ui.document.info.has_pages
|
||||||
|
|
||||||
for _, bm in ipairs(all_bookmarks) do
|
local function wirePos(p, fallback_page)
|
||||||
|
-- v2 pdf positions are tables {page=N,x,y}; the wire format is a
|
||||||
|
-- string — the page number is the durable locator there.
|
||||||
|
if type(p) == "table" then
|
||||||
|
return tostring(p.page or fallback_page or "")
|
||||||
|
end
|
||||||
|
return p or ""
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, bm in ipairs(entries) do
|
||||||
|
local sel_text, user_note
|
||||||
|
if model == "v2" then
|
||||||
|
sel_text = bm.text or "" -- highlighted text
|
||||||
|
user_note = bm.note or "" -- user note / bookmark label
|
||||||
|
else
|
||||||
|
sel_text = bm.notes or "" -- v1: notes held the highlighted text
|
||||||
|
user_note = bm.text or "" -- v1: text held the note/label
|
||||||
|
end
|
||||||
|
|
||||||
-- Thin-client policy: no per-annotation CRE lookups here (a
|
-- Thin-client policy: no per-annotation CRE lookups here (a
|
||||||
-- getPageFromXPointer call each is the most expensive thing this
|
-- getPageFromXPointer call each is the most expensive thing this
|
||||||
-- loop can do on weak hardware). Percentages are arithmetic for
|
-- loop can do on weak hardware). Percentages are arithmetic for
|
||||||
@@ -867,19 +900,19 @@ function Bookhoard:collectAnnotations()
|
|||||||
local entry = {
|
local entry = {
|
||||||
chapter = chapter,
|
chapter = chapter,
|
||||||
datetime = bm.datetime or "",
|
datetime = bm.datetime or "",
|
||||||
notes = bm.notes or "",
|
notes = user_note,
|
||||||
pos0 = bm.pos0 or "",
|
pos0 = wirePos(bm.pos0, bm.page),
|
||||||
pos1 = bm.pos1 or "",
|
pos1 = wirePos(bm.pos1, bm.page),
|
||||||
page = tostring(bm.page or ""),
|
page = tostring(bm.page or ""),
|
||||||
text = bm.text or "",
|
text = sel_text,
|
||||||
book_sha256 = file_sha256,
|
book_sha256 = file_sha256,
|
||||||
}
|
}
|
||||||
if percentage then
|
if percentage then
|
||||||
entry.percentage = Math.roundPercent(percentage)
|
entry.percentage = Math.roundPercent(percentage)
|
||||||
end
|
end
|
||||||
|
|
||||||
local has_text = bm.text and bm.text ~= ""
|
local has_text = sel_text ~= ""
|
||||||
local has_notes = bm.notes and bm.notes ~= ""
|
local has_notes = user_note ~= ""
|
||||||
|
|
||||||
if has_text and has_notes then
|
if has_text and has_notes then
|
||||||
entry.type = "note"
|
entry.type = "note"
|
||||||
@@ -892,6 +925,7 @@ function Bookhoard:collectAnnotations()
|
|||||||
table.insert(highlights, entry)
|
table.insert(highlights, entry)
|
||||||
else
|
else
|
||||||
entry.type = "bookmark"
|
entry.type = "bookmark"
|
||||||
|
entry.text = user_note -- bookmark label travels in `text`
|
||||||
table.insert(bookmarks, entry)
|
table.insert(bookmarks, entry)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1161,30 +1195,40 @@ function Bookhoard:_doGetProgress(interactive)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Bookhoard:applyServerAnnotations(annotations)
|
function Bookhoard:applyServerAnnotations(annotations)
|
||||||
if not self.ui.bookmark or not self.ui.bookmark.bookmarks then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if not annotations then return end
|
if not annotations then return end
|
||||||
|
local model, entries = self:getAnnotationStore()
|
||||||
|
if not model then return end
|
||||||
|
|
||||||
local local_bms = self.ui.bookmark.bookmarks
|
|
||||||
local changed = false
|
local changed = false
|
||||||
local has_pages = self.ui.document.info.has_pages
|
local has_pages = self.ui.document.info.has_pages
|
||||||
|
|
||||||
local function findLocalByPos0(pos0)
|
local function findLocalByPos0(pos0)
|
||||||
for i, bm in ipairs(local_bms) do
|
for i, bm in ipairs(entries) do
|
||||||
if bm.pos0 == pos0 then
|
local p = bm.pos0
|
||||||
|
if type(p) == "table" then p = tostring(p.page or "") end
|
||||||
|
if p == pos0 then
|
||||||
return i
|
return i
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local function makePageFromPos(pos0)
|
-- Get/set the highlighted text and the user note with model-appropriate
|
||||||
if has_pages then
|
-- field names (v2: text/note; v1: notes/text).
|
||||||
local page = tonumber(pos0:match("(%d+)$"))
|
local function getFields(bm)
|
||||||
return page or pos0
|
if model == "v2" then
|
||||||
|
return bm.text or "", bm.note or ""
|
||||||
|
end
|
||||||
|
return bm.notes or "", bm.text or ""
|
||||||
|
end
|
||||||
|
local function setFields(bm, sel_text, user_note)
|
||||||
|
if model == "v2" then
|
||||||
|
bm.text = sel_text ~= "" and sel_text or nil
|
||||||
|
bm.note = user_note ~= "" and user_note or nil
|
||||||
|
else
|
||||||
|
bm.notes = sel_text
|
||||||
|
bm.text = user_note
|
||||||
end
|
end
|
||||||
return pos0
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function isValidPos0(pos0)
|
local function isValidPos0(pos0)
|
||||||
@@ -1201,32 +1245,69 @@ function Bookhoard:applyServerAnnotations(annotations)
|
|||||||
return pos0:sub(1, 6) == "/body/"
|
return pos0:sub(1, 6) == "/body/"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function makePageFromPos(pos0)
|
||||||
|
if has_pages then
|
||||||
|
local page = tonumber(pos0:match("(%d+)$"))
|
||||||
|
return page or pos0
|
||||||
|
end
|
||||||
|
return pos0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- v2 pdf positions are {page=N} tables; CRE keeps xpointer strings.
|
||||||
|
local function makeLocalPos(pos)
|
||||||
|
if has_pages then
|
||||||
|
local page = tonumber(pos)
|
||||||
|
return page and { page = page } or nil
|
||||||
|
end
|
||||||
|
return pos
|
||||||
|
end
|
||||||
|
|
||||||
local function addOrUpdate(server_entry, has_text)
|
local function addOrUpdate(server_entry, has_text)
|
||||||
local pos0 = server_entry.pos0 or ""
|
local pos0 = server_entry.pos0 or ""
|
||||||
if not isValidPos0(pos0) then return end
|
if not isValidPos0(pos0) then return end
|
||||||
local idx = findLocalByPos0(pos0)
|
|
||||||
if idx then
|
|
||||||
local bm = local_bms[idx]
|
|
||||||
local srv_text = server_entry.text or ""
|
local srv_text = server_entry.text or ""
|
||||||
local srv_notes = server_entry.notes or ""
|
local srv_notes = server_entry.notes or ""
|
||||||
if (bm.text or "") ~= srv_text or (bm.notes or "") ~= srv_notes then
|
local idx = findLocalByPos0(pos0)
|
||||||
bm.text = srv_text
|
if idx then
|
||||||
bm.notes = srv_notes
|
local bm = entries[idx]
|
||||||
if server_entry.color then bm.color = server_entry.color end
|
local cur_text, cur_note = getFields(bm)
|
||||||
|
if cur_text ~= srv_text or cur_note ~= srv_notes then
|
||||||
|
setFields(bm, srv_text, srv_notes)
|
||||||
|
if server_entry.color and server_entry.color ~= "" then bm.color = server_entry.color end
|
||||||
changed = true
|
changed = true
|
||||||
end
|
end
|
||||||
|
elseif model == "v2" then
|
||||||
|
-- `drawer` marks a renderable highlight in v2; its absence
|
||||||
|
-- makes the entry a page bookmark (label goes in `note`).
|
||||||
|
local entry = {
|
||||||
|
datetime = server_entry.datetime ~= "" and server_entry.datetime or nil,
|
||||||
|
chapter = server_entry.chapter ~= "" and server_entry.chapter or nil,
|
||||||
|
pos0 = makeLocalPos(pos0),
|
||||||
|
pos1 = makeLocalPos(server_entry.pos1 ~= "" and server_entry.pos1 or pos0),
|
||||||
|
page = makePageFromPos(pos0),
|
||||||
|
}
|
||||||
|
if has_text then
|
||||||
|
entry.drawer = "lighten"
|
||||||
|
entry.text = srv_text
|
||||||
|
if srv_notes ~= "" then entry.note = srv_notes end
|
||||||
|
if server_entry.color and server_entry.color ~= "" then entry.color = server_entry.color end
|
||||||
|
else
|
||||||
|
if srv_text ~= "" then entry.note = srv_text end -- bookmark label
|
||||||
|
end
|
||||||
|
self.ui.annotation:addItem(entry)
|
||||||
|
changed = true
|
||||||
else
|
else
|
||||||
local entry = {
|
local entry = {
|
||||||
page = makePageFromPos(pos0),
|
page = makePageFromPos(pos0),
|
||||||
pos0 = pos0,
|
pos0 = pos0,
|
||||||
pos1 = server_entry.pos1 or pos0,
|
pos1 = server_entry.pos1 or pos0,
|
||||||
datetime = server_entry.datetime or "",
|
datetime = server_entry.datetime or "",
|
||||||
text = server_entry.text or "",
|
notes = srv_text,
|
||||||
notes = server_entry.notes or "",
|
text = srv_notes,
|
||||||
}
|
}
|
||||||
if server_entry.color then entry.color = server_entry.color end
|
if server_entry.color and server_entry.color ~= "" then entry.color = server_entry.color end
|
||||||
if server_entry.chapter then entry.chapter = server_entry.chapter end
|
if server_entry.chapter and server_entry.chapter ~= "" then entry.chapter = server_entry.chapter end
|
||||||
table.insert(local_bms, entry)
|
table.insert(entries, entry)
|
||||||
changed = true
|
changed = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1254,8 +1335,10 @@ function Bookhoard:applyServerAnnotations(annotations)
|
|||||||
local pos0 = del.pos0 or ""
|
local pos0 = del.pos0 or ""
|
||||||
if pos0 ~= "" then
|
if pos0 ~= "" then
|
||||||
local idx = findLocalByPos0(pos0)
|
local idx = findLocalByPos0(pos0)
|
||||||
if idx and (local_bms[idx].text or "") ~= "" then
|
local bm = idx and entries[idx]
|
||||||
table.remove(local_bms, idx)
|
local cur_text = bm and (getFields(bm)) or ""
|
||||||
|
if idx and cur_text ~= "" then
|
||||||
|
table.remove(entries, idx)
|
||||||
changed = true
|
changed = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1268,7 +1351,7 @@ function Bookhoard:applyServerAnnotations(annotations)
|
|||||||
if pos0 ~= "" then
|
if pos0 ~= "" then
|
||||||
local idx = findLocalByPos0(pos0)
|
local idx = findLocalByPos0(pos0)
|
||||||
if idx then
|
if idx then
|
||||||
table.remove(local_bms, idx)
|
table.remove(entries, idx)
|
||||||
changed = true
|
changed = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1276,7 +1359,9 @@ function Bookhoard:applyServerAnnotations(annotations)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if changed then
|
if changed then
|
||||||
if self.ui.bookmark.onSortBookmarks then
|
-- addItem() inserts v2 entries at their sorted position already;
|
||||||
|
-- explicit re-sorting is a v1-model need only.
|
||||||
|
if model == "v1" and self.ui.bookmark and self.ui.bookmark.onSortBookmarks then
|
||||||
self.ui.bookmark:onSortBookmarks()
|
self.ui.bookmark:onSortBookmarks()
|
||||||
end
|
end
|
||||||
if self.ui.saveSettings then
|
if self.ui.saveSettings then
|
||||||
|
|||||||
Reference in New Issue
Block a user