--[[-- This module contains miscellaneous helper functions for FileManager ]] local BD = require("ui/bidi") local BookList = require("ui/widget/booklist") local CheckButton = require("ui/widget/checkbutton") local ConfirmBox = require("ui/widget/confirmbox") local Device = require("device") 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 local filemanagerutil = {} function filemanagerutil.getDefaultDir() return Device.home_dir or "." end function filemanagerutil.abbreviate(path) if not path then return "" end if G_reader_settings:nilOrTrue("shorten_home_dir") then local home_dir = G_reader_settings:readSetting("home_dir") or filemanagerutil.getDefaultDir() if path == home_dir or path == home_dir .. "/" then return _("Home") end local len = home_dir:len() local start = path:sub(1, len) if start == home_dir and path:sub(len+1, len+1) == "/" then return path:sub(len+2) end end return path end function filemanagerutil.splitFileNameType(filepath) local _, filename = util.splitFilePathName(filepath) local filename_without_suffix, filetype = util.splitFileNameSuffix(filename) filetype = filetype:lower() if filetype == "zip" then local filename_without_sub_suffix, sub_filetype = util.splitFileNameSuffix(filename_without_suffix) sub_filetype = sub_filetype:lower() local supported_sub_filetypes = { "fb2", "htm", "html", "log", "md", "rtf", "txt", } if util.arrayContains(supported_sub_filetypes, sub_filetype) then return filename_without_sub_suffix, sub_filetype .. ".zip" end end return filename_without_suffix, filetype end function filemanagerutil.getRandomFile(dir, match_func) local files = {} util.findFiles(dir, function(file) if match_func(file) then table.insert(files, file) end end, false) if #files > 0 then math.randomseed(os.time()) return files[math.random(#files)] end end -- Purge doc settings except kept function filemanagerutil.resetDocumentSettings(file) local settings_to_keep = { annotations = true, annotations_paging = true, annotations_rolling = true, bookmarks = true, bookmarks_paging = true, bookmarks_rolling = true, bookmarks_sorted_20220106 = true, bookmarks_version = true, cre_dom_version = true, highlight = true, highlight_paging = true, highlight_rolling = true, highlights_imported = true, last_page = true, last_xpointer = true, } local file_abs_path = ffiUtil.realpath(file) if file_abs_path then local doc_settings = DocSettings:open(file_abs_path) for k in pairs(doc_settings.data) do if not settings_to_keep[k] then doc_settings:delSetting(k) end end doc_settings:makeTrue("docsettings_reset_done") -- for readertypeset block_rendering_mode doc_settings:flush() BookList.setBookInfoCache(file_abs_path, doc_settings) end end function filemanagerutil.saveSummary(doc_settings_or_file, summary) -- In case the book doesn't have a sidecar file, this'll create it if type(doc_settings_or_file) ~= "table" then doc_settings_or_file = DocSettings:open(doc_settings_or_file) end summary.modified = os.date("%Y-%m-%d", os.time()) doc_settings_or_file:saveSetting("summary", summary) doc_settings_or_file:flush() return doc_settings_or_file end -- Generate all book status file dialog buttons in a row function filemanagerutil.genStatusButtonsRow(doc_settings_or_file, caller_callback) local file, summary, status if type(doc_settings_or_file) == "table" then file = doc_settings_or_file:readSetting("doc_path") summary = doc_settings_or_file:readSetting("summary") or {} status = summary.status else file = doc_settings_or_file summary = {} status = BookList.getBookStatus(file) end local function genStatusButton(to_status) return { text = BookList.getBookStatusString(to_status, false, true) .. (status == to_status and " ✓" or ""), enabled = status ~= to_status, callback = function() summary.status = to_status filemanagerutil.saveSummary(doc_settings_or_file, summary) BookList.setBookInfoCacheProperty(file, "status", to_status) caller_callback() end, } end return { genStatusButton("reading"), genStatusButton("abandoned"), genStatusButton("complete"), } end function filemanagerutil.genMultipleStatusButtonsRow(files, caller_callback, button_disabled) local function genStatusButton(to_status) return { text = BookList.getBookStatusString(to_status, false, true), enabled = not button_disabled, callback = function() UIManager:show(ConfirmBox:new{ text = _("Set selected documents status?"), ok_text = _("Set"), ok_callback = function() for file in pairs(files) do local doc_settings = BookList.getDocSettings(file) local summary = doc_settings:readSetting("summary") or {} summary.status = to_status filemanagerutil.saveSummary(doc_settings, summary) BookList.setBookInfoCacheProperty(file, "status", to_status) end caller_callback() end, }) end, } end return { genStatusButton("reading"), genStatusButton("abandoned"), genStatusButton("complete"), } end -- Generate "Reset" file dialog button function filemanagerutil.genResetSettingsButton(doc_settings_or_file, caller_callback, button_disabled) local doc_settings, file, has_sidecar_file if type(doc_settings_or_file) == "table" then doc_settings = doc_settings_or_file file = doc_settings_or_file:readSetting("doc_path") has_sidecar_file = true else file = ffiUtil.realpath(doc_settings_or_file) or doc_settings_or_file has_sidecar_file = BookList.hasBookBeenOpened(file) end local custom_cover_file = DocSettings:findCustomCoverFile(file) local has_custom_cover_file = custom_cover_file and true or false local custom_metadata_file = DocSettings:findCustomMetadataFile(file) local has_custom_metadata_file = custom_metadata_file and true or false return { text = _("Reset"), enabled = not button_disabled and (has_sidecar_file or has_custom_metadata_file or has_custom_cover_file), callback = function() local check_button_settings, check_button_cover, check_button_metadata local confirmbox = ConfirmBox:new{ text = T(_("Reset this document?") .. "\n\n%1\n\n" .. _("Information will be permanently lost."), BD.filepath(file)), ok_text = _("Reset"), ok_callback = function() local data_to_purge = { doc_settings = check_button_settings.checked, custom_cover_file = check_button_cover.checked and custom_cover_file, custom_metadata_file = check_button_metadata.checked and custom_metadata_file, } (doc_settings or DocSettings:open(file)):purge(nil, data_to_purge) if data_to_purge.custom_cover_file or data_to_purge.custom_metadata_file then UIManager:broadcastEvent(Event:new("InvalidateMetadataCache", file)) end if data_to_purge.doc_settings then BookList.setBookInfoCacheProperty(file, "been_opened", false) require("readhistory"):fileSettingsPurged(file) end caller_callback() end, } check_button_settings = CheckButton:new{ text = _("document settings, progress, bookmarks, highlights, notes"), checked = has_sidecar_file, enabled = has_sidecar_file, parent = confirmbox, } confirmbox:addWidget(check_button_settings) check_button_cover = CheckButton:new{ text = _("custom cover image"), checked = has_custom_cover_file, enabled = has_custom_cover_file, parent = confirmbox, } confirmbox:addWidget(check_button_cover) check_button_metadata = CheckButton:new{ text = _("custom book metadata"), checked = has_custom_metadata_file, enabled = has_custom_metadata_file, parent = confirmbox, } confirmbox:addWidget(check_button_metadata) UIManager:show(confirmbox) end, } end function filemanagerutil.genMultipleResetSettingsButton(files, caller_callback, button_disabled) return { text = _("Reset"), enabled = not button_disabled, callback = function() UIManager:show(ConfirmBox:new{ text = _("Reset selected documents?") .. "\n" .. _("Information will be permanently lost."), ok_text = _("Reset"), ok_callback = function() for file in pairs(files) do if BookList.hasBookBeenOpened(file) then DocSettings:open(file):purge() UIManager:broadcastEvent(Event:new("InvalidateMetadataCache", file)) BookList.setBookInfoCacheProperty(file, "been_opened", false) require("readhistory"):fileSettingsPurged(file) end end caller_callback() end, }) end, } end function filemanagerutil.genShowFolderButton(file, caller_callback, button_disabled) return { text = _("Show folder"), enabled = not button_disabled, callback = function() caller_callback() local ui = require("apps/filemanager/filemanager").instance if ui then local pathname = util.splitFilePathName(file) ui.file_chooser:changeToPath(pathname, file) else ui = require("apps/reader/readerui").instance ui:onClose() ui:showFileManager(file) end end, } end function filemanagerutil.genBookInformationButton(doc_settings_or_file, book_props, caller_callback, button_disabled) return { text = _("Book information"), enabled = not button_disabled, callback = function() caller_callback() local ui = require("apps/reader/readerui").instance or require("apps/filemanager/filemanager").instance ui.bookinfo:show(doc_settings_or_file, book_props and ui.bookinfo.extendProps(book_props)) end, } end function filemanagerutil.genBookCoverButton(file, book_props, caller_callback, button_disabled) local has_cover = book_props and book_props.has_cover return { text = _("Book cover"), enabled = (not button_disabled and (not book_props or has_cover)) and true or false, callback = function() caller_callback() local ui = require("apps/reader/readerui").instance or require("apps/filemanager/filemanager").instance ui.bookinfo:onShowBookCover(file) end, } end function filemanagerutil.genBookDescriptionButton(file, book_props, caller_callback, button_disabled) local description = book_props and book_props.description return { text = _("Book description"), -- enabled for deleted books if description is kept in CoverBrowser bookinfo cache enabled = (not (button_disabled or book_props) or description) and true or false, callback = function() caller_callback() local ui = require("apps/reader/readerui").instance or require("apps/filemanager/filemanager").instance ui.bookinfo:onShowBookDescription(description, file) end, } end -- Generate "Execute script" file dialog button function filemanagerutil.genExecuteScriptButton(file, caller_callback) return { -- @translators This is the script's programming language (e.g., shell or python) text = T(_("Execute %1 script"), util.getScriptType(file)), callback = function() filemanagerutil.executeScript(file, caller_callback) end, } end function filemanagerutil.executeScript(file, caller_callback) if caller_callback then caller_callback() end local InfoMessage = require("ui/widget/infomessage") local script_is_running_msg = InfoMessage:new{ -- @translators %1 is the script's programming language (e.g., shell or python), %2 is the filename text = T(_("Running %1 script %2…"), util.getScriptType(file), BD.filename(ffiUtil.basename(file))), } UIManager:show(script_is_running_msg) UIManager:scheduleIn(0.5, function() local rv if Device:isAndroid() then Device:setIgnoreInput(true) rv = os.execute("sh " .. ffiUtil.realpath(file)) -- run by sh, because sdcard has no execute permissions Device:setIgnoreInput(false) else rv = os.execute(ffiUtil.realpath(file)) end UIManager:close(script_is_running_msg) if rv == 0 then UIManager:show(InfoMessage:new{ text = _("The script exited successfully."), }) else --- @note: Lua 5.1 returns the raw return value from the os's system call. Counteract this madness. UIManager:show(InfoMessage:new{ text = T(_("The script returned a non-zero status code: %1!"), bit.rshift(rv, 8)), icon = "notice-warning", }) end end) end function filemanagerutil.showChooseDialog(title_header, caller_callback, current_path, default_path, file_filter) local is_file = file_filter and true or false local path = current_path or default_path local dialog local buttons = { { { text = is_file and _("Choose file") or _("Choose folder"), callback = function() UIManager:close(dialog) if path then if is_file then path = ffiUtil.dirname(path) end if lfs.attributes(path, "mode") ~= "directory" then path = G_reader_settings:readSetting("home_dir") or filemanagerutil.getDefaultDir() end end local PathChooser = require("ui/widget/pathchooser") local path_chooser = PathChooser:new{ select_directory = not is_file, select_file = is_file, show_files = is_file, file_filter = file_filter, path = path, onConfirm = function(new_path) caller_callback(new_path) end, } UIManager:show(path_chooser) end, }, } } if default_path then table.insert(buttons, { { text = _("Use default"), enabled = path ~= default_path, callback = function() UIManager:close(dialog) caller_callback(default_path) end, }, }) end local title_value = path and (is_file and BD.filepath(path) or BD.dirpath(path)) or _("not set") local ButtonDialog = require("ui/widget/buttondialog") dialog = ButtonDialog:new{ title = title_header .. "\n\n" .. title_value .. "\n", buttons = buttons, } UIManager:show(dialog) end return filemanagerutil