Files
neon 1c27e5dd05 fix(koreader): keep manual links to a different copy matched and suggest links
Report a linked copy whose bytes differ from the library file as positionIncompatible instead of
unmatched, so the plugin keeps the match, skips highlights without a failure and still syncs progress.
Treat a position-less place within a page as the same place and jump to the nearest page.
Suggest library books with the same title and authors for books to review, with bulk linking.
See docs/KOREADER_HASH_MATCHING.md.

Closes #1872
Closes #1919
2026-10-26 15:06:04 +00:00

160 lines
5.2 KiB
Lua

--[[--
Highlight sync summary helpers for BookOrbit.
Keeps the persisted diagnostic shape, count aggregation and concise user
messages consistent across open-book exchange, per-book sync and sweep.
]]
local T = require("ffi/util").template
local _ = require("gettext")
local HighlightSummary = {}
local COUNT_FIELDS = {
"uploaded",
"applied",
"deleted",
"failed",
"skipped",
"touched_books",
-- Books whose device copy differs from the library file: matched, but
-- highlights and bookmarks cannot be placed in it, so none were exchanged.
"incompatible",
}
-- Bookmarks are counted separately from highlights: they are a different user
-- surface, and folding them together would make "3 applied" unreadable.
local BOOKMARK_COUNT_FIELDS = {
"bm_uploaded",
"bm_applied",
"bm_deleted",
"bm_failed",
}
local ACTIONABLE_ERRORS = {
auth = true,
network = true,
unsupported_server = true,
partial_failure = true,
}
local function numberValue(value)
value = tonumber(value)
if not value or value < 0 then return 0 end
return value
end
function HighlightSummary.normalize(summary)
summary = summary or {}
local normalized = {
event = summary.event,
reason = summary.reason,
at = summary.at or os.time(),
message = summary.message,
}
for _, field in ipairs(COUNT_FIELDS) do
normalized[field] = numberValue(summary[field])
end
for _, field in ipairs(BOOKMARK_COUNT_FIELDS) do
normalized[field] = numberValue(summary[field])
end
return normalized
end
function HighlightSummary.add(summary, result, opts)
summary = HighlightSummary.normalize(summary)
result = result or {}
opts = opts or {}
for _, field in ipairs(COUNT_FIELDS) do
summary[field] = summary[field] + numberValue(result[field])
end
if opts.skipped then
summary.skipped = summary.skipped + numberValue(opts.skipped)
end
if opts.closed_book and numberValue(result.applied) + numberValue(result.deleted) > 0 then
summary.touched_books = summary.touched_books + 1
end
if result.had_errors and numberValue(result.failed) == 0 then
summary.failed = summary.failed + 1
end
return summary
end
-- Bookmark exchange results share the highlight result shape, so they are
-- folded into their own counters rather than the highlight ones.
function HighlightSummary.addBookmarks(summary, result)
summary = HighlightSummary.normalize(summary)
result = result or {}
summary.bm_uploaded = summary.bm_uploaded + numberValue(result.uploaded)
summary.bm_applied = summary.bm_applied + numberValue(result.applied)
summary.bm_deleted = summary.bm_deleted + numberValue(result.deleted)
summary.bm_failed = summary.bm_failed + numberValue(result.failed)
if result.had_errors and numberValue(result.failed) == 0 then
summary.bm_failed = summary.bm_failed + 1
end
return summary
end
function HighlightSummary.hasBookmarkCounts(summary)
summary = HighlightSummary.normalize(summary)
for _, field in ipairs(BOOKMARK_COUNT_FIELDS) do
if summary[field] > 0 then return true end
end
return false
end
function HighlightSummary.hasCounts(summary)
summary = HighlightSummary.normalize(summary)
for _, field in ipairs(COUNT_FIELDS) do
if summary[field] > 0 then return true end
end
return HighlightSummary.hasBookmarkCounts(summary)
end
function HighlightSummary.hasRemoteChanges(summary)
summary = HighlightSummary.normalize(summary)
return summary.applied + summary.deleted > 0
end
function HighlightSummary.hasRemoteBookmarkChanges(summary)
summary = HighlightSummary.normalize(summary)
return summary.bm_applied + summary.bm_deleted > 0
end
function HighlightSummary.actionableError(summary, err)
if ACTIONABLE_ERRORS[err] then return err end
summary = HighlightSummary.normalize(summary)
if summary.failed > 0 or summary.bm_failed > 0 then return "partial_failure" end
end
function HighlightSummary.message(summary)
summary = HighlightSummary.normalize(summary)
local text = T(_("Highlights synced: %1 uploaded, %2 applied, %3 deleted."),
summary.uploaded, summary.applied, summary.deleted)
if summary.failed > 0 or summary.skipped > 0 then
text = text .. "\n" .. T(_("Failed: %1. Skipped: %2."), summary.failed, summary.skipped)
end
if HighlightSummary.hasBookmarkCounts(summary) then
text = text .. "\n" .. T(_("Bookmarks: %1 uploaded, %2 applied, %3 deleted."),
summary.bm_uploaded, summary.bm_applied, summary.bm_deleted)
if summary.bm_failed > 0 then
text = text .. "\n" .. T(_("Bookmarks failed: %1."), summary.bm_failed)
end
end
if summary.touched_books > 0 then
text = text .. "\n" .. T(_("%1 closed book(s) updated."), summary.touched_books)
end
if summary.incompatible > 0 then
text = text .. "\n" .. T(_("%1 book(s) skipped: the copy on this device is a different file from the one in BookOrbit."), summary.incompatible)
end
return text
end
function HighlightSummary.diagnosticsText(summary)
if type(summary) ~= "table" then return _("none") end
return HighlightSummary.message(summary)
end
return HighlightSummary