From 9ba01498cf911bca46e2e6e3f49bc3596c787f46 Mon Sep 17 00:00:00 2001 From: neon <18559658+neonsolstice@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:05:02 +0000 Subject: [PATCH] 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 --- .../bookorbit_catalog_board.lua | 23 ++- ...rbit_dashboard_starter_navigation_test.lua | 164 ++++++++++++++++++ 2 files changed, 180 insertions(+), 7 deletions(-) create mode 100644 koreader-plugin/spec/bookorbit_dashboard_starter_navigation_test.lua diff --git a/koreader-plugin/bookorbit.koplugin/bookorbit_catalog_board.lua b/koreader-plugin/bookorbit.koplugin/bookorbit_catalog_board.lua index 930392b86..df53ce834 100644 --- a/koreader-plugin/bookorbit.koplugin/bookorbit_catalog_board.lua +++ b/koreader-plugin/bookorbit.koplugin/bookorbit_catalog_board.lua @@ -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) diff --git a/koreader-plugin/spec/bookorbit_dashboard_starter_navigation_test.lua b/koreader-plugin/spec/bookorbit_dashboard_starter_navigation_test.lua new file mode 100644 index 000000000..7702fd081 --- /dev/null +++ b/koreader-plugin/spec/bookorbit_dashboard_starter_navigation_test.lua @@ -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")