mirror of
https://github.com/koreader/koreader.git
synced 2025-08-10 00:52:38 +00:00
Merge pull request #1079 from chrox/fulltext_search
add fulltext search for EPUB documents
This commit is contained in:
2
base
2
base
Submodule base updated: af763e7143...5a83d4aade
@@ -111,19 +111,24 @@ function ReaderHighlight:onSetDimensions(dimen)
|
||||
end
|
||||
|
||||
function ReaderHighlight:clear()
|
||||
if self.ui.document.info.has_pages then
|
||||
self.view.highlight.temp = {}
|
||||
else
|
||||
self.ui.document:clearSelection()
|
||||
end
|
||||
UIManager:setDirty(self.dialog, "partial")
|
||||
if self.hold_pos then
|
||||
if self.ui.document.info.has_pages then
|
||||
self.view.highlight.temp[self.hold_pos.page] = nil
|
||||
else
|
||||
self.ui.document:clearSelection()
|
||||
end
|
||||
self.hold_pos = nil
|
||||
self.selected_text = nil
|
||||
UIManager:setDirty(self.dialog, "partial")
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderHighlight:onClearHighlight()
|
||||
self:clear()
|
||||
return true
|
||||
end
|
||||
|
||||
function ReaderHighlight:onTap(arg, ges)
|
||||
if not self:clear() then
|
||||
if self.ui.document.info.has_pages then
|
||||
@@ -333,11 +338,10 @@ function ReaderHighlight:onHoldRelease()
|
||||
},
|
||||
{
|
||||
{
|
||||
text = _("Share"),
|
||||
enabled = false,
|
||||
text = _("Search"),
|
||||
callback = function()
|
||||
self:shareHighlight()
|
||||
self:onClose()
|
||||
self:onHighlightSearch()
|
||||
UIManager:close(self.highlight_dialog)
|
||||
end,
|
||||
},
|
||||
{
|
||||
@@ -357,16 +361,20 @@ function ReaderHighlight:onHoldRelease()
|
||||
return true
|
||||
end
|
||||
|
||||
function ReaderHighlight:onHighlight()
|
||||
function ReaderHighlight:highlightFromHoldPos()
|
||||
if self.hold_pos then
|
||||
if not self.selected_text then
|
||||
self.selected_text = self.ui.document:getTextFromPositions(self.hold_pos, self.hold_pos)
|
||||
DEBUG("selected text:", self.selected_text)
|
||||
end
|
||||
self:saveHighlight()
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderHighlight:onHighlight()
|
||||
self:highlightFromHoldPos()
|
||||
self:saveHighlight()
|
||||
end
|
||||
|
||||
function ReaderHighlight:saveHighlight()
|
||||
DEBUG("save highlight")
|
||||
local page = self.hold_pos.page
|
||||
@@ -427,6 +435,14 @@ function ReaderHighlight:lookupWikipedia()
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderHighlight:onHighlightSearch()
|
||||
DEBUG("search highlight")
|
||||
self:highlightFromHoldPos()
|
||||
if self.selected_text then
|
||||
self.ui:handleEvent(Event:new("ShowSearchDialog", self.selected_text.text))
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderHighlight:shareHighlight()
|
||||
DEBUG("share highlight")
|
||||
end
|
||||
|
||||
@@ -208,7 +208,11 @@ function ReaderMenu:onShowReaderMenu()
|
||||
end
|
||||
|
||||
main_menu.close_callback = function ()
|
||||
UIManager:close(menu_container)
|
||||
self.ui:handleEvent(Event:new("CloseReaderMenu"))
|
||||
end
|
||||
|
||||
main_menu.touch_menu_callback = function ()
|
||||
self.ui:handleEvent(Event:new("CloseConfigMenu"))
|
||||
end
|
||||
|
||||
menu_container[1] = main_menu
|
||||
|
||||
100
frontend/apps/reader/modules/readersearch.lua
Normal file
100
frontend/apps/reader/modules/readersearch.lua
Normal file
@@ -0,0 +1,100 @@
|
||||
local InputContainer = require("ui/widget/container/inputcontainer")
|
||||
local ButtonDialog = require("ui/widget/buttondialog")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local Geom = require("ui/geometry")
|
||||
local Screen = require("ui/screen")
|
||||
local DEBUG = require("dbg")
|
||||
local _ = require("gettext")
|
||||
|
||||
local ReaderSearch = InputContainer:new{
|
||||
direction = 0, -- 0 for search forward, 1 for search backward
|
||||
case_insensitive = 1, -- default to case insensitive
|
||||
}
|
||||
|
||||
function ReaderSearch:init()
|
||||
self.ui.menu:registerToMainMenu(self)
|
||||
end
|
||||
|
||||
function ReaderSearch:addToMainMenu(tab_item_table)
|
||||
table.insert(tab_item_table.plugins, {
|
||||
text = _("Fulltext search"),
|
||||
tap_input = {
|
||||
title = _("Input text to search for"),
|
||||
type = "text",
|
||||
callback = function(input)
|
||||
self:onShowSearchDialog(input)
|
||||
end,
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
function ReaderSearch:onShowSearchDialog(text)
|
||||
local do_search = function(search_func, text, param)
|
||||
return function()
|
||||
local res = search_func(self, text, param)
|
||||
if res then
|
||||
self.ui.link:onGotoLink(res[1].start)
|
||||
end
|
||||
end
|
||||
end
|
||||
self.search_dialog = ButtonDialog:new{
|
||||
alpha = 0.5,
|
||||
buttons = {
|
||||
{
|
||||
{
|
||||
text = "|<",
|
||||
callback = do_search(self.searchFromStart, text),
|
||||
},
|
||||
{
|
||||
text = "<",
|
||||
callback = do_search(self.searchNext, text, 1),
|
||||
},
|
||||
{
|
||||
text = ">",
|
||||
callback = do_search(self.searchNext, text, 0),
|
||||
},
|
||||
{
|
||||
text = ">|",
|
||||
callback = do_search(self.searchFromEnd, text),
|
||||
},
|
||||
}
|
||||
},
|
||||
tap_close_callback = function()
|
||||
DEBUG("highlight clear")
|
||||
self.ui.highlight:clear()
|
||||
end,
|
||||
}
|
||||
local res = do_search(self.searchFromCurrent, text, 0)()
|
||||
UIManager:show(self.search_dialog)
|
||||
UIManager:setDirty(self.dialog, "partial")
|
||||
return true
|
||||
end
|
||||
|
||||
function ReaderSearch:search(pattern, origin)
|
||||
local direction = self.direction
|
||||
local case = self.case_insensitive
|
||||
return self.ui.document:findText(pattern, origin, direction, case)
|
||||
end
|
||||
|
||||
function ReaderSearch:searchFromStart(pattern)
|
||||
self.direction = 0
|
||||
return self:search(pattern, -1)
|
||||
end
|
||||
|
||||
function ReaderSearch:searchFromEnd(pattern)
|
||||
self.direction = 1
|
||||
return self:search(pattern, -1)
|
||||
end
|
||||
|
||||
function ReaderSearch:searchFromCurrent(pattern, direction)
|
||||
self.direction = direction
|
||||
return self:search(pattern, 0)
|
||||
end
|
||||
|
||||
-- ignore current page and search next occurrence
|
||||
function ReaderSearch:searchNext(pattern, direction)
|
||||
self.direction = direction
|
||||
return self:search(pattern, 1)
|
||||
end
|
||||
|
||||
return ReaderSearch
|
||||
@@ -37,9 +37,10 @@ local ReaderDictionary = require("apps/reader/modules/readerdictionary")
|
||||
local ReaderWikipedia = require("apps/reader/modules/readerwikipedia")
|
||||
local ReaderHyphenation = require("apps/reader/modules/readerhyphenation")
|
||||
local ReaderActivityIndicator = require("apps/reader/modules/readeractivityindicator")
|
||||
local FileManagerHistory = require("apps/filemanager/filemanagerhistory")
|
||||
local ReaderSearch = require("apps/reader/modules/readersearch")
|
||||
local ReaderLink = require("apps/reader/modules/readerlink")
|
||||
local PluginLoader = require("apps/reader/pluginloader")
|
||||
local FileManagerHistory = require("apps/filemanager/filemanagerhistory")
|
||||
|
||||
--[[
|
||||
This is an abstraction for a reader interface
|
||||
@@ -280,6 +281,12 @@ function ReaderUI:init()
|
||||
ui = self
|
||||
})
|
||||
end
|
||||
-- fulltext search
|
||||
self:registerModule("search", ReaderSearch:new{
|
||||
dialog = self.dialog,
|
||||
view = self.view,
|
||||
ui = self
|
||||
})
|
||||
|
||||
-- koreader plugins
|
||||
for _,module in ipairs(PluginLoader:loadPlugins()) do
|
||||
|
||||
@@ -312,6 +312,7 @@ function CreDocument:setFontFace(new_font_face)
|
||||
end
|
||||
|
||||
function CreDocument:clearSelection()
|
||||
DEBUG("clear selection")
|
||||
self._document:clearSelection()
|
||||
end
|
||||
|
||||
@@ -418,6 +419,11 @@ function CreDocument:setStatusLineProp(prop)
|
||||
self._document:setStringProperty("window.status.line", prop)
|
||||
end
|
||||
|
||||
function CreDocument:findText(pattern, origin, reverse, caseInsensitive)
|
||||
DEBUG("CreDocument: find text", pattern, origin, reverse, caseInsensitive)
|
||||
return self._document:findText(pattern, origin, reverse, caseInsensitive)
|
||||
end
|
||||
|
||||
function CreDocument:register(registry)
|
||||
registry:addProvider("txt", "application/txt", self)
|
||||
registry:addProvider("epub", "application/epub", self)
|
||||
|
||||
@@ -217,6 +217,10 @@ function Document:getCoverPageImage()
|
||||
return nil
|
||||
end
|
||||
|
||||
function Document:findText()
|
||||
return nil
|
||||
end
|
||||
|
||||
function Document:getFullPageHash(pageno, zoom, rotation, gamma, render_mode)
|
||||
return "renderpg|"..self.file.."|"..self.mod_time.."|"..pageno.."|"
|
||||
..zoom.."|"..rotation.."|"..gamma.."|"..render_mode
|
||||
|
||||
@@ -384,12 +384,14 @@ function UIManager:run()
|
||||
if force_fast_refresh then
|
||||
waveform_mode = self.fast_waveform_mode
|
||||
end
|
||||
if self.update_region_func then
|
||||
local update_region = self.update_region_func()
|
||||
-- in some rare cases update region has 1 pixel offset
|
||||
Screen:refresh(refresh_type, waveform_mode,
|
||||
update_region.x-1, update_region.y-1,
|
||||
update_region.w+2, update_region.h+2)
|
||||
if self.update_regions_func then
|
||||
local update_regions = self.update_regions_func()
|
||||
for _, update_region in ipairs(update_regions) do
|
||||
-- in some rare cases update region has 1 pixel offset
|
||||
Screen:refresh(refresh_type, waveform_mode,
|
||||
update_region.x-1, update_region.y-1,
|
||||
update_region.w+2, update_region.h+2)
|
||||
end
|
||||
else
|
||||
Screen:refresh(refresh_type, waveform_mode)
|
||||
end
|
||||
@@ -398,7 +400,7 @@ function UIManager:run()
|
||||
elseif not force_partial_refresh and not force_full_refresh then
|
||||
self.refresh_count = (self.refresh_count + 1)%self.FULL_REFRESH_COUNT
|
||||
end
|
||||
self.update_region_func = nil
|
||||
self.update_regions_func = nil
|
||||
end
|
||||
|
||||
self:checkTasks()
|
||||
|
||||
@@ -187,10 +187,10 @@ function DictQuickLookup:update()
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("More"),
|
||||
enabled = false,
|
||||
text = _("Search"),
|
||||
callback = function()
|
||||
self.ui:handleEvent(Event:new("HighlightMore"))
|
||||
self.ui:handleEvent(Event:new("HighlightSearch"))
|
||||
UIManager:close(self)
|
||||
end,
|
||||
},
|
||||
},
|
||||
@@ -286,10 +286,10 @@ function DictQuickLookup:changeDictionary(index)
|
||||
local orig_dimen = self.dict_frame and self.dict_frame.dimen or Geom:new{}
|
||||
self:update()
|
||||
|
||||
UIManager.update_region_func = function()
|
||||
UIManager.update_regions_func = function()
|
||||
local update_region = self.dict_frame.dimen:combine(orig_dimen)
|
||||
DEBUG("update region", update_region)
|
||||
return update_region
|
||||
DEBUG("update dict region", update_region)
|
||||
return {update_region}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -465,6 +465,9 @@ function TouchMenu:netToggle()
|
||||
end
|
||||
|
||||
function TouchMenu:switchMenuTab(tab_num)
|
||||
if self.touch_menu_callback then
|
||||
self.touch_menu_callback()
|
||||
end
|
||||
if self.tab_item_table[tab_num].callback then
|
||||
self.tab_item_table[tab_num].callback()
|
||||
end
|
||||
@@ -516,6 +519,9 @@ function TouchMenu:onSwipe(arg, ges_ev)
|
||||
end
|
||||
|
||||
function TouchMenu:onMenuSelect(item)
|
||||
if self.touch_menu_callback then
|
||||
self.touch_menu_callback()
|
||||
end
|
||||
if item.tap_input then
|
||||
self:closeMenu()
|
||||
self:onMenuInput(item.tap_input)
|
||||
@@ -547,6 +553,9 @@ function TouchMenu:onMenuSelect(item)
|
||||
end
|
||||
|
||||
function TouchMenu:onMenuHold(item)
|
||||
if self.touch_menu_callback then
|
||||
self.touch_menu_callback()
|
||||
end
|
||||
if item.hold_input then
|
||||
self:closeMenu()
|
||||
self:onMenuInput(item.hold_input)
|
||||
|
||||
@@ -95,27 +95,47 @@ function VirtualKey:init()
|
||||
end
|
||||
end
|
||||
|
||||
function VirtualKey:update_keyboard()
|
||||
UIManager.update_regions_func = function()
|
||||
DEBUG("update key region", self[1].dimen)
|
||||
return {self[1].dimen}
|
||||
end
|
||||
UIManager:setDirty(self.keyboard, "partial")
|
||||
end
|
||||
|
||||
function VirtualKey:update_keyboard_inputbox()
|
||||
local inputbox = self.keyboard.inputbox
|
||||
UIManager.update_regions_func = function()
|
||||
DEBUG("update keyboard and inputbox", self[1].dimen, inputbox.dimen)
|
||||
return {self[1].dimen, inputbox.dimen}
|
||||
end
|
||||
UIManager:setDirty(inputbox, "partial")
|
||||
UIManager:setDirty(self.keyboard, "partial")
|
||||
end
|
||||
|
||||
function VirtualKey:onTapSelect()
|
||||
self[1].invert = true
|
||||
self:update_keyboard_inputbox()
|
||||
if self.callback then
|
||||
self.callback()
|
||||
end
|
||||
UIManager:scheduleIn(0.02, function() self:invert(false) end)
|
||||
UIManager:scheduleIn(0.2, function() self:invert(false) end)
|
||||
return true
|
||||
end
|
||||
|
||||
function VirtualKey:onHoldSelect()
|
||||
self[1].invert = true
|
||||
self:update_keyboard_inputbox()
|
||||
if self.hold_callback then
|
||||
self.hold_callback()
|
||||
end
|
||||
UIManager:scheduleIn(0.5, function() self:invert(false) end)
|
||||
UIManager:scheduleIn(0.2, function() self:invert(false) end)
|
||||
return true
|
||||
end
|
||||
|
||||
function VirtualKey:invert(invert)
|
||||
self[1].invert = invert
|
||||
UIManager:setDirty(self.keyboard, "partial")
|
||||
self:update_keyboard()
|
||||
end
|
||||
|
||||
local VirtualKeyboard = InputContainer:new{
|
||||
@@ -298,28 +318,23 @@ function VirtualKeyboard:setLayout(key)
|
||||
if self.utf8mode then self.umlautmode = false end
|
||||
end
|
||||
self:initLayout()
|
||||
UIManager.update_regions_func = nil
|
||||
UIManager:setDirty(self, "partial")
|
||||
end
|
||||
|
||||
function VirtualKeyboard:addChar(key)
|
||||
DEBUG("add char", key)
|
||||
self.inputbox:addChar(key)
|
||||
UIManager:setDirty(self, "partial")
|
||||
UIManager:setDirty(self.inputbox, "partial")
|
||||
end
|
||||
|
||||
function VirtualKeyboard:delChar()
|
||||
DEBUG("delete char")
|
||||
self.inputbox:delChar()
|
||||
UIManager:setDirty(self, "partial")
|
||||
UIManager:setDirty(self.inputbox, "partial")
|
||||
end
|
||||
|
||||
function VirtualKeyboard:clear()
|
||||
DEBUG("clear input")
|
||||
self.inputbox:clear()
|
||||
UIManager:setDirty(self, "partial")
|
||||
UIManager:setDirty(self.inputbox, "partial")
|
||||
end
|
||||
|
||||
return VirtualKeyboard
|
||||
|
||||
94
spec/unit/readersearch_spec.lua
Normal file
94
spec/unit/readersearch_spec.lua
Normal file
@@ -0,0 +1,94 @@
|
||||
require("commonrequire")
|
||||
local DocumentRegistry = require("document/documentregistry")
|
||||
local ReaderUI = require("apps/reader/readerui")
|
||||
local DEBUG = require("dbg")
|
||||
|
||||
local sample_epub = "spec/front/unit/data/juliet.epub"
|
||||
|
||||
describe("Readersearch module", function()
|
||||
describe("search API for EPUB documents", function()
|
||||
local doc, search, rolling
|
||||
setup(function()
|
||||
local readerui = ReaderUI:new{
|
||||
document = DocumentRegistry:openDocument(sample_epub),
|
||||
}
|
||||
doc = readerui.document
|
||||
search = readerui.search
|
||||
rolling = readerui.rolling
|
||||
end)
|
||||
it("should search backward", function()
|
||||
rolling:gotoPage(10)
|
||||
assert.truthy(search:searchFromCurrent("Verona", 1))
|
||||
for i = 1, 100, 10 do
|
||||
rolling:gotoPage(i)
|
||||
local words = search:searchFromCurrent("Verona", 1)
|
||||
if words then
|
||||
for _, word in ipairs(words) do
|
||||
local pageno = doc:getPageFromXPointer(word.start)
|
||||
--DEBUG("found at pageno", pageno)
|
||||
assert.truthy(pageno <= i)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
it("should search forward", function()
|
||||
rolling:gotoPage(10)
|
||||
assert.truthy(search:searchFromCurrent("Verona", 0))
|
||||
for i = 1, 100, 10 do
|
||||
rolling:gotoPage(i)
|
||||
local words = search:searchFromCurrent("Verona", 0)
|
||||
if words then
|
||||
for _, word in ipairs(words) do
|
||||
local pageno = doc:getPageFromXPointer(word.start)
|
||||
--DEBUG("found at pageno", pageno)
|
||||
assert.truthy(pageno >= i)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
it("should find the first occurrence", function()
|
||||
for i = 10, 100, 10 do
|
||||
rolling:gotoPage(i)
|
||||
local words = search:searchFromStart("Verona")
|
||||
assert.truthy(words)
|
||||
local pageno = doc:getPageFromXPointer(words[1].start)
|
||||
assert.are.equal(8, pageno)
|
||||
end
|
||||
for i = 1, 5, 1 do
|
||||
rolling:gotoPage(i)
|
||||
local words = search:searchFromStart("Verona")
|
||||
assert(words == nil)
|
||||
end
|
||||
end)
|
||||
it("should find the last occurrence", function()
|
||||
for i = 100, 200, 10 do
|
||||
rolling:gotoPage(i)
|
||||
local words = search:searchFromEnd("Verona")
|
||||
assert.truthy(words)
|
||||
local pageno = doc:getPageFromXPointer(words[1].start)
|
||||
assert.are.equal(208, pageno)
|
||||
end
|
||||
for i = 230, 235, 1 do
|
||||
rolling:gotoPage(i)
|
||||
local words = search:searchFromEnd("Verona")
|
||||
assert(words == nil)
|
||||
end
|
||||
end)
|
||||
it("should find all occurrences", function()
|
||||
local count = 0
|
||||
rolling:gotoPage(1)
|
||||
local words = search:searchFromCurrent("Verona", 0)
|
||||
while words do
|
||||
count = count + #words
|
||||
for _, word in ipairs(words) do
|
||||
--DEBUG("found word", word.start)
|
||||
end
|
||||
doc:gotoXPointer(words[1].start)
|
||||
words = search:searchNext("Verona", 0)
|
||||
end
|
||||
assert.are.equal(13, count)
|
||||
end)
|
||||
end)
|
||||
describe("search API for PDF documents", function()
|
||||
end)
|
||||
end)
|
||||
Reference in New Issue
Block a user