Merge pull request #966 from chrox/collapsible_toc

refactoring readertoc and readerfooter
This commit is contained in:
Qingping Hou
2014-10-07 20:00:11 -07:00
7 changed files with 279 additions and 196 deletions

View File

@@ -6,25 +6,59 @@ KOReader
KOReader is a document viewer application, originally created for Kindle
e-ink readers. It currently runs on Kindle 5 (Touch), Kindle Paperwhite,
Kobo, Android(2.3+) devices.
Kobo and Android (2.3+) devices. Developers can also run Koreader emulator
for development purpose on desktop PC with Linux or Windows operating system.
KOReader started as the KindlePDFViewer application, but it supports much more
formats than PDF now. Among them are DJVU, FB2, EPUB, TXT, CBZ, HTML.
Main features for users
-----------------------
KOReader is a frontend written in Lua and uses the API presented by the
koreader-base framework. KOReader implements a GUI and is currently targeted
at touch-based devices - for the classic user interface for button-driven
e-ink devices (like the Kindle 2, Kindle DX, Kindle 3, Kindle 4) see the
KindlePDFviewer legacy project or - especially for the Kindle 4 - have a look
at its fork Librerator.
* supports multi-format documents including:
* paged fixed-layout formats: PDF, DjVu and CBZ
* reflowable e-book formats: ePub, fb2, mobi, doc, chm and plain text
* scanned PDF/DjVu documents can also be reflowed with built-in K2pdfopt
* use StarDict dictionaries / Wikipedia to lookup words
* highlights can be exported to Evernote cloud account
* highly customizable reader view and typeset
* setting arbitrary page margins / line space
* choosing external fonts and styles
* built-in multi-lingual hyphenation dictionaries
* supports adding custom online OPDS catalogs
* calibre integration
* send ebooks from calibre library to koreader wirelessly
* browser calibre library and download ebooks via calibre OPDS server
* can share ebooks to other koreader devices wirelessly
* various optimizations for e-ink devices
* paginated menus without animation
* adjustable text contrast
* multi-lingual user interface
* online Over-The-Air software update
This application is distributed under the GNU AGPL v3 license (read the [COPYING](COPYING) file).
Highlights for developlers
--------------------------
* frontend written in Lua scripting language
* running on multi-platform with only one code-base maintained
* developing koreader in any editor without compilation
* high runtime efficiency by LuaJIT acceleration
* light-weight widget toolkit for small memory footprint
* extensible with plugin system
* interfaced backends for documents parsing and rendering
* high quality document backend libraries like MuPDF, DjvuLibre and Crengine
* interacting with frontend via LuaJIT FFI for best performence
* in active development
* contributed by 28 and more developers around the world
* continuous integration with Travis CI
* with unit tests and code coverage test
* automatic release of nightly builds
* free as in free speech
* licensed under Affero GPL v3
* all dependencies are free software
Check out the [KOReader wiki](https://github.com/koreader/koreader/wiki) to learn
more about this project.
Prerequisites
========
Building Prerequisites
======================
Instructions about how to get and compile the source are intended for a linux
OS. Windows users are suggested to develop in a [Linux VM][linux-vm] or use Wubi.

View File

@@ -20,6 +20,7 @@ local ReaderFooter = InputContainer:new{
visible = true,
pageno = nil,
pages = nil,
toc_level = 0,
progress_percentage = 0.0,
progress_text = nil,
text_font_face = "ffont",
@@ -37,35 +38,39 @@ function ReaderFooter:init()
self.pageno = self.view.state.page
self.pages = self.view.document:getPageCount()
local progress_text_default = ""
local text_default = ""
if DMINIBAR_ALL_AT_ONCE then
local info = {}
if DMINIBAR_BATTERY then
table.insert(info, "B:100%")
end
if DMINIBAR_TIME then
progress_text_default = progress_text_default .. " | WW:WW"
table.insert(info, "WW:WW")
end
if DMINIBAR_PAGES then
progress_text_default = progress_text_default .. " | 0000 / 0000"
table.insert(info, "0000 / 0000")
end
if DMINIBAR_NEXT_CHAPTER then
progress_text_default = progress_text_default .. " | => 000"
table.insert(info, "=> 000")
end
if DMINIBAR_BATTERY then
progress_text_default = progress_text_default .. " | B:100%"
end
progress_text_default = string.sub(progress_text_default, 4)
text_default = table.concat(info, " | ")
else
progress_text_default = string.format(" %d / %d ", self.pages, self.pages)
text_default = string.format(" %d / %d ", self.pages, self.pages)
end
self.progress_text = TextWidget:new{
text = progress_text_default,
text = text_default,
face = Font:getFace(self.text_font_face, self.text_font_size),
}
local text_width = self.progress_text:getSize().w
local ticks = (self.ui.toc and DMINIBAR_PROGRESS_MARKER)
and self.ui.toc:getTocTicks(self.toc_level) or {}
self.progress_bar = ProgressWidget:new{
width = math.floor(Screen:getWidth() - text_width - self.padding),
height = self.bar_height,
percentage = self.progress_percentage,
TOC = self.ui.document:getToc(),
ticks = ticks,
tick_width = DMINIBAR_TOC_MARKER_WIDTH,
last = self.pages,
}
local horizontal_group = HorizontalGroup:new{}
@@ -121,42 +126,55 @@ function ReaderFooter:init()
self:applyFooterMode()
end
function ReaderFooter:fillToc()
self.toc = self.ui.document:getToc()
function ReaderFooter:getBatteryInfo()
local powerd = Device:getPowerDevice()
--local state = powerd:isCharging() and -1 or powerd:getCapacity()
return "B:" .. powerd:getCapacity() .. "%"
end
function ReaderFooter:getTimeInfo()
return os.date("%H:%M")
end
function ReaderFooter:getProgressInfo()
return string.format("%d / %d", self.pageno, self.pages)
end
function ReaderFooter:getNextChapterInfo()
local left = self.ui.toc:getChapterPagesLeft(self.pageno, self.toc_level)
return "=> " .. (left and left or self.pages - self.pageno)
end
function ReaderFooter:updateFooterPage()
if type(self.pageno) ~= "number" then return end
self.progress_bar.percentage = self.pageno / self.pages
if DMINIBAR_ALL_AT_ONCE then
self.progress_text.text = ""
local info = {}
if DMINIBAR_BATTERY then
local powerd = Device:getPowerDevice()
local state = powerd:isCharging() and -1 or powerd:getCapacity()
self.progress_text.text = self.progress_text.text .. " | B:" .. powerd:getCapacity() .. "%"
table.insert(info, self:getBatteryInfo())
end
if DMINIBAR_TIME then
self.progress_text.text = self.progress_text.text .. " | " .. os.date("%H:%M")
table.insert(info, self:getTimeInfo())
end
if DMINIBAR_PAGES then
self.progress_text.text = self.progress_text.text .. " | " .. string.format("%d / %d", self.pageno, self.pages)
table.insert(info, self:getProgressInfo())
end
if DMINIBAR_NEXT_CHAPTER then
self.progress_text.text = self.progress_text.text .. " | => " .. self.ui.toc:_getChapterPagesLeft(self.pageno,self.pages)
table.insert(info, self:getNextChapterInfo())
end
self.progress_text.text = string.sub(self.progress_text.text, 4)
self.progress_text.text = table.concat(info, " | ")
else
local info = ""
if self.mode == 1 then
self.progress_text.text = string.format("%d / %d", self.pageno, self.pages)
info = self:getProgressInfo()
elseif self.mode == 2 then
self.progress_text.text = os.date("%H:%M")
info = self:getTimeInfo()
elseif self.mode == 3 then
self.progress_text.text = "=> " .. self.ui.toc:_getChapterPagesLeft(self.pageno,self.pages)
info = self:getNextChapterInfo()
elseif self.mode == 4 then
local powerd = Device:getPowerDevice()
local state = powerd:isCharging() and -1 or powerd:getCapacity()
self.progress_text.text = "B:" .. powerd:getCapacity() .. "%"
info = self:getBatteryInfo()
end
self.progress_text.text = info
end
end
@@ -166,9 +184,10 @@ function ReaderFooter:updateFooterPos()
self.progress_bar.percentage = self.position / self.doc_height
if self.show_time then
self.progress_text.text = os.date("%H:%M")
self.progress_text.text = self:getTimeInfo()
else
self.progress_text.text = string.format("%1.f", self.progress_bar.percentage*100).."%"
local percentage = self.progress_bar.percentage
self.progress_text.text = string.format("%1.f", percentage*100) .. "%"
end
end

View File

@@ -247,18 +247,12 @@ function ReaderRolling:onResume()
end
function ReaderRolling:onDoubleTapForward()
local i = self.ui.toc:_getNextChapter(self.current_page+self.ui.document:getVisiblePageCount())
if i ~= "" then
self:onGotoPage(i)
end
self:onGotoPage(self.ui.toc:getNextChapter(self.current_page))
return true
end
function ReaderRolling:onDoubleTapBackward()
local i = self.ui.toc:_getPreviousChapter(self.current_page)
if i ~= "" then
self:onGotoPage(i)
end
self:onGotoPage(self.ui.toc:getPreviousChapter(self.current_page))
return true
end
@@ -414,7 +408,9 @@ function ReaderRolling:gotoPercent(new_percent)
end
function ReaderRolling:onGotoPage(number)
self:gotoPage(number)
if number then
self:gotoPage(number)
end
return true
end

View File

@@ -13,6 +13,7 @@ local _ = require("gettext")
local ReaderToc = InputContainer:new{
toc = nil,
ticks = {},
toc_menu_title = _("Table of contents"),
}
@@ -60,30 +61,17 @@ function ReaderToc:onPageUpdate(pageno)
end
function ReaderToc:fillToc()
if self.toc and #self.toc > 0 then return end
self.toc = self.ui.document:getToc()
end
-- _getTocTitleByPage wrapper, so specific reader
-- can tranform pageno according its need
function ReaderToc:getTocTitleByPage(pn_or_xp)
local page = pn_or_xp
self:fillToc()
if #self.toc == 0 then return "" end
local pageno = pn_or_xp
if type(pn_or_xp) == "string" then
page = self.ui.document:getPageFromXPointer(pn_or_xp)
pageno = self.ui.document:getPageFromXPointer(pn_or_xp)
end
return self:_getTocTitleByPage(page)
end
function ReaderToc:_getTocTitleByPage(pageno)
if not self.toc then
-- build toc when needed.
self:fillToc()
end
-- no table of content
if #self.toc == 0 then
return ""
end
local pre_entry = self.toc[1]
for _k,_v in ipairs(self.toc) do
if _v.page > pageno then
@@ -98,135 +86,117 @@ function ReaderToc:getTocTitleOfCurrentPage()
return self:getTocTitleByPage(self.pageno)
end
function ReaderToc:_getChapterPagesLeft(pageno,pages)
local i
local j = 0
if not self.toc then
-- build toc when needed.
self:fillToc()
end
-- no table of content
if #self.toc == 0 then
return ""
end
if #self.toc > 0 then
for i = 1, #self.toc do
v = self.toc[i]
if v.page > pageno then
j = v.page
break
end
function ReaderToc:getMaxDepth()
self:fillToc()
local max_depth = 0
for _, v in ipairs(self.toc) do
if v.depth > max_depth then
max_depth = v.depth
end
end
if j == 0 then
if pages > 0 then
return pages-pageno
return max_depth
end
--[[
TOC ticks is a list of page number in ascending order of TOC nodes at certain level
positive level counts nodes of the depth level (level 1 for depth 1)
non-positive level counts nodes of reversed depth level (level -1 for max_depth-1)
--]]
function ReaderToc:getTocTicks(level)
if self.ticks[level] then return self.ticks[level] end
-- build toc ticks if not found
self:fillToc()
local ticks = {}
if #self.toc > 0 then
local depth = nil
if level > 0 then
depth = level
else
return ""
depth = self:getMaxDepth() + level
end
else
return j-pageno-1
end
end
function ReaderToc:_getChapterPagesDone(pageno)
local i
local j = 0
if not self.toc then
-- build toc when needed.
self:fillToc()
end
-- no table of content
if #self.toc == 0 then
return ""
end
if #self.toc > 0 then
for i = 1, #self.toc do
v = self.toc[i]
if v.page >= pageno then
break
end
j = v.page
end
end
if j < 2 then
return ""
else
return j-pageno
end
end
function ReaderToc:_getPreviousChapter(pageno)
local i
local j = 0
if not self.toc then
-- build toc when needed.
self:fillToc()
end
-- no table of content
if #self.toc == 0 then
return ""
end
if #self.toc > 0 then
for i = 1, #self.toc do
v = self.toc[i]
if v.page >= pageno then
break
end
j = v.page
end
end
if j >= pageno then
return ""
else
return j
end
end
function ReaderToc:_getNextChapter(pageno)
local i
local j = 0
if not self.toc then
-- build toc when needed.
self:fillToc()
end
-- no table of content
if #self.toc == 0 then
return ""
end
if #self.toc > 0 then
for i = 1, #self.toc do
v = self.toc[i]
if v.page >= pageno then
j = v.page
break
for _, v in ipairs(self.toc) do
if v.depth == depth then
table.insert(ticks, v.page)
end
end
-- normally the ticks are sorted already but in rare cases
-- toc nodes may be not in ascending order
table.sort(ticks)
-- cache ticks only if ticks are available
self.ticks[level] = ticks
end
if j < pageno then
return ""
else
return j
end
return ticks
end
function ReaderToc:getNextChapter(cur_pageno, level)
local ticks = self:getTocTicks(level)
local next_chapter = nil
for i = 1, #ticks do
if ticks[i] > cur_pageno then
next_chapter = ticks[i]
break
end
end
return next_chapter
end
function ReaderToc:getPreviousChapter(cur_pageno, level)
local ticks = self:getTocTicks(level)
local previous_chapter = nil
for i = 1, #ticks do
if ticks[i] >= cur_pageno then
break
end
previous_chapter = ticks[i]
end
return previous_chapter
end
function ReaderToc:isChapterBegin(cur_pageno, level)
local ticks = self:getTocTicks(level)
local _begin = false
for i = 1, #ticks do
if ticks[i] == cur_pageno then
_begin = true
break
end
end
return _begin
end
function ReaderToc:isChapterEnd(cur_pageno, level)
local ticks = self:getTocTicks(level)
local _end= false
for i = 1, #ticks do
if ticks[i] - 1 == cur_pageno then
_end = true
break
end
end
return _end
end
function ReaderToc:getChapterPagesLeft(pageno, level)
--if self:isChapterEnd(pageno, level) then return 0 end
local next_chapter = self:getNextChapter(pageno, level)
if next_chapter then
next_chapter = next_chapter - pageno - 1
end
return next_chapter
end
function ReaderToc:getChapterPagesDone(pageno, level)
if self:isChapterBegin(pageno, level) then return 0 end
local previous_chapter = self:getPreviousChapter(pageno, level)
if previous_chapter then
previous_chapter = pageno - previous_chapter
end
return previous_chapter
end
function ReaderToc:onShowToc()
if not self.toc then
self:fillToc()
end
self:fillToc()
-- build menu items
if #self.toc > 0 and not self.toc[1].text then
for _,v in ipairs(self.toc) do

View File

@@ -11,12 +11,12 @@ local ProgressWidget = Widget:new{
margin_v = 1,
radius = 2,
bordersize = 1,
toc_marker_width = DMINIBAR_TOC_MARKER_WIDTH,
bordercolor = 15,
bgcolor = 0,
rectcolor = 10,
percentage = nil,
TOC = {},
ticks = {},
tick_width = 3,
last = nil,
}
@@ -37,14 +37,12 @@ function ProgressWidget:paintTo(bb, x, y)
bb:paintRect(x+self.margin_h, y+self.margin_v+self.bordersize,
(my_size.w-2*self.margin_h)*self.percentage,
(my_size.h-2*(self.margin_v+self.bordersize)), self.rectcolor)
if DMINIBAR_PROGRESS_MARKER then
if #self.TOC > 0 then
for i=1, #self.TOC do
v = self.TOC[i]
bb:paintRect(x+(my_size.w-2*self.margin_h)*(v.page/self.last), y+self.margin_v+self.bordersize,
self.toc_marker_width,(my_size.h-2*(self.margin_v+self.bordersize)), self.bordercolor)
end
end
for i=1, #self.ticks do
local page = self.ticks[i]
bb:paintRect(
x + (my_size.w-2*self.margin_h)*(page/self.last),
y + self.margin_v + self.bordersize, self.tick_width,
(my_size.h-2*(self.margin_v+self.bordersize)), self.bordercolor)
end
end

View File

@@ -0,0 +1,66 @@
require("commonrequire")
local DocumentRegistry = require("document/documentregistry")
local ReaderUI = require("apps/reader/readerui")
local DEBUG = require("dbg")
describe("Readertoc module", function()
local sample_epub = "spec/front/unit/data/juliet.epub"
local readerui = ReaderUI:new{
document = DocumentRegistry:openDocument(sample_epub),
}
local toc = readerui.toc
local toc_max_depth = nil
it("should get max toc depth", function()
toc_max_depth = toc:getMaxDepth()
assert.are.same(2, toc_max_depth)
end)
it("should get toc title from page", function()
local title = toc:getTocTitleByPage(56)
assert(title == "Prologue")
local title = toc:getTocTitleByPage(172)
assert(title == "SCENE IV. Hall in Capulet's house.")
end)
describe("getTocTicks API", function()
local ticks_level_0 = nil
it("should get ticks of level 0", function()
ticks_level_0 = toc:getTocTicks(0)
DEBUG("ticks", ticks_level_0)
assert.are.same(26, #ticks_level_0)
end)
local ticks_level_1 = nil
it("should get ticks of level 1", function()
ticks_level_1 = toc:getTocTicks(1)
assert.are.same(7, #ticks_level_1)
end)
local ticks_level_m1 = nil
it("should get ticks of level -1", function()
ticks_level_m1 = toc:getTocTicks(1)
assert.are.same(7, #ticks_level_m1)
end)
it("should get the same ticks of level -1 and level 1", function()
if toc_max_depth == 2 then
assert.are.same(ticks_level_1, ticks_level_m1)
end
end)
end)
it("should get page of next chapter", function()
assert.are.same(25, toc:getNextChapter(10, 0))
assert.are.same(103, toc:getNextChapter(100, 0))
assert.are.same(nil, toc:getNextChapter(200, 0))
end)
it("should get page of previous chapter", function()
assert.are.same(9, toc:getPreviousChapter(10, 0))
assert.are.same(95, toc:getPreviousChapter(100, 0))
assert.are.same(190, toc:getPreviousChapter(200, 0))
end)
it("should get page left of chapter", function()
assert.are.same(14, toc:getChapterPagesLeft(10, 0))
assert.are.same(2, toc:getChapterPagesLeft(100, 0))
assert.are.same(nil, toc:getChapterPagesLeft(200, 0))
end)
it("should get page done of chapter", function()
assert.are.same(1, toc:getChapterPagesDone(10, 0))
assert.are.same(0, toc:getChapterPagesDone(100, 0))
assert.are.same(10, toc:getChapterPagesDone(200, 0))
end)
end)

BIN
test/juliet.epub Normal file

Binary file not shown.