Files
bookorbit/koreader-plugin/bookorbit.koplugin/bookorbit_main_menu.lua
T
neon f39e0e8ab5 fix(reader): harden KOReader sync and credential refresh
Sanitize match-check batches before strict server validation and classify HTTP failures accurately. Rebind the active catalog client whenever authentication settings change so retries use current credentials.

Fixes #1150
2026-08-28 15:29:36 +00:00

850 lines
31 KiB
Lua

--[[--
Main menu mixin for the BookOrbit plugin.
Builds the BookOrbit entry in KOReader's Tools menu and the dashboard menu that
mirrors it, plus the account dialogs: server address, login/logout. Installed
onto the plugin controller as regular methods.
]]
local Device = require("device")
local InfoMessage = require("ui/widget/infomessage")
local InputDialog = require("ui/widget/inputdialog")
local MultiInputDialog = require("ui/widget/multiinputdialog")
local NetworkMgr = require("ui/network/manager")
local Notification = require("ui/widget/notification")
local UIManager = require("ui/uimanager")
local md5 = require("ffi/sha2").md5
local util = require("util")
local T = require("ffi/util").template
local _ = require("gettext")
local BookOrbitApi = require("bookorbit_api")
local BookOrbitCapabilities = require("bookorbit_capabilities")
local BookOrbitHighlightDiagnostics = require("bookorbit_highlight_diagnostics")
local BookOrbitSweep = require("bookorbit_sweep")
local DashboardSections = require("bookorbit_dashboard_sections")
-- Assigned from the plugin class on install so menu labels can name the
-- shared sync strategies.
local SYNC_STRATEGY
local MainMenu = {}
local function getNameStrategy(strategy)
if strategy == SYNC_STRATEGY.PROMPT then
return _("Prompt")
elseif strategy == SYNC_STRATEGY.SILENT then
return _("Silently")
else
return _("Never")
end
end
function MainMenu:strategyMenu(getter, setter)
local function item(text, value)
return {
text = text,
checked_func = function() return getter() == value end,
callback = function() setter(value) end,
}
end
return {
item(_("Silently"), SYNC_STRATEGY.SILENT),
item(_("Prompt"), SYNC_STRATEGY.PROMPT),
item(_("Never"), SYNC_STRATEGY.DISABLE),
}
end
function MainMenu:catalogAutoOpenLabel()
local mode = self.settings.catalog_auto_open or "off"
if mode == "filemanager" then
return _("File manager only")
elseif mode == "always" then
return _("Every startup")
end
return _("Off")
end
function MainMenu:catalogAutoOpenMenu()
local function item(text, value)
return {
text = text,
checked_func = function() return (self.settings.catalog_auto_open or "off") == value end,
callback = function()
self.settings.catalog_auto_open = value
G_reader_settings:flush()
end,
}
end
return {
item(_("Off"), "off"),
item(_("File manager only"), "filemanager"),
item(_("Every startup"), "always"),
}
end
local function formatBool(value)
return value and _("On") or _("Off")
end
local function formatTime(ts)
ts = tonumber(ts)
if not ts or ts <= 0 then return _("never") end
return os.date("%Y-%m-%d %H:%M", ts)
end
local function formatShortTime(ts)
ts = tonumber(ts)
if not ts or ts <= 0 then return _("never") end
return os.date("%H:%M", ts)
end
local function deviceIdPrefix(device_id)
if type(device_id) ~= "string" or device_id == "" then return _("unknown") end
return device_id:sub(1, 8)
end
local function versionText(version)
if type(version) ~= "string" or version == "" then return _("unknown") end
return "v" .. version:gsub("^v", "")
end
local function recordText(entry)
if type(entry) ~= "table" then return _("none") end
local when = formatTime(entry.at)
local message = entry.message or entry.event or _("unknown")
return T(_("%1 at %2"), tostring(message), when)
end
local function highlightRecordText(entry)
return BookOrbitHighlightDiagnostics.lastSyncText(entry, formatShortTime)
end
local function jobText(job)
if type(job) ~= "table" then return _("none") end
return tostring(job.label or job.family or _("unknown"))
end
local function jobStartedText(job)
if type(job) ~= "table" then return _("none") end
return formatTime(job.started_at)
end
local function safeDocumentDigest(plugin, has_open_book)
if not has_open_book or not plugin.getDocumentDigest then return nil end
local ok, digest = pcall(plugin.getDocumentDigest, plugin)
if ok then return digest end
end
local function hasReaderBook(plugin)
return plugin.ui and plugin.ui.document ~= nil
end
local function hasCredentials(settings)
return settings.server_url ~= nil and settings.username ~= nil and settings.userkey ~= nil
end
function MainMenu:diagnosticsRows()
local status = BookOrbitSweep.syncStatus()
local sync_status = self.getSyncCoordinatorStatus and self:getSyncCoordinatorStatus() or { pending_count = 0 }
local outbox_status = self.getLifecycleOutboxStatus and self:getLifecycleOutboxStatus()
or { count = 0, bytes = 0, blocked = 0 }
local has_open_book = hasReaderBook(self)
local digest = safeDocumentDigest(self, has_open_book)
local scheduler = self.open_annotation_scheduler and self.open_annotation_scheduler:status() or nil
local logged_in = self:isLoggedIn()
local open_highlight_text = BookOrbitHighlightDiagnostics.openHighlightsText{
annotation_sync = self.settings.annotation_sync,
has_open_book = has_open_book,
matched = self.isOpenBookMatched and self:isOpenBookMatched(digest) or false,
scheduler_status = scheduler,
last_highlight_sync = self.settings.last_highlight_sync,
}
local rows = {
{ _("Server"), self.settings.server_url or _("not set") },
{ _("Username"), self.settings.username or _("not set") },
{ _("Login"), self:isLoggedIn() and _("configured") or _("not configured") },
{ _("Device"), Device.model or _("unknown") },
{ _("Device ID"), deviceIdPrefix(self.device_id) },
{ _("Plugin"), versionText(self.PLUGIN_VERSION) },
{ _("Latest plugin"), versionText(self.settings.update_latest_version) },
}
if not logged_in then
if self.settings.last_error then
table.insert(rows, "----------------------------")
table.insert(rows, { _("Last error"), recordText(self.settings.last_error) })
end
table.insert(rows, "----------------------------")
if hasCredentials(self.settings) then
table.insert(rows, {
_("Test connection"),
_("Run"),
callback = function()
self:testConnection()
end,
})
end
return rows
end
table.insert(rows, "----------------------------")
table.insert(rows, { _("Auto sync"), formatBool(self.settings.auto_sync) })
table.insert(rows, { _("Two-way highlights & bookmarks"), formatBool(self.settings.annotation_sync) })
table.insert(rows, { _("Open highlights"), open_highlight_text })
table.insert(rows, { _("Highlight apply retry"), BookOrbitHighlightDiagnostics.retryText(self.open_highlight_retry_status) })
table.insert(rows, { _("Skip offline auto-sync"), formatBool(self.settings.skip_sync_when_offline) })
table.insert(rows, { _("Current sync"), jobText(sync_status.current) })
table.insert(rows, { _("Current sync started"), jobStartedText(sync_status.current) })
table.insert(rows, { _("Pending syncs"), tostring(sync_status.pending_count or 0) })
table.insert(rows, { _("Pending lifecycle syncs"), tostring(outbox_status.count or 0) })
table.insert(rows, { _("Parked lifecycle syncs"), tostring(outbox_status.blocked or 0) })
table.insert(rows, { _("Lifecycle sync storage"), tostring(outbox_status.bytes or 0) .. " B" })
table.insert(rows, { _("Next sync"), jobText(sync_status.next) })
table.insert(rows, { _("Last sweep"), formatTime(status.lastSweepAt) })
table.insert(rows, { _("Matched local books"), tostring(status.matched or 0) })
table.insert(rows, { _("Unmatched local books"), tostring(status.unmatched or 0) })
table.insert(rows, "----------------------------")
table.insert(rows, { _("Last sync"), recordText(self.settings.last_sync) })
table.insert(rows, { _("Last highlight sync"), highlightRecordText(self.settings.last_highlight_sync) })
table.insert(rows, { _("Last bookmark sync"),
BookOrbitHighlightDiagnostics.lastBookmarkSyncText(self.settings.last_highlight_sync, formatShortTime) })
table.insert(rows, { _("Last error"), recordText(self.settings.last_error) })
table.insert(rows, "----------------------------")
if has_open_book then
if self.settings.annotation_sync then
table.insert(rows, {
_("Retry highlight sync"),
_("Run"),
callback = function()
self:retryOpenHighlightSync()
end,
})
end
table.insert(rows, {
_("Retry open-book match"),
_("Run"),
callback = function()
self:retryOpenBookMatch()
end,
})
end
table.insert(rows, {
_("Recheck all book matches"),
_("Run"),
callback = function()
self:startMatchRecheck()
end,
})
table.insert(rows, {
_("Test connection"),
_("Run"),
callback = function()
self:testConnection()
end,
})
table.insert(rows, {
_("Check for update"),
_("Run"),
callback = function()
self:checkForUpdate()
end,
})
return rows
end
function MainMenu:showDiagnostics()
local KeyValuePage = require("ui/widget/keyvaluepage")
UIManager:show(KeyValuePage:new{
title = _("BookOrbit diagnostics"),
kv_pairs = self:diagnosticsRows(),
value_overflow_align = "right",
})
end
function MainMenu:testConnection()
if not self.settings.server_url then
UIManager:show(InfoMessage:new{ text = _("Set the BookOrbit server address first."), timeout = 3 })
return
end
if not self:isLoggedIn() then
self:promptLogin()
return
end
NetworkMgr:runWhenConnected(function()
local checking = InfoMessage:new{ text = _("Testing BookOrbit connection...") }
UIManager:show(checking)
local body, err = self:newClient():auth()
UIManager:close(checking)
if body then
self:recordSyncSuccess("connection_test", _("Connection test succeeded"))
UIManager:show(InfoMessage:new{ text = _("BookOrbit connection works."), timeout = 3 })
else
self:recordSyncError("connection_test", err)
UIManager:show(InfoMessage:new{
text = T(_("BookOrbit connection failed: %1"), self:errorLabel(err)),
timeout = 4,
})
end
end)
end
function MainMenu:dashboardSectionLabel(index)
return DashboardSections.headerText(DashboardSections.at(self.settings, index))
end
-- Applies a new choice for one configurable dashboard row.
function MainMenu:applyDashboardSection(index, config, catalog, touchmenu_instance)
if catalog and catalog.setDashboardSection then
catalog:setDashboardSection(config, index)
else
self.settings[DashboardSections.SETTING_KEY] = DashboardSections.storeAt(self.settings, index, config)
G_reader_settings:flush()
end
if touchmenu_instance then touchmenu_instance:updateItems() end
end
-- Restores the four default slots in one write, so the dashboard refreshes once
-- rather than once per slot. The catalog owns the write while it is open, which
-- keeps the slots and the cache signature in step.
function MainMenu:resetDashboardSections(catalog, touchmenu_instance)
local defaults = DashboardSections.normalize(nil)
if catalog and catalog.setDashboardSections then
catalog:setDashboardSections(defaults)
else
self.settings[DashboardSections.SETTING_KEY] = defaults
G_reader_settings:flush()
end
if touchmenu_instance then touchmenu_instance:updateItems() end
end
function MainMenu:chooseDashboardCatalogSource(index, section, catalog, touchmenu_instance)
if self.dashboard_menu_container then
local menu_container = self.dashboard_menu_container
self.dashboard_menu_container = nil
UIManager:close(menu_container)
elseif touchmenu_instance then
touchmenu_instance:closeMenu()
end
local function openSelector(target)
target:loadSection(section, { dashboard_source_index = index })
end
if catalog then
UIManager:nextTick(function() openSelector(catalog) end)
return
end
self:openCatalogBrowser(false)
UIManager:nextTick(function()
if self.catalog_browser then
openSelector(self.catalog_browser)
else
UIManager:show(InfoMessage:new{ text = _("Could not open the BookOrbit catalog."), timeout = 3 })
end
end)
end
-- The capability list recorded for the configured server, when one is known.
-- Nil (still unknown) keeps every source on offer; the dashboard downgrades
-- the capability itself when the server answers 404.
function MainMenu:knownServerCapabilities(catalog)
local client = catalog and catalog.client
if not client and self.newClient then
local ok, built = pcall(function() return self:newClient() end)
client = ok and built or nil
end
return client and BookOrbitCapabilities.cached(client) or nil
end
function MainMenu:dashboardSectionItems(index, catalog)
local items = {}
local known_capabilities = self:knownServerCapabilities(catalog)
for _index, section_type in ipairs(DashboardSections.TYPES) do
local unsupported = DashboardSections.usesSectionEndpoint(section_type)
and known_capabilities ~= nil
and known_capabilities[DashboardSections.SECTION_ENDPOINT_CAPABILITY] ~= true
local is_selector = DashboardSections.isCatalogSelector(section_type)
if not unsupported then
table.insert(items, {
text = DashboardSections.label(section_type),
mandatory = is_selector and ">" or nil,
help_text = DashboardSections.helpText(section_type),
checked_func = is_selector and nil or function()
return DashboardSections.at(self.settings, index).type == section_type
end,
keep_menu_open = is_selector,
callback = function(touchmenu_instance)
if is_selector then
self:chooseDashboardCatalogSource(index, section_type, catalog, touchmenu_instance)
else
self:applyDashboardSection(index, { type = section_type }, catalog, touchmenu_instance)
end
end,
})
end
end
return items
end
function MainMenu:dashboardSettingsMenu(catalog)
local items = {
{
text_func = function()
return T(_("Open dashboard on startup (%1)"), self:catalogAutoOpenLabel())
end,
sub_item_table = self:catalogAutoOpenMenu(),
},
{
text = _("Tap Continue reading to open the book"),
checked_func = function()
return self.settings.catalog_dashboard_tap_resumes ~= false
end,
help_text = _([[Tapping a book under Continue reading opens it directly when it is already on the device; hold for its actions. When disabled, tapping opens the book's details instead.]]),
callback = function()
self.settings.catalog_dashboard_tap_resumes = self.settings.catalog_dashboard_tap_resumes == false
end,
},
}
for index = 1, DashboardSections.SLOT_COUNT do
table.insert(items, {
text_func = function()
return T(_("Section %1 (%2)"), index, self:dashboardSectionLabel(index))
end,
sub_item_table = self:dashboardSectionItems(index, catalog),
})
end
table.insert(items, {
text = _("Reset to Default"),
separator = true,
callback = function(touchmenu_instance)
self:resetDashboardSections(catalog, touchmenu_instance)
end,
})
return items
end
function MainMenu:syncSettingsMenu(has_open_book)
local items = {}
table.insert(items, {
text = _("Skip auto-sync when offline"),
checked_func = function() return self.settings.skip_sync_when_offline end,
enabled_func = function() return self.settings.auto_sync end,
help_text = _([[When enabled, automatic sync on book open is skipped if the device is not already online. Prevents the e-reader from stalling while trying to connect to Wi-Fi.]]),
callback = function()
self.settings.skip_sync_when_offline = not self.settings.skip_sync_when_offline
end,
})
table.insert(items, {
text_func = function()
return T(_("Periodically sync every # pages (%1)"), self:getSyncPeriod())
end,
enabled_func = function() return self.settings.auto_sync end,
keep_menu_open = true,
separator = true,
callback = function(touchmenu_instance)
local SpinWidget = require("ui/widget/spinwidget")
local spin = SpinWidget:new{
text = _([[This value determines how many page turns it takes to push book progress.
If set to 0, updating progress based on page turns will be disabled.]]),
value = self.settings.pages_before_update or 0,
value_min = 0,
value_max = 999,
value_step = 1,
value_hold_step = 10,
ok_text = _("Set"),
title_text = _("Number of pages before update"),
default_value = 10,
callback = function(widget)
self:setPagesBeforeUpdate(widget.value)
if touchmenu_instance then touchmenu_instance:updateItems() end
end,
}
UIManager:show(spin)
end,
})
table.insert(items, {
text = _("Two-way highlights & bookmarks"),
checked_func = function() return self.settings.annotation_sync end,
help_text = _([[Also applies highlights, notes, bookmarks (dogears) and deletions made in BookOrbit to this device: on book open, after the manual book sync, and during the full sweep for closed books. Turning this off keeps uploads only. Bookmark sync needs a server that supports it, and PDF bookmarks never sync.]]),
callback = function()
self.settings.annotation_sync = not self.settings.annotation_sync
end,
})
table.insert(items, {
text_func = function()
return T(_("Sync to a newer state (%1)"), getNameStrategy(self.settings.sync_forward))
end,
sub_item_table = self:strategyMenu(
function() return self.settings.sync_forward end,
function(value) self.settings.sync_forward = value end
),
})
table.insert(items, {
text_func = function()
return T(_("Sync to an older state (%1)"), getNameStrategy(self.settings.sync_backward))
end,
sub_item_table = self:strategyMenu(
function() return self.settings.sync_backward end,
function(value) self.settings.sync_backward = value end
),
})
return items
end
function MainMenu:pluginSettingsMenu()
return {
{
id = "plugin_update",
text_func = function()
return self:updateCheckMenuText()
end,
keep_menu_open = true,
callback = function()
self:checkForUpdate()
end,
},
}
end
function MainMenu:settingsMenu(has_open_book, opts)
opts = opts or {}
local items = {
{
text = _("Dashboard"),
sub_item_table = self:dashboardSettingsMenu(opts.catalog),
},
{
text = _("Sync"),
sub_item_table = self:syncSettingsMenu(has_open_book),
},
}
if opts.include_plugin ~= false then
table.insert(items, {
text = _("Plugin"),
sub_item_table = self:pluginSettingsMenu(),
})
end
return items
end
function MainMenu:addToMainMenu(menu_items)
local logged_in = self:isLoggedIn()
local has_open_book = hasReaderBook(self)
local items = {}
if logged_in then
table.insert(items, {
id = "open_dashboard",
text = _("Open dashboard"),
callback = function()
self:browseCatalog()
end,
separator = true,
})
if has_open_book then
table.insert(items, {
id = "sync_current_book",
text = _("Sync current book now"),
callback = function()
self:onBookOrbitSyncBook()
end,
})
table.insert(items, {
id = "auto_sync_current_book",
text = _("Auto sync current book"),
checked_func = function() return self.settings.auto_sync end,
help_text = _([[Pulls progress when a book is opened; pushes progress, highlights, status, rating and reading time when it is closed and on suspend.]]),
callback = function()
self:onBookOrbitToggleAutoSync(nil, true)
end,
separator = true,
})
end
table.insert(items, {
text = _("Sync all books now"),
help_text = _([[Syncs reading data, highlights, status and progress for books that changed since the last sync. Use "Recheck all book matches" in diagnostics to re-verify every book against the server.]]),
callback = function()
self:startSweep()
end,
separator = true,
})
table.insert(items, {
id = "settings",
text = _("Settings"),
separator = true,
sub_item_table = self:settingsMenu(has_open_book),
})
end
table.insert(items, {
text = _("Account & setup"),
sub_item_table = {
{
text = _("BookOrbit server address"),
keep_menu_open = true,
callback = function()
self:setServerAddress()
end,
},
{
text_func = function()
return self.settings.userkey and _("Logout") or _("Login")
end,
enabled_func = function()
return self.settings.server_url ~= nil
end,
keep_menu_open = true,
callback_func = function()
if self.settings.userkey then
return function(menu)
self:logout(menu)
end
else
return function(menu)
self:login(menu)
end
end
end,
},
{
text = _("Diagnostics"),
callback = function()
self:showDiagnostics()
end,
},
},
})
menu_items.bookorbit = {
text = _("BookOrbit"),
-- Fallback placement only: BookOrbitMenuPin normally pins this entry
-- right below calibre on the first page of the Tools menu.
sorting_hint = "tools",
sub_item_table = items,
}
end
function MainMenu:dashboardMenuItems(catalog)
local menu_items = {}
self:addToMainMenu(menu_items)
local bookorbit_items = menu_items.bookorbit.sub_item_table or {}
local items = {
icon = "appbar.menu",
}
if self:isLoggedIn() then
local plugin_update = self:pluginSettingsMenu()[1]
plugin_update.separator = true
table.insert(items, plugin_update)
end
for _index, item in ipairs(bookorbit_items) do
if item.id ~= "open_dashboard" and item.id ~= "sync_current_book" and item.id ~= "auto_sync_current_book" then
if item.id == "settings" then
table.insert(items, {
id = "settings",
text = _("Settings"),
separator = item.separator,
sub_item_table = self:settingsMenu(false, { include_plugin = false, catalog = catalog }),
})
else
table.insert(items, item)
end
end
end
if catalog then
table.insert(items, {
text = _("Close BookOrbit"),
separator = true,
callback = function()
local menu_container = self.dashboard_menu_container
self.dashboard_menu_container = nil
if menu_container then UIManager:close(menu_container) end
catalog:onCloseAllMenus()
end,
})
end
return items
end
function MainMenu:showDashboardMenu(catalog)
local CenterContainer = require("ui/widget/container/centercontainer")
local TouchMenu = require("ui/widget/touchmenu")
if self.dashboard_menu_container then
local old_container = self.dashboard_menu_container
self.dashboard_menu_container = nil
UIManager:close(old_container)
end
local menu_container = CenterContainer:new{
covers_header = true,
ignore = "height",
dimen = Device.screen:getSize(),
}
local dashboard_menu = TouchMenu:new{
width = Device.screen:getWidth(),
last_index = 1,
tab_item_table = { self:dashboardMenuItems(catalog) },
show_parent = menu_container,
}
local closing = false
dashboard_menu.close_callback = function()
if closing then return true end
closing = true
if self.dashboard_menu_container == menu_container then
self.dashboard_menu_container = nil
end
UIManager:close(menu_container)
return true
end
menu_container[1] = dashboard_menu
self.dashboard_menu_container = menu_container
UIManager:show(menu_container)
end
function MainMenu:refreshActiveCatalogApi(refresh)
if not self.catalog_browser then return end
self.catalog_browser:setApi(self:apiOpts(true))
if refresh then self.catalog_browser:refreshCurrent() end
end
function MainMenu:applyServerAddress(server_url)
self.settings.server_url = server_url
self:refreshActiveCatalogApi(false)
end
function MainMenu:setServerAddress()
local dialog
dialog = InputDialog:new{
title = _("BookOrbit server address"),
input = self.settings.server_url or "https://",
input_hint = "https://bookorbit.example.com",
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(dialog)
end,
},
{
text = _("OK"),
is_enter_default = true,
callback = function()
local normalized = BookOrbitApi.normalizeServerUrl(dialog:getInputText())
self:applyServerAddress(normalized)
UIManager:close(dialog)
if normalized then
Notification:notify(T(_("BookOrbit server set to %1"), normalized))
end
end,
},
},
},
}
UIManager:show(dialog)
dialog:onShowKeyboard()
end
function MainMenu:login(menu)
if NetworkMgr:willRerunWhenConnected(function() self:login(menu) end) then
return
end
local dialog
dialog = MultiInputDialog:new{
title = self.title,
fields = {
{
text = self.settings.username,
hint = "username",
},
{
hint = "password",
text_type = "password",
},
},
description = _("Credentials are created in BookOrbit web settings under Settings, KOReader."),
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(dialog)
end,
},
{
text = _("Login"),
is_enter_default = true,
callback = function()
local username, password = unpack(dialog:getFields())
username = util.trim(username or "")
if username == "" or not password or password == "" then
UIManager:show(InfoMessage:new{ text = _("Please enter a username and password."), timeout = 2 })
return
end
UIManager:close(dialog)
UIManager:scheduleIn(0.5, function()
self:doLogin(username, password, menu)
end)
UIManager:show(InfoMessage:new{ text = _("Logging in. Please wait."), timeout = 1 })
end,
},
},
},
}
UIManager:show(dialog)
dialog:onShowKeyboard()
end
function MainMenu:doLogin(username, password, menu)
Device:setIgnoreInput(true)
local userkey = md5(password)
local opts = self:apiOpts()
opts.username = username
opts.userkey = userkey
local client = BookOrbitApi.new(opts)
local body, err = client:auth()
Device:setIgnoreInput(false)
if body then
self.settings.username = username
self.settings.userkey = userkey
self:refreshActiveCatalogApi(true)
if menu then menu:updateItems() end
UIManager:show(InfoMessage:new{ text = _("Logged in to BookOrbit.") })
UIManager:scheduleIn(1, function()
if self.requestUpdateCheck then
self:requestUpdateCheck(false, "login")
else
self:maybeCheckForUpdate(false)
end
end)
elseif err == 401 or err == 403 then
UIManager:show(InfoMessage:new{
text = _("Login failed. Create or check your KOReader credentials in BookOrbit web settings."),
})
else
UIManager:show(InfoMessage:new{ text = T(_("Could not reach the BookOrbit server: %1"), tostring(err)) })
end
end
function MainMenu:logout(menu)
self.settings.userkey = nil
self:refreshActiveCatalogApi(false)
if menu then menu:updateItems() end
end
function MainMenu.install(BookOrbit)
SYNC_STRATEGY = BookOrbit.SYNC_STRATEGY
for name, fn in pairs(MainMenu) do
if name ~= "install" then
BookOrbit[name] = fn
end
end
end
return MainMenu