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
|
||||
-- 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).
|
||||
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
|
||||
local prev_book, next_book, current_idx = self:getAdjacentDetailBooks()
|
||||
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
|
||||
-- statistics.sqlite3 (the dashboard re-renders on every row page turn).
|
||||
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 STATS_MIN_BODY_HEIGHT = 56
|
||||
local SECTION_PAGE_PREFIX = "section"
|
||||
@@ -63,6 +66,10 @@ local SECTION_PAGE_PREFIX = "section"
|
||||
local SHELF_PAGE_SIZE = 12
|
||||
-- Cover cards never get narrower than this, however tight the page is.
|
||||
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
|
||||
-- stays as tall as a header without controls; the tap box reaches into the gap
|
||||
-- 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 avail = self.available_height
|
||||
local inner_gap = px(9)
|
||||
local top_gap = px(4)
|
||||
local section_gap = px(18)
|
||||
-- Lets the greeting in the title bar breathe before the first section
|
||||
-- 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)
|
||||
self.dash_inner_gap = inner_gap
|
||||
self.dash_used = 0
|
||||
@@ -945,7 +954,9 @@ function CatalogDashboard:updateDashboardItems(select_number, no_recalculate_dim
|
||||
local browse_row_h = px(42)
|
||||
local browse_cols = 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
|
||||
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 shelf_count = 0
|
||||
local max_shelf_books = 0
|
||||
local visible = 0
|
||||
for index = 1, DashboardSections.SLOT_COUNT do
|
||||
if not hidden[index] then
|
||||
visible = visible + 1
|
||||
local config = configs[index]
|
||||
if config.type == "stats" then
|
||||
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
|
||||
-- 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
|
||||
end
|
||||
|
||||
@@ -1025,11 +1041,37 @@ function CatalogDashboard:updateDashboardItems(select_number, no_recalculate_dim
|
||||
end
|
||||
end
|
||||
|
||||
while not pageFits() do
|
||||
local drop = slotToDrop()
|
||||
if not drop then break end
|
||||
hidden[drop] = true
|
||||
fixed_h, shelf_count, max_shelf_books = measureFixed()
|
||||
local function fitPage()
|
||||
while not pageFits() do
|
||||
local drop = slotToDrop()
|
||||
if not drop then break end
|
||||
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
|
||||
|
||||
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 }
|
||||
end
|
||||
|
||||
local rendered = 0
|
||||
for index = 1, DashboardSections.SLOT_COUNT do
|
||||
if not hidden[index] then
|
||||
if rendered > 0 then
|
||||
self:addDashboardSpacer(section_gap)
|
||||
end
|
||||
local config = configs[index]
|
||||
if config.type == "stats" then
|
||||
renderStats(index)
|
||||
@@ -1142,7 +1188,7 @@ function CatalogDashboard:updateDashboardItems(select_number, no_recalculate_dim
|
||||
else
|
||||
renderShelf(index, config)
|
||||
end
|
||||
self:addDashboardSpacer(section_gap)
|
||||
rendered = rendered + 1
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -57,16 +57,27 @@ local shortText = CatalogUtil.shortText
|
||||
local buildCoverWidget = CatalogWidgets.buildCoverWidget
|
||||
local BookOrbitDetailRelatedCard = CatalogWidgets.DetailRelatedCard
|
||||
local BookOrbitDetailTabButton = CatalogWidgets.DetailTabButton
|
||||
local BookOrbitDashboardIconButton = CatalogWidgets.DashboardIconButton
|
||||
local BookOrbitDetailRatingStar = CatalogWidgets.DetailRatingStar
|
||||
|
||||
-- Max genres/tags shown inline on the detail page before a "+N more" pill.
|
||||
local DETAIL_TAGS_INLINE = 6
|
||||
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
|
||||
-- margin every section aligns to; the gaps are the only vertical rhythm used.
|
||||
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_S = Screen:scaleBySize(8)
|
||||
local DETAIL_GAP_M = Screen:scaleBySize(14)
|
||||
@@ -101,7 +112,19 @@ function DetailDescriptionWidget:init()
|
||||
local face = Font:getFace("smallinfofont", 15)
|
||||
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
|
||||
|
||||
local has_desc = description ~= nil
|
||||
@@ -113,13 +136,7 @@ function DetailDescriptionWidget:init()
|
||||
|
||||
self[1] = HorizontalGroup:new{
|
||||
HorizontalSpan:new{ width = DETAIL_INSET },
|
||||
TextBoxWidget:new{
|
||||
text = BD.auto(text),
|
||||
width = text_w,
|
||||
height = text_h,
|
||||
height_overflow_show_ellipsis = true,
|
||||
face = face,
|
||||
},
|
||||
box,
|
||||
HorizontalSpan:new{ width = DETAIL_INSET },
|
||||
}
|
||||
end
|
||||
@@ -145,7 +162,19 @@ function DetailOverviewWidget:init()
|
||||
local face = Font:getFace("x_smallinfofont")
|
||||
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
|
||||
|
||||
local has_desc = description ~= nil
|
||||
@@ -157,14 +186,7 @@ function DetailOverviewWidget:init()
|
||||
|
||||
self[1] = HorizontalGroup:new{
|
||||
HorizontalSpan:new{ width = DETAIL_INSET },
|
||||
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,
|
||||
},
|
||||
box,
|
||||
HorizontalSpan:new{ width = DETAIL_INSET },
|
||||
}
|
||||
end
|
||||
@@ -898,7 +920,10 @@ function CatalogDetail:detailOverviewLines(detail)
|
||||
local description = cleanDescriptionText(detail.description)
|
||||
if description then
|
||||
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
|
||||
|
||||
if #lines == 0 then
|
||||
@@ -1102,15 +1127,20 @@ end
|
||||
|
||||
function CatalogDetail:buildDetailRating(detail, width)
|
||||
local rating = tonumber(detail.rating) or 0
|
||||
local star_w = CatalogWidgets.detailRatingStarWidth()
|
||||
local star_gap = DETAIL_GAP_XS
|
||||
local row = HorizontalGroup:new{ align = "center" }
|
||||
self.detail_rating_stars = {}
|
||||
for star = 1, 5 do
|
||||
if star > 1 then
|
||||
table.insert(row, HorizontalSpan:new{ width = star_gap })
|
||||
end
|
||||
local widget = BookOrbitDetailRatingStar:new{
|
||||
entry = {
|
||||
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,
|
||||
}
|
||||
table.insert(row, widget)
|
||||
@@ -1118,9 +1148,10 @@ function CatalogDetail:buildDetailRating(detail, width)
|
||||
end
|
||||
table.insert(row, HorizontalSpan:new{ width = DETAIL_GAP_S })
|
||||
local label_face = Font:getFace("xx_smallinfofont", 12)
|
||||
local stars_w = 5 * star_w + 4 * star_gap
|
||||
table.insert(row, TextBoxWidget:new{
|
||||
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_overflow_show_ellipsis = true,
|
||||
face = label_face,
|
||||
@@ -1199,12 +1230,16 @@ end
|
||||
-- glanceable and one tap to change, without competing with the action button.
|
||||
function CatalogDetail:buildDetailProgress(detail, width)
|
||||
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{
|
||||
text = (self:readStatusLabel(detail) or _("Unread")) .. " ▾",
|
||||
bordersize = 0,
|
||||
margin = 0,
|
||||
padding = 0,
|
||||
text_font_size = 13,
|
||||
text_font_size = 12,
|
||||
text_font_bold = false,
|
||||
callback = function()
|
||||
self:showSetStatusDialog(detail)
|
||||
end,
|
||||
@@ -1276,7 +1311,9 @@ function CatalogDetail:buildDetailHeader(detail, width)
|
||||
local title_face = Font:getFace("cfont", 21)
|
||||
local author_face = Font:getFace("smallinfofont", 16)
|
||||
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 function buildTop(with_series)
|
||||
@@ -1441,7 +1478,7 @@ function CatalogDetail:buildDetailRelatedSection(section, width, height)
|
||||
|
||||
local content_w = width - 2 * DETAIL_RELATED_SHELF_INSET
|
||||
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_gutter_w = nav_w + nav_gap
|
||||
local slots = 4
|
||||
@@ -1482,19 +1519,21 @@ function CatalogDetail:buildDetailRelatedSection(section, width, height)
|
||||
local row = HorizontalGroup:new{ align = "center" }
|
||||
local prev_button
|
||||
local next_button
|
||||
if layout.has_nav then
|
||||
prev_button = Button:new{
|
||||
icon = "chevron.left",
|
||||
icon_width = Screen:scaleBySize(16),
|
||||
icon_height = Screen:scaleBySize(16),
|
||||
width = nav_w,
|
||||
bordersize = 0,
|
||||
show_border = false,
|
||||
enabled = page > 1,
|
||||
local function navButton(icon, enabled, delta)
|
||||
return BookOrbitDashboardIconButton:new{
|
||||
entry = { icon = icon },
|
||||
dimen = Geom:new{ x = 0, y = 0, w = nav_w, h = nav_w },
|
||||
icon_size = DETAIL_NAV_ICON,
|
||||
enabled = enabled,
|
||||
callback = function()
|
||||
self:turnDetailRelatedPage(section_id, -1)
|
||||
self:turnDetailRelatedPage(section_id, delta)
|
||||
end,
|
||||
menu = self,
|
||||
}
|
||||
end
|
||||
|
||||
if layout.has_nav then
|
||||
prev_button = navButton("chevron.left", page > 1, -1)
|
||||
table.insert(row, CenterContainer:new{
|
||||
dimen = Geom:new{ w = nav_w, h = layout.total_h },
|
||||
prev_button,
|
||||
@@ -1541,18 +1580,7 @@ function CatalogDetail:buildDetailRelatedSection(section, width, height)
|
||||
|
||||
if layout.has_nav then
|
||||
table.insert(row, HorizontalSpan:new{ width = nav_gap })
|
||||
next_button = Button:new{
|
||||
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,
|
||||
}
|
||||
next_button = navButton("chevron.right", page < page_count, 1)
|
||||
table.insert(row, CenterContainer:new{
|
||||
dimen = Geom:new{ w = nav_w, h = layout.total_h },
|
||||
next_button,
|
||||
|
||||
@@ -54,6 +54,12 @@ local READ_STATUS_BADGE_ICONS = {
|
||||
|
||||
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
|
||||
-- (widgets cannot know the plugin directory). A missing entry falls back to
|
||||
-- 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_h = math.max(1, height - 2 * Size.padding.default - 2 * bordersize)
|
||||
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{
|
||||
width = width,
|
||||
height = height,
|
||||
@@ -137,47 +156,60 @@ function CatalogWidgets.buildFakeCover(book, width, height, footer, quiet, borde
|
||||
background = Blitbuffer.COLOR_WHITE,
|
||||
CenterContainer:new{
|
||||
dimen = Geom:new{ w = inner_w, h = inner_h },
|
||||
TextWidget:new{
|
||||
text = footer or _("No cover"),
|
||||
face = Font:getFace("xx_smallinfofont"),
|
||||
fgcolor = Blitbuffer.COLOR_DARK_GRAY,
|
||||
max_width = inner_w,
|
||||
},
|
||||
label,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
local title_h = math.floor(inner_h * 0.58)
|
||||
local author_h = math.floor(inner_h * 0.22)
|
||||
local footer_h = math.max(1, inner_h - title_h - author_h)
|
||||
-- Blocks are measured in whole lines and each takes only what it needs, so
|
||||
-- the group can be centred in the cover box. Splitting the box into fixed
|
||||
-- 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 has_footer = footer ~= nil and footer ~= ""
|
||||
|
||||
local content = VerticalGroup:new{ align = "center" }
|
||||
table.insert(content, VerticalSpan:new{ width = Size.span.vertical_default })
|
||||
table.insert(content, TextBoxWidget:new{
|
||||
text = BD.auto(shortText(book and book.title or _("Untitled"), 60)),
|
||||
width = inner_w,
|
||||
height = title_h,
|
||||
alignment = "center",
|
||||
face = Font:getFace("smallinfofont", 16),
|
||||
height_overflow_show_ellipsis = true,
|
||||
})
|
||||
table.insert(content, TextBoxWidget:new{
|
||||
text = author and BD.auto(shortText(author, 44)) or "",
|
||||
width = inner_w,
|
||||
height = author_h,
|
||||
alignment = "center",
|
||||
face = Font:getFace("x_smallinfofont"),
|
||||
height_overflow_show_ellipsis = true,
|
||||
})
|
||||
table.insert(content, TextBoxWidget:new{
|
||||
text = footer or "",
|
||||
width = inner_w,
|
||||
height = footer_h,
|
||||
alignment = "center",
|
||||
face = Font:getFace("xx_smallinfofont"),
|
||||
height_overflow_show_ellipsis = true,
|
||||
})
|
||||
local function textBlock(text, face, max_lines)
|
||||
return TextBoxWidget:new{
|
||||
text = text,
|
||||
width = inner_w,
|
||||
height = max_lines * lineHeight(face),
|
||||
height_adjust = true,
|
||||
alignment = "center",
|
||||
face = face,
|
||||
height_overflow_show_ellipsis = true,
|
||||
}
|
||||
end
|
||||
|
||||
local function buildContent(with_author, with_footer)
|
||||
local group = VerticalGroup:new{ align = "center" }
|
||||
table.insert(group, textBlock(
|
||||
BD.auto(shortText(book and book.title or _("Untitled"), 60)), title_face, 3))
|
||||
if with_author then
|
||||
table.insert(group, VerticalSpan:new{ width = gap })
|
||||
table.insert(group, textBlock(BD.auto(shortText(author, 44)), author_face, 2))
|
||||
end
|
||||
if with_footer then
|
||||
table.insert(group, VerticalSpan:new{ width = gap })
|
||||
table.insert(group, textBlock(footer, footer_face, 2))
|
||||
end
|
||||
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{
|
||||
width = width,
|
||||
@@ -232,9 +264,12 @@ function CatalogWidgets.buildCoverWidget(book, width, height, path, state, opts)
|
||||
end
|
||||
-- Quiet placeholders are reserved for covers still loading; a cover that is
|
||||
-- missing or failed keeps the titled fake cover so the book stays
|
||||
-- identifiable on caption-less shelves.
|
||||
return CatalogWidgets.buildFakeCover(
|
||||
book, width, height, footer, opts.quiet_placeholder == true and state == "loading", bordersize)
|
||||
-- identifiable on caption-less shelves. always_quiet is for callers whose
|
||||
-- own row already names the book, where a titled placeholder would only
|
||||
-- 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
|
||||
|
||||
-- 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))
|
||||
end
|
||||
|
||||
function CatalogWidgets.buildCoverWithStateBadges(book, width, height, path, state, downloaded, selected)
|
||||
local cover = CatalogWidgets.buildCoverWidget(book, width, height, path, state)
|
||||
function CatalogWidgets.buildCoverWithStateBadges(book, width, height, path, state, downloaded, selected, cover_opts)
|
||||
local cover = CatalogWidgets.buildCoverWidget(book, width, height, path, state, cover_opts)
|
||||
if not downloaded and not selected then return cover end
|
||||
|
||||
local group = OverlapGroup:new{
|
||||
@@ -346,7 +381,10 @@ function MosaicItem:init()
|
||||
|
||||
local book = self.entry.book
|
||||
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_gap = show_label and Size.span.vertical_default or 0
|
||||
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" }
|
||||
table.insert(
|
||||
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)
|
||||
if bar then
|
||||
table.insert(content, VerticalSpan:new{ width = Size.span.vertical_default })
|
||||
table.insert(content, bar)
|
||||
end
|
||||
table.insert(content, VerticalSpan:new{ width = Size.span.vertical_default })
|
||||
table.insert(content, bar or VerticalSpan:new{ width = PROGRESS_BAR_HEIGHT })
|
||||
if show_label then
|
||||
local label_text = shortText(book and book.title or _("Untitled"), 30)
|
||||
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 cover_h = math.max(Screen:scaleBySize(38), math.min(Screen:scaleBySize(74), content_h))
|
||||
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 side_meta_text = self.menu:listSideMetaText(book)
|
||||
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" }
|
||||
table.insert(body_col, text_col)
|
||||
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, bar)
|
||||
end
|
||||
table.insert(body_col, VerticalSpan:new{ width = Size.span.vertical_default })
|
||||
table.insert(body_col, bar or VerticalSpan:new{ width = PROGRESS_BAR_HEIGHT })
|
||||
|
||||
local row_dimen = Geom:new{ w = self.dimen.w, h = inner_h }
|
||||
local row = OverlapGroup:new{
|
||||
@@ -504,7 +544,13 @@ function ListItem:init()
|
||||
dimen = row_dimen:copy(),
|
||||
CenterContainer:new{
|
||||
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{
|
||||
@@ -587,12 +633,6 @@ local function cardFrame(width, height, padding, child)
|
||||
}
|
||||
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).
|
||||
local CAPTION_TITLE_FONT_SIZE = 13
|
||||
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
|
||||
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)
|
||||
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)
|
||||
@@ -1278,6 +1328,22 @@ function DetailTabButton:onHoldSelect()
|
||||
return self:onTapSelect()
|
||||
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{
|
||||
entry = nil,
|
||||
dimen = nil,
|
||||
@@ -1299,7 +1365,7 @@ function DetailRatingStar:init()
|
||||
height = self.dimen.h,
|
||||
alignment = "center",
|
||||
bold = true,
|
||||
face = Font:getFace("cfont", 22),
|
||||
face = Font:getFace("cfont", DETAIL_STAR_FONT_SIZE),
|
||||
height_overflow_show_ellipsis = true,
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user