2
0
mirror of https://github.com/koreader/koreader synced 2024-10-31 21:20:20 +00:00
koreader/plugins/kosync.koplugin/main.lua
onde2rock e502bf04d3 [feat, UX] Support the virtualKeyboard on non touch-device (#3796)
* [VirtualKeyboard] Add support for keynaviguation

Also rename the variable "layout" to "keyboard_layout" because conflict
with the layout from the focusmanager

* Make the goto dialog compatible with key naviguation

My solution is to change the order of the widget. The last one will the
virtualkeybard so it catch all the keybinding, and below it, make the
dialog "is_always_active = true" so it can receive touch event.

* Correctly show the virtual keyboard on dpad devices

* change the order to call the virtualKeyboard so it end up on top

* Handle the multi input dialog

* Support reopening the virtualKeyboard by the Press key

* add check focusmanager

* Fix https://github.com/koreader/koreader/issues/3797

* MultiInputDialog : Now work on non touch-device

* Set the virtualkeyboard to be a modal widget

* Fix the layout in multiinputwidget

* Fix for the various combination of
hasKeys,hasDpad,isTouchDevice

* [Focusmanager] Better handling of malformed layout
2018-03-30 12:46:36 +02:00

641 lines
21 KiB
Lua

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 NetworkMgr = require("ui/network/manager")
local UIManager = require("ui/uimanager")
local Screen = require("device").screen
local DeviceModel = require("device").model
local Event = require("ui/event")
local Math = require("optmath")
local DEBUG = require("dbg")
local T = require("ffi/util").template
local _ = require("gettext")
local md5 = require("ffi/MD5")
local random = require("random")
if not G_reader_settings:readSetting("device_id") then
G_reader_settings:saveSetting("device_id", random.uuid())
end
local KOSync = InputContainer:new{
name = "kosync",
is_doc_only = true,
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,
}
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)
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
-- identity in the progress sync feature.
self.view.document:fastDigest(self.ui.doc_settings)
end
function KOSync:addToMainMenu(menu_items)
menu_items.progress_sync = {
text = _("Progress sync"),
sub_item_table = {
{
text_func = function()
return self.kosync_userkey and (_("Logout"))
or _("Register") .. " / " .. _("Login")
end,
callback_func = function()
return self.kosync_userkey and
function() self:logout() end or
function() self:login() end
end,
},
{
text = _("Auto sync now and future"),
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.
self:getProgress(true)
else
-- since we won't update the progress when closing document, we should push
-- current progress now to avoid to lose it silently.
self:updateProgress(true)
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:setWhisperForward(SYNC_STRATEGY.WHISPER)
end,
},
{
text = _(" Prompt"),
checked_func = function()
return self.kosync_whisper_forward == SYNC_STRATEGY.PROMPT
end,
callback = function()
self:setWhisperForward(SYNC_STRATEGY.PROMPT)
end,
},
{
text = _(" Disable"),
checked_func = function()
return self.kosync_whisper_forward == SYNC_STRATEGY.DISABLE
end,
callback = function()
self:setWhisperForward(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:setWhisperBackward(SYNC_STRATEGY.WHISPER)
end,
},
{
text = _(" Prompt"),
checked_func = function()
return self.kosync_whisper_backward == SYNC_STRATEGY.PROMPT
end,
callback = function()
self:setWhisperBackward(SYNC_STRATEGY.PROMPT)
end,
},
{
text = _(" Disable"),
checked_func = function()
return self.kosync_whisper_backward == SYNC_STRATEGY.DISABLE
end,
callback = function()
self:setWhisperBackward(SYNC_STRATEGY.DISABLE)
end,
},
},
},
{
text = _("Push progress from this device"),
enabled_func = function()
return self.kosync_userkey ~= nil
end,
callback = function()
self:updateProgress(true)
end,
},
{
text = _("Pull progress from other devices"),
enabled_func = function()
return self.kosync_userkey ~= nil
end,
callback = function()
self:getProgress(true)
end,
},
{
text = _("Custom sync server"),
tap_input_func = function()
return {
title = _("Custom progress sync server address"),
input = self.kosync_custom_server or "https://",
type = "text",
callback = function(input)
self:setCustomServer(input)
end,
}
end,
},
}
}
end
function KOSync:setCustomServer(server)
DEBUG("set custom server", server)
self.kosync_custom_server = server ~= "" and server or nil
self:saveSettings()
end
function KOSync:setWhisperForward(strategy)
self.kosync_whisper_forward = strategy
self:saveSettings()
end
function KOSync:setWhisperBackward(strategy)
self.kosync_whisper_backward = strategy
self:saveSettings()
end
function KOSync:login()
if not NetworkMgr:isOnline() then
NetworkMgr:promptWifiOn()
end
self.login_dialog = LoginDialog:new{
title = self.title,
username = self.kosync_username or "",
buttons = {
{
{
text = _("Cancel"),
enabled = true,
callback = function()
self:closeDialog()
end,
},
{
text = _("Login"),
enabled = true,
callback = function()
local username, password = self:getCredential()
self:closeDialog()
UIManager:scheduleIn(0.5, function()
self:doLogin(username, password)
end)
UIManager:show(InfoMessage:new{
text = _("Logging in. Please wait…"),
timeout = 1,
})
end,
},
{
text = _("Register"),
enabled = true,
callback = function()
local username, password = self:getCredential()
self:closeDialog()
UIManager:scheduleIn(0.5, function()
self:doRegister(username, password)
end)
UIManager:show(InfoMessage:new{
text = _("Registering. Please wait…"),
timeout = 1,
})
end,
},
},
},
width = Screen:getWidth() * 0.8,
height = Screen:getHeight() * 0.4,
}
UIManager:show(self.login_dialog)
self.login_dialog:onShowKeyboard()
end
function KOSync:closeDialog()
self.login_dialog:onClose()
UIManager:close(self.login_dialog)
end
function KOSync:getCredential()
return self.login_dialog:getCredential()
end
function KOSync:doRegister(username, password)
local KOSyncClient = require("KOSyncClient")
local client = KOSyncClient:new{
custom_url = self.kosync_custom_server,
service_spec = self.path .. "/api.json"
}
local userkey = md5.sum(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.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:saveSettings()
end
function KOSync:doLogin(username, password)
local KOSyncClient = require("KOSyncClient")
local client = KOSyncClient:new{
custom_url = self.kosync_custom_server,
service_spec = self.path .. "/api.json"
}
local userkey = md5.sum(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
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:saveSettings()
end
function KOSync:logout()
self.kosync_userkey = nil
self.kosync_auto_sync = true
self:saveSettings()
end
function KOSync:getLastPercent()
if self.ui.document.info.has_pages then
return roundPercent(self.ui.paging:getLastPercent())
else
return 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:syncToProgress(progress)
DEBUG("sync 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(manual)
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 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 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
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 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.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 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 manual then showSyncError() end
if err then DEBUG("err:", err) end
end
end
function KOSync:saveSettings()
local settings = {
custom_server = self.kosync_custom_server,
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
function KOSync:onCloseDocument()
DEBUG("on close document")
if self.kosync_auto_sync then
self:updateProgress()
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:_onFlushSettings()
if self.ui == nil or self.ui.document == nil then return end
self:updateProgress()
end
function KOSync:_onNetworkConnected()
self:_onResume()
end
function KOSync:registerEvents()
if self.kosync_auto_sync then
self.onPageUpdate = self._onPageUpdate
self.onResume = self._onResume
self.onFlushSettings = self._onFlushSettings
self.onNetworkConnected = self._onNetworkConnected
else
self.onPageUpdate = nil
self.onResume = nil
self.onFlushSettings = nil
self.onNetworkConnected = nil
end
end
return KOSync