2
0
mirror of https://github.com/koreader/koreader synced 2024-11-11 19:11:14 +00:00
koreader/frontend/apps/reader/modules/readerfooter.lua
Frans de Jonge 92b9139294 ReaderFooter (lang): "Show toc markers" to "Show chapter markers"
I think it sounds more natural as chapter than as table of content, which is an implementation detail (i.e., the chapter markers are shown based on what's in the TOC).

Also removed redundant "this" from "Remaining pages in this chapter"
2017-04-08 01:39:25 -07:00

641 lines
22 KiB
Lua

local Blitbuffer = require("ffi/blitbuffer")
local BottomContainer = require("ui/widget/container/bottomcontainer")
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 HorizontalGroup = require("ui/widget/horizontalgroup")
local HorizontalSpan = require("ui/widget/horizontalspan")
local ProgressWidget = require("ui/widget/progresswidget")
local RightContainer = require("ui/widget/container/rightcontainer")
local TextWidget = require("ui/widget/textwidget")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local util = require("util")
local _ = require("gettext")
local Screen = Device.screen
local MODE = {
off = 0,
page_progress = 1,
time = 2,
pages_left = 3,
battery = 4,
percentage = 5,
book_time_to_read = 6,
chapter_time_to_read = 7,
frontlight = 8,
mem_usage = 9,
}
local MODE_NB = 0
local MODE_INDEX = {}
for k,v in pairs(MODE) do
MODE_INDEX[v] = k
MODE_NB = MODE_NB + 1
end
-- functions that generates footer text for each mode
local footerTextGeneratorMap = {
empty = function() return "" end,
frontlight = function()
if not Device:hasFrontlight() then return "L: NA" end
local powerd = Device:getPowerDevice()
if powerd.is_fl_on ~= nil and powerd.is_fl_on == true then
if powerd.fl_intensity ~= nil then
return string.format("L: %d%%", powerd.fl_intensity)
end
else
return "L: Off"
end
end,
battery = function()
local powerd = Device:getPowerDevice()
return "B:" .. (powerd:isCharging() and "+" or "") .. powerd:getCapacity() .. "%"
end,
time = function()
return os.date("%H:%M")
end,
page_progress = function(footer)
if footer.pageno then
return string.format("%d / %d", footer.pageno, footer.pages)
else
return string.format("%d / %d", footer.position, footer.doc_height)
end
end,
pages_left = function(footer)
local left = footer.ui.toc:getChapterPagesLeft(
footer.pageno, footer.toc_level)
return "=> " .. (left and left or footer.pages - footer.pageno)
end,
percentage = function(footer)
return string.format("R:%1.f%%", footer.progress_bar.percentage * 100)
end,
book_time_to_read = function(footer)
local current_page
if footer.view.document.info.has_pages then
current_page = footer.ui.paging.current_page
else
current_page = footer.view.document:getCurrentPage()
end
return footer:getDataFromStatistics("TB: ", footer.pages - current_page)
end,
chapter_time_to_read = function(footer)
local left = footer.ui.toc:getChapterPagesLeft(
footer.pageno, footer.toc_level)
return footer:getDataFromStatistics(
"TC: ", (left and left or footer.pages - footer.pageno))
end,
mem_usage = function(footer)
local statm = io.open("/proc/self/statm", "r")
if statm then
local infos = statm:read("*all")
statm:close()
local rss = infos:match("^%S+ (%S+) ")
-- we got the nb of 4Kb-pages used, that we convert to Mb
rss = math.floor(tonumber(rss) * 4096 / 1024 / 1024)
return string.format("M:%d", rss)
end
return ""
end,
}
local ReaderFooter = WidgetContainer:extend{
mode = MODE.page_progress,
pageno = nil,
pages = nil,
toc_level = 0,
progress_percentage = 0.0,
footer_text = nil,
text_font_face = "ffont",
text_font_size = DMINIBAR_FONT_SIZE,
bar_height = Screen:scaleBySize(DMINIBAR_HEIGHT),
height = Screen:scaleBySize(DMINIBAR_CONTAINER_HEIGHT),
horizontal_margin = Screen:scaleBySize(10),
text_left_margin = Screen:scaleBySize(10),
settings = {},
-- added to expose them to unit tests
textGeneratorMap = footerTextGeneratorMap,
}
function ReaderFooter:init()
self.pageno = self.view.state.page
self.settings = G_reader_settings:readSetting("footer") or {
disabled = false,
all_at_once = false,
toc_markers = true,
battery = true,
time = true,
page_progress = true,
pages_left = true,
percentage = true,
book_time_to_read = true,
chapter_time_to_read = true,
frontlight = false,
mem_usage = false,
}
if self.settings.disabled then
-- footer featuren disabled completely, stop initialization now
self:disableFooter()
return
end
self.has_no_mode = true
for _, m in ipairs(MODE_INDEX) do
if self.settings[m] then
self.has_no_mode = false
break
end
end
self.footer_text = TextWidget:new{
text = '',
face = Font:getFace(self.text_font_face, self.text_font_size),
}
-- all width related values will be initialized in self:resetLayout()
self.text_width = 0
self.progress_bar = ProgressWidget:new{
width = nil,
height = self.bar_height,
percentage = self.progress_percentage,
tick_width = DMINIBAR_TOC_MARKER_WIDTH,
ticks = nil, -- ticks will be populated in self:updateFooterText
last = nil, -- last will be initialized in self:updateFooterText
}
local margin_span = HorizontalSpan:new{ width = self.horizontal_margin }
self.horizontal_group = HorizontalGroup:new{ margin_span }
self.text_container = RightContainer:new{
dimen = Geom:new{ w = 0, h = self.height },
self.footer_text,
}
table.insert(self.horizontal_group, self.progress_bar)
table.insert(self.horizontal_group, self.text_container)
table.insert(self.horizontal_group, margin_span)
self.footer_content = FrameContainer:new{
self.horizontal_group,
background = Blitbuffer.COLOR_WHITE,
bordersize = 0,
padding = 0,
}
self.footer_container = BottomContainer:new{
dimen = Geom:new{ w = 0, h = self.height*2 },
self.footer_content,
}
self.footer_positioner = BottomContainer:new{
dimen = Geom:new{},
self.footer_container,
}
self[1] = self.footer_positioner
self.mode = G_reader_settings:readSetting("reader_footer_mode") or self.mode
if self.settings.all_at_once then
self.view.footer_visible = (self.mode ~= MODE.off)
self:updateFooterTextGenerator()
else
self:applyFooterMode()
end
if self.settings.auto_refresh_time then
self:setupAutoRefreshTime()
end
end
function ReaderFooter:setupAutoRefreshTime()
if not self.autoRefreshTime then
self.autoRefreshTime = function()
self:updateFooter()
UIManager:scheduleIn(61 - tonumber(os.date("%S")), self.autoRefreshTime)
end
end
self.onCloseDocument = function()
UIManager:unschedule(self.autoRefreshTime)
end
UIManager:scheduleIn(61 - tonumber(os.date("%S")), self.autoRefreshTime)
end
function ReaderFooter:setupTouchZones()
if not Device:isTouchDevice() then return end
local footer_screen_zone = {
ratio_x = DTAP_ZONE_MINIBAR.x, ratio_y = DTAP_ZONE_MINIBAR.y,
ratio_w = DTAP_ZONE_MINIBAR.w, ratio_h = DTAP_ZONE_MINIBAR.h,
}
self.ui:registerTouchZones({
{
id = "readerfooter_tap",
ges = "tap",
screen_zone = footer_screen_zone,
handler = function(ges) return self:onTapFooter(ges) end,
overrides = {
'tap_forward', 'tap_backward',
-- NOTE: readermenu_tap override is needed to keep behavior
-- consistent with the old code base in case of overlap between
-- footer and menu tap zones
'readermenu_tap',
},
},
{
id = "readerfooter_hold",
ges = "hold",
screen_zone = footer_screen_zone,
handler = function() return self:onHoldFooter() end,
overrides = {'readerhighlight_hold'},
},
})
end
-- call this method whenever the screen size changes
function ReaderFooter:resetLayout()
local new_screen_width = Screen:getWidth()
if new_screen_width == self._saved_screen_width then return end
local new_screen_height = Screen:getHeight()
self.progress_bar.width = math.floor(new_screen_width - self.text_width - self.horizontal_margin*2)
self.horizontal_group:resetLayout()
self.footer_positioner.dimen.w = new_screen_width
self.footer_positioner.dimen.h = new_screen_height
self.footer_container.dimen.w = new_screen_width
self.dimen = self.footer_positioner:getSize()
self._saved_screen_width = new_screen_width
end
function ReaderFooter:getHeight()
return self.footer_text:getSize().h
end
function ReaderFooter:disableFooter()
self.onReaderReady = function() end
self.resetLayout = function() end
self.onCloseDocument = nil
self.onPageUpdate = function() end
self.onPosUpdate = function() end
self.onUpdatePos = function() end
self.onSetStatusLine = function() end
self.mode = MODE.off
self.view.footer_visible = false
end
function ReaderFooter:updateFooterTextGenerator()
local footerTextGenerators = {}
for _, m in pairs(MODE_INDEX) do
if self.settings[m] then
table.insert(footerTextGenerators,
footerTextGeneratorMap[m])
if not self.settings.all_at_once then
-- if not show all at once, then one is enough
break
end
end
end
if #footerTextGenerators == 0 then
-- all modes are disabled
self.genFooterText = footerTextGeneratorMap.empty
elseif #footerTextGenerators == 1 then
-- there is only one mode enabled, simplify the generator
-- function to that one
self.genFooterText = footerTextGenerators[1]
else
self.footerTextGenerators = footerTextGenerators
self.genFooterText = self.genAllFooterText
end
-- notify caller that UI needs update
return true
end
local option_titles = {
all_at_once = _("Show all at once"),
toc_markers = _("Show chapter markers"),
auto_refresh_time = _("Auto refresh time"),
page_progress = _("Current page"),
time = _("Current time"),
pages_left = _("Pages left in chapter"),
battery = _("Battery status"),
percentage = _("Progress percentage"),
book_time_to_read = _("Book time to read"),
chapter_time_to_read = _("Chapter time to read"),
frontlight = _("Frontlight level"),
mem_usage = _("KOReader memory usage"),
}
function ReaderFooter:addToMainMenu(menu_items)
local sub_items = {}
menu_items.status_bar = {
text = _("Status bar"),
sub_item_table = sub_items,
}
-- menu item to fake footer tapping when touch area is disabled
if Geom:new{
x = DTAP_ZONE_MINIBAR.x,
y = DTAP_ZONE_MINIBAR.y,
w = DTAP_ZONE_MINIBAR.w,
h = DTAP_ZONE_MINIBAR.h
}:area() == 0 then
table.insert(sub_items, {
text = _("Toggle mode"),
enabled_func = function()
return not self.view.flipping_visible
end,
callback = function() self:onTapFooter() end,
})
end
-- footer is enabled, build the full status bar menu
local isEnabled = function()
return not self.settings.disabled
end
local getMinibarOption = function(option, callback)
return {
text = option_titles[option],
checked_func = function()
return self.settings[option] == true
end,
enabled_func = isEnabled,
callback = function()
self.settings[option] = not self.settings[option]
G_reader_settings:saveSetting("footer", self.settings)
-- only case that we don't need a UI update is enable/disable
-- non-current mode when all_at_once is disabled.
local should_update = false
local first_enabled_mode_num
local prev_has_no_mode = self.has_no_mode
self.has_no_mode = true
for mode_num, m in pairs(MODE_INDEX) do
if self.settings[m] then
first_enabled_mode_num = mode_num
self.has_no_mode = false
break
end
end
if callback then
should_update = callback(self)
elseif self.settings.all_at_once then
should_update = self:updateFooterTextGenerator()
elseif (MODE[option] == self.mode and self.settings[option] == false)
or (prev_has_no_mode ~= self.has_no_mode) then
-- current mode got disabled, redraw footer with other
-- enabled modes. if all modes are disabled, then only show
-- progress bar
if not self.has_no_mode then
self.mode = first_enabled_mode_num
end
should_update = true
self:applyFooterMode()
end
if should_update then
self:updateFooter()
UIManager:setDirty("all", "partial")
end
end,
}
end
table.insert(sub_items,
getMinibarOption("all_at_once", self.updateFooterTextGenerator))
table.insert(sub_items, getMinibarOption("toc_markers", self.setTocMarkers))
-- TODO: only enable auto refresh when time is shown
table.insert(sub_items, getMinibarOption("auto_refresh_time", function()
if self.settings.auto_refresh_time then
self:setupAutoRefreshTime()
else
UIManager:unschedule(self.autoRefreshTime)
self.onCloseDocument = nil
end
end))
table.insert(sub_items, getMinibarOption("page_progress"))
table.insert(sub_items, getMinibarOption("time"))
table.insert(sub_items, getMinibarOption("pages_left"))
table.insert(sub_items, getMinibarOption("battery"))
table.insert(sub_items, getMinibarOption("percentage"))
table.insert(sub_items, getMinibarOption("book_time_to_read"))
table.insert(sub_items, getMinibarOption("chapter_time_to_read"))
table.insert(sub_items, getMinibarOption("frontlight"))
table.insert(sub_items, getMinibarOption("mem_usage"))
end
-- this method will be updated at runtime based on user setting
function ReaderFooter:genFooterText() end
function ReaderFooter:genAllFooterText()
local info = {}
for _, gen in ipairs(self.footerTextGenerators) do
table.insert(info, gen(self))
end
return table.concat(info, " | ")
end
-- this function should never be called with footer is disabled
function ReaderFooter:setTocMarkers()
if self.settings.toc_markers then
if self.progress_bar.ticks ~= nil then return end
local ticks_candidates = {}
if self.ui.toc then
local max_level = self.ui.toc:getMaxDepth()
for i = 0, -max_level, -1 do
local ticks = self.ui.toc:getTocTicks(i)
table.insert(ticks_candidates, ticks)
end
-- find the finest toc ticks by sorting out the largest one
table.sort(ticks_candidates, function(a, b) return #a > #b end)
end
if #ticks_candidates > 0 then
self.progress_bar.ticks = ticks_candidates[1]
self.progress_bar.last = self.pages or self.view.document:getPageCount()
else
-- we still set ticks here so self.progress_bar.ticks will not be
-- initialized again if ticks_candidates is empty
self.progress_bar.ticks = {}
end
else
self.progress_bar.ticks = nil
end
-- notify caller that UI needs update
return true
end
function ReaderFooter:getDataFromStatistics(title, pages)
local statistics_data = self.ui.doc_settings:readSetting("stats")
local sec = 'na'
if statistics_data and statistics_data.performance_in_pages then
local read_pages = util.tableSize(statistics_data.performance_in_pages)
local average_time_per_page = statistics_data.total_time_in_sec / read_pages
sec = util.secondsToClock(pages * average_time_per_page, true)
end
return title .. sec
end
function ReaderFooter:updateFooter()
if self.pageno then
self:updateFooterPage()
else
self:updateFooterPos()
end
end
function ReaderFooter:updateFooterPage()
if type(self.pageno) ~= "number" then return end
self.progress_bar.percentage = self.pageno / self.pages
self:updateFooterText()
end
function ReaderFooter:updateFooterPos()
if type(self.position) ~= "number" then return end
self.progress_bar.percentage = self.position / self.doc_height
self:updateFooterText()
end
-- updateFooterText will start as a noop. After onReaderReady event is
-- received, it will initialized as _updateFooterText below
function ReaderFooter:updateFooterText()
end
-- only call this function after document is fully loaded
function ReaderFooter:_updateFooterText()
self.footer_text:setText(self:genFooterText())
if self.has_no_mode then
self.text_width = 0
else
self.text_width = self.footer_text:getSize().w + self.text_left_margin
end
self.progress_bar.width = math.floor(
self._saved_screen_width - self.text_width - self.horizontal_margin*2)
self.text_container.dimen.w = self.text_width
self.horizontal_group:resetLayout()
UIManager:setDirty(self.view.dialog, "ui", self.footer_content.dimen)
end
function ReaderFooter:onPageUpdate(pageno)
self.pageno = pageno
self.pages = self.view.document:getPageCount()
self:updateFooterPage()
end
function ReaderFooter:onPosUpdate(pos)
self.position = pos
self.doc_height = self.view.document.info.doc_height
self:updateFooterPos()
end
-- recalculate footer sizes when document page count is updated
-- see documentation for more info about this event.
ReaderFooter.onUpdatePos = ReaderFooter.updateFooter
function ReaderFooter:onReaderReady()
self.ui.menu:registerToMainMenu(self)
self:setupTouchZones()
self:resetLayout() -- set widget dimen
self:setTocMarkers()
self.updateFooterText = self._updateFooterText
self:updateFooter()
end
function ReaderFooter:applyFooterMode(mode)
-- three modes switcher for reader footer
-- 0 for footer off
-- 1 for footer page info
-- 2 for footer time info
-- 3 for footer next_chapter info
-- 4 for battery status
-- 5 for progress percentage
-- 6 for from statistics book time to read
-- 7 for from statistics chapter time to read
-- 8 for front light level
-- 9 for memory usage
if mode ~= nil then self.mode = mode end
self.view.footer_visible = (self.mode ~= MODE.off)
if not self.view.footer_visible or self.settings.all_at_once then return end
local mode_name = MODE_INDEX[self.mode]
if not self.settings[mode_name] or self.has_no_mode then
-- all modes disabled, only show progress bar
mode_name = "empty"
end
self.genFooterText = footerTextGeneratorMap[mode_name]
end
function ReaderFooter:onEnterFlippingMode()
self.orig_mode = self.mode
self:applyFooterMode(MODE.page_progress)
end
function ReaderFooter:onExitFlippingMode()
self:applyFooterMode(self.orig_mode)
end
function ReaderFooter:onTapFooter(ges)
if self.view.flipping_visible then
local pos = ges.pos
local dimen = self.progress_bar.dimen
-- if reader footer is not drawn before the dimen value should be nil
if dimen then
local percentage = (pos.x - dimen.x)/dimen.w
self.ui:handleEvent(Event:new("GotoPercentage", percentage))
end
else
if self.settings.all_at_once or self.has_no_mode then
if self.mode >= 1 then
self.mode = MODE.off
else
self.mode = MODE.page_progress
end
else
self.mode = (self.mode + 1) % MODE_NB
for i, m in ipairs(MODE_INDEX) do
if self.mode == MODE.off then break end
if self.mode == i then
if self.settings[m] then
break
else
self.mode = (self.mode + 1) % MODE_NB
end
end
end
end
self:applyFooterMode()
G_reader_settings:saveSetting("reader_footer_mode", self.mode)
end
self:updateFooter()
return true
end
function ReaderFooter:onHoldFooter()
if self.mode == MODE.off then return end
self.ui:handleEvent(Event:new("ShowGotoDialog"))
return true
end
function ReaderFooter:onSetStatusLine(status_line)
-- 1 is min progress bar while 0 is full cre header progress bar
if status_line == 1 then
self.view.footer_visible = (self.mode ~= MODE.off)
else
self:applyFooterMode(MODE.off)
end
self.ui.document:setStatusLineProp(status_line)
self.ui:handleEvent(Event:new("UpdatePos"))
end
function ReaderFooter:onResume()
self:updateFooter()
if self.settings.auto_refresh_time then
self:setupAutoRefreshTime()
end
end
function ReaderFooter:onSuspend()
if self.settings.auto_refresh_time then
UIManager:unschedule(self.autoRefreshTime)
self.onCloseDocument = nil
end
end
return ReaderFooter