diff --git a/frontend/apps/filemanager/filemanager.lua b/frontend/apps/filemanager/filemanager.lua index 7acfc1322..65e46a16c 100644 --- a/frontend/apps/filemanager/filemanager.lua +++ b/frontend/apps/filemanager/filemanager.lua @@ -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... diff --git a/frontend/apps/filemanager/filemanagerbookinfo.lua b/frontend/apps/filemanager/filemanagerbookinfo.lua index 255e44fbb..46d972a82 100644 --- a/frontend/apps/filemanager/filemanagerbookinfo.lua +++ b/frontend/apps/filemanager/filemanagerbookinfo.lua @@ -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 diff --git a/frontend/apps/filemanager/filemanagerfilesearcher.lua b/frontend/apps/filemanager/filemanagerfilesearcher.lua index 3c143e6b8..f7d2153e9 100644 --- a/frontend/apps/filemanager/filemanagerfilesearcher.lua +++ b/frontend/apps/filemanager/filemanagerfilesearcher.lua @@ -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 diff --git a/frontend/apps/filemanager/filemanagermenu.lua b/frontend/apps/filemanager/filemanagermenu.lua index 70a23b6d4..9380aec6d 100644 --- a/frontend/apps/filemanager/filemanagermenu.lua +++ b/frontend/apps/filemanager/filemanagermenu.lua @@ -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 diff --git a/frontend/ui/elements/filemanager_menu_order.lua b/frontend/ui/elements/filemanager_menu_order.lua index f219eda4b..6e170ac8f 100644 --- a/frontend/ui/elements/filemanager_menu_order.lua +++ b/frontend/ui/elements/filemanager_menu_order.lua @@ -15,6 +15,7 @@ local order = { "----------------------------", "sort_by", "reverse_sorting", + "sort_mixed", "----------------------------", "start_with", }, diff --git a/frontend/ui/widget/filechooser.lua b/frontend/ui/widget/filechooser.lua index c07d96683..8277c8a74 100644 --- a/frontend/ui/widget/filechooser.lua +++ b/frontend/ui/widget/filechooser.lua @@ -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, { diff --git a/frontend/ui/widget/menu.lua b/frontend/ui/widget/menu.lua index 417fbe5e9..b8722c6f4 100644 --- a/frontend/ui/widget/menu.lua +++ b/frontend/ui/widget/menu.lua @@ -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 diff --git a/plugins/coverbrowser.koplugin/main.lua b/plugins/coverbrowser.koplugin/main.lua index 554fc9584..8a1e7202d 100644 --- a/plugins/coverbrowser.koplugin/main.lua +++ b/plugins/coverbrowser.koplugin/main.lua @@ -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