You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
koreader/frontend/apps/reader/modules/readerfooter.lua

2257 lines
96 KiB
Lua

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

local BD = require("ui/bidi")
local Blitbuffer = require("ffi/blitbuffer")
local BottomContainer = require("ui/widget/container/bottomcontainer")
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 HorizontalGroup = require("ui/widget/horizontalgroup")
local HorizontalSpan = require("ui/widget/horizontalspan")
local LeftContainer = require("ui/widget/container/leftcontainer")
local LineWidget = require("ui/widget/linewidget")
local ProgressWidget = require("ui/widget/progresswidget")
local RightContainer = require("ui/widget/container/rightcontainer")
local Size = require("ui/size")
local TextWidget = require("ui/widget/textwidget")
local UIManager = require("ui/uimanager")
local VerticalGroup = require("ui/widget/verticalgroup")
local VerticalSpan = require("ui/widget/verticalspan")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local logger = require("logger")
local util = require("util")
local T = require("ffi/util").template
local _ = require("gettext")
local C_ = _.pgettext
local Screen = Device.screen
local MODE = {
off = 0,
page_progress = 1,
pages_left_book = 2,
time = 3,
pages_left = 4,
battery = 5,
percentage = 6,
book_time_to_read = 7,
chapter_time_to_read = 8,
frontlight = 9,
mem_usage = 10,
wifi_status = 11,
book_title = 12,
book_chapter = 13,
bookmark_count = 14,
}
local symbol_prefix = {
letters = {
time = nil,
pages_left = "=>",
-- @translators This is the footer letter prefix for battery % remaining.
battery = C_("FooterLetterPrefix", "B:"),
-- @translators This is the footer letter prefix for the number of bookmarks (bookmark count).
bookmark_count = C_("FooterLetterPrefix", "BM:"),
-- @translators This is the footer letter prefix for percentage read.
percentage = C_("FooterLetterPrefix", "R:"),
-- @translators This is the footer letter prefix for book time to read.
book_time_to_read = C_("FooterLetterPrefix", "TB:"),
-- @translators This is the footer letter prefix for chapter time to read.
chapter_time_to_read = C_("FooterLetterPrefix", "TC:"),
-- @translators This is the footer letter prefix for frontlight level.
frontlight = C_("FooterLetterPrefix", "L:"),
-- @translators This is the footer letter prefix for memory usage.
mem_usage = C_("FooterLetterPrefix", "M:"),
-- @translators This is the footer letter prefix for Wi-Fi status.
wifi_status = C_("FooterLetterPrefix", "W:"),
},
icons = {
time = "",
pages_left = BD.mirroredUILayout() and "" or "",
battery = "",
bookmark_count = "",
percentage = BD.mirroredUILayout() and "" or "",
book_time_to_read = "",
chapter_time_to_read = BD.mirroredUILayout() and "" or "",
frontlight = "",
mem_usage = "",
wifi_status = "",
wifi_status_off = "",
},
compact_items = {
time = nil,
pages_left = "",
battery = "",
-- @translators This is the footer compact item prefix for the number of bookmarks (bookmark count).
bookmark_count = C_("FooterCompactItemsPrefix", "BM"),
percentage = nil,
book_time_to_read = nil,
chapter_time_to_read = "»",
frontlight = "*",
-- @translators This is the footer compact item prefix for memory usage.
mem_usage = C_("FooterCompactItemsPrefix", "M"),
wifi_status = "",
wifi_status_off = "",
}
}
if BD.mirroredUILayout() then
-- We need to RTL-wrap these letters and symbols for proper layout
for k, v in pairs(symbol_prefix.letters) do
local colon = v:find(":")
local wrapped
if colon then
local pre = v:sub(1, colon-1)
local post = v:sub(colon)
wrapped = BD.wrap(pre) .. BD.wrap(post)
else
wrapped = BD.wrap(v)
end
symbol_prefix.letters[k] = wrapped
end
for k, v in pairs(symbol_prefix.icons) do
symbol_prefix.icons[k] = BD.wrap(v)
end
end
local PROGRESS_BAR_STYLE_THICK_DEFAULT_HEIGHT = 7
local PROGRESS_BAR_STYLE_THIN_DEFAULT_HEIGHT = 3
local DMINIBAR_TOC_MARKER_WIDTH = 2
local DMINIBAR_FONT_SIZE = 14
-- android: guidelines for rounded corner margins
local material_pixels = 16 * math.floor(Screen:getDPI() / 160)
-- functions that generates footer text for each mode
local footerTextGeneratorMap = {
empty = function() return "" end,
frontlight = function(footer)
local symbol_type = footer.settings.item_prefix or "icons"
local prefix = symbol_prefix[symbol_type].frontlight
local powerd = Device:getPowerDevice()
if powerd:isFrontlightOn() then
if Device:isCervantes() or Device:isKobo() then
return (prefix .. " %d%%"):format(powerd:frontlightIntensity())
else
return (prefix .. " %d"):format(powerd:frontlightIntensity())
end
else
if footer.settings.hide_empty_generators then
return ""
else
return T(_("%1 Off"), prefix)
end
end
end,
battery = function(footer)
local symbol_type = footer.settings.item_prefix or "icons"
local prefix = symbol_prefix[symbol_type].battery
local powerd = Device:getPowerDevice()
local batt_lvl = powerd:getCapacity()
-- If we're using icons, use fancy variable icons
if symbol_type == "icons" or symbol_type == "compact_items" then
if powerd:isCharging() then
prefix = ""
else
if batt_lvl >= 100 then
prefix = ""
elseif batt_lvl >= 90 then
prefix = ""
elseif batt_lvl >= 80 then
prefix = ""
elseif batt_lvl >= 70 then
prefix = ""
elseif batt_lvl >= 60 then
prefix = ""
elseif batt_lvl >= 50 then
prefix = ""
elseif batt_lvl >= 40 then
prefix = ""
elseif batt_lvl >= 30 then
prefix = ""
elseif batt_lvl >= 20 then
prefix = ""
elseif batt_lvl >= 10 then
prefix = ""
else
prefix = ""
end
end
if symbol_type == "compact_items" then
return BD.wrap(prefix)
else
return BD.wrap(prefix) .. batt_lvl .. "%"
end
else
return BD.wrap(prefix) .. " " .. (powerd:isCharging() and "+" or "") .. batt_lvl .. "%"
end
end,
bookmark_count = function(footer)
local symbol_type = footer.settings.item_prefix or "icons"
local prefix = symbol_prefix[symbol_type].bookmark_count
local bookmark_count = footer.ui.bookmark:getNumberOfBookmarks()
if footer.settings.hide_empty_generators and bookmark_count == 0 then
return ""
end
return prefix .. " " .. tostring(bookmark_count)
end,
time = function(footer)
local symbol_type = footer.settings.item_prefix or "icons"
local prefix = symbol_prefix[symbol_type].time
local clock = util.secondsToHour(os.time(), G_reader_settings:isTrue("twelve_hour_clock"))
if not prefix then
return clock
else
return prefix .. " " .. clock
end
end,
page_progress = function(footer)
if footer.pageno then
if footer.ui.pagemap and footer.ui.pagemap:wantsPageLabels() then
-- (Page labels might not be numbers)
return ("%s / %s"):format(footer.ui.pagemap:getCurrentPageLabel(true),
footer.ui.pagemap:getLastPageLabel(true))
end
if footer.ui.document:hasHiddenFlows() then
-- i.e., if we are hiding non-linear fragments and there's anything to hide,
local flow = footer.ui.document:getPageFlow(footer.pageno)
local page = footer.ui.document:getPageNumberInFlow(footer.pageno)
local pages = footer.ui.document:getTotalPagesInFlow(flow)
if flow == 0 then
return ("%d // %d"):format(page, pages)
else
return ("[%d / %d]%d"):format(page, pages, flow)
end
else
return ("%d / %d"):format(footer.pageno, footer.pages)
end
elseif footer.position then
return ("%d / %d"):format(footer.position, footer.doc_height)
end
end,
pages_left_book = function(footer)
if footer.pageno then
if footer.ui.pagemap and footer.ui.pagemap:wantsPageLabels() then
-- (Page labels might not be numbers)
return ("%s / %s"):format(footer.ui.pagemap:getCurrentPageLabel(true),
footer.ui.pagemap:getLastPageLabel(true))
end
if footer.ui.document:hasHiddenFlows() then
-- i.e., if we are hiding non-linear fragments and there's anything to hide,
local flow = footer.ui.document:getPageFlow(footer.pageno)
local page = footer.ui.document:getPageNumberInFlow(footer.pageno)
local pages = footer.ui.document:getTotalPagesInFlow(flow)
local remaining = pages - page;
if flow == 0 then
return ("-%d // %d"):format(remaining, pages)
else
return ("[-%d / %d]%d"):format(remaining, pages, flow)
end
else
local remaining = footer.pages - footer.pageno
return ("-%d / %d"):format(remaining, footer.pages)
end
elseif footer.position then
return ("-%d / %d"):format(footer.doc_height - footer.position, footer.doc_height)
end
end,
pages_left = function(footer)
local symbol_type = footer.settings.item_prefix or "icons"
local prefix = symbol_prefix[symbol_type].pages_left
local left = footer.ui.toc:getChapterPagesLeft(footer.pageno)
return prefix .. " " .. (left or footer.ui.document:getTotalPagesLeft(footer.pageno))
end,
percentage = function(footer)
local symbol_type = footer.settings.item_prefix or "icons"
local prefix = symbol_prefix[symbol_type].percentage
local digits = footer.settings.progress_pct_format or "0"
local string_percentage = "%." .. digits .. "f%%"
if footer.ui.document:hasHiddenFlows() then
local flow = footer.ui.document:getPageFlow(footer.pageno)
if flow ~= 0 then
string_percentage = "[" .. string_percentage .. "]"
end
end
if prefix then
string_percentage = prefix .. " " .. string_percentage
end
return string_percentage:format(footer.progress_bar.percentage * 100)
end,
book_time_to_read = function(footer)
local symbol_type = footer.settings.item_prefix or "icons"
local prefix = symbol_prefix[symbol_type].book_time_to_read
local left = footer.ui.document:getTotalPagesLeft(footer.pageno)
return footer:getDataFromStatistics(prefix and (prefix.." ") or "", left)
end,
chapter_time_to_read = function(footer)
local symbol_type = footer.settings.item_prefix or "icons"
local prefix = symbol_prefix[symbol_type].chapter_time_to_read
local left = footer.ui.toc:getChapterPagesLeft(footer.pageno)
return footer:getDataFromStatistics(
prefix .. " ", (left or footer.ui.document:getTotalPagesLeft(footer.pageno)))
end,
mem_usage = function(footer)
local symbol_type = footer.settings.item_prefix or "icons"
local prefix = symbol_prefix[symbol_type].mem_usage
local statm = io.open("/proc/self/statm", "r")
if statm then
local dummy, rss = statm:read("*number", "*number")
statm:close()
-- we got the nb of 4Kb-pages used, that we convert to Mb
rss = math.floor(rss * 4096 / 1024 / 1024)
return (prefix .. " %d"):format(rss)
end
return ""
end,
wifi_status = function(footer)
-- NOTE: This one deviates a bit from the mold because, in icons mode, we simply use two different icons and no text.
local symbol_type = footer.settings.item_prefix or "icons"
local NetworkMgr = require("ui/network/manager")
if symbol_type == "icons" or symbol_type == "compact_items" then
if NetworkMgr:isWifiOn() then
return symbol_prefix.icons.wifi_status
else
if footer.settings.hide_empty_generators then
return ""
else
return symbol_prefix.icons.wifi_status_off
end
end
else
local prefix = symbol_prefix[symbol_type].wifi_status
if NetworkMgr:isWifiOn() then
return T(_("%1 On"), prefix)
else
if footer.settings.hide_empty_generators then
return ""
else
return T(_("%1 Off"), prefix)
end
end
end
end,
book_title = function(footer)
local doc_info = footer.ui.document:getProps()
if doc_info and doc_info.title then
local title = doc_info.title:gsub(" ", "\xC2\xA0") -- replace space with no-break-space
local title_widget = TextWidget:new{
text = title,
max_width = footer._saved_screen_width * footer.settings.book_title_max_width_pct / 100,
face = Font:getFace(footer.text_font_face, footer.settings.text_font_size),
bold = footer.settings.text_font_bold,
}
local fitted_title_text, add_ellipsis = title_widget:getFittedText()
title_widget:free()
if add_ellipsis then
fitted_title_text = fitted_title_text .. ""
end
return BD.auto(fitted_title_text)
else
return ""
end
end,
book_chapter = function(footer)
local chapter_title = footer.ui.toc:getTocTitleByPage(footer.pageno)
if chapter_title and chapter_title ~= "" then
chapter_title = chapter_title:gsub(" ", "\xC2\xA0") -- replace space with no-break-space
local chapter_widget = TextWidget:new{
text = chapter_title,
max_width = footer._saved_screen_width * footer.settings.book_chapter_max_width_pct / 100,
face = Font:getFace(footer.text_font_face, footer.settings.text_font_size),
bold = footer.settings.text_font_bold,
}
local fitted_chapter_text, add_ellipsis = chapter_widget:getFittedText()
chapter_widget:free()
if add_ellipsis then
fitted_chapter_text = fitted_chapter_text .. ""
end
return BD.auto(fitted_chapter_text)
else
return ""
end
end
}
local ReaderFooter = WidgetContainer:extend{
mode = MODE.page_progress,
pageno = nil,
pages = nil,
progress_percentage = 0.0,
footer_text = nil,
text_font_face = "ffont",
height = Screen:scaleBySize(DMINIBAR_CONTAINER_HEIGHT),
horizontal_margin = Size.span.horizontal_default,
bottom_padding = Size.padding.tiny,
settings = {},
-- added to expose them to unit tests
textGeneratorMap = footerTextGeneratorMap,
}
function ReaderFooter:init()
self.settings = G_reader_settings:readSetting("footer", {
-- enable progress bar by default
-- disable_progress_bar = true,
disabled = false,
all_at_once = false,
reclaim_height = false,
toc_markers = true,
battery = Device:hasBattery(),
bookmark_count = true,
time = true,
page_progress = true,
pages_left_book = false,
pages_left = true,
percentage = true,
book_time_to_read = true,
chapter_time_to_read = true,
frontlight = false,
mem_usage = false,
wifi_status = false,
book_title = false,
book_chapter = false,
item_prefix = "icons",
toc_markers_width = DMINIBAR_TOC_MARKER_WIDTH,
text_font_size = DMINIBAR_FONT_SIZE,
text_font_bold = false,
container_height = DMINIBAR_CONTAINER_HEIGHT,
container_bottom_padding = 1, -- unscaled_size_check: ignore
})
-- Remove items not supported by the current device
if not Device:hasFastWifiStatusQuery() then
MODE.wifi_status = nil
end
if not Device:hasFrontlight() then
MODE.frontlight = nil
end
if not Device:hasBattery() then
MODE.battery = nil
end
-- self.mode_index will be an array of MODE names, with an additional element
-- with key 0 for "off", which feels a bit strange but seems to work...
-- (The same is true for self.settings.order which is saved in settings.)
self.mode_index = {}
self.mode_nb = 0
local handled_modes = {}
if self.settings.order then
-- Start filling self.mode_index from what's been ordered by the user and saved
for i=0, #self.settings.order do
local name = self.settings.order[i]
-- (if name has been removed from our supported MODEs: ignore it)
if MODE[name] then -- this mode still exists
self.mode_index[self.mode_nb] = name
self.mode_nb = self.mode_nb + 1
handled_modes[name] = true
end
end
-- go on completing it with remaining new modes in MODE
end
-- If no previous self.settings.order, fill mode_index with what's in MODE
-- in the original indices order
local orig_indexes = {}
local orig_indexes_to_name = {}
for name, orig_index in pairs(MODE) do
if not handled_modes[name] then
table.insert(orig_indexes, orig_index)
orig_indexes_to_name[orig_index] = name
end
end
table.sort(orig_indexes)
for i = 1, #orig_indexes do
self.mode_index[self.mode_nb] = orig_indexes_to_name[orig_indexes[i]]
self.mode_nb = self.mode_nb + 1
end
-- require("logger").dbg(self.mode_nb, self.mode_index)
-- Container settings
if not self.settings.container_height then
self.settings.container_height = DMINIBAR_CONTAINER_HEIGHT
end
self.height = Screen:scaleBySize(self.settings.container_height)
if not self.settings.container_bottom_padding then
self.settings.container_bottom_padding = 1 -- unscaled_size_check: ignore
end
self.bottom_padding = Screen:scaleBySize(self.settings.container_bottom_padding)
-- default margin (like self.horizontal_margin)
if not self.settings.progress_margin_width then
local defaults = Device:isAndroid() and material_pixels or 10
self.settings.progress_margin_width = Screen:scaleBySize(defaults)
end
if not self.settings.toc_markers_width then
self.settings.toc_markers_width = DMINIBAR_TOC_MARKER_WIDTH
end
if not self.settings.progress_bar_min_width_pct then
self.settings.progress_bar_min_width_pct = 20
end
if not self.settings.book_title_max_width_pct then
self.settings.book_title_max_width_pct = 30
end
if not self.settings.book_chapter_max_width_pct then
self.settings.book_chapter_max_width_pct = 30
end
if self.settings.skim_widget_on_hold == nil then
self.settings.skim_widget_on_hold = true
end
self.mode_list = {}
for i = 0, #self.mode_index do
self.mode_list[self.mode_index[i]] = i
end
if self.settings.disabled then
-- footer feature is completely disabled, stop initialization now
self:disableFooter()
return
end
self.pageno = self.view.state.page
self.has_no_mode = true
self.reclaim_height = self.settings.reclaim_height or false
for _, m in ipairs(self.mode_index) do
if self.settings[m] then
self.has_no_mode = false
break
end
end
if not self.settings.text_font_size then
self.settings.text_font_size = DMINIBAR_FONT_SIZE
end
if not self.settings.text_font_bold then
self.settings.text_font_bold = false
end
self.footer_text = TextWidget:new{
text = '',
face = Font:getFace(self.text_font_face, self.settings.text_font_size),
bold = self.settings.text_font_bold,
}
-- all width related values will be initialized in self:resetLayout()
self.text_width = 0
self.footer_text.height = 0
self.progress_bar = ProgressWidget:new{
width = nil,
height = nil,
percentage = self.progress_percentage,
tick_width = Screen:scaleBySize(self.settings.toc_markers_width),
ticks = nil, -- ticks will be populated in self:updateFooterText
last = nil, -- last will be initialized in self:updateFooterText
}
if self.settings.progress_style_thin then
self.progress_bar:updateStyle(false, nil)
end
self.text_container = RightContainer:new{
dimen = Geom:new{ w = 0, h = self.height },
self.footer_text,
}
self:updateFooterContainer()
self.mode = G_reader_settings:readSetting("reader_footer_mode") or self.mode
if self.has_no_mode and self.settings.disable_progress_bar then
self.mode = self.mode_list.off
self.view.footer_visible = false
self:resetLayout()
self.footer_text.height = 0
end
if self.settings.all_at_once then
self.view.footer_visible = (self.mode ~= self.mode_list.off)
self:updateFooterTextGenerator()
if self.settings.progress_bar_position and self.has_no_mode then
self.footer_text.height = 0
end
else
self:applyFooterMode()
end
self.visibility_change = nil
end
function ReaderFooter:updateFooterContainer()
local margin_span = HorizontalSpan:new{ width = self.horizontal_margin }
self.vertical_frame = VerticalGroup:new{}
if self.settings.bottom_horizontal_separator then
self.separator_line = LineWidget:new{
dimen = Geom:new{
w = 0,
h = Size.line.medium,
}
}
local vertical_span = VerticalSpan:new{width = Size.span.vertical_default}
table.insert(self.vertical_frame, self.separator_line)
table.insert(self.vertical_frame, vertical_span)
end
if self.settings.progress_bar_position and not self.settings.disable_progress_bar then
self.horizontal_group = HorizontalGroup:new{
margin_span,
self.text_container,
margin_span,
}
else
self.horizontal_group = HorizontalGroup:new{
margin_span,
self.progress_bar,
self.text_container,
margin_span,
}
end
if self.settings.align == "left" then
self.footer_container = LeftContainer:new{
dimen = Geom:new{ w = 0, h = self.height },
self.horizontal_group
}
elseif self.settings.align == "right" then
self.footer_container = RightContainer:new{
dimen = Geom:new{ w = 0, h = self.height },
self.horizontal_group
}
else
self.footer_container = CenterContainer:new{
dimen = Geom:new{ w = 0, h = self.height },
self.horizontal_group
}
end
local vertical_span = VerticalSpan:new{width = Size.span.vertical_default}
if self.settings.progress_bar_position == "above" and not self.settings.disable_progress_bar then
table.insert(self.vertical_frame, self.progress_bar)
table.insert(self.vertical_frame, vertical_span)
table.insert(self.vertical_frame, self.footer_container)
elseif self.settings.progress_bar_position == "below" and not self.settings.disable_progress_bar then
table.insert(self.vertical_frame, self.footer_container)
table.insert(self.vertical_frame, vertical_span)
table.insert(self.vertical_frame, self.progress_bar)
else
table.insert(self.vertical_frame, self.footer_container)
end
self.footer_content = FrameContainer:new{
self.vertical_frame,
background = Blitbuffer.COLOR_WHITE,
bordersize = 0,
padding = 0,
padding_bottom = self.bottom_padding,
}
self.footer_positioner = BottomContainer:new{
dimen = Geom:new{},
self.footer_content,
}
self[1] = self.footer_positioner
end
function ReaderFooter:unscheduleFooterAutoRefresh()
if not self.autoRefreshFooter then return end -- not yet set up
UIManager:unschedule(self.autoRefreshFooter)
logger.dbg("ReaderFooter.autoRefreshFooter unscheduled")
end
function ReaderFooter:rescheduleFooterAutoRefreshIfNeeded()
if not self.autoRefreshFooter then
-- Create this function the first time we're called
self.autoRefreshFooter = function()
-- Only actually repaint the footer if nothing's being shown over ReaderUI (#6616)
-- (We want to avoid the footer to be painted over a widget covering it - we would
-- be fine refreshing it if the widget is not covering it, but this is hard to
-- guess from here.)
if UIManager:getTopWidget() == "ReaderUI" then
self:onUpdateFooter(self.view.footer_visible)
else
logger.dbg("Skipping ReaderFooter repaint, because ReaderUI is not the top-level widget")
-- NOTE: We *do* keep its content up-to-date, though
self:onUpdateFooter()
end
self:rescheduleFooterAutoRefreshIfNeeded() -- schedule (or not) next refresh
end
end
local unscheduled = UIManager:unschedule(self.autoRefreshFooter) -- unschedule if already scheduled
-- Only schedule an update if the footer has items that may change
-- As self.view.footer_visible may be temporarily toggled off by other modules,
-- we can't trust it for not scheduling auto refresh
local schedule = false
if self.settings.auto_refresh_time then
if self.settings.all_at_once then
if self.settings.time or self.settings.battery or self.settings.wifi_status or self.settings.mem_usage then
schedule = true
end
else
if self.mode == self.mode_list.time or self.mode == self.mode_list.battery
or self.mode == self.mode_list.wifi_status or self.mode == self.mode_list.mem_usage then
schedule = true
end
end
end
if schedule then
UIManager:scheduleIn(61 - tonumber(os.date("%S")), self.autoRefreshFooter)
if not unscheduled then
logger.dbg("ReaderFooter.autoRefreshFooter scheduled")
else
logger.dbg("ReaderFooter.autoRefreshFooter rescheduled")
end
elseif unscheduled then
logger.dbg("ReaderFooter.autoRefreshFooter unscheduled")
end
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 = {
"readerconfigmenu_ext_tap",
"readerconfigmenu_tap",
"tap_forward",
"tap_backward",
},
-- (Low priority: tap on existing highlights
-- or links have priority)
},
{
id = "readerfooter_hold",
ges = "hold",
screen_zone = footer_screen_zone,
handler = function() return self:onHoldFooter() end,
-- (Low priority: word lookup and text selection
-- have priority - SkimTo widget can be more easily
-- brought up via some other gestures)
},
})
end
-- call this method whenever the screen size changes
function ReaderFooter:resetLayout(force_reset)
local new_screen_width = Screen:getWidth()
local new_screen_height = Screen:getHeight()
if new_screen_width == self._saved_screen_width
and new_screen_height == self._saved_screen_height and not force_reset then return end
if self.settings.disable_progress_bar then
self.progress_bar.width = 0
elseif self.settings.progress_bar_position then
self.progress_bar.width = math.floor(new_screen_width - 2 * self.settings.progress_margin_width)
else
self.progress_bar.width = math.floor(
new_screen_width - 2 * self.settings.progress_margin_width - self.text_width)
end
if self.separator_line then
self.separator_line.dimen.w = new_screen_width - 2 * self.horizontal_margin
end
if self.settings.disable_progress_bar then
self.progress_bar.height = 0
else
local bar_height
if self.settings.progress_style_thin then
bar_height = self.settings.progress_style_thin_height or PROGRESS_BAR_STYLE_THIN_DEFAULT_HEIGHT
else
bar_height = self.settings.progress_style_thick_height or PROGRESS_BAR_STYLE_THICK_DEFAULT_HEIGHT
end
self.progress_bar:setHeight(bar_height)
end
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
self._saved_screen_height = new_screen_height
end
function ReaderFooter:getHeight()
if self.footer_content then
-- NOTE: self.footer_content is self.vertical_frame + self.bottom_padding,
-- self.vertical_frame includes self.text_container (which includes self.footer_text)
return self.footer_content:getSize().h
else
return 0
end
end
function ReaderFooter:disableFooter()
self.onReaderReady = function() end
self.resetLayout = function() end
self.updateFooterPage = function() end
self.updateFooterPos = function() end
self.onUpdatePos = function() end
self.mode = self.mode_list.off
self.view.footer_visible = false
end
function ReaderFooter:updateFooterTextGenerator()
local footerTextGenerators = {}
for i, m in pairs(self.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
self.mode = i
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.genFooterText = self.genAllFooterText
end
-- Even if there's no or a single mode enabled, all_at_once requires this to be set
self.footerTextGenerators = footerTextGenerators
-- notify caller that UI needs update
return true
end
function ReaderFooter:progressPercentage(digits)
local symbol_type = self.settings.item_prefix or "icons"
local prefix = symbol_prefix[symbol_type].percentage
local string_percentage
if not prefix then
string_percentage = "%." .. digits .. "f%%"
else
string_percentage = prefix .. " %." .. digits .. "f%%"
end
return string_percentage:format(self.progress_bar.percentage * 100)
end
function ReaderFooter:textOptionTitles(option)
local symbol = self.settings.item_prefix or "icons"
local option_titles = {
all_at_once = _("Show all at once"),
reclaim_height = _("Reclaim bar height from bottom margin"),
bookmark_count = T(_("Bookmark count (%1)"), symbol_prefix[symbol].bookmark_count),
page_progress = T(_("Current page (%1)"), "/"),
pages_left_book = T(_("Pages left in book (%1)"), "-"),
time = symbol_prefix[symbol].time
and T(_("Current time (%1)"), symbol_prefix[symbol].time) or _("Current time"),
pages_left = T(_("Pages left in chapter (%1)"), symbol_prefix[symbol].pages_left),
battery = T(_("Battery status (%1)"), symbol_prefix[symbol].battery),
percentage = symbol_prefix[symbol].percentage
and T(_("Progress percentage (%1)"), symbol_prefix[symbol].percentage) or _("Progress percentage"),
book_time_to_read = symbol_prefix[symbol].book_time_to_read
and T(_("Book time to read (%1)"),symbol_prefix[symbol].book_time_to_read) or _("Book time to read"),
chapter_time_to_read = T(_("Chapter time to read (%1)"), symbol_prefix[symbol].chapter_time_to_read),
frontlight = T(_("Frontlight level (%1)"), symbol_prefix[symbol].frontlight),
mem_usage = T(_("KOReader memory usage (%1)"), symbol_prefix[symbol].mem_usage),
wifi_status = T(_("Wi-Fi status (%1)"), symbol_prefix[symbol].wifi_status),
book_title = _("Book title"),
book_chapter = _("Current chapter"),
}
return option_titles[option]
end
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(true) end,
})
end
local getMinibarOption = function(option, callback)
return {
text_func = function()
return self:textOptionTitles(option)
end,
checked_func = function()
return self.settings[option] == true
end,
callback = function()
self.settings[option] = not self.settings[option]
-- We only need to send a SetPageBottomMargin event when we truly affect the margin
local should_signal = false
-- 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
local prev_reclaim_height = self.reclaim_height
self.has_no_mode = true
for mode_num, m in pairs(self.mode_index) do
if self.settings[m] then
first_enabled_mode_num = mode_num
self.has_no_mode = false
break
end
end
self.reclaim_height = self.settings.reclaim_height or false
-- refresh margins position
if self.has_no_mode then
self.footer_text.height = 0
should_signal = true
self.genFooterText = footerTextGeneratorMap.empty
self.mode = self.mode_list.off
elseif prev_has_no_mode then
if self.settings.all_at_once then
self.mode = self.mode_list.page_progress
self:applyFooterMode()
G_reader_settings:saveSetting("reader_footer_mode", self.mode)
else
G_reader_settings:saveSetting("reader_footer_mode", first_enabled_mode_num)
end
should_signal = true
elseif self.reclaim_height ~= prev_reclaim_height then
should_signal = true
should_update = true
end
if callback then
should_update = callback(self)
elseif self.settings.all_at_once then
should_update = self:updateFooterTextGenerator()
elseif (self.mode_list[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
else
-- If we've just disabled our last mode, first_enabled_mode_num is nil
-- If the progress bar is enabled,
-- fake an innocuous mode so that we switch to showing the progress bar alone, instead of nothing,
-- This is exactly what the "Show progress bar" toggle does.
self.mode = self.settings.disable_progress_bar and self.mode_list.off or self.mode_list.page_progress
end
should_update = true
self:applyFooterMode()
G_reader_settings:saveSetting("reader_footer_mode", self.mode)
end
if should_update or should_signal then
self:refreshFooter(should_update, should_signal)
end
-- The absence or presence of some items may change whether auto-refresh should be ensured
self:rescheduleFooterAutoRefreshIfNeeded()
end,
}
end
table.insert(sub_items, {
text = _("Settings"),
sub_item_table = {
{
text = _("Sort items"),
callback = function()
local item_table = {}
for i=1, #self.mode_index do
table.insert(item_table, {text = self:textOptionTitles(self.mode_index[i]), label = self.mode_index[i]})
end
local SortWidget = require("ui/widget/sortwidget")
local sort_item
sort_item = SortWidget:new{
title = _("Sort footer items"),
item_table = item_table,
callback = function()
for i=1, #sort_item.item_table do
self.mode_index[i] = sort_item.item_table[i].label
end
self.settings.order = self.mode_index
self:updateFooterTextGenerator()
self:onUpdateFooter()
UIManager:setDirty(nil, "ui")
end
}
UIManager:show(sort_item)
end,
},
getMinibarOption("all_at_once", self.updateFooterTextGenerator),
getMinibarOption("reclaim_height"),
{
text = _("Auto refresh"),
checked_func = function()
return self.settings.auto_refresh_time == true
end,
callback = function()
self.settings.auto_refresh_time = not self.settings.auto_refresh_time
self:rescheduleFooterAutoRefreshIfNeeded()
end
},
{
text = _("Show footer separator"),
checked_func = function()
return self.settings.bottom_horizontal_separator
end,
callback = function()
self.settings.bottom_horizontal_separator = not self.settings.bottom_horizontal_separator
self:refreshFooter(true, true)
end,
},
{
text = _("Hide empty items"),
help_text = _([[This will hide values like 0 or off.]]),
checked_func = function()
return self.settings.hide_empty_generators
end,
callback = function()
self.settings.hide_empty_generators = not self.settings.hide_empty_generators
self:refreshFooter(true, true)
end,
},
{
text = _("Lock status bar"),
checked_func = function()
return self.settings.lock_tap
end,
callback = function()
self.settings.lock_tap = not self.settings.lock_tap
end,
},
{
text = _("Hold footer to skim"),
checked_func = function()
return self.settings.skim_widget_on_hold
end,
callback = function()
self.settings.skim_widget_on_hold = not self.settings.skim_widget_on_hold
end,
},
{
text = _("Font"),
separator = true,
sub_item_table = {
{
text_func = function()
return T(_("Font size (%1)"), self.settings.text_font_size)
end,
callback = function(touchmenu_instance)
local SpinWidget = require("ui/widget/spinwidget")
local font_size = self.settings.text_font_size
local items_font = SpinWidget:new{
width = math.floor(Screen:getWidth() * 0.6),
value = font_size,
value_min = 8,
value_max = 36,
default_value = 14,
ok_text = _("Set size"),
title_text = _("Footer font size"),
keep_shown_on_apply = true,
callback = function(spin)
self.settings.text_font_size = spin.value
self.footer_text:free()
self.footer_text = TextWidget:new{
text = self.footer_text.text,
face = Font:getFace(self.text_font_face, self.settings.text_font_size),
bold = self.settings.text_font_bold,
}
self.text_container[1] = self.footer_text
self:refreshFooter(true, true)
if touchmenu_instance then touchmenu_instance:updateItems() end
end,
}
UIManager:show(items_font)
end,
keep_menu_open = true,
},
{
text = _("Use bold font"),
checked_func = function()
return self.settings.text_font_bold == true
end,
callback = function(touchmenu_instance)
self.settings.text_font_bold = not self.settings.text_font_bold
self.footer_text:free()
self.footer_text = TextWidget:new{
text = self.footer_text.text,
face = Font:getFace(self.text_font_face, self.settings.text_font_size),
bold = self.settings.text_font_bold,
}
self.text_container[1] = self.footer_text
self:refreshFooter(true, true)
if touchmenu_instance then touchmenu_instance:updateItems() end
end,
keep_menu_open = true,
},
}
},
{
text_func = function()
return T(_("Container height (%1)"), self.settings.container_height)
end,
callback = function(touchmenu_instance)
local SpinWidget = require("ui/widget/spinwidget")
local container_height = self.settings.container_height
local items_font = SpinWidget:new{
width = math.floor(Screen:getWidth() * 0.6),
value = container_height,
value_min = 7,
value_max = 98,
default_value = DMINIBAR_CONTAINER_HEIGHT,
ok_text = _("Set height"),
title_text = _("Container height"),
keep_shown_on_apply = true,
callback = function(spin)
self.settings.container_height = spin.value
self.height = Screen:scaleBySize(self.settings.container_height)
self:refreshFooter(true, true)
if touchmenu_instance then touchmenu_instance:updateItems() end
end,
}
UIManager:show(items_font)
end,
keep_menu_open = true,
},
{
text_func = function()
return T(_("Container bottom margin (%1)"), self.settings.container_bottom_padding)
end,
callback = function(touchmenu_instance)
local SpinWidget = require("ui/widget/spinwidget")
local container_bottom_padding = self.settings.container_bottom_padding
local items_font = SpinWidget:new{
width = math.floor(Screen:getWidth() * 0.6),
value = container_bottom_padding,
value_min = 0,
value_max = 49,
default_value = 1,
ok_text = _("Set margin"),
title_text = _("Container bottom margin"),
keep_shown_on_apply = true,
callback = function(spin)
self.settings.container_bottom_padding = spin.value
self.bottom_padding = Screen:scaleBySize(self.settings.container_bottom_padding)
self:refreshFooter(true, true)
if touchmenu_instance then touchmenu_instance:updateItems() end
end,
}
UIManager:show(items_font)
end,
keep_menu_open = true,
},
{
text = _("Maximum width of items"),
sub_item_table = {
{
text_func = function()
return T(_("Book title (%1%)"), self.settings.book_title_max_width_pct)
end,
callback = function(touchmenu_instance)
local SpinWidget = require("ui/widget/spinwidget")
local items = SpinWidget:new{
width = math.floor(Screen:getWidth() * 0.6),
value = self.settings.book_title_max_width_pct,
value_min = 10,
value_step = 5,
value_hold_step = 20,
value_max = 100,
title_text = _("Maximum width"),
info_text = _("Maximum book title width in percentage of screen width"),
keep_shown_on_apply = true,
callback = function(spin)
self.settings.book_title_max_width_pct = spin.value
self:refreshFooter(true, true)
if touchmenu_instance then touchmenu_instance:updateItems() end
end
}
UIManager:show(items)
end,
keep_menu_open = true,
},
{
text_func = function()
return T(_("Current chapter (%1%)"), self.settings.book_chapter_max_width_pct)
end,
callback = function(touchmenu_instance)
local SpinWidget = require("ui/widget/spinwidget")
local items = SpinWidget:new{
width = math.floor(Screen:getWidth() * 0.6),
value = self.settings.book_chapter_max_width_pct,
value_min = 10,
value_step = 5,
value_hold_step = 20,
value_max = 100,
title_text = _("Maximum width"),
info_text = _("Maximum chapter width in percentage of screen width"),
keep_shown_on_apply = true,
callback = function(spin)
self.settings.book_chapter_max_width_pct = spin.value
self:refreshFooter(true, true)
if touchmenu_instance then touchmenu_instance:updateItems() end
end
}
UIManager:show(items)
end,
keep_menu_open = true,
}
},
},
{
text = _("Alignment"),
separator = true,
enabled_func = function()
return self.settings.disable_progress_bar or self.settings.progress_bar_position ~= nil
end,
sub_item_table = {
{
text = _("Center"),
checked_func = function()
return self.settings.align == "center" or self.settings.align == nil
end,
callback = function()
self.settings.align = "center"
self:refreshFooter(true)
end,
},
{
text = _("Left"),
checked_func = function()
return self.settings.align == "left"
end,
callback = function()
self.settings.align = "left"
self:refreshFooter(true)
end,
},
{
text = _("Right"),
checked_func = function()
return self.settings.align == "right"
end,
callback = function()
self.settings.align = "right"
self:refreshFooter(true)
end,
},
}
},
{
text = _("Prefix"),
sub_item_table = {
{
text_func = function()
local sym_tbl = {}
for _, letter in pairs(symbol_prefix.icons) do
table.insert(sym_tbl, letter)
end
return T(_("Icons (%1)"), table.concat(sym_tbl, " "))
end,
checked_func = function()
return self.settings.item_prefix == "icons" or self.settings.item_prefix == nil
end,
callback = function()
self.settings.item_prefix = "icons"
self:refreshFooter(true)
end,
},
{
text_func = function()
local sym_tbl = {}
for _, letter in pairs(symbol_prefix.compact_items) do
table.insert(sym_tbl, letter)
end
return T(_("Compact Items (%1)"), table.concat(sym_tbl, " "))
end,
checked_func = function()
return self.settings.item_prefix == "compact_items"
end,
callback = function()
self.settings.item_prefix = "compact_items"
self:refreshFooter(true)
end,
},
{
text_func = function()
local sym_tbl = {}
for _, letter in pairs(symbol_prefix.letters) do
table.insert(sym_tbl, letter)
end
return T(_("Letters (%1)"), table.concat(sym_tbl, " "))
end,
checked_func = function()
return self.settings.item_prefix == "letters"
end,
callback = function()
self.settings.item_prefix = "letters"
self:refreshFooter(true)
end,
},
},
},
{
text = _("Item separator"),
sub_item_table = {
{
text = _("Vertical line (|)"),
checked_func = function()
return self.settings.items_separator == "bar" or self.settings.items_separator == nil
end,
callback = function()
self.settings.items_separator = "bar"
self:refreshFooter(true)
end,
},
{
text = _("Bullet (•)"),
checked_func = function()
return self.settings.items_separator == "bullet"
end,
callback = function()
self.settings.items_separator = "bullet"
self:refreshFooter(true)
end,
},
{
text = _("No separator"),
checked_func = function()
return self.settings.items_separator == "none"
end,
callback = function()
self.settings.items_separator = "none"
self:refreshFooter(true)
end,
},
},
},
{
text = _("Progress percentage format"),
sub_item_table = {
{
text_func = function()
return T(_("No decimal point (%1)"), self:progressPercentage(0))
end,
checked_func = function()
return self.settings.progress_pct_format == "0" or self.settings.progress_pct_format == nil
end,
callback = function()
self.settings.progress_pct_format = "0"
self:refreshFooter(true)
end,
},
{
text_func = function()
return T(_("1 digit after decimal point (%1)"), self:progressPercentage(1))
end,
checked_func = function()
return self.settings.progress_pct_format == "1"
end,
callback = function()
self.settings.progress_pct_format = "1"
self:refreshFooter(true)
end,
},
{
text_func = function()
return T(_("2 digits after decimal point (%1)"), self:progressPercentage(2))
end,
checked_func = function()
return self.settings.progress_pct_format == "2"
end,
callback = function()
self.settings.progress_pct_format = "2"
self:refreshFooter(true)
end,
},
},
},
{
text = _("Duration format"),
sub_item_table = {
{
text_func = function()
local current_duration_format = self.settings.duration_format
local return_text
self.settings.duration_format = "modern"
return_text = footerTextGeneratorMap.book_time_to_read(self)
self.settings.duration_format = current_duration_format
return T(_("Modern (%1)"), return_text)
end,
checked_func = function()
return self.settings.duration_format == "modern" or self.settings.duration_format == nil
end,
callback = function()
self.settings.duration_format = "modern"
self:refreshFooter(true)
end,
},
{
text_func = function()
local current_duration_format = self.settings.duration_format
local return_text
self.settings.duration_format = "classic"
return_text = footerTextGeneratorMap.book_time_to_read(self)
self.settings.duration_format = current_duration_format
return T(_("Classic (%1)"), return_text)
end,
checked_func = function()
return self.settings.duration_format == "classic"
end,
callback = function()
self.settings.duration_format = "classic"
self:refreshFooter(true)
end,
},
}
},
}
})
table.insert(sub_items, {
text = _("Progress bar"),
separator = true,
sub_item_table = {
{
text = _("Show progress bar"),
checked_func = function()
return not self.settings.disable_progress_bar
end,
callback = function()
self.settings.disable_progress_bar = not self.settings.disable_progress_bar
if not self.settings.disable_progress_bar then
self:setTocMarkers()
end
-- If the status bar is currently disabled, switch to an innocuous mode to display it
if not self.view.footer_visible then
self.mode = self.mode_list.page_progress
self:applyFooterMode()
G_reader_settings:saveSetting("reader_footer_mode", self.mode)
end
self:refreshFooter(true, true)
end,
},
{
text_func = function()
local text = _("alongside items")
if self.settings.progress_bar_position == "above" then
text = _("above items")
elseif self.settings.progress_bar_position == "below" then
text = _("below items")
end
return T(_("Position: %1"), text)
end,
enabled_func = function()
return not self.settings.disable_progress_bar
end,
sub_item_table = {
{
text = _("Above items"),
checked_func = function()
return self.settings.progress_bar_position == "above"
end,
callback = function()
self.settings.progress_bar_position = "above"
self:refreshFooter(true, true)
end,
},
{
text = _("Below items"),
checked_func = function()
return self.settings.progress_bar_position == "below"
end,
callback = function()
self.settings.progress_bar_position = "below"
self:refreshFooter(true, true)
end,
},
{
text = _("Alongside items"),
checked_func = function()
return not self.settings.progress_bar_position
end,
callback = function()
-- "Same as book" is disabled in this mode, and we enforce the defaults.
if self.settings.progress_margin then
self.settings.progress_margin = false
self.settings.progress_margin_width = self.horizontal_margin
end
-- Text alignment is also disabled
self.settings.align = "center"
self.settings.progress_bar_position = nil
self:refreshFooter(true, true)
end
},
},
},
{
text_func = function()
if self.settings.progress_style_thin then
return _("Style: thin")
else
return _("Style: thick")
end
end,
enabled_func = function()
return not self.settings.disable_progress_bar
end,
sub_item_table = {
{
text = _("Thick"),
checked_func = function()
return not self.settings.progress_style_thin
end,
callback = function()
self.settings.progress_style_thin = nil
local bar_height = self.settings.progress_style_thick_height or PROGRESS_BAR_STYLE_THICK_DEFAULT_HEIGHT
self.progress_bar:updateStyle(true, bar_height)
self:setTocMarkers()
self:refreshFooter(true, true)
end,
},
{
text = _("Thin"),
checked_func = function()
return self.settings.progress_style_thin
end,
callback = function()
self.settings.progress_style_thin = true
local bar_height = self.settings.progress_style_thin_height or PROGRESS_BAR_STYLE_THIN_DEFAULT_HEIGHT
self.progress_bar:updateStyle(false, bar_height)
self:refreshFooter(true, true)
end,
},
{
text = _("Set size"),
callback = function()
local value, value_min, value_max, default_value
if self.settings.progress_style_thin then
default_value = PROGRESS_BAR_STYLE_THIN_DEFAULT_HEIGHT
value = self.settings.progress_style_thin_height or default_value
value_min = 1
value_max = 12
else
default_value = PROGRESS_BAR_STYLE_THICK_DEFAULT_HEIGHT
value = self.settings.progress_style_thick_height or default_value
value_min = 5
value_max = 28
end
local SpinWidget = require("ui/widget/spinwidget")
local items = SpinWidget:new{
width = math.floor(Screen:getWidth() * 0.6),
value = value,
value_min = value_min,
value_step = 1,
value_hold_step = 2,
value_max = value_max,
default_value = default_value,
title_text = _("Progress bar size"),
keep_shown_on_apply = true,
callback = function(spin)
if self.settings.progress_style_thin then
self.settings.progress_style_thin_height = spin.value
else
self.settings.progress_style_thick_height = spin.value
end
self:refreshFooter(true, true)
end
}
UIManager:show(items)
end,
separator = true,
keep_menu_open = true,
},
{
text = _("Show chapter markers"),
checked_func = function()
return self.settings.toc_markers
end,
enabled_func = function()
return not self.settings.progress_style_thin
end,
callback = function()
self.settings.toc_markers = not self.settings.toc_markers
self:setTocMarkers()
self:refreshFooter(true)
end
},
{
text_func = function()
local markers_width_text = _("thick")
if self.settings.toc_markers_width == 1 then
markers_width_text = _("thin")
elseif self.settings.toc_markers_width == 2 then
markers_width_text = _("medium")
end
return T(_("Chapter marker width (%1)"), markers_width_text)
end,
enabled_func = function()
return not self.settings.progress_style_thin and self.settings.toc_markers
end,
sub_item_table = {
{
text = _("Thin"),
checked_func = function()
return self.settings.toc_markers_width == 1
end,
callback = function()
self.settings.toc_markers_width = 1 -- unscaled_size_check: ignore
self:setTocMarkers()
self:refreshFooter(true)
end,
},
{
text = _("Medium"),
checked_func = function()
return self.settings.toc_markers_width == 2
end,
callback = function()
self.settings.toc_markers_width = 2 -- unscaled_size_check: ignore
self:setTocMarkers()
self:refreshFooter(true)
end,
},
{
text = _("Thick"),
checked_func = function()
return self.settings.toc_markers_width == 3
end,
callback = function()
self.settings.toc_markers_width = 3 -- unscaled_size_check: ignore
self:setTocMarkers()
self:refreshFooter(true)
end
},
},
},
},
},
{
text_func = function()
local text = _("static margins (10)")
local cur_width = self.settings.progress_margin_width
if cur_width == 0 then
text = _("no margins (0)")
elseif cur_width == Screen:scaleBySize(material_pixels) then
text = T(_("static margins (%1)"), material_pixels)
end
if self.settings.progress_margin and not self.ui.document.info.has_pages then
text = T(_("same as book margins (%1)"), self.book_margins_footer_width)
end
return T(_("Margins: %1"), text)
end,
enabled_func = function()
return not self.settings.disable_progress_bar
end,
sub_item_table_func = function()
local common = {
{
text = _("No margins (0)"),
checked_func = function()
return self.settings.progress_margin_width == 0
and not self.settings.progress_margin
end,
callback = function()
self.settings.progress_margin_width = 0
self.settings.progress_margin = false
self:refreshFooter(true)
end,
},
{
text_func = function()
if self.ui.document.info.has_pages then
return _("Same as book margins")
end
return T(_("Same as book margins (%1)"), self.book_margins_footer_width)
end,
checked_func = function()
return self.settings.progress_margin and not self.ui.document.info.has_pages
end,
enabled_func = function()
return not self.ui.document.info.has_pages and self.settings.progress_bar_position ~= nil
end,
callback = function()
self.settings.progress_margin = true
self.settings.progress_margin_width = Screen:scaleBySize(self.book_margins_footer_width)
self:refreshFooter(true)
end
},
}
local function customMargin(px)
return {
text = T(_("Static margins (%1)"), px),
checked_func = function()
return self.settings.progress_margin_width == Screen:scaleBySize(px)
and not self.settings.progress_margin
-- if same as book margins is selected in document with pages (pdf) we enforce static margins
or (self.ui.document.info.has_pages and self.settings.progress_margin)
end,
callback = function()
self.settings.progress_margin_width = Screen:scaleBySize(px)
self.settings.progress_margin = false
self:refreshFooter(true)
end,
}
end
local device_defaults
if Device:isAndroid() then
device_defaults = customMargin(material_pixels)
else
device_defaults = customMargin(10)
end
table.insert(common, 2, device_defaults)
return common
end,
},
{
text_func = function()
return T(_("Minimal width (%1%)"), self.settings.progress_bar_min_width_pct)
end,
enabled_func = function()
return not self.settings.progress_bar_position and not self.settings.disable_progress_bar
and self.settings.all_at_once
end,
callback = function(touchmenu_instance)
local SpinWidget = require("ui/widget/spinwidget")
local items = SpinWidget:new{
width = math.floor(Screen:getWidth() * 0.6),
value = self.settings.progress_bar_min_width_pct,
value_min = 5,
value_step = 5,
value_hold_step = 20,
value_max = 50,
title_text = _("Minimal width"),
text = _("Minimal progress bar width in percentage of screen width"),
keep_shown_on_apply = true,
callback = function(spin)
self.settings.progress_bar_min_width_pct = spin.value
self:refreshFooter(true, true)
if touchmenu_instance then touchmenu_instance:updateItems() end
end
}
UIManager:show(items)
end,
keep_menu_open = true,
}
}
})
table.insert(sub_items, getMinibarOption("page_progress"))
table.insert(sub_items, getMinibarOption("pages_left_book"))
table.insert(sub_items, getMinibarOption("time"))
table.insert(sub_items, getMinibarOption("pages_left"))
if Device:hasBattery() then
table.insert(sub_items, getMinibarOption("battery"))
end
table.insert(sub_items, getMinibarOption("bookmark_count"))
table.insert(sub_items, getMinibarOption("percentage"))
table.insert(sub_items, getMinibarOption("book_time_to_read"))
table.insert(sub_items, getMinibarOption("chapter_time_to_read"))
if Device:hasFrontlight() then
table.insert(sub_items, getMinibarOption("frontlight"))
end
table.insert(sub_items, getMinibarOption("mem_usage"))
if Device:hasFastWifiStatusQuery() then
table.insert(sub_items, getMinibarOption("wifi_status"))
end
table.insert(sub_items, getMinibarOption("book_title"))
table.insert(sub_items, getMinibarOption("book_chapter"))
-- If using crengine, add Alt status bar items at top
if self.ui.crelistener then
table.insert(sub_items, 1, self.ui.crelistener:getAltStatusBarMenu())
end
end
-- this method will be updated at runtime based on user setting
function ReaderFooter:genFooterText() end
function ReaderFooter:genAllFooterText()
local info = {}
local separator = " "
if self.settings.item_prefix == "compact_items" then
separator = " "
end
if self.settings.items_separator == "bar" or self.settings.items_separator == nil then
separator = " | "
elseif self.settings.items_separator == "bullet" then
separator = ""
end
-- We need to BD.wrap() all items and separators, so we're
-- sure they are laid out in our order (reversed in RTL),
-- without ordering by the RTL Bidi algorithm.
for _, gen in ipairs(self.footerTextGenerators) do
-- Skip empty generators, so they don't generate bogus separators
local text = gen(self)
if text and text ~= "" then
if self.settings.item_prefix == "compact_items" then
-- remove whitespace from footer items if symbol_type is compact_items
-- use a hair-space to avoid issues with RTL display
text = text:gsub("%s", "\xE2\x80\x8A")
end
table.insert(info, BD.wrap(text))
end
end
return table.concat(info, BD.wrap(separator))
end
function ReaderFooter:setTocMarkers(reset)
if self.settings.disable_progress_bar or self.settings.progress_style_thin then return end
if reset then
self.progress_bar.ticks = nil
self.pages = self.ui.document:getPageCount()
end
if self.settings.toc_markers then
self.progress_bar.tick_width = Screen:scaleBySize(self.settings.toc_markers_width)
if self.progress_bar.ticks ~= nil then -- already computed
return
end
if self.ui.document:hasHiddenFlows() and self.pageno then
local flow = self.ui.document:getPageFlow(self.pageno)
self.progress_bar.ticks = {}
if self.ui.toc then
-- filter the ticks to show only those in the current flow
for n, pageno in ipairs(self.ui.toc:getTocTicksFlattened()) do
if self.ui.document:getPageFlow(pageno) == flow then
table.insert(self.progress_bar.ticks, self.ui.document:getPageNumberInFlow(pageno))
end
end
end
self.progress_bar.last = self.ui.document:getTotalPagesInFlow(flow)
else
if self.ui.toc then
self.progress_bar.ticks = self.ui.toc:getTocTicksFlattened()
end
if self.view.view_mode == "page" then
self.progress_bar.last = self.pages or self.ui.document:getPageCount()
else
-- in scroll mode, convert pages to positions
if self.ui.toc then
self.progress_bar.ticks = {}
for n, pageno in ipairs(self.ui.toc:getTocTicksFlattened()) do
local idx = self.ui.toc:getTocIndexByPage(pageno)
local pos = self.ui.document:getPosFromXPointer(self.ui.toc.toc[idx].xpointer)
table.insert(self.progress_bar.ticks, pos)
end
end
self.progress_bar.last = self.doc_height or self.ui.document.info.doc_height
end
end
else
self.progress_bar.ticks = nil
end
-- notify caller that UI needs update
return true
end
function ReaderFooter:getAvgTimePerPage()
return
end
function ReaderFooter:getDataFromStatistics(title, pages)
local sec = _("N/A")
local average_time_per_page = self:getAvgTimePerPage()
if average_time_per_page then
if self.settings.duration_format == "classic" then
sec = util.secondsToClock(pages * average_time_per_page, true)
else
sec = util.secondsToHClock(pages * average_time_per_page, true)
end
end
return title .. sec
end
function ReaderFooter:onUpdateFooter(force_repaint, force_recompute)
if self.pageno then
self:updateFooterPage(force_repaint, force_recompute)
else
self:updateFooterPos(force_repaint, force_recompute)
end
end
function ReaderFooter:updateFooterPage(force_repaint, force_recompute)
if type(self.pageno) ~= "number" then return end
if self.ui.document:hasHiddenFlows() then
local flow = self.ui.document:getPageFlow(self.pageno)
local page = self.ui.document:getPageNumberInFlow(self.pageno)
local pages = self.ui.document:getTotalPagesInFlow(flow)
self.progress_bar.percentage = page / pages
else
self.progress_bar.percentage = self.pageno / self.pages
end
self:updateFooterText(force_repaint, force_recompute)
end
function ReaderFooter:updateFooterPos(force_repaint, force_recompute)
if type(self.position) ~= "number" then return end
self.progress_bar.percentage = self.position / self.doc_height
self:updateFooterText(force_repaint, force_recompute)
end
-- updateFooterText will start as a noop. After onReaderReady event is
-- received, it will initialized as _updateFooterText below
function ReaderFooter:updateFooterText(force_repaint, force_recompute)
end
-- only call this function after document is fully loaded
function ReaderFooter:_updateFooterText(force_repaint, force_recompute)
-- footer is invisible, we need neither a repaint nor a recompute, go away.
if not self.view.footer_visible and not force_repaint and not force_recompute then
return
end
local text = self:genFooterText()
if not text then text = "" end
self.footer_text:setText(text)
if self.settings.disable_progress_bar then
if self.has_no_mode or text == "" then
self.text_width = 0
self.footer_text.height = 0
else
-- No progress bar, we're only constrained to fit inside self.footer_container
self.footer_text:setMaxWidth(math.floor(self._saved_screen_width - 2 * self.horizontal_margin))
self.text_width = self.footer_text:getSize().w
self.footer_text.height = self.footer_text:getSize().h
end
self.progress_bar.height = 0
self.progress_bar.width = 0
elseif self.settings.progress_bar_position then
if self.has_no_mode or text == "" then
self.text_width = 0
self.footer_text.height = 0
else
-- With a progress bar above or below us, we want to align ourselves to the bar's margins... iff text is centered.
if self.settings.align == "center" then
self.footer_text:setMaxWidth(math.floor(self._saved_screen_width - 2 * self.settings.progress_margin_width))
else
-- Otherwise, we have to constrain ourselves to the container, or weird shit happens.
self.footer_text:setMaxWidth(math.floor(self._saved_screen_width - 2 * self.horizontal_margin))
end
self.text_width = self.footer_text:getSize().w
self.footer_text.height = self.footer_text:getSize().h
end
self.progress_bar.width = math.floor(self._saved_screen_width - 2 * self.settings.progress_margin_width)
else
if self.has_no_mode or text == "" then
self.text_width = 0
self.footer_text.height = 0
else
-- Alongside a progress bar, it's the bar's width plus whatever's left.
local text_max_available_ratio = (100 - self.settings.progress_bar_min_width_pct) / 100
self.footer_text:setMaxWidth(math.floor(text_max_available_ratio * self._saved_screen_width - 2 * self.settings.progress_margin_width - self.horizontal_margin))
-- Add some spacing between the text and the bar
self.text_width = self.footer_text:getSize().w + self.horizontal_margin
self.footer_text.height = self.footer_text:getSize().h
end
self.progress_bar.width = math.floor(
self._saved_screen_width - 2 * self.settings.progress_margin_width - self.text_width)
end
if self.separator_line then
self.separator_line.dimen.w = self._saved_screen_width - 2 * self.horizontal_margin
end
self.text_container.dimen.w = self.text_width
self.horizontal_group:resetLayout()
-- NOTE: This is essentially preventing us from truly using "fast" for panning,
-- since it'll get coalesced in the "fast" panning update, upgrading it to "ui".
-- NOTE: That's assuming using "fast" for pans was a good idea, which, it turned out, not so much ;).
-- NOTE: We skip repaints on page turns/pos update, as that's redundant (and slow).
if force_repaint then
-- If there was a visibility change, notify ReaderView
if self.visibility_change then
self.ui:handleEvent(Event:new("ReaderFooterVisibilityChange"))
self.visibility_change = nil
end
-- NOTE: Getting the dimensions of the widget is impossible without having drawn it first,
-- so, we'll fudge it if need be...
-- i.e., when it's no longer visible, because there's nothing to draw ;).
local refresh_dim = self.footer_content.dimen
-- No more content...
if not self.view.footer_visible and not refresh_dim then
-- So, instead, rely on self:getHeight to compute self.footer_content's height early...
refresh_dim = self.dimen
refresh_dim.h = self:getHeight()
refresh_dim.y = self._saved_screen_height - refresh_dim.h
end
-- If we're making the footer visible (or it already is), we don't need to repaint ReaderUI behind it
if self.view.footer_visible then
-- Unfortunately, it's not a modal (we never show() it), so it's not in the window stack,
-- instead, it's baked inside ReaderUI, so it gets slightly trickier...
-- NOTE: self.view.footer -> self ;).
-- c.f., ReaderView:paintTo()
UIManager:widgetRepaint(self.view.footer, 0, 0)
-- We've painted it first to ensure self.footer_content.dimen is sane
UIManager:setDirty(self.view.footer, function()
return "ui", self.footer_content.dimen
end)
else
UIManager:setDirty(self.view.dialog, function()
return "ui", refresh_dim
end)
end
end
end
function ReaderFooter:onTocReset()
self:setTocMarkers(true)
if self.view.view_mode == "page" then
self:updateFooterPage()
else
self:updateFooterPos()
end
end
function ReaderFooter:onPageUpdate(pageno)
local toc_markers_update = false
if self.ui.document:hasHiddenFlows() then
local flow = self.pageno and self.ui.document:getPageFlow(self.pageno)
local new_flow = pageno and self.ui.document:getPageFlow(pageno)
if pageno and new_flow ~= flow then
toc_markers_update = true
end
end
self.pageno = pageno
self.pages = self.ui.document:getPageCount()
if toc_markers_update then
self:setTocMarkers(true)
end
self.ui.doc_settings:saveSetting("doc_pages", self.pages) -- for Book information
self:updateFooterPage()
end
function ReaderFooter:onPosUpdate(pos, pageno)
self.position = pos
self.doc_height = self.ui.document.info.doc_height
if pageno then
self.pageno = pageno
self.pages = self.ui.document:getPageCount()
self.ui.doc_settings:saveSetting("doc_pages", self.pages) -- for Book information
end
self:updateFooterPos()
end
-- recalculate footer sizes when document page count is updated
-- see documentation for more info about this event.
ReaderFooter.onUpdatePos = ReaderFooter.onUpdateFooter
function ReaderFooter:onReaderReady()
self.ui.menu:registerToMainMenu(self)
self:setupTouchZones()
-- if same as book margins is selected in document with pages (pdf) we enforce static margins
if self.ui.document.info.has_pages and self.settings.progress_margin then
self.settings.progress_margin_width = Size.span.horizontal_default
self:updateFooterContainer()
-- set progress bar margins for current book
elseif self.settings.progress_margin then
local margins = self.ui.document:getPageMargins()
self.settings.progress_margin_width = math.floor((margins.left + margins.right)/2)
self:updateFooterContainer()
end
self:resetLayout(self.settings.progress_margin_width) -- set widget dimen
self:setTocMarkers()
self.updateFooterText = self._updateFooterText
self:onUpdateFooter()
self:rescheduleFooterAutoRefreshIfNeeded()
end
function ReaderFooter:onReadSettings(config)
if not self.ui.document.info.has_pages then
local h_margins = config:readSetting("copt_h_page_margins")
or G_reader_settings:readSetting("copt_h_page_margins")
or DCREREADER_CONFIG_H_MARGIN_SIZES_MEDIUM
self.book_margins_footer_width = math.floor((h_margins[1] + h_margins[2])/2)
end
end
function ReaderFooter:applyFooterMode(mode)
if mode ~= nil then self.mode = mode end
local prev_visible_state = self.view.footer_visible
self.view.footer_visible = (self.mode ~= self.mode_list.off)
-- NOTE: _updateFooterText won't actually run the text generator(s) when hidden ;).
-- We're hidden, disable text generation entirely
if not self.view.footer_visible then
self.genFooterText = footerTextGeneratorMap.empty
else
if self.settings.all_at_once then
-- If all-at-once is enabled, we only have toggle from empty to All.
self.genFooterText = self.genAllFooterText
else
-- Otherwise, switch to the right text generator for the new mode
local mode_name = self.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
end
-- If we changed visibility state at runtime (as opposed to during init), better make sure the layout has been reset...
if prev_visible_state ~= nil and self.view.footer_visible ~= prev_visible_state then
self:updateFooterContainer()
-- NOTE: _updateFooterText does a resetLayout, but not a forced one!
self:resetLayout(true)
-- Flag _updateFooterText to notify ReaderView to recalculate the visible_area!
self.visibility_change = true
end
end
function ReaderFooter:onEnterFlippingMode()
self.orig_mode = self.mode
self:applyFooterMode(self.mode_list.page_progress)
self:rescheduleFooterAutoRefreshIfNeeded()
end
function ReaderFooter:onExitFlippingMode()
self:applyFooterMode(self.orig_mode)
self:rescheduleFooterAutoRefreshIfNeeded()
end
function ReaderFooter:onTapFooter(ges)
local ignore_lock = false
if ges == true then
ignore_lock = true
ges = nil
end
if self.view.flipping_visible and ges 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
self:onUpdateFooter(true)
return true
end
if self.has_no_mode or (self.settings.lock_tap and not ignore_lock) then
return
end
if self.settings.all_at_once or self.has_no_mode then
if self.mode >= 1 then
self.mode = self.mode_list.off
else
self.mode = self.mode_list.page_progress
end
else
self.mode = (self.mode + 1) % self.mode_nb
for i, m in ipairs(self.mode_index) do
if self.mode == self.mode_list.off then break end
if self.mode == i then
if self.settings[m] then
break
else
self.mode = (self.mode + 1) % self.mode_nb
end
end
end
end
self:applyFooterMode()
G_reader_settings:saveSetting("reader_footer_mode", self.mode)
self:onUpdateFooter(true)
self:rescheduleFooterAutoRefreshIfNeeded()
return true
end
function ReaderFooter:onHoldFooter()
if self.mode == self.mode_list.off then return end
if self.settings.skim_widget_on_hold then
self.ui:handleEvent(Event:new("ShowSkimtoDialog"))
return true
end
end
function ReaderFooter:refreshFooter(refresh, signal)
self:updateFooterContainer()
self:resetLayout(true)
-- If we signal, the event we send will trigger a full repaint anyway, so we should be able to skip this one.
-- We *do* need to ensure we at least re-compute the footer layout, though, especially when going from visible to invisible...
self:onUpdateFooter(refresh and not signal, refresh and signal)
if signal then
if self.ui.document.provider == "crengine" then
-- This will ultimately trigger an UpdatePos, hence a ReaderUI repaint.
self.ui:handleEvent(Event:new("SetPageBottomMargin", self.ui.document.configurable.b_page_margin))
else
-- No fancy chain of events outside of CRe, just ask for a ReaderUI repaint ourselves ;).
UIManager:setDirty(self.view.dialog, "partial")
end
end
end
function ReaderFooter:onResume()
-- Don't repaint the footer until OutOfScreenSaver if screensaver_delay is enabled...
local screensaver_delay = G_reader_settings:readSetting("screensaver_delay")
if screensaver_delay and screensaver_delay ~= "disable" then
self._delayed_screensaver = true
return
end
-- Force a footer repaint on resume if it was visible
self:onUpdateFooter(self.view.footer_visible)
self:rescheduleFooterAutoRefreshIfNeeded()
end
function ReaderFooter:onOutOfScreenSaver()
if not self._delayed_screensaver then
return
end
self._delayed_screensaver = nil
-- Force a footer repaint on resume if it was visible
self:onUpdateFooter(self.view.footer_visible)
self:rescheduleFooterAutoRefreshIfNeeded()
end
function ReaderFooter:onSuspend()
self:unscheduleFooterAutoRefresh()
end
function ReaderFooter:onCloseDocument()
self:unscheduleFooterAutoRefresh()
end
function ReaderFooter:onFrontlightStateChanged()
if self.settings.frontlight then
self:onUpdateFooter(true)
end
end
function ReaderFooter:onNetworkConnected()
if self.settings.wifi_status then
self:onUpdateFooter(true)
end
end
function ReaderFooter:onNetworkDisconnected()
if self.settings.wifi_status then
self:onUpdateFooter(true)
end
end
function ReaderFooter:onSetRotationMode()
self:updateFooterContainer()
self:resetLayout(true)
end
function ReaderFooter:onSetPageHorizMargins(h_margins)
self.book_margins_footer_width = math.floor((h_margins[1] + h_margins[2])/2)
if self.settings.progress_margin then
self.settings.progress_margin_width = Screen:scaleBySize(self.book_margins_footer_width)
self:refreshFooter(true)
end
end
function ReaderFooter:onScreenResize()
self:updateFooterContainer()
self:resetLayout(true)
end
return ReaderFooter