mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
40e366a463
Add lastReadAt timestamp to KOReader catalog book items and introduce reading streak and goal statistics widgets to the KOReader plugin dashboard layout.
264 lines
12 KiB
Lua
264 lines
12 KiB
Lua
-- Proves the dashboard's vertical budget: that shelves are measured at the
|
|
-- height their cover cards actually need, that a page too tight for the
|
|
-- configured slots drops the stats strip before any shelf, and that nothing is
|
|
-- laid out past the available height.
|
|
|
|
package.loaded["document/documentregistry"] = { hasProvider = function() return true end }
|
|
package.loaded["util"] = {
|
|
fixUtf8 = function(value) return value end,
|
|
urlEncode = function(value) return value end,
|
|
}
|
|
package.loaded["ffi/util"] = { template = function(value) return value end }
|
|
package.loaded["gettext"] = function(text) return text end
|
|
|
|
local function widgetClass(height)
|
|
return {
|
|
new = function(_, opts)
|
|
opts = opts or {}
|
|
if not opts.getSize then
|
|
function opts:getSize()
|
|
return { w = 100, h = self.width or height or 20 }
|
|
end
|
|
end
|
|
return opts
|
|
end,
|
|
}
|
|
end
|
|
|
|
package.loaded["ffi/blitbuffer"] = { COLOR_DARK_GRAY = 1, COLOR_LIGHT_GRAY = 2 }
|
|
package.loaded["ui/widget/button"] = widgetClass()
|
|
package.loaded["ui/widget/container/centercontainer"] = widgetClass()
|
|
package.loaded["ui/font"] = { getFace = function() return {} end }
|
|
package.loaded["ui/geometry"] = widgetClass()
|
|
package.loaded["ui/widget/horizontalgroup"] = widgetClass(0)
|
|
package.loaded["ui/widget/horizontalspan"] = widgetClass(0)
|
|
package.loaded["ui/widget/infomessage"] = widgetClass()
|
|
package.loaded["ui/widget/linewidget"] = widgetClass()
|
|
package.loaded["ui/network/manager"] = { isConnected = function() return true end }
|
|
package.loaded["device"] = { screen = { scaleBySize = function(_, value) return value end } }
|
|
package.loaded["ui/size"] = {
|
|
line = { thin = 1 },
|
|
padding = { large = 8 },
|
|
span = { horizontal_default = 4, vertical_default = 4 },
|
|
}
|
|
package.loaded["ui/widget/textboxwidget"] = widgetClass()
|
|
package.loaded["ui/widget/textwidget"] = widgetClass()
|
|
package.loaded["ui/uimanager"] = { show = function() end }
|
|
package.loaded["ui/widget/verticalgroup"] = widgetClass(0)
|
|
package.loaded["ui/widget/verticalspan"] = widgetClass()
|
|
package.loaded["bookorbit_stats_reader"] = {
|
|
getReadingSummary = function()
|
|
return { today_seconds = 600, week_seconds = 3600, streak_days = 3 }
|
|
end,
|
|
}
|
|
|
|
local HEADER_HEIGHT = 20
|
|
local HEADER_ROW_HEIGHT = 14
|
|
local STATUS_LINE_HEIGHT = 16
|
|
local HIGHLIGHT_CARD_HEIGHT = 64
|
|
|
|
package.loaded["bookorbit_catalog_widgets"] = {
|
|
DashboardCoverCard = widgetClass(),
|
|
DashboardHeroCard = widgetClass(),
|
|
DashboardHighlightCard = widgetClass(),
|
|
DashboardBrowseRow = widgetClass(),
|
|
DashboardIconButton = widgetClass(),
|
|
assetIconFile = function() return nil end,
|
|
dashboardSectionHeaderRowHeight = function() return HEADER_ROW_HEIGHT end,
|
|
buildDashboardSectionHeader = function()
|
|
return { getSize = function() return { w = 100, h = HEADER_HEIGHT } end }
|
|
end,
|
|
buildStatusLabel = function()
|
|
return { getSize = function() return { w = 100, h = STATUS_LINE_HEIGHT } end }
|
|
end,
|
|
dashboardHighlightCardHeight = function()
|
|
return HIGHLIGHT_CARD_HEIGHT
|
|
end,
|
|
coverCardHeight = function(card_w)
|
|
return card_w + 20
|
|
end,
|
|
}
|
|
|
|
package.path = "koreader-plugin/bookorbit.koplugin/?.lua;" .. package.path
|
|
|
|
local CatalogDashboard = require("bookorbit_catalog_dashboard")
|
|
local DashboardSections = require("bookorbit_dashboard_sections")
|
|
|
|
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 STATS_HEIGHT = 60
|
|
|
|
local function books(count)
|
|
local list = {}
|
|
for id = 1, count do list[id] = { id = id } end
|
|
return list
|
|
end
|
|
|
|
local function newCatalog(opts)
|
|
local slots = {}
|
|
for index = 1, DashboardSections.SLOT_COUNT do
|
|
slots[index] = { type = opts.sections[index].type, books = books(opts.books or 12) }
|
|
end
|
|
local catalog = {
|
|
settings = {
|
|
[DashboardSections.SETTING_KEY] = {
|
|
opts.sections[1], opts.sections[2], opts.sections[3], opts.sections[4],
|
|
schemaVersion = DashboardSections.SCHEMA_VERSION,
|
|
},
|
|
},
|
|
available_height = opts.avail,
|
|
content_w = opts.content_w or 500,
|
|
content_inset = 0,
|
|
item_group = {},
|
|
layout = {},
|
|
current_context = {
|
|
kind = "dashboard",
|
|
dashboard = { continueReading = books(4), dashboardSlots = slots },
|
|
},
|
|
shelves = {},
|
|
headers = {},
|
|
}
|
|
for name, fn in pairs(CatalogDashboard) do
|
|
if name ~= "install" then catalog[name] = fn end
|
|
end
|
|
function catalog:prepareCustomUpdate() return nil end
|
|
function catalog:finishCustomUpdate() end
|
|
function catalog:ensureOnDeviceCurrent() end
|
|
function catalog:isOnDevice() return false end
|
|
function catalog:onDeviceCount() return 0 end
|
|
function catalog:readStatusLabel() return nil end
|
|
function catalog:buildDashboardStatsStrip()
|
|
return { getSize = function() return { w = 100, h = STATS_HEIGHT } end }
|
|
end
|
|
|
|
local addHeader = catalog.addDashboardHeader
|
|
function catalog:addDashboardHeader(text, controls)
|
|
table.insert(self.headers, text)
|
|
return addHeader(self, text, controls)
|
|
end
|
|
local addGrid = catalog.addDashboardCoverGrid
|
|
function catalog:addDashboardCoverGrid(section_id, list, height, with_progress, with_caption, cols, card_w, page, rows)
|
|
table.insert(self.shelves, { id = section_id, height = height, cols = cols, card_w = card_w })
|
|
return addGrid(self, section_id, list, height, with_progress, with_caption, cols, card_w, page, rows)
|
|
end
|
|
return catalog
|
|
end
|
|
|
|
local DEFAULTS = {
|
|
{ type = "stats" }, { type = "continue-reading" }, { type = "random" }, { type = "browse" },
|
|
}
|
|
local FOUR_SHELVES = {
|
|
{ type = "random" }, { type = "recently-added" }, { type = "in-progress" }, { type = "random" },
|
|
}
|
|
|
|
-- A roomy page renders every configured slot, and each shelf is exactly as tall
|
|
-- as its cover cards need to be.
|
|
local catalog = newCatalog{ sections = DEFAULTS, avail = 800 }
|
|
catalog:updateDashboardItems()
|
|
assertEqual(#catalog.shelves, 1, "the default dashboard renders its one shelf")
|
|
assertEqual(#catalog.headers, 3, "Continue reading, the shelf and Browse each get a header")
|
|
assertEqual(catalog.shelves[1].height, catalog.shelves[1].card_w + 20, "a roomy shelf is drawn in aspect")
|
|
assertEqual(catalog.shelves[1].cols, 5, "a roomy shelf keeps its natural slot count")
|
|
assertEqual(catalog.dash_used <= 800, true, "a roomy page stays inside the available height")
|
|
|
|
-- Four shelves share the page and stay in aspect: the cards get narrower rather
|
|
-- than being squashed to fit.
|
|
catalog = newCatalog{ sections = FOUR_SHELVES, avail = 600 }
|
|
catalog:updateDashboardItems()
|
|
assertEqual(#catalog.shelves, 4, "four configured shelves all render")
|
|
for _, shelf in ipairs(catalog.shelves) do
|
|
assertEqual(shelf.height, shelf.card_w + 20, "every shelf is drawn in aspect")
|
|
end
|
|
assertEqual(catalog.shelves[1].cols > 5, true, "a crowded page fits more cards per row")
|
|
assertEqual(catalog.dash_used <= 600, true, "four shelves stay inside the available height")
|
|
|
|
-- Too tight for everything: the stats strip goes before any shelf does.
|
|
catalog = newCatalog{ sections = DEFAULTS, avail = 450 }
|
|
catalog:updateDashboardItems()
|
|
assertEqual(#catalog.shelves, 1, "the shelf survives a tight page")
|
|
assertEqual(catalog.headers[1], "Continue reading", "the stats strip is dropped first")
|
|
assertEqual(catalog.dash_used <= 450, true, "dropping stats brings the page back inside its height")
|
|
|
|
-- Tighter still: shelves go from the bottom up, and the native slots stay.
|
|
catalog = newCatalog{ sections = FOUR_SHELVES, avail = 260 }
|
|
catalog:updateDashboardItems()
|
|
assertEqual(#catalog.shelves < 4, true, "an impossible page drops shelves from the bottom")
|
|
assertEqual(catalog.shelves[1].id, "section1", "the first configured shelf is the last to go")
|
|
assertEqual(catalog.dash_used <= 260, true, "dropping shelves brings the page back inside its height")
|
|
|
|
-- A slot waiting on a refresh renders its placeholder rather than the books
|
|
-- cached for the previous choice.
|
|
catalog = newCatalog{ sections = DEFAULTS, avail = 800 }
|
|
catalog.current_context.section_stale = { [3] = true }
|
|
catalog:updateDashboardItems()
|
|
assertEqual(#catalog.shelves, 0, "a pending shelf renders no cover grid")
|
|
assertEqual(catalog.headers[2], "Discover", "a pending shelf still shows its header")
|
|
|
|
-- A stale (cached) dashboard announces itself with a status line that takes
|
|
-- part in the vertical budget.
|
|
catalog = newCatalog{ sections = DEFAULTS, avail = 800 }
|
|
catalog.current_context.stale = true
|
|
catalog:updateDashboardItems()
|
|
assertEqual(catalog.dash_used <= 800, true, "the status line stays inside the available height")
|
|
local without_status = newCatalog{ sections = DEFAULTS, avail = 800 }
|
|
without_status:updateDashboardItems()
|
|
assertEqual(catalog.dash_used, without_status.dash_used, "budget accounting is unchanged aside from the trailing spacer")
|
|
|
|
-- The Highlight of the day slot renders its measured card and drops after
|
|
-- stats but before any shelf when the page gets tight.
|
|
local WITH_HIGHLIGHT = {
|
|
{ type = "stats" }, { type = "continue-reading" }, { type = "highlight" }, { type = "random" },
|
|
}
|
|
catalog = newCatalog{ sections = WITH_HIGHLIGHT, avail = 800 }
|
|
catalog.current_context.dashboard.highlightOfTheDay = { text = "quote", bookId = 7 }
|
|
catalog:updateDashboardItems()
|
|
assertEqual(catalog.headers[2], "Highlight of the day", "the highlight slot renders its header")
|
|
assertEqual(#catalog.shelves, 1, "the shelf renders alongside the highlight card")
|
|
assertEqual(catalog.dash_used <= 800, true, "the highlight card stays inside the available height")
|
|
|
|
catalog = newCatalog{ sections = WITH_HIGHLIGHT, avail = 340 }
|
|
catalog.current_context.dashboard.highlightOfTheDay = { text = "quote", bookId = 7 }
|
|
catalog:updateDashboardItems()
|
|
local saw_highlight = false
|
|
for _, header in ipairs(catalog.headers) do
|
|
if header == "Highlight of the day" then saw_highlight = true end
|
|
end
|
|
assertEqual(saw_highlight, false, "a tight page drops the highlight card after stats, before shelves")
|
|
assertEqual(#catalog.shelves, 1, "the shelf survives the tight page")
|
|
|
|
-- Header controls are built at the header's label-row height, so a section
|
|
-- carrying chevrons or a reroll button is exactly as tall as a bare one. That
|
|
-- is what keeps the single measured header height the budget uses honest, and
|
|
-- what stops the controls from floating above the label.
|
|
catalog = newCatalog{ sections = DEFAULTS, avail = 800 }
|
|
local prev_control, next_control = catalog:buildDashboardHeaderNav("section3", 2, 4)
|
|
local reroll_control = catalog:buildDashboardRerollButton(3)
|
|
for label, control in pairs({ prev = prev_control, next = next_control, reroll = reroll_control }) do
|
|
assertEqual(control.dimen.h, HEADER_ROW_HEIGHT, label .. " control fills the header row exactly")
|
|
assertEqual(control.dimen.w, HEADER_ROW_HEIGHT, label .. " control is square")
|
|
assertEqual(control.tap_padding_v > 0, true, label .. " control takes taps from the gap around it")
|
|
end
|
|
assertEqual(prev_control.enabled, true, "a page-2 back chevron is live")
|
|
assertEqual(catalog:buildDashboardHeaderNav("section3", 1, 4).enabled, false,
|
|
"the back chevron is disabled on the first page")
|
|
|
|
-- Page turns record each section's band and narrow the refresh to it.
|
|
catalog = newCatalog{ sections = DEFAULTS, avail = 800 }
|
|
catalog.dimen = { x = 0, y = 0, w = 520, h = 900 }
|
|
function catalog:menuChromeHeight() return 30, 40 end
|
|
catalog.dash_refresh_section = "section3"
|
|
catalog:updateDashboardItems()
|
|
local band = catalog.dash_section_bands["section3"]
|
|
assertEqual(band ~= nil and band.h > 0, true, "the shelf records its rendered band")
|
|
local region = catalog.custom_refresh_region
|
|
assertEqual(region ~= nil, true, "a page turn narrows the refresh to the changed band")
|
|
assertEqual(region.y, 30 + band.y - 4, "the refresh band sits below the title bar with its padding")
|
|
assertEqual(region.h, band.h + 8, "the refresh band covers the section plus padding")
|
|
assertEqual(catalog.dash_refresh_section, nil, "the page-turn hint is consumed by the rebuild")
|
|
|
|
print("bookorbit_dashboard_layout_test.lua: ok")
|