diff --git a/frontend/apps/filemanager/filemanager.lua b/frontend/apps/filemanager/filemanager.lua index e5eb88f91..797955ccb 100644 --- a/frontend/apps/filemanager/filemanager.lua +++ b/frontend/apps/filemanager/filemanager.lua @@ -166,8 +166,7 @@ function FileManager:setupLayout() end self:refreshPath() else - local ReaderUI = require("apps/reader/readerui") - ReaderUI:showReader(file) + file_manager:openFile(file) end return true end @@ -268,43 +267,7 @@ function FileManager:setupLayout() text = _("Open with…"), callback = function() UIManager:close(self.file_dialog) - local one_time_providers = {} - if DocumentRegistry:isImageFile(file) then - table.insert(one_time_providers, { - provider_name = _("Image viewer"), - callback = function() - local ImageViewer = require("ui/widget/imageviewer") - UIManager:show(ImageViewer:new{ - file = file, - fullscreen = true, - with_title_bar = false, - }) - end, - }) - end - table.insert(one_time_providers, { - provider_name = _("Text viewer"), - callback = function() - file_manager:openTextViewer(file) - end, - }) - if file_manager.texteditor then - table.insert(one_time_providers, { - provider_name = _("Text editor"), - callback = function() - file_manager.texteditor:checkEditFile(file) - end, - }) - end - if file_manager.archiveviewer and file_manager.archiveviewer:isSupported(file) then - table.insert(one_time_providers, { - provider_name = _("Archive viewer"), - callback = function() - file_manager.archiveviewer:openArchiveViewer(file) - end, - }) - end - self:showSetProviderButtons(file, one_time_providers) + file_manager:showOpenWithDialog(file) end, }, filemanagerutil.genBookInformationButton(file, close_dialog_callback), @@ -410,7 +373,7 @@ function FileManager:init() self.active_widgets = {} self:registerModule("screenshot", Screenshoter:new{ - prefix = 'FileManager', + prefix = "FileManager", ui = self, }, true) @@ -838,7 +801,10 @@ function FileManager:setHome(path) end function FileManager:openRandomFile(dir) - local random_file = DocumentRegistry:getRandomFile(dir, false) + local match_func = function(file) -- documents, not yet opened + return DocumentRegistry:hasProvider(file) and not DocSettings:hasSidecarFile(file) + end + local random_file = filemanagerutil.getRandomFile(dir, match_func) if random_file then UIManager:show(MultiConfirmBox:new{ text = T(_("Do you want to open %1?"), BD.filename(BaseUtil.basename(random_file))), @@ -1139,36 +1105,6 @@ function FileManager:showFiles(path, focused_file) UIManager:show(file_manager) end -function FileManager:openTextViewer(file_path) - local function _openTextViewer(filepath) - local file = io.open(filepath, "rb") - if not file then return end - local file_content = file:read("*all") - file:close() - UIManager:show(require("ui/widget/textviewer"):new{ - title = filepath, - title_multilines = true, - justified = false, - text = file_content, - }) - end - local attr = lfs.attributes(file_path) - if attr then - if attr.size > 400000 then - UIManager:show(ConfirmBox:new{ - text = T(_("This file is %2:\n\n%1\n\nAre you sure you want to open it?\n\nOpening big files may take some time."), - BD.filepath(file_path), util.getFriendlySize(attr.size)), - ok_text = _("Open"), - ok_callback = function() - _openTextViewer(file_path) - end, - }) - else - _openTextViewer(file_path) - end - end -end - --- A shortcut to execute mv. -- @treturn boolean result of mv command function FileManager:moveFile(from, to) @@ -1304,4 +1240,168 @@ function FileManager:showSelectedFilesList() UIManager:show(menu_container) end +function FileManager:showOpenWithDialog(file) + local file_associated_provider_key = DocumentRegistry:getAssociatedProviderKey(file, false) + local type_associated_provider_key = DocumentRegistry:getAssociatedProviderKey(file, true) + local file_provider_key = file_associated_provider_key + or type_associated_provider_key + or DocumentRegistry:getProvider(file).provider + + -- radio buttons (all providers) + local function genRadioButton(provider, is_unsupported) + return {{ + -- @translators %1 is the provider name, such as Cool Reader Engine or MuPDF. + text = is_unsupported and T(_("%1 ~Unsupported"), provider.provider_name) or provider.provider_name, + checked = provider.provider == file_provider_key, + provider = provider, + }} + end + local radio_buttons = {} + local providers = DocumentRegistry:getProviders(file) -- document providers + if providers then + for _, provider in ipairs(providers) do + table.insert(radio_buttons, genRadioButton(provider.provider)) + end + else + local provider = DocumentRegistry:getFallbackProvider() + table.insert(radio_buttons, genRadioButton(provider, true)) + end + for _, provider in ipairs(DocumentRegistry:getAuxProviders()) do -- auxiliary providers + local is_filetype_supported + if provider.enabled_func then -- module + is_filetype_supported = provider.enabled_func(file) + else -- plugin + is_filetype_supported = self[provider.provider]:isFileTypeSupported(file) + end + if is_filetype_supported then + table.insert(radio_buttons, genRadioButton(provider)) + end + end + + -- buttons + local __, filename_pure = util.splitFilePathName(file) + filename_pure = BD.filename(filename_pure) + local filename_suffix = util.getFileNameSuffix(file):lower() + local dialog + local buttons = {} + -- row: wide button + if file_associated_provider_key then + table.insert(buttons, {{ + text = _("Reset default for this file"), + callback = function() + DocumentRegistry:setProvider(file, nil, false) + UIManager:close(dialog) + end, + }}) + end + -- row: wide button + if type_associated_provider_key then + table.insert(buttons, {{ + text = T(_("Reset default for %1 files"), filename_suffix), + callback = function() + DocumentRegistry:setProvider(file, nil, true) + UIManager:close(dialog) + end, + }}) + end + -- row: wide button + local associated_providers = DocumentRegistry:getAssociatedProviderKey() -- hash table + if next(associated_providers) ~= nil then + table.insert(buttons, {{ + text = _("View defaults for file types"), + callback = function() + local max_len = 0 -- align extensions + for extension in pairs(associated_providers) do + if max_len < #extension then + max_len = #extension + end + end + local t = {} + for extension, provider_key in BaseUtil.orderedPairs(associated_providers) do + local provider = DocumentRegistry:getProviderFromKey(provider_key) + if provider then + local space = string.rep(" ", max_len - #extension) + table.insert(t, T("%1%2: %3", extension, space, provider.provider_name)) + end + end + UIManager:show(InfoMessage:new{ + text = table.concat(t, "\n"), + monospace_font = true, + }) + end, + }}) + end + -- row: 2 buttons + table.insert(buttons, { + { + text = _("Cancel"), + callback = function() + UIManager:close(dialog) + end, + }, + { + text = _("Open"), + is_enter_default = true, + callback = function() + local provider = dialog.radio_button_table.checked_button.provider + if dialog._check_file_button.checked then -- set this file associated provider + UIManager:show(ConfirmBox:new{ + text = T(_("Always open '%2' with %1?"), provider.provider_name, filename_pure), + ok_text = _("Always"), + ok_callback = function() + DocumentRegistry:setProvider(file, provider, false) + self:openFile(file, provider) + UIManager:close(dialog) + end, + }) + elseif dialog._check_global_button.checked then -- set file type associated provider + UIManager:show(ConfirmBox:new{ + text = T(_("Always open %2 files with %1?"), provider.provider_name, filename_suffix), + ok_text = _("Always"), + ok_callback = function() + DocumentRegistry:setProvider(file, provider, true) + self:openFile(file, provider) + UIManager:close(dialog) + end, + }) + else -- open just once + self:openFile(file, provider) + UIManager:close(dialog) + end + end, + }, + }) + + local OpenWithDialog = require("ui/widget/openwithdialog") + dialog = OpenWithDialog:new{ + title = T(_("Open %1 with:"), filename_pure), + radio_buttons = radio_buttons, + buttons = buttons, + } + UIManager:show(dialog) +end + +function FileManager:openFile(file, provider, doc_caller_callback, aux_caller_callback) + if not provider then -- check associated + local provider_key = DocumentRegistry:getAssociatedProviderKey(file) + provider = provider_key and DocumentRegistry:getProviderFromKey(provider_key) + end + if provider and provider.order then -- auxiliary + if aux_caller_callback then + aux_caller_callback() + end + if provider.callback then -- module + provider.callback(file) + else -- plugin + self[provider.provider]:openFile(file) + end + else -- document + if doc_caller_callback then + doc_caller_callback() + end + local ReaderUI = require("apps/reader/readerui") + ReaderUI:showReader(file, provider) + end +end + return FileManager diff --git a/frontend/apps/filemanager/filemanagercollection.lua b/frontend/apps/filemanager/filemanagercollection.lua index 30bdd1e05..54d445643 100644 --- a/frontend/apps/filemanager/filemanagercollection.lua +++ b/frontend/apps/filemanager/filemanagercollection.lua @@ -161,7 +161,7 @@ function FileManagerCollection:showCollDialog() path = G_reader_settings:readSetting("home_dir"), select_directory = false, file_filter = function(file) - return DocumentRegistry:getProviders(file) ~= nil + return DocumentRegistry:hasProvider(file) end, onConfirm = function(file) if not ReadCollection:checkItemExist(file) then diff --git a/frontend/apps/filemanager/filemanagerfilesearcher.lua b/frontend/apps/filemanager/filemanagerfilesearcher.lua index fbb75f3b1..131af0774 100644 --- a/frontend/apps/filemanager/filemanagerfilesearcher.lua +++ b/frontend/apps/filemanager/filemanagerfilesearcher.lua @@ -269,7 +269,6 @@ end function FileSearcher:onMenuSelect(item) local file = item.path - local has_provider = false local dialog local function close_dialog_callback() UIManager:close(dialog) @@ -281,8 +280,7 @@ function FileSearcher:onMenuSelect(item) local buttons = {} if item.is_file then local is_currently_opened = self.ui.document and self.ui.document.file == file - has_provider = DocumentRegistry:hasProvider(file) - if has_provider or DocSettings:hasSidecarFile(file) then + if DocumentRegistry:hasProvider(file) or DocSettings:hasSidecarFile(file) then local doc_settings_or_file = is_currently_opened and self.ui.doc_settings or file table.insert(buttons, filemanagerutil.genStatusButtonsRow(doc_settings_or_file, close_dialog_callback)) table.insert(buttons, {}) -- separator @@ -317,11 +315,11 @@ function FileSearcher:onMenuSelect(item) filemanagerutil.genShowFolderButton(file, close_dialog_menu_callback), { text = _("Open"), - enabled = has_provider, + enabled = DocumentRegistry:hasProvider(file, nil, true), -- allow auxiliary providers callback = function() - close_dialog_menu_callback() - local ReaderUI = require("apps/reader/readerui") - ReaderUI:showReader(file) + close_dialog_callback() + local FileManager = require("apps/filemanager/filemanager") + FileManager.openFile(self.ui, file, nil, self.close_callback) end, }, }) @@ -334,10 +332,9 @@ end function FileSearcher:onMenuHold(item) if item.is_file then - if DocumentRegistry:hasProvider(item.path) then - self.close_callback() - local ReaderUI = require("apps/reader/readerui") - ReaderUI:showReader(item.path) + if DocumentRegistry:hasProvider(item.path, nil, true) then + local FileManager = require("apps/filemanager/filemanager") + FileManager.openFile(self.ui, item.path, nil, self.close_callback) end else self.close_callback() diff --git a/frontend/apps/filemanager/filemanagerutil.lua b/frontend/apps/filemanager/filemanagerutil.lua index 9fc209a2d..a70fd127c 100644 --- a/frontend/apps/filemanager/filemanagerutil.lua +++ b/frontend/apps/filemanager/filemanagerutil.lua @@ -8,6 +8,7 @@ local DocSettings = require("docsettings") local Event = require("ui/event") local UIManager = require("ui/uimanager") local ffiutil = require("ffi/util") +local lfs = require("libs/libkoreader-lfs") local util = require("util") local _ = require("gettext") local T = ffiutil.template @@ -49,6 +50,26 @@ function filemanagerutil.splitFileNameType(filepath) return filename_without_suffix, filetype end +function filemanagerutil.getRandomFile(dir, match_func) + if not dir:match("/$") then + dir = dir .. "/" + end + local files = {} + local ok, iter, dir_obj = pcall(lfs.dir, dir) + if ok then + for entry in iter, dir_obj do + local file = dir .. entry + if lfs.attributes(file, "mode") == "file" and match_func(file) then + table.insert(files, entry) + end + end + if #files > 0 then + math.randomseed(os.time()) + return dir .. files[math.random(#files)] + end + end +end + -- Purge doc settings except kept function filemanagerutil.resetDocumentSettings(file) local settings_to_keep = { diff --git a/frontend/apps/reader/modules/readerlink.lua b/frontend/apps/reader/modules/readerlink.lua index b3c2c0aeb..da9f8faca 100644 --- a/frontend/apps/reader/modules/readerlink.lua +++ b/frontend/apps/reader/modules/readerlink.lua @@ -6,6 +6,7 @@ local BD = require("ui/bidi") local ButtonDialogTitle = require("ui/widget/buttondialogtitle") local ConfirmBox = require("ui/widget/confirmbox") local Device = require("device") +local DocumentRegistry = require("document/documentregistry") local Event = require("ui/event") local InfoMessage = require("ui/widget/infomessage") local InputContainer = require("ui/widget/container/inputcontainer") @@ -822,9 +823,7 @@ function ReaderLink:onGotoLink(link, neglect_current_location, allow_footnote_po linked_filename = ffiutil.joinPath(self.document_dir, linked_filename) -- get full path linked_filename = ffiutil.realpath(linked_filename) -- clean full path from ./ or ../ if linked_filename and lfs.attributes(linked_filename, "mode") == "file" then - local DocumentRegistry = require("document/documentregistry") - local provider = DocumentRegistry:getProvider(linked_filename) - if provider then + if DocumentRegistry:hasProvider(linked_filename) then -- Display filename with anchor or query string, so the user gets -- this information and can manually go to the appropriate place local display_filename = linked_filename diff --git a/frontend/document/documentregistry.lua b/frontend/document/documentregistry.lua index 039cac81a..76a9cd46f 100644 --- a/frontend/document/documentregistry.lua +++ b/frontend/document/documentregistry.lua @@ -4,12 +4,12 @@ This is a registry for document providers local DocSettings = require("docsettings") local logger = require("logger") -local lfs = require("libs/libkoreader-lfs") local util = require("util") local DocumentRegistry = { registry = {}, providers = {}, + known_providers = {}, -- hash table of registered providers { provider_key = provider } filetype_provider = {}, mimetype_ext = {}, image_ext = { @@ -24,6 +24,10 @@ local DocumentRegistry = { }, } +local function getSuffix(file) + return util.getFileNameSuffix(file):lower() +end + function DocumentRegistry:addProvider(extension, mimetype, provider, weight) extension = string.lower(extension) table.insert(self.providers, { @@ -37,97 +41,65 @@ function DocumentRegistry:addProvider(extension, mimetype, provider, weight) -- Provided we order the calls to addProvider() correctly, -- that means epub instead of epub3, etc. self.mimetype_ext[mimetype] = self.mimetype_ext[mimetype] or extension + if self.known_providers[provider.provider] == nil then + self.known_providers[provider.provider] = provider + end end -function DocumentRegistry:getRandomFile(dir, opened, extension) - if dir:sub(-1) ~= "/" then - dir = dir .. "/" - end - local files = {} - local i = 0 - local ok, iter, dir_obj = pcall(lfs.dir, dir) - if ok then - for entry in iter, dir_obj do - local file = dir .. entry - local file_opened = DocSettings:hasSidecarFile(file) - if lfs.attributes(file, "mode") == "file" and self:hasProvider(file) - and (opened == nil or file_opened == opened) - and (extension == nil or extension[util.getFileNameSuffix(entry)]) then - i = i + 1 - files[i] = entry - end - end - if i == 0 then - return nil - end - else - return nil - end - math.randomseed(os.time()) - return dir .. files[math.random(i)] +-- Register an auxiliary (non-document) provider. +-- Aux providers are modules (eg TextViewer) or plugins (eg TextEditor). +-- It does not implement the Document API. +-- For plugins the hash table value does not contain file handler, +-- but only a provider_key (provider.provider) to call the corresponding +-- plugin in FileManager:openFile(). +function DocumentRegistry:addAuxProvider(provider) + self.known_providers[provider.provider] = provider end --- Returns true if file has provider. -- @string file +-- @bool include_aux include auxiliary (non-document) providers -- @treturn boolean -function DocumentRegistry:hasProvider(file, mimetype) +function DocumentRegistry:hasProvider(file, mimetype, include_aux) if mimetype and self.mimetype_ext[mimetype] then return true end if not file then return false end - local filename_suffix = string.lower(util.getFileNameSuffix(file)) - - local filetype_provider = G_reader_settings:readSetting("provider") or {} - if self.filetype_provider[filename_suffix] or filetype_provider[filename_suffix] then + -- registered document provider + local filename_suffix = getSuffix(file) + if self.filetype_provider[filename_suffix] then return true end + -- associated document or auxiliary provider for file type + local filetype_provider_key = G_reader_settings:readSetting("provider", {})[filename_suffix] + local provider = filetype_provider_key and self.known_providers[filetype_provider_key] + if provider and (not provider.order or include_aux) then -- excluding auxiliary by default + return true + end + -- associated document provider for this file if DocSettings:hasSidecarFile(file) then return DocSettings:open(file):has("provider") end return false end ---- Returns the preferred registered document handler. +--- Returns the preferred registered document handler or fallback provider. -- @string file --- @treturn table provider, or nil +-- @treturn table provider function DocumentRegistry:getProvider(file) local providers = self:getProviders(file) - if providers then - -- provider for document - if DocSettings:hasSidecarFile(file) then - local doc_settings_provider = DocSettings:open(file):readSetting("provider") - if doc_settings_provider then - for _, provider in ipairs(providers) do - if provider.provider.provider == doc_settings_provider then - return provider.provider - end - end - end + -- associated provider + local provider_key = DocumentRegistry:getAssociatedProviderKey(file) + local provider = provider_key and self.known_providers[provider_key] + if provider and not provider.order then -- excluding auxiliary + return provider end - - -- global provider for filetype - local filename_suffix = util.getFileNameSuffix(file) - local g_settings_provider = G_reader_settings:readSetting("provider") - - if g_settings_provider and g_settings_provider[filename_suffix] then - for _, provider in ipairs(providers) do - if provider.provider.provider == g_settings_provider[filename_suffix] then - return provider.provider - end - end - end - -- highest weighted provider return providers[1].provider - else - for _, provider in ipairs(self.providers) do - if provider.extension == "txt" then - return provider.provider - end - end end + return self:getFallbackProvider() end --- Returns the registered document handlers. @@ -166,6 +138,59 @@ function DocumentRegistry:getProviders(file) end end +function DocumentRegistry:getProviderFromKey(provider_key) + return self.known_providers[provider_key] +end + +function DocumentRegistry:getFallbackProvider() + for _, provider in ipairs(self.providers) do + if provider.extension == "txt" then + return provider.provider + end + end +end + +function DocumentRegistry:getAssociatedProviderKey(file, all) + -- all: nil - first not empty, false - this file, true - file type + + if not file then -- get the full list of associated providers + return G_reader_settings:readSetting("provider") + end + + -- provider for this file + local provider_key + if all ~= true then + if DocSettings:hasSidecarFile(file) then + provider_key = DocSettings:open(file):readSetting("provider") + if provider_key or all == false then + return provider_key + end + end + if all == false then return end + end + + -- provider for file type + local providers = G_reader_settings:readSetting("provider") + provider_key = providers and providers[getSuffix(file)] + if provider_key and self.known_providers[provider_key] then + return provider_key + end +end + +-- Returns array: registered auxiliary providers sorted by order. +function DocumentRegistry:getAuxProviders() + local providers = {} + for _, provider in pairs(self.known_providers) do + if provider.order then -- aux + table.insert(providers, provider) + end + end + if #providers >= 1 then + table.sort(providers, function(a, b) return a.order < b.order end) + return providers + end +end + --- Get mapping of file extensions to providers -- @treturn table mapping file extensions to a list of providers function DocumentRegistry:getExtensions() @@ -182,8 +207,7 @@ end -- @string file -- @bool all function DocumentRegistry:setProvider(file, provider, all) - local _, filename_suffix = util.splitFileNameSuffix(file) - + provider = provider or {} -- call with nil to reset -- per-document if not all then local doc_settings = DocSettings:open(file) @@ -191,9 +215,8 @@ function DocumentRegistry:setProvider(file, provider, all) doc_settings:flush() -- global else - local filetype_provider = G_reader_settings:readSetting("provider") or {} - filetype_provider[filename_suffix] = provider.provider - G_reader_settings:saveSetting("provider", filetype_provider) + local filetype_provider = G_reader_settings:readSetting("provider", {}) + filetype_provider[getSuffix(file)] = provider.provider end end @@ -257,7 +280,7 @@ function DocumentRegistry:getReferenceCount(file) end function DocumentRegistry:isImageFile(file) - return self.image_ext[util.getFileNameSuffix(file):lower()] and true or false + return self.image_ext[getSuffix(file)] and true or false end -- load implementations: @@ -265,5 +288,9 @@ require("document/credocument"):register(DocumentRegistry) require("document/pdfdocument"):register(DocumentRegistry) require("document/djvudocument"):register(DocumentRegistry) require("document/picdocument"):register(DocumentRegistry) +-- auxiliary built-in +require("ui/widget/imageviewer"):register(DocumentRegistry) +require("ui/widget/textviewer"):register(DocumentRegistry) +-- auxiliary from plugins return DocumentRegistry diff --git a/frontend/ui/screensaver.lua b/frontend/ui/screensaver.lua index ded637f2c..7ef2c0647 100644 --- a/frontend/ui/screensaver.lua +++ b/frontend/ui/screensaver.lua @@ -23,6 +23,7 @@ local VerticalGroup = require("ui/widget/verticalgroup") local VerticalSpan = require("ui/widget/verticalspan") local datetime = require("datetime") local ffiUtil = require("ffi/util") +local filemanagerutil = require("apps/filemanager/filemanagerutil") local lfs = require("libs/libkoreader-lfs") local logger = require("logger") local util = require("util") @@ -78,35 +79,12 @@ if Device:isEmulator() then Screensaver.default_screensaver_message = Screensaver.default_screensaver_message .. "\n" .. _("(Press F2 to resume)") end -function Screensaver:_getRandomImage(dir) - if not dir then - return nil +local function _getRandomImage(dir) + if not dir then return end + local match_func = function(file) -- images, ignore macOS resource forks + return not util.stringStartsWith(file, "._") and DocumentRegistry:isImageFile(file) end - - if string.sub(dir, string.len(dir)) ~= "/" then - dir = dir .. "/" - end - local pics = {} - local i = 0 - math.randomseed(os.time()) - local ok, iter, dir_obj = pcall(lfs.dir, dir) - if ok then - for f in iter, dir_obj do - -- Always ignore macOS resource forks, too. - if lfs.attributes(dir .. f, "mode") == "file" and not util.stringStartsWith(f, "._") - and DocumentRegistry:isImageFile(f) then - i = i + 1 - pics[i] = f - end - end - if i == 0 then - return nil - end - else - return nil - end - - return dir .. pics[math.random(i)] + return filemanagerutil.getRandomFile(dir, match_func) end -- This is implemented by the Statistics plugin @@ -573,7 +551,7 @@ function Screensaver:setup(event, event_message) if self.screensaver_type == "random_image" then local screensaver_dir = G_reader_settings:readSetting(self.prefix .. "screensaver_dir") or G_reader_settings:readSetting("screensaver_dir") - self.image_file = self:_getRandomImage(screensaver_dir) or "resources/koreader.png" -- Fallback image + self.image_file = _getRandomImage(screensaver_dir) or "resources/koreader.png" -- Fallback image end -- Use the right background setting depending on the effective mode, now that fallbacks have kicked in. diff --git a/frontend/ui/widget/filechooser.lua b/frontend/ui/widget/filechooser.lua index 2d64c906c..3971f2ef6 100644 --- a/frontend/ui/widget/filechooser.lua +++ b/frontend/ui/widget/filechooser.lua @@ -1,12 +1,10 @@ local BD = require("ui/bidi") -local ConfirmBox = require("ui/widget/confirmbox") local datetime = require("datetime") local Device = require("device") local DocSettings = require("docsettings") local DocumentRegistry = require("document/documentregistry") local filemanagerutil = require("apps/filemanager/filemanagerutil") local Menu = require("ui/widget/menu") -local OpenWithDialog = require("ui/widget/openwithdialog") local UIManager = require("ui/uimanager") local ffi = require("ffi") local ffiUtil = require("ffi/util") @@ -515,121 +513,4 @@ function FileChooser:selectAllFilesInFolder() end end -function FileChooser:showSetProviderButtons(file, one_time_providers) - local ReaderUI = require("apps/reader/readerui") - - local __, filename_pure = util.splitFilePathName(file) - local filename_suffix = util.getFileNameSuffix(file) - - local buttons = {} - local radio_buttons = {} - local filetype_provider = G_reader_settings:readSetting("provider") or {} - local providers = DocumentRegistry:getProviders(file) - if providers ~= nil then - for ___, provider in ipairs(providers) do - -- we have no need for extension, mimetype, weights, etc. here - provider = provider.provider - table.insert(radio_buttons, { - { - text = provider.provider_name, - checked = DocumentRegistry:getProvider(file) == provider, - provider = provider, - }, - }) - end - else - local provider = DocumentRegistry:getProvider(file) - table.insert(radio_buttons, { - { - -- @translators %1 is the provider name, such as Cool Reader Engine or MuPDF. - text = T(_("%1 ~Unsupported"), provider.provider_name), - checked = true, - 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, { - { - text = _("Cancel"), - callback = function() - UIManager:close(self.set_provider_dialog) - end, - }, - { - text = _("Open"), - is_enter_default = true, - callback = function() - local provider = self.set_provider_dialog.radio_button_table.checked_button.provider - if provider.one_time_provider then - UIManager:close(self.set_provider_dialog) - provider.callback() - return - end - - -- always for this file - if self.set_provider_dialog._check_file_button.checked then - UIManager:show(ConfirmBox:new{ - text = T(_("Always open '%2' with %1?"), - provider.provider_name, BD.filename(filename_pure)), - ok_text = _("Always"), - ok_callback = function() - DocumentRegistry:setProvider(file, provider, false) - - ReaderUI:showReader(file, provider) - UIManager:close(self.set_provider_dialog) - end, - }) - -- always for all files of this file type - elseif self.set_provider_dialog._check_global_button.checked then - UIManager:show(ConfirmBox:new{ - text = T(_("Always open %2 files with %1?"), - provider.provider_name, filename_suffix), - ok_text = _("Always"), - ok_callback = function() - DocumentRegistry:setProvider(file, provider, true) - - ReaderUI:showReader(file, provider) - UIManager:close(self.set_provider_dialog) - end, - }) - else - -- just once - ReaderUI:showReader(file, provider) - UIManager:close(self.set_provider_dialog) - end - end, - }, - }) - - if filetype_provider[filename_suffix] ~= nil then - table.insert(buttons, { - { - text = _("Reset default"), - callback = function() - filetype_provider[filename_suffix] = nil - G_reader_settings:saveSetting("provider", filetype_provider) - UIManager:close(self.set_provider_dialog) - end, - }, - }) - end - - self.set_provider_dialog = OpenWithDialog:new{ - title = T(_("Open %1 with:"), BD.filename(filename_pure)), - radio_buttons = radio_buttons, - buttons = buttons, - } - UIManager:show(self.set_provider_dialog) -end - return FileChooser diff --git a/frontend/ui/widget/imageviewer.lua b/frontend/ui/widget/imageviewer.lua index 46a8e22dd..438e4cc2e 100644 --- a/frontend/ui/widget/imageviewer.lua +++ b/frontend/ui/widget/imageviewer.lua @@ -851,4 +851,27 @@ function ImageViewer:onCloseWidget() end) end +-- Register DocumentRegistry auxiliary provider. +function ImageViewer:register(registry) + registry:addAuxProvider({ + provider_name = _("Image viewer"), + provider = "imageviewer", + order = 10, -- order in OpenWith dialog + enabled_func = function(file) + return registry:isImageFile(file) + end, + callback = ImageViewer.openFile, + disable_file = true, + disable_type = false, + }) +end + +function ImageViewer.openFile(file) + UIManager:show(ImageViewer:new{ + file = file, + fullscreen = true, + with_title_bar = false, + }) +end + return ImageViewer diff --git a/frontend/ui/widget/imagewidget.lua b/frontend/ui/widget/imagewidget.lua index 33b0a4e1a..d452c9df3 100644 --- a/frontend/ui/widget/imagewidget.lua +++ b/frontend/ui/widget/imagewidget.lua @@ -22,7 +22,6 @@ Show image from memory example: local Blitbuffer = require("ffi/blitbuffer") local Cache = require("cache") -local DocumentRegistry = require("document/documentregistry") local Geom = require("ui/geometry") local RenderImage = require("ui/renderimage") local Screen = require("device").screen @@ -131,6 +130,7 @@ function ImageWidget:_loadimage() end function ImageWidget:_loadfile() + local DocumentRegistry = require("document/documentregistry") if DocumentRegistry:isImageFile(self.file) then -- In our use cases for files (icons), we either provide width and height, -- or just scale_for_dpi, and scale_factor should stay nil. diff --git a/frontend/ui/widget/infomessage.lua b/frontend/ui/widget/infomessage.lua index 6acdd0e31..88d4f06f8 100644 --- a/frontend/ui/widget/infomessage.lua +++ b/frontend/ui/widget/infomessage.lua @@ -46,7 +46,8 @@ local Screen = Device.screen local InfoMessage = InputContainer:extend{ modal = true, - face = Font:getFace("infofont"), + face = nil, + monospace_font = false, text = "", timeout = nil, -- in seconds width = nil, -- The width of the InfoMessage. Keep it nil to use default value. @@ -77,6 +78,10 @@ local InfoMessage = InputContainer:extend{ } function InfoMessage:init() + if not self.face then + self.face = Font:getFace(self.monospace_font and "infont" or "infofont") + end + if self.dismissable then if Device:hasKeys() then self.key_events.AnyKeyPressed = { { Input.group.Any } } diff --git a/frontend/ui/widget/openwithdialog.lua b/frontend/ui/widget/openwithdialog.lua index 47b34e54d..0a6b97528 100644 --- a/frontend/ui/widget/openwithdialog.lua +++ b/frontend/ui/widget/openwithdialog.lua @@ -32,11 +32,14 @@ function OpenWithDialog:init() scroll = false, parent = self, button_select_callback = function(btn) - if btn.provider.one_time_provider then + if btn.provider.disable_file then self._check_file_button:disable() - self._check_global_button:disable() else self._check_file_button:enable() + end + if btn.provider.disable_type then + self._check_global_button:disable() + else self._check_global_button:enable() end end @@ -83,11 +86,13 @@ function OpenWithDialog:init() self._check_file_button = self._check_file_button or CheckButton:new{ text = _("Always use this engine for this file"), + enabled = not self.radio_button_table.checked_button.provider.disable_file, parent = self, } self:addWidget(self._check_file_button) self._check_global_button = self._check_global_button or CheckButton:new{ text = _("Always use this engine for file type"), + enabled = not self.radio_button_table.checked_button.provider.disable_type, parent = self, } self:addWidget(self._check_global_button) diff --git a/frontend/ui/widget/textviewer.lua b/frontend/ui/widget/textviewer.lua index e15f675d0..884cd90d7 100644 --- a/frontend/ui/widget/textviewer.lua +++ b/frontend/ui/widget/textviewer.lua @@ -29,6 +29,7 @@ local UIManager = require("ui/uimanager") local VerticalGroup = require("ui/widget/verticalgroup") local WidgetContainer = require("ui/widget/container/widgetcontainer") local T = require("ffi/util").template +local lfs = require("libs/libkoreader-lfs") local util = require("util") local _ = require("gettext") local Screen = Device.screen @@ -431,7 +432,6 @@ function TextViewer:onForwardingPanRelease(arg, ges) return self.movable:onMovablePanRelease(arg, ges) end - function TextViewer:findDialog() local input_dialog input_dialog = InputDialog:new{ @@ -530,4 +530,50 @@ function TextViewer:handleTextSelection(text, hold_duration, start_idx, end_idx, end end +-- Register DocumentRegistry auxiliary provider. +function TextViewer:register(registry) + registry:addAuxProvider({ + provider_name = _("Text viewer"), + provider = "textviewer", + order = 20, -- order in OpenWith dialog + enabled_func = function() + return true -- all files + end, + callback = TextViewer.openFile, + disable_file = true, + disable_type = false, + }) +end + +function TextViewer.openFile(file) + local function _openFile(file_path) + local file_handle = io.open(file_path, "rb") + if not file_handle then return end + local file_content = file_handle:read("*all") + file_handle:close() + UIManager:show(TextViewer:new{ + title = file_path, + title_multilines = true, + justified = false, + text = file_content, + }) + end + local attr = lfs.attributes(file) + if attr then + if attr.size > 400000 then + local ConfirmBox = require("ui/widget/confirmbox") + UIManager:show(ConfirmBox:new{ + text = T(_("This file is %2:\n\n%1\n\nAre you sure you want to open it?\n\nOpening big files may take some time."), + BD.filepath(file), util.getFriendlySize(attr.size)), + ok_text = _("Open"), + ok_callback = function() + _openFile(file) + end, + }) + else + _openFile(file) + end + end +end + return TextViewer diff --git a/plugins/archiveviewer.koplugin/main.lua b/plugins/archiveviewer.koplugin/main.lua index 51c78d24f..53eac9502 100644 --- a/plugins/archiveviewer.koplugin/main.lua +++ b/plugins/archiveviewer.koplugin/main.lua @@ -2,8 +2,10 @@ local BD = require("ui/bidi") local ButtonDialog = require("ui/widget/buttondialog") local CenterContainer = require("ui/widget/container/centercontainer") local DocumentRegistry = require("document/documentregistry") -local InfoMessage = require("ui/widget/infomessage") +local ImageViewer = require("ui/widget/imageviewer") local Menu = require("ui/widget/menu") +local RenderImage = require("ui/renderimage") +local TextViewer = require("ui/widget/textviewer") local UIManager = require("ui/uimanager") local WidgetContainer = require("ui/widget/container/widgetcontainer") local ffiUtil = require("ffi/util") @@ -13,6 +15,9 @@ local Screen = require("device").screen local T = ffiUtil.template local ArchiveViewer = WidgetContainer:extend{ + name = "archiveviewer", + fullname = _("Archive viewer"), + arc_file = nil, -- archive -- list_table is a flat table containing archive files and folders -- key - a full path of the folder ("/" for root), for all folders and subfolders of any level -- value - a subtable of subfolders and files in the folder @@ -27,21 +32,44 @@ local ArchiveViewer = WidgetContainer:extend{ }, } -function ArchiveViewer:isSupported(file) - return self.arc_ext[util.getFileNameSuffix(file):lower()] and true or false +local ZIP_LIST = "unzip -qql \"%1\"" +local ZIP_EXTRACT_CONTENT = "unzip -qqp \"%1\" \"%2\"" +local ZIP_EXTRACT_FILE = "unzip -qqo \"%1\" \"%2\" -d \"%3\"" -- overwrite + +local function getSuffix(file) + return util.getFileNameSuffix(file):lower() +end + +function ArchiveViewer:init() + self:registerDocumentRegistryAuxProvider() +end + +function ArchiveViewer:registerDocumentRegistryAuxProvider() + DocumentRegistry:addAuxProvider({ + provider_name = self.fullname, + provider = self.name, + order = 40, -- order in OpenWith dialog + disable_file = true, + disable_type = false, + }) end -function ArchiveViewer:openArchiveViewer(file) +function ArchiveViewer:isFileTypeSupported(file) + return self.arc_ext[getSuffix(file)] and true or false +end + +function ArchiveViewer:openFile(file) local _, filename = util.splitFilePathName(file) - local fileext = util.getFileNameSuffix(file):lower() + local fileext = getSuffix(filename) if fileext == "cbz" or fileext == "epub" or fileext == "zip" then self.arc_type = "zip" end + self.arc_file = file self.fm_updated = nil self.list_table = {} if self.arc_type == "zip" then - self:getZipListTable(file) + self:getZipListTable() else -- add other archivers here return end @@ -50,17 +78,17 @@ function ArchiveViewer:openArchiveViewer(file) local menu_container = CenterContainer:new{ dimen = Screen:getSize(), } - local menu = Menu:new{ + self.menu = Menu:new{ is_borderless = true, is_popout = false, title_multilines = true, show_parent = menu_container, onMenuSelect = function(self_menu, item) - if item.text:sub(-1) == "/" then -- folder + if item.is_file then + self:showFileDialog(item.path) + else local title = item.path == "" and filename or filename.."/"..item.path self_menu:switchItemTable(title, self:getItemTable(item.path)) - else - self:showFileDialog(file, item.path) end end, close_callback = function() @@ -70,12 +98,12 @@ function ArchiveViewer:openArchiveViewer(file) end end, } - table.insert(menu_container, menu) - menu:switchItemTable(filename, item_table) + table.insert(menu_container, self.menu) + self.menu:switchItemTable(filename, item_table) UIManager:show(menu_container) end -function ArchiveViewer:getZipListTable(file) +function ArchiveViewer:getZipListTable() local function parse_path(filepath, filesize) if not filepath then return end local path, name = util.splitFilePathName(filepath) @@ -97,7 +125,7 @@ function ArchiveViewer:getZipListTable(file) end end - local std_out = io.popen("unzip ".."-qql \""..file.."\"") + local std_out = io.popen(T(ZIP_LIST, self.arc_file)) if std_out then for line in std_out:lines() do -- entry datetime not used so far @@ -129,6 +157,7 @@ function ArchiveViewer:getItemTable(path) if v then -- file table.insert(files, { text = name, + is_file = true, bidi_wrap_func = BD.filename, path = prefix..name, mandatory = util.getFriendlySize(tonumber(v)), @@ -173,7 +202,7 @@ function ArchiveViewer:getItemDirMandatory(name) return text end -function ArchiveViewer:showFileDialog(arcfile, filepath) +function ArchiveViewer:showFileDialog(filepath) local dialog local buttons = { { @@ -181,14 +210,14 @@ function ArchiveViewer:showFileDialog(arcfile, filepath) text = _("Extract"), callback = function() UIManager:close(dialog) - self:extractFile(arcfile, filepath) + self:extractFile(filepath) end, }, { text = _("View"), callback = function() UIManager:close(dialog) - self:viewFile(arcfile, filepath) + self:viewFile(filepath) end, }, }, @@ -201,47 +230,53 @@ function ArchiveViewer:showFileDialog(arcfile, filepath) UIManager:show(dialog) end -function ArchiveViewer:viewFile(arcfile, filepath) - local content - if self.arc_type == "zip" then - local std_out = io.popen("unzip ".."-qp \""..arcfile.."\"".." ".."\""..filepath.."\"") - if std_out then - content = std_out:read("*all") - std_out:close() - else - UIManager:show(InfoMessage:new{ - text = _("Cannot extract file content."), - icon = "notice-warning", - }) - return - end - else - return - end - +function ArchiveViewer:viewFile(filepath) if DocumentRegistry:isImageFile(filepath) then - local RenderImage = require("ui/renderimage") - local ImageViewer = require("ui/widget/imageviewer") - UIManager:show(ImageViewer:new{ - image = RenderImage:renderImageData(content, #content), + local index = 0 + local curr_index + local images_list = {} + for i, item in ipairs(self.menu.item_table) do + local item_path = item.path + if item.is_file and DocumentRegistry:isImageFile(item_path) then + table.insert(images_list, item_path) + if not curr_index then + index = index + 1 + if item_path == filepath then + curr_index = index + end + end + end + end + local image_table = { image_disposable = true } + setmetatable(image_table, {__index = function (_, key) + local content = self:extractContent(images_list[key]) + if content then + return RenderImage:renderImageData(content, #content) + end + end + }) + local viewer = ImageViewer:new{ + image = image_table, + images_list_nb = #images_list, fullscreen = true, with_title_bar = false, - }) + image_disposable = false, + } + UIManager:show(viewer) + viewer:switchToImageNum(curr_index) else - local TextViewer = require("ui/widget/textviewer") UIManager:show(TextViewer:new{ title = filepath, title_multilines = true, - text = content, + text = self:extractContent(filepath), justified = false, }) end end -function ArchiveViewer:extractFile(arcfile, filepath) +function ArchiveViewer:extractFile(filepath) if self.arc_type == "zip" then - local std_out = io.popen("unzip ".."-qo \""..arcfile.."\"".." ".."\""..filepath.."\"".. - " -d ".."\""..util.splitFilePathName(arcfile).."\"") + local std_out = io.popen(T(ZIP_EXTRACT_FILE, self.arc_file, filepath, util.splitFilePathName(self.arc_file))) if std_out then std_out:close() end @@ -251,4 +286,18 @@ function ArchiveViewer:extractFile(arcfile, filepath) self.fm_updated = true end +function ArchiveViewer:extractContent(filepath) + local content + if self.arc_type == "zip" then + local std_out = io.popen(T(ZIP_EXTRACT_CONTENT, self.arc_file, filepath)) + if std_out then + content = std_out:read("*all") + std_out:close() + return content + end + else + return + end +end + return ArchiveViewer diff --git a/plugins/texteditor.koplugin/main.lua b/plugins/texteditor.koplugin/main.lua index 8c2f34364..2cea99c6d 100644 --- a/plugins/texteditor.koplugin/main.lua +++ b/plugins/texteditor.koplugin/main.lua @@ -8,6 +8,7 @@ local BD = require("ui/bidi") local ConfirmBox = require("ui/widget/confirmbox") local DataStorage = require("datastorage") local Dispatcher = require("dispatcher") +local DocumentRegistry = require("document/documentregistry") local Font = require("ui/font") local QRMessage = require("ui/widget/qrmessage") local InfoMessage = require("ui/widget/infomessage") @@ -28,6 +29,7 @@ local T = ffiutil.template local TextEditor = WidgetContainer:extend{ name = "texteditor", + fullname = _("Text editor"), settings_file = DataStorage:getSettingsDir() .. "/text_editor.lua", settings = nil, -- loaded only when needed -- how many to display in menu (10x3 pages minus our 3 default menu items): @@ -46,6 +48,25 @@ end function TextEditor:init() self:onDispatcherRegisterActions() self.ui.menu:registerToMainMenu(self) + self:registerDocumentRegistryAuxProvider() +end + +function TextEditor:registerDocumentRegistryAuxProvider() + DocumentRegistry:addAuxProvider({ + provider_name = self.fullname, + provider = self.name, + order = 30, -- order in OpenWith dialog + disable_file = true, + disable_type = false, + }) +end + +function TextEditor:isFileTypeSupported(file) + return true +end + +function TextEditor:openFile(file) + self:checkEditFile(file) end function TextEditor:loadSettings() @@ -94,7 +115,7 @@ end function TextEditor:addToMainMenu(menu_items) menu_items.text_editor = { - text = _("Text editor"), + text = self.fullname, sub_item_table_func = function() return self:getSubMenuItems() end,