Filesearcher: add search in book metadata (#10198)

reviewable/pr10257/r1
hius07 1 year ago committed by GitHub
parent df9c2bcb7f
commit 4d26650ad6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -134,15 +134,12 @@ function FileManager:setupLayout()
-- remember to adjust the height when new item is added to the group
path = self.root_path,
focused_path = self.focused_file,
collate = G_reader_settings:readSetting("collate") or "strcoll",
reverse_collate = G_reader_settings:isTrue("reverse_collate"),
show_parent = self.show_parent,
show_hidden = show_hidden,
width = Screen:getWidth(),
height = Screen:getHeight() - self.title_bar:getHeight(),
is_popout = false,
is_borderless = true,
has_close_button = true,
show_hidden = show_hidden,
show_unsupported = show_unsupported,
file_filter = function(filename)
if DocumentRegistry:hasProvider(filename) then
@ -793,16 +790,6 @@ function FileManager:toggleUnsupportedFiles()
G_reader_settings:saveSetting("show_unsupported", self.file_chooser.show_unsupported)
end
function FileManager:setCollate(collate)
self.file_chooser:setCollate(collate)
G_reader_settings:saveSetting("collate", self.file_chooser.collate)
end
function FileManager:toggleReverseCollate()
self.file_chooser:toggleReverseCollate()
G_reader_settings:saveSetting("reverse_collate", self.file_chooser.reverse_collate)
end
function FileManager:onClose()
logger.dbg("close filemanager")
PluginLoader:finalize()
@ -876,7 +863,6 @@ function FileManager:openRandomFile(dir)
self:openRandomFile(dir)
end,
})
UIManager:close(self.file_dialog)
else
UIManager:show(InfoMessage:new {
text = _("File not found"),
@ -1138,106 +1124,6 @@ function FileManager:renameFile(file, basename, is_file)
end
end
function FileManager:getSortingMenuTable()
local fm = self
local collates = {
strcoll = {_("filename"), _("Sort by filename")},
natural = {_("natural"), _("Sort by filename (natural sorting)")},
strcoll_mixed = {_("name mixed"), _("Sort by name mixed files and folders")},
access = {_("date read"), _("Sort by last read date")},
change = {_("date added"), _("Sort by date added")},
modification = {_("date modified"), _("Sort by date modified")},
size = {_("size"), _("Sort by size")},
type = {_("type"), _("Sort by type")},
percent_unopened_first = {_("percent unopened first"), _("Sort by percent unopened first")},
percent_unopened_last = {_("percent unopened last"), _("Sort by percent unopened last")},
}
local set_collate_table = function(collate)
return {
text = collates[collate][2],
checked_func = function()
return fm.file_chooser.collate == collate
end,
callback = function() fm:setCollate(collate) end,
}
end
local get_collate_percent = function()
local collate_type = G_reader_settings:readSetting("collate")
if collate_type == "percent_unopened_first" or collate_type == "percent_unopened_last" then
return collates[collate_type][2]
else
return _("Sort by percent")
end
end
return {
text_func = function()
return T(
_("Sort by: %1"),
collates[fm.file_chooser.collate][1]
)
end,
sub_item_table = {
set_collate_table("strcoll"),
set_collate_table("natural"),
set_collate_table("strcoll_mixed"),
set_collate_table("access"),
set_collate_table("change"),
set_collate_table("modification"),
set_collate_table("size"),
set_collate_table("type"),
{
text_func = get_collate_percent,
checked_func = function()
return fm.file_chooser.collate == "percent_unopened_first"
or fm.file_chooser.collate == "percent_unopened_last"
end,
sub_item_table = {
set_collate_table("percent_unopened_first"),
set_collate_table("percent_unopened_last"),
}
},
}
}
end
function FileManager:getStartWithMenuTable()
local start_with_setting = G_reader_settings:readSetting("start_with") or "filemanager"
local start_withs = {
filemanager = {_("file browser"), _("Start with file browser")},
history = {_("history"), _("Start with history")},
favorites = {_("favorites"), _("Start with favorites")},
folder_shortcuts = {_("folder shortcuts"), _("Start with folder shortcuts")},
last = {_("last file"), _("Start with last file")},
}
local set_sw_table = function(start_with)
return {
text = start_withs[start_with][2],
checked_func = function()
return start_with_setting == start_with
end,
callback = function()
start_with_setting = start_with
G_reader_settings:saveSetting("start_with", start_with)
end,
}
end
return {
text_func = function()
return T(
_("Start with: %1"),
start_withs[start_with_setting][1]
)
end,
sub_item_table = {
set_sw_table("filemanager"),
set_sw_table("history"),
set_sw_table("favorites"),
set_sw_table("folder_shortcuts"),
set_sw_table("last"),
}
}
end
--- @note: This is the *only* safe way to instantiate a new FileManager instance!
function FileManager:showFiles(path, focused_file)
-- Warn about and close any pre-existing FM instances first...

@ -157,7 +157,7 @@ function BookInfo:show(file, book_props)
UIManager:show(widget)
end
function BookInfo:getBookProps(file, book_props)
function BookInfo:getBookProps(file, book_props, no_open_document)
if DocSettings:hasSidecarFile(file) then
local doc_settings = DocSettings:open(file)
if not book_props then
@ -185,7 +185,7 @@ function BookInfo:getBookProps(file, book_props)
end
-- If still no book_props (book never opened or empty "stats"), open the document to get them
if not book_props then
if not book_props and not no_open_document then
local document = DocumentRegistry:openDocument(file)
if document then
local loaded = true

@ -3,10 +3,9 @@ local CheckButton = require("ui/widget/checkbutton")
local CenterContainer = require("ui/widget/container/centercontainer")
local DocumentRegistry = require("document/documentregistry")
local FileChooser = require("ui/widget/filechooser")
local InfoMessage = require("ui/widget/infomessage")
local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo")
local InputDialog = require("ui/widget/inputdialog")
local Menu = require("ui/widget/menu")
local Size = require("ui/size")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local BaseUtil = require("ffi/util")
@ -14,136 +13,24 @@ local Utf8Proc = require("ffi/utf8proc")
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
local T = BaseUtil.template
local FileSearcher = WidgetContainer:extend{
dirs = nil, -- table
files = nil, -- table
results = nil, -- table
case_sensitive = false,
include_subfolders = true,
}
local sys_folders = { -- do not search in sys_folders
["/dev"] = true,
["/proc"] = true,
["/sys"] = true,
include_metadata = false,
}
function FileSearcher:init()
self.dirs = {}
self.files = {}
self.results = {}
end
function FileSearcher:readDir()
local ReaderUI = require("apps/reader/readerui")
local show_unsupported = G_reader_settings:isTrue("show_unsupported")
self.dirs = {self.path}
self.files = {}
while #self.dirs ~= 0 do
local new_dirs = {}
-- handle each dir
for __, d in pairs(self.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 (G_reader_settings:isTrue("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
table.insert(self.files, {
dir = d,
name = f,
text = f.."/",
attr = attributes,
callback = function()
self:showFolder(fullpath .. "/")
end,
})
-- Always ignore macOS resource forks, too.
elseif attributes.mode == "file" and not util.stringStartsWith(f, "._")
and (show_unsupported or DocumentRegistry:hasProvider(fullpath))
and FileChooser:show_file(f)
then
table.insert(self.files, {
dir = d,
name = f,
text = f,
mandatory = util.getFriendlySize(attributes.size or 0),
attr = attributes,
callback = function()
ReaderUI:showReader(fullpath)
end,
})
end
end
end
end
self.dirs = new_dirs
end
end
function FileSearcher:setSearchResults()
local keywords = self.search_value
self.results = {}
if keywords == "*" then -- one * to show all files
self.results = self.files
else
if not self.case_sensitive then
keywords = Utf8Proc.lowercase(util.fixUtf8(keywords, "?"))
end
-- replace '.' with '%.'
keywords = keywords:gsub("%.","%%%.")
-- replace '*' with '.*'
keywords = keywords:gsub("%*","%.%*")
-- replace '?' with '.'
keywords = keywords:gsub("%?","%.")
for __,f in pairs(self.files) do
if self.case_sensitive then
if string.find(f.name, keywords) then
table.insert(self.results, f)
end
else
if string.find(Utf8Proc.lowercase(util.fixUtf8(f.name, "?")), keywords) then
table.insert(self.results, f)
end
end
end
end
end
function FileSearcher:close()
UIManager:close(self.search_dialog)
self:readDir() --- @todo this probably doesn't need to be repeated once it's been done
self:setSearchResults() --- @todo doesn't have to be repeated if the search term is the same
if #self.results > 0 then
self:showSearchResults()
else
UIManager:show(
InfoMessage:new{
text = BaseUtil.template(_("No results for '%1'."),
self.search_value)
}
)
end
end
function FileSearcher:onShowFileSearch(search_string)
self.search_dialog = InputDialog:new{
title = _("Enter filename to search for"),
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_value,
buttons = {
{
@ -151,125 +38,297 @@ function FileSearcher:onShowFileSearch(search_string)
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(self.search_dialog)
UIManager:close(search_dialog)
end,
},
{
text = _("Home folder"),
enabled = G_reader_settings:has("home_dir"),
callback = function()
self.search_value = self.search_dialog:getInputText()
self.search_value = search_dialog:getInputText()
if self.search_value == "" then return end
UIManager:close(search_dialog)
self.path = G_reader_settings:readSetting("home_dir")
self:close()
self:doSearch()
end,
},
{
text = _("Current folder"),
text = self.ui.file_chooser and _("Current folder") or _("Book folder"),
is_enter_default = true,
callback = function()
self.search_value = self.search_dialog:getInputText()
self.search_value = search_dialog:getInputText()
if self.search_value == "" then return end
UIManager:close(search_dialog)
self.path = self.ui.file_chooser and self.ui.file_chooser.path or self.ui:getLastDirFile()
self:close()
self:doSearch()
end,
},
},
},
}
self.check_button_case = CheckButton:new{
check_button_case = CheckButton:new{
text = _("Case sensitive"),
checked = self.case_sensitive,
parent = self.search_dialog,
parent = search_dialog,
callback = function()
self.case_sensitive = self.check_button_case.checked
self.case_sensitive = check_button_case.checked
end,
}
self.search_dialog:addWidget(self.check_button_case)
self.check_button_subfolders = CheckButton:new{
search_dialog:addWidget(check_button_case)
check_button_subfolders = CheckButton:new{
text = _("Include subfolders"),
checked = self.include_subfolders,
parent = self.search_dialog,
parent = search_dialog,
callback = function()
self.include_subfolders = self.check_button_subfolders.checked
self.include_subfolders = check_button_subfolders.checked
end,
}
self.search_dialog:addWidget(self.check_button_subfolders)
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(self.search_dialog)
self.search_dialog:onShowKeyboard()
UIManager:show(search_dialog)
search_dialog:onShowKeyboard()
end
function FileSearcher:showSearchResults()
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 show_hidden = G_reader_settings:isTrue("show_hidden")
local show_unsupported = G_reader_settings:isTrue("show_unsupported")
local collate = G_reader_settings:readSetting("collate")
local keywords = self.search_value
if keywords ~= "*" then -- one * to show all files
if not self.case_sensitive then
keywords = Utf8Proc.lowercase(util.fixUtf8(keywords, "?"))
end
-- replace '.' with '%.'
keywords = keywords:gsub("%.","%%%.")
-- replace '*' with '.*'
keywords = keywords:gsub("%*","%.%*")
-- replace '?' with '.'
keywords = keywords: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 (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, keywords) 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 (show_unsupported or DocumentRegistry:hasProvider(fullpath))
and FileChooser:show_file(f) then
if self:isFileMatch(f, fullpath, keywords, 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, keywords, is_file)
local metadata_keys = {
"authors",
"title",
"series",
"description",
"keywords",
"language",
}
if keywords == "*" then
return true
end
if not self.case_sensitive then
filename = Utf8Proc.lowercase(util.fixUtf8(filename, "?"))
end
if string.find(filename, keywords) 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
FileManagerBookInfo:getBookProps(fullpath, nil, true)
if next(book_props) ~= nil then
for _, key in ipairs(metadata_keys) do
local prop = book_props[key]
if prop and prop ~= "" then
if not self.case_sensitive then
prop = Utf8Proc.lowercase(util.fixUtf8(prop, "?"))
end
if key == "description" then
prop = util.htmlToPlainTextIfHtml(prop)
end
if string.find(prop, keywords) then
return true
end
end
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_value)
if self.no_metadata_count == 0 then
local InfoMessage = require("ui/widget/infomessage")
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
local ConfirmBox = require("ui/widget/confirmbox")
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{
width = Screen:getWidth() - (Size.margin.fullscreen_popout * 2),
height = Screen:getHeight() - (Size.margin.fullscreen_popout * 2),
width = Screen:getWidth(),
height = Screen:getHeight(),
is_borderless = true,
is_popout = false,
show_parent = menu_container,
onMenuSelect = self.onMenuSelect,
onMenuHold = self.onMenuHold,
handle_hold_on_hold_release = true,
_manager = self,
}
table.insert(menu_container, self.search_menu)
self.search_menu.close_callback = function()
UIManager:close(menu_container)
end
local collate = G_reader_settings:readSetting("collate") or "strcoll"
local reverse_collate = G_reader_settings:isTrue("reverse_collate")
-- If we have a FileChooser instance, use it, to be able to make use of its natsort cache
local sorting
if self.ui.file_chooser then
sorting = self.ui.file_chooser:getSortingFunction(collate, reverse_collate)
else
sorting = FileChooser:getSortingFunction(collate, reverse_collate)
end
table.sort(self.results, sorting)
self.search_menu:switchItemTable(T(_("Search results (%1)"), #self.results), self.results)
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:onMenuHold(item)
local ReaderUI = require("apps/reader/readerui")
local is_file = item.attr.mode == "file"
local fullpath = item.dir .. "/" .. item.name .. (is_file and "" or "/")
local buttons = {
{
function FileSearcher:onMenuSelect(item)
local dialog
local buttons = {}
if item.is_file then
table.insert(buttons, {
{
text = _("Cancel"),
text = _("Book information"),
callback = function()
UIManager:close(self.results_dialog)
UIManager:close(dialog)
FileManagerBookInfo:show(item.path)
end,
},
{
text = _("Show folder"),
text = _("Open"),
enabled = DocumentRegistry:hasProvider(item.path),
callback = function()
UIManager:close(self.results_dialog)
UIManager:close(dialog)
self.close_callback()
self._manager:showFolder(fullpath)
require("apps/reader/readerui"):showReader(item.path)
end,
},
})
end
table.insert(buttons, {
{
text = _("Cancel"),
callback = function()
UIManager:close(dialog)
end,
},
}
if is_file then
table.insert(buttons[1], {
text = _("Open"),
{
text = _("Show folder"),
callback = function()
UIManager:close(self.results_dialog)
UIManager:close(dialog)
self.close_callback()
ReaderUI:showReader(fullpath)
self._manager:showFolder(item.path)
end,
})
end
self.results_dialog = ButtonDialogTitle:new{
title = fullpath,
},
})
dialog = ButtonDialogTitle:new{
title = item.path,
buttons = buttons,
}
UIManager:show(self.results_dialog)
UIManager:show(dialog)
end
function FileSearcher:onMenuHold(item)
if item.is_file then
if DocumentRegistry:hasProvider(item.path) then
self.close_callback()
require("apps/reader/readerui"):showReader(item.path)
end
else
self.close_callback()
self._manager:showFolder(item.path)
end
return true
end

@ -396,7 +396,7 @@ To:
UIManager:show(items)
end,
},
}
},
}
for _, widget in pairs(self.registered_widgets) do
@ -406,19 +406,48 @@ To:
end
end
self.menu_items.sort_by = self.ui:getSortingMenuTable()
self.menu_items.sort_by = self:getSortingMenuTable()
self.menu_items.reverse_sorting = {
text = _("Reverse sorting"),
checked_func = function() return self.ui.file_chooser.reverse_collate end,
callback = function() self.ui:toggleReverseCollate() end
checked_func = function()
return G_reader_settings:isTrue("reverse_collate")
end,
callback = function()
G_reader_settings:flipNilOrFalse("reverse_collate")
self.ui.file_chooser:refreshPath()
end,
}
self.menu_items.sort_mixed = {
text = _("Folders and files mixed"),
enabled_func = function()
local collate = G_reader_settings:readSetting("collate")
return collate ~= "size" and
collate ~= "type" and
collate ~= "percent_unopened_first" and
collate ~= "percent_unopened_last"
end,
checked_func = function()
local collate = G_reader_settings:readSetting("collate")
return G_reader_settings:isTrue("collate_mixed") and
collate ~= "size" and
collate ~= "type" and
collate ~= "percent_unopened_first" and
collate ~= "percent_unopened_last"
end,
callback = function()
G_reader_settings:flipNilOrFalse("collate_mixed")
self.ui.file_chooser:refreshPath()
end,
}
self.menu_items.start_with = self.ui:getStartWithMenuTable()
self.menu_items.start_with = self:getStartWithMenuTable()
if Device:supportsScreensaver() then
self.menu_items.screensaver = {
text = _("Screensaver"),
sub_item_table = require("ui/elements/screensaver_menu"),
}
end
-- insert common settings
for id, common_setting in pairs(dofile("frontend/ui/elements/common_settings_menu_table.lua")) do
self.menu_items[id] = common_setting
@ -501,7 +530,7 @@ To:
end
end,
},
}
},
}
if Device:isKobo() and not Device:isSunxi() then
table.insert(self.menu_items.developer_options.sub_item_table, {
@ -665,8 +694,8 @@ To:
G_reader_settings:flipNilOrFalse("dev_reverse_ui_text_direction")
UIManager:askForRestart()
end
}
}
},
},
})
table.insert(self.menu_items.developer_options.sub_item_table, {
text_func = function()
@ -800,6 +829,77 @@ dbg:guard(FileManagerMenu, 'setUpdateItemTable',
end
end)
function FileManagerMenu:getSortingMenuTable()
local collates = {
{ _("name"), "strcoll" },
{ _("name (natural sorting)"), "natural" },
{ _("last read date"), "access" },
{ _("date added"), "change" },
{ _("date modified"), "modification" },
{ _("size"), "size" },
{ _("type"), "type" },
{ _("percent unopened first"), "percent_unopened_first" },
{ _("percent unopened last"), "percent_unopened_last" },
}
local sub_item_table = {}
for i, v in ipairs(collates) do
table.insert(sub_item_table, {
text = v[1],
checked_func = function()
return v[2] == G_reader_settings:readSetting("collate", "strcoll")
end,
callback = function()
G_reader_settings:saveSetting("collate", v[2])
self.ui.file_chooser:refreshPath()
end,
})
end
return {
text_func = function()
local collate = G_reader_settings:readSetting("collate")
for i, v in ipairs(collates) do
if v[2] == collate then
return T(_("Sort by: %1"), v[1])
end
end
end,
sub_item_table = sub_item_table,
}
end
function FileManagerMenu:getStartWithMenuTable()
local start_withs = {
{ _("file browser"), "filemanager" },
{ _("history"), "history" },
{ _("favorites"), "favorites" },
{ _("folder shortcuts"), "folder_shortcuts" },
{ _("last file"), "last" },
}
local sub_item_table = {}
for i, v in ipairs(start_withs) do
table.insert(sub_item_table, {
text = v[1],
checked_func = function()
return v[2] == G_reader_settings:readSetting("start_with", "filemanager")
end,
callback = function()
G_reader_settings:saveSetting("start_with", v[2])
end,
})
end
return {
text_func = function()
local start_with = G_reader_settings:readSetting("start_with")
for i, v in ipairs(start_withs) do
if v[2] == start_with then
return T(_("Start with: %1"), v[1])
end
end
end,
sub_item_table = sub_item_table,
}
end
function FileManagerMenu:moveBookMetadata()
local DocSettings = require("docsettings")
local FileChooser = self.ui.file_chooser

@ -15,6 +15,7 @@ local order = {
"----------------------------",
"sort_by",
"reverse_sorting",
"sort_mixed",
"----------------------------",
"start_with",
},

@ -71,8 +71,6 @@ local FileChooser = Menu:extend{
"^%.fat32%-epoch$",
"^%.metadata%.json$",
},
collate = "strcoll",
reverse_collate = false,
path_items = nil, -- hash, store last browsed location (item index) for each path
goto_letter = true,
}
@ -97,119 +95,114 @@ function FileChooser:show_file(filename)
end
function FileChooser:init()
self.up_folder_arrow = BD.mirroredUILayout() and BD.ltr("../ ⬆") or "⬆ ../"
self.path_items = {}
self.width = Screen:getWidth()
self.list = function(path, dirs, files, count_only)
-- lfs.dir directory without permission will give error
local ok, iter, dir_obj = pcall(lfs.dir, path)
if ok then
unreadable_dir_content[path] = nil
for f in iter, dir_obj do
if self.show_hidden or not util.stringStartsWith(f, ".") then
local filename = path.."/"..f
local attributes = lfs.attributes(filename)
if attributes ~= nil then
local item = true
if attributes.mode == "directory" and f ~= "." and f ~= ".." then
if self:show_dir(f) then
if not count_only then
item = {name = f,
fullpath = filename,
attr = attributes,}
end
table.insert(dirs, item)
self.item_table = self:genItemTableFromPath(self.path)
Menu.init(self) -- call parent's init()
end
function FileChooser:getList(path, collate)
local dirs, files = {}, {}
-- lfs.dir directory without permission will give error
local ok, iter, dir_obj = pcall(lfs.dir, path)
if ok then
unreadable_dir_content[path] = nil
for f in iter, dir_obj do
if self.show_hidden or not util.stringStartsWith(f, ".") then
local filename = path.."/"..f
local attributes = lfs.attributes(filename)
if attributes ~= nil then
local item = true
if attributes.mode == "directory" and f ~= "." and f ~= ".." then
if self:show_dir(f) then
if collate then -- when collate == nil count only to display in folder mandatory
item = self:getListItem(f, filename, attributes)
end
-- Always ignore macOS resource forks.
elseif attributes.mode == "file" and not util.stringStartsWith(f, "._") then
if self:show_file(f) then
if not count_only then
local percent_finished
if self.collate == "percent_unopened_first" or self.collate == "percent_unopened_last" then
if DocSettings:hasSidecarFile(filename) then
local docinfo = DocSettings:open(filename)
percent_finished = docinfo:readSetting("percent_finished")
end
end
item = {name = f,
fullpath = filename,
attr = attributes,
suffix = util.getFileNameSuffix(f),
percent_finished = percent_finished or 0,}
end
table.insert(files, item)
table.insert(dirs, item)
end
-- Always ignore macOS resource forks.
elseif attributes.mode == "file" and not util.stringStartsWith(f, "._") then
if self:show_file(f) then
if collate then -- when collate == nil count only to display in folder mandatory
item = self:getListItem(f, filename, attributes, collate)
end
table.insert(files, item)
end
end
end
end
else -- error, probably "permission denied"
if unreadable_dir_content[path] then
-- Add this dummy item that will be replaced with a message
-- by genItemTableFromPath()
table.insert(dirs, {
name = "./.",
fullpath = path,
attr = lfs.attributes(path),
})
-- If we knew about some content (if we had come up from them
-- to this directory), have them shown
for k, v in pairs(unreadable_dir_content[path]) do
if v.attr and v.attr.mode == "directory" then
table.insert(dirs, v)
else
table.insert(files, v)
end
end
else -- error, probably "permission denied"
if unreadable_dir_content[path] then
-- Add this dummy item that will be replaced with a message by genItemTable()
table.insert(dirs, self:getListItem("./.", path, lfs.attributes(path)))
-- If we knew about some content (if we had come up from them
-- to this directory), have them shown
for k, v in pairs(unreadable_dir_content[path]) do
if v.attr and v.attr.mode == "directory" then
table.insert(dirs, v)
else
table.insert(files, v)
end
end
end
end
return dirs, files
end
self.item_table = self:genItemTableFromPath(self.path)
Menu.init(self) -- call parent's init()
function FileChooser:getListItem(f, filename, attributes, collate)
local item = {
text = f,
fullpath = filename,
attr = attributes,
}
if collate then -- file
if G_reader_settings:readSetting("show_file_in_bold") then
item.opened = DocSettings:hasSidecarFile(filename)
end
if collate == "type" then
item.suffix = util.getFileNameSuffix(f)
elseif collate == "percent_unopened_first" or collate == "percent_unopened_last" then
local percent_finished
item.opened = DocSettings:hasSidecarFile(filename)
if item.opened then
local doc_settings = DocSettings:open(filename)
percent_finished = doc_settings:readSetting("percent_finished")
end
item.percent_finished = percent_finished or 0
end
end
return item
end
function FileChooser:getSortingFunction(collate, reverse_collate)
local sorting
if collate == "strcoll" then
sorting = function(a, b)
return ffiUtil.strcoll(a.name, b.name)
return ffiUtil.strcoll(a.text, b.text)
end
elseif collate == "natural" then
local natsort
-- Only keep the cache if we're an *instance* of FileChooser
if self ~= FileChooser then
natsort, self.natsort_cache = sort.natsort_cmp(self.natsort_cache)
sorting = function(a, b)
return natsort(a.name, b.name)
end
else
natsort = sort.natsort_cmp()
sorting = function(a, b)
return natsort(a.name, b.name)
end
end
elseif self.collate == "strcoll_mixed" then
sorting = function(a, b)
if b.text == self.up_folder_arrow then return false end
return ffiUtil.strcoll(a.text, b.text)
return natsort(a.text, b.text)
end
elseif collate == "access" then
sorting = function(a, b)
return a.attr.access > b.attr.access
end
elseif collate == "modification" then
elseif collate == "change" then
sorting = function(a, b)
return a.attr.modification > b.attr.modification
return a.attr.change > b.attr.change
end
elseif collate == "change" then
elseif collate == "modification" then
sorting = function(a, b)
local a_opened = DocSettings:hasSidecarFile(a.fullpath)
local b_opened = DocSettings:hasSidecarFile(b.fullpath)
if a_opened == b_opened then
return a.attr.change > b.attr.change
end
return b_opened
return a.attr.modification > b.attr.modification
end
elseif collate == "size" then
sorting = function(a, b)
@ -220,22 +213,20 @@ function FileChooser:getSortingFunction(collate, reverse_collate)
if (a.suffix or b.suffix) and a.suffix ~= b.suffix then
return ffiUtil.strcoll(a.suffix, b.suffix)
end
return ffiUtil.strcoll(a.name, b.name)
return ffiUtil.strcoll(a.text, b.text)
end
else -- collate == "percent_unopened_first" or collate == "percent_unopened_last"
sorting = function(a, b)
local a_opened = DocSettings:hasSidecarFile(a.fullpath)
local b_opened = DocSettings:hasSidecarFile(b.fullpath)
if a_opened == b_opened then
if a_opened then
if a.opened == b.opened then
if a.opened then
return a.percent_finished < b.percent_finished
end
return a.name < b.name
return ffiUtil.strcoll(a.text, b.text)
end
if collate == "percent_unopened_first" then
return b_opened
return b.opened
end
return a_opened
return a.opened
end
end
@ -248,59 +239,47 @@ function FileChooser:getSortingFunction(collate, reverse_collate)
end
function FileChooser:genItemTableFromPath(path)
local dirs = {}
local files = {}
self.list(path, dirs, files)
local sorting = self:getSortingFunction(self.collate, self.reverse_collate)
local collate = G_reader_settings:readSetting("collate", "strcoll")
local dirs, files = self:getList(path, collate)
return self:genItemTable(dirs, files, path)
end
if self.collate ~= "strcoll_mixed" then
function FileChooser:genItemTable(dirs, files, path)
local collate = G_reader_settings:readSetting("collate")
local collate_not_for_mixed = collate == "size" or
collate == "type" or
collate == "percent_unopened_first" or
collate == "percent_unopened_last"
local collate_mixed = G_reader_settings:isTrue("collate_mixed")
local reverse_collate = G_reader_settings:isTrue("reverse_collate")
local sorting = self:getSortingFunction(collate, reverse_collate)
if collate_not_for_mixed or not collate_mixed then
table.sort(files, sorting)
if self.collate == "size" or
self.collate == "type" or
self.collate == "percent_unopened_first" or
self.collate == "percent_unopened_last" then
sorting = self:getSortingFunction("strcoll", self.reverse_collate)
if collate_not_for_mixed then
sorting = self:getSortingFunction("strcoll", reverse_collate)
end
table.sort(dirs, sorting)
end
if path ~= "/" and not (G_reader_settings:isTrue("lock_home_folder") and
path == G_reader_settings:readSetting("home_dir")) then
table.insert(dirs, 1, {name = ".."})
end
if self.show_current_dir_for_hold then
table.insert(dirs, 1, {name = "."})
end
local item_table = {}
for i, dir in ipairs(dirs) do
local subdir_path = self.path.."/"..dir.name
local text, bidi_wrap_func, istr
if dir.name == ".." then
text = self.up_folder_arrow
elseif dir.name == "." then -- possible with show_current_dir_for_hold
text = _("Long-press to choose current folder")
elseif dir.name == "./." then -- added as content of an unreadable directory
local text, bidi_wrap_func, mandatory
if dir.text == "./." then -- added as content of an unreadable directory
text = _("Current folder not readable. Some content may not be shown.")
else
text = dir.name.."/"
text = dir.text.."/"
bidi_wrap_func = BD.directory
-- count number of folders and files inside dir
local sub_dirs = {}
local dir_files = {}
self.list(subdir_path, sub_dirs, dir_files, true)
istr = T("%1 \u{F016}", #dir_files)
if #sub_dirs > 0 then
istr = T("%1 \u{F114} ", #sub_dirs) .. istr
if path then -- file browser or PathChooser
mandatory = self:getMenuItemMandatory(dir)
end
end
table.insert(item_table, {
text = text,
attr = dir.attr,
bidi_wrap_func = bidi_wrap_func,
mandatory = istr,
path = subdir_path,
is_go_up = dir.name == "..",
mandatory = mandatory,
path = dir.fullpath,
})
end
@ -310,30 +289,47 @@ function FileChooser:genItemTableFromPath(path)
local show_file_in_bold = G_reader_settings:readSetting("show_file_in_bold")
for i, file in ipairs(files) do
local full_path = self.path.."/"..file.name
local sstr = util.getFriendlySize(file.attr.size or 0)
local file_item = {
text = file.name,
text = file.text,
attr = file.attr,
bidi_wrap_func = BD.filename,
mandatory = sstr,
path = full_path,
mandatory = self:getMenuItemMandatory(file, collate),
path = file.fullpath,
is_file = true,
}
if show_file_in_bold ~= false then
file_item.bold = DocSettings:hasSidecarFile(full_path)
file_item.bold = file.opened
if show_file_in_bold ~= "opened" then
file_item.bold = not file_item.bold
end
end
if self.filemanager and self.filemanager.selected_files and self.filemanager.selected_files[full_path] then
if self.filemanager and self.filemanager.selected_files and self.filemanager.selected_files[file.fullpath] then
file_item.dim = true
end
table.insert(item_table, file_item)
end
if self.collate == "strcoll_mixed" then
if not collate_not_for_mixed and collate_mixed then
table.sort(item_table, sorting)
end
if path then -- file browser or PathChooser
if path ~= "/" and not (G_reader_settings:isTrue("lock_home_folder") and
path == G_reader_settings:readSetting("home_dir")) then
table.insert(item_table, 1, {
text = BD.mirroredUILayout() and BD.ltr("../ ⬆") or "⬆ ../",
path = path.."/..",
is_go_up = true,
})
end
if self.show_current_dir_for_hold then
table.insert(item_table, 1, {
text = _("Long-press to choose current folder"),
path = path.."/.",
})
end
end
-- lfs.dir iterated node string may be encoded with some weird codepage on
-- Windows we need to encode them to utf-8
if ffi.os == "Windows" then
@ -347,6 +343,31 @@ function FileChooser:genItemTableFromPath(path)
return item_table
end
function FileChooser:getMenuItemMandatory(item, collate)
local text
if collate then -- file
-- display the sorting parameter in mandatory
if collate == "access" then
text = os.date("%Y-%m-%d %H:%M", item.attr.access)
elseif collate == "change" then
text = os.date("%Y-%m-%d %H:%M", item.attr.change)
elseif collate == "modification" then
text = os.date("%Y-%m-%d %H:%M", item.attr.modification)
elseif collate == "percent_unopened_first" or collate == "percent_unopened_last" then
text = item.opened and string.format("%d %%", 100 * item.percent_finished) or ""
else
text = util.getFriendlySize(item.attr.size or 0)
end
else -- folder, count number of folders and files inside it
local sub_dirs, dir_files = self:getList(item.fullpath)
text = T("%1 \u{F016}", #dir_files)
if #sub_dirs > 0 then
text = T("%1 \u{F114} ", #sub_dirs) .. text
end
end
return text
end
function FileChooser:updateItems(select_number)
Menu.updateItems(self, select_number) -- call parent's updateItems()
self:mergeTitleBarIntoLayout()
@ -444,16 +465,6 @@ function FileChooser:toggleUnsupportedFiles()
self:refreshPath()
end
function FileChooser:setCollate(collate)
self.collate = collate
self:refreshPath()
end
function FileChooser:toggleReverseCollate()
self.reverse_collate = not self.reverse_collate
self:refreshPath()
end
function FileChooser:onMenuSelect(item)
-- parent directory of dir without permission get nil mode
-- we need to change to parent path in this case
@ -542,16 +553,14 @@ function FileChooser:showSetProviderButtons(file, one_time_providers)
},
})
end
if one_time_providers and #one_time_providers > 0 then
for ___, provider in ipairs(one_time_providers) do
provider.one_time_provider = true
table.insert(radio_buttons, {
{
text = provider.provider_name,
provider = provider,
},
})
end
for _, provider in ipairs(one_time_providers) do
provider.one_time_provider = true
table.insert(radio_buttons, {
{
text = provider.provider_name,
provider = provider,
},
})
end
table.insert(buttons, {

@ -131,7 +131,7 @@ function MenuItem:init()
},
HoldSelect = {
GestureRange:new{
ges = "hold",
ges = self.handle_hold_on_hold_release and "hold_release" or "hold",
range = self.dimen,
},
},
@ -1067,6 +1067,7 @@ function Menu:updateItems(select_number)
with_dots = self.with_dots,
line_color = self.line_color,
items_padding = self.items_padding,
handle_hold_on_hold_release = self.handle_hold_on_hold_release,
}
table.insert(self.item_group, item_tmp)
-- this is for focus manager

@ -668,4 +668,15 @@ function CoverBrowser:setupCollectionDisplayMode(display_mode)
end
end
function CoverBrowser:getBookInfo(file)
return BookInfoManager:getBookInfo(file)
end
function CoverBrowser:extractBooksInDirectory(path)
local Trapper = require("ui/trapper")
Trapper:wrap(function()
BookInfoManager:extractBooksInDirectory(path)
end)
end
return CoverBrowser

Loading…
Cancel
Save