Add Book map and Page browser features

- Book map: shows a map of content, including TOC,
  boomarks, read pages, non-linear flows...
- Page browser: shows thumbnails of pages.

- ReaderThumbnail: new Reader module that provides
  a service for generating thumbnails of book pages.
  It makes available these 2 new fullscreen widgets.
- ReaderBookmark, ReaderLink, Statistics: add methods
  to return new views of bookmarks, previous locations
  and read pages, that are needed by BookMapWidget.
- ReaderToc: compute TOC max_depth.
- ReaderBookmark, ReaderHighlight: send events on
  bookmark add/update/remove so thumbnails of the
  pages impacted can be trashed.
reviewable/pr8618/r1
poire-z 2 years ago
parent 2530e954a2
commit bc16b32395

@ -840,6 +840,7 @@ function ReaderBookmark:addBookmark(item)
end
end
table.insert(self.bookmarks, _middle + direction, item)
self.ui:handleEvent(Event:new("BookmarkAdded", item))
self.view.footer:onUpdateFooter(self.view.footer_visible)
end
@ -888,6 +889,7 @@ function ReaderBookmark:removeBookmark(item, reset_auto_text_only)
v.text = nil
end
else
self.ui:handleEvent(Event:new("BookmarkRemoved", v))
table.remove(self.bookmarks, _middle)
self.view.footer:onUpdateFooter(self.view.footer_visible)
end
@ -911,6 +913,7 @@ function ReaderBookmark:removeBookmark(item, reset_auto_text_only)
v.text = nil
end
else
self.ui:handleEvent(Event:new("BookmarkRemoved", v))
table.remove(self.bookmarks, i)
self.view.footer:onUpdateFooter(self.view.footer_visible)
end
@ -923,6 +926,7 @@ end
function ReaderBookmark:updateBookmark(item)
for i=1, #self.bookmarks do
if item.datetime == self.bookmarks[i].datetime and item.page == self.bookmarks[i].page then
local bookmark_before = util.tableDeepCopy(self.bookmarks[i])
local is_auto_text = self:isBookmarkAutoText(self.bookmarks[i])
self.bookmarks[i].page = item.updated_highlight.pos0
self.bookmarks[i].pos0 = item.updated_highlight.pos0
@ -933,6 +937,7 @@ function ReaderBookmark:updateBookmark(item)
if is_auto_text then
self.bookmarks[i].text = self:getBookmarkAutoText(self.bookmarks[i])
end
self.ui:handleEvent(Event:new("BookmarkUpdated", self.bookmarks[i], bookmark_before))
self:onSaveSettings()
break
end
@ -953,6 +958,7 @@ function ReaderBookmark:renameBookmark(item, from_highlight, is_new_note)
bookmark = util.tableDeepCopy(bm)
bookmark.text_orig = bm.text or bm.notes
bookmark.mandatory = self:getBookmarkPageString(bm.page)
self.ui:handleEvent(Event:new("BookmarkEdited", bm))
break
end
end
@ -997,6 +1003,7 @@ function ReaderBookmark:renameBookmark(item, from_highlight, is_new_note)
for __, bm in ipairs(self.bookmarks) do
if bookmark.datetime == bm.datetime and bookmark.page == bm.page then
bm.text = value
self.ui:handleEvent(Event:new("BookmarkEdited", bm))
-- A bookmark isn't necessarily a highlight (it doesn't have pboxes)
if bookmark.pboxes then
local setting = G_reader_settings:readSetting("save_document")
@ -1131,6 +1138,7 @@ end
function ReaderBookmark:toggleBookmark(pn_or_xp)
local index = self:getDogearBookmarkIndex(pn_or_xp)
if index then
self.ui:handleEvent(Event:new("BookmarkRemoved", self.bookmarks[index]))
table.remove(self.bookmarks, index)
else
-- build notes from TOC
@ -1297,6 +1305,26 @@ function ReaderBookmark:getBookmarkPageString(page)
return tostring(page)
end
function ReaderBookmark:getBookmarkedPages()
local pages = {}
for _, bm in ipairs(self.bookmarks) do
local page
if self.ui.rolling then
page = self.ui.document:getPageFromXPointer(bm.page)
else
page = bm.page
end
local btype = self:getBookmarkType(bm)
if not pages[page] then
pages[page] = {}
end
if not pages[page][btype] then
pages[page][btype] = true
end
end
return pages
end
function ReaderBookmark:getBookmarkAutoText(bookmark, force_auto_text)
if G_reader_settings:nilOrTrue("bookmarks_items_auto_text") or force_auto_text then
local page = self:getBookmarkPageString(bookmark.page)
@ -1309,7 +1337,7 @@ end
--- Check if the 'text' field has not been edited manually
function ReaderBookmark:isBookmarkAutoText(bookmark)
return (bookmark.text == nil) or (bookmark.text == bookmark.notes)
return (bookmark.text == nil) or (bookmark.text == "") or (bookmark.text == bookmark.notes)
or (bookmark.text == self:getBookmarkAutoText(bookmark, true))
end
@ -1321,4 +1349,12 @@ function ReaderBookmark:getBookmarkNote(item)
end
end
function ReaderBookmark:getBookmarkForHighlight(item)
for i=1, #self.bookmarks do
if item.datetime == self.bookmarks[i].datetime and item.page == self.bookmarks[i].page then
return self.bookmarks[i]
end
end
end
return ReaderBookmark

@ -1710,8 +1710,13 @@ function ReaderHighlight:editHighlightStyle(page, i)
default_provider = self.view.highlight.saved_drawer or
G_reader_settings:readSetting("highlight_drawing_style", "lighten"),
callback = function(radio)
self.view.highlight.saved[page][i].drawer = radio.provider
item.drawer = radio.provider
UIManager:setDirty(self.dialog, "ui")
self.ui:handleEvent(Event:new("BookmarkUpdated",
self.ui.bookmark:getBookmarkForHighlight({
page = self.ui.paging and item.pos0.page or item.pos0,
datetime = item.datetime,
})))
end,
})
end

@ -524,6 +524,21 @@ function ReaderLink:onClearLocationStack(show_notification)
return true
end
function ReaderLink:getPreviousLocationPages()
local previous_locations = {}
if #self.location_stack > 0 then
for num, location in ipairs(self.location_stack) do
if self.ui.rolling and location.xpointer then
previous_locations[self.ui.document:getPageFromXPointer(location.xpointer)] = num
end
if self.ui.paging and location[1] and location[1].page then
previous_locations[location[1].page] = num
end
end
end
return previous_locations
end
--- Goes to link.
-- (This is called by other modules (highlight, search) to jump to a xpointer,
-- they should not provide allow_footnote_popup=true)

