mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
feat(koreader): refine catalog detail layout and widget height calculations
Improve KOReader plugin catalog detail view layout by auto-adjusting widget text heights, fixing series line resolution, and polishing star rating, progress button, and navigation button styling.
This commit is contained in:
@@ -1717,7 +1717,8 @@ function BookOrbitCatalog:menuChromeHeight()
|
|||||||
if self:dashboardMode() then
|
if self:dashboardMode() then
|
||||||
-- Reserve a deliberate bottom margin so the single-page dashboard does not
|
-- Reserve a deliberate bottom margin so the single-page dashboard does not
|
||||||
-- run edge-to-edge (the pagination footer that normally fills this is hidden).
|
-- run edge-to-edge (the pagination footer that normally fills this is hidden).
|
||||||
bottom_height = Screen:scaleBySize(40)
|
-- Kept modest: the rest of the page buys its cover sizes out of this.
|
||||||
|
bottom_height = Screen:scaleBySize(16)
|
||||||
elseif self:detailMode() then
|
elseif self:detailMode() then
|
||||||
local prev_book, next_book, current_idx = self:getAdjacentDetailBooks()
|
local prev_book, next_book, current_idx = self:getAdjacentDetailBooks()
|
||||||
local parent = self.stack[#self.stack]
|
local parent = self.stack[#self.stack]
|
||||||
|
|||||||
@@ -54,7 +54,10 @@ local BookOrbitDashboardIconButton = CatalogWidgets.DashboardIconButton
|
|||||||
-- How long the local reading-stats summary is reused before re-querying
|
-- How long the local reading-stats summary is reused before re-querying
|
||||||
-- statistics.sqlite3 (the dashboard re-renders on every row page turn).
|
-- statistics.sqlite3 (the dashboard re-renders on every row page turn).
|
||||||
local STATS_CACHE_TTL = 120
|
local STATS_CACHE_TTL = 120
|
||||||
local SECTION_TARGET_SLOTS = 5
|
-- Six columns is what makes a two-up Continue reading card exactly three shelf
|
||||||
|
-- columns wide at any screen size: one for its cover, two for its text. That is
|
||||||
|
-- also the width the hero sizes its cover from, so the two rows agree.
|
||||||
|
local SECTION_TARGET_SLOTS = 6
|
||||||
local SECTION_COMPACT_GAP = 6
|
local SECTION_COMPACT_GAP = 6
|
||||||
local STATS_MIN_BODY_HEIGHT = 56
|
local STATS_MIN_BODY_HEIGHT = 56
|
||||||
local SECTION_PAGE_PREFIX = "section"
|
local SECTION_PAGE_PREFIX = "section"
|
||||||
@@ -63,6 +66,10 @@ local SECTION_PAGE_PREFIX = "section"
|
|||||||
local SHELF_PAGE_SIZE = 12
|
local SHELF_PAGE_SIZE = 12
|
||||||
-- Cover cards never get narrower than this, however tight the page is.
|
-- Cover cards never get narrower than this, however tight the page is.
|
||||||
local SHELF_MIN_CARD_WIDTH = 72
|
local SHELF_MIN_CARD_WIDTH = 72
|
||||||
|
-- Bounds on the Continue reading hero, which is otherwise sized to match the
|
||||||
|
-- shelf cover.
|
||||||
|
local HERO_MIN_HEIGHT = 76
|
||||||
|
local HERO_MAX_HEIGHT = 170
|
||||||
-- Header controls are drawn inside the section header's label row, so the row
|
-- Header controls are drawn inside the section header's label row, so the row
|
||||||
-- stays as tall as a header without controls; the tap box reaches into the gap
|
-- stays as tall as a header without controls; the tap box reaches into the gap
|
||||||
-- above and below it to stay finger-sized.
|
-- above and below it to stay finger-sized.
|
||||||
@@ -934,8 +941,10 @@ function CatalogDashboard:updateDashboardItems(select_number, no_recalculate_dim
|
|||||||
local function px(n) return Screen:scaleBySize(n) end
|
local function px(n) return Screen:scaleBySize(n) end
|
||||||
local avail = self.available_height
|
local avail = self.available_height
|
||||||
local inner_gap = px(9)
|
local inner_gap = px(9)
|
||||||
local top_gap = px(4)
|
-- Lets the greeting in the title bar breathe before the first section
|
||||||
local section_gap = px(18)
|
-- starts, rather than the stats rule sitting right under it.
|
||||||
|
local top_gap = px(10)
|
||||||
|
local section_gap = px(20)
|
||||||
local compact_gap = px(SECTION_COMPACT_GAP)
|
local compact_gap = px(SECTION_COMPACT_GAP)
|
||||||
self.dash_inner_gap = inner_gap
|
self.dash_inner_gap = inner_gap
|
||||||
self.dash_used = 0
|
self.dash_used = 0
|
||||||
@@ -945,7 +954,9 @@ function CatalogDashboard:updateDashboardItems(select_number, no_recalculate_dim
|
|||||||
local browse_row_h = px(42)
|
local browse_row_h = px(42)
|
||||||
local browse_cols = 3
|
local browse_cols = 3
|
||||||
local browse_rows = 3
|
local browse_rows = 3
|
||||||
local hero_h = math.min(px(150), math.max(px(76), math.floor(avail * 0.18)))
|
-- A provisional height: once the shelves have settled their column width the
|
||||||
|
-- hero is re-sized from their cover, below.
|
||||||
|
local hero_h = math.min(px(HERO_MAX_HEIGHT), math.max(px(HERO_MIN_HEIGHT), math.floor(avail * 0.18)))
|
||||||
|
|
||||||
local status_widget
|
local status_widget
|
||||||
if context.stale and not context.loading and context.dashboard then
|
if context.stale and not context.loading and context.dashboard then
|
||||||
@@ -971,8 +982,10 @@ function CatalogDashboard:updateDashboardItems(select_number, no_recalculate_dim
|
|||||||
local fixed = top_gap + status_h
|
local fixed = top_gap + status_h
|
||||||
local shelf_count = 0
|
local shelf_count = 0
|
||||||
local max_shelf_books = 0
|
local max_shelf_books = 0
|
||||||
|
local visible = 0
|
||||||
for index = 1, DashboardSections.SLOT_COUNT do
|
for index = 1, DashboardSections.SLOT_COUNT do
|
||||||
if not hidden[index] then
|
if not hidden[index] then
|
||||||
|
visible = visible + 1
|
||||||
local config = configs[index]
|
local config = configs[index]
|
||||||
if config.type == "stats" then
|
if config.type == "stats" then
|
||||||
stats_widgets[index] = stats_widgets[index] or self:buildDashboardStatsStrip(summary, dashboard)
|
stats_widgets[index] = stats_widgets[index] or self:buildDashboardStatsStrip(summary, dashboard)
|
||||||
@@ -993,6 +1006,9 @@ function CatalogDashboard:updateDashboardItems(select_number, no_recalculate_dim
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
-- The gap separates sections, so the last one does not carry one: that
|
||||||
|
-- space belongs to the page's bottom margin, not to the section.
|
||||||
|
if visible > 0 then fixed = fixed - section_gap end
|
||||||
return fixed, shelf_count, max_shelf_books
|
return fixed, shelf_count, max_shelf_books
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1025,11 +1041,37 @@ function CatalogDashboard:updateDashboardItems(select_number, no_recalculate_dim
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
while not pageFits() do
|
local function fitPage()
|
||||||
local drop = slotToDrop()
|
while not pageFits() do
|
||||||
if not drop then break end
|
local drop = slotToDrop()
|
||||||
hidden[drop] = true
|
if not drop then break end
|
||||||
fixed_h, shelf_count, max_shelf_books = measureFixed()
|
hidden[drop] = true
|
||||||
|
fixed_h, shelf_count, max_shelf_books = measureFixed()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
fitPage()
|
||||||
|
|
||||||
|
-- Now that the shelves have settled their column width, grow Continue
|
||||||
|
-- reading so its cover is the same size as a shelf cover rather than the
|
||||||
|
-- smallest cover on the page. Only taken when it costs nothing: a page too
|
||||||
|
-- tight to absorb it keeps the shelf it already has, since a matching hero
|
||||||
|
-- is not worth a denser row of covers or a dropped section. pageFits() only
|
||||||
|
-- recomputes the shelf metrics, so trying it here changes nothing else.
|
||||||
|
if shelf_card_w > 0 then
|
||||||
|
local settled_slots = shelf_slots
|
||||||
|
local matched = math.min(px(HERO_MAX_HEIGHT),
|
||||||
|
math.max(px(HERO_MIN_HEIGHT), CatalogWidgets.dashboardHeroHeightForCoverCard(shelf_card_w)))
|
||||||
|
if matched ~= hero_h then
|
||||||
|
local provisional = hero_h
|
||||||
|
hero_h = matched
|
||||||
|
fixed_h, shelf_count, max_shelf_books = measureFixed()
|
||||||
|
if not pageFits() or shelf_slots ~= settled_slots then
|
||||||
|
hero_h = provisional
|
||||||
|
fixed_h, shelf_count, max_shelf_books = measureFixed()
|
||||||
|
pageFits()
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
self:addDashboardSpacer(top_gap)
|
self:addDashboardSpacer(top_gap)
|
||||||
@@ -1128,8 +1170,12 @@ function CatalogDashboard:updateDashboardItems(select_number, no_recalculate_dim
|
|||||||
self.dash_section_bands[page_id] = { y = band_start, h = self.dash_used - band_start }
|
self.dash_section_bands[page_id] = { y = band_start, h = self.dash_used - band_start }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local rendered = 0
|
||||||
for index = 1, DashboardSections.SLOT_COUNT do
|
for index = 1, DashboardSections.SLOT_COUNT do
|
||||||
if not hidden[index] then
|
if not hidden[index] then
|
||||||
|
if rendered > 0 then
|
||||||
|
self:addDashboardSpacer(section_gap)
|
||||||
|
end
|
||||||
local config = configs[index]
|
local config = configs[index]
|
||||||
if config.type == "stats" then
|
if config.type == "stats" then
|
||||||
renderStats(index)
|
renderStats(index)
|
||||||
@@ -1142,7 +1188,7 @@ function CatalogDashboard:updateDashboardItems(select_number, no_recalculate_dim
|
|||||||
else
|
else
|
||||||
renderShelf(index, config)
|
renderShelf(index, config)
|
||||||
end
|
end
|
||||||
self:addDashboardSpacer(section_gap)
|
rendered = rendered + 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -57,16 +57,27 @@ local shortText = CatalogUtil.shortText
|
|||||||
local buildCoverWidget = CatalogWidgets.buildCoverWidget
|
local buildCoverWidget = CatalogWidgets.buildCoverWidget
|
||||||
local BookOrbitDetailRelatedCard = CatalogWidgets.DetailRelatedCard
|
local BookOrbitDetailRelatedCard = CatalogWidgets.DetailRelatedCard
|
||||||
local BookOrbitDetailTabButton = CatalogWidgets.DetailTabButton
|
local BookOrbitDetailTabButton = CatalogWidgets.DetailTabButton
|
||||||
|
local BookOrbitDashboardIconButton = CatalogWidgets.DashboardIconButton
|
||||||
local BookOrbitDetailRatingStar = CatalogWidgets.DetailRatingStar
|
local BookOrbitDetailRatingStar = CatalogWidgets.DetailRatingStar
|
||||||
|
|
||||||
-- Max genres/tags shown inline on the detail page before a "+N more" pill.
|
-- Max genres/tags shown inline on the detail page before a "+N more" pill.
|
||||||
local DETAIL_TAGS_INLINE = 6
|
local DETAIL_TAGS_INLINE = 6
|
||||||
local DETAIL_CACHE_MAX_BOOKS = 80
|
local DETAIL_CACHE_MAX_BOOKS = 80
|
||||||
|
-- Comfortably more description than fits on any screen the plugin runs on; the
|
||||||
|
-- overview box does the actual truncating.
|
||||||
|
local DETAIL_OVERVIEW_MAX_CHARS = 2000
|
||||||
|
|
||||||
-- Shared spacing scale for the detail page. INSET is the left/right content
|
-- Shared spacing scale for the detail page. INSET is the left/right content
|
||||||
-- margin every section aligns to; the gaps are the only vertical rhythm used.
|
-- margin every section aligns to; the gaps are the only vertical rhythm used.
|
||||||
local DETAIL_INSET = Screen:scaleBySize(16)
|
local DETAIL_INSET = Screen:scaleBySize(16)
|
||||||
local DETAIL_RELATED_SHELF_INSET = Screen:scaleBySize(12)
|
-- The related shelf's paging controls are its outermost elements, so they sit
|
||||||
|
-- on the same margin as every other section rather than the shelf being nudged
|
||||||
|
-- out on an inset of its own.
|
||||||
|
local DETAIL_RELATED_SHELF_INSET = DETAIL_INSET
|
||||||
|
-- Matches the dashboard's section-header controls, so the two pages share one
|
||||||
|
-- paging idiom instead of the shelf using a smaller, fainter pair.
|
||||||
|
local DETAIL_NAV_BOX = Screen:scaleBySize(26)
|
||||||
|
local DETAIL_NAV_ICON = Screen:scaleBySize(19)
|
||||||
local DETAIL_GAP_XS = Screen:scaleBySize(4)
|
local DETAIL_GAP_XS = Screen:scaleBySize(4)
|
||||||
local DETAIL_GAP_S = Screen:scaleBySize(8)
|
local DETAIL_GAP_S = Screen:scaleBySize(8)
|
||||||
local DETAIL_GAP_M = Screen:scaleBySize(14)
|
local DETAIL_GAP_M = Screen:scaleBySize(14)
|
||||||
@@ -101,7 +112,19 @@ function DetailDescriptionWidget:init()
|
|||||||
local face = Font:getFace("smallinfofont", 15)
|
local face = Font:getFace("smallinfofont", 15)
|
||||||
local text_h = math.max(lineHeight(face), self.max_height)
|
local text_h = math.max(lineHeight(face), self.max_height)
|
||||||
|
|
||||||
local dimen = Geom:new{ w = self.width, h = text_h }
|
-- height_adjust lets a short description end where its text ends instead of
|
||||||
|
-- reserving the full block, so the tabs below it do not float on a band of
|
||||||
|
-- white; a long one is still capped and ellipsized at max_height.
|
||||||
|
local box = TextBoxWidget:new{
|
||||||
|
text = BD.auto(text),
|
||||||
|
width = text_w,
|
||||||
|
height = text_h,
|
||||||
|
height_adjust = true,
|
||||||
|
height_overflow_show_ellipsis = true,
|
||||||
|
face = face,
|
||||||
|
}
|
||||||
|
|
||||||
|
local dimen = Geom:new{ w = self.width, h = box:getSize().h }
|
||||||
self.dimen = dimen
|
self.dimen = dimen
|
||||||
|
|
||||||
local has_desc = description ~= nil
|
local has_desc = description ~= nil
|
||||||
@@ -113,13 +136,7 @@ function DetailDescriptionWidget:init()
|
|||||||
|
|
||||||
self[1] = HorizontalGroup:new{
|
self[1] = HorizontalGroup:new{
|
||||||
HorizontalSpan:new{ width = DETAIL_INSET },
|
HorizontalSpan:new{ width = DETAIL_INSET },
|
||||||
TextBoxWidget:new{
|
box,
|
||||||
text = BD.auto(text),
|
|
||||||
width = text_w,
|
|
||||||
height = text_h,
|
|
||||||
height_overflow_show_ellipsis = true,
|
|
||||||
face = face,
|
|
||||||
},
|
|
||||||
HorizontalSpan:new{ width = DETAIL_INSET },
|
HorizontalSpan:new{ width = DETAIL_INSET },
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
@@ -145,7 +162,19 @@ function DetailOverviewWidget:init()
|
|||||||
local face = Font:getFace("x_smallinfofont")
|
local face = Font:getFace("x_smallinfofont")
|
||||||
local text_h = math.max(lineHeight(face), self.max_height)
|
local text_h = math.max(lineHeight(face), self.max_height)
|
||||||
|
|
||||||
local dimen = Geom:new{ w = self.width, h = text_h }
|
local box = TextBoxWidget:new{
|
||||||
|
text = table.concat(self.menu:detailOverviewLines(self.detail), "\n"),
|
||||||
|
width = text_w,
|
||||||
|
height = text_h,
|
||||||
|
height_adjust = true,
|
||||||
|
height_overflow_show_ellipsis = true,
|
||||||
|
face = face,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- The box shrinks to its text, so the widget must report that height too:
|
||||||
|
-- reporting the full block would leave the page padded out with white below
|
||||||
|
-- a short description.
|
||||||
|
local dimen = Geom:new{ w = self.width, h = box:getSize().h }
|
||||||
self.dimen = dimen
|
self.dimen = dimen
|
||||||
|
|
||||||
local has_desc = description ~= nil
|
local has_desc = description ~= nil
|
||||||
@@ -157,14 +186,7 @@ function DetailOverviewWidget:init()
|
|||||||
|
|
||||||
self[1] = HorizontalGroup:new{
|
self[1] = HorizontalGroup:new{
|
||||||
HorizontalSpan:new{ width = DETAIL_INSET },
|
HorizontalSpan:new{ width = DETAIL_INSET },
|
||||||
TextBoxWidget:new{
|
box,
|
||||||
text = table.concat(self.menu:detailOverviewLines(self.detail), "\n"),
|
|
||||||
width = text_w,
|
|
||||||
height = text_h,
|
|
||||||
height_adjust = true,
|
|
||||||
height_overflow_show_ellipsis = true,
|
|
||||||
face = face,
|
|
||||||
},
|
|
||||||
HorizontalSpan:new{ width = DETAIL_INSET },
|
HorizontalSpan:new{ width = DETAIL_INSET },
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
@@ -898,7 +920,10 @@ function CatalogDetail:detailOverviewLines(detail)
|
|||||||
local description = cleanDescriptionText(detail.description)
|
local description = cleanDescriptionText(detail.description)
|
||||||
if description then
|
if description then
|
||||||
table.insert(lines, _("Description"))
|
table.insert(lines, _("Description"))
|
||||||
table.insert(lines, shortText(description, 360))
|
-- A bound on layout work, not a display limit: the overview box gets the
|
||||||
|
-- whole rest of the page and ellipsizes at its own height, so cutting
|
||||||
|
-- the text short here only stranded it mid-sentence above empty space.
|
||||||
|
table.insert(lines, shortText(description, DETAIL_OVERVIEW_MAX_CHARS))
|
||||||
end
|
end
|
||||||
|
|
||||||
if #lines == 0 then
|
if #lines == 0 then
|
||||||
@@ -1102,15 +1127,20 @@ end
|
|||||||
|
|
||||||
function CatalogDetail:buildDetailRating(detail, width)
|
function CatalogDetail:buildDetailRating(detail, width)
|
||||||
local rating = tonumber(detail.rating) or 0
|
local rating = tonumber(detail.rating) or 0
|
||||||
|
local star_w = CatalogWidgets.detailRatingStarWidth()
|
||||||
|
local star_gap = DETAIL_GAP_XS
|
||||||
local row = HorizontalGroup:new{ align = "center" }
|
local row = HorizontalGroup:new{ align = "center" }
|
||||||
self.detail_rating_stars = {}
|
self.detail_rating_stars = {}
|
||||||
for star = 1, 5 do
|
for star = 1, 5 do
|
||||||
|
if star > 1 then
|
||||||
|
table.insert(row, HorizontalSpan:new{ width = star_gap })
|
||||||
|
end
|
||||||
local widget = BookOrbitDetailRatingStar:new{
|
local widget = BookOrbitDetailRatingStar:new{
|
||||||
entry = {
|
entry = {
|
||||||
rating = star,
|
rating = star,
|
||||||
filled = rating >= star,
|
filled = rating >= star,
|
||||||
},
|
},
|
||||||
dimen = Geom:new{ w = DETAIL_STAR_SIZE, h = DETAIL_STAR_SIZE },
|
dimen = Geom:new{ w = star_w, h = DETAIL_STAR_SIZE },
|
||||||
menu = self,
|
menu = self,
|
||||||
}
|
}
|
||||||
table.insert(row, widget)
|
table.insert(row, widget)
|
||||||
@@ -1118,9 +1148,10 @@ function CatalogDetail:buildDetailRating(detail, width)
|
|||||||
end
|
end
|
||||||
table.insert(row, HorizontalSpan:new{ width = DETAIL_GAP_S })
|
table.insert(row, HorizontalSpan:new{ width = DETAIL_GAP_S })
|
||||||
local label_face = Font:getFace("xx_smallinfofont", 12)
|
local label_face = Font:getFace("xx_smallinfofont", 12)
|
||||||
|
local stars_w = 5 * star_w + 4 * star_gap
|
||||||
table.insert(row, TextBoxWidget:new{
|
table.insert(row, TextBoxWidget:new{
|
||||||
text = rating > 0 and formatRating(rating) or _("Not rated"),
|
text = rating > 0 and formatRating(rating) or _("Not rated"),
|
||||||
width = math.max(1, width - 5 * DETAIL_STAR_SIZE - DETAIL_GAP_S),
|
width = math.max(1, width - stars_w - DETAIL_GAP_S),
|
||||||
height = lineHeight(label_face),
|
height = lineHeight(label_face),
|
||||||
height_overflow_show_ellipsis = true,
|
height_overflow_show_ellipsis = true,
|
||||||
face = label_face,
|
face = label_face,
|
||||||
@@ -1199,12 +1230,16 @@ end
|
|||||||
-- glanceable and one tap to change, without competing with the action button.
|
-- glanceable and one tap to change, without competing with the action button.
|
||||||
function CatalogDetail:buildDetailProgress(detail, width)
|
function CatalogDetail:buildDetailProgress(detail, width)
|
||||||
local label_face = Font:getFace("xx_smallinfofont", 12)
|
local label_face = Font:getFace("xx_smallinfofont", 12)
|
||||||
|
-- A caption, not a headline: the chevron carries the affordance, so it sits
|
||||||
|
-- at the same weight and size as the progress text it shares the line with
|
||||||
|
-- rather than out-shouting it.
|
||||||
self.detail_status_button = Button:new{
|
self.detail_status_button = Button:new{
|
||||||
text = (self:readStatusLabel(detail) or _("Unread")) .. " ▾",
|
text = (self:readStatusLabel(detail) or _("Unread")) .. " ▾",
|
||||||
bordersize = 0,
|
bordersize = 0,
|
||||||
margin = 0,
|
margin = 0,
|
||||||
padding = 0,
|
padding = 0,
|
||||||
text_font_size = 13,
|
text_font_size = 12,
|
||||||
|
text_font_bold = false,
|
||||||
callback = function()
|
callback = function()
|
||||||
self:showSetStatusDialog(detail)
|
self:showSetStatusDialog(detail)
|
||||||
end,
|
end,
|
||||||
@@ -1276,7 +1311,9 @@ function CatalogDetail:buildDetailHeader(detail, width)
|
|||||||
local title_face = Font:getFace("cfont", 21)
|
local title_face = Font:getFace("cfont", 21)
|
||||||
local author_face = Font:getFace("smallinfofont", 16)
|
local author_face = Font:getFace("smallinfofont", 16)
|
||||||
local meta_face = Font:getFace("x_smallinfofont", 13)
|
local meta_face = Font:getFace("x_smallinfofont", 13)
|
||||||
local series_line = self:detailSeriesLine(detail)
|
-- Static helper: calling it with `self:` passes the catalog as the detail
|
||||||
|
-- and silently yields no series line at all.
|
||||||
|
local series_line = CatalogDetail.detailSeriesLine(detail)
|
||||||
local meta_line = self:detailHeroMetaLine(detail)
|
local meta_line = self:detailHeroMetaLine(detail)
|
||||||
|
|
||||||
local function buildTop(with_series)
|
local function buildTop(with_series)
|
||||||
@@ -1441,7 +1478,7 @@ function CatalogDetail:buildDetailRelatedSection(section, width, height)
|
|||||||
|
|
||||||
local content_w = width - 2 * DETAIL_RELATED_SHELF_INSET
|
local content_w = width - 2 * DETAIL_RELATED_SHELF_INSET
|
||||||
local gap = Screen:scaleBySize(10)
|
local gap = Screen:scaleBySize(10)
|
||||||
local nav_w = Screen:scaleBySize(20)
|
local nav_w = DETAIL_NAV_BOX
|
||||||
local nav_gap = Screen:scaleBySize(4)
|
local nav_gap = Screen:scaleBySize(4)
|
||||||
local nav_gutter_w = nav_w + nav_gap
|
local nav_gutter_w = nav_w + nav_gap
|
||||||
local slots = 4
|
local slots = 4
|
||||||
@@ -1482,19 +1519,21 @@ function CatalogDetail:buildDetailRelatedSection(section, width, height)
|
|||||||
local row = HorizontalGroup:new{ align = "center" }
|
local row = HorizontalGroup:new{ align = "center" }
|
||||||
local prev_button
|
local prev_button
|
||||||
local next_button
|
local next_button
|
||||||
if layout.has_nav then
|
local function navButton(icon, enabled, delta)
|
||||||
prev_button = Button:new{
|
return BookOrbitDashboardIconButton:new{
|
||||||
icon = "chevron.left",
|
entry = { icon = icon },
|
||||||
icon_width = Screen:scaleBySize(16),
|
dimen = Geom:new{ x = 0, y = 0, w = nav_w, h = nav_w },
|
||||||
icon_height = Screen:scaleBySize(16),
|
icon_size = DETAIL_NAV_ICON,
|
||||||
width = nav_w,
|
enabled = enabled,
|
||||||
bordersize = 0,
|
|
||||||
show_border = false,
|
|
||||||
enabled = page > 1,
|
|
||||||
callback = function()
|
callback = function()
|
||||||
self:turnDetailRelatedPage(section_id, -1)
|
self:turnDetailRelatedPage(section_id, delta)
|
||||||
end,
|
end,
|
||||||
|
menu = self,
|
||||||
}
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
if layout.has_nav then
|
||||||
|
prev_button = navButton("chevron.left", page > 1, -1)
|
||||||
table.insert(row, CenterContainer:new{
|
table.insert(row, CenterContainer:new{
|
||||||
dimen = Geom:new{ w = nav_w, h = layout.total_h },
|
dimen = Geom:new{ w = nav_w, h = layout.total_h },
|
||||||
prev_button,
|
prev_button,
|
||||||
@@ -1541,18 +1580,7 @@ function CatalogDetail:buildDetailRelatedSection(section, width, height)
|
|||||||
|
|
||||||
if layout.has_nav then
|
if layout.has_nav then
|
||||||
table.insert(row, HorizontalSpan:new{ width = nav_gap })
|
table.insert(row, HorizontalSpan:new{ width = nav_gap })
|
||||||
next_button = Button:new{
|
next_button = navButton("chevron.right", page < page_count, 1)
|
||||||
icon = "chevron.right",
|
|
||||||
icon_width = Screen:scaleBySize(16),
|
|
||||||
icon_height = Screen:scaleBySize(16),
|
|
||||||
width = nav_w,
|
|
||||||
bordersize = 0,
|
|
||||||
show_border = false,
|
|
||||||
enabled = page < page_count,
|
|
||||||
callback = function()
|
|
||||||
self:turnDetailRelatedPage(section_id, 1)
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
table.insert(row, CenterContainer:new{
|
table.insert(row, CenterContainer:new{
|
||||||
dimen = Geom:new{ w = nav_w, h = layout.total_h },
|
dimen = Geom:new{ w = nav_w, h = layout.total_h },
|
||||||
next_button,
|
next_button,
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ local READ_STATUS_BADGE_ICONS = {
|
|||||||
|
|
||||||
local CatalogWidgets = {}
|
local CatalogWidgets = {}
|
||||||
|
|
||||||
|
-- Mirrors TextBoxWidget's own line height math so text blocks can be sized in
|
||||||
|
-- whole lines (same helper as the detail page uses).
|
||||||
|
local function lineHeight(face)
|
||||||
|
return math.floor(1.3 * face.size + 0.5)
|
||||||
|
end
|
||||||
|
|
||||||
-- Absolute paths to the plugin's own SVG icons, resolved once by the catalog
|
-- Absolute paths to the plugin's own SVG icons, resolved once by the catalog
|
||||||
-- (widgets cannot know the plugin directory). A missing entry falls back to
|
-- (widgets cannot know the plugin directory). A missing entry falls back to
|
||||||
-- the stock KOReader glyph named alongside it.
|
-- the stock KOReader glyph named alongside it.
|
||||||
@@ -128,6 +134,19 @@ function CatalogWidgets.buildFakeCover(book, width, height, footer, quiet, borde
|
|||||||
local inner_w = math.max(1, width - 2 * Size.padding.default - 2 * bordersize)
|
local inner_w = math.max(1, width - 2 * Size.padding.default - 2 * bordersize)
|
||||||
local inner_h = math.max(1, height - 2 * Size.padding.default - 2 * bordersize)
|
local inner_h = math.max(1, height - 2 * Size.padding.default - 2 * bordersize)
|
||||||
if quiet then
|
if quiet then
|
||||||
|
local label = TextWidget:new{
|
||||||
|
text = footer or _("No cover"),
|
||||||
|
face = Font:getFace("xx_smallinfofont"),
|
||||||
|
fgcolor = Blitbuffer.COLOR_DARK_GRAY,
|
||||||
|
max_width = inner_w,
|
||||||
|
}
|
||||||
|
-- On a narrow cover the label survives only as an initial and an
|
||||||
|
-- ellipsis, which reads as damage; an empty frame says "no cover"
|
||||||
|
-- better than "C..." does.
|
||||||
|
if label:isTruncated() then
|
||||||
|
label:free()
|
||||||
|
label = HorizontalSpan:new{ width = inner_w }
|
||||||
|
end
|
||||||
return FrameContainer:new{
|
return FrameContainer:new{
|
||||||
width = width,
|
width = width,
|
||||||
height = height,
|
height = height,
|
||||||
@@ -137,47 +156,60 @@ function CatalogWidgets.buildFakeCover(book, width, height, footer, quiet, borde
|
|||||||
background = Blitbuffer.COLOR_WHITE,
|
background = Blitbuffer.COLOR_WHITE,
|
||||||
CenterContainer:new{
|
CenterContainer:new{
|
||||||
dimen = Geom:new{ w = inner_w, h = inner_h },
|
dimen = Geom:new{ w = inner_w, h = inner_h },
|
||||||
TextWidget:new{
|
label,
|
||||||
text = footer or _("No cover"),
|
|
||||||
face = Font:getFace("xx_smallinfofont"),
|
|
||||||
fgcolor = Blitbuffer.COLOR_DARK_GRAY,
|
|
||||||
max_width = inner_w,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
local title_h = math.floor(inner_h * 0.58)
|
-- Blocks are measured in whole lines and each takes only what it needs, so
|
||||||
local author_h = math.floor(inner_h * 0.22)
|
-- the group can be centred in the cover box. Splitting the box into fixed
|
||||||
local footer_h = math.max(1, inner_h - title_h - author_h)
|
-- proportions instead left a short title floating above a band of white and
|
||||||
|
-- clipped a two-word author to "Arkady...". Anything that still does not fit
|
||||||
|
-- is dropped from the bottom up rather than ellipsized into a fragment.
|
||||||
|
-- Sized explicitly: the face defaults run 22/20/18, so leaving the author
|
||||||
|
-- and footer unsized set them larger than a title asked for at 16 and stood
|
||||||
|
-- the hierarchy on its head.
|
||||||
|
local title_face = Font:getFace("smallinfofont", 17)
|
||||||
|
local author_face = Font:getFace("x_smallinfofont", 14)
|
||||||
|
local footer_face = Font:getFace("xx_smallinfofont", 12)
|
||||||
|
local gap = Size.span.vertical_default
|
||||||
local author = book and firstAuthor(book) or nil
|
local author = book and firstAuthor(book) or nil
|
||||||
|
local has_footer = footer ~= nil and footer ~= ""
|
||||||
|
|
||||||
local content = VerticalGroup:new{ align = "center" }
|
local function textBlock(text, face, max_lines)
|
||||||
table.insert(content, VerticalSpan:new{ width = Size.span.vertical_default })
|
return TextBoxWidget:new{
|
||||||
table.insert(content, TextBoxWidget:new{
|
text = text,
|
||||||
text = BD.auto(shortText(book and book.title or _("Untitled"), 60)),
|
width = inner_w,
|
||||||
width = inner_w,
|
height = max_lines * lineHeight(face),
|
||||||
height = title_h,
|
height_adjust = true,
|
||||||
alignment = "center",
|
alignment = "center",
|
||||||
face = Font:getFace("smallinfofont", 16),
|
face = face,
|
||||||
height_overflow_show_ellipsis = true,
|
height_overflow_show_ellipsis = true,
|
||||||
})
|
}
|
||||||
table.insert(content, TextBoxWidget:new{
|
end
|
||||||
text = author and BD.auto(shortText(author, 44)) or "",
|
|
||||||
width = inner_w,
|
local function buildContent(with_author, with_footer)
|
||||||
height = author_h,
|
local group = VerticalGroup:new{ align = "center" }
|
||||||
alignment = "center",
|
table.insert(group, textBlock(
|
||||||
face = Font:getFace("x_smallinfofont"),
|
BD.auto(shortText(book and book.title or _("Untitled"), 60)), title_face, 3))
|
||||||
height_overflow_show_ellipsis = true,
|
if with_author then
|
||||||
})
|
table.insert(group, VerticalSpan:new{ width = gap })
|
||||||
table.insert(content, TextBoxWidget:new{
|
table.insert(group, textBlock(BD.auto(shortText(author, 44)), author_face, 2))
|
||||||
text = footer or "",
|
end
|
||||||
width = inner_w,
|
if with_footer then
|
||||||
height = footer_h,
|
table.insert(group, VerticalSpan:new{ width = gap })
|
||||||
alignment = "center",
|
table.insert(group, textBlock(footer, footer_face, 2))
|
||||||
face = Font:getFace("xx_smallinfofont"),
|
end
|
||||||
height_overflow_show_ellipsis = true,
|
return group
|
||||||
})
|
end
|
||||||
|
|
||||||
|
local content = buildContent(author ~= nil, has_footer)
|
||||||
|
if content:getSize().h > inner_h and has_footer then
|
||||||
|
content = buildContent(author ~= nil, false)
|
||||||
|
end
|
||||||
|
if content:getSize().h > inner_h and author then
|
||||||
|
content = buildContent(false, false)
|
||||||
|
end
|
||||||
|
|
||||||
return FrameContainer:new{
|
return FrameContainer:new{
|
||||||
width = width,
|
width = width,
|
||||||
@@ -232,9 +264,12 @@ function CatalogWidgets.buildCoverWidget(book, width, height, path, state, opts)
|
|||||||
end
|
end
|
||||||
-- Quiet placeholders are reserved for covers still loading; a cover that is
|
-- Quiet placeholders are reserved for covers still loading; a cover that is
|
||||||
-- missing or failed keeps the titled fake cover so the book stays
|
-- missing or failed keeps the titled fake cover so the book stays
|
||||||
-- identifiable on caption-less shelves.
|
-- identifiable on caption-less shelves. always_quiet is for callers whose
|
||||||
return CatalogWidgets.buildFakeCover(
|
-- own row already names the book, where a titled placeholder would only
|
||||||
book, width, height, footer, opts.quiet_placeholder == true and state == "loading", bordersize)
|
-- repeat it - badly, since those covers are far too narrow to set it in.
|
||||||
|
local quiet = opts.always_quiet == true
|
||||||
|
or (opts.quiet_placeholder == true and state == "loading")
|
||||||
|
return CatalogWidgets.buildFakeCover(book, width, height, footer, quiet, bordersize)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Badges sit on a small white plate so they stay legible over busy cover art;
|
-- Badges sit on a small white plate so they stay legible over busy cover art;
|
||||||
@@ -287,8 +322,8 @@ function CatalogWidgets.buildDownloadedBadge(max_width)
|
|||||||
return badgeChip(IconWidget:new(opts))
|
return badgeChip(IconWidget:new(opts))
|
||||||
end
|
end
|
||||||
|
|
||||||
function CatalogWidgets.buildCoverWithStateBadges(book, width, height, path, state, downloaded, selected)
|
function CatalogWidgets.buildCoverWithStateBadges(book, width, height, path, state, downloaded, selected, cover_opts)
|
||||||
local cover = CatalogWidgets.buildCoverWidget(book, width, height, path, state)
|
local cover = CatalogWidgets.buildCoverWidget(book, width, height, path, state, cover_opts)
|
||||||
if not downloaded and not selected then return cover end
|
if not downloaded and not selected then return cover end
|
||||||
|
|
||||||
local group = OverlapGroup:new{
|
local group = OverlapGroup:new{
|
||||||
@@ -346,7 +381,10 @@ function MosaicItem:init()
|
|||||||
|
|
||||||
local book = self.entry.book
|
local book = self.entry.book
|
||||||
local show_label = self.menu.mosaic_show_titles == true
|
local show_label = self.menu.mosaic_show_titles == true
|
||||||
local bar_reserve = hasProgress(book) and (PROGRESS_BAR_HEIGHT + Size.span.vertical_default) or 0
|
-- Reserved for every cell, drawn only where there is progress: sizing the
|
||||||
|
-- cell from whether this particular book has a bar left covers in the same
|
||||||
|
-- row sitting a few pixels off each other.
|
||||||
|
local bar_reserve = PROGRESS_BAR_HEIGHT + Size.span.vertical_default
|
||||||
local label_h = show_label and math.max(Screen:scaleBySize(44), math.floor(self.dimen.h * 0.24)) or 0
|
local label_h = show_label and math.max(Screen:scaleBySize(44), math.floor(self.dimen.h * 0.24)) or 0
|
||||||
local label_gap = show_label and Size.span.vertical_default or 0
|
local label_gap = show_label and Size.span.vertical_default or 0
|
||||||
local max_cover_w = math.max(1, self.dimen.w - 2 * Size.padding.default)
|
local max_cover_w = math.max(1, self.dimen.w - 2 * Size.padding.default)
|
||||||
@@ -361,12 +399,14 @@ function MosaicItem:init()
|
|||||||
local content = VerticalGroup:new{ align = "center" }
|
local content = VerticalGroup:new{ align = "center" }
|
||||||
table.insert(
|
table.insert(
|
||||||
content,
|
content,
|
||||||
CatalogWidgets.buildCoverWithStateBadges(book, cover_w, cover_h, path, state, downloaded, selected))
|
-- With labels on, the cell already names the book below the cover, so a
|
||||||
|
-- titled placeholder would just say it twice.
|
||||||
|
CatalogWidgets.buildCoverWithStateBadges(
|
||||||
|
book, cover_w, cover_h, path, state, downloaded, selected,
|
||||||
|
{ always_quiet = show_label }))
|
||||||
local bar = CatalogWidgets.buildProgressBar(book and book.progressPercentage, cover_w)
|
local bar = CatalogWidgets.buildProgressBar(book and book.progressPercentage, cover_w)
|
||||||
if bar then
|
table.insert(content, VerticalSpan:new{ width = Size.span.vertical_default })
|
||||||
table.insert(content, VerticalSpan:new{ width = Size.span.vertical_default })
|
table.insert(content, bar or VerticalSpan:new{ width = PROGRESS_BAR_HEIGHT })
|
||||||
table.insert(content, bar)
|
|
||||||
end
|
|
||||||
if show_label then
|
if show_label then
|
||||||
local label_text = shortText(book and book.title or _("Untitled"), 30)
|
local label_text = shortText(book and book.title or _("Untitled"), 30)
|
||||||
local label_w = math.max(1, self.dimen.w - 2 * Size.padding.tiny)
|
local label_w = math.max(1, self.dimen.w - 2 * Size.padding.tiny)
|
||||||
@@ -442,7 +482,9 @@ function ListItem:init()
|
|||||||
local content_h = math.max(1, inner_h - 2 * Size.padding.small)
|
local content_h = math.max(1, inner_h - 2 * Size.padding.small)
|
||||||
local cover_h = math.max(Screen:scaleBySize(38), math.min(Screen:scaleBySize(74), content_h))
|
local cover_h = math.max(Screen:scaleBySize(38), math.min(Screen:scaleBySize(74), content_h))
|
||||||
local cover_w = math.floor(cover_h * 0.68)
|
local cover_w = math.floor(cover_h * 0.68)
|
||||||
local bar_reserve = hasProgress(book) and (PROGRESS_BAR_HEIGHT + Size.span.vertical_default) or 0
|
-- Reserved on every row so the title and subtitle sit on the same lines
|
||||||
|
-- whether or not this book has a progress bar under them.
|
||||||
|
local bar_reserve = PROGRESS_BAR_HEIGHT + Size.span.vertical_default
|
||||||
local left_w = cover_w + 2 * pad
|
local left_w = cover_w + 2 * pad
|
||||||
local side_meta_text = self.menu:listSideMetaText(book)
|
local side_meta_text = self.menu:listSideMetaText(book)
|
||||||
local show_side_meta = side_meta_text ~= "" and self.dimen.w >= Screen:scaleBySize(520)
|
local show_side_meta = side_meta_text ~= "" and self.dimen.w >= Screen:scaleBySize(520)
|
||||||
@@ -492,10 +534,8 @@ function ListItem:init()
|
|||||||
local body_col = VerticalGroup:new{ align = "left" }
|
local body_col = VerticalGroup:new{ align = "left" }
|
||||||
table.insert(body_col, text_col)
|
table.insert(body_col, text_col)
|
||||||
local bar = CatalogWidgets.buildProgressBar(book and book.progressPercentage, main_w)
|
local bar = CatalogWidgets.buildProgressBar(book and book.progressPercentage, main_w)
|
||||||
if bar then
|
table.insert(body_col, VerticalSpan:new{ width = Size.span.vertical_default })
|
||||||
table.insert(body_col, VerticalSpan:new{ width = Size.span.vertical_default })
|
table.insert(body_col, bar or VerticalSpan:new{ width = PROGRESS_BAR_HEIGHT })
|
||||||
table.insert(body_col, bar)
|
|
||||||
end
|
|
||||||
|
|
||||||
local row_dimen = Geom:new{ w = self.dimen.w, h = inner_h }
|
local row_dimen = Geom:new{ w = self.dimen.w, h = inner_h }
|
||||||
local row = OverlapGroup:new{
|
local row = OverlapGroup:new{
|
||||||
@@ -504,7 +544,13 @@ function ListItem:init()
|
|||||||
dimen = row_dimen:copy(),
|
dimen = row_dimen:copy(),
|
||||||
CenterContainer:new{
|
CenterContainer:new{
|
||||||
dimen = Geom:new{ w = left_w, h = inner_h },
|
dimen = Geom:new{ w = left_w, h = inner_h },
|
||||||
CatalogWidgets.buildCoverWithStateBadges(book, cover_w, cover_h, path, state, downloaded, selected),
|
-- The meta column already spells out "On device", so the badge
|
||||||
|
-- would only repeat it on a cover far too small to spare the
|
||||||
|
-- room; it stays for the narrow layouts that hide that column.
|
||||||
|
CatalogWidgets.buildCoverWithStateBadges(
|
||||||
|
book, cover_w, cover_h, path, state,
|
||||||
|
downloaded and not show_side_meta, selected,
|
||||||
|
{ always_quiet = true }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
LeftContainer:new{
|
LeftContainer:new{
|
||||||
@@ -587,12 +633,6 @@ local function cardFrame(width, height, padding, child)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Mirrors TextBoxWidget's own line height math so single-line boxes can be
|
|
||||||
-- sized exactly (same helper as the detail page uses).
|
|
||||||
local function lineHeight(face)
|
|
||||||
return math.floor(1.3 * face.size + 0.5)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Caption fonts under captioned dashboard cover cards (title + author/percent).
|
-- Caption fonts under captioned dashboard cover cards (title + author/percent).
|
||||||
local CAPTION_TITLE_FONT_SIZE = 13
|
local CAPTION_TITLE_FONT_SIZE = 13
|
||||||
local CAPTION_SUB_FONT_SIZE = 11
|
local CAPTION_SUB_FONT_SIZE = 11
|
||||||
@@ -629,6 +669,16 @@ function CatalogWidgets.coverCardHeight(card_w, with_progress, with_caption)
|
|||||||
return cover_h + bar_h + caption_h + 2 * COVER_CARD_PAD + 2 * CARD_BORDER
|
return cover_h + bar_h + caption_h + 2 * COVER_CARD_PAD + 2 * CARD_BORDER
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- The Continue reading hero card height whose cover comes out the same size as
|
||||||
|
-- a shelf cover card of the given width. The hero is otherwise sized from a
|
||||||
|
-- share of the page, which left the featured row showing a smaller cover than
|
||||||
|
-- the browse shelf under it.
|
||||||
|
function CatalogWidgets.dashboardHeroHeightForCoverCard(card_w)
|
||||||
|
local cover_h = coverCardCoverHeight(
|
||||||
|
CatalogWidgets.coverCardHeight(card_w, false, false), false, false)
|
||||||
|
return cover_h + 2 * Size.padding.default + 2 * CARD_BORDER
|
||||||
|
end
|
||||||
|
|
||||||
function CatalogWidgets.detailRelatedCardWidth(card_h)
|
function CatalogWidgets.detailRelatedCardWidth(card_h)
|
||||||
local cover_h = math.max(Screen:scaleBySize(54), card_h - 2 * COVER_CARD_PAD - 2 * CARD_BORDER)
|
local cover_h = math.max(Screen:scaleBySize(54), card_h - 2 * COVER_CARD_PAD - 2 * CARD_BORDER)
|
||||||
local cover_w = math.floor(cover_h * COVER_ASPECT)
|
local cover_w = math.floor(cover_h * COVER_ASPECT)
|
||||||
@@ -1278,6 +1328,22 @@ function DetailTabButton:onHoldSelect()
|
|||||||
return self:onTapSelect()
|
return self:onTapSelect()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local DETAIL_STAR_FONT_SIZE = 22
|
||||||
|
|
||||||
|
-- The width a star cell needs to hold its glyph. Sizing cells to the glyph
|
||||||
|
-- rather than to the row height keeps the first star flush with the title and
|
||||||
|
-- author above it; a square cell would centre the glyph and indent the row.
|
||||||
|
function CatalogWidgets.detailRatingStarWidth()
|
||||||
|
local probe = TextWidget:new{
|
||||||
|
text = "★",
|
||||||
|
face = Font:getFace("cfont", DETAIL_STAR_FONT_SIZE),
|
||||||
|
bold = true,
|
||||||
|
}
|
||||||
|
local width = probe:getSize().w
|
||||||
|
probe:free()
|
||||||
|
return width + 2 * Size.padding.tiny
|
||||||
|
end
|
||||||
|
|
||||||
local DetailRatingStar = InputContainer:extend{
|
local DetailRatingStar = InputContainer:extend{
|
||||||
entry = nil,
|
entry = nil,
|
||||||
dimen = nil,
|
dimen = nil,
|
||||||
@@ -1299,7 +1365,7 @@ function DetailRatingStar:init()
|
|||||||
height = self.dimen.h,
|
height = self.dimen.h,
|
||||||
alignment = "center",
|
alignment = "center",
|
||||||
bold = true,
|
bold = true,
|
||||||
face = Font:getFace("cfont", 22),
|
face = Font:getFace("cfont", DETAIL_STAR_FONT_SIZE),
|
||||||
height_overflow_show_ellipsis = true,
|
height_overflow_show_ellipsis = true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,9 +180,9 @@ catalog = newCatalog{}
|
|||||||
catalog.content_w = 500
|
catalog.content_w = 500
|
||||||
catalog.dash_inner_gap = 9
|
catalog.dash_inner_gap = 9
|
||||||
local slots, card_w, grid_h = catalog:sectionRowMetrics(12, 6)
|
local slots, card_w, grid_h = catalog:sectionRowMetrics(12, 6)
|
||||||
assertEqual(slots, 5, "a wide dashboard budgets five cover slots")
|
assertEqual(slots, 6, "a wide dashboard budgets six cover slots, the column cap")
|
||||||
assertEqual(card_w, 95, "cover width subtracts only the four inter-card gaps")
|
assertEqual(card_w, 78, "cover width subtracts only the five inter-card gaps")
|
||||||
assertEqual(grid_h, 115, "grid height follows the measured cover-card height")
|
assertEqual(grid_h, 98, "grid height follows the measured cover-card height")
|
||||||
|
|
||||||
catalog.content_w = 320
|
catalog.content_w = 320
|
||||||
slots, card_w, grid_h = catalog:sectionRowMetrics(12, 6)
|
slots, card_w, grid_h = catalog:sectionRowMetrics(12, 6)
|
||||||
@@ -194,8 +194,8 @@ assertEqual(grid_h, 95, "the narrower grid recomputes its card height")
|
|||||||
-- always the height those cards need, so covers are never drawn out of aspect.
|
-- always the height those cards need, so covers are never drawn out of aspect.
|
||||||
catalog.content_w = 500
|
catalog.content_w = 500
|
||||||
slots, card_w, grid_h = catalog:shelfRowMetrics(12, 6, 999)
|
slots, card_w, grid_h = catalog:shelfRowMetrics(12, 6, 999)
|
||||||
assertEqual(slots, 5, "a shelf with room to spare keeps its natural row")
|
assertEqual(slots, 6, "a shelf with room to spare keeps its natural row")
|
||||||
assertEqual(grid_h, 115, "an unconstrained shelf keeps its natural height")
|
assertEqual(grid_h, 98, "an unconstrained shelf keeps its natural height")
|
||||||
|
|
||||||
slots, card_w, grid_h = catalog:shelfRowMetrics(12, 6, 100)
|
slots, card_w, grid_h = catalog:shelfRowMetrics(12, 6, 100)
|
||||||
assertEqual(slots, 6, "a tight shelf fits more cards per row")
|
assertEqual(slots, 6, "a tight shelf fits more cards per row")
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ package.loaded["bookorbit_catalog_widgets"] = {
|
|||||||
coverCardHeight = function(card_w)
|
coverCardHeight = function(card_w)
|
||||||
return card_w + 20
|
return card_w + 20
|
||||||
end,
|
end,
|
||||||
|
dashboardHeroHeightForCoverCard = function(card_w)
|
||||||
|
return card_w + 24
|
||||||
|
end,
|
||||||
}
|
}
|
||||||
|
|
||||||
package.path = "koreader-plugin/bookorbit.koplugin/?.lua;" .. package.path
|
package.path = "koreader-plugin/bookorbit.koplugin/?.lua;" .. package.path
|
||||||
@@ -162,7 +165,7 @@ catalog:updateDashboardItems()
|
|||||||
assertEqual(#catalog.shelves, 1, "the default dashboard renders its one shelf")
|
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.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].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.shelves[1].cols, 6, "a roomy shelf keeps its natural slot count")
|
||||||
assertEqual(catalog.dash_used <= 800, true, "a roomy page stays inside the available height")
|
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
|
-- Four shelves share the page and stay in aspect: the cards get narrower rather
|
||||||
@@ -173,7 +176,7 @@ assertEqual(#catalog.shelves, 4, "four configured shelves all render")
|
|||||||
for _, shelf in ipairs(catalog.shelves) do
|
for _, shelf in ipairs(catalog.shelves) do
|
||||||
assertEqual(shelf.height, shelf.card_w + 20, "every shelf is drawn in aspect")
|
assertEqual(shelf.height, shelf.card_w + 20, "every shelf is drawn in aspect")
|
||||||
end
|
end
|
||||||
assertEqual(catalog.shelves[1].cols > 5, true, "a crowded page fits more cards per row")
|
assertEqual(catalog.shelves[1].cols, 6, "a crowded page still stops at the column cap")
|
||||||
assertEqual(catalog.dash_used <= 600, true, "four shelves stay inside the available height")
|
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.
|
-- Too tight for everything: the stats strip goes before any shelf does.
|
||||||
|
|||||||
@@ -59,10 +59,12 @@ package.loaded["ffi/util"] = {
|
|||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
package.loaded["gettext"] = setmetatable({}, { __call = function(_, value) return value end })
|
package.loaded["gettext"] = setmetatable({}, { __call = function(_, value) return value end })
|
||||||
|
local widget_stub = stubWidget()
|
||||||
package.loaded["bookorbit_catalog_widgets"] = {
|
package.loaded["bookorbit_catalog_widgets"] = {
|
||||||
buildCoverWidget = function() end,
|
buildCoverWidget = function() return widget_stub:new() end,
|
||||||
buildDetailPill = function() end,
|
buildDetailPill = function() return widget_stub:new() end,
|
||||||
buildDetailProgressBar = function() end,
|
buildDetailProgressBar = function() return widget_stub:new() end,
|
||||||
|
detailRatingStarWidth = function() return 20 end,
|
||||||
DetailRelatedCard = stubWidget(),
|
DetailRelatedCard = stubWidget(),
|
||||||
DetailTabButton = stubWidget(),
|
DetailTabButton = stubWidget(),
|
||||||
DetailRatingStar = stubWidget(),
|
DetailRatingStar = stubWidget(),
|
||||||
@@ -199,4 +201,37 @@ assertEqual(menu:downloadButtonLabel({ { format = "epub", sizeBytes = 2411724 }
|
|||||||
assertEqual(menu:downloadButtonLabel({ { format = "epub" } }), "Download (EPUB)",
|
assertEqual(menu:downloadButtonLabel({ { format = "epub" } }), "Download (EPUB)",
|
||||||
"an unknown size leaves the original label untouched")
|
"an unknown size leaves the original label untouched")
|
||||||
|
|
||||||
|
-- Hero series line
|
||||||
|
--
|
||||||
|
-- Asserted through buildDetailHeader rather than against the helper directly:
|
||||||
|
-- the helper is a plain function, and the header used to call it as a method,
|
||||||
|
-- which passed the catalog as the detail and silently dropped the line from
|
||||||
|
-- every book page. Only the real call path catches that.
|
||||||
|
|
||||||
|
local header_menu = setmetatable({
|
||||||
|
available_height = 800,
|
||||||
|
inner_dimen = { w = 600, h = 900 },
|
||||||
|
on_device = {},
|
||||||
|
current_context = { supported_files = {} },
|
||||||
|
}, { __index = CatalogDetail })
|
||||||
|
function header_menu:thumbnailDisplay() return nil, "failed" end
|
||||||
|
function header_menu:isOnDevice() return false end
|
||||||
|
function header_menu:readStatusLabel() return nil end
|
||||||
|
function header_menu:supportedFiles() return {} end
|
||||||
|
function header_menu:onDeviceFilePath() return nil end
|
||||||
|
function header_menu:downloadButtonLabel() return "Download" end
|
||||||
|
|
||||||
|
header_menu:buildDetailHeader({
|
||||||
|
title = "The Left Hand of Darkness",
|
||||||
|
authors = { "Ursula K. Le Guin" },
|
||||||
|
seriesName = "Hainish Cycle",
|
||||||
|
seriesIndex = 4,
|
||||||
|
}, 600)
|
||||||
|
assertEqual(header_menu.detail_series_line ~= nil, true,
|
||||||
|
"a book in a series renders its hero series line")
|
||||||
|
|
||||||
|
header_menu:buildDetailHeader({ title = "Standalone", authors = { "Someone" } }, 600)
|
||||||
|
assertEqual(header_menu.detail_series_line, nil,
|
||||||
|
"a book with no series renders no series line")
|
||||||
|
|
||||||
print("bookorbit_detail_info_rows_test.lua: ok")
|
print("bookorbit_detail_info_rows_test.lua: ok")
|
||||||
|
|||||||
Reference in New Issue
Block a user