feat: implement bidirectional annotation sync and delete propagation
Bring the plugin to feature parity with the server's annotation sync
system. Previously annotations were push-only (and even that was broken
because the server ignored inline annotations in progress pushes).
ANNOTATION PULL (server → device):
- Add applyServerAnnotations() method called from _doGetProgress
after progress handling
- Parses annotations.highlights, annotations.notes, and
annotations.bookmarks from GetMetadata response
- Matches incoming annotations against local bookmarks by pos0
(start position) to detect existing entries
- New entries are inserted with correct KOReader bookmark format:
page, pos0, pos1, datetime, text, notes, color, chapter
- Existing entries with changed content are updated (LWW: server
version is authoritative since the server already resolved conflicts)
- Respects sync_highlights, sync_notes, sync_bookmarks toggles for
inbound sync, not just outbound
- Triggers onSortBookmarks + saveSettings on change for persistence
DELETE PROPAGATION:
- Parses annotations.deleted_highlights and annotations.deleted_bookmarks
from GetMetadata response
- Each entry contains device_sync_data (pos0, datetime, page) from the
original device push
- Matches local bookmarks by pos0 and removes them
- Deleted highlights only remove entries that have text (to avoid
removing plain bookmarks at the same position)
CLEANUP:
- Fix BookhoardAPI.lua: rename pcall return _ to err for clarity
(was functional but misleading variable name)
- Remove dead code: unused getLibrary() and syncBookmarks() API methods
- Remove sync_endpoints field (stored from registration response but
never read by any code)
This commit is contained in:
+5
-11
@@ -60,23 +60,23 @@ function BookhoardAPI:_request(method, path, body, timeouts)
|
|||||||
|
|
||||||
socketutil:set_timeout(timeouts[1], timeouts[2])
|
socketutil:set_timeout(timeouts[1], timeouts[2])
|
||||||
|
|
||||||
local ok, _, code
|
local ok, err, code
|
||||||
if url:match("^https://") then
|
if url:match("^https://") then
|
||||||
if ssl_ok then
|
if ssl_ok then
|
||||||
ok, _, code = pcall(ssl_https.request, request)
|
ok, err, code = pcall(ssl_https.request, request)
|
||||||
else
|
else
|
||||||
socketutil:reset_timeout()
|
socketutil:reset_timeout()
|
||||||
return false, { error = "HTTPS not supported on this device" }
|
return false, { error = "HTTPS not supported on this device" }
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
ok, _, code = pcall(http.request, request)
|
ok, err, code = pcall(http.request, request)
|
||||||
end
|
end
|
||||||
|
|
||||||
socketutil:reset_timeout()
|
socketutil:reset_timeout()
|
||||||
|
|
||||||
if not ok then
|
if not ok then
|
||||||
logger.warn("BookhoardAPI: request failed:", _)
|
logger.warn("BookhoardAPI: request failed:", err)
|
||||||
return false, { error = tostring(_) }
|
return false, { error = tostring(err) }
|
||||||
end
|
end
|
||||||
|
|
||||||
local response_body = table.concat(sink)
|
local response_body = table.concat(sink)
|
||||||
@@ -130,12 +130,6 @@ function BookhoardAPI:getMetadata(uuid)
|
|||||||
return self:_request("GET", "/api/sync/koreader/metadata/" .. uuid, nil, PROGRESS_TIMEOUTS)
|
return self:_request("GET", "/api/sync/koreader/metadata/" .. uuid, nil, PROGRESS_TIMEOUTS)
|
||||||
end
|
end
|
||||||
|
|
||||||
function BookhoardAPI:getLibrary()
|
|
||||||
return self:_request("GET", "/api/sync/koreader/library", nil, PROGRESS_TIMEOUTS)
|
|
||||||
end
|
|
||||||
|
|
||||||
function BookhoardAPI:syncBookmarks(data)
|
|
||||||
return self:_request("POST", "/api/sync/koreader/bookmarks", data, PROGRESS_TIMEOUTS)
|
|
||||||
end
|
|
||||||
|
|
||||||
return BookhoardAPI
|
return BookhoardAPI
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ Bookhoard.default_settings = {
|
|||||||
sync_highlights = true,
|
sync_highlights = true,
|
||||||
sync_notes = true,
|
sync_notes = true,
|
||||||
sync_mode = SYNC_MODE.IMMEDIATE,
|
sync_mode = SYNC_MODE.IMMEDIATE,
|
||||||
sync_endpoints = nil,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Bookhoard:init()
|
function Bookhoard:init()
|
||||||
@@ -212,7 +211,6 @@ function Bookhoard:buildMainMenu()
|
|||||||
ok_callback = function()
|
ok_callback = function()
|
||||||
self.settings.auth_token = nil
|
self.settings.auth_token = nil
|
||||||
self.settings.device_id = nil
|
self.settings.device_id = nil
|
||||||
self.settings.sync_endpoints = nil
|
|
||||||
self.settings.auto_sync = false
|
self.settings.auto_sync = false
|
||||||
G_reader_settings:saveSetting(self.settings_key, self.settings)
|
G_reader_settings:saveSetting(self.settings_key, self.settings)
|
||||||
self:registerEvents()
|
self:registerEvents()
|
||||||
@@ -623,7 +621,6 @@ function Bookhoard:startRegistrationPoll()
|
|||||||
self.registration_id = nil
|
self.registration_id = nil
|
||||||
self.settings.auth_token = result.auth_token
|
self.settings.auth_token = result.auth_token
|
||||||
self.settings.device_id = result.device_id and tostring(result.device_id) or nil
|
self.settings.device_id = result.device_id and tostring(result.device_id) or nil
|
||||||
self.settings.sync_endpoints = result.sync_endpoints
|
|
||||||
G_reader_settings:saveSetting(self.settings_key, self.settings)
|
G_reader_settings:saveSetting(self.settings_key, self.settings)
|
||||||
self:registerEvents()
|
self:registerEvents()
|
||||||
self:setupOPDS()
|
self:setupOPDS()
|
||||||
@@ -1039,6 +1036,10 @@ function Bookhoard:_doGetProgress(interactive)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if result.annotations then
|
||||||
|
self:applyServerAnnotations(result.annotations)
|
||||||
|
end
|
||||||
|
|
||||||
local progress = result.progress
|
local progress = result.progress
|
||||||
local percentage = self:getLastPercent()
|
local percentage = self:getLastPercent()
|
||||||
local server_percentage = progress.percentage or 0
|
local server_percentage = progress.percentage or 0
|
||||||
@@ -1100,6 +1101,118 @@ function Bookhoard:_doGetProgress(interactive)
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Bookhoard:applyServerAnnotations(annotations)
|
||||||
|
if not self.ui.bookmark or not self.ui.bookmark.bookmarks then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not annotations then return end
|
||||||
|
|
||||||
|
local local_bms = self.ui.bookmark.bookmarks
|
||||||
|
local changed = false
|
||||||
|
local has_pages = self.ui.document.info.has_pages
|
||||||
|
|
||||||
|
local function findLocalByPos0(pos0)
|
||||||
|
for i, bm in ipairs(local_bms) do
|
||||||
|
if bm.pos0 == pos0 then
|
||||||
|
return i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function makePageFromPos(pos0)
|
||||||
|
if has_pages then
|
||||||
|
local page = tonumber(pos0:match("(%d+)$"))
|
||||||
|
return page or pos0
|
||||||
|
end
|
||||||
|
return pos0
|
||||||
|
end
|
||||||
|
|
||||||
|
local function addOrUpdate(server_entry, has_text)
|
||||||
|
local pos0 = server_entry.pos0 or ""
|
||||||
|
if 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_notes = server_entry.notes or ""
|
||||||
|
if (bm.text or "") ~= srv_text or (bm.notes or "") ~= srv_notes then
|
||||||
|
bm.text = srv_text
|
||||||
|
bm.notes = srv_notes
|
||||||
|
if server_entry.color then bm.color = server_entry.color end
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local entry = {
|
||||||
|
page = makePageFromPos(pos0),
|
||||||
|
pos0 = pos0,
|
||||||
|
pos1 = server_entry.pos1 or pos0,
|
||||||
|
datetime = server_entry.datetime or "",
|
||||||
|
text = server_entry.text or "",
|
||||||
|
notes = server_entry.notes or "",
|
||||||
|
}
|
||||||
|
if server_entry.color then entry.color = server_entry.color end
|
||||||
|
if server_entry.chapter then entry.chapter = server_entry.chapter end
|
||||||
|
table.insert(local_bms, entry)
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.settings.sync_highlights and annotations.highlights then
|
||||||
|
for _, hl in ipairs(annotations.highlights) do
|
||||||
|
addOrUpdate(hl, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.settings.sync_notes and annotations.notes then
|
||||||
|
for _, note in ipairs(annotations.notes) do
|
||||||
|
addOrUpdate(note, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.settings.sync_bookmarks and annotations.bookmarks then
|
||||||
|
for _, bm in ipairs(annotations.bookmarks) do
|
||||||
|
addOrUpdate(bm, false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if annotations.deleted_highlights then
|
||||||
|
for _, del in ipairs(annotations.deleted_highlights) do
|
||||||
|
local pos0 = del.pos0 or ""
|
||||||
|
if pos0 ~= "" then
|
||||||
|
local idx = findLocalByPos0(pos0)
|
||||||
|
if idx and (local_bms[idx].text or "") ~= "" then
|
||||||
|
table.remove(local_bms, idx)
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if annotations.deleted_bookmarks then
|
||||||
|
for _, del in ipairs(annotations.deleted_bookmarks) do
|
||||||
|
local pos0 = del.pos0 or ""
|
||||||
|
if pos0 ~= "" then
|
||||||
|
local idx = findLocalByPos0(pos0)
|
||||||
|
if idx then
|
||||||
|
table.remove(local_bms, idx)
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if changed then
|
||||||
|
if self.ui.bookmark.onSortBookmarks then
|
||||||
|
self.ui.bookmark:onSortBookmarks()
|
||||||
|
end
|
||||||
|
if self.ui.saveSettings then
|
||||||
|
self.ui:saveSettings()
|
||||||
|
end
|
||||||
|
logger.dbg("Bookhoard: annotations updated from server")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function Bookhoard:_showSyncedMessage()
|
function Bookhoard:_showSyncedMessage()
|
||||||
UIManager:show(InfoMessage:new{
|
UIManager:show(InfoMessage:new{
|
||||||
text = _("Progress has been synchronized."),
|
text = _("Progress has been synchronized."),
|
||||||
|
|||||||
Reference in New Issue
Block a user