koreader/frontend/apps/reader/modules/readerlink.lua
poire-z 97d34c5bb0 Better handling of tap on invalid/external links
Tap on external urls or invalid links would jump to
document's 1st page. We check first the validity of the
link, and deal with wikipedia links or just show the external
url.
ConfirmBox: allow dismissing/cancelling by taping outside,
to keep UX consistent.
2017-01-18 09:03:31 -08:00

303 lines
11 KiB
Lua

local InputContainer = require("ui/widget/container/inputcontainer")
local GestureRange = require("ui/gesturerange")
local LinkBox = require("ui/widget/linkbox")
local UIManager = require("ui/uimanager")
local Geom = require("ui/geometry")
local Screen = require("device").screen
local Device = require("device")
local logger = require("logger")
local Event = require("ui/event")
local _ = require("gettext")
local T = require("ffi/util").template
local ReaderLink = InputContainer:new{
location_stack = {}
}
function ReaderLink:init()
if Device:isTouchDevice() then
self:initGesListener()
end
self.ui:registerPostInitCallback(function()
self.ui.menu:registerToMainMenu(self)
end)
end
function ReaderLink:onReadSettings(config)
-- called when loading new document
self.location_stack = {}
end
function ReaderLink:initGesListener()
if Device:isTouchDevice() then
self.ges_events = {
Tap = {
GestureRange:new{
ges = "tap",
range = Geom:new{
x = 0, y = 0,
w = Screen:getWidth(),
h = Screen:getHeight()
}
}
},
Swipe = {
GestureRange:new{
ges = "swipe",
range = Geom:new{
x = 0, y = 0,
w = Screen:getWidth(),
h = Screen:getHeight(),
}
}
},
}
end
end
local function isFollowLinksOn()
return G_reader_settings:readSetting("follow_links") ~= false
end
local function isSwipeToGoBackEnabled()
return G_reader_settings:readSetting("swipe_to_go_back") == true
end
local function isSwipeToFollowFirstLinkEnabled()
return G_reader_settings:readSetting("swipe_to_follow_first_link") == true
end
function ReaderLink:addToMainMenu(tab_item_table)
-- insert table to main reader menu
table.insert(tab_item_table.navi, {
text = _("Follow links"),
sub_item_table = {
{
text_func = function()
return isFollowLinksOn() and _("Disable") or _("Enable")
end,
callback = function()
G_reader_settings:saveSetting("follow_links",
not isFollowLinksOn())
end
},
{
text = _("Go back"),
enabled_func = function() return #self.location_stack > 0 end,
callback = function() self:onGoBackLink() end,
},
{
text = _("Swipe to go back"),
checked_func = isSwipeToGoBackEnabled,
callback = function()
G_reader_settings:saveSetting("swipe_to_go_back",
not isSwipeToGoBackEnabled())
end,
},
{
text = _("Swipe to follow first link"),
checked_func = isSwipeToFollowFirstLinkEnabled,
callback = function()
G_reader_settings:saveSetting("swipe_to_follow_first_link",
not isSwipeToFollowFirstLinkEnabled())
end,
},
}
})
end
function ReaderLink:onSetDimensions(dimen)
-- update listening according to new screen dimen
if Device:isTouchDevice() then
self:initGesListener()
end
end
function ReaderLink:onTap(_, ges)
if not isFollowLinksOn() then return end
if self.ui.document.info.has_pages then
local pos = self.view:screenToPageTransform(ges.pos)
if pos then
-- link box in native page
local link, lbox = self.ui.document:getLinkFromPosition(pos.page, pos)
if link and lbox then
-- screen box that holds the link
local sbox = self.view:pageToScreenTransform(pos.page,
self.ui.document:nativeToPageRectTransform(pos.page, lbox))
if sbox then
UIManager:show(LinkBox:new{
box = sbox,
timeout = FOLLOW_LINK_TIMEOUT,
callback = function() self:onGotoLink(link) end
})
return true
end
end
end
else
local link = self.ui.document:getLinkFromPosition(ges.pos)
if link ~= "" then
return self:onGotoLink(link)
end
end
end
function ReaderLink:onGotoLink(link)
logger.dbg("onGotoLink:", link)
if self.ui.document.info.has_pages then
-- internal pdf links have a "page" attribute, while external ones have an "uri" attribute
if link.page then -- Internal link
logger.dbg("Internal link:", link)
table.insert(self.location_stack, self.ui.paging:getBookLocation())
self.ui:handleEvent(Event:new("GotoPage", link.page + 1))
return true
end
link = link.uri -- external link
else
-- For crengine, internal links may look like :
-- #_doc_fragment_0_Organisation (link from anchor)
-- /body/DocFragment/body/ul[2]/li[5]/text()[3].16 (xpointer from full-text search)
-- If the XPointer does not exist (or is a full url), we will jump to page 1
-- Best to check that this link exists in document with the following,
-- which accepts both of the above legitimate xpointer as input.
if self.ui.document:isXPointerInDocument(link) then
logger.dbg("Internal link:", link)
table.insert(self.location_stack, self.ui.rolling:getBookLocation())
self.ui:handleEvent(Event:new("GotoXPointer", link))
return true
end
end
logger.dbg("External link:", link)
-- Check if it is a wikipedia link
local wiki_lang, wiki_page = link:match([[https?://([^%.]+).wikipedia.org/wiki/([^/]+)]])
if wiki_lang and wiki_page then
logger.dbg("Wikipedia link:", wiki_lang, wiki_page)
-- Ask for user confirmation before launching lookup (on a
-- wikipedia page saved as epub, full of wikipedia links, it's
-- too easy to click on links when wanting to change page...)
local ConfirmBox = require("ui/widget/confirmbox")
UIManager:show(ConfirmBox:new{
text = T(_("Would you like to read this Wikipedia %1 full page?\n\n%2\n"), wiki_lang:upper(), wiki_page:gsub("_", " ")),
cancel_on_tap_outside = true,
ok_callback = function()
UIManager:nextTick(function()
self.ui:handleEvent(Event:new("LookupWikipedia", wiki_page, false, true, wiki_lang))
end)
end
})
else
-- local Notification = require("ui/widget/notification")
local InfoMessage = require("ui/widget/infomessage")
UIManager:show(InfoMessage:new{
text = T(_("Invalid or external link:\n%1"), link),
timeout = 1.0,
})
end
-- don't propagate, user will notice and tap elsewhere if he wants to change page
return true
end
function ReaderLink:onGoBackLink()
local saved_location = table.remove(self.location_stack)
if saved_location then
self.ui:handleEvent(Event:new('RestoreBookLocation', saved_location))
return true
end
end
function ReaderLink:onSwipe(_, ges)
if ges.direction == "east" then
if isSwipeToGoBackEnabled() then
return self:onGoBackLink()
end
elseif ges.direction == "west" then
if isSwipeToFollowFirstLinkEnabled() then
return self:onGoToFirstLink(ges)
end
end
end
function ReaderLink:onGoToFirstLink(ges)
if not isFollowLinksOn() then return end
local firstlink = nil
if self.ui.document.info.has_pages then
local pos = self.view:screenToPageTransform(ges.pos)
if not pos then
return
end
local links = self.ui.document:getPageLinks(pos.page)
if #links == 0 then
return
end
-- DEBUG("PDF Page links : ", links)
-- We may get multiple links: internal ones (with "page" key)
-- that we're interested in, but also external links (no "page", but
-- a "uri" key) that we don't care about.
-- [2] = {
-- ["y1"] = 107.88977050781,
-- ["x1"] = 176.60360717773,
-- ["y0"] = 97.944396972656,
-- ["x0"] = 97,
-- ["page"] = 347
-- },
-- Links may not be in the order they are in the page, so let's
-- find the one with the smallest y0.
local first_y0 = nil
for _, link in ipairs(links) do
if link["page"] then
if first_y0 == nil or link["y0"] < first_y0 then
-- onGotoLink()'s GotoPage event needs the link
-- itself, and will use its "page" value
firstlink = link
first_y0 = link["y0"]
end
end
end
else
local links = self.ui.document:getPageLinks()
if #links == 0 then
return
end
-- DEBUG("CRE Page links : ", links)
-- We may get multiple links: internal ones (they have a "section" key)
-- that we're interested in, but also external links (no "section", but
-- a "uri" key) that we don't care about.
-- [1] = {
-- ["end_x"] = 825,
-- ["uri"] = "",
-- ["end_y"] = 333511,
-- ["start_x"] = 90,
-- ["start_y"] = 333511
-- },
-- [2] = {
-- ["end_x"] = 366,
-- ["section"] = "#_doc_fragment_19_ftn_fn6",
-- ["end_y"] = 1201,
-- ["start_x"] = 352,
-- ["start_y"] = 1201
-- },
-- links may not be in the order they are in the page, so let's
-- find the one with the smallest start_y.
local first_start_y = nil
for _, link in ipairs(links) do
if link["section"] then
if first_start_y == nil or link["start_y"] < first_start_y then
-- onGotoLink()'s GotoXPointer event needs
-- the "section" value
firstlink = link["section"]
first_start_y = link["start_y"]
end
end
end
-- cre.cpp getPageLinks() does highlight found links :
-- sel.add( new ldomXRange(*links[i]) ); // highlight
-- and we'll find them highlighted when back from link.
-- So let's clear them now.
self.ui.document:clearSelection()
end
if firstlink then
return self:onGotoLink(firstlink)
end
end
return ReaderLink