diff --git a/spec/annotation_sync_controller_spec.lua b/spec/annotation_sync_controller_spec.lua index 5b9ca54..9b3982e 100644 --- a/spec/annotation_sync_controller_spec.lua +++ b/spec/annotation_sync_controller_spec.lua @@ -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() diff --git a/spec/menu_prefetch_spec.lua b/spec/menu_prefetch_spec.lua index 338c554..c20ff7e 100644 --- a/spec/menu_prefetch_spec.lua +++ b/spec/menu_prefetch_spec.lua @@ -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 @@ -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() diff --git a/spec/reader_quick_menu_spec.lua b/spec/reader_quick_menu_spec.lua index a4f47dd..c6f47df 100644 --- a/spec/reader_quick_menu_spec.lua +++ b/spec/reader_quick_menu_spec.lua @@ -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) @@ -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") diff --git a/weread/lib/i18n.lua b/weread/lib/i18n.lua index a0c5f79..afde9e0 100755 --- a/weread/lib/i18n.lua +++ b/weread/lib/i18n.lua @@ -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"] = "清除数据", diff --git a/weread/ui/annotation_sync_controller.lua b/weread/ui/annotation_sync_controller.lua index 0207c2b..020803e 100644 --- a/weread/ui/annotation_sync_controller.lua +++ b/weread/ui/annotation_sync_controller.lua @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/weread/ui/common.lua b/weread/ui/common.lua index 76b658d..c0e637b 100644 --- a/weread/ui/common.lua +++ b/weread/ui/common.lua @@ -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 diff --git a/weread/ui/menu.lua b/weread/ui/menu.lua index 9816e1a..26d4b30 100644 --- a/weread/ui/menu.lua +++ b/weread/ui/menu.lua @@ -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() diff --git a/weread/ui/reader_navigation.lua b/weread/ui/reader_navigation.lua index f2b55fe..ad2d5fa 100644 --- a/weread/ui/reader_navigation.lua +++ b/weread/ui/reader_navigation.lua @@ -61,6 +61,11 @@ 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, @@ -68,7 +73,7 @@ function M:showEndOfBookDialog(book_id) 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()