Files
neon 267f53fccd feat(koreader): colour the widget board on colour screens
Add a Colour setting to Settings > Home: Accents, the default, colours each
widget's marks and draws Continue reading in its cover's deep shade; Tinted
also washes every tile in its widget's colour; Off keeps the greys. Black-and-
white screens draw as before, colour e-readers get stronger shades, and night
mode keeps each colour's hue.
2026-10-27 15:27:39 +00:00

474 lines
27 KiB
Lua

--[[--
Customizing the widget dashboard.
Hold any widget (or choose Customize dashboard in the menu) to edit. A tap
selects a widget and shows its bar: Earlier, Later, Size, Page and Remove.
There is no dragging, so editing works on e-ink and on D-pad devices alike.
Add widget opens a sheet of live previews.
The first edit turns the starter layout into the reader's own pages for this
column count. Every change is checked against the board's smallest row
height before it is kept, so a page can never hold more than fits.
]]
local T = require("ffi/util").template
local _ = require("gettext")
local BoardLayout = require("bookorbit_board_layout")
local BoardMetrics = require("bookorbit_board_metrics")
local BoardStore = require("bookorbit_board_store")
local px = BoardMetrics.px
local BoardEdit = {}
-- The widgets a reader can add. Sizes are the codes in BoardLayout.sizeDims;
-- the first is the size a new widget gets.
BoardEdit.LIBRARY = {
{ type = "forecast", name = function() return _("Finish forecast") end, desc = function() return _("When your book ends at your pace") end, sizes = { "M", "S", "L" }, tab = "reading", new = true },
{ type = "heatmap", name = function() return _("Year in reading") end, desc = function() return _("One square a day for a whole year") end, sizes = { "XL", "L" }, tab = "stats", new = true },
{ type = "goalpace", name = function() return _("Goal pace") end, desc = function() return _("Ahead or behind on your yearly goal") end, sizes = { "M", "S" }, tab = "stats", new = true },
{ type = "light", name = function() return _("Light") end, desc = function() return _("Brightness and warmth steppers") end, sizes = { "M", "S" }, tab = "device", new = true },
{ type = "word", name = function() return _("Word of the day") end, desc = function() return _("A word you looked up, in context") end, sizes = { "M", "L" }, tab = "reading", new = true },
{ type = "yearago", name = function() return _("A year ago") end, desc = function() return _("What you were reading last year") end, sizes = { "M", "L" }, tab = "reading", new = true },
{ type = "toggles", name = function() return _("Quick settings") end, desc = function() return _("Wi-Fi, light, night mode, rotate, sync") end, sizes = { "M", "S" }, tab = "device", new = true },
{ type = "records", name = function() return _("Reading records") end, desc = function() return _("Your longest and average sessions") end, sizes = { "L", "M" }, tab = "stats", new = true },
{ type = "hours", name = function() return _("When you read") end, desc = function() return _("Your reading by hour of the day") end, sizes = { "M", "L" }, tab = "stats", new = true },
{ type = "shelf", name = function() return _("Finished this year") end, desc = function() return _("Covers of the books you finished") end, sizes = { "L", "XL" }, tab = "stats", new = true, props = { source = "finished" } },
{ type = "shelf", name = function() return _("Pick back up") end, desc = function() return _("Books you put down a while ago") end, sizes = { "XL", "L" }, tab = "library", new = true, props = { source = "pick-back-up" } },
{ type = "shelf", name = function() return _("Want to read") end, desc = function() return _("Books waiting for their turn") end, sizes = { "L", "XL" }, tab = "library", new = true, props = { source = "want-to-read" } },
{ type = "hero", name = function() return _("Continue reading") end, desc = function() return _("Your current book with Resume") end, sizes = { "W", "M", "L" }, tab = "reading", chrome = "ink" },
{ type = "also", name = function() return _("Also reading") end, desc = function() return _("The rest of your pile") end, sizes = { "L", "M" }, tab = "reading" },
{ type = "highlight", name = function() return _("Highlight of the day") end, desc = function() return _("One of your highlights, daily") end, sizes = { "L", "M", "W" }, tab = "reading", chrome = "fill" },
{ type = "streak", name = function() return _("Streak") end, desc = function() return _("Days in a row, and your best") end, sizes = { "S", "M" }, tab = "stats" },
{ type = "week", name = function() return _("This week") end, desc = function() return _("Minutes read each day") end, sizes = { "M", "S" }, tab = "stats" },
{ type = "today", name = function() return _("Today") end, desc = function() return _("Minutes read today") end, sizes = { "S" }, tab = "stats" },
{ type = "calendar", name = function() return _("Reading calendar") end, desc = function() return _("This month, one square a day") end, sizes = { "M", "L" }, tab = "stats" },
{ type = "shelf", name = function() return _("Up next in series") end, desc = function() return _("The next book in each series you read") end, sizes = { "L", "XL" }, tab = "library", props = { source = "up-next-in-series" } },
{ type = "shelf", name = function() return _("Discover") end, desc = function() return _("A shuffle of your library") end, sizes = { "XL", "L" }, tab = "library", props = { source = "random", shuffle = true } },
{ type = "shelf", name = function() return _("Recently added") end, desc = function() return _("The newest books in your library") end, sizes = { "XL", "L" }, tab = "library", props = { source = "recently-added" } },
{ type = "shelf", name = function() return _("Shelf from your library") end, desc = function() return _("One library, collection, SmartScope, author or series") end, sizes = { "XL", "L" }, tab = "library", pick = true, props = { source = "random" } },
{ type = "link", name = function() return _("Library") end, desc = function() return _("All your books") end, sizes = { "S" }, tab = "library", props = { link = "library" } },
{ type = "link", name = function() return _("Series") end, desc = function() return _("Every series") end, sizes = { "S" }, tab = "library", props = { link = "series" } },
{ type = "link", name = function() return _("Authors") end, desc = function() return _("Every author") end, sizes = { "S" }, tab = "library", props = { link = "authors" } },
{ type = "link", name = function() return _("Collections") end, desc = function() return _("Your collections") end, sizes = { "S" }, tab = "library", props = { link = "collections" } },
{ type = "link", name = function() return _("SmartScopes") end, desc = function() return _("Your saved SmartScopes") end, sizes = { "S" }, tab = "library", props = { link = "scopes" } },
{ type = "link", name = function() return _("On device") end, desc = function() return _("Books on this device, and space left") end, sizes = { "S" }, tab = "device", props = { link = "device" } },
{ type = "link", name = function() return _("Search") end, desc = function() return _("Search your library") end, sizes = { "S" }, tab = "library", props = { link = "search" } },
{ type = "clock", name = function() return _("Clock") end, desc = function() return _("Time and date, each minute") end, sizes = { "S", "M" }, tab = "device", chrome = "ink" },
{ type = "sync", name = function() return _("Sync status") end, desc = function() return _("Last sync, with Sync now") end, sizes = { "S" }, tab = "device" },
{ type = "buddy", name = function() return _("Reading buddy") end, desc = function() return _("A cat that keeps you company") end, sizes = { "L", "M", "S" }, tab = "decor", new = true },
{ type = "buddy", name = function() return _("Reading dog") end, desc = function() return _("A floppy-eared friend for every chapter") end, sizes = { "L", "M", "S" }, tab = "decor", new = true, props = { animal = "dog" } },
{ type = "buddy", name = function() return _("Reading rabbit") end, desc = function() return _("A quiet companion with a curious nose") end, sizes = { "L", "M", "S" }, tab = "decor", new = true, props = { animal = "rabbit" } },
{ type = "buddy", name = function() return _("Reading owl") end, desc = function() return _("A feathered friend for one more page") end, sizes = { "L", "M", "S" }, tab = "decor", new = true, props = { animal = "owl" } },
{ type = "buddy", name = function() return _("Reading penguin") end, desc = function() return _("A little bookworm in a feathered coat") end, sizes = { "L", "M", "S" }, tab = "decor", new = true, props = { animal = "penguin" } },
{ type = "buddy", name = function() return _("Reading robot") end, desc = function() return _("Recharges with a good book") end, sizes = { "L", "M", "S" }, tab = "decor", new = true, props = { animal = "robot" } },
{ type = "buddy", name = function() return _("Reading vampire") end, desc = function() return _("A tiny night owl with a cape and fangs") end, sizes = { "L", "M", "S" }, tab = "decor", new = true, props = { animal = "vampire" } },
{ type = "buddy", name = function() return _("Reading dragon") end, desc = function() return _("A little dragon guarding a hoard of stories") end, sizes = { "L", "M", "S" }, tab = "decor", new = true, props = { animal = "dragon" } },
{ type = "buddy", name = function() return _("Reading ghost") end, desc = function() return _("A friendly spirit between the pages") end, sizes = { "L", "M", "S" }, tab = "decor", new = true, props = { animal = "ghost" } },
{ type = "buddy", name = function() return _("Reading mimic") end, desc = function() return _("A book that reads you back") end, sizes = { "L", "M", "S" }, tab = "decor", new = true, props = { animal = "mimic" } },
{ type = "buddy", name = function() return _("Reading mummy") end, desc = function() return _("Three thousand years of catching up") end, sizes = { "L", "M", "S" }, tab = "decor", new = true, props = { animal = "mummy" } },
{ type = "buddy", name = function() return _("Reading witch") end, desc = function() return _("Never without her spellbook") end, sizes = { "L", "M", "S" }, tab = "decor", new = true, props = { animal = "witch" } },
{ type = "buddy", name = function() return _("Reading skeleton") end, desc = function() return _("Reads to the bone") end, sizes = { "L", "M", "S" }, tab = "decor", new = true, props = { animal = "skeleton" } },
{ type = "buddy", name = function() return _("Reading candle") end, desc = function() return _("Burns the midnight oil") end, sizes = { "L", "M", "S" }, tab = "decor", new = true, props = { animal = "candle" } },
{ type = "buddy", name = function() return _("Reading alien") end, desc = function() return _("Came a long way for a good book") end, sizes = { "L", "M", "S" }, tab = "decor", new = true, props = { animal = "alien" } },
{ type = "chapters", name = function() return _("Chapter line") end, desc = function() return _("Your book as a line, chapters as stops") end, sizes = { "L", "M", "W", "S" }, tab = "decor", new = true },
{ type = "badge", name = function() return _("Next badge") end, desc = function() return _("The badge you are closest to earning") end, sizes = { "M", "L", "S" }, tab = "decor", new = true },
{ type = "dust", name = function() return _("Gathering dust") end, desc = function() return _("Books you downloaded and never opened") end, sizes = { "M", "L", "S" }, tab = "decor", new = true },
{ type = "rings", name = function() return _("Tree rings") end, desc = function() return _("A ring for every month you read") end, sizes = { "L", "M" }, tab = "decor", new = true },
{ type = "odometer", name = function() return _("Pages turned") end, desc = function() return _("Every page, on a counter") end, sizes = { "M", "S" }, tab = "decor", new = true },
{ type = "glyph", name = function() return _("Glyph of the day") end, desc = function() return _("One letter from your fonts") end, sizes = { "S", "M", "L" }, tab = "decor", new = true },
{ type = "analog", name = function() return _("Analog clock") end, desc = function() return _("A clock face, each minute") end, sizes = { "S", "M" }, tab = "decor", new = true, chrome = "ink" },
{ type = "moon", name = function() return _("Moon phase") end, desc = function() return _("Tonight's moon") end, sizes = { "S", "M" }, tab = "decor", new = true, chrome = "ink" },
}
BoardEdit.TABS = {
{ id = "new", label = function() return _("New") end },
{ id = "reading", label = function() return _("Reading") end },
{ id = "stats", label = function() return _("Stats") end },
{ id = "library", label = function() return _("Library") end },
{ id = "device", label = function() return _("Device") end },
{ id = "decor", label = function() return _("Decor") end },
}
local function sameProps(a, b)
a, b = a or {}, b or {}
return a.source == b.source and a.link == b.link and (a.animal or "cat") == (b.animal or "cat")
end
-- The library entry an item came from, for its supported sizes.
function BoardEdit.entryFor(item)
local picked = item.type == "shelf" and item.props
and require("bookorbit_dashboard_sections").isCatalogSelector(item.props.source)
for _i, entry in ipairs(BoardEdit.LIBRARY) do
if picked and entry.pick then return entry end
if entry.type == item.type and (entry.type ~= "shelf" and entry.type ~= "link" and entry.type ~= "buddy" or sameProps(entry.props, item.props)) then
return entry
end
end
return nil
end
function BoardEdit.sizeCode(item, cols)
for _i, code in ipairs({ "S", "M", "L", "W", "XL" }) do
local cw, rh = BoardLayout.sizeDims(code, cols)
if cw == math.min(item.cw, cols) and rh == item.rh then return code end
end
return nil
end
local function copyItem(it)
local props = nil
if it.props then
props = {}
for k, v in pairs(it.props) do props[k] = v end
end
return { type = it.type, cw = it.cw, rh = it.rh, chrome = it.chrome, props = props }
end
-- ------------------------------------------------------------- state
function BoardEdit:boardEditing()
return self.board_edit ~= nil
end
function BoardEdit:boardGeometry()
local m = BoardMetrics.get()
local _top, bottom = self:menuChromeHeight()
return BoardLayout.geometry(px, self.inner_dimen.w, self.inner_dimen.h - bottom, m.LW)
end
-- The pages being edited, as they are currently drawn: the reader's own or
-- the starter layout, fitted to this screen. A saved page that no longer fits
-- (a larger interface size, a shelf grown to two rows) is edited as shown,
-- never as an overfull grid the editor could not change; the widgets it has
-- no room for are kept aside as hidden.
function BoardEdit:boardEditablePages(g)
local pages = self:boardPages(g.cols)
local out = {}
for index = 1, BoardStore.PAGE_COUNT do
local page = pages[index] or { base = {} }
local items, hidden = {}, {}
local packed = BoardLayout.fitPage(g, page.base, page.extras)
for _i, it in ipairs(packed.placed) do table.insert(items, copyItem(it)) end
for _i, it in ipairs(packed.dropped) do table.insert(hidden, copyItem(it)) end
out[index] = { id = page.id, items = items, hidden = hidden }
end
return out
end
-- Hidden widgets are saved after the ones on screen, so a smaller interface
-- size shows them again.
function BoardEdit:boardSavePages(g, pages)
local m = BoardMetrics.get()
local saved = {}
for index, page in ipairs(pages) do
local items = {}
for _i, it in ipairs(page.items or {}) do table.insert(items, it) end
for _i, it in ipairs(page.hidden or {}) do table.insert(items, it) end
saved[index] = { id = page.id, items = items }
end
self:persistSetting(BoardStore.SETTING_KEY, BoardStore.withCustomPages(self.settings, g.cols, saved, m.w > m.h))
end
function BoardEdit:boardPageFits(g, items)
if #items == 0 then return true end
local packed = BoardLayout.pack(items, g.cols)
return BoardLayout.rowHeight(g, packed.rows) >= g.min_row
end
function BoardEdit:boardEnterEdit(select_index)
if not self:dashboardMode() then return end
local g = self:boardGeometry()
self.board_edit = {
pages = self:boardEditablePages(g),
selected = select_index,
sheet = false,
sheet_tab = "new",
sheet_page = 1,
cols = g.cols,
}
self:updateItems(nil, true)
end
function BoardEdit:boardExitEdit()
local edit = self.board_edit
if not edit then return end
self.board_edit = nil
if edit.changed then
self:boardSavePages(self:boardGeometry(), edit.pages)
end
self:boardRefreshIfMissing()
end
-- Redraws the board, then fetches the dashboard again only when it now shows
-- a shelf or a list of names the cached body does not hold. Drawing first
-- keeps the screen current while offline; covers stay cached either way.
function BoardEdit:boardRefreshIfMissing()
self:updateItems(nil, true)
local body = self.current_context and self.current_context.dashboard
local sources, names = self:boardSources()
local missing = body == nil
for source in pairs(sources) do
if source ~= "in-progress" and not (body and type(body.boardShelves) == "table" and body.boardShelves[source]) then missing = true end
end
for key in pairs(names) do
if not (body and type(body.boardNames) == "table" and body.boardNames[key]) then missing = true end
end
if missing then
self:loadDashboardRoot(true, { invisible = true })
end
end
local function full(text)
require("bookorbit_toast").error(text)
end
-- ------------------------------------------------------------- actions
function BoardEdit:boardEditRun(action)
local edit = self.board_edit
if not edit then return end
local g = self:boardGeometry()
local page = self:boardPage()
local items = edit.pages[page].items
local kind = action.kind
if kind == "edit-done" then
return self:boardExitEdit()
elseif kind == "edit-select" then
edit.selected = edit.selected == action.index and nil or action.index
elseif kind == "edit-move" then
local i = edit.selected
local j = i and i + action.delta
if i and j and j >= 1 and j <= #items then
items[i], items[j] = items[j], items[i]
edit.selected = j
edit.changed = true
end
elseif kind == "edit-size" then
local it = items[edit.selected or 0]
if it then
local cw, rh = BoardLayout.sizeDims(action.code, g.cols)
local old_cw, old_rh = it.cw, it.rh
it.cw, it.rh = cw, rh
if not self:boardPageFits(g, items) then
it.cw, it.rh = old_cw, old_rh
return full(_("There is not enough room on this page for that size."))
end
edit.changed = true
end
elseif kind == "edit-remove" then
local i = action.index or edit.selected
if i and items[i] then
table.remove(items, i)
edit.selected = nil
edit.changed = true
end
elseif kind == "edit-page" then
local i = edit.selected
if i and items[i] then
local target = page % BoardStore.PAGE_COUNT + 1
local moved = items[i]
local dest = edit.pages[target].items
table.insert(dest, moved)
if not self:boardPageFits(g, dest) then
table.remove(dest)
return full(T(_("%1 is full. Remove a widget there first."), BoardStore.pageNames(edit.pages)[target]))
end
table.remove(items, i)
edit.selected = nil
edit.changed = true
full(T(_("Moved to %1."), BoardStore.pageNames(edit.pages)[target]))
end
elseif kind == "edit-add" then
edit.sheet = true
edit.sheet_page = 1
elseif kind == "sheet-close" then
edit.sheet = false
elseif kind == "sheet-tab" then
edit.sheet_tab = action.tab
edit.sheet_page = 1
elseif kind == "sheet-page" then
edit.sheet_page = math.max(1, (edit.sheet_page or 1) + action.delta)
elseif kind == "sheet-add" and action.entry.pick then
edit.sheet = false
self:updateItems(nil, true)
return self:boardPickShelfSource()
elseif kind == "sheet-add" then
local entry = action.entry
local cw, rh = BoardLayout.sizeDims(entry.sizes[1], g.cols)
local item = { type = entry.type, cw = cw, rh = rh, chrome = entry.chrome or "line", props = entry.props and copyItem(entry).props or nil }
table.insert(items, item)
if not self:boardPageFits(g, items) then
-- Try the smallest size it offers before giving up.
local smallest = entry.sizes[#entry.sizes]
item.cw, item.rh = BoardLayout.sizeDims(smallest, g.cols)
for _i, code in ipairs(entry.sizes) do
local tcw, trh = BoardLayout.sizeDims(code, g.cols)
if tcw * trh < item.cw * item.rh then item.cw, item.rh = tcw, trh end
end
if not self:boardPageFits(g, items) then
table.remove(items)
return full(_("This page is full. Remove a widget first, or add it on another page."))
end
end
edit.sheet = false
edit.selected = #items
edit.changed = true
elseif kind == "edit-pages" then
return self:boardShowPagesMenu()
end
self:updateItems(nil, true)
end
-- Which list to pick a shelf from. The list opens in pick mode: tapping an
-- entry adds it as a shelf instead of opening it.
function BoardEdit:boardPickShelfSource()
require("bookorbit_dialog_ui").actions{
title = _("Show a shelf of"),
sub = _("Pick where its books come from"),
rows = {
{ icon = "library", label = _("A library, collection or SmartScope"), opens = true, run = function()
self:loadShelves({ dashboard_source_index = "board" })
end },
{ icon = "user", label = _("An author"), opens = true, run = function()
self:loadSection("authors", { dashboard_source_index = "board" })
end },
{ icon = "layers-2", label = _("A series"), opens = true, run = function()
self:loadSection("series", { dashboard_source_index = "board" })
end },
},
}
end
-- The picked list becomes a full-width shelf on the page being edited.
function BoardEdit:boardAddPickedShelf(source)
self:onReturn()
while self.stack and #self.stack > 0 and not self:dashboardMode() do self:onReturn() end
local edit = self.board_edit
if not edit or not self:dashboardMode() then return end
local g = self:boardGeometry()
local items = edit.pages[self:boardPage()].items
local params = {}
for k, v in pairs(source.params or {}) do params[k] = v end
table.insert(items, { type = "shelf", cw = g.cols, rh = 2, chrome = "line",
props = { source = source.type, params = params, title = source.sourceName } })
if not self:boardPageFits(g, items) then
table.remove(items)
self:updateItems(nil, true)
return full(_("This page is full. Remove a widget first, or add it on another page."))
end
edit.selected = #items
edit.changed = true
-- The new shelf's books are fetched now, not when editing ends.
self:boardRefreshIfMissing()
end
function BoardEdit:boardShowPagesMenu()
local DialogUI = require("bookorbit_dialog_ui")
DialogUI.actions{
title = _("Pages"),
rows = {
{ icon = "rotate-ccw", label = _("Reset this page"), sub = _("Back to how it started"), run = function()
self:boardResetPage(self:boardPage())
end },
{ icon = "rotate-ccw", label = _("Reset all pages"), sub = _("Every page back to how it started"), run = function()
DialogUI.confirm{
title = _("Reset all pages?"),
text = _("Every page goes back to its starting layout."),
ok_label = _("Reset"),
on_ok = function() self:boardResetAll() end,
}
end },
},
}
end
function BoardEdit:boardResetPage(page)
local edit = self.board_edit
if not edit then return end
local g = self:boardGeometry()
local state = BoardStore.read(self.settings)
local m = BoardMetrics.get()
local starter = BoardStore.starterPages(g.cols, state.preset, self:boardHasLight(), m.w > m.h)[page]
local packed = BoardLayout.fitPage(g, starter.base, starter.extras)
local items = {}
for _i, it in ipairs(packed.placed) do table.insert(items, copyItem(it)) end
edit.pages[page].items = items
edit.selected = nil
edit.changed = true
self:updateItems(nil, true)
end
function BoardEdit:boardResetAll()
local state = BoardStore.read(self.settings)
self:persistSetting(BoardStore.SETTING_KEY, BoardStore.withPreset(self.settings, state.preset))
self.board_edit = nil
self:boardRefreshIfMissing()
end
-- Shelves a reader picked for the old four-slot dashboard (one library,
-- collection, SmartScope, author or series) carry over onto the board once it
-- has started, on the first page with room for them. The old setting is then
-- dropped, so this runs once. Returns true when it ran.
function BoardEdit:boardMigrateLegacyShelves()
local DashboardSections = require("bookorbit_dashboard_sections")
local stored = self.settings[DashboardSections.SETTING_KEY]
if stored == nil or not BoardStore.read(self.settings).started then return false end
local legacy = DashboardSections.normalize(stored)
local g = self:boardGeometry()
local shelves = {}
for index = 1, DashboardSections.SLOT_COUNT do
local entry = legacy[index]
if entry and DashboardSections.isCatalogSelector(entry.type) and type(entry.params) == "table" then
table.insert(shelves, { type = "shelf", cw = g.cols, rh = 2, chrome = "line",
props = { source = entry.type, params = entry.params, title = entry.sourceName } })
end
end
if #shelves > 0 then
local pages = self:boardEditablePages(g)
for _i, shelf in ipairs(shelves) do
local placed = false
for _j, page in ipairs({ 1, 3, 2 }) do
local items = pages[page].items
table.insert(items, shelf)
if self:boardPageFits(g, items) then
placed = true
break
end
table.remove(items)
end
-- The reader chose this shelf; the starter's last widgets on the
-- Library page give way to it.
if not placed then
local items = pages[3].items
table.insert(items, 1, shelf)
while #items > 1 and not self:boardPageFits(g, items) do table.remove(items) end
end
end
self:boardSavePages(g, pages)
end
self:persistSetting(DashboardSections.SETTING_KEY, nil)
return true
end
-- ------------------------------------------------------------- settings
function BoardEdit:boardSetTiles(tiles)
self:persistSetting(BoardStore.SETTING_KEY, BoardStore.withTiles(self.settings, tiles))
if self:dashboardMode() then self:updateItems(nil, true) end
end
function BoardEdit:boardSetColour(colour)
self:persistSetting(BoardStore.SETTING_KEY, BoardStore.withColour(self.settings, colour))
require("bookorbit_board_ui").clearSnapshots()
if self:dashboardMode() then self:updateItems(nil, true) end
end
function BoardEdit.install(Catalog)
for name, fn in pairs(BoardEdit) do
if name ~= "install" and type(fn) == "function" and name:match("^board") then
Catalog[name] = fn
end
end
end
return BoardEdit