mirror of
https://github.com/koreader/koreader
synced 2024-11-10 01:10:34 +00:00
917 lines
32 KiB
Lua
917 lines
32 KiB
Lua
--[[
|
|
ReaderUI is an abstraction for a reader interface.
|
|
|
|
It works using data gathered from a document interface.
|
|
]]--
|
|
|
|
local BD = require("ui/bidi")
|
|
local ConfirmBox = require("ui/widget/confirmbox")
|
|
local Device = require("device")
|
|
local DeviceListener = require("device/devicelistener")
|
|
local DocCache = require("document/doccache")
|
|
local DocSettings = require("docsettings")
|
|
local DocumentRegistry = require("document/documentregistry")
|
|
local Event = require("ui/event")
|
|
local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo")
|
|
local FileManagerCollection = require("apps/filemanager/filemanagercollection")
|
|
local FileManagerHistory = require("apps/filemanager/filemanagerhistory")
|
|
local FileManagerFileSearcher = require("apps/filemanager/filemanagerfilesearcher")
|
|
local FileManagerShortcuts = require("apps/filemanager/filemanagershortcuts")
|
|
local InfoMessage = require("ui/widget/infomessage")
|
|
local InputContainer = require("ui/widget/container/inputcontainer")
|
|
local InputDialog = require("ui/widget/inputdialog")
|
|
local LanguageSupport = require("languagesupport")
|
|
local NetworkListener = require("ui/network/networklistener")
|
|
local Notification = require("ui/widget/notification")
|
|
local PluginLoader = require("pluginloader")
|
|
local ReaderActivityIndicator = require("apps/reader/modules/readeractivityindicator")
|
|
local ReaderAnnotation = require("apps/reader/modules/readerannotation")
|
|
local ReaderBack = require("apps/reader/modules/readerback")
|
|
local ReaderBookmark = require("apps/reader/modules/readerbookmark")
|
|
local ReaderConfig = require("apps/reader/modules/readerconfig")
|
|
local ReaderCoptListener = require("apps/reader/modules/readercoptlistener")
|
|
local ReaderCropping = require("apps/reader/modules/readercropping")
|
|
local ReaderDeviceStatus = require("apps/reader/modules/readerdevicestatus")
|
|
local ReaderDictionary = require("apps/reader/modules/readerdictionary")
|
|
local ReaderFont = require("apps/reader/modules/readerfont")
|
|
local ReaderGoto = require("apps/reader/modules/readergoto")
|
|
local ReaderHandMade = require("apps/reader/modules/readerhandmade")
|
|
local ReaderHinting = require("apps/reader/modules/readerhinting")
|
|
local ReaderHighlight = require("apps/reader/modules/readerhighlight")
|
|
local ReaderScrolling = require("apps/reader/modules/readerscrolling")
|
|
local ReaderKoptListener = require("apps/reader/modules/readerkoptlistener")
|
|
local ReaderLink = require("apps/reader/modules/readerlink")
|
|
local ReaderMenu = require("apps/reader/modules/readermenu")
|
|
local ReaderPageMap = require("apps/reader/modules/readerpagemap")
|
|
local ReaderPanning = require("apps/reader/modules/readerpanning")
|
|
local ReaderRotation = require("apps/reader/modules/readerrotation")
|
|
local ReaderPaging = require("apps/reader/modules/readerpaging")
|
|
local ReaderRolling = require("apps/reader/modules/readerrolling")
|
|
local ReaderSearch = require("apps/reader/modules/readersearch")
|
|
local ReaderStatus = require("apps/reader/modules/readerstatus")
|
|
local ReaderStyleTweak = require("apps/reader/modules/readerstyletweak")
|
|
local ReaderThumbnail = require("apps/reader/modules/readerthumbnail")
|
|
local ReaderToc = require("apps/reader/modules/readertoc")
|
|
local ReaderTypeset = require("apps/reader/modules/readertypeset")
|
|
local ReaderTypography = require("apps/reader/modules/readertypography")
|
|
local ReaderUserHyph = require("apps/reader/modules/readeruserhyph")
|
|
local ReaderView = require("apps/reader/modules/readerview")
|
|
local ReaderWikipedia = require("apps/reader/modules/readerwikipedia")
|
|
local ReaderZooming = require("apps/reader/modules/readerzooming")
|
|
local Screenshoter = require("ui/widget/screenshoter")
|
|
local SettingsMigration = require("ui/data/settings_migration")
|
|
local UIManager = require("ui/uimanager")
|
|
local ffiUtil = require("ffi/util")
|
|
local filemanagerutil = require("apps/filemanager/filemanagerutil")
|
|
local lfs = require("libs/libkoreader-lfs")
|
|
local logger = require("logger")
|
|
local time = require("ui/time")
|
|
local util = require("util")
|
|
local _ = require("gettext")
|
|
local Input = Device.input
|
|
local Screen = Device.screen
|
|
local T = ffiUtil.template
|
|
|
|
local ReaderUI = InputContainer:extend{
|
|
name = "ReaderUI",
|
|
active_widgets = nil, -- array
|
|
|
|
-- if we have a parent container, it must be referenced for now
|
|
dialog = nil,
|
|
|
|
-- the document interface
|
|
document = nil,
|
|
|
|
-- password for document unlock
|
|
password = nil,
|
|
|
|
postInitCallback = nil,
|
|
postReaderReadyCallback = nil,
|
|
}
|
|
|
|
function ReaderUI:registerModule(name, ui_module, always_active)
|
|
if name then
|
|
self[name] = ui_module
|
|
ui_module.name = "reader" .. name
|
|
end
|
|
table.insert(self, ui_module)
|
|
if always_active then
|
|
-- to get events even when hidden
|
|
table.insert(self.active_widgets, ui_module)
|
|
end
|
|
end
|
|
|
|
function ReaderUI:registerPostInitCallback(callback)
|
|
table.insert(self.postInitCallback, callback)
|
|
end
|
|
|
|
function ReaderUI:registerPostReaderReadyCallback(callback)
|
|
table.insert(self.postReaderReadyCallback, callback)
|
|
end
|
|
|
|
function ReaderUI:init()
|
|
self.active_widgets = {}
|
|
|
|
-- cap screen refresh on pan to 2 refreshes per second
|
|
local pan_rate = Screen.low_pan_rate and 2.0 or 30.0
|
|
|
|
Input:inhibitInput(true) -- Inhibit any past and upcoming input events.
|
|
Device:setIgnoreInput(true) -- Avoid ANRs on Android with unprocessed events.
|
|
|
|
self.postInitCallback = {}
|
|
self.postReaderReadyCallback = {}
|
|
-- if we are not the top level dialog ourselves, it must be given in the table
|
|
if not self.dialog then
|
|
self.dialog = self
|
|
end
|
|
|
|
self.doc_settings = DocSettings:open(self.document.file)
|
|
-- Handle local settings migration
|
|
SettingsMigration:migrateSettings(self.doc_settings)
|
|
|
|
self:registerKeyEvents()
|
|
|
|
-- a view container (so it must be child #1!)
|
|
-- all paintable widgets need to be a child of reader view
|
|
self:registerModule("view", ReaderView:new{
|
|
dialog = self.dialog,
|
|
dimen = self.dimen,
|
|
ui = self,
|
|
document = self.document,
|
|
})
|
|
-- goto link controller
|
|
self:registerModule("link", ReaderLink:new{
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self,
|
|
document = self.document,
|
|
})
|
|
-- text highlight
|
|
self:registerModule("highlight", ReaderHighlight:new{
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self,
|
|
document = self.document,
|
|
})
|
|
-- menu widget should be registered after link widget and highlight widget
|
|
-- so that taps on link and highlight areas won't popup reader menu
|
|
-- reader menu controller
|
|
self:registerModule("menu", ReaderMenu:new{
|
|
view = self.view,
|
|
ui = self
|
|
})
|
|
-- rotation controller
|
|
self:registerModule("rotation", ReaderRotation:new{
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self
|
|
})
|
|
-- Handmade/custom ToC and hidden flows
|
|
self:registerModule("handmade", ReaderHandMade:new{
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self,
|
|
document = self.document,
|
|
})
|
|
-- Table of content controller
|
|
self:registerModule("toc", ReaderToc:new{
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self
|
|
})
|
|
-- bookmark controller
|
|
self:registerModule("bookmark", ReaderBookmark:new{
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self
|
|
})
|
|
self:registerModule("annotation", ReaderAnnotation:new{
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self,
|
|
document = self.document,
|
|
})
|
|
-- reader goto controller
|
|
-- "goto" being a dirty keyword in Lua?
|
|
self:registerModule("gotopage", ReaderGoto:new{
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self,
|
|
document = self.document,
|
|
})
|
|
self:registerModule("languagesupport", LanguageSupport:new{
|
|
ui = self,
|
|
document = self.document,
|
|
})
|
|
-- dictionary
|
|
self:registerModule("dictionary", ReaderDictionary:new{
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self,
|
|
document = self.document,
|
|
})
|
|
-- wikipedia
|
|
self:registerModule("wikipedia", ReaderWikipedia:new{
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self,
|
|
document = self.document,
|
|
})
|
|
-- screenshot controller
|
|
self:registerModule("screenshot", Screenshoter:new{
|
|
prefix = 'Reader',
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self
|
|
}, true)
|
|
-- device status controller
|
|
self:registerModule("devicestatus", ReaderDeviceStatus:new{
|
|
ui = self,
|
|
})
|
|
-- configurable controller
|
|
if self.document.info.configurable then
|
|
-- config panel controller
|
|
self:registerModule("config", ReaderConfig:new{
|
|
configurable = self.document.configurable,
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self,
|
|
document = self.document,
|
|
})
|
|
if self.document.info.has_pages then
|
|
-- kopt option controller
|
|
self:registerModule("koptlistener", ReaderKoptListener:new{
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self,
|
|
document = self.document,
|
|
})
|
|
else
|
|
-- cre option controller
|
|
self:registerModule("crelistener", ReaderCoptListener:new{
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self,
|
|
document = self.document,
|
|
})
|
|
end
|
|
-- activity indicator for when some settings take time to take effect (Kindle under KPV)
|
|
if not ReaderActivityIndicator:isStub() then
|
|
self:registerModule("activityindicator", ReaderActivityIndicator:new{
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self,
|
|
document = self.document,
|
|
})
|
|
end
|
|
end
|
|
-- for page specific controller
|
|
if self.document.info.has_pages then
|
|
-- cropping controller
|
|
self:registerModule("cropping", ReaderCropping:new{
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self,
|
|
document = self.document,
|
|
})
|
|
-- paging controller
|
|
self:registerModule("paging", ReaderPaging:new{
|
|
pan_rate = pan_rate,
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self
|
|
})
|
|
-- zooming controller
|
|
self:registerModule("zooming", ReaderZooming:new{
|
|
dialog = self.dialog,
|
|
document = self.document,
|
|
view = self.view,
|
|
ui = self
|
|
})
|
|
-- panning controller
|
|
self:registerModule("panning", ReaderPanning:new{
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self
|
|
})
|
|
-- hinting controller
|
|
self:registerModule("hinting", ReaderHinting:new{
|
|
dialog = self.dialog,
|
|
zoom = self.zooming,
|
|
view = self.view,
|
|
ui = self,
|
|
document = self.document,
|
|
})
|
|
else
|
|
-- load crengine default settings (from cr3.ini, some of these
|
|
-- will be overriden by our settings by some reader modules below)
|
|
if self.document.setupDefaultView then
|
|
self.document:setupDefaultView()
|
|
end
|
|
-- make sure we render document first before calling any callback
|
|
self:registerPostInitCallback(function()
|
|
local start_time = time.now()
|
|
if not self.document:loadDocument() then
|
|
self:dealWithLoadDocumentFailure()
|
|
end
|
|
logger.dbg(string.format(" loading took %.3f seconds", time.to_s(time.since(start_time))))
|
|
|
|
-- used to read additional settings after the document has been
|
|
-- loaded (but not rendered yet)
|
|
self:handleEvent(Event:new("PreRenderDocument", self.doc_settings))
|
|
|
|
start_time = time.now()
|
|
self.document:render()
|
|
logger.dbg(string.format(" rendering took %.3f seconds", time.to_s(time.since(start_time))))
|
|
|
|
-- Uncomment to output the built DOM (for debugging)
|
|
-- logger.dbg(self.document:getHTMLFromXPointer(".0", 0x6830))
|
|
end)
|
|
-- styletweak controller (must be before typeset controller)
|
|
self:registerModule("styletweak", ReaderStyleTweak:new{
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self
|
|
})
|
|
-- typeset controller
|
|
self:registerModule("typeset", ReaderTypeset:new{
|
|
configurable = self.document.configurable,
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self
|
|
})
|
|
-- font menu
|
|
self:registerModule("font", ReaderFont:new{
|
|
configurable = self.document.configurable,
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self
|
|
})
|
|
-- user hyphenation (must be registered before typography)
|
|
self:registerModule("userhyph", ReaderUserHyph:new{
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self
|
|
})
|
|
-- typography menu (replaces previous hyphenation menu / ReaderHyphenation)
|
|
self:registerModule("typography", ReaderTypography:new{
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self
|
|
})
|
|
-- rolling controller
|
|
self:registerModule("rolling", ReaderRolling:new{
|
|
configurable = self.document.configurable,
|
|
pan_rate = pan_rate,
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self
|
|
})
|
|
-- pagemap controller
|
|
self:registerModule("pagemap", ReaderPageMap:new{
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self
|
|
})
|
|
end
|
|
self.disable_double_tap = G_reader_settings:nilOrTrue("disable_double_tap")
|
|
-- scrolling (scroll settings + inertial scrolling)
|
|
self:registerModule("scrolling", ReaderScrolling:new{
|
|
pan_rate = pan_rate,
|
|
dialog = self.dialog,
|
|
ui = self,
|
|
view = self.view,
|
|
})
|
|
-- back location stack
|
|
self:registerModule("back", ReaderBack:new{
|
|
ui = self,
|
|
view = self.view,
|
|
})
|
|
-- fulltext search
|
|
self:registerModule("search", ReaderSearch:new{
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self
|
|
})
|
|
-- book status
|
|
self:registerModule("status", ReaderStatus:new{
|
|
ui = self,
|
|
document = self.document,
|
|
view = self.view,
|
|
})
|
|
-- thumbnails service (book map, page browser)
|
|
self:registerModule("thumbnail", ReaderThumbnail:new{
|
|
ui = self,
|
|
document = self.document,
|
|
})
|
|
-- file searcher
|
|
self:registerModule("filesearcher", FileManagerFileSearcher:new{
|
|
dialog = self.dialog,
|
|
ui = self,
|
|
})
|
|
-- folder shortcuts
|
|
self:registerModule("folder_shortcuts", FileManagerShortcuts:new{
|
|
dialog = self.dialog,
|
|
ui = self,
|
|
})
|
|
-- history view
|
|
self:registerModule("history", FileManagerHistory:new{
|
|
dialog = self.dialog,
|
|
ui = self,
|
|
})
|
|
-- collections/favorites view
|
|
self:registerModule("collections", FileManagerCollection:new{
|
|
dialog = self.dialog,
|
|
ui = self,
|
|
})
|
|
-- book info
|
|
self:registerModule("bookinfo", FileManagerBookInfo:new{
|
|
dialog = self.dialog,
|
|
document = self.document,
|
|
ui = self,
|
|
})
|
|
-- event listener to change device settings
|
|
self:registerModule("devicelistener", DeviceListener:new {
|
|
document = self.document,
|
|
view = self.view,
|
|
ui = self,
|
|
})
|
|
self:registerModule("networklistener", NetworkListener:new {
|
|
document = self.document,
|
|
view = self.view,
|
|
ui = self,
|
|
})
|
|
|
|
-- koreader plugins
|
|
for _, plugin_module in ipairs(PluginLoader:loadPlugins()) do
|
|
local ok, plugin_or_err = PluginLoader:createPluginInstance(
|
|
plugin_module,
|
|
{
|
|
dialog = self.dialog,
|
|
view = self.view,
|
|
ui = self,
|
|
document = self.document,
|
|
})
|
|
if ok then
|
|
self:registerModule(plugin_module.name, plugin_or_err)
|
|
logger.dbg("RD loaded plugin", plugin_module.name,
|
|
"at", plugin_module.path)
|
|
end
|
|
end
|
|
|
|
-- Allow others to change settings based on external factors
|
|
-- Must be called after plugins are loaded & before setting are read.
|
|
self:handleEvent(Event:new("DocSettingsLoad", self.doc_settings, self.document))
|
|
-- we only read settings after all the widgets are initialized
|
|
self:handleEvent(Event:new("ReadSettings", self.doc_settings))
|
|
|
|
for _,v in ipairs(self.postInitCallback) do
|
|
v()
|
|
end
|
|
self.postInitCallback = nil
|
|
|
|
-- Now that document is loaded, store book metadata in settings.
|
|
local props = self.document:getProps()
|
|
self.doc_settings:saveSetting("doc_props", props)
|
|
-- And have an extended and customized copy in memory for quick access.
|
|
self.doc_props = FileManagerBookInfo.extendProps(props, self.document.file)
|
|
|
|
local md5 = self.doc_settings:readSetting("partial_md5_checksum")
|
|
if md5 == nil then
|
|
md5 = util.partialMD5(self.document.file)
|
|
self.doc_settings:saveSetting("partial_md5_checksum", md5)
|
|
end
|
|
|
|
local summary = self.doc_settings:readSetting("summary", {})
|
|
if summary.status == nil then
|
|
summary.status = "reading"
|
|
summary.modified = os.date("%Y-%m-%d", os.time())
|
|
end
|
|
|
|
if summary.status ~= "complete" or not G_reader_settings:isTrue("history_freeze_finished_books") then
|
|
require("readhistory"):addItem(self.document.file) -- (will update "lastfile")
|
|
end
|
|
|
|
-- After initialisation notify that document is loaded and rendered
|
|
-- CREngine only reports correct page count after rendering is done
|
|
-- Need the same event for PDF document
|
|
self:handleEvent(Event:new("ReaderReady", self.doc_settings))
|
|
|
|
for _,v in ipairs(self.postReaderReadyCallback) do
|
|
v()
|
|
end
|
|
self.postReaderReadyCallback = nil
|
|
|
|
Device:setIgnoreInput(false) -- Allow processing of events (on Android).
|
|
Input:inhibitInputUntil(0.2)
|
|
|
|
-- print("Ordered registered gestures:")
|
|
-- for _, tzone in ipairs(self._ordered_touch_zones) do
|
|
-- print(" "..tzone.def.id)
|
|
-- end
|
|
|
|
if ReaderUI.instance == nil then
|
|
logger.dbg("Spinning up new ReaderUI instance", tostring(self))
|
|
else
|
|
-- Should never happen, given what we did in (do)showReader...
|
|
logger.err("ReaderUI instance mismatch! Opened", tostring(self), "while we still have an existing instance:", tostring(ReaderUI.instance), debug.traceback())
|
|
end
|
|
ReaderUI.instance = self
|
|
end
|
|
|
|
function ReaderUI:registerKeyEvents()
|
|
if Device:hasKeys() then
|
|
self.key_events.Home = { { "Home" } }
|
|
self.key_events.Reload = { { "F5" } }
|
|
if Device:hasDPad() and Device:useDPadAsActionKeys() then
|
|
self.key_events.KeyContentSelection = { { { "Up", "Down" } }, event = "StartHighlightIndicator" }
|
|
end
|
|
if Device:hasScreenKB() or Device:hasSymKey() then
|
|
if Device:hasKeyboard() then
|
|
self.key_events.KeyToggleWifi = { { "Shift", "Home" }, event = "ToggleWifi" }
|
|
self.key_events.OpenLastDoc = { { "Shift", "Back" } }
|
|
else -- Currently exclusively targets Kindle 4.
|
|
self.key_events.KeyToggleWifi = { { "ScreenKB", "Home" }, event = "ToggleWifi" }
|
|
self.key_events.OpenLastDoc = { { "ScreenKB", "Back" } }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
ReaderUI.onPhysicalKeyboardConnected = ReaderUI.registerKeyEvents
|
|
|
|
function ReaderUI:setLastDirForFileBrowser(dir)
|
|
if dir and #dir > 1 and dir:sub(-1) == "/" then
|
|
dir = dir:sub(1, -2)
|
|
end
|
|
self.last_dir_for_file_browser = dir
|
|
end
|
|
|
|
function ReaderUI:getLastDirFile(to_file_browser)
|
|
if to_file_browser and self.last_dir_for_file_browser then
|
|
local dir = self.last_dir_for_file_browser
|
|
self.last_dir_for_file_browser = nil
|
|
return dir
|
|
end
|
|
local QuickStart = require("ui/quickstart")
|
|
local last_dir
|
|
local last_file = G_reader_settings:readSetting("lastfile")
|
|
-- ignore quickstart guide as last_file so we can go back to home dir
|
|
if last_file and last_file ~= QuickStart.quickstart_filename then
|
|
last_dir = last_file:match("(.*)/")
|
|
end
|
|
return last_dir, last_file
|
|
end
|
|
|
|
function ReaderUI:showFileManager(file, selected_files)
|
|
local FileManager = require("apps/filemanager/filemanager")
|
|
|
|
local last_dir, last_file
|
|
if file then
|
|
last_dir = util.splitFilePathName(file)
|
|
last_file = file
|
|
else
|
|
last_dir, last_file = self:getLastDirFile(true)
|
|
end
|
|
if FileManager.instance then
|
|
FileManager.instance:reinit(last_dir, last_file)
|
|
else
|
|
FileManager:showFiles(last_dir, last_file, selected_files)
|
|
end
|
|
end
|
|
|
|
function ReaderUI:onShowingReader()
|
|
-- Allows us to optimize out a few useless refreshes in various CloseWidgets handlers...
|
|
self.tearing_down = true
|
|
self.dithered = nil
|
|
|
|
-- Don't enforce a "full" refresh, leave that decision to the next widget we'll *show*.
|
|
self:onClose(false)
|
|
end
|
|
|
|
-- Same as above, except we don't close it yet. Useful for plugins that need to close custom Menus before calling showReader.
|
|
function ReaderUI:onSetupShowReader()
|
|
self.tearing_down = true
|
|
self.dithered = nil
|
|
end
|
|
|
|
--- @note: Will sanely close existing FileManager/ReaderUI instance for you!
|
|
--- This is the *only* safe way to instantiate a new ReaderUI instance!
|
|
--- (i.e., don't look at the testsuite, which resorts to all kinds of nasty hacks).
|
|
function ReaderUI:showReader(file, provider, seamless)
|
|
logger.dbg("show reader ui")
|
|
|
|
file = ffiUtil.realpath(file)
|
|
if lfs.attributes(file, "mode") ~= "file" then
|
|
UIManager:show(InfoMessage:new{
|
|
text = T(_("File '%1' does not exist."), BD.filepath(filemanagerutil.abbreviate(file)))
|
|
})
|
|
return
|
|
end
|
|
|
|
if not DocumentRegistry:hasProvider(file) and provider == nil then
|
|
UIManager:show(InfoMessage:new{
|
|
text = T(_("File '%1' is not supported."), BD.filepath(filemanagerutil.abbreviate(file)))
|
|
})
|
|
self:showFileManager(file)
|
|
return
|
|
end
|
|
|
|
-- We can now signal the existing ReaderUI/FileManager instances that it's time to go bye-bye...
|
|
UIManager:broadcastEvent(Event:new("ShowingReader"))
|
|
provider = provider or DocumentRegistry:getProvider(file)
|
|
if provider.provider then
|
|
self:showReaderCoroutine(file, provider, seamless)
|
|
end
|
|
end
|
|
|
|
function ReaderUI:showReaderCoroutine(file, provider, seamless)
|
|
UIManager:show(InfoMessage:new{
|
|
text = T(_("Opening file '%1'."), BD.filepath(filemanagerutil.abbreviate(file))),
|
|
timeout = 0.0,
|
|
invisible = seamless,
|
|
})
|
|
-- doShowReader might block for a long time, so force repaint here
|
|
UIManager:forceRePaint()
|
|
UIManager:nextTick(function()
|
|
logger.dbg("creating coroutine for showing reader")
|
|
local co = coroutine.create(function()
|
|
self:doShowReader(file, provider, seamless)
|
|
end)
|
|
local ok, err = coroutine.resume(co)
|
|
if err ~= nil or ok == false then
|
|
io.stderr:write('[!] doShowReader coroutine crashed:\n')
|
|
io.stderr:write(debug.traceback(co, err, 1))
|
|
-- Restore input if we crashed before ReaderUI has restored it
|
|
Device:setIgnoreInput(false)
|
|
Input:inhibitInputUntil(0.2)
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("No reader engine for this file or invalid file.")
|
|
})
|
|
self:showFileManager(file)
|
|
end
|
|
end)
|
|
end
|
|
|
|
function ReaderUI:doShowReader(file, provider, seamless)
|
|
if seamless then
|
|
UIManager:avoidFlashOnNextRepaint()
|
|
end
|
|
logger.info("opening file", file)
|
|
-- Only keep a single instance running
|
|
if ReaderUI.instance then
|
|
logger.warn("ReaderUI instance mismatch! Tried to spin up a new instance, while we still have an existing one:", tostring(ReaderUI.instance))
|
|
ReaderUI.instance:onClose()
|
|
end
|
|
local document = DocumentRegistry:openDocument(file, provider)
|
|
if not document then
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("No reader engine for this file or invalid file.")
|
|
})
|
|
self:showFileManager(file)
|
|
return
|
|
end
|
|
if document.is_locked then
|
|
logger.info("document is locked")
|
|
self._coroutine = coroutine.running() or self._coroutine
|
|
self:unlockDocumentWithPassword(document)
|
|
if coroutine.running() then
|
|
local unlock_success = coroutine.yield()
|
|
if not unlock_success then
|
|
self:showFileManager(file)
|
|
return
|
|
end
|
|
end
|
|
end
|
|
local reader = ReaderUI:new{
|
|
dimen = Screen:getSize(),
|
|
covers_fullscreen = true, -- hint for UIManager:_repaint()
|
|
document = document,
|
|
}
|
|
|
|
Screen:setWindowTitle(reader.doc_props.display_title)
|
|
Device:notifyBookState(reader.doc_props.display_title, document)
|
|
|
|
-- This is mostly for the few callers that bypass the coroutine shenanigans and call doShowReader directly,
|
|
-- instead of showReader...
|
|
-- Otherwise, showReader will have taken care of that *before* instantiating a new RD,
|
|
-- in order to ensure a sane ordering of plugins teardown -> instantiation.
|
|
local FileManager = require("apps/filemanager/filemanager")
|
|
if FileManager.instance then
|
|
FileManager.instance:onClose()
|
|
end
|
|
|
|
UIManager:show(reader, seamless and "ui" or "full")
|
|
end
|
|
|
|
function ReaderUI:unlockDocumentWithPassword(document, try_again)
|
|
logger.dbg("show input password dialog")
|
|
self.password_dialog = InputDialog:new{
|
|
title = try_again and _("Password is incorrect, try again?")
|
|
or _("Input document password"),
|
|
buttons = {
|
|
{
|
|
{
|
|
text = _("Cancel"),
|
|
id = "close",
|
|
callback = function()
|
|
self:closeDialog()
|
|
coroutine.resume(self._coroutine)
|
|
end,
|
|
},
|
|
{
|
|
text = _("OK"),
|
|
callback = function()
|
|
local success = self:onVerifyPassword(document)
|
|
self:closeDialog()
|
|
if success then
|
|
coroutine.resume(self._coroutine, success)
|
|
else
|
|
self:unlockDocumentWithPassword(document, true)
|
|
end
|
|
end,
|
|
},
|
|
},
|
|
},
|
|
text_type = "password",
|
|
}
|
|
UIManager:show(self.password_dialog)
|
|
self.password_dialog:onShowKeyboard()
|
|
end
|
|
|
|
function ReaderUI:onVerifyPassword(document)
|
|
local password = self.password_dialog:getInputText()
|
|
return document:unlock(password)
|
|
end
|
|
|
|
function ReaderUI:closeDialog()
|
|
self.password_dialog:onClose()
|
|
UIManager:close(self.password_dialog)
|
|
end
|
|
|
|
function ReaderUI:onScreenResize(dimen)
|
|
self.dimen = dimen
|
|
self:updateTouchZonesOnScreenResize(dimen)
|
|
end
|
|
|
|
function ReaderUI:saveSettings()
|
|
self:handleEvent(Event:new("SaveSettings"))
|
|
self.doc_settings:flush()
|
|
G_reader_settings:flush()
|
|
end
|
|
|
|
function ReaderUI:onFlushSettings(show_notification)
|
|
self:saveSettings()
|
|
if show_notification then
|
|
-- Invoked from dispatcher to explicitely flush settings
|
|
Notification:notify(_("Book metadata saved."))
|
|
end
|
|
end
|
|
|
|
function ReaderUI:closeDocument()
|
|
self.document:close()
|
|
self.document = nil
|
|
end
|
|
|
|
function ReaderUI:notifyCloseDocument()
|
|
self:handleEvent(Event:new("CloseDocument"))
|
|
if self.document:isEdited() then
|
|
local setting = G_reader_settings:readSetting("save_document")
|
|
if setting == "always" then
|
|
self:closeDocument()
|
|
elseif setting == "disable" then
|
|
self.document:discardChange()
|
|
self:closeDocument()
|
|
else
|
|
UIManager:show(ConfirmBox:new{
|
|
text = _("Write highlights into this PDF?"),
|
|
ok_text = _("Write"),
|
|
dismissable = false,
|
|
ok_callback = function()
|
|
self:closeDocument()
|
|
end,
|
|
cancel_callback = function()
|
|
self.document:discardChange()
|
|
self:closeDocument()
|
|
end,
|
|
})
|
|
end
|
|
else
|
|
self:closeDocument()
|
|
end
|
|
end
|
|
|
|
function ReaderUI:onClose(full_refresh)
|
|
logger.dbg("closing reader")
|
|
PluginLoader:finalize()
|
|
Device:notifyBookState(nil, nil)
|
|
if full_refresh == nil then
|
|
full_refresh = true
|
|
end
|
|
-- if self.dialog is us, we'll have our onFlushSettings() called
|
|
-- by UIManager:close() below, so avoid double save
|
|
if self.dialog ~= self then
|
|
self:saveSettings()
|
|
end
|
|
if self.document ~= nil then
|
|
require("readhistory"):updateLastBookTime(self.tearing_down)
|
|
-- Serialize the most recently displayed page for later launch
|
|
DocCache:serialize(self.document.file)
|
|
logger.dbg("closing document")
|
|
self:notifyCloseDocument()
|
|
end
|
|
UIManager:close(self.dialog, full_refresh and "full")
|
|
end
|
|
|
|
function ReaderUI:onCloseWidget()
|
|
if ReaderUI.instance == self then
|
|
logger.dbg("Tearing down ReaderUI", tostring(self))
|
|
else
|
|
logger.warn("ReaderUI instance mismatch! Closed", tostring(self), "while the active one is supposed to be", tostring(ReaderUI.instance))
|
|
end
|
|
ReaderUI.instance = nil
|
|
self._coroutine = nil
|
|
end
|
|
|
|
function ReaderUI:dealWithLoadDocumentFailure()
|
|
-- Sadly, we had to delay loadDocument() to about now, so we only
|
|
-- know now this document is not valid or recognized.
|
|
-- We can't do much more than crash properly here (still better than
|
|
-- going on and segfaulting when calling other methods on unitiliazed
|
|
-- _document)
|
|
-- As we are in a coroutine, we can pause and show an InfoMessage before exiting
|
|
local _coroutine = coroutine.running()
|
|
if coroutine then
|
|
logger.warn("crengine failed recognizing or parsing this file: unsupported or invalid document")
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("Failed recognizing or parsing this file: unsupported or invalid document.\nKOReader will exit now."),
|
|
dismiss_callback = function()
|
|
coroutine.resume(_coroutine, false)
|
|
end,
|
|
})
|
|
-- Restore input, so can catch the InfoMessage dismiss and exit
|
|
Device:setIgnoreInput(false)
|
|
Input:inhibitInputUntil(0.2)
|
|
coroutine.yield() -- pause till InfoMessage is dismissed
|
|
end
|
|
-- We have to error and exit the coroutine anyway to avoid any segfault
|
|
error("crengine failed recognizing or parsing this file: unsupported or invalid document")
|
|
end
|
|
|
|
function ReaderUI:onHome()
|
|
local file = self.document.file
|
|
self:onClose()
|
|
self:showFileManager(file)
|
|
return true
|
|
end
|
|
|
|
function ReaderUI:onReload()
|
|
self:reloadDocument()
|
|
end
|
|
|
|
function ReaderUI:reloadDocument(after_close_callback, seamless)
|
|
local file = self.document.file
|
|
local provider = getmetatable(self.document).__index
|
|
|
|
-- Mimic onShowingReader's refresh optimizations
|
|
self.tearing_down = true
|
|
self.dithered = nil
|
|
|
|
self:handleEvent(Event:new("CloseReaderMenu"))
|
|
self:handleEvent(Event:new("CloseConfigMenu"))
|
|
self:handleEvent(Event:new("PreserveCurrentSession")) -- don't reset statistics' start_current_period
|
|
self.highlight:onClose() -- close highlight dialog if any
|
|
self:onClose(false)
|
|
if after_close_callback then
|
|
-- allow caller to do stuff between close an re-open
|
|
after_close_callback(file, provider)
|
|
end
|
|
|
|
self:showReader(file, provider, seamless)
|
|
end
|
|
|
|
function ReaderUI:switchDocument(new_file)
|
|
if not new_file then return end
|
|
|
|
-- Mimic onShowingReader's refresh optimizations
|
|
self.tearing_down = true
|
|
self.dithered = nil
|
|
|
|
self:handleEvent(Event:new("CloseReaderMenu"))
|
|
self:handleEvent(Event:new("CloseConfigMenu"))
|
|
self.highlight:onClose() -- close highlight dialog if any
|
|
self:onClose(false)
|
|
|
|
self:showReader(new_file)
|
|
end
|
|
|
|
function ReaderUI:onOpenLastDoc()
|
|
self:switchDocument(self.menu:getPreviousFile())
|
|
end
|
|
|
|
function ReaderUI:getCurrentPage()
|
|
return self.paging and self.paging.current_page or self.document:getCurrentPage()
|
|
end
|
|
|
|
return ReaderUI
|