mirror of
https://github.com/koreader/koreader.git
synced 2025-08-10 00:52:38 +00:00
* Enable before_wifi_action & after_wifi_action on hasWifiToggle platforms (which is basically all of 'em except naked SDL). * Decouple restoreWifiAsync from hasWifiManger, because we can do that on other platforms (namely, Kindle. Probably PB, too, but WiFi is already a mess there, and I can't test it). * Implement restoreWifiAsync on Kindle. * Properly flag rM as hasWifiManager & hasFastWifiStatusQuery, because it is actually both of those (it uses our wpa_supplicant backend). * Update the KOSync checks to take these changes into account, to properly disable auto_sync if necessary. * Really made the Network* event signaling consistent. For realz this time. * In an effort to make the whole beforeWifiAction framework somewhat usable there, we now assume connectivity is always available on !hasWifiToggle platforms...
934 lines
34 KiB
Lua
934 lines
34 KiB
Lua
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 Math = require("optmath")
|
|
local MultiInputDialog = require("ui/widget/multiinputdialog")
|
|
local NetworkMgr = require("ui/network/manager")
|
|
local UIManager = require("ui/uimanager")
|
|
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
|
local logger = require("logger")
|
|
local md5 = require("ffi/sha2").md5
|
|
local random = require("random")
|
|
local time = require("ui/time")
|
|
local util = require("util")
|
|
local T = require("ffi/util").template
|
|
local _ = require("gettext")
|
|
|
|
if G_reader_settings:hasNot("device_id") then
|
|
G_reader_settings:saveSetting("device_id", random.uuid())
|
|
end
|
|
|
|
local KOSync = WidgetContainer:extend{
|
|
name = "kosync",
|
|
is_doc_only = true,
|
|
title = _("Register/login to KOReader server"),
|
|
|
|
push_timestamp = nil,
|
|
pull_timestamp = nil,
|
|
page_update_counter = nil,
|
|
last_page = nil,
|
|
last_page_turn_timestamp = nil,
|
|
periodic_push_task = nil,
|
|
periodic_push_scheduled = nil,
|
|
|
|
settings = nil,
|
|
}
|
|
|
|
local SYNC_STRATEGY = {
|
|
PROMPT = 1,
|
|
SILENT = 2,
|
|
DISABLE = 3,
|
|
}
|
|
|
|
local CHECKSUM_METHOD = {
|
|
BINARY = 0,
|
|
FILENAME = 1
|
|
}
|
|
|
|
-- Debounce push/pull attempts
|
|
local API_CALL_DEBOUNCE_DELAY = time.s(25)
|
|
|
|
-- NOTE: This is used in a migration script by ui/data/onetime_migration,
|
|
-- which is why it's public.
|
|
KOSync.default_settings = {
|
|
custom_server = nil,
|
|
username = nil,
|
|
userkey = nil,
|
|
-- Do *not* default to auto-sync, as wifi may not be on at all times, and the nagging enabling this may cause requires careful consideration.
|
|
auto_sync = false,
|
|
pages_before_update = nil,
|
|
sync_forward = SYNC_STRATEGY.PROMPT,
|
|
sync_backward = SYNC_STRATEGY.DISABLE,
|
|
checksum_method = CHECKSUM_METHOD.BINARY,
|
|
}
|
|
|
|
function KOSync:init()
|
|
self.push_timestamp = 0
|
|
self.pull_timestamp = 0
|
|
self.page_update_counter = 0
|
|
self.last_page = -1
|
|
self.last_page_turn_timestamp = 0
|
|
self.periodic_push_scheduled = false
|
|
|
|
-- Like AutoSuspend, we need an instance-specific task for scheduling/resource management reasons.
|
|
self.periodic_push_task = function()
|
|
self.periodic_push_scheduled = false
|
|
self.page_update_counter = 0
|
|
-- We do *NOT* want to make sure networking is up here, as the nagging would be extremely annoying; we're leaving that to the network activity check...
|
|
self:updateProgress(false, false)
|
|
end
|
|
|
|
self.settings = G_reader_settings:readSetting("kosync", self.default_settings)
|
|
self.device_id = G_reader_settings:readSetting("device_id")
|
|
|
|
-- Disable auto-sync if beforeWifiAction was reset to "prompt" behind our back...
|
|
if self.settings.auto_sync and Device:hasWifiToggle() and G_reader_settings:readSetting("wifi_enable_action") ~= "turn_on" then
|
|
self.settings.auto_sync = false
|
|
logger.warn("KOSync: Automatic sync has been disabled because wifi_enable_action is *not* turn_on")
|
|
end
|
|
|
|
self.ui.menu:registerToMainMenu(self)
|
|
end
|
|
|
|
function KOSync: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
|
|
else
|
|
return _("Never")
|
|
end
|
|
end
|
|
|
|
local function getNameStrategy(type)
|
|
if type == 1 then
|
|
return _("Prompt")
|
|
elseif type == 2 then
|
|
return _("Auto")
|
|
else
|
|
return _("Disable")
|
|
end
|
|
end
|
|
|
|
local function showSyncedMessage()
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("Progress has been synchronized."),
|
|
timeout = 3,
|
|
})
|
|
end
|
|
|
|
local function promptLogin()
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("Please register or login before using the progress synchronization feature."),
|
|
timeout = 3,
|
|
})
|
|
end
|
|
|
|
local function showSyncError()
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("Something went wrong when syncing progress, please check your network connection and try again later."),
|
|
timeout = 3,
|
|
})
|
|
end
|
|
|
|
local function validate(entry)
|
|
if not entry then return false end
|
|
if type(entry) == "string" then
|
|
if entry == "" or not entry:match("%S") then return false end
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function validateUser(user, pass)
|
|
local error_message = nil
|
|
local user_ok = validate(user)
|
|
local pass_ok = validate(pass)
|
|
if not user_ok and not pass_ok then
|
|
error_message = _("invalid username and password")
|
|
elseif not user_ok then
|
|
error_message = _("invalid username")
|
|
elseif not pass_ok then
|
|
error_message = _("invalid password")
|
|
end
|
|
|
|
if not error_message then
|
|
return user_ok and pass_ok
|
|
else
|
|
return user_ok and pass_ok, error_message
|
|
end
|
|
end
|
|
|
|
function KOSync:onDispatcherRegisterActions()
|
|
Dispatcher:registerAction("kosync_push_progress", { category="none", event="KOSyncPushProgress", title=_("Push progress from this device"), reader=true,})
|
|
Dispatcher:registerAction("kosync_pull_progress", { category="none", event="KOSyncPullProgress", title=_("Pull progress from other devices"), reader=true, separator=true,})
|
|
end
|
|
|
|
function KOSync:onReaderReady()
|
|
-- Make sure checksum has been calculated before we ever query it,
|
|
-- to prevent document saving features from affecting the checksum,
|
|
-- and eventually affecting the document identity for the progress sync feature.
|
|
self.view.document:fastDigest(self.ui.doc_settings)
|
|
|
|
if self.settings.auto_sync then
|
|
UIManager:nextTick(function()
|
|
self:getProgress(true, false)
|
|
end)
|
|
end
|
|
self:registerEvents()
|
|
self:onDispatcherRegisterActions()
|
|
|
|
self.last_page = self.ui:getCurrentPage()
|
|
end
|
|
|
|
function KOSync:addToMainMenu(menu_items)
|
|
menu_items.progress_sync = {
|
|
text = _("Progress sync"),
|
|
sub_item_table = {
|
|
{
|
|
text = _("Custom sync server"),
|
|
keep_menu_open = true,
|
|
tap_input_func = function()
|
|
return {
|
|
-- @translators Server address defined by user for progress sync.
|
|
title = _("Custom progress sync server address"),
|
|
input = self.settings.custom_server or "https://",
|
|
type = "text",
|
|
callback = function(input)
|
|
self:setCustomServer(input)
|
|
end,
|
|
}
|
|
end,
|
|
},
|
|
{
|
|
text_func = function()
|
|
return self.settings.userkey and (_("Logout"))
|
|
or _("Register") .. " / " .. _("Login")
|
|
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,
|
|
separator = true,
|
|
},
|
|
{
|
|
text = _("Automatically keep documents in sync"),
|
|
checked_func = function() return self.settings.auto_sync end,
|
|
help_text = _([[This may lead to nagging about toggling WiFi on document close and suspend/resume, depending on the device's connectivity.]]),
|
|
callback = function()
|
|
-- Actively recommend switching the before wifi action to "turn_on" instead of prompt, as prompt will just not be practical (or even plain usable) here.
|
|
if Device:hasWifiToggle() and G_reader_settings:readSetting("wifi_enable_action") ~= "turn_on" then
|
|
UIManager:show(InfoMessage:new{ text = _("You will have to switch the 'Action when Wi-Fi is off' Network setting to 'turn on' to be able to enable this feature!") })
|
|
return
|
|
end
|
|
|
|
self.settings.auto_sync = not self.settings.auto_sync
|
|
self:registerEvents()
|
|
if self.settings.auto_sync then
|
|
-- Since we will update the progress when closing the document,
|
|
-- pull the current progress now so as not to silently overwrite it.
|
|
self:getProgress(true, true)
|
|
else
|
|
-- Since we won't update the progress when closing the document,
|
|
-- push the current progress now so as not to lose it.
|
|
self:updateProgress(true, true)
|
|
end
|
|
end,
|
|
},
|
|
{
|
|
text_func = function()
|
|
return T(_("Periodically sync every # pages (%1)"), self:getSyncPeriod())
|
|
end,
|
|
enabled_func = function() return self.settings.auto_sync end,
|
|
-- This is the condition that allows enabling auto_disable_wifi in NetworkManager ;).
|
|
help_text = NetworkMgr:getNetworkInterfaceName() and _([[Unlike the automatic sync above, this will *not* attempt to setup a network connection, but instead relies on it being already up, and may trigger enough network activity to passively keep WiFi enabled!]]),
|
|
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 update 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 = 0,
|
|
callback = function(spin)
|
|
self:setPagesBeforeUpdate(spin.value)
|
|
if touchmenu_instance then touchmenu_instance:updateItems() end
|
|
end
|
|
}
|
|
UIManager:show(items)
|
|
end,
|
|
separator = true,
|
|
},
|
|
{
|
|
text = _("Sync behavior"),
|
|
sub_item_table = {
|
|
{
|
|
text_func = function()
|
|
-- NOTE: With an up-to-date Sync server, "forward" means *newer*, not necessarily ahead in the document.
|
|
return T(_("Sync to a newer state (%1)"), getNameStrategy(self.settings.sync_forward))
|
|
end,
|
|
sub_item_table = {
|
|
{
|
|
text = _("Silently"),
|
|
checked_func = function()
|
|
return self.settings.sync_forward == SYNC_STRATEGY.SILENT
|
|
end,
|
|
callback = function()
|
|
self:setSyncForward(SYNC_STRATEGY.SILENT)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Prompt"),
|
|
checked_func = function()
|
|
return self.settings.sync_forward == SYNC_STRATEGY.PROMPT
|
|
end,
|
|
callback = function()
|
|
self:setSyncForward(SYNC_STRATEGY.PROMPT)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Never"),
|
|
checked_func = function()
|
|
return self.settings.sync_forward == SYNC_STRATEGY.DISABLE
|
|
end,
|
|
callback = function()
|
|
self:setSyncForward(SYNC_STRATEGY.DISABLE)
|
|
end,
|
|
},
|
|
}
|
|
},
|
|
{
|
|
text_func = function()
|
|
return T(_("Sync to an older state (%1)"), getNameStrategy(self.settings.sync_backward))
|
|
end,
|
|
sub_item_table = {
|
|
{
|
|
text = _("Silently"),
|
|
checked_func = function()
|
|
return self.settings.sync_backward == SYNC_STRATEGY.SILENT
|
|
end,
|
|
callback = function()
|
|
self:setSyncBackward(SYNC_STRATEGY.SILENT)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Prompt"),
|
|
checked_func = function()
|
|
return self.settings.sync_backward == SYNC_STRATEGY.PROMPT
|
|
end,
|
|
callback = function()
|
|
self:setSyncBackward(SYNC_STRATEGY.PROMPT)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Never"),
|
|
checked_func = function()
|
|
return self.settings.sync_backward == SYNC_STRATEGY.DISABLE
|
|
end,
|
|
callback = function()
|
|
self:setSyncBackward(SYNC_STRATEGY.DISABLE)
|
|
end,
|
|
},
|
|
}
|
|
},
|
|
},
|
|
separator = true,
|
|
},
|
|
{
|
|
text = _("Push progress from this device now"),
|
|
enabled_func = function()
|
|
return self.settings.userkey ~= nil
|
|
end,
|
|
callback = function()
|
|
self:updateProgress(true, true)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Pull progress from other devices now"),
|
|
enabled_func = function()
|
|
return self.settings.userkey ~= nil
|
|
end,
|
|
callback = function()
|
|
self:getProgress(true, true)
|
|
end,
|
|
separator = true,
|
|
},
|
|
{
|
|
text = _("Document matching method"),
|
|
sub_item_table = {
|
|
{
|
|
text = _("Binary. Only identical files will be kept in sync."),
|
|
checked_func = function()
|
|
return self.settings.checksum_method == CHECKSUM_METHOD.BINARY
|
|
end,
|
|
callback = function()
|
|
self:setChecksumMethod(CHECKSUM_METHOD.BINARY)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Filename. Files with matching names will be kept in sync."),
|
|
checked_func = function()
|
|
return self.settings.checksum_method == CHECKSUM_METHOD.FILENAME
|
|
end,
|
|
callback = function()
|
|
self:setChecksumMethod(CHECKSUM_METHOD.FILENAME)
|
|
end,
|
|
},
|
|
}
|
|
},
|
|
}
|
|
}
|
|
end
|
|
|
|
function KOSync:setPagesBeforeUpdate(pages_before_update)
|
|
self.settings.pages_before_update = pages_before_update > 0 and pages_before_update or nil
|
|
end
|
|
|
|
function KOSync:setCustomServer(server)
|
|
logger.dbg("KOSync: Setting custom server to:", server)
|
|
self.settings.custom_server = server ~= "" and server or nil
|
|
end
|
|
|
|
function KOSync:setSyncForward(strategy)
|
|
self.settings.sync_forward = strategy
|
|
end
|
|
|
|
function KOSync:setSyncBackward(strategy)
|
|
self.settings.sync_backward = strategy
|
|
end
|
|
|
|
function KOSync:setChecksumMethod(method)
|
|
self.settings.checksum_method = method
|
|
end
|
|
|
|
function KOSync: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",
|
|
},
|
|
},
|
|
buttons = {
|
|
{
|
|
{
|
|
text = _("Cancel"),
|
|
id = "close",
|
|
callback = function()
|
|
UIManager:close(dialog)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Login"),
|
|
callback = function()
|
|
local username, password = unpack(dialog:getFields())
|
|
local ok, err = validateUser(username, password)
|
|
if not ok then
|
|
UIManager:show(InfoMessage:new{
|
|
text = T(_("Cannot login: %1"), err),
|
|
timeout = 2,
|
|
})
|
|
else
|
|
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
|
|
end,
|
|
},
|
|
{
|
|
text = _("Register"),
|
|
callback = function()
|
|
local username, password = unpack(dialog:getFields())
|
|
local ok, err = validateUser(username, password)
|
|
if not ok then
|
|
UIManager:show(InfoMessage:new{
|
|
text = T(_("Cannot register: %1"), err),
|
|
timeout = 2,
|
|
})
|
|
else
|
|
UIManager:close(dialog)
|
|
UIManager:scheduleIn(0.5, function()
|
|
self:doRegister(username, password, menu)
|
|
end)
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("Registering. Please wait…"),
|
|
timeout = 1,
|
|
})
|
|
end
|
|
end,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
UIManager:show(dialog)
|
|
dialog:onShowKeyboard()
|
|
end
|
|
|
|
function KOSync:doRegister(username, password, menu)
|
|
local KOSyncClient = require("KOSyncClient")
|
|
local client = KOSyncClient:new{
|
|
custom_url = self.settings.custom_server,
|
|
service_spec = self.path .. "/api.json"
|
|
}
|
|
-- on Android to avoid ANR (no-op on other platforms)
|
|
Device:setIgnoreInput(true)
|
|
local userkey = md5(password)
|
|
local ok, status, body = pcall(client.register, client, username, userkey)
|
|
if not ok then
|
|
if status then
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("An error occurred while registering:") ..
|
|
"\n" .. status,
|
|
})
|
|
else
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("An unknown error occurred while registering."),
|
|
})
|
|
end
|
|
elseif status then
|
|
self.settings.username = username
|
|
self.settings.userkey = userkey
|
|
if menu then
|
|
menu:updateItems()
|
|
end
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("Registered to KOReader server."),
|
|
})
|
|
else
|
|
UIManager:show(InfoMessage:new{
|
|
text = body and body.message or _("Unknown server error"),
|
|
})
|
|
end
|
|
Device:setIgnoreInput(false)
|
|
end
|
|
|
|
function KOSync:doLogin(username, password, menu)
|
|
local KOSyncClient = require("KOSyncClient")
|
|
local client = KOSyncClient:new{
|
|
custom_url = self.settings.custom_server,
|
|
service_spec = self.path .. "/api.json"
|
|
}
|
|
Device:setIgnoreInput(true)
|
|
local userkey = md5(password)
|
|
local ok, status, body = pcall(client.authorize, client, username, userkey)
|
|
if not ok then
|
|
if status then
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("An error occurred while logging in:") ..
|
|
"\n" .. status,
|
|
})
|
|
else
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("An unknown error occurred while logging in."),
|
|
})
|
|
end
|
|
Device:setIgnoreInput(false)
|
|
return
|
|
elseif status then
|
|
self.settings.username = username
|
|
self.settings.userkey = userkey
|
|
if menu then
|
|
menu:updateItems()
|
|
end
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("Logged in to KOReader server."),
|
|
})
|
|
else
|
|
UIManager:show(InfoMessage:new{
|
|
text = body and body.message or _("Unknown server error"),
|
|
})
|
|
end
|
|
Device:setIgnoreInput(false)
|
|
end
|
|
|
|
function KOSync:logout(menu)
|
|
self.settings.userkey = nil
|
|
self.settings.auto_sync = true
|
|
if menu then
|
|
menu:updateItems()
|
|
end
|
|
end
|
|
|
|
function KOSync: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 KOSync:getLastProgress()
|
|
if self.ui.document.info.has_pages then
|
|
return self.ui.paging:getLastProgress()
|
|
else
|
|
return self.ui.rolling:getLastProgress()
|
|
end
|
|
end
|
|
|
|
function KOSync:getDocumentDigest()
|
|
if self.settings.checksum_method == CHECKSUM_METHOD.FILENAME then
|
|
return self:getFileNameDigest()
|
|
else
|
|
return self:getFileDigest()
|
|
end
|
|
end
|
|
|
|
function KOSync:getFileDigest()
|
|
return self.ui.doc_settings:readSetting("partial_md5_checksum")
|
|
end
|
|
|
|
function KOSync:getFileNameDigest()
|
|
local file = self.ui.document.file
|
|
if not file then return end
|
|
|
|
local file_path, file_name = util.splitFilePathName(file) -- luacheck: no unused
|
|
if not file_name then return end
|
|
|
|
return md5(file_name)
|
|
end
|
|
|
|
function KOSync:syncToProgress(progress)
|
|
logger.dbg("KOSync: [Sync] progress to", 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 KOSync:updateProgress(ensure_networking, interactive, refresh_on_success)
|
|
if not self.settings.username or not self.settings.userkey then
|
|
if interactive then
|
|
promptLogin()
|
|
end
|
|
return
|
|
end
|
|
|
|
local now = UIManager:getElapsedTimeSinceBoot()
|
|
if not interactive and now - self.push_timestamp <= API_CALL_DEBOUNCE_DELAY then
|
|
logger.dbg("KOSync: We've already pushed progress less than 25s ago!")
|
|
return
|
|
end
|
|
|
|
if ensure_networking and NetworkMgr:willRerunWhenOnline(function() self:updateProgress(ensure_networking, interactive, refresh_on_success) end) then
|
|
return
|
|
end
|
|
|
|
local KOSyncClient = require("KOSyncClient")
|
|
local client = KOSyncClient:new{
|
|
custom_url = self.settings.custom_server,
|
|
service_spec = self.path .. "/api.json"
|
|
}
|
|
local doc_digest = self:getDocumentDigest()
|
|
local progress = self:getLastProgress()
|
|
local percentage = self:getLastPercent()
|
|
local ok, err = pcall(client.update_progress,
|
|
client,
|
|
self.settings.username,
|
|
self.settings.userkey,
|
|
doc_digest,
|
|
progress,
|
|
percentage,
|
|
Device.model,
|
|
self.device_id,
|
|
function(ok, body)
|
|
logger.dbg("KOSync: [Push] progress to", percentage * 100, "% =>", progress, "for", self.view.document.file)
|
|
logger.dbg("KOSync: ok:", ok, "body:", body)
|
|
if interactive then
|
|
if ok then
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("Progress has been pushed."),
|
|
timeout = 3,
|
|
})
|
|
else
|
|
showSyncError()
|
|
end
|
|
end
|
|
end)
|
|
if not ok then
|
|
if interactive then showSyncError() end
|
|
if err then logger.dbg("err:", err) end
|
|
else
|
|
-- This is solely for onSuspend's sake, to clear the ghosting left by the the "Connected" InfoMessage
|
|
if refresh_on_success then
|
|
-- Our top-level widget should be the "Connected to network" InfoMessage from NetworkMgr's reconnectOrShowNetworkMenu
|
|
local widget = UIManager:getTopmostVisibleWidget()
|
|
if widget and widget.modal and widget.tag == "NetworkMgr" and not widget.dismiss_callback then
|
|
-- We want a full-screen flash on dismiss
|
|
widget.dismiss_callback = function()
|
|
-- Enqueued, because we run before the InfoMessage's close
|
|
UIManager:setDirty(nil, "full")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
self.push_timestamp = now
|
|
end
|
|
|
|
function KOSync:getProgress(ensure_networking, interactive)
|
|
if not self.settings.username or not self.settings.userkey then
|
|
if interactive then
|
|
promptLogin()
|
|
end
|
|
return
|
|
end
|
|
|
|
local now = UIManager:getElapsedTimeSinceBoot()
|
|
if not interactive and now - self.pull_timestamp <= API_CALL_DEBOUNCE_DELAY then
|
|
logger.dbg("KOSync: We've already pulled progress less than 25s ago!")
|
|
return
|
|
end
|
|
|
|
if ensure_networking and NetworkMgr:willRerunWhenOnline(function() self:getProgress(ensure_networking, interactive) end) then
|
|
return
|
|
end
|
|
|
|
local KOSyncClient = require("KOSyncClient")
|
|
local client = KOSyncClient:new{
|
|
custom_url = self.settings.custom_server,
|
|
service_spec = self.path .. "/api.json"
|
|
}
|
|
local doc_digest = self:getDocumentDigest()
|
|
local ok, err = pcall(client.get_progress,
|
|
client,
|
|
self.settings.username,
|
|
self.settings.userkey,
|
|
doc_digest,
|
|
function(ok, body)
|
|
logger.dbg("KOSync: [Pull] progress for", self.view.document.file)
|
|
logger.dbg("KOSync: ok:", ok, "body:", body)
|
|
if not ok or not body then
|
|
if interactive then
|
|
showSyncError()
|
|
end
|
|
return
|
|
end
|
|
|
|
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()
|
|
logger.dbg("KOSync: Current progress:", percentage * 100, "% =>", progress)
|
|
|
|
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
|
|
|
|
-- The progress needs to be updated.
|
|
if interactive then
|
|
-- If user actively pulls progress from other devices,
|
|
-- we always update the progress without further confirmation.
|
|
self:syncToProgress(body.progress)
|
|
showSyncedMessage()
|
|
return
|
|
end
|
|
|
|
local self_older
|
|
if body.timestamp ~= nil then
|
|
self_older = (body.timestamp > self.last_page_turn_timestamp)
|
|
else
|
|
-- If we are working with an old sync server, we can only use the percentage field.
|
|
self_older = (body.percentage > percentage)
|
|
end
|
|
if self_older then
|
|
if self.settings.sync_forward == SYNC_STRATEGY.SILENT then
|
|
self:syncToProgress(body.progress)
|
|
showSyncedMessage()
|
|
elseif self.settings.sync_forward == SYNC_STRATEGY.PROMPT then
|
|
UIManager:show(ConfirmBox:new{
|
|
text = T(_("Sync to latest location %1% from device '%2'?"),
|
|
Math.round(body.percentage * 100),
|
|
body.device),
|
|
ok_callback = function()
|
|
self:syncToProgress(body.progress)
|
|
end,
|
|
})
|
|
end
|
|
else -- if not self_older then
|
|
if self.settings.sync_backward == SYNC_STRATEGY.SILENT then
|
|
self:syncToProgress(body.progress)
|
|
showSyncedMessage()
|
|
elseif self.settings.sync_backward == SYNC_STRATEGY.PROMPT then
|
|
UIManager:show(ConfirmBox:new{
|
|
text = T(_("Sync to previous location %1% from device '%2'?"),
|
|
Math.round(body.percentage * 100),
|
|
body.device),
|
|
ok_callback = function()
|
|
self:syncToProgress(body.progress)
|
|
end,
|
|
})
|
|
end
|
|
end
|
|
end)
|
|
if not ok then
|
|
if interactive then showSyncError() end
|
|
if err then logger.dbg("err:", err) end
|
|
end
|
|
|
|
self.pull_timestamp = now
|
|
end
|
|
|
|
function KOSync:_onCloseDocument()
|
|
logger.dbg("KOSync: onCloseDocument")
|
|
-- NOTE: Because we'll lose the document instance on return, we need to *block* until the connection is actually up here,
|
|
-- we cannot rely on willRerunWhenOnline, because if we're not currently online,
|
|
-- it *will* return early, and that means the actual callback *will* run *after* teardown of the document instance
|
|
-- (and quite likely ours, too).
|
|
NetworkMgr:goOnlineToRun(function()
|
|
-- Drop the inner willRerunWhenOnline ;).
|
|
self:updateProgress(false, false)
|
|
end)
|
|
end
|
|
|
|
function KOSync:schedulePeriodicPush()
|
|
UIManager:unschedule(self.periodic_push_task)
|
|
-- Use a sizable delay to make debouncing this on skim feasible...
|
|
UIManager:scheduleIn(10, self.periodic_push_task)
|
|
self.periodic_push_scheduled = true
|
|
end
|
|
|
|
function KOSync:_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
|
|
-- If we've already scheduled a push, regardless of the counter's state, delay it until we're *actually* idle
|
|
if self.periodic_push_scheduled or self.settings.pages_before_update and self.page_update_counter >= self.settings.pages_before_update then
|
|
self:schedulePeriodicPush()
|
|
end
|
|
end
|
|
end
|
|
|
|
function KOSync:_onResume()
|
|
logger.dbg("KOSync: onResume")
|
|
-- If we have auto_restore_wifi enabled, skip this to prevent both the "Connecting..." UI to pop-up,
|
|
-- *and* a duplicate NetworkConnected event from firing...
|
|
if Device:hasWifiRestore() and NetworkMgr.wifi_was_on and G_reader_settings:isTrue("auto_restore_wifi") then
|
|
return
|
|
end
|
|
|
|
-- And if we don't, this *will* (attempt to) trigger a connection and as such a NetworkConnected event,
|
|
-- but only a single pull will happen, since getProgress debounces itself.
|
|
UIManager:scheduleIn(1, function()
|
|
self:getProgress(true, false)
|
|
end)
|
|
end
|
|
|
|
function KOSync:_onSuspend()
|
|
logger.dbg("KOSync: onSuspend")
|
|
-- We request an extra flashing refresh on success, to deal with potential ghosting left by the NetworkMgr UI
|
|
self:updateProgress(true, false, true)
|
|
end
|
|
|
|
function KOSync:_onNetworkConnected()
|
|
logger.dbg("KOSync: onNetworkConnected")
|
|
UIManager:scheduleIn(0.5, function()
|
|
-- Network is supposed to be on already, don't wrap this in willRerunWhenOnline
|
|
self:getProgress(false, false)
|
|
end)
|
|
end
|
|
|
|
function KOSync:_onNetworkDisconnecting()
|
|
logger.dbg("KOSync: onNetworkDisconnecting")
|
|
-- Network is supposed to be on already, don't wrap this in willRerunWhenOnline
|
|
self:updateProgress(false, false)
|
|
end
|
|
|
|
function KOSync:onKOSyncPushProgress()
|
|
self:updateProgress(true, true)
|
|
end
|
|
|
|
function KOSync:onKOSyncPullProgress()
|
|
self:getProgress(true, true)
|
|
end
|
|
|
|
function KOSync: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 KOSync:onCloseWidget()
|
|
UIManager:unschedule(self.periodic_push_task)
|
|
self.periodic_push_task = nil
|
|
end
|
|
|
|
return KOSync
|