|
|
|
local ButtonDialog = require("ui/widget/buttondialog")
|
|
|
|
local CheckButton = require("ui/widget/checkbutton")
|
|
|
|
local CenterContainer = require("ui/widget/container/centercontainer")
|
|
|
|
local ConfirmBox = require("ui/widget/confirmbox")
|
|
|
|
local DocSettings = require("docsettings")
|
|
|
|
local DocumentRegistry = require("document/documentregistry")
|
|
|
|
local FileChooser = require("ui/widget/filechooser")
|
|
|
|
local InfoMessage = require("ui/widget/infomessage")
|
|
|
|
local InputDialog = require("ui/widget/inputdialog")
|
|
|
|
local Menu = require("ui/widget/menu")
|
|
|
|
local UIManager = require("ui/uimanager")
|
Clarify our OOP semantics across the codebase (#9586)
Basically:
* Use `extend` for class definitions
* Use `new` for object instantiations
That includes some minor code cleanups along the way:
* Updated `Widget`'s docs to make the semantics clearer.
* Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283)
* Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass).
* Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events.
* Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier.
* Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references.
* ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak).
* Terminal: Make sure the shell is killed on plugin teardown.
* InputText: Fix Home/End/Del physical keys to behave sensibly.
* InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...).
* OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of.
* ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed!
* Kobo: Minor code cleanups.
2 years ago
|
|
|
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
|
|
|
local Utf8Proc = require("ffi/utf8proc")
|
|
|
|
local filemanagerutil = require("apps/filemanager/filemanagerutil")
|
|
|
|
local lfs = require("libs/libkoreader-lfs")
|
|
|
|
local util = require("util")
|
|
|
|
local _ = require("gettext")
|
|
|
|
local N_ = _.ngettext
|
|
|
|
local Screen = require("device").screen
|
|
|
|
local T = require("ffi/util").template
|
|
|
|
|
Clarify our OOP semantics across the codebase (#9586)
Basically:
* Use `extend` for class definitions
* Use `new` for object instantiations
That includes some minor code cleanups along the way:
* Updated `Widget`'s docs to make the semantics clearer.
* Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283)
* Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass).
* Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events.
* Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier.
* Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references.
* ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak).
* Terminal: Make sure the shell is killed on plugin teardown.
* InputText: Fix Home/End/Del physical keys to behave sensibly.
* InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...).
* OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of.
* ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed!
* Kobo: Minor code cleanups.
2 years ago
|
|
|
local FileSearcher = WidgetContainer:extend{
|
|
|
|
case_sensitive = false,
|
|
|
|
include_subfolders = true,
|
|
|
|
include_metadata = false,
|
|
|
|
}
|
|
|
|
|
Clarify our OOP semantics across the codebase (#9586)
Basically:
* Use `extend` for class definitions
* Use `new` for object instantiations
That includes some minor code cleanups along the way:
* Updated `Widget`'s docs to make the semantics clearer.
* Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283)
* Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass).
* Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events.
* Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier.
* Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references.
* ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak).
* Terminal: Make sure the shell is killed on plugin teardown.
* InputText: Fix Home/End/Del physical keys to behave sensibly.
* InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...).
* OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of.
* ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed!
* Kobo: Minor code cleanups.
2 years ago
|
|
|
function FileSearcher:init()
|
|
|
|
end
|
|
|
|
|
|
|
|
function FileSearcher:onShowFileSearch(search_string)
|
|
|
|
local search_dialog
|
|
|
|
local check_button_case, check_button_subfolders, check_button_metadata
|
|
|
|
search_dialog = InputDialog:new{
|
|
|
|
title = _("Enter text to search for in filename"),
|
|
|
|
input = search_string or self.search_string,
|
|
|
|
buttons = {
|
|
|
|
{
|
|
|
|
{
|
|
|
|
text = _("Cancel"),
|
|
|
|
id = "close",
|
|
|
|
callback = function()
|
|
|
|
UIManager:close(search_dialog)
|
|
|
|
end,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text = _("Home folder"),
|
|
|
|
enabled = G_reader_settings:has("home_dir"),
|
|
|
|
callback = function()
|
|
|
|
self.search_string = search_dialog:getInputText()
|
|
|
|
if self.search_string == "" then return end
|
|
|
|
UIManager:close(search_dialog)
|
|
|
|
self.path = G_reader_settings:readSetting("home_dir")
|
|
|
|
self:doSearch()
|
|
|
|
end,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text = self.ui.file_chooser and _("Current folder") or _("Book folder"),
|
|
|
|
is_enter_default = true,
|
|
|
|
callback = function()
|
|
|
|
self.search_string = search_dialog:getInputText()
|
|
|
|
if self.search_string == "" then return end
|
|
|
|
UIManager:close(search_dialog)
|
|
|
|
self.path = self.ui.file_chooser and self.ui.file_chooser.path or self.ui:getLastDirFile()
|
|
|
|
self:doSearch()
|
|
|
|
end,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
check_button_case = CheckButton:new{
|
|
|
|
text = _("Case sensitive"),
|
|
|
|
checked = self.case_sensitive,
|
|
|
|
parent = search_dialog,
|
|
|
|
callback = function()
|
|
|
|
self.case_sensitive = check_button_case.checked
|
|
|
|
end,
|
|
|
|
}
|
|
|
|
search_dialog:addWidget(check_button_case)
|
|
|
|
check_button_subfolders = CheckButton:new{
|
|
|
|
text = _("Include subfolders"),
|
|
|
|
checked = self.include_subfolders,
|
|
|
|
parent = search_dialog,
|
|
|
|
callback = function()
|
|
|
|
self.include_subfolders = check_button_subfolders.checked
|
|
|
|
end,
|
|
|
|
}
|
|
|
|
search_dialog:addWidget(check_button_subfolders)
|
|
|
|
if self.ui.coverbrowser then
|
|
|
|
check_button_metadata = CheckButton:new{
|
|
|
|
text = _("Also search in book metadata"),
|
|
|
|
checked = self.include_metadata,
|
|
|
|
parent = search_dialog,
|
|
|
|
callback = function()
|
|
|
|
self.include_metadata = check_button_metadata.checked
|
|
|
|
end,
|
|
|
|
}
|
|
|
|
search_dialog:addWidget(check_button_metadata)
|
|
|
|
end
|
|
|
|
UIManager:show(search_dialog)
|
|
|
|
search_dialog:onShowKeyboard()
|
|
|
|
end
|
|
|
|
|
|
|
|
function FileSearcher:doSearch()
|
|
|
|
local results
|
|
|
|
local dirs, files = self:getList()
|
|
|
|
-- If we have a FileChooser instance, use it, to be able to make use of its natsort cache
|
|
|
|
if self.ui.file_chooser then
|
|
|
|
results = self.ui.file_chooser:genItemTable(dirs, files)
|
|
|
|
else
|
|
|
|
results = FileChooser:genItemTable(dirs, files)
|
|
|
|
end
|
|
|
|
if #results > 0 then
|
|
|
|
self:showSearchResults(results)
|
|
|
|
else
|
|
|
|
self:showSearchResultsMessage(true)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function FileSearcher:getList()
|
|
|
|
self.no_metadata_count = 0
|
|
|
|
local sys_folders = { -- do not search in sys_folders
|
|
|
|
["/dev"] = true,
|
|
|
|
["/proc"] = true,
|
|
|
|
["/sys"] = true,
|
|
|
|
}
|
|
|
|
local collate = G_reader_settings:readSetting("collate")
|
|
|
|
local search_string = self.search_string
|
|
|
|
if search_string ~= "*" then -- one * to show all files
|
|
|
|
if not self.case_sensitive then
|
|
|
|
search_string = Utf8Proc.lowercase(util.fixUtf8(search_string, "?"))
|
|
|
|
end
|
|
|
|
-- replace '.' with '%.'
|
|
|
|
search_string = search_string:gsub("%.","%%%.")
|
|
|
|
-- replace '*' with '.*'
|
|
|
|
search_string = search_string:gsub("%*","%.%*")
|
|
|
|
-- replace '?' with '.'
|
|
|
|
search_string = search_string:gsub("%?","%.")
|
|
|
|
end
|
|
|
|
|
|
|
|
local dirs, files = {}, {}
|
|
|
|
local scan_dirs = {self.path}
|
|
|
|
while #scan_dirs ~= 0 do
|
|
|
|
local new_dirs = {}
|
|
|
|
-- handle each dir
|
|
|
|
for _, d in ipairs(scan_dirs) do
|
|
|
|
-- handle files in d
|
|
|
|
local ok, iter, dir_obj = pcall(lfs.dir, d)
|
|
|
|
if ok then
|
|
|
|
for f in iter, dir_obj do
|
|
|
|
local fullpath = "/" .. f
|
|
|
|
if d ~= "/" then
|
|
|
|
fullpath = d .. fullpath
|
|
|
|
end
|
|
|
|
local attributes = lfs.attributes(fullpath) or {}
|
|
|
|
-- Don't traverse hidden folders if we're not showing them
|
|
|
|
if attributes.mode == "directory" and f ~= "." and f ~= ".."
|
|
|
|
and (FileChooser.show_hidden or not util.stringStartsWith(f, "."))
|
|
|
|
and FileChooser:show_dir(f) then
|
|
|
|
if self.include_subfolders and not sys_folders[fullpath] then
|
|
|
|
table.insert(new_dirs, fullpath)
|
|
|
|
end
|
|
|
|
if self:isFileMatch(f, fullpath, search_string) then
|
|
|
|
table.insert(dirs, FileChooser.getListItem(f, fullpath, attributes))
|
|
|
|
end
|
|
|
|
-- Always ignore macOS resource forks, too.
|
|
|
|
elseif attributes.mode == "file" and not util.stringStartsWith(f, "._")
|
|
|
|
and (FileChooser.show_unsupported or DocumentRegistry:hasProvider(fullpath))
|
|
|
|
and FileChooser:show_file(f) then
|
|
|
|
if self:isFileMatch(f, fullpath, search_string, true) then
|
|
|
|
table.insert(files, FileChooser.getListItem(f, fullpath, attributes, collate))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
scan_dirs = new_dirs
|
|
|
|
end
|
|
|
|
return dirs, files
|
|
|
|
end
|
|
|
|
|
|
|
|
function FileSearcher:isFileMatch(filename, fullpath, search_string, is_file)
|
|
|
|
if search_string == "*" then
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
if not self.case_sensitive then
|
|
|
|
filename = Utf8Proc.lowercase(util.fixUtf8(filename, "?"))
|
|
|
|
end
|
|
|
|
if string.find(filename, search_string) then
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
if self.include_metadata and is_file and DocumentRegistry:hasProvider(fullpath) then
|
|
|
|
local book_props = self.ui.coverbrowser:getBookInfo(fullpath) or
|
|
|
|
self.ui.bookinfo.getDocProps(fullpath, nil, true) -- do not open the document
|
|
|
|
if next(book_props) ~= nil then
|
|
|
|
if self.ui.bookinfo:findInProps(book_props, search_string, self.case_sensitive) then
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
else
|
|
|
|
self.no_metadata_count = self.no_metadata_count + 1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function FileSearcher:showSearchResultsMessage(no_results)
|
|
|
|
local text = no_results and T(_("No results for '%1'."), self.search_string)
|
|
|
|
if self.no_metadata_count == 0 then
|
|
|
|
UIManager:show(InfoMessage:new{ text = text })
|
|
|
|
else
|
|
|
|
local txt = T(N_("1 book has been skipped.", "%1 books have been skipped.",
|
|
|
|
self.no_metadata_count), self.no_metadata_count) .. "\n" ..
|
|
|
|
_("Not all books metadata extracted yet.\nExtract metadata now?")
|
|
|
|
text = no_results and text .. "\n\n" .. txt or txt
|
|
|
|
UIManager:show(ConfirmBox:new{
|
|
|
|
text = text,
|
|
|
|
ok_text = _("Extract"),
|
|
|
|
ok_callback = function()
|
|
|
|
if not no_results then
|
|
|
|
self.search_menu.close_callback()
|
|
|
|
end
|
|
|
|
self.ui.coverbrowser:extractBooksInDirectory(self.path)
|
|
|
|
end
|
|
|
|
})
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function FileSearcher:showSearchResults(results)
|
|
|
|
local menu_container = CenterContainer:new{
|
|
|
|
dimen = Screen:getSize(),
|
|
|
|
}
|
|
|
|
self.search_menu = Menu:new{
|
|
|
|
ui = self.ui,
|
|
|
|
covers_fullscreen = true, -- hint for UIManager:_repaint()
|
|
|
|
is_borderless = true,
|
|
|
|
is_popout = false,
|
|
|
|
show_parent = menu_container,
|
|
|
|
onMenuSelect = self.onMenuSelect,
|
|
|
|
onMenuHold = self.onMenuHold,
|
|
|
|
handle_hold_on_hold_release = true,
|
|
|
|
}
|
|
|
|
table.insert(menu_container, self.search_menu)
|
|
|
|
self.search_menu.close_callback = function()
|
|
|
|
UIManager:close(menu_container)
|
|
|
|
if self.ui.file_chooser then
|
|
|
|
self.ui.file_chooser:refreshPath()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
self.search_menu:switchItemTable(T(_("Search results (%1)"), #results), results)
|
|
|
|
UIManager:show(menu_container)
|
|
|
|
if self.no_metadata_count ~= 0 then
|
|
|
|
self:showSearchResultsMessage()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function FileSearcher:onMenuSelect(item)
|
|
|
|
local file = item.path
|
|
|
|
local dialog
|
|
|
|
local function close_dialog_callback()
|
|
|
|
UIManager:close(dialog)
|
|
|
|
end
|
|
|
|
local function close_dialog_menu_callback()
|
|
|
|
UIManager:close(dialog)
|
|
|
|
self.close_callback()
|
|
|
|
end
|
|
|
|
local buttons = {}
|
|
|
|
if item.is_file then
|
|
|
|
local is_currently_opened = self.ui.document and self.ui.document.file == file
|
|
|
|
if DocumentRegistry:hasProvider(file) or DocSettings:hasSidecarFile(file) then
|
|
|
|
local doc_settings_or_file = is_currently_opened and self.ui.doc_settings or file
|
|
|
|
table.insert(buttons, filemanagerutil.genStatusButtonsRow(doc_settings_or_file, close_dialog_callback))
|
|
|
|
table.insert(buttons, {}) -- separator
|
|
|
|
table.insert(buttons, {
|
|
|
|
filemanagerutil.genResetSettingsButton(file, close_dialog_callback, is_currently_opened),
|
|
|
|
filemanagerutil.genAddRemoveFavoritesButton(file, close_dialog_callback),
|
|
|
|
})
|
|
|
|
end
|
|
|
|
table.insert(buttons, {
|
|
|
|
{
|
|
|
|
text = _("Delete"),
|
|
|
|
enabled = not is_currently_opened,
|
|
|
|
callback = function()
|
|
|
|
local function post_delete_callback()
|
|
|
|
UIManager:close(dialog)
|
|
|
|
for i, menu_item in ipairs(self.item_table) do
|
|
|
|
if menu_item.path == file then
|
|
|
|
table.remove(self.item_table, i)
|
|
|
|
break
|
|
|
|
end
|
|
|
|
self:switchItemTable(T(_("Search results (%1)"), #self.item_table), self.item_table)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
local FileManager = require("apps/filemanager/filemanager")
|
|
|
|
FileManager:showDeleteFileDialog(file, post_delete_callback)
|
|
|
|
end,
|
|
|
|
},
|
|
|
|
filemanagerutil.genBookInformationButton(file, close_dialog_callback),
|
|
|
|
})
|
|
|
|
end
|
|
|
|
table.insert(buttons, {
|
|
|
|
filemanagerutil.genShowFolderButton(file, close_dialog_menu_callback),
|
|
|
|
{
|
|
|
|
text = _("Open"),
|
|
|
|
enabled = DocumentRegistry:hasProvider(file, nil, true), -- allow auxiliary providers
|
|
|
|
callback = function()
|
|
|
|
close_dialog_callback()
|
|
|
|
local FileManager = require("apps/filemanager/filemanager")
|
|
|
|
FileManager.openFile(self.ui, file, nil, self.close_callback)
|
|
|
|
end,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
dialog = ButtonDialog:new{
|
|
|
|
title = file,
|
|
|
|
buttons = buttons,
|
|
|
|
}
|
|
|
|
UIManager:show(dialog)
|
|
|
|
end
|
|
|
|
|
|
|
|
function FileSearcher:onMenuHold(item)
|
|
|
|
if item.is_file then
|
|
|
|
if DocumentRegistry:hasProvider(item.path, nil, true) then
|
|
|
|
local FileManager = require("apps/filemanager/filemanager")
|
|
|
|
FileManager.openFile(self.ui, item.path, nil, self.close_callback)
|
|
|
|
end
|
|
|
|
else
|
|
|
|
self.close_callback()
|
|
|
|
if self.ui.file_chooser then
|
|
|
|
local pathname = util.splitFilePathName(item.path)
|
|
|
|
self.ui.file_chooser:changeToPath(pathname, item.path)
|
|
|
|
else -- called from Reader
|
|
|
|
self.ui:onClose()
|
|
|
|
self.ui:showFileManager(item.path)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
|
|
|
return FileSearcher
|