Files
neon e0ce6d3348 chore(annotations): add KOReader highlight drawer
Keep long dashboard highlights readable while preserving quick access to book details.
2026-10-20 16:05:28 -06:00

140 lines
5.4 KiB
Lua

--[[--
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