mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
d8e8765518
Closes #449
1351 lines
46 KiB
Lua
1351 lines
46 KiB
Lua
--[[--
|
|
BookOrbit Sync plugin.
|
|
|
|
Live progress sync mirrors the stock kosync plugin (pull on open with
|
|
conflict strategies, periodic push every N page turns, push on close and
|
|
suspend, 25 second debounce) against BookOrbit's kosync-compatible endpoints.
|
|
Closing or suspending additionally snapshots the open book from live memory
|
|
(progress, highlights, status/rating, page stats) and uploads it per book.
|
|
The full-library sweep is manual-only.
|
|
]]
|
|
|
|
local ConfirmBox = require("ui/widget/confirmbox")
|
|
local Device = require("device")
|
|
local Dispatcher = require("dispatcher")
|
|
local Event = require("ui/event")
|
|
local InfoMessage = require("ui/widget/infomessage")
|
|
local InputDialog = require("ui/widget/inputdialog")
|
|
local Math = require("optmath")
|
|
local MultiInputDialog = require("ui/widget/multiinputdialog")
|
|
local NetworkMgr = require("ui/network/manager")
|
|
local Notification = require("ui/widget/notification")
|
|
local PluginShare = require("pluginshare")
|
|
local UIManager = require("ui/uimanager")
|
|
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
|
local logger = require("logger")
|
|
local md5 = require("ffi/sha2").md5
|
|
local time = require("ui/time")
|
|
local util = require("util")
|
|
local T = require("ffi/util").template
|
|
local _ = require("gettext")
|
|
|
|
local BookOrbitAnnotations = require("bookorbit_annotations")
|
|
local BookOrbitApi = require("bookorbit_api")
|
|
local BookOrbitBookSync = require("bookorbit_book_sync")
|
|
local BookOrbitCatalog = require("bookorbit_catalog")
|
|
local BookOrbitState = require("bookorbit_state")
|
|
local BookOrbitMenuPin = require("bookorbit_menu_pin")
|
|
local BookOrbitSweep = require("bookorbit_sweep")
|
|
local BookOrbitUpdater = require("bookorbit_updater")
|
|
|
|
local PLUGIN_VERSION = "1.0.1"
|
|
|
|
local SYNC_STRATEGY = {
|
|
PROMPT = 1,
|
|
SILENT = 2,
|
|
DISABLE = 3,
|
|
}
|
|
|
|
local API_CALL_DEBOUNCE_DELAY = time.s(25)
|
|
local UPDATE_CHECK_INTERVAL = 24 * 60 * 60
|
|
|
|
local BookOrbit = WidgetContainer:extend{
|
|
name = "bookorbit",
|
|
title = _("Login to BookOrbit"),
|
|
|
|
push_timestamp = nil,
|
|
pull_timestamp = nil,
|
|
last_page = nil,
|
|
last_page_turn_timestamp = nil,
|
|
|
|
settings = nil,
|
|
}
|
|
|
|
BookOrbit.default_settings = {
|
|
settings_version = 1,
|
|
server_url = nil,
|
|
username = nil,
|
|
userkey = nil,
|
|
auto_sync = false,
|
|
annotation_sync = true,
|
|
pages_before_update = 10,
|
|
sync_forward = SYNC_STRATEGY.PROMPT,
|
|
sync_backward = SYNC_STRATEGY.DISABLE,
|
|
catalog_view_mode = "mosaic",
|
|
catalog_sort = "recently_added",
|
|
catalog_grid_cols = 3,
|
|
catalog_grid_rows = 3,
|
|
catalog_recent_searches = {},
|
|
catalog_auto_open = "off",
|
|
catalog_dashboard_cache = nil,
|
|
update_check_last_at = 0,
|
|
}
|
|
|
|
function BookOrbit:init()
|
|
self.push_timestamp = 0
|
|
self.pull_timestamp = 0
|
|
self.last_page = -1
|
|
self.last_page_turn_timestamp = 0
|
|
self.page_update_counter = 0
|
|
self.periodic_push_scheduled = false
|
|
self.provision_applied = false
|
|
self.periodic_push_task = function()
|
|
self.periodic_push_scheduled = false
|
|
self.page_update_counter = 0
|
|
-- Push only, no pull, no network nagging: relies on the connection
|
|
-- being already up, like the stock kosync periodic push.
|
|
if self.settings.auto_sync and (self.settings.pages_before_update or 0) > 0 then
|
|
self:updateProgress(false, false)
|
|
end
|
|
end
|
|
|
|
self.settings = G_reader_settings:readSetting("bookorbit", self.default_settings)
|
|
self.device_id = G_reader_settings:readSetting("device_id")
|
|
|
|
-- Detect settings from the old bookorbit-koplugin, which used the same
|
|
-- "bookorbit" key but a different structure. Reset behavior keys to
|
|
-- defaults while preserving any existing credentials so users aren't
|
|
-- logged out.
|
|
if not self.settings.settings_version then
|
|
local server_url = self.settings.server_url
|
|
local username = self.settings.username
|
|
local userkey = self.settings.userkey
|
|
local provision_fingerprint = self.settings.provision_fingerprint
|
|
self.settings = {}
|
|
for k, v in pairs(self.default_settings) do
|
|
self.settings[k] = type(v) == "table" and {} or v
|
|
end
|
|
self.settings.server_url = server_url
|
|
self.settings.username = username
|
|
self.settings.userkey = userkey
|
|
self.settings.provision_fingerprint = provision_fingerprint
|
|
G_reader_settings:saveSetting("bookorbit", self.settings)
|
|
G_reader_settings:flush()
|
|
logger.info("BookOrbit: migrated settings from previous schema")
|
|
end
|
|
|
|
-- v1 settings cleanup: full sweeps are manual-only since 0.2.0.
|
|
self.settings.sweep_on_close = nil
|
|
self.settings.sweep_on_suspend = nil
|
|
if self.settings.pages_before_update == nil then
|
|
self.settings.pages_before_update = 10
|
|
end
|
|
if self.settings.annotation_sync == nil then
|
|
self.settings.annotation_sync = true
|
|
end
|
|
if self.settings.update_check_last_at == nil then
|
|
self.settings.update_check_last_at = 0
|
|
end
|
|
if self.settings.catalog_auto_open == nil then
|
|
self.settings.catalog_auto_open = "off"
|
|
end
|
|
|
|
self:applyProvision()
|
|
|
|
if self.settings.auto_sync and Device:hasSeamlessWifiToggle() and G_reader_settings:readSetting("wifi_enable_action") ~= "turn_on" then
|
|
self.settings.auto_sync = false
|
|
logger.warn("BookOrbit: auto sync disabled because wifi_enable_action is not turn_on")
|
|
end
|
|
|
|
pcall(BookOrbitMenuPin.ensure)
|
|
self:onDispatcherRegisterActions()
|
|
self.ui.menu:registerToMainMenu(self)
|
|
self:onStart()
|
|
UIManager:scheduleIn(5, function()
|
|
self:maybeCheckForUpdate(false)
|
|
end)
|
|
end
|
|
|
|
local PROVISION_FILE = "bookorbit_provision.lua"
|
|
|
|
-- Applies the connection settings bundled by "Download preconfigured plugin"
|
|
-- in BookOrbit web settings. The fingerprint ties one download to one apply:
|
|
-- a freshly generated zip always (re)configures, while reinstalling an old
|
|
-- one never overrides a logout.
|
|
function BookOrbit:applyProvision()
|
|
if not self.path then return end
|
|
local provision_path = self.path .. "/" .. PROVISION_FILE
|
|
local chunk = loadfile(provision_path)
|
|
if not chunk then return end
|
|
|
|
setfenv(chunk, {})
|
|
local ok, provision = pcall(chunk)
|
|
if not ok or type(provision) ~= "table" then
|
|
logger.warn("BookOrbit: ignoring unreadable provision file", provision_path)
|
|
return
|
|
end
|
|
|
|
local server_url = BookOrbitApi.normalizeServerUrl(provision.server_url)
|
|
if not server_url or type(provision.username) ~= "string" or type(provision.userkey) ~= "string" then
|
|
logger.warn("BookOrbit: ignoring incomplete provision file", provision_path)
|
|
return
|
|
end
|
|
|
|
local fingerprint = md5(table.concat({
|
|
server_url, provision.username, provision.userkey, tostring(provision.generated_at),
|
|
}, "\0"))
|
|
if self.settings.provision_fingerprint ~= fingerprint then
|
|
self.settings.server_url = server_url
|
|
self.settings.username = provision.username
|
|
self.settings.userkey = provision.userkey
|
|
self.settings.provision_fingerprint = fingerprint
|
|
self.provision_applied = true
|
|
G_reader_settings:flush()
|
|
logger.info("BookOrbit: applied provision file for", provision.username)
|
|
UIManager:nextTick(function()
|
|
UIManager:show(InfoMessage:new{
|
|
text = T(_("BookOrbit sync is set up for %1 as %2."), provision.server_url, provision.username),
|
|
timeout = 5,
|
|
})
|
|
end)
|
|
end
|
|
os.remove(provision_path)
|
|
end
|
|
|
|
function BookOrbit:apiOpts()
|
|
return {
|
|
server_url = self.settings.server_url,
|
|
username = self.settings.username,
|
|
userkey = self.settings.userkey,
|
|
device_id = self.device_id,
|
|
device_model = Device.model,
|
|
plugin_version = PLUGIN_VERSION,
|
|
}
|
|
end
|
|
|
|
function BookOrbit:newClient()
|
|
return BookOrbitApi.new(self:apiOpts())
|
|
end
|
|
|
|
function BookOrbit:isLoggedIn()
|
|
return self.settings.server_url ~= nil and self.settings.username ~= nil and self.settings.userkey ~= nil
|
|
end
|
|
|
|
function BookOrbit:onStart()
|
|
if PluginShare.bookorbit_auto_open_done then return end
|
|
PluginShare.bookorbit_auto_open_done = true
|
|
|
|
local mode = self.settings.catalog_auto_open or "off"
|
|
if mode == "off" then return end
|
|
UIManager:scheduleIn(1.2, function()
|
|
self:maybeAutoOpenCatalog(mode)
|
|
end)
|
|
end
|
|
|
|
function BookOrbit:maybeAutoOpenCatalog(mode)
|
|
if mode ~= "filemanager" and mode ~= "always" then return end
|
|
if self.provision_applied then return end
|
|
if not self:isLoggedIn() then return end
|
|
if self.catalog_browser ~= nil then return end
|
|
if mode == "filemanager" and not (self.ui and self.ui.file_chooser ~= nil) then
|
|
return
|
|
end
|
|
self:browseCatalog(true)
|
|
end
|
|
|
|
local function showSyncError()
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("Something went wrong when syncing to BookOrbit, please check your network connection and try again later."),
|
|
timeout = 3,
|
|
})
|
|
end
|
|
|
|
local function promptLogin()
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("Please configure the BookOrbit server and login first."),
|
|
timeout = 3,
|
|
})
|
|
end
|
|
|
|
function BookOrbit:onDispatcherRegisterActions()
|
|
Dispatcher:registerAction("bookorbit_sync_now",
|
|
{ category = "none", event = "BookOrbitSyncNow", title = _("BookOrbit: sync all books"), general = true })
|
|
Dispatcher:registerAction("bookorbit_sync_book",
|
|
{ category = "none", event = "BookOrbitSyncBook", title = _("BookOrbit: sync this book"), reader = true })
|
|
Dispatcher:registerAction("bookorbit_push_progress",
|
|
{ category = "none", event = "BookOrbitPushProgress", title = _("BookOrbit: push progress"), reader = true })
|
|
Dispatcher:registerAction("bookorbit_pull_progress",
|
|
{ category = "none", event = "BookOrbitPullProgress", title = _("BookOrbit: pull progress"), reader = true })
|
|
Dispatcher:registerAction("bookorbit_open_dashboard",
|
|
{ category = "none", event = "BookOrbitOpenDashboard", title = _("BookOrbit: open dashboard"), general = true, separator = true })
|
|
end
|
|
|
|
function BookOrbit:onReaderReady()
|
|
if self.settings.auto_sync then
|
|
UIManager:nextTick(function()
|
|
self:getProgress(true, false)
|
|
end)
|
|
if self.settings.annotation_sync then
|
|
UIManager:scheduleIn(2, function()
|
|
self:exchangeAnnotationsForOpenBook()
|
|
end)
|
|
end
|
|
end
|
|
self:registerEvents()
|
|
|
|
self.last_page = self.ui:getCurrentPage()
|
|
end
|
|
|
|
-- Two-way annotation pull/push for the open book. Runs once per book open;
|
|
-- safe to call again manually, guarded against concurrent syncs.
|
|
function BookOrbit:exchangeAnnotationsForOpenBook()
|
|
if self.annotation_exchange_running then return end
|
|
if not self:isLoggedIn() or not self.ui or not self.ui.document then return end
|
|
if BookOrbitBookSync.isRunning() or BookOrbitSweep.isRunning() then return end
|
|
|
|
local digest = self:getDocumentDigest()
|
|
if not digest then return end
|
|
local state = BookOrbitState.open()
|
|
if not state:getBook(digest) then
|
|
-- Unknown or unmatched book: the close-path snapshot sync matches it.
|
|
return
|
|
end
|
|
|
|
self.annotation_exchange_running = true
|
|
local ok, result, err = pcall(BookOrbitAnnotations.exchangeOpenBook, {
|
|
client = self:newClient(),
|
|
state = state,
|
|
digest = digest,
|
|
ui = self.ui,
|
|
})
|
|
state:flush()
|
|
self.annotation_exchange_running = false
|
|
|
|
if not ok then
|
|
logger.err("BookOrbit: annotation exchange error:", result)
|
|
elseif result then
|
|
local touched = (result.applied or 0) + (result.deleted or 0)
|
|
if touched > 0 then
|
|
Notification:notify(T(_("BookOrbit: %1 highlight(s) updated"), touched))
|
|
end
|
|
elseif err and err ~= "unmatched" and err ~= "unsupported_server" and err ~= "network" then
|
|
logger.dbg("BookOrbit: annotation exchange skipped:", err)
|
|
end
|
|
end
|
|
|
|
-- Menu
|
|
|
|
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 BookOrbit: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 BookOrbit:hasKnownUpdate()
|
|
return BookOrbitUpdater.isNewer(self.settings.update_latest_version, PLUGIN_VERSION) == true
|
|
end
|
|
|
|
function BookOrbit:updateCheckMenuText()
|
|
if self:hasKnownUpdate() then
|
|
return T(_("Plugin update available: v%1 -> v%2"), PLUGIN_VERSION, self.settings.update_latest_version)
|
|
end
|
|
if self:isLoggedIn() then
|
|
return T(_("Installed plugin: v%1 (Check for update)"), PLUGIN_VERSION)
|
|
end
|
|
return T(_("Installed plugin: v%1 (Login required)"), PLUGIN_VERSION)
|
|
end
|
|
|
|
function BookOrbit: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 BookOrbit: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
|
|
|
|
function BookOrbit:addToMainMenu(menu_items)
|
|
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 = {
|
|
{
|
|
text = _("Open dashboard"),
|
|
enabled_func = function()
|
|
return self:isLoggedIn()
|
|
end,
|
|
callback = function()
|
|
self:browseCatalog()
|
|
end,
|
|
},
|
|
{
|
|
text_func = function()
|
|
return self:updateCheckMenuText()
|
|
end,
|
|
enabled_func = function()
|
|
return self:isLoggedIn()
|
|
and not BookOrbitSweep.isRunning()
|
|
and not BookOrbitBookSync.isRunning()
|
|
end,
|
|
keep_menu_open = true,
|
|
callback = function()
|
|
self:checkForUpdate()
|
|
end,
|
|
},
|
|
{
|
|
text_func = function()
|
|
local status = BookOrbitSweep.syncStatus()
|
|
local when = (status.lastSweepAt == 0) and _("never")
|
|
or os.date("%Y-%m-%d %H:%M", status.lastSweepAt)
|
|
if status.unmatched > 0 then
|
|
return T(_("Last sync: %1 (%2 linked, %3 unmatched)"), when, status.matched, status.unmatched)
|
|
elseif status.matched > 0 then
|
|
return T(_("Last sync: %1 (%2 linked)"), when, status.matched)
|
|
end
|
|
return T(_("Last sync: %1"), when)
|
|
end,
|
|
enabled = false,
|
|
separator = true,
|
|
},
|
|
{
|
|
text = _("Sync this book now"),
|
|
enabled_func = function()
|
|
return self:isLoggedIn() and self.ui.document ~= nil
|
|
and not BookOrbitSweep.isRunning() and not BookOrbitBookSync.isRunning()
|
|
end,
|
|
callback = function()
|
|
self:onBookOrbitSyncBook()
|
|
end,
|
|
},
|
|
{
|
|
text = _("Sync all books now"),
|
|
enabled_func = function()
|
|
return self:isLoggedIn() and not BookOrbitSweep.isRunning() and not BookOrbitBookSync.isRunning()
|
|
end,
|
|
callback = function()
|
|
self:startSweep()
|
|
end,
|
|
separator = true,
|
|
},
|
|
{
|
|
text = _("Auto sync this 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,
|
|
},
|
|
{
|
|
text = _("Two-way highlight sync"),
|
|
checked_func = function() return self.settings.annotation_sync end,
|
|
help_text = _([[Also applies highlights, notes 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.]]),
|
|
callback = function()
|
|
self.settings.annotation_sync = not self.settings.annotation_sync
|
|
end,
|
|
separator = true,
|
|
},
|
|
{
|
|
text = _("Sync settings"),
|
|
sub_item_table = {
|
|
{
|
|
text_func = function()
|
|
return T(_("Open dashboard on startup (%1)"), self:catalogAutoOpenLabel())
|
|
end,
|
|
sub_item_table = self:catalogAutoOpenMenu(),
|
|
},
|
|
{
|
|
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,
|
|
callback = function(touchmenu_instance)
|
|
local SpinWidget = require("ui/widget/spinwidget")
|
|
local items = 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(spin)
|
|
self:setPagesBeforeUpdate(spin.value)
|
|
if touchmenu_instance then touchmenu_instance:updateItems() end
|
|
end,
|
|
}
|
|
UIManager:show(items)
|
|
end,
|
|
},
|
|
{
|
|
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
|
|
),
|
|
},
|
|
{
|
|
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
|
|
),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
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,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
end
|
|
|
|
function BookOrbit:dashboardMenuItems(catalog)
|
|
local menu_items = {}
|
|
self:addToMainMenu(menu_items)
|
|
local bookorbit_items = menu_items.bookorbit.sub_item_table or {}
|
|
local items = {
|
|
icon = "appbar.menu",
|
|
}
|
|
for index, item in ipairs(bookorbit_items) do
|
|
if index ~= 1 then
|
|
table.insert(items, item)
|
|
end
|
|
end
|
|
return items
|
|
end
|
|
|
|
function BookOrbit: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 BookOrbit:openCatalogBrowser(prefer_cached_dashboard)
|
|
if self.catalog_browser ~= nil then return end
|
|
self.catalog_browser = BookOrbitCatalog:new{
|
|
title = _("BookOrbit"),
|
|
api = self:apiOpts(),
|
|
settings = self.settings,
|
|
prefer_cached_dashboard = prefer_cached_dashboard,
|
|
save_settings = function()
|
|
G_reader_settings:flush()
|
|
end,
|
|
show_dashboard_menu = function(catalog)
|
|
self:showDashboardMenu(catalog)
|
|
end,
|
|
_manager = self,
|
|
close_callback = function()
|
|
UIManager:close(self.catalog_browser)
|
|
self.catalog_browser = nil
|
|
end,
|
|
}
|
|
UIManager:show(self.catalog_browser)
|
|
end
|
|
|
|
function BookOrbit:browseCatalog(allow_offline)
|
|
if not self:isLoggedIn() then
|
|
promptLogin()
|
|
return
|
|
end
|
|
|
|
if allow_offline then
|
|
self:openCatalogBrowser(true)
|
|
else
|
|
NetworkMgr:runWhenConnected(function()
|
|
self:openCatalogBrowser(false)
|
|
end)
|
|
end
|
|
end
|
|
|
|
function BookOrbit: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.settings.server_url = 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 BookOrbit:login(menu)
|
|
if NetworkMgr:willRerunWhenOnline(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 BookOrbit:doLogin(username, password, menu)
|
|
Device:setIgnoreInput(true)
|
|
local userkey = md5(password)
|
|
local client = BookOrbitApi.new{
|
|
server_url = self.settings.server_url,
|
|
username = username,
|
|
userkey = userkey,
|
|
device_id = self.device_id,
|
|
device_model = Device.model,
|
|
plugin_version = PLUGIN_VERSION,
|
|
}
|
|
local body, err = client:auth()
|
|
Device:setIgnoreInput(false)
|
|
|
|
if body then
|
|
self.settings.username = username
|
|
self.settings.userkey = userkey
|
|
if menu then menu:updateItems() end
|
|
UIManager:show(InfoMessage:new{ text = _("Logged in to BookOrbit.") })
|
|
UIManager:scheduleIn(1, function()
|
|
self:maybeCheckForUpdate(false)
|
|
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 BookOrbit:logout(menu)
|
|
self.settings.userkey = nil
|
|
if menu then menu:updateItems() end
|
|
end
|
|
|
|
function BookOrbit:getSyncPeriod()
|
|
if not self.settings.auto_sync then
|
|
return _("Not available")
|
|
end
|
|
local period = self.settings.pages_before_update
|
|
if period and period > 0 then
|
|
return period
|
|
end
|
|
return _("Never")
|
|
end
|
|
|
|
function BookOrbit:setPagesBeforeUpdate(value)
|
|
self.settings.pages_before_update = value
|
|
end
|
|
|
|
function BookOrbit:schedulePeriodicPush()
|
|
UIManager:unschedule(self.periodic_push_task)
|
|
-- A sizable delay debounces nicely while skimming.
|
|
UIManager:scheduleIn(10, self.periodic_push_task)
|
|
self.periodic_push_scheduled = true
|
|
end
|
|
|
|
-- Live progress sync (kosync mirror)
|
|
|
|
function BookOrbit:getLastPercent()
|
|
if self.ui.document.info.has_pages then
|
|
return Math.roundPercent(self.ui.paging:getLastPercent())
|
|
else
|
|
return Math.roundPercent(self.ui.rolling:getLastPercent())
|
|
end
|
|
end
|
|
|
|
function BookOrbit:getLastProgress()
|
|
if self.ui.document.info.has_pages then
|
|
return self.ui.paging:getLastProgress()
|
|
else
|
|
return self.ui.rolling:getLastProgress()
|
|
end
|
|
end
|
|
|
|
-- Always the binary partial MD5: BookOrbit matches on the scanner-computed
|
|
-- partial MD5 of the file, so the filename checksum method does not exist here.
|
|
function BookOrbit:getDocumentDigest()
|
|
local digest = self.ui.doc_settings:readSetting("partial_md5_checksum")
|
|
if digest then return digest end
|
|
|
|
local file = self.ui.document.file
|
|
if not file then return nil end
|
|
local ok, computed = pcall(util.partialMD5, file)
|
|
if not ok or not computed then return nil end
|
|
self.ui.doc_settings:saveSetting("partial_md5_checksum", computed)
|
|
return computed
|
|
end
|
|
|
|
function BookOrbit:syncToProgress(progress)
|
|
logger.dbg("BookOrbit: syncing to progress", progress)
|
|
if self.ui.document.info.has_pages then
|
|
self.ui:handleEvent(Event:new("GotoPage", tonumber(progress)))
|
|
else
|
|
self.ui:handleEvent(Event:new("GotoXPointer", progress))
|
|
end
|
|
end
|
|
|
|
function BookOrbit:updateProgress(ensure_networking, interactive, on_suspend)
|
|
if not self:isLoggedIn() then
|
|
if interactive then promptLogin() end
|
|
return
|
|
end
|
|
if not self.ui or not self.ui.document then return end
|
|
|
|
local now = UIManager:getElapsedTimeSinceBoot()
|
|
if not interactive and now - self.push_timestamp <= API_CALL_DEBOUNCE_DELAY then
|
|
logger.dbg("BookOrbit: push debounced")
|
|
return
|
|
end
|
|
|
|
if ensure_networking and NetworkMgr:willRerunWhenOnline(function() self:updateProgress(ensure_networking, interactive, on_suspend) end) then
|
|
return
|
|
end
|
|
|
|
local digest = self:getDocumentDigest()
|
|
if not digest then return end
|
|
|
|
local client = self:newClient()
|
|
local body, err = client:updateProgress(digest, self:getLastPercent(), self:getLastProgress(), os.time())
|
|
if interactive then
|
|
if body then
|
|
UIManager:show(InfoMessage:new{ text = _("Progress has been pushed to BookOrbit."), timeout = 3 })
|
|
if not on_suspend then self:maybeCheckForUpdate(false) end
|
|
else
|
|
showSyncError()
|
|
end
|
|
elseif not body then
|
|
logger.dbg("BookOrbit: push failed:", err)
|
|
elseif not on_suspend then
|
|
self:maybeCheckForUpdate(false)
|
|
end
|
|
|
|
if on_suspend and Device:hasWifiManager() then
|
|
NetworkMgr:disableWifi()
|
|
end
|
|
|
|
self.push_timestamp = now
|
|
end
|
|
|
|
function BookOrbit:getProgress(ensure_networking, interactive)
|
|
if not self:isLoggedIn() then
|
|
if interactive then promptLogin() end
|
|
return
|
|
end
|
|
if not self.ui or not self.ui.document then return end
|
|
|
|
local now = UIManager:getElapsedTimeSinceBoot()
|
|
if not interactive and now - self.pull_timestamp <= API_CALL_DEBOUNCE_DELAY then
|
|
logger.dbg("BookOrbit: pull debounced")
|
|
return
|
|
end
|
|
|
|
if ensure_networking and NetworkMgr:willRerunWhenOnline(function() self:getProgress(ensure_networking, interactive) end) then
|
|
return
|
|
end
|
|
|
|
local digest = self:getDocumentDigest()
|
|
if not digest then return end
|
|
|
|
local client = self:newClient()
|
|
local body, err = client:getProgress(digest)
|
|
self.pull_timestamp = now
|
|
|
|
if not body then
|
|
if interactive then showSyncError() end
|
|
logger.dbg("BookOrbit: pull failed:", err)
|
|
return
|
|
end
|
|
self:maybeCheckForUpdate(false)
|
|
|
|
if not body.percentage then
|
|
if interactive then
|
|
UIManager:show(InfoMessage:new{ text = _("No progress found for this document."), timeout = 3 })
|
|
end
|
|
return
|
|
end
|
|
|
|
if body.device == Device.model and body.device_id == self.device_id then
|
|
if interactive then
|
|
UIManager:show(InfoMessage:new{ text = _("Latest progress is coming from this device."), timeout = 3 })
|
|
end
|
|
return
|
|
end
|
|
|
|
body.percentage = Math.roundPercent(body.percentage)
|
|
local progress = self:getLastProgress()
|
|
local percentage = self:getLastPercent()
|
|
|
|
if percentage == body.percentage or body.progress == progress then
|
|
if interactive then
|
|
UIManager:show(InfoMessage:new{ text = _("The progress has already been synchronized."), timeout = 3 })
|
|
end
|
|
return
|
|
end
|
|
|
|
if interactive then
|
|
self:syncToProgress(body.progress)
|
|
UIManager:show(InfoMessage:new{ text = _("Progress has been synchronized."), timeout = 3 })
|
|
return
|
|
end
|
|
|
|
local self_older
|
|
if body.timestamp ~= nil then
|
|
self_older = (body.timestamp > self.last_page_turn_timestamp)
|
|
else
|
|
self_older = (body.percentage > percentage)
|
|
end
|
|
|
|
local strategy = self_older and self.settings.sync_forward or self.settings.sync_backward
|
|
if strategy == SYNC_STRATEGY.SILENT then
|
|
self:syncToProgress(body.progress)
|
|
UIManager:show(InfoMessage:new{ text = _("Progress has been synchronized."), timeout = 3 })
|
|
elseif strategy == SYNC_STRATEGY.PROMPT then
|
|
local template = self_older and _("Sync to latest location %1% from device '%2'?")
|
|
or _("Sync to previous location %1% from device '%2'?")
|
|
UIManager:show(ConfirmBox:new{
|
|
text = T(template, Math.round(body.percentage * 100), body.device),
|
|
ok_callback = function()
|
|
self:syncToProgress(body.progress)
|
|
end,
|
|
})
|
|
end
|
|
end
|
|
|
|
-- Manual sync triggers
|
|
|
|
function BookOrbit:startSweep()
|
|
if not self:isLoggedIn() then
|
|
promptLogin()
|
|
return
|
|
end
|
|
if BookOrbitBookSync.isRunning() then
|
|
UIManager:show(InfoMessage:new{ text = _("BookOrbit is syncing the current book, try again shortly."), timeout = 2 })
|
|
return
|
|
end
|
|
|
|
local sweep_opts = {
|
|
api = self:apiOpts(),
|
|
interactive = true,
|
|
annotation_sync = self.settings.annotation_sync,
|
|
on_finish = function(err)
|
|
if not err then self:maybeCheckForUpdate(false) end
|
|
end,
|
|
}
|
|
if NetworkMgr:willRerunWhenOnline(function() BookOrbitSweep.run(sweep_opts) end) then
|
|
return
|
|
end
|
|
BookOrbitSweep.run(sweep_opts)
|
|
end
|
|
|
|
function BookOrbit:onBookOrbitSyncBook()
|
|
if not self:isLoggedIn() then
|
|
promptLogin()
|
|
return
|
|
end
|
|
if not self.ui or not self.ui.document then
|
|
UIManager:show(InfoMessage:new{ text = _("Open a book to sync it."), timeout = 2 })
|
|
return
|
|
end
|
|
if BookOrbitSweep.isRunning() or BookOrbitBookSync.isRunning() then
|
|
UIManager:show(InfoMessage:new{ text = _("BookOrbit sync is already running."), timeout = 2 })
|
|
return
|
|
end
|
|
|
|
local snap = BookOrbitBookSync.capture(self)
|
|
if not snap then
|
|
UIManager:show(InfoMessage:new{ text = _("Could not read this book's data."), timeout = 2 })
|
|
return
|
|
end
|
|
|
|
local api_opts = self:apiOpts()
|
|
local run = function()
|
|
BookOrbitBookSync.run{ api = api_opts, snap = snap, reason = "manual", interactive = true, plugin = self,
|
|
annotation_sync = self.settings.annotation_sync }
|
|
end
|
|
if NetworkMgr:willRerunWhenOnline(run) then
|
|
return
|
|
end
|
|
run()
|
|
end
|
|
|
|
-- Events
|
|
|
|
function BookOrbit:_onCloseDocument()
|
|
logger.dbg("BookOrbit: onCloseDocument")
|
|
self.onResume = nil
|
|
self.onSuspend = nil
|
|
UIManager:unschedule(self.periodic_push_task)
|
|
self.periodic_push_scheduled = false
|
|
|
|
if BookOrbitSweep.isRunning() or BookOrbitBookSync.isRunning() then
|
|
logger.dbg("BookOrbit: close sync skipped, another sync is running")
|
|
return
|
|
end
|
|
|
|
-- Snapshot now: reader objects die after this handler returns. ReaderUI
|
|
-- already flushed the sidecar and statistics before broadcasting
|
|
-- CloseDocument, so memory, sidecar and stats DB agree at this point.
|
|
local snap = BookOrbitBookSync.capture(self)
|
|
if not snap then return end
|
|
|
|
local api_opts = self:apiOpts()
|
|
NetworkMgr:goOnlineToRun(function()
|
|
BookOrbitBookSync.run{ api = api_opts, snap = snap, reason = "close", interactive = false,
|
|
annotation_sync = self.settings.annotation_sync }
|
|
end)
|
|
end
|
|
|
|
function BookOrbit:_onPageUpdate(page)
|
|
if page == nil then return end
|
|
if self.last_page ~= page then
|
|
self.last_page = page
|
|
self.last_page_turn_timestamp = os.time()
|
|
self.page_update_counter = self.page_update_counter + 1
|
|
-- A pending periodic push is re-delayed on every page turn so it
|
|
-- only fires once the reader is actually idle.
|
|
if self.periodic_push_scheduled
|
|
or (self.settings.pages_before_update or 0) > 0 and self.page_update_counter >= self.settings.pages_before_update then
|
|
self:schedulePeriodicPush()
|
|
end
|
|
end
|
|
end
|
|
|
|
function BookOrbit:_onResume()
|
|
logger.dbg("BookOrbit: onResume")
|
|
if Device:hasWifiRestore() and NetworkMgr.wifi_was_on and G_reader_settings:isTrue("auto_restore_wifi") then
|
|
return
|
|
end
|
|
UIManager:scheduleIn(1, function()
|
|
self:getProgress(true, false)
|
|
end)
|
|
end
|
|
|
|
function BookOrbit:_onSuspend()
|
|
logger.dbg("BookOrbit: onSuspend")
|
|
UIManager:unschedule(self.periodic_push_task)
|
|
self.periodic_push_scheduled = false
|
|
|
|
if not self:isLoggedIn() then return end
|
|
if BookOrbitSweep.isRunning() or BookOrbitBookSync.isRunning() then return end
|
|
|
|
local snap = BookOrbitBookSync.capture(self)
|
|
if not snap then return end
|
|
|
|
local on_finish
|
|
if Device:hasWifiManager() then
|
|
on_finish = function() NetworkMgr:disableWifi() end
|
|
end
|
|
local api_opts = self:apiOpts()
|
|
-- Synchronous: the device is about to sleep, so the uploads must not be
|
|
-- parked on the scheduler (they would run after resume and then kill
|
|
-- wifi mid-read via on_finish).
|
|
local run = function()
|
|
BookOrbitBookSync.run{
|
|
api = api_opts, snap = snap, reason = "suspend",
|
|
interactive = false, synchronous = true, plugin = self, on_finish = on_finish,
|
|
annotation_sync = self.settings.annotation_sync,
|
|
}
|
|
end
|
|
if NetworkMgr:willRerunWhenOnline(run) then
|
|
return
|
|
end
|
|
run()
|
|
end
|
|
|
|
function BookOrbit:_onNetworkConnected()
|
|
logger.dbg("BookOrbit: onNetworkConnected")
|
|
UIManager:scheduleIn(0.5, function()
|
|
self:getProgress(false, false)
|
|
self:maybeCheckForUpdate(false)
|
|
end)
|
|
end
|
|
|
|
function BookOrbit:_onNetworkDisconnecting()
|
|
logger.dbg("BookOrbit: onNetworkDisconnecting")
|
|
self:updateProgress(false, false)
|
|
end
|
|
|
|
function BookOrbit:onBookOrbitPushProgress()
|
|
self:updateProgress(true, true)
|
|
end
|
|
|
|
function BookOrbit:onBookOrbitPullProgress()
|
|
self:getProgress(true, true)
|
|
end
|
|
|
|
function BookOrbit:onBookOrbitSyncNow()
|
|
self:startSweep()
|
|
end
|
|
|
|
function BookOrbit:onBookOrbitOpenDashboard()
|
|
self:browseCatalog(false)
|
|
end
|
|
|
|
function BookOrbit:onBookOrbitToggleAutoSync(toggle, from_menu)
|
|
if toggle == self.settings.auto_sync then
|
|
return true
|
|
end
|
|
if not self.settings.auto_sync
|
|
and Device:hasSeamlessWifiToggle()
|
|
and G_reader_settings:readSetting("wifi_enable_action") ~= "turn_on" then
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("Auto sync needs KOReader to turn Wi-Fi on without asking. Open Network settings, set 'Action when Wi-Fi is off' to 'Turn on', then enable Auto sync again."),
|
|
})
|
|
return true
|
|
end
|
|
self.settings.auto_sync = not self.settings.auto_sync
|
|
self:registerEvents()
|
|
|
|
if self.settings.auto_sync then
|
|
self:getProgress(true, true)
|
|
else
|
|
UIManager:unschedule(self.periodic_push_task)
|
|
self.periodic_push_scheduled = false
|
|
self.page_update_counter = 0
|
|
if from_menu then
|
|
self:updateProgress(true, true)
|
|
end
|
|
end
|
|
|
|
if not from_menu then
|
|
Notification:notify(self.settings.auto_sync and _("BookOrbit auto progress sync: on") or _("BookOrbit auto progress sync: off"))
|
|
end
|
|
return true
|
|
end
|
|
|
|
function BookOrbit:registerEvents()
|
|
if self.settings.auto_sync then
|
|
self.onCloseDocument = self._onCloseDocument
|
|
self.onPageUpdate = self._onPageUpdate
|
|
self.onResume = self._onResume
|
|
self.onSuspend = self._onSuspend
|
|
self.onNetworkConnected = self._onNetworkConnected
|
|
self.onNetworkDisconnecting = self._onNetworkDisconnecting
|
|
else
|
|
self.onCloseDocument = nil
|
|
self.onPageUpdate = nil
|
|
self.onResume = nil
|
|
self.onSuspend = nil
|
|
self.onNetworkConnected = nil
|
|
self.onNetworkDisconnecting = nil
|
|
end
|
|
end
|
|
|
|
function BookOrbit:onCloseWidget()
|
|
if self.periodic_push_task then
|
|
UIManager:unschedule(self.periodic_push_task)
|
|
self.periodic_push_task = nil
|
|
end
|
|
end
|
|
|
|
function BookOrbit:checkForUpdate()
|
|
if not self:isLoggedIn() then
|
|
promptLogin()
|
|
return
|
|
end
|
|
NetworkMgr:runWhenConnected(function()
|
|
self:doCheckForUpdate()
|
|
end)
|
|
end
|
|
|
|
function BookOrbit:maybeCheckForUpdate(interactive)
|
|
if not self:isLoggedIn() or self._checking_update or self._updating then return end
|
|
if BookOrbitSweep.isRunning() or BookOrbitBookSync.isRunning() then return end
|
|
|
|
local now = os.time()
|
|
if not interactive and now - (self.settings.update_check_last_at or 0) < UPDATE_CHECK_INTERVAL then
|
|
return
|
|
end
|
|
|
|
self._checking_update = true
|
|
local body, err = self:newClient():getPluginVersion()
|
|
self._checking_update = false
|
|
|
|
if not body then
|
|
if interactive then
|
|
UIManager:show(InfoMessage:new{
|
|
text = T(_("Could not check for update: %1"), tostring(err or "network error")),
|
|
timeout = 4,
|
|
})
|
|
else
|
|
logger.dbg("BookOrbit: plugin update check failed:", err)
|
|
end
|
|
return
|
|
end
|
|
|
|
self:handleUpdateVersionResponse(body, interactive, interactive or self.catalog_browser == nil)
|
|
end
|
|
|
|
function BookOrbit:doCheckForUpdate()
|
|
local checking = InfoMessage:new{ text = _("Checking for update...") }
|
|
UIManager:show(checking)
|
|
|
|
local body, err = self:newClient():getPluginVersion()
|
|
UIManager:close(checking)
|
|
|
|
if not body then
|
|
UIManager:show(InfoMessage:new{
|
|
text = T(_("Could not check for update: %1"), tostring(err or "network error")),
|
|
timeout = 4,
|
|
})
|
|
return
|
|
end
|
|
|
|
self:handleUpdateVersionResponse(body, true, true)
|
|
end
|
|
|
|
function BookOrbit:handleUpdateVersionResponse(body, interactive, prompt_allowed)
|
|
local server_ver = body.serverVersion or "unknown"
|
|
local plugin_latest = body.pluginVersion
|
|
self.settings.update_check_last_at = os.time()
|
|
|
|
if type(plugin_latest) ~= "string" or plugin_latest == "unknown" then
|
|
G_reader_settings:flush()
|
|
if interactive then
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("Could not determine the latest plugin version from the server."),
|
|
timeout = 4,
|
|
})
|
|
end
|
|
return
|
|
end
|
|
|
|
self.settings.update_latest_version = plugin_latest
|
|
G_reader_settings:flush()
|
|
|
|
if BookOrbitUpdater.isNewer(plugin_latest, PLUGIN_VERSION) ~= true then
|
|
if interactive then
|
|
UIManager:show(InfoMessage:new{
|
|
text = T(_("Plugin is up to date (v%1).\nServer: v%2"), PLUGIN_VERSION, server_ver),
|
|
timeout = 4,
|
|
})
|
|
end
|
|
return
|
|
end
|
|
|
|
if not prompt_allowed then
|
|
return
|
|
end
|
|
|
|
if not interactive and self.settings.update_dismissed_version == plugin_latest then
|
|
return
|
|
end
|
|
if not interactive then
|
|
self.settings.update_dismissed_version = plugin_latest
|
|
G_reader_settings:flush()
|
|
end
|
|
|
|
UIManager:show(ConfirmBox:new{
|
|
text = T(_("Update available: v%1 -> v%2\nServer: v%3\n\nDownload and apply the update now?"),
|
|
PLUGIN_VERSION, plugin_latest, server_ver),
|
|
ok_text = _("Update"),
|
|
ok_callback = function()
|
|
self:applyUpdate(plugin_latest)
|
|
end,
|
|
})
|
|
end
|
|
|
|
function BookOrbit:applyUpdate(new_version)
|
|
if self._updating then
|
|
UIManager:show(InfoMessage:new{ text = _("An update is already in progress."), timeout = 3 })
|
|
return
|
|
end
|
|
self._updating = true
|
|
self:_doApplyUpdate(new_version)
|
|
self._updating = false
|
|
end
|
|
|
|
function BookOrbit:_doApplyUpdate(new_version)
|
|
if not self.path then
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("Cannot determine plugin path. Update aborted."),
|
|
timeout = 3,
|
|
})
|
|
return
|
|
end
|
|
|
|
local progress = InfoMessage:new{ text = T(_("Downloading BookOrbit v%1..."), new_version) }
|
|
UIManager:show(progress)
|
|
UIManager:forceRePaint()
|
|
|
|
local ok, err = BookOrbitUpdater.apply(self:newClient(), self.path)
|
|
UIManager:close(progress)
|
|
|
|
if not ok then
|
|
local msg
|
|
if type(err) == "number" and err == 503 then
|
|
msg = _("Update failed: the server does not have the plugin package available.")
|
|
else
|
|
msg = T(_("Update failed: %1"), tostring(err or "unknown error"))
|
|
end
|
|
UIManager:show(InfoMessage:new{ text = msg, timeout = 6 })
|
|
return
|
|
end
|
|
|
|
UIManager:show(ConfirmBox:new{
|
|
text = T(_("BookOrbit v%1 installed. KOReader needs to restart to apply the update."), new_version),
|
|
ok_text = _("Restart now"),
|
|
ok_callback = function()
|
|
-- Exit code 85 triggers an app restart on Kobo and most e-ink platforms.
|
|
-- On other platforms KOReader exits cleanly; reopen it to apply the update.
|
|
UIManager:quit(UIManager.RETURN_CODE_REBOOT or 85)
|
|
end,
|
|
cancel_text = _("Later"),
|
|
})
|
|
end
|
|
|
|
return BookOrbit
|