diff --git a/plugins/kosync.koplugin/KOSyncClient.lua b/plugins/kosync.koplugin/KOSyncClient.lua index 67e9ac8e9..ecf816e23 100644 --- a/plugins/kosync.koplugin/KOSyncClient.lua +++ b/plugins/kosync.koplugin/KOSyncClient.lua @@ -15,6 +15,7 @@ function KOSyncClient:new(o) end function KOSyncClient:init() + require("socket.http").TIMEOUT = 1 local Spore = require("Spore") self.client = Spore.new_from_spec(self.service_spec, { base_url = self.custom_url, @@ -89,7 +90,7 @@ function KOSyncClient:authorize(username, password) return res.status == 200, res.body else DEBUG("err:", res) - return false, res + return false, res.body end end @@ -123,7 +124,7 @@ function KOSyncClient:update_progress( callback(res.status == 200, res.body) else DEBUG("err:", res) - callback(false, res) + callback(false, res.body) end end) self.client:enable("AsyncHTTP", {thread = co}) @@ -153,7 +154,7 @@ function KOSyncClient:get_progress( callback(res.status == 200, res.body) else DEBUG("err:", res) - callback(false, res) + callback(false, res.body) end end) self.client:enable("AsyncHTTP", {thread = co}) diff --git a/plugins/kosync.koplugin/main.lua b/plugins/kosync.koplugin/main.lua index 6a25a3563..42fee5637 100644 --- a/plugins/kosync.koplugin/main.lua +++ b/plugins/kosync.koplugin/main.lua @@ -2,7 +2,6 @@ local InputContainer = require("ui/widget/container/inputcontainer") local LoginDialog = require("ui/widget/logindialog") local InfoMessage = require("ui/widget/infomessage") local ConfirmBox = require("ui/widget/confirmbox") -local DocSettings = require("docsettings") local NetworkMgr = require("ui/network/manager") local UIManager = require("ui/uimanager") local Screen = require("device").screen @@ -22,21 +21,62 @@ end local KOSync = InputContainer:new{ name = "kosync", title = _("Register/login to KOReader server"), + + page_update_times = 0, + last_page = -1, + last_page_turn_ticks = 0, +} + +local SYNC_STRATEGY = { + -- Forward and backward whisper sync settings are using different + -- default value, so none of following opinions should be zero. + PROMPT = 1, + WHISPER = 2, + DISABLE = 3, + + DEFAULT_FORWARD = 1, + DEFAULT_BACKWARD = 3, } -function KOSync:init() +local function roundPercent(percent) + return math.floor(percent * 10000) / 10000 +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 + +function KOSync:onReaderReady() local settings = G_reader_settings:readSetting("kosync") or {} self.kosync_custom_server = settings.custom_server self.kosync_username = settings.username self.kosync_userkey = settings.userkey self.kosync_auto_sync = not (settings.auto_sync == false) + self.kosync_whisper_forward = settings.whisper_forward or SYNC_STRATEGY.DEFAULT_FORWARD + self.kosync_whisper_backward = settings.whisper_backward or SYNC_STRATEGY.DEFAULT_BACKWARD self.kosync_device_id = G_reader_settings:readSetting("device_id") --assert(self.kosync_device_id) - self.ui:registerPostInitCallback(function() - if self.kosync_auto_sync then - UIManager:scheduleIn(1, function() self:getProgress() end) - end - end) + if self.kosync_auto_sync then + self:_onResume() + end + self:registerEvents() self.ui.menu:registerToMainMenu(self) -- Make sure checksum has been calculated at the very first time a document has been opened, to -- avoid document saving feature to impact the checksum, and eventually impact the document @@ -64,6 +104,7 @@ function KOSync:addToMainMenu(tab_item_table) checked_func = function() return self.kosync_auto_sync end, callback = function() self.kosync_auto_sync = not self.kosync_auto_sync + self:registerEvents() if self.kosync_auto_sync then -- since we will update the progress when closing document, we should pull -- current progress now to avoid to overwrite it silently. @@ -75,6 +116,74 @@ function KOSync:addToMainMenu(tab_item_table) end end, }, + { + text = _("Whisper sync"), + enabled_func = function() return self.kosync_auto_sync end, + sub_item_table = { + { + text = "Sync to latest record >>>>", + enabled = false, + }, + { + text = _(" Auto"), + checked_func = function() + return self.kosync_whisper_forward == SYNC_STRATEGY.WHISPER + end, + callback = function() + self.kosync_whisper_forward = SYNC_STRATEGY.WHISPER + end, + }, + { + text = _(" Prompt"), + checked_func = function() + return self.kosync_whisper_forward == SYNC_STRATEGY.PROMPT + end, + callback = function() + self.kosync_whisper_forward = SYNC_STRATEGY.PROMPT + end, + }, + { + text = _(" Disable"), + checked_func = function() + return self.kosync_whisper_forward == SYNC_STRATEGY.DISABLE + end, + callback = function() + self.kosync_whisper_forward = SYNC_STRATEGY.DISABLE + end, + }, + { + text = "Sync to a previous record <<<<", + enabled = false, + }, + { + text = _(" Auto"), + checked_func = function() + return self.kosync_whisper_backward == SYNC_STRATEGY.WHISPER + end, + callback = function() + self.kosync_whisper_backward = SYNC_STRATEGY.WHISPER + end, + }, + { + text = _(" Prompt"), + checked_func = function() + return self.kosync_whisper_backward == SYNC_STRATEGY.PROMPT + end, + callback = function() + self.kosync_whisper_backward = SYNC_STRATEGY.PROMPT + end, + }, + { + text = _(" Disable"), + checked_func = function() + return self.kosync_whisper_backward == SYNC_STRATEGY.DISABLE + end, + callback = function() + self.kosync_whisper_backward = SYNC_STRATEGY.DISABLE + end, + }, + }, + }, { text = _("Push progress from this device"), enabled_func = function() @@ -189,23 +298,27 @@ function KOSync:doRegister(username, password) } local userkey = md5.sum(password) local ok, status, body = pcall(client.register, client, username, userkey) - if not ok and status then - UIManager:show(InfoMessage:new{ - text = _("An error occurred while registering:") .. - "\n" .. status, - }) - elseif ok then + if not ok then if status then - self.kosync_username = username - self.kosync_userkey = userkey UIManager:show(InfoMessage:new{ - text = _("Registered to KOReader server."), + text = _("An error occurred while registering:") .. + "\n" .. status, }) else UIManager:show(InfoMessage:new{ - text = _(body and body.message or "Unknown server error"), + text = _("An unknown error occurred while registering."), }) end + elseif status then + self.kosync_username = username + self.kosync_userkey = userkey + UIManager:show(InfoMessage:new{ + text = _("Registered to KOReader server."), + }) + else + UIManager:show(InfoMessage:new{ + text = _(body and body.message or "Unknown server error"), + }) end self:onSaveSettings() @@ -219,23 +332,28 @@ function KOSync:doLogin(username, password) } local userkey = md5.sum(password) local ok, status, body = pcall(client.authorize, client, username, userkey) - if not ok and status then - UIManager:show(InfoMessage:new{ - text = _("An error occurred while logging in:") .. - "\n" .. status, - }) - elseif ok then + if not ok then if status then - self.kosync_username = username - self.kosync_userkey = userkey UIManager:show(InfoMessage:new{ - text = _("Logged in to KOReader server."), + text = _("An error occurred while logging in:") .. + "\n" .. status, }) else UIManager:show(InfoMessage:new{ - text = _(body and body.message or "Unknown server error"), + text = _("An unknown error occurred while logging in."), }) end + return + elseif status then + self.kosync_username = username + self.kosync_userkey = userkey + 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 self:onSaveSettings() @@ -247,10 +365,6 @@ function KOSync:logout() self:onSaveSettings() end -local function roundPercent(percent) - return math.floor(percent * 10000) / 10000 -end - function KOSync:getLastPercent() if self.ui.document.info.has_pages then return roundPercent(self.ui.paging:getLastPercent()) @@ -276,120 +390,165 @@ function KOSync:syncToProgress(progress) end 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 - function KOSync:updateProgress(manual) - if self.kosync_username and self.kosync_userkey then - local KOSyncClient = require("KOSyncClient") - local client = KOSyncClient:new{ - custom_url = self.kosync_custom_server, - service_spec = self.path .. "/api.json" - } - local doc_digest = self.view.document:fastDigest() - local progress = self:getLastProgress() - local percentage = self:getLastPercent() - local ok, err = pcall(client.update_progress, - client, - self.kosync_username, - self.kosync_userkey, - doc_digest, - progress, - percentage, - DeviceModel, - self.kosync_device_id, - function(ok, body) - DEBUG("update progress for", self.view.document.file, ok) - if manual 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 manual then showSyncError() end - if err then DEBUG("err:", err) end + if not self.kosync_username or not self.kosync_userkey then + if manual then + promptLogin() end - elseif manual then - promptLogin() + return + end + + local KOSyncClient = require("KOSyncClient") + local client = KOSyncClient:new{ + custom_url = self.kosync_custom_server, + service_spec = self.path .. "/api.json" + } + local doc_digest = self.view.document:fastDigest() + local progress = self:getLastProgress() + local percentage = self:getLastPercent() + local ok, err = pcall(client.update_progress, + client, + self.kosync_username, + self.kosync_userkey, + doc_digest, + progress, + percentage, + DeviceModel, + self.kosync_device_id, + function(ok, body) + DEBUG("update progress for", self.view.document.file, ok) + if manual 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 manual then showSyncError() end + if err then DEBUG("err:", err) end end end function KOSync:getProgress(manual) - if self.kosync_username and self.kosync_userkey then - local KOSyncClient = require("KOSyncClient") - local client = KOSyncClient:new{ - custom_url = self.kosync_custom_server, - service_spec = self.path .. "/api.json" - } - local doc_digest = self.view.document:fastDigest() - local ok, err = pcall(client.get_progress, - client, - self.kosync_username, - self.kosync_userkey, - doc_digest, - function(ok, body) - DEBUG("get progress for", self.view.document.file, ok, body) - if body then - if body.percentage then - if body.device ~= DeviceModel - or body.device_id ~= self.kosync_device_id then - body.percentage = roundPercent(body.percentage) - local progress = self:getLastProgress() - local percentage = self:getLastPercent() - DEBUG("current progress", percentage) - if body.percentage > percentage and body.progress ~= progress then - UIManager:show(ConfirmBox:new{ - text = T(_("Sync to furthest location read (%1%) from device '%2'?"), - Math.round(body.percentage*100), body.device), - ok_callback = function() - self:syncToProgress(body.progress) - end, - }) - elseif manual then - UIManager:show(InfoMessage:new{ - text = _("Already synchronized."), - timeout = 3, - }) - end - elseif manual then - UIManager:show(InfoMessage:new{ - text = _("Latest progress is coming from this device."), - timeout = 3, - }) - end - elseif manual then - UIManager:show(InfoMessage:new{ - text = _("No progress found for this document."), - timeout = 3, - }) - end - elseif manual then + if not self.kosync_username or not self.kosync_userkey then + if manual then + promptLogin() + end + return + end + + local KOSyncClient = require("KOSyncClient") + local client = KOSyncClient:new{ + custom_url = self.kosync_custom_server, + service_spec = self.path .. "/api.json" + } + local doc_digest = self.view.document:fastDigest() + local ok, err = pcall(client.get_progress, + client, + self.kosync_username, + self.kosync_userkey, + doc_digest, + function(ok, body) + DEBUG("get progress for", self.view.document.file, ok, body) + if not ok or not body then + if manual then showSyncError() end - end) - if not ok then - if manual then showSyncError() end - if err then DEBUG("err:", err) end - end - elseif manual then - promptLogin() + return + end + + if not body.percentage then + if manual then + UIManager:show(InfoMessage:new{ + text = _("No progress found for this document."), + timeout = 3, + }) + end + return + end + + if body.device == DeviceModel + and body.device_id == self.kosync_device_id then + if manual then + UIManager:show(InfoMessage:new{ + text = _("Latest progress is coming from this device."), + timeout = 3, + }) + end + return + end + + body.percentage = roundPercent(body.percentage) + local progress = self:getLastProgress() + local percentage = self:getLastPercent() + DEBUG("current progress", percentage) + + if percentage == body.percentage + or body.progress == progress then + if manual then + UIManager:show(InfoMessage:new{ + text = _("The progress has already been synchronized."), + timeout = 3, + }) + end + return + end + + -- The progress needs to be updated. + if manual 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_ticks) + else + -- If we are working with old sync server, we can only use + -- percentage field. + self_older = (body.percentage > percentage) + end + if self_older then + if self.kosync_whisper_forward == SYNC_STRATEGY.WHISPER then + self:syncToProgress(body.progress) + showSyncedMessage() + elseif self.kosync_whisper_forward == SYNC_STRATEGY.PROMPT then + UIManager:show(ConfirmBox:new{ + text = T(_("Sync to the latest record %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.kosync_whisper_backward == SYNC_STRATEGY.WHISPER then + self:syncToProgress(body.progress) + showSyncedMessage() + elseif self.kosync_whisper_backward == SYNC_STRATEGY.PROMPT then + UIManager:show(ConfirmBox:new{ + text = T(_("Sync to a previous record %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 manual then showSyncError() end + if err then DEBUG("err:", err) end end end @@ -399,6 +558,14 @@ function KOSync:onSaveSettings() username = self.kosync_username, userkey = self.kosync_userkey, auto_sync = self.kosync_auto_sync, + whisper_forward = + (self.kosync_whisper_forward == SYNC_STRATEGY.DEFAULT_FORWARD + and nil + or self.kosync_whisper_forward), + whisper_backward = + (self.kosync_whisper_backward == SYNC_STRATEGY.DEFAULT_BACKWARD + and nil + or self.kosync_whisper_backward), } G_reader_settings:saveSetting("kosync", settings) end @@ -410,4 +577,38 @@ function KOSync:onCloseDocument() end end +function KOSync:_onPageUpdate(page) + if page == nil then + return + end + + if self.last_page == -1 then + self.last_page = page + elseif self.last_page ~= page then + self.last_page = page + self.last_page_turn_ticks = os.time() + self.page_update_times = self.page_update_times + 1 + if DAUTO_SAVE_PAGING_COUNT ~= nil + and (DAUTO_SAVE_PAGING_COUNT <= 0 + or self.page_update_times == DAUTO_SAVE_PAGING_COUNT) then + self.page_update_times = 0 + UIManager:scheduleIn(1, function() self:updateProgress() end) + end + end +end + +function KOSync:_onResume() + UIManager:scheduleIn(1, function() self:getProgress() end) +end + +function KOSync:registerEvents() + if self.kosync_auto_sync then + self.onPageUpdate = self._onPageUpdate + self.onResume = self._onResume + else + self.onPageUpdate = nil + self.onResume = nil + end +end + return KOSync diff --git a/spec/unit/kosync_spec.lua b/spec/unit/kosync_spec.lua index 2ad3034cc..72acb1624 100644 --- a/spec/unit/kosync_spec.lua +++ b/spec/unit/kosync_spec.lua @@ -173,4 +173,40 @@ describe("KOSync modules #notest #nocov", function() DEBUG("Please retry later", res) end end) + + -- The response of mockKOSyncClient + local res = { + result = false, + body = {} + } + + -- TODO: Test kosync module + local function mockKOSyncClient() + package.loaded["KOSyncClient"] = nil + local c = require("KOSyncClient") + c.new = function(o) + local o = o or {} + setmetatable(o, self) + self.__index = self + return o + end + + c.init = function() end + + c.register = function(name, passwd) + return res.result, res.body + end + + c.authorize = function(name, passwd) + return res.result, res.body + end + + c.update_progress = function(name, passwd, doc, prog, percent, device, device_id, cb) + cb(res.result, res.body) + end + + c.get_progress = function(name, passwd, doc, cb) + cb(res.result, res.body) + end + end end)