Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions spec/annotation_sync_controller_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,37 @@ drain()
assert(calls == 2)
-- Multi-select keeps source catalog order, including noncontiguous choices.
context.chapters = { { chapterUid = "1" }, { chapterUid = "2" }, { chapterUid = "3" } }
local picker, chosen
host.showList = function(_self, _title, items) picker = items; return { updateItems = function() end } end
local picker, chosen, picker_options
host.showList = function(_self, _title, items, _empty, options)
picker, picker_options = items, options
return { updateItems = function() end }
end
host.startUnifiedAnnotationSync = function(_self, options) chosen = options.chapters end
host:chooseAnnotationChapters()
picker[4].callback(); picker[2].callback(); picker[1].callback()
assert(#chosen == 2 and chosen[1].chapterUid == "1" and chosen[2].chapterUid == "3")
-- The picker opens on the page containing the current local XPointer range.
-- This deliberately uses uneven local chapter positions and unrelated remote
-- UIDs so remote/local chapter-number offsets cannot affect the result.
context.chapters, context.ranges = {}, {}
local starts = { 0, 8, 19, 33, 48, 65, 79, 91, 103, 1000, 1300, 1600, 1900, 2200, 2500 }
for index, start in ipairs(starts) do
local uid = tostring(100 + index)
context.chapters[index] = { chapterUid = uid }
context.ranges[uid] = { start_xpointer = tostring(start) }
end
host.ui.document.getXPointer = function() return "1120" end
host.ui.document.compareXPointers = function(_self, a, b)
a, b = tonumber(a), tonumber(b)
return a == b and 0 or a < b and 1 or -1
end
host:chooseAnnotationChapters()
assert(picker_options.initial_page == 2,
"chapter picker did not open on the current local chapter page")
-- Clearing is the explicit refresh path: shared annotations and every file's
-- coordinates are removed, while cached chapter text remains reusable.
context.chapters = { { chapterUid = "1" }, { chapterUid = "2" }, { chapterUid = "3" } }
context.ranges = {}
store:put("book", "original", "1", { spans = {} }, "1")
store:put("book", "projection", "other:1", { records = {} }, "1")
host:clearUnifiedAnnotationProjections()
Expand Down
7 changes: 4 additions & 3 deletions spec/menu_prefetch_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ local cache = {
download_underlines_and_thoughts = false,
prefetch_annotations = false,
show_prefetch_notifications = true,
show_annotations = false,
}
local shelf = { sort_order = "time_desc" }
local flush_count = 0
Expand Down Expand Up @@ -269,10 +270,10 @@ for _, item in ipairs(weread_reader_items) do
if item.text == "Show underlines and thoughts" then visibility_item = item end
end
expect(visibility_item and not visibility_item.checked_func(),
"unmatched document does not show the visibility item as checked")
annotations_visible = true
"hidden annotation preference should show an unchecked item")
cache.show_annotations = true
expect(visibility_item and visibility_item.checked_func(),
"matched visible document shows the visibility item as checked")
"shown annotation preference should show a checked item")