@ -0,0 +1,507 @@
local Blitbuffer = require("ffi/blitbuffer")
local Cache = require("cache")
local Device = require("device")
local Geom = require("ui/geometry")
local InputContainer = require("ui/widget/container/inputcontainer")
local Persist = require("persist")
local RenderImage = require("ui/renderimage")
local TileCacheItem = require("document/tilecacheitem")
local UIManager = require("ui/uimanager")
local Screen = Device.screen
local ffiutil = require("ffi/util")
local logger = require("logger")
local util = require("util")
local _ = require("gettext")
-- This ReaderThumbnail module provides a service for generating thumbnails
-- of book pages.
-- It handles launching via the menu or Dispatcher/Gestures two fullscreen
-- widgets related to showing pages and thumbnails that will make use of
-- its services: BookMap and PageBrowser.
local ReaderThumbnail = InputContainer:new{}
function ReaderThumbnail:init()
if not Device:isTouchDevice() then
-- The BookMap and PageBrowser widgets depend too much on gestures,
-- making them work with keys would be hard and very limited, so
-- just don't make them available.
return
end
self.ui.menu:registerToMainMenu(self)
-- Use LuaJIT fast buffer.encode()/decode() when serializing BlitBuffer
-- for exchange between subprocess and parent.
self.codec = Persist.getCodec("luajit")
self:setupColor()
self.thumbnails_requests = {}
self.current_target_size_tag = nil
-- Ensure no multiple executions, and nextTick() the scheduleIn()
-- so we get a chance to process events in-between refreshes and
-- this can be interrupted (otherwise, something scheduleIn(0.1),
-- if a screen refresh is then done and taking longer than 0.1s,
-- would be executed immediately, without emptying any input event).
local schedule_step = 0
self._ensureTileGeneration_action = function(restart)
if restart then
UIManager:unschedule(self._ensureTileGeneration_action)
schedule_step = 0
end
if schedule_step == 0 then
schedule_step = 1
UIManager:nextTick(self._ensureTileGeneration_action)
elseif schedule_step == 1 then
schedule_step = 2
UIManager:scheduleIn(0.1, self._ensureTileGeneration_action)
else
schedule_step = 0
self:ensureTileGeneration()
end
end
end
function ReaderThumbnail:addToMainMenu(menu_items)
menu_items.book_map = {
text = _("Book map"),
callback = function()
self:onShowBookMap()
end,
}
menu_items.page_browser = {
text = _("Page browser"),
callback = function()
self:onShowPageBrowser()
end,
}
end
function ReaderThumbnail:onShowBookMap()
local BookMapWidget = require("ui/widget/bookmapwidget")
UIManager:show(BookMapWidget:new{
ui = self.ui,
})
return true
end
function ReaderThumbnail:onShowPageBrowser()
local PageBrowserWidget = require("ui/widget/pagebrowserwidget")
UIManager:show(PageBrowserWidget:new{
ui = self.ui,
})
return true
end
-- This is made a module local so we can keep track of pids
-- to collect across multiple Reader instantiations
local pids_to_collect = {}
function ReaderThumbnail:collectPids()
if #pids_to_collect == 0 then
return false
end
for i=#pids_to_collect, 1, -1 do
if ffiutil.isSubProcessDone(pids_to_collect[i]) then
table.remove(pids_to_collect, i)
end
end
return #pids_to_collect > 0
end
function ReaderThumbnail:setupColor()
self.bb_type = self.ui.document.render_color and self.ui.document.color_bb_type or Blitbuffer.TYPE_BB8
end
function ReaderThumbnail:setupCache()
if not self.tile_cache then
-- We want to allow browsing at least N pages worth of thumbnails
-- without cache trashing. A little more than N pages (because inter
-- thumbnail margins) will fit in N * screen size.
-- With N=5, this should use from 5 to 15 Mb on a classic eInk device.
local N = 5
local max_bytes = math.ceil(N * Screen:getWidth() * Screen:getHeight() * Blitbuffer.TYPE_TO_BPP[self.bb_type] / 8)
-- We don't really care about limiting any number of slots, so allow
-- for at least 5 pages of 10x10 tiles
local avg_itemsize = math.ceil(max_bytes / 500)
self.tile_cache = Cache:new{
size = max_bytes,
avg_itemsize = avg_itemsize, -- will make slots=500
enable_eviction_cb = true,
}
end
end
function ReaderThumbnail:logCacheSize()
logger.info(string.format("Thumbnails cache: %d/%d (%s/%s)",
self.tile_cache.cache.used_slots(),
self.tile_cache.slots,
util.getFriendlySize(self.tile_cache.cache.used_size()),
util.getFriendlySize(self.tile_cache.size)))
end
function ReaderThumbnail:resetCache()
if self.tile_cache then
self.tile_cache:clear()
self.tile_cache = nil
end
end
function ReaderThumbnail:removeFromCache(hash_subs, remove_only_non_matching)
-- Remove from cache all tiles matching any hash from hash_subs.
-- IF only_non_matching=true, keep those matching and remove all others.
if not self.tile_cache then
return
end
if type(hash_subs) ~= "table" then
hash_subs = { hash_subs }
end
local nb_removed, size_removed = 0, 0
local to_remove = {}
for thash, tile in self.tile_cache.cache:pairs() do
local remove = remove_only_non_matching
for _, h in ipairs(hash_subs) do
if thash:find(h, 1, true) then -- plain text match (no pattern needed)
remove = not remove
break
end
end
if remove then
to_remove[thash] = true
nb_removed = nb_removed + 1
size_removed = size_removed + tile.size
end
end
for thash, _ in pairs(to_remove) do
self.tile_cache.cache:delete(thash)
logger.dbg("removed cached thumbnail", thash)
end
return nb_removed, size_removed
end
function ReaderThumbnail:resetCachedPagesForBookmarks(...)
-- Multiple bookmarks may be provided
local start_page, end_page
for _, bm in ipairs({...}) do
if self.ui.rolling then
-- Look at all properties that may be xpointers
for _, k in ipairs({"page", "pos0", "pos1"}) do
if bm[k] and type(bm[k]) == "string" then
local p = self.ui.document:getPageFromXPointer(bm[k])
if not start_page or p < start_page then
start_page = p
end
if not end_page or p > end_page then
end_page = p
end
end
end
else
if bm.page and type(bm.page) == "number" then
local p = bm.page
if not start_page or p < start_page then
start_page = p
end
if not end_page or p > end_page then
end_page = p
end
end
end
end
if start_page and end_page then
local hash_subs_to_remove = {}
for p=start_page, end_page do
table.insert(hash_subs_to_remove, string.format("p%d-", p))
end
self:removeFromCache(hash_subs_to_remove)
end
end
function ReaderThumbnail:tidyCache()
if self.current_target_size_tag then
-- Remove all thumbnails generated for an older target size
self:removeFromCache("-"..self.current_target_size_tag, true)
end
end
function ReaderThumbnail:cancelPageThumbnailRequests(batch_id)
if batch_id then
self.thumbnails_requests[batch_id] = nil
else
self.thumbnails_requests = {}
end
if self.req_in_progress and (not batch_id or self.req_in_progress.batch_id == batch_id) then
-- Kill any reference to the module cancelling it
self.req_in_progress.when_generated_callback = nil
end
end
function ReaderThumbnail:getPageThumbnail(page, width, height, batch_id, when_generated_callback)
self:setupCache()
self.current_target_size_tag = string.format("w%d_h%d", width, height)
local hash = string.format("p%d-%s", page, self.current_target_size_tag)
local tile = self.tile_cache and self.tile_cache:check(hash)
if tile then
-- Cached: call callback and we're done.
when_generated_callback(tile, batch_id, false)
return false -- not delayed
end
if not self.thumbnails_requests[batch_id] then
self.thumbnails_requests[batch_id] = {}
end
table.insert(self.thumbnails_requests[batch_id], {
batch_id = batch_id,
hash = hash,
page = page,
width = width,
height = height,
when_generated_callback = when_generated_callback,
})
-- Start tile generation, avoid multiple ones
self._ensureTileGeneration_action(true)
return true -- delayed
end
function ReaderThumbnail:ensureTileGeneration()
local has_pids_still_to_collect = self:collectPids()
local still_in_progress = false
if self.req_in_progress then
local pid_still_to_collect
still_in_progress, pid_still_to_collect = self:checkTileGeneration(self.req_in_progress)
if pid_still_to_collect then
has_pids_still_to_collect = true
end
end
if not still_in_progress then
self.req_in_progress = nil
while true do
local req_id, requests = next(self.thumbnails_requests)
if not req_id then -- no more requests
break
end
local req = table.remove(requests, 1)
if #requests == 0 then
self.thumbnails_requests[req_id] = nil
end
if req.when_generated_callback then -- not cancelled since queued
-- It might have been generated and cached by a previous batch
local tile = self.tile_cache and self.tile_cache:check(req.hash)
if tile then
req.when_generated_callback(tile, req.batch_id, true)
else
if self:startTileGeneration(req) then
self.req_in_progress = req
break
else
-- Failure starting it: let requester know in case it cares, and forget it
req.when_generated_callback(nil, req.batch_id, true)
end
end
end
end
end
if self.req_in_progress or has_pids_still_to_collect or next(self.thumbnails_requests) then
self._ensureTileGeneration_action()
end
end
function ReaderThumbnail:startTileGeneration(request)
local pid, parent_read_fd = ffiutil.runInSubProcess(function(pid, child_write_fd)
-- Get page image as if drawn on the screen
local bb = self:_getPageImage(request.page)
-- Scale it to fit in the requested size
local scale_factor = math.min(request.width / bb:getWidth(), request.height / bb:getHeight())
local target_w = math.floor(bb:getWidth() * scale_factor)
local target_h = math.floor(bb:getHeight() * scale_factor)
-- local TimeVal = require("ui/timeval")
-- local start_tv = TimeVal:now()
local tile = TileCacheItem:new{
bb = RenderImage:scaleBlitBuffer(bb, target_w, target_h, true),
pageno = request.page,
}
tile.size = tonumber(tile.bb.stride) * tile.bb.h
-- logger.info("tile size", tile.bb.w, tile.bb.h, "=>", tile.size)
-- logger.info(string.format(" scaling took %.3f seconds, %d bpp", TimeVal:getDuration(start_tv), tile.bb:getBpp()))
-- bb:free() -- no need to spend time freeing, we're dying soon anyway!
ffiutil.writeToFD(child_write_fd, self.codec.serialize(tile:totable()), true)
end, true) -- with_pipe = true
if pid then
-- Store these in the request object itself
request.pid = pid
request.parent_read_fd = parent_read_fd
return true
end
logger.warn("PageBrowserWidget thumbnail start failure:", parent_read_fd)
return false
end
function ReaderThumbnail:checkTileGeneration(request)
local pid, parent_read_fd = request.pid, request.parent_read_fd
local stuff_to_read = ffiutil.getNonBlockingReadSize(parent_read_fd) ~= 0
local subprocess_done = ffiutil.isSubProcessDone(pid)
logger.dbg("subprocess_done:", subprocess_done, " stuff_to_read:", stuff_to_read)
if stuff_to_read then
-- local TimeVal = require("ui/timeval")
-- local start_tv = TimeVal:now()
local result, err = self.codec.deserialize(ffiutil.readAllFromFD(parent_read_fd))
if result then
local tile = TileCacheItem:new{}
tile:fromtable(result)
if self.tile_cache then
self.tile_cache:insert(request.hash, tile)
end
if request.when_generated_callback then -- not cancelled
request.when_generated_callback(tile, request.batch_id, true)
end
else
logger.warn("PageBrowserWidget thumbnail deserialize() failed:", err)
if request.when_generated_callback then -- not cancelled
request.when_generated_callback(nil, request.batch_id, true)
end
end
-- logger.info(string.format(" parsing result from subprocess took %.3f seconds", TimeVal:getDuration(start_tv)))
if not subprocess_done then
table.insert(pids_to_collect, pid)
return false, true
end
return false
elseif subprocess_done then
-- subprocess_done: process exited with no output
ffiutil.readAllFromFD(parent_read_fd) -- close our fd
return false
end
logger.dbg("process not yet done, will check again soon")
return true
end
function ReaderThumbnail:_getPageImage(page)
-- This is run in a subprocess: we can tweak all document settings
-- to get an adequate image of the page.
-- No need to worry about the final state of things: this subprocess
-- will die just after drawing the page, and all will be forgotten,
-- without impact on the parent process.
-- Be sure to limit our impact on the disk-saved book state
self.ui.saveSettings = function() end -- Be sure nothing is flushed
self.ui.statistics = nil -- Don't update statistics for pages we visit
-- By default, our target page size is the current screen size
local target_w, target_h = Screen:getWidth(), Screen:getHeight()
-- This was all mostly chosen by experimenting.
-- Be sure to call the innermost methods enough to get what we want, and
-- not upper event handlers that may trigger other unneeded events and stuff.
-- Especially, be sure to not trigger any paint on the screen buffer, or
-- any processing of input events.
-- No need to worry about UIManager:scheduleIn() or :nextTick(), as
-- we will die before the callback gets a chance to be run.
-- Common to ReaderRolling and ReaderPaging
self.ui.view.footer_visible = false -- We want no footer on page image
if self.ui.view.highlight.lighten_factor < 0.3 then
self.ui.view.highlight.lighten_factor = 0.3 -- make lighten highlight a bit darker
end
if self.ui.rolling then
-- CRE documents: pages all have the aspect ratio of our screen (alt top status bar
-- will be croped out after drawing), we will show them just as rendered.
self.ui.view:onSetViewMode("page") -- Get out of scroll mode
if self.ui.font.gamma_index < 30 then -- Increase font gamma (if not already increased),
self.ui.document:setGammaIndex(30) -- as downscaling will make text grayer
end
self.ui.document:setImageScaling(false) -- No need for smooth scaling as all will be downscaled
self.ui.document:setNightmodeImages(false) -- We don't invert page images even if nightmode set: keep images as-is
self.ui.view.state.page = page -- Be on requested page
self.ui.document:gotoPage(page) -- Current xpointer needs to be updated for some of what follows
self.ui.bookmark:onPageUpdate(page) -- Update dogear state for this page
self.ui.pagemap:onPageUpdate(page) -- Update pagemap labels for this page
end
if self.ui.paging then
-- With PDF/DJVU/Pics, we will show the native page (no reflow, no crop, no zoom
-- to columns...). This makes thumbnail generation faster, and will allow the user
-- to get an overview of the book native pages to better decide which option will
-- be best to use for the book.
-- We also want to get a thumbnail with the aspect ratio of the native page
-- (so we don't get a native landscape page smallish and centered with blank above
-- and below in a portrait thumbnail, if the screen is in portrait mode).
self.ui.view.hinting = false -- Disable hinting
self.ui.view.page_scroll = false -- Get out of scroll mode
self.ui.view.flipping_visible = false -- No page flipping icon
self.ui.document.configurable.text_wrap = false -- Get out of reflow mode
self.ui.document.configurable.trim_page = 3 -- Page crop: none
-- self.ui.document.configurable.trim_page = 1 -- Page crop: auto (very slower)
self.ui.document.configurable.auto_straighten = 0 -- No auto straighten
-- We can let dewatermark if the user has enabled it, it helps
-- limiting annoying eInk refreshes of light gray areas
-- self.ui.document.configurable.page_opt = 0 -- No dewatermark
-- We won't touch the contrast (to try making text less gray), as it applies on
-- images that could get too dark.
-- Get native page dimensions, and update our target bb dimensions so it gets the
-- same aspect ratio (we don't use native dimensions as is, as they may get huge)
local dimen = self.ui.document:getPageDimensions(page, 1, 0)
local scale_factor = math.min(target_w / dimen.w, target_h / dimen.h)
target_w = math.floor(dimen.w * scale_factor)
target_h = math.floor(dimen.h * scale_factor)
dimen = Geom:new{ w=target_w, h=target_h }
-- logger.info("getPageImage", page, dimen, "=>", target_w, target_h, scale_factor)
-- This seems to do it all well:
-- local Event = require("ui/event")
-- self.ui:handleEvent(Event:new("SetDimensions", dimen))
-- self.ui.view.dogear[1].dimen.w = dimen.w -- (hack... its code uses the Screen width)
-- self.ui:handleEvent(Event:new("PageUpdate", page))
-- self.ui:handleEvent(Event:new("SetZoomMode", "page"))
-- Trying to do as little as needed, knowing the internals:
self.ui.view:onSetDimensions(dimen)
self.ui.view:onBBoxUpdate(nil) -- drop any bbox, draw native page
self.ui.view.state.page = page
self.ui.view.state.zoom = scale_factor
self.ui.view.state.rotation = 0
self.ui.view:recalculate()
self.ui.view.dogear[1].dimen.w = dimen.w -- (hack... its code uses the Screen width)
self.ui.bookmark:onPageUpdate(page) -- Update dogear state for this page
end
-- Draw the page on a new BB with the targetted size
local bb = Blitbuffer.new(target_w, target_h, self.bb_type)
self.ui.view:paintTo(bb, 0, 0)
if self.ui.rolling then
-- Crop out the top alt status bar if enabled
local header_height = self.ui.document:getHeaderHeight()
if header_height > 0 then
bb = bb:viewport(0, header_height, bb.w, bb.h - header_height)
end
end
return bb
end
function ReaderThumbnail:onCloseDocument()
self:cancelPageThumbnailRequests()
if self.tile_cache then
self:logCacheSize()
self.tile_cache:clear()
self.tile_cache = nil
end
end
function ReaderThumbnail:onColorRenderingUpdate()
self:setupColor()
self:resetCache()
end
-- CRE: emitted after a re-rendering
ReaderThumbnail.onTocReset = ReaderThumbnail.resetCache
-- Emitted When adding/removing/updating bookmarks and highlights
ReaderThumbnail.onBookmarkAdded = ReaderThumbnail.resetCachedPagesForBookmarks
ReaderThumbnail.onBookmarkRemoved = ReaderThumbnail.resetCachedPagesForBookmarks
ReaderThumbnail.onBookmarkUpdated = ReaderThumbnail.resetCachedPagesForBookmarks
return ReaderThumbnail

@ -170,6 +170,7 @@ function ReaderToc:validateAndFixToc()
logger.dbg("validateAndFixToc(): quick scan")
local has_bogus
local cur_page = 0
local max_depth = 0
for i = first, last do
local page = toc[i].page
if page < cur_page then
@ -177,15 +178,22 @@ function ReaderToc:validateAndFixToc()
break
end
cur_page = page
-- Use this loop to compute max_depth here (if has_bogus,
-- we will recompute it in the loop below)
if toc[i].depth > max_depth then
max_depth = toc[i].depth
end
end
if not has_bogus then -- no TOC items, or all are valid
logger.dbg("validateAndFixToc(): TOC is fine")
self.toc_depth = max_depth
return
end
logger.dbg("validateAndFixToc(): TOC needs fixing")
-- Bad ordering previously noticed: try to fix the wrong items' page
-- by setting it to the previous or next good item page.
max_depth = 0 -- recompute this
local nb_bogus = 0
local nb_fixed_pages = 0
-- We fix only one bogus item per loop, taking the option that
@ -198,6 +206,9 @@ function ReaderToc:validateAndFixToc()
-- (These cases are met in the following code with cur_page=57 and page=6)
cur_page = 0
for i = first, last do
if toc[i].depth > max_depth then
max_depth = toc[i].depth
end
local page = toc[i].fixed_page or toc[i].page
if page >= cur_page then
cur_page = page
@ -265,6 +276,7 @@ function ReaderToc:validateAndFixToc()
end
end
logger.info(string.format("TOC had %d bogus page numbers: fixed %d items to keep them ordered.", nb_bogus, nb_fixed_pages))
self.toc_depth = max_depth
end
function ReaderToc:getTocIndexByPage(pn_or_xp, skip_ignored_ticks)
@ -635,10 +647,11 @@ function ReaderToc:onShowToc()
self:fillToc()
-- build menu items
if #self.toc > 0 and not self.toc[1].text then
local has_hidden_flows = self.ui.document:hasHiddenFlows()
for _, v in ipairs(self.toc) do
v.text = self.toc_indent:rep(v.depth-1)..self:cleanUpTocTitle(v.title, true)
v.mandatory = v.page
if self.ui.document:hasHiddenFlows() then
if has_hidden_flows then
local flow = self.ui.document:getPageFlow(v.page)
if v.orig_page then -- bogus page fixed: show original page number
-- This is an ugly piece of code, which can result in an ugly TOC,

@ -46,6 +46,7 @@ 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")
@ -375,6 +376,11 @@ function ReaderUI:init()
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,

@ -118,6 +118,8 @@ local settingsList = {
follow_nearest_internal_link = {category="arg", event="GoToInternalPageLink", arg={pos={x=0,y=0}}, title=_("Follow nearest internal link"), reader=true},
clear_location_history = {category="none", event="ClearLocationStack", arg=true, title=_("Clear location history"), reader=true, separator=true},
toc = {category="none", event="ShowToc", title=_("Table of contents"), reader=true},
book_map = {category="none", event="ShowBookMap", title=_("Book map"), reader=true, condition=Device:isTouchDevice()},
page_browser = {category="none", event="ShowPageBrowser", title=_("Page browser"), reader=true, condition=Device:isTouchDevice()},
bookmarks = {category="none", event="ShowBookmark", title=_("Bookmarks"), reader=true},
bookmark_search = {category="none", event="SearchBookmark", title=_("Bookmark search"), reader=true},
book_status = {category="none", event="ShowBookStatus", title=_("Book status"), reader=true},
@ -281,6 +283,8 @@ local dispatcher_menu_order = {
"clear_location_history",
"toc",
"book_map",
"page_browser",
"bookmarks",
"bookmark_search",

@ -13,13 +13,16 @@ local order = {
navi = {
"table_of_contents",
"bookmarks",
"toggle_bookmark",
"toggle_bookmark", -- if not Device:isTouchDevice()
"bookmark_browsing_mode",
"navi_settings",
"----------------------------",
"page_map",
"hide_nonlinear_flows",
"----------------------------",
"book_map", -- if Device:isTouchDevice()
"page_browser", -- if Device:isTouchDevice()
"----------------------------",
"go_to",
"skim_to",
"autoturn",
@ -64,7 +67,7 @@ local order = {
"network",
"screen",
"----------------------------",
"taps_and_gestures",
"taps_and_gestures", -- if Device:isTouchDevice()
"navigation",
"document",
"----------------------------",

File diff suppressed because it is too large Load Diff

@ -242,6 +242,16 @@ function ScrollableContainer:onCloseWidget()
end
end
function ScrollableContainer:reset()
if self._bb then
self._bb:free()
self._bb = nil
end
self._is_scrollable = nil
self._scroll_offset_x = 0
self._scroll_offset_y = 0
end
function ScrollableContainer:paintTo(bb, x, y)
if self[1] == nil then
return
@ -255,6 +265,9 @@ function ScrollableContainer:paintTo(bb, x, y)
if not self._is_scrollable then
-- nothing to scroll: pass-through
if self._mirroredUI then -- behave as LeftContainer
x = x + (self.dimen.w - self[1]:getSize().w)
end
self[1]:paintTo(bb, x, y)
return
end

@ -0,0 +1,947 @@
local BD = require("ui/bidi")
local Blitbuffer = require("ffi/blitbuffer")
local CenterContainer = require("ui/widget/container/centercontainer")
local Device = require("device")
local Event = require("ui/event")
local Font = require("ui/font")
local FrameContainer = require("ui/widget/container/framecontainer")
local Geom = require("ui/geometry")
local GestureRange = require("ui/gesturerange")
local ImageWidget = require("ui/widget/imagewidget")
local InfoMessage = require("ui/widget/infomessage")
local InputContainer = require("ui/widget/container/inputcontainer")
local OverlapGroup = require("ui/widget/overlapgroup")
local Size = require("ui/size")
local TextWidget = require("ui/widget/textwidget")
local TitleBar = require("ui/widget/titlebar")
local UIManager = require("ui/uimanager")
local VerticalGroup = require("ui/widget/verticalgroup")
local VerticalSpan = require("ui/widget/verticalspan")
local Input = Device.input
local Screen = Device.screen
local logger = require("logger")
local _ = require("gettext")
-- We use the BookMapRow widget, a local widget defined in bookmapwidget.lua,
-- that we made available via BookMapWidget itself
local BookMapWidget = require("ui/widget/bookmapwidget")
local BookMapRow = BookMapWidget.BookMapRow
-- PageBrowserWidget: shows thumbnails of pages
local PageBrowserWidget = InputContainer:new{
title = _("Page browser"),
-- Focus page: will be put at the best place in the thumbnail grid
-- (that is, the grid will pick thumbnails from pages before and
-- after it, and more pages after than before)
focus_page = nil,
-- Should only be nil on the first launch via ReaderThumbnail
launcher = nil,
_mirroredUI = BD.mirroredUILayout(),
}
function PageBrowserWidget:init()
-- Compute non-settings-dependant sizes and options
self.dimen = Geom:new{
w = Screen:getWidth(),
h = Screen:getHeight(),
}
self.covers_fullscreen = true -- hint for UIManager:_repaint()
if Device:hasKeys() then
self.key_events = {
Close = { {"Back"}, doc = "close page" },
ScrollRowUp = {{"Up"}, doc = "scroll up"},
ScrollRowDown = {{"Down"}, doc = "scrol down"},
ScrollPageUp = {{Input.group.PgBack}, doc = "prev page"},
ScrollPageDown = {{Input.group.PgFwd}, doc = "next page"},
}
end
if Device:isTouchDevice() then
self.ges_events.Swipe = {
GestureRange:new{
ges = "swipe",
range = self.dimen,
}
}
self.ges_events.MultiSwipe = {
GestureRange:new{
ges = "multiswipe",
range = self.dimen,
}
}
self.ges_events.Tap = {
GestureRange:new{
ges = "tap",
range = self.dimen,
}
}
self.ges_events.Hold = {
GestureRange:new{
ges = "hold",
range = self.dimen,
}
}
self.ges_events.Pinch = {
GestureRange:new{
ges = "pinch",
range = self.dimen,
}
}
self.ges_events.Spread = {
GestureRange:new{
ges = "spread",
range = self.dimen,
}
}
end
-- Put the BookMapRow left and right border outside of screen
self.row_width = self.dimen.w + 2*BookMapRow.pages_frame_border
self.title_bar = TitleBar:new{
fullscreen = true,
title = self.title,
left_icon = "notice-info",
left_icon_tap_callback = function() self:showHelp() end,
close_callback = function() self:onClose() end,
hold_close_callback = function() self:onClose(true) end,
show_parent = self,
}
self.title_bar_h = self.title_bar:getHeight()
-- Guess grid TOC span height from its font size
-- (it feels this font size does not need to be configurable: too large and
-- titles will be too easily truncated, too small and they will be unreadable)
self.toc_span_font_name = "infofont"
self.toc_span_font_size = 14
self.toc_span_face = Font:getFace(self.toc_span_font_name, self.toc_span_font_size)
local test_w = TextWidget:new{
text = "z",
face = self.toc_span_face,
}
self.span_height = test_w:getSize().h + BookMapRow.toc_span_border
test_w:free()
self.min_nb_rows = 1
self.max_nb_rows = 6
self.min_nb_cols = 1
self.max_nb_cols = 6
self.ui.toc:fillToc()
self.max_toc_depth = self.ui.toc.toc_depth
-- We show the toc depth chosen in BookMapWidget, or all of it if not set
-- (nothing in this PageBrowserWidget to allow changing it)
self.nb_toc_spans = self.ui.doc_settings:readSetting("book_map_toc_depth", self.max_toc_depth)
-- Row will contain: nb_toc_spans + page slots + spacing (+ some borders)
self.statistics_enabled = self.ui.statistics and self.ui.statistics:isEnabled()
local page_slots_height_ratio = 1 -- default to 1 * span_height
if not self.statistics_enabled and self.nb_toc_spans > 0 then
-- Just enough to show page separators below toc spans
page_slots_height_ratio = 0.2
end
self.row_height = math.ceil((self.nb_toc_spans + page_slots_height_ratio + 1) * self.span_height + 2*BookMapRow.pages_frame_border)
self.grid_width = self.dimen.w
self.grid_height = self.dimen.h - self.title_bar_h - self.row_height
-- We'll draw some kind of static transparent glass over the BookMapRow,
-- which should span over the page slots that get their thumbnails shown.
self.view_finder_r = Size.radius.window
self.view_finder_bw = Size.border.default
-- Have its top border noticable above the BookMapRow top border
self.view_finder_y = self.dimen.h - self.row_height - 2*self.view_finder_bw
-- And put its bottom rounded corner outside of screen
self.view_finder_h = self.row_height + 2*self.view_finder_bw + Size.radius.window
self.grid = OverlapGroup:new{
dimen = Geom:new{
w = self.grid_width,
h = self.grid_height,
},
allow_mirroring = false,
}
self.row = CenterContainer:new{
dimen = Geom:new{
w = self.dimen.w,
h = self.row_height,
},
-- Will contain a BookMapRow wider, with l/r borders outside screen
}
self[1] = FrameContainer:new{
width = self.dimen.w,
height = self.dimen.h,
padding = 0,
margin = 0,
bordersize = 0,
background = Blitbuffer.COLOR_WHITE,
VerticalGroup:new{
align = "center",
self.title_bar,
self.grid,
self.row,
}
}
-- Get some info that shouldn't change across calls to update() and updateLayout()
self.nb_pages = self.ui.document:getPageCount()
self.cur_page = self.ui.toc.pageno
-- Get bookmarks and highlights from ReaderBookmark
self.bookmarked_pages = self.ui.bookmark:getBookmarkedPages()
-- Get read page from the statistics plugin if enabled
self.read_pages = self.ui.statistics and self.ui.statistics:getCurrentBookReadPages()
self.current_session_duration = self.ui.statistics and (os.time() - self.ui.statistics.start_current_period)
-- Hidden flows, for first page display, and to draw them gray
self.has_hidden_flows = self.ui.document:hasHiddenFlows()
if self.has_hidden_flows and #self.ui.document.flows > 0 then
self.hidden_flows = {}
-- Pick into credocument internal data to build a table
-- of {first_page_number, last_page_number) for each flow
for flow, tab in ipairs(self.ui.document.flows) do
table.insert(self.hidden_flows, { tab[1], tab[1]+tab[2]-1 })
end
end
-- Reference page numbers, for first row page display
self.page_labels = nil
if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then
self.page_labels = self.ui.document:getPageMap()
end
-- Location stack
self.previous_locations = self.ui.link:getPreviousLocationPages()
-- Compute settings-dependant sizes and options, and build the inner widgets
-- (this will call self:update())
self:updateLayout()
end
function PageBrowserWidget:updateLayout()
self.nb_rows = self.ui.doc_settings:readSetting("page_browser_nb_rows")
or G_reader_settings:readSetting("page_browser_nb_rows")
self.nb_cols = self.ui.doc_settings:readSetting("page_browser_nb_cols")
or G_reader_settings:readSetting("page_browser_nb_cols")
if not self.nb_rows or not self.nb_cols then
-- 3 x 2 seems like a good default, in both portrait or landscape mode
self.nb_cols = 3
self.nb_rows = 2
end
self.nb_grid_items = self.nb_rows * self.nb_cols
-- Set our items target size
self.grid_item_margin = Screen:scaleBySize(10) -- borders will eat into this, it should be larger than borders thin+thick
self.grid_item_height = math.floor((self.grid_height - (self.nb_rows)*self.grid_item_margin) / self.nb_rows) -- no need for top margin, title bottom padding is enough
self.grid_item_width = math.floor((self.grid_width - (1+self.nb_cols)*self.grid_item_margin) / self.nb_cols)
self.grid_item_dimen = Geom:new{
w = self.grid_item_width,
h = self.grid_item_height
}
self.grid:clear()
for idx = 1, self.nb_grid_items do
local row = math.floor((idx-1)/self.nb_cols) -- start from 0
local col = (idx-1) % self.nb_cols
if self._mirroredUI then
col = self.nb_cols - col - 1
end
local offset_x = self.grid_item_margin*(col+1) + self.grid_item_width*col
local offset_y = self.grid_item_margin*(row) + self.grid_item_height*row -- no need for 1st margin
local grid_item = CenterContainer:new{
dimen = self.grid_item_dimen:copy(),
}
table.insert(self.grid, FrameContainer:new{
overlap_offset = {offset_x, offset_y},
margin = 0,
padding = 0,
bordersize = 0,
background = Blitbuffer.COLOR_WHITE,
grid_item,
})
end
-- Put the focused (requested) page at some appropriate place in the grid
if self.nb_rows > 1 then -- Multiple rows
-- Show the focus page at the rightmost position in the first row
self.focus_page_shift = self.nb_cols - 1
else -- Single row
if self.nb_cols > 2 then -- 3+ columns: show one page behind only
self.focus_page_shift = 1
else -- 1 or 2 columns: show it first
self.focus_page_shift = 0
end
end
-- Don't go with too small page slots
self.pages_per_row = math.max(self.nb_grid_items*3, 20)
-- We want our view finder centered over the BookMapRow
if self.pages_per_row % 2 ~= self.nb_grid_items % 2 then
self.pages_per_row = self.pages_per_row + 1
end
-- Update the BookMapRow and page thumbnails for the current view
self:update()
end
function PageBrowserWidget:update()
if self.requests_batch_id then
self.ui.thumbnail:cancelPageThumbnailRequests(self.requests_batch_id)
end
self.requests_batch_id = "PageBrowserWidget"..tostring(os.time())
if not self.focus_page then
self.focus_page = self.cur_page or 1
end
local grid_page_start = self.focus_page - self.focus_page_shift
local grid_page_end = grid_page_start + self.nb_grid_items - 1
-- Get p_start so that our viewfinder is centered
local p_start = math.ceil(grid_page_start + self.nb_grid_items/2 - self.pages_per_row/2)
local p_end = p_start + self.pages_per_row - 1
local blank_page_slots_before_start = 0
local blank_page_slots_after_end = 0 -- used only when _mirroredUI
if p_end > self.nb_pages then
blank_page_slots_after_end = p_end - self.nb_pages
p_end = self.nb_pages
end
if p_start < 1 then
blank_page_slots_before_start = 1 - p_start
p_start = 1
end
-- Show the page number or label at the bottom page slot every N slots, with N
-- the nb of thumbnails so we get at least one page label in our viewport.
local page_texts_cycle = math.min(self.nb_grid_items, 10) -- but max 10
local next_p = p_start
local cur_page_label_idx = 1
local page_texts = {}
for p=p_start, p_end do
if p >= next_p then
-- Only show a page text if there is no indicator on that slot
if p ~= self.cur_page and not self.bookmarked_pages[p] and not self.previous_locations[p] then
local page_text
if self.page_labels then
local page_label
for idx=cur_page_label_idx, #self.page_labels do
local item = self.page_labels[idx]
if item.page >= p then
if item.page == p then
page_label = item.label
end
break
end
cur_page_label_idx = idx
end
if page_label then
page_text = self.ui.pagemap:cleanPageLabel(page_label)
end
elseif self.has_hidden_flows then
local flow = self.ui.document:getPageFlow(p)
if flow == 0 then
page_text = tostring(self.ui.document:getPageNumberInFlow(p))
else
page_text = string.format("[%d]%d", self.ui.document:getPageNumberInFlow(p), self.ui.document:getPageFlow(p))
end
else
page_text = tostring(p)
end
if page_text then
local page_block, page_block_dx -- centered by default
if p == p_start or p == grid_page_start or p == grid_page_end+1 then
page_block = "left"
page_block_dx = Size.padding.tiny
if p == grid_page_start then
page_block_dx = page_block_dx + self.view_finder_bw + 1
end
elseif p == p_end or p == grid_page_end or p == grid_page_start-1 then
page_block = "right"
page_block_dx = Size.padding.tiny
if p == grid_page_end then
page_block_dx = page_block_dx + self.view_finder_bw + 1
end
end
page_texts[p] = {
text = page_text,
block = page_block,
block_dx = page_block_dx,
}
next_p = p + page_texts_cycle
end
end
end
end
-- We need to rebuilt the full set of toc spans that will be shown
-- Similar (but simplified) to what is done in BookMapWidget.
self.toc_depth = self.nb_toc_spans
local toc = self.ui.toc.toc
local cur_toc_items = {}
local row_toc_items = {}
local toc_idx = 1
while toc_idx <= #toc do
-- Find out the toc items that can be shown on this row
local item = toc[toc_idx]
if item.page > p_end then
break
end
if item.depth <= self.toc_depth then -- ignore lower levels we won't show
-- An item at level N closes all previous items at level >= N
for lvl = item.depth, self.toc_depth do
local done_toc_item = cur_toc_items[lvl]
cur_toc_items[lvl] = nil
if done_toc_item then
done_toc_item.p_end = math.max(item.page - 1, done_toc_item.p_start)
if done_toc_item.p_end >= p_start then
-- Can go into row_toc_items[lvl]
if done_toc_item.p_start < p_start then
done_toc_item.p_start = p_start
done_toc_item.started_before = true -- no left margin
end
if not row_toc_items[lvl] then
row_toc_items[lvl] = {}
end
-- We're done with it, we can just move it
table.insert(row_toc_items[lvl], done_toc_item)
end
end
end
cur_toc_items[item.depth] = {
title = item.title,
p_start = item.page,
p_end = nil,
}
end
toc_idx = toc_idx + 1
end
local is_last_row = p_end >= self.nb_pages
for lvl = 1, self.nb_toc_spans do -- (no-op/no-loop if flat_map)
local active_toc_item = cur_toc_items[lvl]
if active_toc_item then
if active_toc_item.p_start < p_start then
active_toc_item.p_start = p_start
active_toc_item.started_before = true -- no left margin
end
active_toc_item.p_end = p_end
active_toc_item.continues_after = not is_last_row -- no right margin (except if last row)
-- Look at next TOC item to see if it would close this one
local coming_up_toc_item = toc[toc_idx]
if coming_up_toc_item and coming_up_toc_item.page == p_end+1 and coming_up_toc_item.depth <= lvl then
active_toc_item.continues_after = false -- right margin
end
if not row_toc_items[lvl] then
row_toc_items[lvl] = {}
end
table.insert(row_toc_items[lvl], active_toc_item)
end
end
local left_spacing = 0
if blank_page_slots_before_start > 0 then
left_spacing = BookMapRow:getLeftSpacingForNumberOfPageSlots(blank_page_slots_before_start, self.pages_per_row, self.row_width)
end
local row = BookMapRow:new{
height = self.row_height,
width = self.row_width,
show_parent = self,
left_spacing = left_spacing,
nb_toc_spans = self.nb_toc_spans,
span_height = self.span_height,
font_face = self.toc_span_face,
start_page_text = "",
start_page = p_start,
end_page = p_end,
pages_per_row = self.pages_per_row - blank_page_slots_before_start,
cur_page = self.cur_page,
with_page_sep = true,
toc_items = row_toc_items,
bookmarked_pages = self.bookmarked_pages,
previous_locations = self.previous_locations,
hidden_flows = self.hidden_flows,
read_pages = self.read_pages,
current_session_duration = self.current_session_duration,
page_texts = page_texts,
}
self.row[1] = row
if self._mirroredUI then
self.view_finder_x = row:getPageX(grid_page_end)
self.view_finder_w = row:getPageX(grid_page_start, true) - self.view_finder_x
if blank_page_slots_after_end > 0 then
self.view_finder_x = self.view_finder_x
+ BookMapRow:getLeftSpacingForNumberOfPageSlots(blank_page_slots_after_end, self.pages_per_row, self.row_width)
+ row.pages_frame_border -- (needed, but not sure why it is needed...)
end
else
self.view_finder_x = row:getPageX(grid_page_start)
self.view_finder_w = row:getPageX(grid_page_end, true) - self.view_finder_x
self.view_finder_x = self.view_finder_x + left_spacing
end
-- we requested with_page_sep, so leave these blank spaces between page slots outside the viewfinder
self.view_finder_x = self.view_finder_x + 1
self.view_finder_w = self.view_finder_w - 1
for idx=1, self.nb_grid_items do
local p = grid_page_start + idx - 1
if p < 1 or p > self.nb_pages then
self.grid[idx].page_idx = nil -- no action on Tap
self:clearTile(idx)
else
self.grid[idx].page_idx = p -- go there on Tap
local delayed = self.ui.thumbnail:getPageThumbnail(p, self.grid_item_width, self.grid_item_height, self.requests_batch_id, function(tile, batch_id, async_response)
if batch_id ~= self.requests_batch_id then
-- Response from an obsolete request
return
end
if not tile then -- failure notification
return
end
-- If tile was in the cache, we get this immediately called with async_response=false,
-- and we don't need to do any setDirty as a full one will be done below.
self:showTile(idx, p, tile, async_response)
end)
if delayed then
self:clearTile(idx, true)
self.wait_for_refresh_on_show_tile = true
end
end
end
UIManager:setDirty(self, function()
return "ui", self.dimen
end)
end
function PageBrowserWidget:paintTo(bb, x, y)
-- Paint regular sub widgets the classic way
InputContainer.paintTo(self, bb, x, y)
-- If we would prefer to see the BookMapRow top border always take the full width
-- so it acts as a separator from the thumbnail grid, add this:
-- bb:paintRect(0, self.dimen.h - self.row_height, self.dimen.w, BookMapRow.pages_frame_border, Blitbuffer.COLOR_BLACK)
-- And explicitely paint our viewfinder over the BookMapRow
bb:paintBorder(self.view_finder_x, self.view_finder_y, self.view_finder_w, self.view_finder_h,
self.view_finder_bw, Blitbuffer.COLOR_BLACK, self.view_finder_r)
end
function PageBrowserWidget:clearTile(grid_idx, in_progress, do_refresh)
local item_frame = self.grid[grid_idx] -- FrameContainer
local item_container = item_frame[1] -- CenterContainer
local dimen = item_frame.dimen
if item_container[1] then -- TextWidget or FrameContainer
if item_container[1].dimen then
dimen = item_container[1].dimen:copy()
end
if item_container[1].free then
item_container[1]:free()
end
end
-- Quickly showing the first tile while the whole page is still being refreshed
-- can cause some papercut-like refresh glitch on this first tile, with even more
-- chances if we put gray things in the initial page (as gray is painted black
-- and then becomes gray, making it 2 steps and longer).
-- This seems to be mitigated with our self.wait_for_refresh_on_show_tile trick.
if in_progress then
item_container[1] = TextWidget:new{
text = "", -- gray symbol (which initially caused refresh glitches)
-- Alternatives (mostly from Nerdfont):
-- text = "\u{26F6}", -- square with four corners
-- text = "\u{ED36}",
-- text = "\u{F196}", -- square with plus inside
-- text = "\u{ED5F}", -- square with plus at top right
-- text = "\u{F141}",
-- text = "\u{EB52}",
-- text = "\u{EB4F}",
-- text = "\u{F021}",
face = Font:getFace("cfont", 20),
}
else
item_container[1] = VerticalSpan:new{ width = 0, }
end
if do_refresh then
UIManager:setDirty(self, function()
return "ui", dimen
end)
end
end
function PageBrowserWidget:showTile(grid_idx, page, tile, do_refresh)
local item_frame = self.grid[grid_idx] -- FrameContainer
local item_container = item_frame[1] -- CenterContainer
if item_container[1] and item_container[1].free then -- TextWidget
item_container[1]:free()
end
local border = page == self.cur_page and Size.border.thick or Size.border.thin
local thumb_frame = FrameContainer:new{
is_page_thumbnail = true, -- for tap handler
margin = 0,
padding = 0,
bordersize = border,
background = Blitbuffer.COLOR_WHITE,
ImageWidget:new{
image = tile.bb,
image_disposable = false,
},
}
item_container[1] = thumb_frame
-- thumb_frame will overflow its CenterContainer because of the added borders,
-- but CenterContainer handles that well. We will refresh the outer dimensions.
if do_refresh then
if self.wait_for_refresh_on_show_tile then
self.wait_for_refresh_on_show_tile = nil
-- Be sure the main view initial refresh has ended before refreshing
-- this first thumbnail, to avoid papercut refresh glitches.
UIManager:waitForVSync()
end
UIManager:setDirty(self, function()
return "ui", thumb_frame.dimen
end)
end
end
function PageBrowserWidget:showHelp()
UIManager:show(InfoMessage:new{
text = [[
Page browser shows thumbnails of pages.
The bottom row displays an extract of the book map around the shown pages: see the book map help for details.
Swipe along the top or left screen edge to change the number of columns or rows.
Swipe vertically to move one row, horizontally to move one page.
Swipe horizontally in the bottom row to move by the full stripe.
Tap in the bottom row on a page to focus thumbnails on this page.
Tap on a thumbnail to go read this page.
Any multiswipe will close the page browser.]],
})
end
function PageBrowserWidget:onClose(close_all_parents)
if self.requests_batch_id then
self.ui.thumbnail:cancelPageThumbnailRequests(self.requests_batch_id)
end
logger.dbg("closing PageBrowserWidget")
-- Close this widget
UIManager:close(self)
if self.launcher then
-- We were launched by a BookMapWidget, don't do any cleanup.
if close_all_parents then
-- The last one of these (which has no launcher attribute)
-- will do the cleanup below.
self.launcher:onClose(true)
end
else
-- Remove all thumbnails generated for a different target size than
-- the last one used (no need to keep old sizes if the user played
-- with nb_cols/nb_rows, as on next opening, we just need the ones
-- with the current size to be available)
self.ui.thumbnail:tidyCache()
-- Force a GC to free the memory used by the widgets and tiles
-- (delay it a bit so this pause is less noticable)
UIManager:scheduleIn(0.5, function()
collectgarbage()
collectgarbage()
end)
-- As we're getting back to Reader, do a full flashing refresh to remove
-- any ghost trace of thumbnails or black page slots
UIManager:setDirty(self.ui.dialog, "full")
end
return true
end
function PageBrowserWidget:saveSettings(reset)
if reset then
self.nb_rows = nil
self.nb_cols = nil
end
self.ui.doc_settings:saveSetting("page_browser_nb_rows", self.nb_rows)
self.ui.doc_settings:saveSetting("page_browser_nb_cols", self.nb_cols)
-- We also save them as global settings, so they will apply on other books
-- where they were not already set
G_reader_settings:saveSetting("page_browser_nb_rows", self.nb_rows)
G_reader_settings:saveSetting("page_browser_nb_cols", self.nb_cols)
end
function PageBrowserWidget:updateNbCols(value, relative)
local new_nb_cols
if relative then
new_nb_cols = self.nb_cols + value
else
new_nb_cols = value
end
if new_nb_cols < self.min_nb_cols then
new_nb_cols = self.min_nb_cols
end
if new_nb_cols > self.max_nb_cols then
new_nb_cols = self.max_nb_cols
end
if new_nb_cols == self.nb_cols then
return false
end
self.nb_cols = new_nb_cols
self:saveSettings()
return true
end
function PageBrowserWidget:updateNbRows(value, relative)
local new_nb_rows
if relative then
new_nb_rows = self.nb_rows + value
else
new_nb_rows = value
end
if new_nb_rows < self.min_nb_rows then
new_nb_rows = self.min_nb_rows
end
if new_nb_rows > self.max_nb_rows then
new_nb_rows = self.max_nb_rows
end
if new_nb_rows == self.nb_rows then
return false
end
self.nb_rows = new_nb_rows
self:saveSettings()
return true
end
function PageBrowserWidget:updateFocusPage(value, relative)
local new_focus_page
if relative then
new_focus_page = self.focus_page + value
else
new_focus_page = value
end
if new_focus_page < 1 then
new_focus_page = 1
end
if new_focus_page > self.nb_pages then
new_focus_page = self.nb_pages
end
if new_focus_page == self.focus_page then
return false
end
self.focus_page = new_focus_page
return true
end
function PageBrowserWidget:onScrollPageUp()
if self:updateFocusPage(-self.nb_grid_items, true) then
self:update()
end
return true
end
function PageBrowserWidget:onScrollPageDown()
if self:updateFocusPage(self.nb_grid_items, true) then
self:update()
end
return true
end
function PageBrowserWidget:onScrollRowUp()
if self:updateFocusPage(-self.nb_cols, true) then
self:update()
end
return true
end
function PageBrowserWidget:onScrollRowDown()
if self:updateFocusPage(self.nb_cols, true) then
self:update()
end
return true
end
function PageBrowserWidget:onSwipe(arg, ges)
local direction = BD.flipDirectionIfMirroredUILayout(ges.direction)
if direction == "north" or direction == "south" then
-- Swipe along the screen left edge: increase/decrease nb of thumbnail rows
-- (Should this be mirrored if RTL UI? It would be consistent with how it
-- happens in BookMapWidget - but here, having it on the left is to have it
-- less accessible to right handed people so they can scroll up/down more
-- easily.)
if ges.pos.x < Screen:getWidth() * 1/8 then
local rel = direction == "north" and 1 or -1
if self:updateNbRows(rel, true) then
self:updateLayout()
end
return true
else
-- As onScrollRowUp/Down()
local rel = direction == "north" and 1 or -1
if self:updateFocusPage(rel*self.nb_cols, true) then
self:update()
end
return true
end
elseif direction == "west" or direction == "east" then
if ges.pos.y < Screen:getHeight() * 1/8 then
-- Swipe along the screen top edge: increase/decrease nb of thumbnail cols
local rel = direction == "west" and 1 or -1
if self:updateNbCols(rel, true) then
self:updateLayout()
end
return true
elseif ges.pos.y > Screen:getHeight() - self.row_height then
-- Inside BookMapRow at bottom: scroll by a full pages_per_row
-- (Handling pan and hold/pan/release when started on view finder
-- would be nice, as it might be an intuitive naive action on
-- this area... but well...)
local rel = direction == "west" and 1 or -1
if self:updateFocusPage(rel*self.pages_per_row, true) then
self:update()
end
return true
else
-- As onScrollPageUp/Down()
local rel = direction == "west" and 1 or -1
if self:updateFocusPage(rel*self.nb_grid_items, true) then
self:update()
end
return true
end
else
-- diagonal swipe
-- trigger full refresh
UIManager:setDirty(nil, "full")
-- a long diagonal swipe may also be used for taking a screenshot,
-- so let it propagate
return false
end
end
function PageBrowserWidget:onPinch(arg, ges)
if ges.direction == "horizontal" then
if self:updateNbCols(1, true) then
self:updateLayout()
end
elseif ges.direction == "vertical" then
if self:updateNbRows(1, true) then
self:updateLayout()
end
elseif ges.direction == "diagonal" then
local updated = self:updateNbCols(1, true)
updated = self:updateNbRows(1, true) or updated
if updated then
self:updateLayout()
end
end
return true
end
function PageBrowserWidget:onSpread(arg, ges)
if ges.direction == "horizontal" then
if self:updateNbCols(-1, true) then
self:updateLayout()
end
elseif ges.direction == "vertical" then
if self:updateNbRows(-1, true) then
self:updateLayout()
end
elseif ges.direction == "diagonal" then
local updated = self:updateNbCols(-1, true)
updated = self:updateNbRows(-1, true) or updated
if updated then
self:updateLayout()
end
end
return true
end
function PageBrowserWidget:onMultiSwipe(arg, ges)
-- All swipes gestures are used for navigation.
-- Allow for quick closing with any multiswipe.
self:onClose()
return true
end
function PageBrowserWidget:onTap(arg, ges)
-- If tap in the bottom BookMapRow, put page at tap position
-- as focus page, so it goes into our viewfinder
if ges.pos.y > Screen:getHeight() - self.row_height then
local page = self.row[1]:getPageAtX(ges.pos.x)
if page then
-- Have it in the middle of viewfinder, and not where
-- the self.focus_page_shift would put it
page = page - math.floor(self.nb_grid_items/2) + self.focus_page_shift
if self:updateFocusPage(page, false) then
self:update()
end
end
return true
end
-- Tap on title: do nothing
if ges.pos.y < self.title_bar_h then
return true
end
-- If tap on a thumbnail, close widget and go to that page
for idx=1, self.nb_grid_items do
if ges.pos:intersectWith(self.grid[idx].dimen) then
local page = self.grid[idx].page_idx
if page and self.grid[idx][1][1].is_page_thumbnail then
-- Only allow tap on fully displayed thumbnails.
-- Also, a thumbnail might be smaller than the original grid
-- item dimension. Be sure the tap is on it (otherwise, it's
-- a tap in the inter thumbnail margin, that we'd rather not
-- handle)
local thumb_frame = self.grid[idx][1][1]
if ges.pos:intersectWith(thumb_frame.dimen) then
-- On PDF documents, jumping to a page may block for a few
-- seconds while the page is rendered. So, make the border
-- bigger so the user knows his tap is being processed.
local orig_bordersize = thumb_frame.bordersize
thumb_frame.bordersize = Size.border.thick * 2
local b_inc = thumb_frame.bordersize - orig_bordersize
UIManager:widgetRepaint(thumb_frame, thumb_frame.dimen.x-b_inc, thumb_frame.dimen.y-b_inc)
Screen:refreshFast(thumb_frame.dimen.x, thumb_frame.dimen.y, thumb_frame.dimen.w, thumb_frame.dimen.h)
-- (refresh "fast" will make gray drawn black and may make the
-- thumbnail a little uglier - but this enhances the effect
-- of "being processed"!)
-- Close the BookMapWidget that launched this PageBrowser
-- and all their ancestors up to Reader
self:onClose(true)
self.ui.link:addCurrentLocationToStack()
self.ui:handleEvent(Event:new("GotoPage", page))
-- Note: with ReaderPaging, if we tap on the thumbnail for the current
-- page, nothing would be refreshed. Our :onClose(true) will have the
-- last ancestor issue a full refresh that will ensure it is painted.
return true
end
end
break
end
end
-- If tap on a blank area, handle as prev/next page, so people
-- not friend with swipe can still move around
if BD.flipIfMirroredUILayout(ges.pos.x < Screen:getWidth()/2) then
self:onScrollPageUp()
else
self:onScrollPageDown()
end
return true
end
function PageBrowserWidget:onHold(arg, ges)
-- If hold in the bottom BookMapRow, open a new BookMapWidget
-- and focus on this page. We'll show a rounded square below
-- our current focus_page to help locating where we were (it's
-- quite more complicated to draw a rounded rectangle around
-- multiple pages to figure our view finder, as these pages
-- may be splitted onto multiple BookMapRows...)
if ges.pos.y > Screen:getHeight() - self.row_height then
local page = self.row[1]:getPageAtX(ges.pos.x)
if page then
local extra_symbols_pages = {}
extra_symbols_pages[self.focus_page] = 0x25A2 -- white square with rounder corners
UIManager:show(BookMapWidget:new{
launcher = self,
ui = self.ui,
focus_page = page,
extra_symbols_pages = extra_symbols_pages,
})
end
return true
end
return true
end
return PageBrowserWidget

@ -234,6 +234,10 @@ function ReaderStatistics:initData()
end
end
function ReaderStatistics:isEnabled()
return self.settings.is_enabled
end
-- Reset the (volatile) stats on page count changes (e.g., after a font size update)
function ReaderStatistics:onUpdateToc()
local new_pagecount = self.view.document:getPageCount()
@ -2454,4 +2458,41 @@ function ReaderStatistics:onShowCalendarView()
UIManager:show(self:getCalendarView())
end
function ReaderStatistics:getCurrentBookReadPages()
if self:isDocless() or not self.settings.is_enabled then return end
self:insertDB(self.id_curr_book)
local sql_stmt = [[
SELECT
page,
min(sum(duration), ?) AS durations,
strftime("%s", "now") - max(start_time) AS delay
FROM page_stat
WHERE id_book = ?
GROUP BY page
ORDER BY page;
]]
local conn = SQ3.open(db_location)
local stmt = conn:prepare(sql_stmt)
local res, nb = stmt:reset():bind(self.settings.max_sec, self.id_curr_book):resultset("i")
stmt:close()
conn:close()
local read_pages = {}
local max_duration = 0
for i=1, nb do
local page, duration, delay = res[1][i], res[2][i], res[3][i]
page = tonumber(page)
duration = tonumber(duration)
delay = tonumber(delay)
read_pages[page] = {duration, delay}
if duration > max_duration then
max_duration = duration
end
end
for page, info in pairs(read_pages) do
-- Make the value a duration ratio (vs capped or max duration)
read_pages[page][1] = info[1] / max_duration
end
return read_pages
end
return ReaderStatistics

Loading…
Cancel
Save