diff --git a/koreader-plugin/bookorbit.koplugin/bookorbit_catalog_board.lua b/koreader-plugin/bookorbit.koplugin/bookorbit_catalog_board.lua index 28d6ff610..930392b86 100644 --- a/koreader-plugin/bookorbit.koplugin/bookorbit_catalog_board.lua +++ b/koreader-plugin/bookorbit.koplugin/bookorbit_catalog_board.lua @@ -231,8 +231,8 @@ function CatalogBoard:boardItemAction(item, d) return { kind = "books", params = { sort = "title" }, title = _("All books") } elseif t == "also" or t == "goalpace" then return { kind = "books", params = { sort = "recently_read", readStatus = "reading" }, title = _("In progress") } - elseif t == "highlight" and d.highlight and d.highlight.bookId then - return { kind = "detail", book_id = d.highlight.bookId } + elseif t == "highlight" and d.highlight then + return { kind = "highlight", highlight = d.highlight } elseif t == "shelf" then local source = item.props and item.props.source if source == "want-to-read" and not (item.props and item.props.params) then return nil end @@ -800,6 +800,8 @@ function CatalogBoard:boardRun(action, tile) self:onMenuSelect({ kind = "dashboard-book", book_id = action.book.id, book = action.book, resume = action.resume, list = action.list }) elseif kind == "detail" then self:loadBookDetail(action.book_id, { list = action.list }) + elseif kind == "highlight" then + require("bookorbit_highlight_sheet").open(self, action.highlight) elseif kind == "books" then self:loadBooks(action.params or {}, action.title) elseif kind == "shelf" then diff --git a/koreader-plugin/bookorbit.koplugin/bookorbit_highlight_sheet.lua b/koreader-plugin/bookorbit.koplugin/bookorbit_highlight_sheet.lua new file mode 100644 index 000000000..3be818920 --- /dev/null +++ b/koreader-plugin/bookorbit.koplugin/bookorbit_highlight_sheet.lua @@ -0,0 +1,139 @@ +--[[-- +The Highlight of the day drawer. The dashboard tile is deliberately concise; +this sheet keeps the full passage readable and leaves book details one tap away. +Long passages turn page by page inside the drawer. +]] + +local Device = require("device") +local TextBoxWidget = require("ui/widget/textboxwidget") +local UIManager = require("ui/uimanager") +local _ = require("gettext") + +local BoardMetrics = require("bookorbit_board_metrics") +local BoardUI = require("bookorbit_board_ui") +local DialogUI = require("bookorbit_dialog_ui") +local TextPages = require("bookorbit_text_pages") + +local INK = BoardMetrics.INK +local px, lh = BoardMetrics.px, BoardMetrics.lh +local text, vstack, hstack, space = BoardUI.text, BoardUI.vstack, BoardUI.hstack, BoardUI.space + +local HighlightSheet = {} + +local BODY_SIZE = 16 + +local function hOf(widget) return widget:getSize().h end + +function HighlightSheet.pageWindow(total_lines, available_height, line_height, index) + total_lines = math.max(1, tonumber(total_lines) or 1) + line_height = math.max(1, tonumber(line_height) or 1) + local per_page = math.max(1, math.floor(math.max(1, available_height) / line_height)) + local count = math.max(1, math.ceil(total_lines / per_page)) + index = math.max(1, math.min(count, tonumber(index) or 1)) + return { + count = count, + index = index, + per_page = per_page, + first_line = (index - 1) * per_page + 1, + height = math.min(total_lines, per_page) * line_height, + } +end + +local function chapterLine(highlight, w) + local chapter = type(highlight.chapterTitle) == "string" and highlight.chapterTitle:match("^%s*(.-)%s*$") or nil + if not chapter or chapter == "" then return nil end + return text(chapter, { s = 13, b = true, col = INK.g3, w = w, data = true }) +end + +local function buildBody(page, w, max_body_h) + local highlight = page.highlight + local head = DialogUI.titleRow(w, _("Highlight of the day"), highlight.bookTitle) + local chapter = chapterLine(highlight, w) + local details = highlight.bookId and DialogUI.pillRow(w, { + { label = _("Book details"), icon = "book-open-text", kind = "outline", action = { kind = "details" } }, + }) or nil + local gap = px(14) + local line_height = lh(BODY_SIZE) + local bar = math.max(2, px(3)) + local quote_gap = px(14) + local quote_w = math.max(1, w - bar - quote_gap) + local total_lines = TextPages.linesNeeded(highlight.text or "", { s = BODY_SIZE, serif = true, i = true }, quote_w) + + local function fixedHeight(with_pager) + local fixed_items = { head } + if chapter then table.insert(fixed_items, chapter) end + if with_pager then table.insert(fixed_items, true) end + if details then table.insert(fixed_items, details) end + local height = 0 + for _i, item in ipairs(fixed_items) do + height = height + (item == true and px(40) or hOf(item)) + end + return height + #fixed_items * gap + end + + local window = HighlightSheet.pageWindow(total_lines, max_body_h - fixedHeight(false), line_height, page.index) + if window.count > 1 then + window = HighlightSheet.pageWindow(total_lines, max_body_h - fixedHeight(true), line_height, page.index) + end + page.index = window.index + page.count = window.count + + local quote = TextBoxWidget:new{ + text = highlight.text or "", + face = BoardMetrics.face(BODY_SIZE, "serif_italic"), + width = quote_w, + height = window.height, + virtual_line_num = window.first_line, + fgcolor = INK.black, + bgcolor = INK.white, + } + local quote_block = hstack({ w = w, valign = "top" }, { + BoardUI.rect(bar, window.height, INK.black, math.floor(bar / 2)), + space(quote_gap, 1), + quote, + }) + local items = { head, quote_block } + if chapter then table.insert(items, chapter) end + if window.count > 1 then table.insert(items, TextPages.pager(w, window.index, window.count)) end + if details then table.insert(items, details) end + return items +end + +local function turn(page, delta) + local next_index = math.max(1, math.min(page.count or 1, page.index + delta)) + if next_index == page.index then return end + page.index = next_index + page:refresh(Device:hasEinkScreen() and "partial" or "ui") +end + +function HighlightSheet.open(catalog, highlight) + if type(highlight) ~= "table" then return nil end + return DialogUI.open{ + name = "bookorbit_highlight_of_the_day", + build = function(page, sw, sh) + return DialogUI.sheet(page, sw, sh, function(w, max_body_h) + return buildBody(page, w, max_body_h) + end, { pop_w = 520 }) + end, + on_action = function(page, action) + if action.kind == "page" then + turn(page, action.delta) + elseif action.kind == "details" and page.highlight.bookId then + local book_id = page.highlight.bookId + page:close() + UIManager:tickAfterNext(function() catalog:loadBookDetail(book_id) end) + end + return true + end, + on_swipe = function(page, ges) + if ges.direction == "west" then turn(page, 1) return true end + if ges.direction == "east" then turn(page, -1) return true end + return false + end, + on_page_key = function(page, delta) turn(page, delta) end, + highlight = highlight, + index = 1, + } +end + +return HighlightSheet diff --git a/koreader-plugin/spec/bookorbit_highlight_sheet_test.lua b/koreader-plugin/spec/bookorbit_highlight_sheet_test.lua new file mode 100644 index 000000000..2f26dc823 --- /dev/null +++ b/koreader-plugin/spec/bookorbit_highlight_sheet_test.lua @@ -0,0 +1,34 @@ +-- The Highlight of the day drawer keeps every line reachable in bounded pages. + +package.path = "koreader-plugin/bookorbit.koplugin/?.lua;" .. package.path + +local function identity(value) return value end +for _, name in ipairs({ "device", "ui/widget/textboxwidget", "ui/uimanager", "bookorbit_board_ui", + "bookorbit_dialog_ui", "bookorbit_text_pages" }) do + package.loaded[name] = {} +end +package.loaded["gettext"] = identity +package.loaded["bookorbit_board_metrics"] = { INK = {}, px = identity, lh = identity } + +local HighlightSheet = require("bookorbit_highlight_sheet") + +local function assertEqual(actual, expected, label) + if actual ~= expected then error(string.format("%s: expected %s, got %s", label, tostring(expected), tostring(actual))) end +end + +local window = HighlightSheet.pageWindow(23, 100, 10, 1) +assertEqual(window.per_page, 10, "the drawer fits ten lines") +assertEqual(window.count, 3, "all lines are split across three pages") +assertEqual(window.first_line, 1, "the first page starts at the first line") +assertEqual(window.height, 100, "multipage drawers keep a stable text height") + +window = HighlightSheet.pageWindow(23, 100, 10, 3) +assertEqual(window.first_line, 21, "the last page starts after the first twenty lines") +assertEqual(window.height, 100, "the last page keeps the drawer from jumping") + +window = HighlightSheet.pageWindow(3, 100, 10, 8) +assertEqual(window.count, 1, "a short highlight needs one page") +assertEqual(window.index, 1, "the requested page is clamped") +assertEqual(window.height, 30, "a short highlight keeps the drawer compact") + +print("bookorbit_highlight_sheet_test.lua: ok")