host.detectWeReadBook = function() return "mp-book" end
local mp_reader_items = host:getMainMenuItems()
Expand Down
12 changes: 10 additions & 2 deletions spec/reader_quick_menu_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,14 @@ expect(host:onShowWeReadReadingStatistics() and action_stats_opened,
local stats_opened = false
local context_books = {}
local annotations_toggled = false
local annotation_cache = { show_annotations = true }
local context_host = {
ui = { document = { file = "/books/local.epub" } },
settings = {
get = function(_self, key, default)
return key == "books" and context_books or default
if key == "books" then return context_books end
if key == "cache" then return annotation_cache end
return default
end,
},
showTransientInfo = function(_self, text, timeout)
Expand All @@ -98,8 +101,13 @@ expect(dialog_options.show_chapter_nav and dialog_options.show_next_chapter
and dialog_options.enable_next_chapter == false
and dialog_options.enable_book_details == false
and dialog_options.enable_sync_progress == false
and dialog_options.annotations_visible == true,
"quick menu reflects the global annotation visibility preference")
annotation_cache.show_annotations = false
expect(context_host:showEndOfBookDialog(nil)
and dialog_options.annotations_visible == false,
"context-dependent actions are visible but disabled for local documents")
"quick menu hides annotations when the global preference is disabled")
annotation_cache.show_annotations = true
dialog_callbacks.on_chapter_list()
expect(notice and notice.timeout == 1,
"context-dependent action explains why it is unavailable")
Expand Down
5 changes: 5 additions & 0 deletions weread/lib/i18n.lua
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ local zh = {
["Matching underlines in the local book…"] = "正在本地书中匹配划线……",
["Sync underlines and thoughts"] = "同步划线与想法",
["Sync underlines and thoughts · %1 matched"] = "同步划线与想法(已匹配 %1 条)",
["Sync chapters…"] = "按需同步多个章节…",
["Select chapters to sync"] = "选择要同步的章节",
["No chapters to sync."] = "没有可同步的章节。",
["Synced"] = "已同步",
["Downloading selected chapters…"] = "正在下载所选章节…",
["Sync completed: %1/%2 underlines matched."] = "同步完成:成功匹配 %1/%2 条划线。",
["Downloaded %1 underlines, but none could be matched. Existing data was not changed; retry to continue."] = "已经下载 %1 条划线,但未能匹配任何一条。已有数据不会被修改;请重试以继续同步。",
["Clear data"] = "清除数据",
Expand Down
58 changes: 45 additions & 13 deletions weread/ui/annotation_sync_controller.lua
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,36 @@ function M:_annotationsVisibleForCurrentDocument()
return context ~= nil and self:_annotationSummary(context).chapters > 0
end

function M:_annotationChapterIndex(context, point)
local document = self.ui and self.ui.document
if not document or not point or type(document.compareXPointers) ~= "function" then
return nil
end

local latest_index, latest_start
for index, chapter in ipairs(context.chapters or {}) do
local range = context.ranges
and context.ranges[Chapters.uid(chapter)]
local start_xpointer = range and range.start_xpointer
if start_xpointer then
local ok_start, start_cmp = pcall(document.compareXPointers,
document, start_xpointer, point)
if ok_start and (start_cmp == 0 or start_cmp == 1) then
if not latest_start then
latest_index, latest_start = index, start_xpointer
else
local ok_order, order = pcall(document.compareXPointers,
document, latest_start, start_xpointer)
if ok_order and order == 1 then
latest_index, latest_start = index, start_xpointer
end
end
end
end
end
return latest_index
end

function M:_refreshAnnotationOverlay()
local context, overlay = self._annotation_context, self._xpointer_overlay
if not context or not overlay or #context.chapters == 0 then return end
Expand All @@ -145,17 +175,7 @@ function M:_refreshAnnotationOverlay()
local document = self.ui.document
local current = document:getXPointer()
local function chapter_at(point)
if not point or not document.compareXPointers then return 1 end
local low, high, result = 1, #context.chapters, 1
while low <= high do
local middle = math.floor((low + high) / 2)
local range = context.ranges[Chapters.uid(context.chapters[middle])]
local cmp = range and range.start_xpointer
and document:compareXPointers(range.start_xpointer, point)
if cmp == 0 or cmp == 1 then result, low = middle, middle + 1
else high = middle - 1 end
end
return result
return self:_annotationChapterIndex(context, point) or 1
end
local active = chapter_at(current)
local last = active
Expand Down Expand Up @@ -546,6 +566,12 @@ function M:chooseAnnotationChapters()
if menu then UIManager:close(menu) end
self:startUnifiedAnnotationSync({ chapters = chapters, offline = not self:isNetworkConnected() })
end
local current_index
local document = self.ui and self.ui.document
if document and type(document.getXPointer) == "function" then
local ok, point = pcall(document.getXPointer, document)
if ok then current_index = self:_annotationChapterIndex(context, point) end
end
-- Keep the action reachable on every page, including keyboard devices.
local per_page = 8
for index, chapter in ipairs(context.chapters) do
Expand All @@ -560,13 +586,19 @@ function M:chooseAnnotationChapters()
local status = context.statuses[context.store:projectionKey(context.document_key, uid)]
items[#items + 1] = { text_func = function()
return (selected[uid] and "[✓] " or "[ ] ") .. title
end, mandatory_func = function() return status and _("Matched") or nil end,
end, bold = current_index == index,
mandatory_func = function() return status and _("Matched") or nil end,
callback = function()
selected[uid] = not selected[uid]
if menu then menu:updateItems() end
end }
end
menu = self:showList(_("Choose chapters to match"), items, nil, { items_per_page = per_page })
local initial_page = current_index
and math.floor((current_index - 1) / (per_page - 1)) + 1 or 1
menu = self:showList(_("Choose chapters to match"), items, nil, {
items_per_page = per_page,
initial_page = initial_page,
})
end
local context = self:_prepareAnnotationContext(false)
if context and #context.chapters > 0 then return show() end
Expand Down
8 changes: 8 additions & 0 deletions weread/ui/common.lua
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,14 @@ function M:showList(title, items, empty_text, options)
subtitle = options.subtitle,
}
UIManager:show(menu)
if options.initial_page and options.initial_page > 1
and type(menu.onGotoPage) == "function" then
UIManager:scheduleIn(0.1, function()
if menu and type(menu.onGotoPage) == "function" then
menu:onGotoPage(options.initial_page)
end
end)
end
return menu
end

Expand Down
6 changes: 5 additions & 1 deletion weread/ui/menu.lua
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,11 @@ function M:getMainMenuItems()
reader_items[#reader_items + 1] = {
text = _("Show underlines and thoughts"),
checked_func = function()
return self:_annotationsVisibleForCurrentDocument()
-- The checkbox reflects the user's display preference. Annotation
-- data may still be downloading (or may be unavailable for this
-- document), but that must not make a checked preference render
-- as unchecked.
return self.settings:get("cache", {}).show_annotations ~= false
end,
keep_menu_open = true,
callback = self:safeCallback(_("Show underlines and thoughts"), function()
Expand Down
7 changes: 6 additions & 1 deletion weread/ui/reader_navigation.lua
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,19 @@ function M:showEndOfBookDialog(book_id)
_("This action requires an open WeRead book."), 1)
end

-- Keep the reader quick menu in sync with the global display preference.
-- Document-level matching may be incomplete while chapters are being
-- prepared, but the user's show/hide choice is still authoritative here.
local cache = self.settings:get("cache", {})
local annotations_visible = cache.show_annotations ~= false
EndOfBookDialog.show({
show_chapter_nav = true,
show_next_chapter = true,
enable_chapter_list = chapters ~= nil,
enable_next_chapter = next_chapter ~= nil,
enable_book_details = book ~= nil,
enable_sync_progress = is_regular_weread_book,
annotations_visible = self:_annotationsVisibleForCurrentDocument(),
annotations_visible = annotations_visible,
}, {
on_bookshelf = function()
self:showBookshelf()
Expand Down