mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
e97d54953b
When manually syncing a book, the plugin uploaded progress without checking if another device had written a newer progress state to the server first, causing reading progress to be overwritten and lost. Fetch the remote progress from the server before starting a manual sync. If a newer progress state is found on the server, prompt the user to apply the remote progress locally first. If the prompt is rejected, proceed with the sync of annotations and reading history while bypassing the progress upload. Closes #461
1496 lines
52 KiB
Lua
1496 lines
52 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.1.0"
|
|
|
|
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,
|
|
skip_sync_when_offline = 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_mosaic_show_titles = false,
|
|
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()
|
|
if self.settings.skip_sync_when_offline and not NetworkMgr:isOnline() then
|
|
return
|
|
end
|
|
self:getProgress(true, false)
|
|
end)
|
|
if self.settings.annotation_sync then
|
|
UIManager:scheduleIn(2, function()
|
|
if self.settings.skip_sync_when_offline and not NetworkMgr:isOnline() then
|
|
return
|
|
end
|
|
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,
|
|
},
|
|
{
|
|
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,
|
|
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
|
|
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 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,
|
|
path = self.path,
|
|
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
|
|
|
|
local function hasUsableRemoteProgress(progress)
|
|
return progress ~= nil and progress ~= ""
|
|
end
|
|
|
|
function BookOrbit:remoteProgressIsNewer(body, local_percentage)
|
|
local local_timestamp = self.last_page_turn_timestamp or 0
|
|
if body.timestamp ~= nil then
|
|
if local_timestamp > 0 then
|
|
return body.timestamp > local_timestamp
|
|
end
|
|
return body.percentage > local_percentage
|
|
end
|
|
return body.percentage > local_percentage
|
|
end
|
|
|
|
function BookOrbit:applyRemoteProgress(body, on_done)
|
|
self:syncToProgress(body.progress)
|
|
if on_done then
|
|
UIManager:scheduleIn(0.1, function()
|
|
on_done(false)
|
|
end)
|
|
end
|
|
end
|
|
|
|
function BookOrbit:reconcileProgressBeforeBookSync(digest, on_done)
|
|
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 self:getDocumentDigest() ~= digest then
|
|
UIManager:show(InfoMessage:new{ text = _("The open book changed. Start the sync again."), timeout = 3 })
|
|
return
|
|
end
|
|
|
|
local client = self:newClient()
|
|
local body, err = client:getProgress(digest)
|
|
self.pull_timestamp = UIManager:getElapsedTimeSinceBoot()
|
|
|
|
if not body then
|
|
logger.dbg("BookOrbit: progress check before book sync failed:", err)
|
|
UIManager:show(InfoMessage:new{ text = _("Could not check latest progress. Syncing book data without changing progress."), timeout = 4 })
|
|
on_done(true)
|
|
return
|
|
end
|
|
|
|
if not body.percentage then
|
|
on_done(false)
|
|
return
|
|
end
|
|
|
|
if body.device == Device.model and body.device_id == self.device_id then
|
|
on_done(false)
|
|
return
|
|
end
|
|
|
|
body.percentage = Math.roundPercent(body.percentage)
|
|
local local_progress = self:getLastProgress()
|
|
local local_percentage = self:getLastPercent()
|
|
|
|
if local_percentage == body.percentage or body.progress == local_progress then
|
|
on_done(false)
|
|
return
|
|
end
|
|
|
|
local remote_newer = self:remoteProgressIsNewer(body, local_percentage)
|
|
if not hasUsableRemoteProgress(body.progress) then
|
|
if remote_newer then
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("BookOrbit has newer progress but no exact location. Syncing book data without changing progress."),
|
|
timeout = 4,
|
|
})
|
|
on_done(true)
|
|
else
|
|
on_done(false)
|
|
end
|
|
return
|
|
end
|
|
|
|
local strategy = remote_newer and self.settings.sync_forward or self.settings.sync_backward
|
|
if strategy == SYNC_STRATEGY.SILENT then
|
|
self:applyRemoteProgress(body, on_done)
|
|
elseif strategy == SYNC_STRATEGY.PROMPT then
|
|
local template = remote_newer and _("Sync to latest location %1% from device '%2' before uploading this book?")
|
|
or _("Sync to previous location %1% from device '%2' before uploading this book?")
|
|
UIManager:show(ConfirmBox:new{
|
|
text = T(template, Math.round(body.percentage * 100), body.device),
|
|
ok_callback = function()
|
|
self:applyRemoteProgress(body, on_done)
|
|
end,
|
|
cancel_callback = function()
|
|
on_done(remote_newer)
|
|
end,
|
|
})
|
|
elseif remote_newer then
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("BookOrbit has newer progress. Syncing book data without changing progress."),
|
|
timeout = 4,
|
|
})
|
|
on_done(true)
|
|
else
|
|
on_done(false)
|
|
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_book_sync = function(skip_progress)
|
|
local latest_snap = BookOrbitBookSync.capture(self)
|
|
if not latest_snap then
|
|
UIManager:show(InfoMessage:new{ text = _("Could not read this book's data."), timeout = 2 })
|
|
return
|
|
end
|
|
if latest_snap.digest ~= snap.digest then
|
|
UIManager:show(InfoMessage:new{ text = _("The open book changed. Start the sync again."), timeout = 3 })
|
|
return
|
|
end
|
|
BookOrbitBookSync.run{ api = api_opts, snap = latest_snap, reason = "manual", interactive = true, plugin = self,
|
|
annotation_sync = self.settings.annotation_sync, skip_progress = skip_progress }
|
|
end
|
|
local run = function()
|
|
self:reconcileProgressBeforeBookSync(snap.digest, run_book_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.settings.auto_sync and (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()
|
|
self.onPageUpdate = self._onPageUpdate
|
|
if self.settings.auto_sync then
|
|
self.onCloseDocument = self._onCloseDocument
|
|
self.onResume = self._onResume
|
|
self.onSuspend = self._onSuspend
|
|
self.onNetworkConnected = self._onNetworkConnected
|
|
self.onNetworkDisconnecting = self._onNetworkDisconnecting
|
|
else
|
|
self.onCloseDocument = 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
|