File browser: sort by metadata (#13437)
Some checks are pending
macos / macOS 13 x86-64 🔨15.2 🎯10.15 (push) Waiting to run
macos / macOS 14 ARM64 🔨15.4 🎯11.0 (push) Waiting to run

This commit is contained in:
hius07
2025-03-24 19:12:46 +02:00
committed by GitHub
parent a852d535d1
commit 93ee0a1415
11 changed files with 269 additions and 272 deletions

View File

@@ -138,6 +138,7 @@ function FileManager:setupLayout()
}
local file_chooser = FileChooser:new{
name = "filemanager",
path = self.root_path,
focused_path = self.focused_file,
show_parent = self.show_parent,
@@ -146,7 +147,7 @@ function FileManager:setupLayout()
-- allow left bottom tap gesture, otherwise it is eaten by hidden return button
return_arrow_propagation = true,
-- allow Menu widget to delegate handling of some gestures to GestureManager
filemanager = self,
ui = self,
-- Tell FileChooser (i.e., Menu) to use our own title bar instead of Menu's default one
custom_title_bar = self.title_bar,
search_callback = function(search_string)
@@ -344,10 +345,6 @@ function FileManager:setupLayout()
self[1] = fm_ui
self.menu = FileManagerMenu:new{
ui = self
}
-- No need to reinvent the wheel, use FileChooser's layout
self.layout = file_chooser.layout
@@ -390,15 +387,13 @@ end
-- NOTE: The only thing that will *ever* instantiate a new FileManager object is our very own showFiles below!
function FileManager:init()
self:setupLayout()
self.active_widgets = {}
self:registerModule("screenshot", Screenshoter:new{
prefix = "FileManager",
ui = self,
}, true)
self:registerModule("menu", self.menu)
self:registerModule("menu", FileManagerMenu:new{ ui = self })
self:registerModule("history", FileManagerHistory:new{ ui = self })
self:registerModule("bookinfo", FileManagerBookInfo:new{ ui = self })
self:registerModule("collections", FileManagerCollection:new{ ui = self })
@@ -425,6 +420,7 @@ function FileManager:init()
end
end
self:setupLayout()
self:initGesListener()
self:handleEvent(Event:new("SetDimensions", self.dimen))
self:handleEvent(Event:new("PathChanged", self.file_chooser.path))

View File

@@ -284,7 +284,7 @@ function BookInfo:getDocProps(file, book_props, no_open_document)
-- If still no book_props, open the document to get them
if not book_props and not no_open_document then
local document = DocumentRegistry:openDocument(file)
local document = DocumentRegistry:hasProvider(file) and DocumentRegistry:openDocument(file)
if document then
local loaded = true
local pages

View File

@@ -49,13 +49,6 @@ function FileManagerCollection:addToMainMenu(menu_items)
}
end
function FileManagerCollection:getDocProps(file)
if self.doc_props_cache[file] == nil then
self.doc_props_cache[file] = self.ui.bookinfo:getDocProps(file, nil, true) -- do not open the document
end
return self.doc_props_cache[file]
end
-- collection
function FileManagerCollection:getCollectionTitle(collection_name)
@@ -122,7 +115,7 @@ function FileManagerCollection:updateItemTable(item_table, focused_file)
mandatory = self.mandatory_func and self.mandatory_func(item) or util.getFriendlySize(item.attr.size or 0),
}
if self.item_func then
self.item_func(item_tmp, self:getDocProps(item_tmp.file))
self.item_func(item_tmp, self.ui)
end
table.insert(item_table, item_tmp)
end
@@ -143,7 +136,7 @@ function FileManagerCollection:isItemMatch(item)
end
end
if self.match_table.props then
local doc_props = self:getDocProps(item.file)
local doc_props = self.ui.bookinfo:getDocProps(item.file, nil, true)
for prop, value in pairs(self.match_table.props) do
if (doc_props[prop] or self.empty_prop) ~= value then
return false
@@ -317,7 +310,7 @@ function FileManagerCollection:showCollDialog()
UIManager:close(coll_dialog)
local prop_values = {}
for idx, item in ipairs(self.booklist_menu.item_table) do
local doc_prop = self:getDocProps(item.file)[button_prop]
local doc_prop = self.ui.bookinfo:getDocProps(item.file, nil, true)[button_prop]
if doc_prop == nil then
doc_prop = { self.empty_prop }
elseif button_prop == "series" then
@@ -496,7 +489,7 @@ function FileManagerCollection:setCollate(collate_id, collate_reverse)
coll_settings.collate_reverse = collate_reverse or nil
end
if collate_id then
local collate = BookList.metadata_collates[collate_id] or BookList.collates[collate_id]
local collate = BookList.collates[collate_id]
self.item_func = collate.item_func
self.mandatory_func = collate.mandatory_func
self.sorting_func, self.sort_cache = collate.init_sort_func(self.sort_cache)
@@ -517,7 +510,7 @@ function FileManagerCollection:showArrangeBooksDialog()
local curr_collate_id = coll_settings.collate
local arrange_dialog
local function genCollateButton(collate_id)
local collate = BookList.metadata_collates[collate_id] or BookList.collates[collate_id]
local collate = BookList.collates[collate_id]
return {
text = collate.text .. (curr_collate_id == collate_id and "" or ""),
callback = function()
@@ -1135,7 +1128,7 @@ function FileManagerCollection:searchCollections(coll_name)
if not DocumentRegistry:hasProvider(file) then
return false
end
local book_props = self:getDocProps(file)
local book_props = self.ui.bookinfo:getDocProps(file, nil, true)
if next(book_props) ~= nil and self.ui.bookinfo:findInProps(book_props, self.search_str, self.case_sensitive) then
return true
end

View File

@@ -151,17 +151,17 @@ function FileSearcher:doSearch()
FileSearcher.search_hash = search_hash
self.no_metadata_count = no_metadata_count
-- Cannot do this in getList() within Trapper (cannot serialize function)
local collate = FileChooser:getCollate()
local fc = self.ui.file_chooser or FileChooser:new{ ui = self.ui }
local collate = fc:getCollate()
for i, v in ipairs(dirs) do
local f, fullpath, attributes = unpack(v)
dirs[i] = FileChooser:getListItem(nil, f, fullpath, attributes, collate)
dirs[i] = fc:getListItem(nil, f, fullpath, attributes, collate)
end
for i, v in ipairs(files) do
local f, fullpath, attributes = unpack(v)
files[i] = FileChooser:getListItem(nil, f, fullpath, attributes, collate)
files[i] = fc:getListItem(nil, f, fullpath, attributes, collate)
end
-- If we have a FileChooser instance, use it, to be able to make use of its natsort cache
FileSearcher.search_results = (self.ui.file_chooser or FileChooser):genItemTable(dirs, files)
FileSearcher.search_results = fc:genItemTable(dirs, files)
end
if #FileSearcher.search_results > 0 then
self:onShowSearchResults(not_cached)

View File

@@ -205,12 +205,14 @@ function FileManagerMenu:setUpdateItemTable()
},
{
text_func = function()
return T(_("Item font size: %1"), FileChooser.font_size)
end,
callback = function(touchmenu_instance)
local current_value = FileChooser.font_size
local default_value = FileChooser.getItemFontSize(G_reader_settings:readSetting("items_per_page")
or FileChooser.items_per_page_default)
return T(_("Item font size: %1"), FileChooser.font_size or default_value)
end,
callback = function(touchmenu_instance)
local default_value = FileChooser.getItemFontSize(G_reader_settings:readSetting("items_per_page")
or FileChooser.items_per_page_default)
local current_value = FileChooser.font_size or default_value
local widget = SpinWidget:new{
title_text = _("Item font size"),
value = current_value,
@@ -883,7 +885,9 @@ dbg:guard(FileManagerMenu, 'setUpdateItemTable',
end)
function FileManagerMenu:getSortingMenuTable()
local sub_item_table = {}
local sub_item_table = {
max_per_page = 9, -- metadata collates in page 2
}
for k, v in pairs(self.ui.file_chooser.collates) do
table.insert(sub_item_table, {
text = v.text,
@@ -893,9 +897,7 @@ function FileManagerMenu:getSortingMenuTable()
return k == id
end,
callback = function()
G_reader_settings:saveSetting("collate", k)
self.ui.file_chooser:clearSortingCache()
self.ui.file_chooser:refreshPath()
self.ui:onSetSortBy(k)
end,
})
end

View File

@@ -13,231 +13,241 @@ local BookList = Menu:extend{
is_borderless = true,
is_popout = false,
book_info_cache = {}, -- cache in the base class
metadata_collates = {
title = {
text = _("Title"),
item_func = function(item, doc_props)
item.doc_props = doc_props
end,
init_sort_func = function()
return function(a, b)
return ffiUtil.strcoll(a.doc_props.display_title, b.doc_props.display_title)
}
BookList.collates = {
strcoll = {
text = _("name"),
menu_order = 10,
can_collate_mixed = true,
init_sort_func = function()
return function(a, b)
return ffiUtil.strcoll(a.text, b.text)
end
end,
},
natural = {
text = _("name (natural sorting)"),
menu_order = 20,
can_collate_mixed = true,
init_sort_func = function(cache)
local natsort
natsort, cache = sort.natsort_cmp(cache)
return function(a, b)
return natsort(a.text, b.text)
end, cache
end,
},
access = {
text = _("last read date"),
menu_order = 30,
can_collate_mixed = true,
init_sort_func = function()
return function(a, b)
return a.attr.access > b.attr.access
end
end,
mandatory_func = function(item)
return datetime.secondsToDateTime(item.attr.access)
end,
},
date = {
text = _("date modified"),
menu_order = 40,
can_collate_mixed = true,
init_sort_func = function()
return function(a, b)
return a.attr.modification > b.attr.modification
end
end,
mandatory_func = function(item)
return datetime.secondsToDateTime(item.attr.modification)
end,
},
size = {
text = _("size"),
menu_order = 50,
can_collate_mixed = false,
init_sort_func = function()
return function(a, b)
return a.attr.size < b.attr.size
end
end,
},
type = {
text = _("type"),
menu_order = 60,
can_collate_mixed = false,
init_sort_func = function()
return function(a, b)
if (a.suffix or b.suffix) and a.suffix ~= b.suffix then
return ffiUtil.strcoll(a.suffix, b.suffix)
end
end,
},
authors = {
text = _("Authors"),
item_func = function(item, doc_props)
doc_props.authors = doc_props.authors or "\u{FFFF}" -- sorted last
item.doc_props = doc_props
end,
init_sort_func = function()
return function(a, b)
if a.doc_props.authors ~= b.doc_props.authors then
return ffiUtil.strcoll(a.doc_props.authors, b.doc_props.authors)
return ffiUtil.strcoll(a.text, b.text)
end
end,
item_func = function(item)
item.suffix = util.getFileNameSuffix(item.text)
end,
},
percent_unopened_first = {
text = _("percent - unopened first"),
menu_order = 70,
can_collate_mixed = false,
init_sort_func = function()
return function(a, b)
if a.opened == b.opened then
if a.opened then
return a.percent_finished < b.percent_finished
end
return ffiUtil.strcoll(a.doc_props.display_title, b.doc_props.display_title)
return ffiUtil.strcoll(a.text, b.text)
end
end,
},
series = {
text = _("Series"),
item_func = function(item, doc_props)
doc_props.series = doc_props.series or "\u{FFFF}"
item.doc_props = doc_props
end,
init_sort_func = function()
return function(a, b)
if a.doc_props.series ~= b.doc_props.series then
return ffiUtil.strcoll(a.doc_props.series, b.doc_props.series)
return b.opened
end
end,
item_func = function(item)
local book_info = BookList.getBookInfo(item.path)
item.opened = book_info.been_opened
-- smooth 2 decimal points (0.00) instead of 16 decimal points
item.percent_finished = util.round_decimal(book_info.percent_finished or 0, 2)
end,
mandatory_func = function(item)
return item.opened and string.format("%d\u{202F}%%", 100 * item.percent_finished) or ""
end,
},
percent_unopened_last = {
text = _("percent - unopened last"),
menu_order = 80,
can_collate_mixed = false,
init_sort_func = function()
return function(a, b)
if a.opened == b.opened then
if a.opened then
return a.percent_finished < b.percent_finished
end
if a.doc_props.series_index and b.doc_props.series_index then
return a.doc_props.series_index < b.doc_props.series_index
end
return ffiUtil.strcoll(a.doc_props.display_title, b.doc_props.display_title)
return ffiUtil.strcoll(a.text, b.text)
end
end,
},
keywords = {
text = _("Keywords"),
item_func = function(item, doc_props)
doc_props.keywords = doc_props.keywords or "\u{FFFF}"
item.doc_props = doc_props
end,
init_sort_func = function()
return function(a, b)
return a.opened
end
end,
item_func = function(item)
local book_info = BookList.getBookInfo(item.path)
item.opened = book_info.been_opened
-- smooth 2 decimal points (0.00) instead of 16 decimal points
item.percent_finished = util.round_decimal(book_info.percent_finished or 0, 2)
end,
mandatory_func = function(item)
return item.opened and string.format("%d\u{202F}%%", 100 * item.percent_finished) or ""
end,
},
percent_natural = {
-- sort 90% > 50% > 0% > on hold > unopened > 100% or finished
text = _("percent unopened finished last"),
menu_order = 90,
can_collate_mixed = false,
init_sort_func = function(cache)
local natsort
natsort, cache = sort.natsort_cmp(cache)
local sortfunc = function(a, b)
if a.sort_percent == b.sort_percent then
return natsort(a.text, b.text)
elseif a.sort_percent == 1 then
return false
elseif b.sort_percent == 1 then
return true
else
return a.sort_percent > b.sort_percent
end
end
return sortfunc, cache
end,
item_func = function(item)
local book_info = BookList.getBookInfo(item.path)
item.opened = book_info.been_opened
local percent_finished = book_info.percent_finished
local sort_percent
if item.opened then
-- books marked as "finished" or "on hold" should be considered the same as 100% and less than 0% respectively
if book_info.status == "complete" then
sort_percent = 1.0
elseif book_info.status == "abandoned" then
sort_percent = -0.01
end
end
-- smooth 2 decimal points (0.00) instead of 16 decimal points
item.sort_percent = sort_percent or util.round_decimal(percent_finished or -1, 2)
item.percent_finished = percent_finished or 0
end,
mandatory_func = function(item)
return item.opened and string.format("%d\u{202F}%%", 100 * item.percent_finished) or ""
end,
},
title = {
text = _("Title"),
menu_order = 100,
item_func = function(item, ui)
local doc_props = ui.bookinfo:getDocProps(item.path or item.file)
item.doc_props = doc_props
end,
init_sort_func = function()
return function(a, b)
return ffiUtil.strcoll(a.doc_props.display_title, b.doc_props.display_title)
end
end,
},
authors = {
text = _("Authors"),
menu_order = 110,
item_func = function(item, ui)
local doc_props = ui.bookinfo:getDocProps(item.path or item.file)
doc_props.authors = doc_props.authors or "\u{FFFF}" -- sorted last
item.doc_props = doc_props
end,
init_sort_func = function()
return function(a, b)
if a.doc_props.authors ~= b.doc_props.authors then
return ffiUtil.strcoll(a.doc_props.authors, b.doc_props.authors)
end
return ffiUtil.strcoll(a.doc_props.display_title, b.doc_props.display_title)
end
end,
},
series = {
text = _("Series"),
menu_order = 120,
item_func = function(item, ui)
local doc_props = ui.bookinfo:getDocProps(item.path or item.file)
doc_props.series = doc_props.series or "\u{FFFF}"
item.doc_props = doc_props
end,
init_sort_func = function()
return function(a, b)
if a.doc_props.series ~= b.doc_props.series then
return ffiUtil.strcoll(a.doc_props.series, b.doc_props.series)
end
if a.doc_props.series_index and b.doc_props.series_index then
return a.doc_props.series_index < b.doc_props.series_index
end
return ffiUtil.strcoll(a.doc_props.display_title, b.doc_props.display_title)
end
end,
},
keywords = {
text = _("Keywords"),
menu_order = 130,
item_func = function(item, ui)
local doc_props = ui.bookinfo:getDocProps(item.path or item.file)
doc_props.keywords = doc_props.keywords or "\u{FFFF}"
item.doc_props = doc_props
end,
init_sort_func = function()
return function(a, b)
if a.doc_props.keywords ~= b.doc_props.keywords then
return ffiUtil.strcoll(a.doc_props.keywords, b.doc_props.keywords)
end
end,
},
},
collates = {
strcoll = {
text = _("name"),
menu_order = 10,
can_collate_mixed = true,
init_sort_func = function()
return function(a, b)
return ffiUtil.strcoll(a.text, b.text)
end
end,
},
natural = {
text = _("name (natural sorting)"),
menu_order = 20,
can_collate_mixed = true,
init_sort_func = function(cache)
local natsort
natsort, cache = sort.natsort_cmp(cache)
return function(a, b)
return natsort(a.text, b.text)
end, cache
end,
},
access = {
text = _("last read date"),
menu_order = 30,
can_collate_mixed = true,
init_sort_func = function()
return function(a, b)
return a.attr.access > b.attr.access
end
end,
mandatory_func = function(item)
return datetime.secondsToDateTime(item.attr.access)
end,
},
date = {
text = _("date modified"),
menu_order = 40,
can_collate_mixed = true,
init_sort_func = function()
return function(a, b)
return a.attr.modification > b.attr.modification
end
end,
mandatory_func = function(item)
return datetime.secondsToDateTime(item.attr.modification)
end,
},
size = {
text = _("size"),
menu_order = 50,
can_collate_mixed = false,
init_sort_func = function()
return function(a, b)
return a.attr.size < b.attr.size
end
end,
},
type = {
text = _("type"),
menu_order = 60,
can_collate_mixed = false,
init_sort_func = function()
return function(a, b)
if (a.suffix or b.suffix) and a.suffix ~= b.suffix then
return ffiUtil.strcoll(a.suffix, b.suffix)
end
return ffiUtil.strcoll(a.text, b.text)
end
end,
item_func = function(item)
item.suffix = util.getFileNameSuffix(item.text)
end,
},
percent_unopened_first = {
text = _("percent - unopened first"),
bookinfo_required = true,
menu_order = 70,
can_collate_mixed = false,
init_sort_func = function()
return function(a, b)
if a.opened == b.opened then
if a.opened then
return a.percent_finished < b.percent_finished
end
return ffiUtil.strcoll(a.text, b.text)
end
return b.opened
end
end,
item_func = function(item, book_info)
item.opened = book_info.been_opened
-- smooth 2 decimal points (0.00) instead of 16 decimal points
item.percent_finished = util.round_decimal(book_info.percent_finished or 0, 2)
end,
mandatory_func = function(item)
return item.opened and string.format("%d\u{202F}%%", 100 * item.percent_finished) or ""
end,
},
percent_unopened_last = {
text = _("percent - unopened last"),
bookinfo_required = true,
menu_order = 80,
can_collate_mixed = false,
init_sort_func = function()
return function(a, b)
if a.opened == b.opened then
if a.opened then
return a.percent_finished < b.percent_finished
end
return ffiUtil.strcoll(a.text, b.text)
end
return a.opened
end
end,
item_func = function(item, book_info)
item.opened = book_info.been_opened
-- smooth 2 decimal points (0.00) instead of 16 decimal points
item.percent_finished = util.round_decimal(book_info.percent_finished or 0, 2)
end,
mandatory_func = function(item)
return item.opened and string.format("%d\u{202F}%%", 100 * item.percent_finished) or ""
end,
},
percent_natural = {
-- sort 90% > 50% > 0% > on hold > unopened > 100% or finished
text = _("percent unopened finished last"),
bookinfo_required = true,
menu_order = 90,
can_collate_mixed = false,
init_sort_func = function(cache)
local natsort
natsort, cache = sort.natsort_cmp(cache)
local sortfunc = function(a, b)
if a.sort_percent == b.sort_percent then
return natsort(a.text, b.text)
elseif a.sort_percent == 1 then
return false
elseif b.sort_percent == 1 then
return true
else
return a.sort_percent > b.sort_percent
end
end
return sortfunc, cache
end,
item_func = function(item, book_info)
item.opened = book_info.been_opened
local percent_finished = book_info.percent_finished
local sort_percent
if item.opened then
-- books marked as "finished" or "on hold" should be considered the same as 100% and less than 0% respectively
if book_info.status == "complete" then
sort_percent = 1.0
elseif book_info.status == "abandoned" then
sort_percent = -0.01
end
end
-- smooth 2 decimal points (0.00) instead of 16 decimal points
item.sort_percent = sort_percent or util.round_decimal(percent_finished or -1, 2)
item.percent_finished = percent_finished or 0
end,
mandatory_func = function(item)
return item.opened and string.format("%d\u{202F}%%", 100 * item.percent_finished) or ""
end,
},
return ffiUtil.strcoll(a.doc_props.display_title, b.doc_props.display_title)
end
end,
},
}

View File

@@ -164,8 +164,7 @@ function FileChooser:getListItem(dirpath, f, fullpath, attributes, collate)
item.bidi_wrap_func = BD.filename
item.is_file = true
if collate.item_func ~= nil then
local book_info = collate.bookinfo_required and BookList.getBookInfo(item.path)
collate.item_func(item, book_info)
collate.item_func(item, self.ui)
end
if show_file_in_bold ~= false then
if item.opened == nil then -- could be set in item_func
@@ -176,8 +175,7 @@ function FileChooser:getListItem(dirpath, f, fullpath, attributes, collate)
item.bold = not item.bold
end
end
item.dim = self.filemanager and self.filemanager.selected_files
and self.filemanager.selected_files[item.path]
item.dim = self.ui and self.ui.selected_files and self.ui.selected_files[item.path]
item.mandatory = self:getMenuItemMandatory(item, collate)
else -- folder
if item.text == "./." then -- added as content of an unreadable directory
@@ -186,7 +184,7 @@ function FileChooser:getListItem(dirpath, f, fullpath, attributes, collate)
item.text = item.text.."/"
item.bidi_wrap_func = BD.directory
item.is_file = false
if collate.can_collate_mixed and collate.item_func ~= nil then
if collate.can_collate_mixed and collate.item_func ~= nil then -- used by user plugin/patch, don't remove
collate.item_func(item)
end
if dirpath then -- file browser or PathChooser
@@ -328,7 +326,7 @@ function FileChooser:refreshPath()
self.prev_focused_path = self.focused_path
self.focused_path = nil
end
local subtitle = self.filemanager == nil and BD.directory(filemanagerutil.abbreviate(self.path))
local subtitle = self.name ~= "filemanager" and BD.directory(filemanagerutil.abbreviate(self.path)) -- PathChooser
self:switchItemTable(nil, self:genItemTableFromPath(self.path), self.path_items[self.path], itemmatch, subtitle)
end
@@ -354,8 +352,8 @@ function FileChooser:changeToPath(path, focused_path)
end
self:refreshPath()
if self.filemanager then
self.filemanager:handleEvent(Event:new("PathChanged", path))
if self.name == "filemanager" then
self.ui:handleEvent(Event:new("PathChanged", path))
end
end
@@ -464,7 +462,7 @@ function FileChooser:selectAllFilesInFolder(do_select)
for _, item in ipairs(self.item_table) do
if item.is_file then
if do_select then
self.filemanager.selected_files[item.path] = true
self.ui.selected_files[item.path] = true
item.dim = true
else
item.dim = nil

View File

@@ -920,7 +920,7 @@ function Menu:init()
}
end
-- delegate swipe gesture to GestureManager in filemanager
if not self.filemanager then
if self.name ~= "filemanager" then
self.ges_events.Swipe = {
GestureRange:new{
ges = "swipe",

View File

@@ -22,6 +22,11 @@ local PathChooser = FileChooser:extend{
}
function PathChooser:init()
local collate = G_reader_settings:readSetting("collate")
if self.show_files and (collate == "title" or collate == "authors" or collate == "series" or collate == "keywords") then
self.ui = require("apps/reader/readerui").instance or require("apps/filemanager/filemanager").instance
end
if self.title == true then -- default title depending on options
if self.select_directory and not self.select_file then
self.title = _("Long-press folder's name to choose it")

View File

@@ -74,7 +74,7 @@ local CoverBrowser = WidgetContainer:extend{
}
function CoverBrowser:init()
if self.ui.file_chooser then -- FileManager menu only
if not self.ui.document then -- FileManager menu only
self.ui.menu:registerToMainMenu(self)
end
@@ -675,13 +675,6 @@ function CoverBrowser:setupFileManagerDisplayMode(display_mode)
if init_done then
self:refreshFileManagerInstance()
else
-- If KOReader has started directly to FileManager, the FileManager
-- instance is being init()'ed and there is no FileManager.instance yet,
-- but there'll be one at next tick.
UIManager:nextTick(function()
self:refreshFileManagerInstance()
end)
end
end

View File

@@ -62,7 +62,7 @@ function OPDS:onDispatcherRegisterActions()
end
function OPDS:addToMainMenu(menu_items)
if self.ui.file_chooser then
if not self.ui.document then -- FileManager menu only
menu_items.opds = {
text = _("OPDS catalog"),
callback = function()