mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
1f4420b454
* feat(koreader): add dashboard row configuration model * feat(koreader): reuse catalog for dashboard source selection * feat(koreader): configure dashboard rows on 1.4.0 * chore(koreader): preserve dashboard source line endings * fix(koreader): keep BookOrbit tools menu loadable * fix(koreader): keep dashboard requests out of rendering * revert(koreader): restore stock 1.4.0 dashboard renderer * revert(koreader): use stock single dashboard row * fix(koreader): apply selected dashboard row source * feat(koreader): add second dashboard row setting * feat(koreader): render second dashboard row * fix(koreader): fetch configured dashboard rows directly * fix(koreader): honor configured dashboard row sources * feat(koreader): support four configurable dashboard sections * fix(koreader): use real catalog dashboard sources * fix(koreader): select dashboard shelves from catalog * fix(koreader): reuse catalog for dashboard source selection * fix(koreader): close dashboard menu before catalog selection * test(koreader): split dashboard configuration coverage * chore(koreader): bump version to 1.5.0 * fix(koreader): address dashboard review feedback * fix(koreader): address dashboard section review feedback * test(koreader): cover dashboard behavior at runtime * test(koreader): use Nest setup for dashboard DTO * chore(ci): trigger pipeline * fix(koreader): migrate untagged dashboard section settings Plugin 1.4.x stored the configurable dashboard rows as a bare ordered list with no schema marker, so gating the migration on schemaVersion == 1 never matched anything a device had actually written. Every upgrading user who had ever picked a row was read positionally instead: the Stats strip disappeared and Discover ended up in two slots, with a stored SmartScope dropped entirely. Treat an untagged stored list as the legacy shape and migrate it into the slots those rows used to occupy. Cover it with the settings shapes 1.4.x really wrote, rather than a marker nothing ever produced. Also drop store() and primary(), which no caller uses now that slots are written through storeAt(), along with the dead grow loop inside storeAt() that normalize() already makes unreachable. * fix(koreader): keep dashboard shelves in aspect, inside the page, and deduped The four-slot renderer handed each shelf a height derived from the leftover vertical budget while its cover cards kept the width computed from the natural row, so any page with more than one shelf drew covers out of aspect. It also had no answer for a page too small for the configured slots: shelves clamped to a 48px floor and the layout simply overflowed the available height. Size shelves from their cards instead. A shelf that does not fit gets more, narrower cards per row, so its height is always the height those cards need. When even the narrowest row does not fit, drop slots the way the pre-four-slot renderer did: the stats strip first, then shelves from the bottom up. Fetch each distinct shelf source once per dashboard load. Two slots showing Discover previously rendered the same list twice, and two slots on the same catalog filter each paid for their own request; a source that fails is now also not retried by the next slot that wants it. Drop what the four-slot rewrite left behind: the unused capabilities require, the tall-layout helper and its constants, and the section accessors and request helper that no longer have callers. Name the per-shelf page size and the minimum cover width instead of repeating the literals. Add setDashboardSections() so a whole configuration can be applied in one refresh. Add a layout spec that drives updateDashboardItems and asserts covers stay in aspect, the drop order holds, and nothing is laid out past the available height. * fix(koreader): close the dashboard slot menu through its own API Reset to Default wrote section_stale as a boolean where the renderer reads a per-slot table, so the reset never actually marked anything pending and stale books stayed on screen under the new headers until the refresh landed. It also dereferenced current_context without a guard and wrote the setting behind the catalog's back. Route it through the new setDashboardSections() so the slots and the cache signature move together in one refresh. Close the invoking touch menu with closeMenu() rather than handing the widget to UIManager:close(), which skips the close_callback that owns its container. Drop the section parameter from catalogDashboard() and the unused catalogDashboardSection() helper: the plugin builds its shelves from the ordinary catalog endpoints now and sends neither. The server keeps both routes for devices still on 1.4.x, so its DTO test stays; rename it to match the file it covers and drop the empty Nest module compile. * revert(koreader): keep the plugin at 1.4.0 1.4.0 has not shipped yet: 1.3.0 is the last released plugin version, so the dashboard slot work can land inside 1.4.0 rather than claiming a version of its own. Devices in the field are on 1.3.0 and see a single 1.3.0 -> 1.4.0 step either way. * fix(koreader): stop rotating upright read-status badges buildReadStatusBadge turned every status icon a quarter turn, but only two of the seven are corner folds. The dogear assets are drawn with their right angle at the top left and the badge sits in the cover's top-right corner, so those do need the turn. Bookmark, check and reload are upright glyphs and were being drawn on their side: a want-to-read cover showed its bookmark ribbon lying sideways like a chevron. Rotate only the dogear icons. --------- Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
213 lines
8.4 KiB
Lua
213 lines
8.4 KiB
Lua
--[[--
|
|
Registry for the four configurable dashboard slots.
|
|
|
|
Each source keeps its native renderer: Stats is a strip, Continue reading is a
|
|
hero row, book sources are cover grids, and Browse is the compact action list.
|
|
A book source is fully described by its type plus, for the sources picked
|
|
through a catalog list, the filter that was selected. The stored value is an
|
|
ordered list matching the four dashboard slots.
|
|
|
|
The selected entry's name is cached alongside its filter purely so the settings
|
|
menu and the section header can label the slot without a request.
|
|
]]
|
|
|
|
local _ = require("gettext")
|
|
|
|
local DashboardSections = {}
|
|
|
|
local PARAM_KEYS = { "libraryId", "collectionId", "smartScopeId", "author", "seriesId", "series", "sort" }
|
|
|
|
DashboardSections.SETTING_KEY = "catalog_dashboard_sections"
|
|
DashboardSections.DEFAULT_TYPE = "random"
|
|
DashboardSections.SCHEMA_VERSION = 2
|
|
DashboardSections.LEGACY_SCHEMA_VERSION = 1
|
|
DashboardSections.SLOT_COUNT = 4
|
|
DashboardSections.DEFAULT_SLOTS = {
|
|
{ type = "stats" },
|
|
{ type = "continue-reading" },
|
|
{ type = "random" },
|
|
{ type = "browse" },
|
|
}
|
|
|
|
-- Order here mirrors the destinations exposed by the dashboard Browse block.
|
|
DashboardSections.TYPES = {
|
|
"stats",
|
|
"continue-reading",
|
|
"random",
|
|
"browse",
|
|
"recently-added",
|
|
"in-progress",
|
|
"libraries",
|
|
"authors",
|
|
"series",
|
|
"collections",
|
|
"smart-scopes",
|
|
}
|
|
|
|
local LABELS = {
|
|
["stats"] = function() return _("Stats") end,
|
|
["continue-reading"] = function() return _("Continue reading") end,
|
|
["random"] = function() return _("Discover") end,
|
|
["browse"] = function() return _("Browse") end,
|
|
["recently-added"] = function() return _("Recently added") end,
|
|
["in-progress"] = function() return _("In progress") end,
|
|
["libraries"] = function() return _("Libraries") end,
|
|
["authors"] = function() return _("Authors") end,
|
|
["series"] = function() return _("Series") end,
|
|
["collections"] = function() return _("Collections") end,
|
|
["smart-scopes"] = function() return _("SmartScopes") end,
|
|
}
|
|
|
|
local HELP_TEXT = {
|
|
["stats"] = function() return _("Your reading summary shown as a compact horizontal strip.") end,
|
|
["continue-reading"] = function() return _("Books currently in progress, shown as the dashboard hero row.") end,
|
|
["random"] = function() return _("A random selection from your whole library, reshuffled with the button in the section header.") end,
|
|
["browse"] = function() return _("Dashboard shortcuts for libraries, collections, SmartScopes, authors, series, and books.") end,
|
|
["recently-added"] = function() return _("The books most recently added to your library.") end,
|
|
["in-progress"] = function() return _("The catalog view for books currently in progress.") end,
|
|
["libraries"] = function() return _("The existing Libraries catalog, including its normal navigation and back button.") end,
|
|
["authors"] = function() return _("The existing Authors catalog, including its normal pagination and back button.") end,
|
|
["series"] = function() return _("The existing Series catalog, including its normal pagination and back button.") end,
|
|
["collections"] = function() return _("The existing Collections catalog and its normal back navigation.") end,
|
|
["smart-scopes"] = function() return _("The existing SmartScopes catalog. Choose a scope there rather than configuring one directly on the dashboard.") end,
|
|
}
|
|
|
|
function DashboardSections.isValid(section_type)
|
|
return LABELS[section_type] ~= nil
|
|
end
|
|
|
|
function DashboardSections.label(section_type)
|
|
local label = LABELS[section_type]
|
|
return label and label() or LABELS[DashboardSections.DEFAULT_TYPE]()
|
|
end
|
|
|
|
function DashboardSections.helpText(section_type)
|
|
local help = HELP_TEXT[section_type]
|
|
return help and help() or nil
|
|
end
|
|
|
|
local BOOK_SOURCES = {
|
|
["random"] = true,
|
|
["recently-added"] = true,
|
|
["in-progress"] = true,
|
|
["libraries"] = true,
|
|
["authors"] = true,
|
|
["series"] = true,
|
|
["collections"] = true,
|
|
["smart-scopes"] = true,
|
|
}
|
|
|
|
local CATALOG_SELECTORS = {
|
|
["libraries"] = true,
|
|
["authors"] = true,
|
|
["series"] = true,
|
|
["collections"] = true,
|
|
["smart-scopes"] = true,
|
|
}
|
|
|
|
function DashboardSections.isBookSource(section_type)
|
|
return BOOK_SOURCES[section_type] == true
|
|
end
|
|
|
|
function DashboardSections.isCatalogSelector(section_type)
|
|
return CATALOG_SELECTORS[section_type] == true
|
|
end
|
|
|
|
function DashboardSections.headerText(config)
|
|
if config and type(config.sourceName) == "string" and config.sourceName ~= "" then
|
|
return config.sourceName
|
|
end
|
|
return DashboardSections.label(config and config.type)
|
|
end
|
|
|
|
function DashboardSections.defaultConfig(index)
|
|
local config = (index and DashboardSections.DEFAULT_SLOTS[index]) or { type = DashboardSections.DEFAULT_TYPE }
|
|
return { type = config.type }
|
|
end
|
|
|
|
-- An unusable or obsolete entry degrades to Discover rather than being dropped.
|
|
function DashboardSections.normalizeEntry(value)
|
|
if type(value) ~= "table" or not DashboardSections.isValid(value.type) then
|
|
return DashboardSections.defaultConfig()
|
|
end
|
|
local entry = { type = value.type }
|
|
if DashboardSections.isCatalogSelector(value.type) then
|
|
if type(value.sourceName) ~= "string" or value.sourceName == "" or type(value.params) ~= "table" then
|
|
return DashboardSections.defaultConfig()
|
|
end
|
|
entry.sourceName = value.sourceName
|
|
entry.params = {}
|
|
for _, key in ipairs(PARAM_KEYS) do
|
|
if value.params[key] ~= nil then entry.params[key] = value.params[key] end
|
|
end
|
|
end
|
|
return entry
|
|
end
|
|
|
|
function DashboardSections.normalize(value)
|
|
local normalized = { schemaVersion = DashboardSections.SCHEMA_VERSION }
|
|
if type(value) ~= "table" or #value == 0 then
|
|
for index = 1, DashboardSections.SLOT_COUNT do
|
|
normalized[index] = DashboardSections.defaultConfig(index)
|
|
end
|
|
return normalized
|
|
end
|
|
|
|
-- Releases before the four-slot dashboard stored the one or two configurable
|
|
-- rows that sat below Continue reading, and wrote no schema marker at all.
|
|
-- An untagged list is therefore that older shape, not a short current one:
|
|
-- reading it positionally would drop Stats and duplicate Discover.
|
|
if value.schemaVersion == nil or value.schemaVersion == DashboardSections.LEGACY_SCHEMA_VERSION then
|
|
normalized[1] = DashboardSections.defaultConfig(1)
|
|
normalized[2] = DashboardSections.defaultConfig(2)
|
|
normalized[3] = DashboardSections.normalizeEntry(value[1])
|
|
normalized[4] = value[2] and DashboardSections.normalizeEntry(value[2]) or DashboardSections.defaultConfig(4)
|
|
return normalized
|
|
end
|
|
|
|
for index = 1, DashboardSections.SLOT_COUNT do
|
|
normalized[index] = value[index] and DashboardSections.normalizeEntry(value[index]) or DashboardSections.defaultConfig(index)
|
|
end
|
|
return normalized
|
|
end
|
|
|
|
function DashboardSections.at(settings, index)
|
|
index = index or 1
|
|
local stored = settings and settings[DashboardSections.SETTING_KEY]
|
|
local sections = DashboardSections.normalize(stored)
|
|
return sections[index] or DashboardSections.defaultConfig(index)
|
|
end
|
|
|
|
function DashboardSections.storeAt(settings, index, config)
|
|
index = math.max(1, math.min(DashboardSections.SLOT_COUNT, tonumber(index) or 1))
|
|
local stored = settings and settings[DashboardSections.SETTING_KEY]
|
|
local sections = DashboardSections.normalize(stored)
|
|
sections[index] = DashboardSections.normalizeEntry(config)
|
|
sections.schemaVersion = DashboardSections.SCHEMA_VERSION
|
|
return sections
|
|
end
|
|
|
|
-- Identifies which section a cached dashboard body was fetched for, so a body
|
|
-- cached under a different choice is not mistaken for the current one.
|
|
function DashboardSections.signature(config)
|
|
config = DashboardSections.normalizeEntry(config)
|
|
if DashboardSections.isCatalogSelector(config.type) then
|
|
local parts = { config.type }
|
|
for _, key in ipairs(PARAM_KEYS) do
|
|
if config.params[key] ~= nil then table.insert(parts, key .. "=" .. tostring(config.params[key])) end
|
|
end
|
|
return table.concat(parts, ":")
|
|
end
|
|
return config.type
|
|
end
|
|
|
|
function DashboardSections.settingsSignature(settings)
|
|
local parts = {}
|
|
for index = 1, DashboardSections.SLOT_COUNT do
|
|
parts[index] = DashboardSections.signature(DashboardSections.at(settings, index))
|
|
end
|
|
return table.concat(parts, "|")
|
|
end
|
|
|
|
return DashboardSections
|