fix(reader): enable D-pad dashboard setup

Expose each starter preset and the Start button as separate focus targets so non-touch KOReader devices can complete first-run setup.

Closes #1880
This commit is contained in:
neon
2026-10-21 19:05:02 +00:00
parent 9d1319ef8b
commit 9ba01498cf
2 changed files with 180 additions and 7 deletions
@@ -271,7 +271,13 @@ end
function CatalogBoard:boardRegion(build, action)
local widget, hits = BoardUI.collectHits(build)
local size = widget:getSize()
return BoardTile:new{ dimen = Geom:new{ w = size.w, h = size.h }, item = { action = action }, menu = self, hits = hits, widget }
return BoardTile:new{
dimen = Geom:new{ w = size.w, h = size.h },
item = { action = action },
menu = self,
hits = hits,
CatalogWidgets.focusable(widget),
}
end
-- ---------------------------------------------------------- edit overlays
@@ -517,7 +523,8 @@ function CatalogBoard:boardBuildStarter(ctx, g, screen_w, screen_h)
{ widget = inner },
{ widget = BoardUI.box({ w = card_w, h = size.h, radius = px(12), border = sel and px(2.5) or math.max(1, px(1)), border_col = sel and INK.black or INK.gb }) },
})
table.insert(cards, BoardUI.hit(card_box, { kind = "starter-pick", preset = info.id }))
table.insert(cards, self:boardRegion(function() return card_box end,
{ kind = "starter-pick", preset = info.id }))
end
local cards_row = hstack({ gap = gap, valign = "top" }, cards)
local strip_label_h = lh1(13) + px(8)
@@ -548,9 +555,11 @@ function CatalogBoard:boardBuildStarter(ctx, g, screen_w, screen_h)
end
local btn_label = text(T(_("Start with %1"), selected_name), { s = 16, b = true, col = INK.white })
local btn_w = btn_label:getSize().w + 2 * px(28)
local button = BoardUI.hit(BoardUI.box({ w = btn_w, h = btn_h, radius = math.floor(btn_h / 2), bg = INK.black },
hstack({ w = btn_w, h = btn_h }, { flex(0), btn_label, flex(0) })), { kind = "starter-start", preset = choice })
return vstack({ w = screen_w, h = screen_h, pad = { top, mx, top, mx } }, {
local button = self:boardRegion(function()
return BoardUI.box({ w = btn_w, h = btn_h, radius = math.floor(btn_h / 2), bg = INK.black },
hstack({ w = btn_w, h = btn_h }, { flex(0), btn_label, flex(0) }))
end, { kind = "starter-start", preset = choice })
local screen = vstack({ w = screen_w, h = screen_h, pad = { top, mx, top, mx } }, {
head,
space(nil, px(24)),
cards_row,
@@ -559,6 +568,7 @@ function CatalogBoard:boardBuildStarter(ctx, g, screen_w, screen_h)
strip and flex(px(16)) or false,
hstack({ w = w }, { flex(0), button, flex(0) }),
})
return screen, { cards, { button } }
end
-- ------------------------------------------------------------ the board
@@ -671,9 +681,8 @@ function CatalogBoard:updateDashboardItems(select_number, no_recalculate_dimen)
local screen, rows, packed
if self:boardShowStarter(state, context) then
screen = self:boardRegion(function() return self:boardBuildStarter(ctx, g, screen_w, screen_h) end)
screen, rows = self:boardBuildStarter(ctx, g, screen_w, screen_h)
self._board_tiles, self._board_header = {}, nil
rows = { { screen } }
else
BoardUI.clearSnapshots("starter:")
screen, rows, packed = self:boardBuildScreen(ctx, g, page, screen_w, screen_h)
@@ -0,0 +1,164 @@
-- Proves that the first-run dashboard chooser exposes each preset and the
-- Start button to KOReader's D-Pad focus layout on non-touch devices.
local function widget(width, height, children)
return {
width = width or 0,
height = height or 0,
children = children,
getSize = function(self) return { w = self.width, h = self.height } end,
}
end
local function class()
local base = {}
function base:getSize()
return { w = self.dimen and self.dimen.w or 0, h = self.dimen and self.dimen.h or 0 }
end
function base:extend(spec)
spec = spec or {}
spec.__index = spec
setmetatable(spec, { __index = self })
function spec:new(opts)
opts = opts or {}
setmetatable(opts, spec)
if opts.init then opts:init() end
return opts
end
return spec
end
return base
end
package.loaded["device"] = {
screen = {},
hasDPad = function() return true end,
isTouchDevice = function() return false end,
}
package.loaded["ui/event"] = { new = function(_, name) return { name = name } end }
package.loaded["ui/geometry"] = { new = function(_, opts) return opts end }
package.loaded["ui/gesturerange"] = { new = function(_, opts) return opts end }
package.loaded["ui/widget/container/inputcontainer"] = class()
package.loaded["ui/widget/verticalspan"] = class()
package.loaded["ui/network/manager"] = {}
package.loaded["ui/uimanager"] = {}
package.loaded["logger"] = {}
package.loaded["gettext"] = function(text) return text end
package.loaded["ffi/util"] = {
template = function(text, ...)
local values = { ... }
return (text:gsub("%%(%d+)", function(index) return tostring(values[tonumber(index)]) end))
end,
}
local function stack(horizontal, opts, children)
local width, height = 0, 0
for _, child in ipairs(children or {}) do
if child then
local size = child:getSize()
if horizontal then
width = width + size.w
height = math.max(height, size.h)
else
width = math.max(width, size.w)
height = height + size.h
end
end
end
return widget(opts.w or width, opts.h or height, children)
end
local BoardUI = {
text = function(value, opts) return widget((opts and opts.w) or #tostring(value) * 7, 18) end,
runs = function() return widget(100, 18) end,
vstack = function(opts, children) return stack(false, opts, children) end,
hstack = function(opts, children) return stack(true, opts, children) end,
flex = function(minimum) return widget(0, minimum or 0) end,
space = function(width, height) return widget(width or 0, height or 0) end,
icon = function(_, size) return widget(size, size) end,
overlay = function(width, height, children) return widget(width, height, children) end,
rect = function(width, height) return widget(width, height) end,
box = function(opts, child)
local size = child and child:getSize() or { w = 0, h = 0 }
return widget(opts.w or size.w, opts.h or size.h, { child })
end,
circle = function(size) return widget(size, size) end,
hit = function(value) return value end,
dot = function(size) return widget(size, size) end,
snapshot = function(_, _, _, width, height) return widget(width, height) end,
collectHits = function(build) return build(), {} end,
clearSnapshots = function() end,
statusCluster = function() return widget(40, 20) end,
}
package.loaded["bookorbit_board_ui"] = BoardUI
package.loaded["bookorbit_board_data"] = {}
package.loaded["bookorbit_board_edit"] = {}
package.loaded["bookorbit_board_layout"] = {}
package.loaded["bookorbit_board_store"] = {
SETTING_KEY = "catalog_board",
PAGE_COUNT = 4,
starterPages = function() return { {}, {}, {}, {} } end,
pageNames = function() return { "Home", "Insights", "Library", "Decor" } end,
withPreset = function(_, preset) return { preset = preset, started = true } end,
}
package.loaded["bookorbit_board_widgets"] = {}
local focusable_calls = 0
package.loaded["bookorbit_catalog_widgets"] = {
focusable = function(value)
focusable_calls = focusable_calls + 1
value.focusable = true
return value
end,
}
package.loaded["bookorbit_board_metrics"] = {
INK = { white = 1, black = 0, g3 = 3, g4 = 4, ge = 5, gb = 6 },
px = function(value) return value end,
lh1 = function(value) return value end,
lh = function(value) return value end,
get = function() return { W = 600 } end,
}
package.path = "koreader-plugin/bookorbit.koplugin/?.lua;" .. package.path
local CatalogBoard = require("bookorbit_catalog_board")
local function assertEqual(actual, expected, label)
if actual ~= expected then
error(string.format("%s: expected %s, got %s", label, tostring(expected), tostring(actual)))
end
end
local catalog = setmetatable({
board_starter_choice = "reader",
current_context = { board_page = 3 },
settings = {},
updates = 0,
refreshes = 0,
}, { __index = CatalogBoard })
function catalog:boardHasLight() return false end
function catalog:boardLandscape() return false end
function catalog:updateItems() self.updates = self.updates + 1 end
function catalog:persistSetting(key, value) self.settings[key] = value end
function catalog:boardRefreshIfMissing() self.refreshes = self.refreshes + 1 end
local screen, rows = catalog:boardBuildStarter({}, { mx = 20, cols = 4 }, 600, 800)
assertEqual(screen:getSize().w, 600, "the chooser keeps the full dashboard width")
assertEqual(#rows, 2, "the chooser exposes preset and Start rows")
assertEqual(#rows[1], 3, "all three presets share a focus row")
assertEqual(#rows[2], 1, "the Start button has its own focus row")
assertEqual(focusable_calls, 4, "every reachable control draws a D-Pad focus cursor")
rows[1][2]:onTapSelect()
assertEqual(catalog.board_starter_choice, "stats", "pressing the focused Stats preset selects it")
assertEqual(catalog.updates, 1, "selecting a preset redraws the chooser")
local _, updated_rows = catalog:boardBuildStarter({}, { mx = 20, cols = 4 }, 600, 800)
assertEqual(updated_rows[2][1].item.action.preset, "stats", "the redrawn Start button carries the selected preset")
updated_rows[2][1]:onTapSelect()
assertEqual(catalog.settings.catalog_board.preset, "stats", "pressing focused Start stores the selected preset")
assertEqual(catalog.settings.catalog_board.started, true, "pressing focused Start completes setup")
assertEqual(catalog.current_context.board_page, 1, "the started dashboard opens on its first page")
assertEqual(catalog.refreshes, 1, "the started dashboard replaces the chooser")
print("bookorbit_dashboard_starter_navigation_test.lua: ok")