Files
neon 99aa7dce70 fix(reader): preserve third-party KOReader menu entries
Extend KOReader's cached menu order instead of persisting complete
override lists. Remove marker-owned legacy overrides during upgrade
while preserving hand-maintained entries.

Closes #1878
2026-10-21 19:51:36 +00:00

195 lines
6.0 KiB
Lua

--[[--
Pins the BookOrbit menu entry onto the first page of the Tools menu, right
below calibre, and puts BookOrbit's icon in KOReader's menu bar just before
the main menu.
KOReader caches its bundled menu-order tables and lets plugins extend them
before the menu is built. Mutating those shared tables preserves entries added
by every plugin regardless of load order. Older BookOrbit releases instead
wrote complete user override lists, which hid entries added by later plugins;
those marker-owned files are removed during migration. A hand-maintained file
keeps its other entries and only has BookOrbit ids normalized.
]]
local DataStorage = require("datastorage")
local dump = require("dump")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local util = require("util")
local MARKER = "-- bookorbit-menu-pin v3 (generated; delete this file to reset menu order)"
local OLD_MARKERS = {
"-- bookorbit-sync-pin v1 (generated; delete this file to reset menu order)",
"-- bookorbit-menu-pin v2 (generated; delete this file to reset menu order)",
}
local TAB_ID = "bookorbit_tab"
local BUTTONS = "KOMenu:menu_buttons"
local MENU_ID = "bookorbit"
local OLD_MENU_IDS = {
"bookorbit_browse",
"bookorbit_sync",
}
local PREFIXES = {
{ prefix = "reader", order_module = "ui/elements/reader_menu_order" },
{ prefix = "filemanager", order_module = "ui/elements/filemanager_menu_order" },
}
local EMPTY_OVERRIDE = MARKER .. "\nreturn {}\n"
local BookOrbitMenuPin = {
done = false,
-- Whether BookOrbit's icon belongs in the menu bar; set from the
-- plugin's setting before ensure or refresh runs.
tab = true,
}
local function removeId(tools, menu_id)
local removed = false
for i = #tools, 1, -1 do
if tools[i] == menu_id then
table.remove(tools, i)
removed = true
end
end
return removed
end
local function removeOldIds(tools)
local removed = false
for _, menu_id in ipairs(OLD_MENU_IDS) do
removed = removeId(tools, menu_id) or removed
end
return removed
end
local function normalizeBookOrbitIds(tools)
removeOldIds(tools)
removeId(tools, MENU_ID)
local insert_at = #tools
for i, id in ipairs(tools) do
if id == "calibre" then
insert_at = i
break
end
end
table.insert(tools, insert_at + 1, MENU_ID)
end
-- The menu bar with BookOrbit's tab just before the main menu (the last
-- button), or without it.
local function normalizeButtons(buttons, with_tab)
removeId(buttons, TAB_ID)
if not with_tab then return end
local insert_at = #buttons + 1
for i, id in ipairs(buttons) do
if id == "main" then
insert_at = i
break
end
end
table.insert(buttons, insert_at, TAB_ID)
end
BookOrbitMenuPin.normalizeButtons = normalizeButtons
local function normalizeRuntimeOrder(order)
if type(order) ~= "table" or type(order.tools) ~= "table" then return end
normalizeBookOrbitIds(order.tools)
if type(order[BUTTONS]) == "table" then
normalizeButtons(order[BUTTONS], BookOrbitMenuPin.tab)
-- MenuSorter treats a button as a tab only when it has an order list.
order[TAB_ID] = BookOrbitMenuPin.tab and {} or nil
end
end
local function isGeneratedByUs(existing)
if existing:sub(1, #MARKER) == MARKER then
return true
end
for _, marker in ipairs(OLD_MARKERS) do
if existing:sub(1, #marker) == marker then
return true
end
end
return false
end
local function migrateOverride(prefix)
local path = DataStorage:getSettingsDir() .. "/" .. prefix .. "_menu_order.lua"
if not lfs.attributes(path) then return end
local existing = util.readFromFile(path, "r")
if not existing then
logger.warn("BookOrbit: cannot read menu order file, leaving it alone:", path)
return
end
if isGeneratedByUs(existing) then
local ok, err = os.remove(path)
if not ok then
local cleared, clear_err = util.writeToFile(EMPTY_OVERRIDE, path, true)
if not cleared then
logger.warn("BookOrbit: cannot clear generated menu order file", path, err, clear_err)
end
end
return
end
-- Hand-maintained file: replace stale BookOrbit ids without touching other entries.
local parsed, user = pcall(dofile, path)
if not parsed or type(user) ~= "table" then
logger.warn("BookOrbit: cannot parse user menu order file, leaving it alone:", path)
return
end
if type(user.tools) == "table" then
normalizeBookOrbitIds(user.tools)
end
if type(user[BUTTONS]) == "table" then
normalizeButtons(user[BUTTONS], BookOrbitMenuPin.tab)
user[TAB_ID] = BookOrbitMenuPin.tab and {} or nil
else
user[TAB_ID] = nil
end
local ok, err = util.writeToFile(dump(user, nil, true), path, true, true)
if not ok then
logger.warn("BookOrbit: cannot update user menu order file", path, err)
end
end
local function ensureOne(prefix, order_module)
local ok, order = pcall(require, order_module)
if not ok then
logger.warn("BookOrbit: cannot load menu order", order_module, order)
return
end
normalizeRuntimeOrder(order)
migrateOverride(prefix)
end
local function ensureAll()
for _, entry in ipairs(PREFIXES) do
local ok, err = pcall(ensureOne, entry.prefix, entry.order_module)
if not ok then
logger.warn("BookOrbit: menu pin failed for", entry.prefix, err)
end
end
end
function BookOrbitMenuPin.ensure(with_tab)
if BookOrbitMenuPin.done then return end
BookOrbitMenuPin.done = true
if with_tab ~= nil then BookOrbitMenuPin.tab = with_tab end
ensureAll()
end
-- Updates both cached orders after the menu bar setting changes.
function BookOrbitMenuPin.refresh(with_tab)
BookOrbitMenuPin.tab = with_tab ~= false
BookOrbitMenuPin.done = true
ensureAll()
end
return BookOrbitMenuPin