From e4c9409f97099c36616e6f122657cb9b537d47bf Mon Sep 17 00:00:00 2001 From: zwim <36999612+zwim@users.noreply.github.com> Date: Thu, 22 Apr 2021 08:38:49 +0200 Subject: [PATCH] [plugin] Add a caching mechanism for CoverImage (#7510) --- frontend/device/android/device.lua | 24 +- frontend/device/generic/device.lua | 5 + frontend/device/pocketbook/device.lua | 4 + frontend/device/remarkable/device.lua | 4 + plugins/coverimage.koplugin/main.lua | 859 +++++++++++++++++--------- 5 files changed, 590 insertions(+), 306 deletions(-) diff --git a/frontend/device/android/device.lua b/frontend/device/android/device.lua index 0d4c9f66d..61166d768 100644 --- a/frontend/device/android/device.lua +++ b/frontend/device/android/device.lua @@ -392,7 +392,20 @@ function Device:canExecuteScript(file) end function Device:isValidPath(path) - return android.isPathInsideSandbox(path) + -- the fast check + if android.isPathInsideSandbox(path) then + return true + end + + -- the thorough check + local real_ext_storage = FFIUtil.realpath(android.getExternalStoragePath()) + local real_path = FFIUtil.realpath(path) + + if real_path then + return real_path:sub(1, #real_ext_storage) == real_ext_storage + else + return false + end end function Device:showLightDialog() @@ -432,6 +445,15 @@ function Device:untar(archive, extract_to) return android.untar(archive, extract_to) end +-- todo: Wouldn't we like an android.deviceIdentifier() method, so we can use better default paths? +function Device:getDefaultCoverPath() + if android.prop.product == "ntx_6sl" then -- Tolino HD4 and other + return android.getExternalStoragePath() .. "/suspend_others.jpg" + else + return android.getExternalStoragePath() .. "/cover.jpg" + end +end + android.LOGI(string.format("Android %s - %s (API %d) - flavor: %s", android.prop.version, getCodename(), Device.firmware_rev, android.prop.flavor)) diff --git a/frontend/device/generic/device.lua b/frontend/device/generic/device.lua index 477de69b9..809c18be9 100644 --- a/frontend/device/generic/device.lua +++ b/frontend/device/generic/device.lua @@ -4,6 +4,7 @@ Generic device abstraction. This module defines stubs for common methods. --]] +local DataStorage = require("datastorage") local logger = require("logger") local util = require("util") local _ = require("gettext") @@ -503,6 +504,10 @@ function Device:isStartupScriptUpToDate() return true end +function Device:getDefaultCoverPath() + return DataStorage:getDataDir() .. "/cover.jpg" +end + --- Unpack an archive. -- Extract the contents of an archive, detecting its format by -- filename extension. Inspired by luarocks archive_unpack() diff --git a/frontend/device/pocketbook/device.lua b/frontend/device/pocketbook/device.lua index b20f4ecc2..ee12b1222 100644 --- a/frontend/device/pocketbook/device.lua +++ b/frontend/device/pocketbook/device.lua @@ -363,6 +363,10 @@ function PocketBook:getDeviceModel() return ffi.string(inkview.GetDeviceModel()) end +function PocketBook:getDefaultCoverPath() + return "/mnt/ext1/system/logo/offlogo/cover.bmp" +end + -- Pocketbook HW rotation modes start from landsape, CCW local function landscape_ccw() return { 1, 0, 3, 2, -- PORTRAIT, LANDSCAPE, PORTRAIT_180, LANDSCAPE_180 diff --git a/frontend/device/remarkable/device.lua b/frontend/device/remarkable/device.lua index 965b76fe6..08f00a0a9 100644 --- a/frontend/device/remarkable/device.lua +++ b/frontend/device/remarkable/device.lua @@ -200,6 +200,10 @@ end logger.info(string.format("Starting %s", rm_model)) +function Remarkable:getDefaultCoverPath() + return "/usr/share/remarkable/poweroff.png" +end + if isRm2 then if not os.getenv("RM2FB_SHIM") then error("reMarkable2 requires RM2FB to work (https://github.com/ddvk/remarkable2-framebuffer)") diff --git a/plugins/coverimage.koplugin/main.lua b/plugins/coverimage.koplugin/main.lua index 1d5bb8e10..64e56f460 100644 --- a/plugins/coverimage.koplugin/main.lua +++ b/plugins/coverimage.koplugin/main.lua @@ -6,31 +6,51 @@ if not (Device.isAndroid() or Device.isEmulator() or Device.isRemarkable() or De return { disabled = true } end +local A, android = pcall(require, "android") -- luacheck: ignore local Blitbuffer = require("ffi/blitbuffer") +local ConfirmBox = require("ui/widget/confirmbox") +local DataStorage = require("datastorage") local InfoMessage = require("ui/widget/infomessage") +local InputDialog = require("ui/widget/inputdialog") +local PathChooser = require("ui/widget/pathchooser") local UIManager = require("ui/uimanager") -local WidgetContainer = require("ui/widget/container/widgetcontainer") local RenderImage = require("ui/renderimage") +local Screen = require("device").screen +local T = require("ffi/util").template +local WidgetContainer = require("ui/widget/container/widgetcontainer") local ffiutil = require("ffi/util") local lfs = require("libs/libkoreader-lfs") local logger = require("logger") +local md5 = require("ffi/sha2").md5 local util = require("util") local _ = require("gettext") -local T = require("ffi/util").template -local function pathOk(filename) - local path, name = util.splitFilePathName(filename) +-- todo: please check the default paths directly on the depending Device:getDefaultCoverPath() + + +local function isPathAllowed(path) + -- don't allow a path that interferes with frontent cache-framework; quick and dirty check + if not Device:isValidPath(path) then -- isValidPath expects a trailing slash - return false, T(_("Path \"%1\" isn't in a writable location."), path) + return false elseif not util.pathExists(path:gsub("/$", "")) then -- pathExists expects no trailing slash - return false, T(_("The path \"%1\" doesn't exist."), path) - elseif name == "" then - return false, _("Please enter a filename at the end of the path.") - elseif lfs.attributes(filename, "mode") == "directory" then - return false, T(_("The path \"%1\" must point to a file, but it points to a folder."), filename) + return false + elseif Device.isAndroid() then + return path ~= "/sdcard/koreader/cache/" + and ffiutil.realpath(path) ~= ffiutil.realpath(android.getExternalStoragePath() .. "/koreader/cache/") + else + return path ~= "./cache/" and ffiutil.realpath(path) ~= ffiutil.realpath("./cache/") end +end + +local function isFileOk(filename) + local path, name = util.splitFilePathName(filename) - return true + if not isPathAllowed(path) then + return false + end + + return name ~="" and lfs.attributes(filename, "mode") ~= "directory" end local function getExtension(filename) @@ -43,53 +63,60 @@ local CoverImage = WidgetContainer:new{ is_doc_only = true, } +local default_cache_path = DataStorage:getDataDir() .. "/cache/cover_image.cache/" +local default_fallback_path = DataStorage:getDataDir() .. "/" + function CoverImage:init() - self.cover_image_path = G_reader_settings:readSetting("cover_image_path") or "cover.jpg" + self.cover_image_path = G_reader_settings:readSetting("cover_image_path") or Device:getDefaultCoverPath() self.cover_image_format = G_reader_settings:readSetting("cover_image_format") or "auto" - self.cover_image_extension = getExtension(self.cover_image_path) self.cover_image_quality = G_reader_settings:readSetting("cover_image_quality") or 75 - self.cover_image_stretch_limit = G_reader_settings:readSetting("cover_image_stretch_limit") or 5 + self.cover_image_stretch_limit = G_reader_settings:readSetting("cover_image_stretch_limit") or 8 self.cover_image_background = G_reader_settings:readSetting("cover_image_background") or "black" - self.cover_image_fallback_path = G_reader_settings:readSetting("cover_image_fallback_path") or "cover_fallback.png" - self.enabled = G_reader_settings:isTrue("cover_image_enabled") + self.cover_image_fallback_path = G_reader_settings:readSetting("cover_image_fallback_path") or default_fallback_path + self.cover_image_cache_path = G_reader_settings:readSetting("cover_image_cache_path") or default_cache_path + self.cover_image_cache_maxfiles = G_reader_settings:readSetting("cover_image_cache_maxfiles") or 36 + self.cover_image_cache_maxsize = G_reader_settings:readSetting("cover_image_cache_maxsize") or 5 -- MiB + self.cover_image_cache_prefix = "cover_" + self.cover = G_reader_settings:isTrue("cover_image_enabled") self.fallback = G_reader_settings:isTrue("cover_image_fallback") - self.ui.menu:registerToMainMenu(self) -end - -function CoverImage:_enabled() - return self.enabled -end + lfs.mkdir(self.cover_image_cache_path) -function CoverImage:_fallback() - return self.fallback + self.ui.menu:registerToMainMenu(self) end function CoverImage:cleanUpImage() - if self.cover_image_fallback_path == "" or not self.fallback then + if self.cover_image_fallback_path == "" or not self:fallbackEnabled() then os.remove(self.cover_image_path) elseif lfs.attributes(self.cover_image_fallback_path, "mode") ~= "file" then UIManager:show(InfoMessage:new{ - text = T(_("\"%1\" \nis not a valid image file!\nA valid fallback image is required in Cover-Image."), self.cover_image_fallback_path), + text = T(_("\"%1\"\nis not a valid image file!\nA valid fallback image is required in Cover-Image."), self.cover_image_fallback_path), show_icon = true, timeout = 10, }) os.remove(self.cover_image_path) - elseif pathOk(self.cover_image_path) then + elseif isFileOk(self.cover_image_path) then ffiutil.copyFile(self.cover_image_fallback_path, self.cover_image_path) end end function CoverImage:createCoverImage(doc_settings) - if self.enabled and doc_settings:nilOrFalse("exclude_cover_image") then + if self:coverEnabled() and doc_settings:nilOrFalse("exclude_cover_image") then local cover_image = self.ui.document:getCoverPageImage() if cover_image then + local cache_file = self:getCacheFile() + if lfs.attributes(cache_file, "mode") == "file" then + ffiutil.copyFile(cache_file, self.cover_image_path) + lfs.touch(cache_file) -- update date + return + end + local s_w, s_h = Device.screen:getWidth(), Device.screen:getHeight() local i_w, i_h = cover_image:getWidth(), cover_image:getHeight() local scale_factor = math.min(s_w / i_w, s_h / i_h) if self.cover_image_background == "none" or scale_factor == 1 then - local act_format = self.cover_image_format == "auto" and self.cover_image_extension or self.cover_image_format + local act_format = self.cover_image_format == "auto" and getExtension(self.cover_image_path) or self.cover_image_format if not cover_image:writeToFile(self.cover_image_path, act_format, self.cover_image_quality) then UIManager:show(InfoMessage:new{ text = _("Error writing file") .. "\n" .. self.cover_image_path, @@ -97,6 +124,8 @@ function CoverImage:createCoverImage(doc_settings) }) end cover_image:free() + ffiutil.copyFile(self.cover_image_path, cache_file) + self:cleanCache() return end @@ -132,7 +161,7 @@ function CoverImage:createCoverImage(doc_settings) cover_image:free() - local act_format = self.cover_image_format == "auto" and self.cover_image_extension or self.cover_image_format + local act_format = self.cover_image_format == "auto" and getExtension(self.cover_image_path) or self.cover_image_format if not image:writeToFile(self.cover_image_path, act_format, self.cover_image_quality) then UIManager:show(InfoMessage:new{ text = _("Error writing file") .. "\n" .. self.cover_image_path, @@ -142,13 +171,16 @@ function CoverImage:createCoverImage(doc_settings) image:free() logger.dbg("CoverImage: image written to " .. self.cover_image_path) + + ffiutil.copyFile(self.cover_image_path, cache_file) + self:cleanCache() end end end function CoverImage:onCloseDocument() logger.dbg("CoverImage: onCloseDocument") - if self.fallback then + if self:fallbackEnabled() then self:cleanUpImage() end end @@ -158,105 +190,515 @@ function CoverImage:onReaderReady(doc_settings) self:createCoverImage(doc_settings) end +function CoverImage:fallbackEnabled() + return self.fallback and isFileOk(self.cover_image_fallback_path) +end + +function CoverImage:coverEnabled() + return self.cover and isFileOk(self.cover_image_path) +end + +--------------------------- +-- cache handling functions +--------------------------- + +function CoverImage:getCacheFile() + local dummy, document_name = util.splitFilePathName(self.ui.document.file) + -- use document_name here. Title may contain characters not allowed on every filesystem (esp. vfat on /sdcard) + local key = document_name .. "_" .. self.cover_image_quality .. "_" .. self.cover_image_stretch_limit .. "_" + .. self.cover_image_background .. "_" .. self.cover_image_format + + return self.cover_image_cache_path .. self.cover_image_cache_prefix .. md5(key) .. "." .. getExtension(self.cover_image_path) +end + +function CoverImage:emptyCache() + for entry in lfs.dir(self.cover_image_cache_path) do + if entry ~= "." and entry ~= ".." then + local file = self.cover_image_cache_path .. entry + if entry:sub(1, self.cover_image_cache_prefix:len()) == self.cover_image_cache_prefix + and lfs.attributes(file, "mode") == "file" then + os.remove(file) + end + end + end +end + +function CoverImage:getCacheFiles(cache_path, cache_prefix) + local cache_count = 0 + local cache_size_KiB = 0 + local files = {} + for entry in lfs.dir(self.cover_image_cache_path) do + if entry ~= "." and entry ~= ".." then + local file = cache_path .. entry + if entry:sub(1, self.cover_image_cache_prefix:len()) == cache_prefix + and lfs.attributes(file, "mode") == "file" then + cache_count = cache_count + 1 + files[cache_count] = { + name = file, + size = math.floor((lfs.attributes(file).size + 1023) / 1024), -- round up to KiB + mod = lfs.attributes(file).modification, + } + cache_size_KiB = cache_size_KiB + files[cache_count].size -- size in KiB + end + end + end + logger.dbg("CoverImage: start - cache size: ".. cache_size_KiB .. " KiB, cached files: " .. cache_count) + return cache_count, cache_size_KiB, files +end + +function CoverImage:cleanCache() + if not self:isCacheEnabled() then + self:emptyCache() + return + end + + local cache_count, cache_size_KiB, files = self:getCacheFiles(self.cover_image_cache_path, self.cover_image_cache_prefix) + + -- delete the oldest files first + table.sort(files, function(a, b) return a.mod < b.mod end) + local index = 1 + while (cache_count > self.cover_image_cache_maxfiles and self.cover_image_cache_maxfiles ~= 0) + or (cache_size_KiB > self.cover_image_cache_maxsize * 1024 and self.cover_image_cache_maxsize ~= 0) + and index <= #files do + os.remove(files[index].name) + cache_count = cache_count - 1 + cache_size_KiB = cache_size_KiB - files[index].size + index = index + 1 + end + logger.dbg("CoverImage: clean - cache size: ".. cache_size_KiB .. " KiB, cached files: " .. cache_count) +end + +function CoverImage:isCacheEnabled(path) + if not path then + path = self.cover_image_cache_path + end + + return self.cover_image_cache_maxfiles >= 0 and self.cover_image_cache_maxsize >= 0 + and lfs.attributes(path, "mode") == "directory" and isPathAllowed(path) +end + +-- callback for choosePathFile() +function CoverImage:migrateCache(old_path, new_path) + if old_path == new_path or not self:isCacheEnabled(new_path) then + return + end + for entry in lfs.dir(old_path) do + if entry ~= "." and entry ~= ".." then + local old_file = old_path .. entry + if lfs.attributes(old_file, "mode") == "file" and entry:sub(1, self.cover_image_cache_prefix:len()) == self.cover_image_cache_prefix then + local old_access_time = lfs.attributes(old_file, "access") + local new_file = new_path .. entry + os.rename(old_file, new_file) + lfs.touch(new_file, old_access_time) -- restore original time + end + end + end +end + +-- callback for choosePathFile() +function CoverImage:migrateCover(old_file, new_file) + if old_file ~= new_file then + os.rename(old_file, new_file) + end +end + +--[[-- +chooses a path or (an existing) file + +@touchmenu_instance for updating of the menu +@string key is the G_reader_setting key which is used and changed +@boolean folder_only just selects a path, no file handling +@boolean new_file allows to enter a new filename, or use just an existing file +@function migrate(a,b) callback to a function to mangle old folder/file with new folder/file. + Can be used for migrating the contents of the old path to the new one +]] +function CoverImage:choosePathFile(touchmenu_instance, key, folder_only, new_file, migrate) + local old_path, dummy = util.splitFilePathName(self[key]) + UIManager:show(PathChooser:new{ + select_directory = folder_only or new_file, + select_file = not folder_only, + height = Screen:getHeight(), + path = old_path, + onConfirm = function(dir_path) + local mode = lfs.attributes(dir_path, "mode") + if folder_only then -- just select a folder + if not dir_path:find("/$") then + dir_path = dir_path .. "/" + end + if migrate then + migrate(self, self[key], dir_path) + end + self[key] = dir_path + G_reader_settings:saveSetting(key, dir_path) + if touchmenu_instance then + touchmenu_instance:updateItems() + end + elseif new_file and mode == "directory" then -- new filename should be entered or a file could be selected + local file_input + file_input = InputDialog:new{ + title = _("Append filename"), + input = dir_path .. "/", + buttons = {{ + { + text = _("Cancel"), + callback = function() + UIManager:close(file_input) + end, + }, + { + text = _("Save"), + callback = function() + local file = file_input:getInputText() + if migrate and self[key] and self[key] ~= "" then + migrate(self, self[key], file) + end + self[key] = file + G_reader_settings:saveSetting(key, file) + if touchmenu_instance then + touchmenu_instance:updateItems() + end + UIManager:close(file_input) + end, + }, + }}, + } + UIManager:show(file_input) + file_input:onShowKeyboard() + elseif mode == "file" then -- just select an existing file + if migrate then + migrate(self, self[key], dir_path) + end + self[key] = dir_path + G_reader_settings:saveSetting(key, dir_path) + if touchmenu_instance then + touchmenu_instance:updateItems() + end + end + end, + }) +end + +--[[-- +Update a specific G_reader_setting's value via a Spinner + +@touchmenu_instance used for updating the menu +@string setting is the G_reader_setting key which is used and changed +@string title shown in the spinner +@int min minimum value of the spinner +@int max maximum value of the spinner +@int default default value of the spinner +@function callback to call, when spinner changed the value +]] +function CoverImage:sizeSpinner(touchmenu_instance, setting, title, min, max, default, callback) + local SpinWidget = require("ui/widget/spinwidget") + local old_val = self[setting] + UIManager:show(SpinWidget:new{ + width = math.floor(Device.screen:getWidth() * 0.6), + value = old_val, + value_min = min, + value_max = max, + default_value = default, + title_text = title, + ok_text = _("Set"), + callback = function(spin) + if self:coverEnabled() and spin.value ~= old_val then + self[setting] = spin.value + G_reader_settings:saveSetting(setting, self[setting]) + if callback then + callback(self) + end + end + if touchmenu_instance then touchmenu_instance:updateItems() end + end + }) +end + +-------------- menus and longer texts ----------- + + local about_text = _([[ -This plugin saves the current book cover to a file. That file can be used as a screensaver on certain Android devices, such as Tolinos. +This plugin saves a book cover to a file. That file can then be used as a screensaver on certain devices. + +If enabled, the cover image of the current file is stored in the set path on book opening. Books can be excluded if desired. -If enabled, the cover image of the actual file is stored in the selected screensaver file. Books can be excluded if desired. +If disabled, the cover file will be deleted. -If fallback is activated, the fallback file will be copied to the screensaver file on book closing. -If the filename is empty or the file doesn't exist, the cover file will be deleted and the system screensaver will be used instead. +If fallback is enabled, the fallback file will be copied to the screensaver file on book closing. +If the filename is empty or the file doesn't exist, the cover file will be deleted. -If the fallback image isn't activated, the screensaver image will stay in place after closing a book.]]) +If fallback is disabled, the screensaver image will stay in place after closing a book.]]) +local set_image_text = _([[ +You can either choose an existing file: +- Select a file + +or specify a new file: +- First select a directory +- Then add the name of the new file + +or delete the path: +- First select a directory +- Clear the name of the file]]) + +-- menu entry: Cache settings +function CoverImage:menu_entry_cache() + return { + text = _("Cache settings"), + checked_func = function() + return self:isCacheEnabled() + end, + sub_item_table = { + { + text_func = function() + local number + if self.cover_image_cache_maxfiles > 0 then + number = self.cover_image_cache_maxfiles + elseif self.cover_image_cache_maxfiles == 0 then + number = _("unlimited") + else + number = _("off") + end + return T(_("Maximum number of cached covers (%1)"), number) + end, + help_text = _("If set to zero the number of cache files is unlimited.\nIf set to -1 the cache is disabled."), + checked_func = function() + return self.cover_image_cache_maxfiles >= 0 + end, + callback = function(touchmenu_instance) + self:sizeSpinner(touchmenu_instance, "cover_image_cache_maxfiles", _("Number of covers"), -1, 100, 36, self.cleanCache) + end, + }, + { + text_func = function() + local number + if self.cover_image_cache_maxsize > 0 then + number = self.cover_image_cache_maxsize + elseif self.cover_image_cache_maxsize == 0 then + number = _("unlimited") + else + number = _("off") + end + return T(_("Maximum size of cached covers (%1MiB)"), number) + end, + help_text = _("If set to zero the cache size is unlimited.\nIf set to -1 the cache is disabled."), + checked_func = function() + return self.cover_image_cache_maxsize >= 0 + end, + callback = function(touchmenu_instance) + self:sizeSpinner(touchmenu_instance, "cover_image_cache_maxsize", _("Cache size"), -1, 100, 5, self.cleanCache) + end, + }, + self:menu_entry_set_path("cover_image_cache_path", _("Cover cache folder"), _("Current cache path:\n%1"), + ("Select a cache folder. The contents of the old folder will be migrated."), default_cache_path, true, false, self.migrateCache), + { + text = _("Clear cached covers"), + help_text_func = function() + local cache_count, cache_size_KiB + = self:getCacheFiles(self.cover_image_cache_path, self.cover_image_cache_prefix) + return T(_("The cache contains %1 files and uses %2 MiB."), cache_count, math.floor((cache_size_KiB + 1023) / 1024)) + end, + callback = function() + UIManager:show(ConfirmBox:new{ + text = _("Cear the cover image cache?"), + ok_text = _("Clear"), + ok_callback = function() + self:emptyCache() + end, + }) + end, + keep_menu_open = true, + }, + }, + } +end + +--[[-- +Menu entry for setting an specific G_reader_setting key for a path/file + +@string key is the G_reader_setting key which is used and changed +@string title shown in the menu +@string help shown in the menu +@string info shown in the menu (if containing %1, the value of the key is shown) +@string the default value +@bool folder_only sets if only folders can be selected +@bool new_file sets if a new filename can be entered +@function migrate a callback for example moving the folder contents +]] +function CoverImage:menu_entry_set_path(key, title, help, info, default, folder_only, new_file, migrate) + return { + text = title, + help_text_func = function() + local text = self[key] + text = text ~= "" and text or _("not set") + return T(help, text) + end, + checked_func = function() + return isFileOk(self[key]) or (isPathAllowed(self[key]) and folder_only) + end, + callback = function(touchmenu_instance) + UIManager:show(ConfirmBox:new{ + text = info, + ok_callback = function() + self:choosePathFile(touchmenu_instance, key, folder_only, new_file, migrate) + end, + other_buttons = {{ + { + text = _("Default"), + callback = function() + if migrate then + migrate(self, self[key],default) + end + self[key] = default + G_reader_settings:saveSetting(key, default) + if touchmenu_instance then + touchmenu_instance:updateItems() + end + end + + } + }}, + }) + end, + } +end + +function CoverImage:menu_entry_format(title, format) + return { + text = title, + checked_func = function() + return self.cover_image_format == format + end, + callback = function() + local old_cover_image_format = self.cover_image_format + self.cover_image_format = format + G_reader_settings:saveSetting("cover_image_format", format) + if self:coverEnabld() and old_cover_image_format ~= format then + self:createCoverImage(self.ui.doc_settings) + end + end, + } +end + +function CoverImage:menu_entry_background(color) + return { + text = _("Fit to screen, " .. color .. " background"), + checked_func = function() + return self.cover_image_background == color + end, + callback = function() + local old_background = self.cover_image_background + self.cover_image_background = color + G_reader_settings:saveSetting("cover_image_background", self.cover_image_background) + if self:coverEnabled() and old_background ~= self.cover_image_background then + self:createCoverImage(self.ui.doc_settings) + end + end, + } +end + +-- menu entry: scale, background, format +function CoverImage:menu_entry_sbf() + return { + text = _("Size, background and format"), + enabled_func = function() + return self:coverEnabled() + end, + sub_item_table = { + { + text_func = function() + return T(_("Aspect ratio stretch threshold (%1)"), + self.cover_image_stretch_limit ~= 0 and self.cover_image_stretch_limit .."%" or "off") + end, + keep_menu_open = true, + help_text_func = function() + return T(_("If the image and the screen have a similar aspect ratio (±%1%), stretch the image instead of keeping its aspect ratio."), self.cover_image_stretch_limit ) + end, + callback = function(touchmenu_instance) + local function createCover() + self:createCoverImage(self.ui.doc_settings) + end + self:sizeSpinner(touchmenu_instance, "cover_image_stretch_limit", _("Set strech threshold"), 0, 20, 8, createCover) + end, + }, + self:menu_entry_background("black"), + self:menu_entry_background("white"), + self:menu_entry_background("gray"), + { + text = _("Original image"), + checked_func = function() + return self.cover_image_background == "none" + end, + callback = function() + local old_background = self.cover_image_background + self.cover_image_background = "none" + G_reader_settings:saveSetting("cover_image_background", self.cover_image_background) + if self:coverEnabled() and old_background ~= self.cover_image_background then + self:createCoverImage(self.ui.doc_settings) + end + end, + separator = true, + }, + -- menu entries: File format + { + text = _("File format derived from filename"), + help_text = _("If the file format is not supported, then JPG will be used."), + checked_func = function() + return self.cover_image_format == "auto" + end, + callback = function() + local old_cover_image_format = self.cover_image_format + self.cover_image_format = "auto" + G_reader_settings:saveSetting("cover_image_format", self.cover_image_format) + if self:coverEnabled() and old_cover_image_format ~= self.cover_image_format then + self:createCoverImage(self.ui.doc_settings) + end + end, + }, + self:menu_entry_format(_("JPG file format"), "jpg"), + self:menu_entry_format(_("PNG file format"), "png"), + self:menu_entry_format(_("BMP file format"), "bmp"), + }, + } +end + +-- CoverImage main menu function CoverImage:addToMainMenu(menu_items) menu_items.coverimage = { sorting_hint = "screen", text = _("Cover image"), checked_func = function() - return self.enabled or self.fallback + return self:coverEnabled() or self:fallbackEnabled() end, sub_item_table = { -- menu entry: about cover image { text = _("About cover image"), - keep_menu_open = true, callback = function() UIManager:show(InfoMessage:new{ text = about_text, }) end, + keep_menu_open = true, separator = true, }, -- menu entry: filename dialog - { - text = _("Set image path"), - checked_func = function() - return self.cover_image_path ~= "" and pathOk(self.cover_image_path) - end, - help_text = _("The cover of the current book will be stored in this file."), - keep_menu_open = true, - callback = function(menu) - local InputDialog = require("ui/widget/inputdialog") - local sample_input - sample_input = InputDialog:new{ - title = _("Screensaver image filename"), - input = self.cover_image_path, - input_type = "string", - description = _("You can enter the filename of the cover image here."), - buttons = { - { - { - text = _("Cancel"), - callback = function() - UIManager:close(sample_input) - end, - }, - { - text = _("Save"), - is_enter_default = true, - callback = function() - local new_cover_image_path = sample_input:getInputText() - if new_cover_image_path ~= self.cover_image_path then - self:cleanUpImage() -- with old filename - self.cover_image_path = new_cover_image_path -- update filename - G_reader_settings:saveSetting("cover_image_path", self.cover_image_path) - local is_path_ok, is_path_ok_message = pathOk(self.cover_image_path) - if self.cover_image_path ~= "" and is_path_ok then - self:createCoverImage(self.ui.doc_settings) -- with new filename - else - self.enabled = false - UIManager:show(InfoMessage:new{ - text = is_path_ok_message, - show_icon = true, - }) - end - end - self.cover_image_extension = getExtension(self.cover_image_path) - UIManager:close(sample_input) - menu:updateItems() - end, - }, - } - }, - } - UIManager:show(sample_input) - sample_input:onShowKeyboard() - end, - }, + self:menu_entry_set_path("cover_image_path", _("Set image path"), _("Current Cover image path:\n%1"), set_image_text, + Device:getDefaultCoverPath(), false, true, self.migrateCover), -- menu entry: enable { text = _("Save cover image"), checked_func = function() - return self:_enabled() and pathOk(self.cover_image_path) + return self:coverEnabled() end, enabled_func = function() - return self.cover_image_path ~= "" and pathOk(self.cover_image_path) + return self.cover_image_path ~= "" and isFileOk(self.cover_image_path) end, callback = function() if self.cover_image_path ~= "" then - self.enabled = not self.enabled - G_reader_settings:saveSetting("cover_image_enabled", self.enabled) - if self.enabled then + self.cover = not self.cover + self.cover = self.cover and self:coverEnabled() + G_reader_settings:saveSetting("cover_image_enabled", self.cover) + if self:coverEnabled() then self:createCoverImage(self.ui.doc_settings) else self:cleanUpImage() @@ -264,164 +706,8 @@ function CoverImage:addToMainMenu(menu_items) end end, }, - -- menu entry: scale book cover - { - text = _("Size, background and format"), - enabled_func = function() - return self.enabled - end, - sub_item_table = { - { - text_func = function() - return T(_("Aspect ratio stretch threshold (%1%)"), self.cover_image_stretch_limit ) - end, - help_text_func = function() - return T(_("If the image and the screen have a similar aspect ratio (±%1%), stretch the image instead of keeping its aspect ratio."), self.cover_image_stretch_limit ) - end, - keep_menu_open = true, - callback = function(touchmenu_instance) - local old_stretch_limit = self.cover_image_stretch_limit - local SpinWidget = require("ui/widget/spinwidget") - local size_spinner = SpinWidget:new{ - width = math.floor(Device.screen:getWidth() * 0.6), - value = old_stretch_limit, - value_min = 0, - value_max = 25, - default_value = 5, - title_text = _("Set stretch threshold"), - ok_text = _("Set"), - callback = function(spin) - if self.enabled and spin.value ~= old_stretch_limit then - self.cover_image_stretch_limit = spin.value - G_reader_settings:saveSetting("cover_image_stretch_limit", self.cover_image_stretch_limit) - self:createCoverImage(self.ui.doc_settings) - end - if touchmenu_instance then touchmenu_instance:updateItems() end - end - } - UIManager:show(size_spinner) - if self.enabled and old_stretch_limit ~= self.cover_image_stretch_limit then - self:createCoverImage(self.ui.doc_settings) - end - end, - }, - { - text = _("Fit to screen, black background"), - checked_func = function() - return self.cover_image_background == "black" - end, - callback = function() - local old_background = self.cover_image_background - self.cover_image_background = "black" - G_reader_settings:saveSetting("cover_image_background", self.cover_image_background) - if self.enabled and old_background ~= self.cover_image_background then - self:createCoverImage(self.ui.doc_settings) - end - end, - }, - { - text = _("Fit to screen, white background"), - checked_func = function() - return self.cover_image_background == "white" - end, - callback = function() - local old_background = self.cover_image_background - self.cover_image_background = "white" - G_reader_settings:saveSetting("cover_image_background", self.cover_image_background) - if self.enabled and old_background ~= self.cover_image_background then - self:createCoverImage(self.ui.doc_settings) - end - end, - }, - { - text = _("Fit to screen, gray background"), - checked_func = function() - return self.cover_image_background == "gray" - end, - callback = function() - local old_background = self.cover_image_background - self.cover_image_background = "gray" - G_reader_settings:saveSetting("cover_image_background", self.cover_image_background) - if self.enabled and old_background ~= self.cover_image_background then - self:createCoverImage(self.ui.doc_settings) - end - end, - }, - { - text = _("Original image"), - checked_func = function() - return self.cover_image_background == "none" - end, - callback = function() - local old_background = self.cover_image_background - self.cover_image_background = "none" - G_reader_settings:saveSetting("cover_image_background", self.cover_image_background) - if self.enabled and old_background ~= self.cover_image_background then - self:createCoverImage(self.ui.doc_settings) - end - end, - separator = true, - }, - -- menu entries: File format - { - text = _("File format derived from filename"), - help_text = _("If the file format is not supported, then JPG will be used."), - checked_func = function() - return self.cover_image_format == "auto" - end, - callback = function() - local old_cover_image_format = self.cover_image_format - self.cover_image_format = "auto" - G_reader_settings:saveSetting("cover_image_format", self.cover_image_format) - if self.enabled and old_cover_image_format ~= self.cover_image_format then - self:createCoverImage(self.ui.doc_settings) - end - end, - }, - { - text = _("JPG file format"), - checked_func = function() - return self.cover_image_format == "jpg" - end, - callback = function() - local old_cover_image_format = self.cover_image_format - self.cover_image_format = "jpg" - G_reader_settings:saveSetting("cover_image_format", self.cover_image_format) - if self.enabled and old_cover_image_format ~= self.cover_image_format then - self:createCoverImage(self.ui.doc_settings) - end - end, - }, - { - text = _("PNG file format"), - checked_func = function() - return self.cover_image_format == "png" - end, - callback = function() - local old_cover_image_format = self.cover_image_format - self.cover_image_format = "png" - G_reader_settings:saveSetting("cover_image_format", self.cover_image_format) - if self.enabled and old_cover_image_format ~= self.cover_image_format then - self:createCoverImage(self.ui.doc_settings) - end - end, - }, - { - text = _("BMP file format"), - checked_func = function() - return self.cover_image_format == "bmp" - end, - callback = function() - local old_cover_image_format = self.cover_image_format - self.cover_image_format = "bmp" - G_reader_settings:saveSetting("cover_image_format", self.cover_image_format) - if self.enabled and old_cover_image_format ~= self.cover_image_format then - self:createCoverImage(self.ui.doc_settings) - end - end, - }, - } - }, + -- menu entry: scale, background, format + self:menu_entry_sbf(), -- menu entry: exclude this cover { text = _("Exclude this book cover"), @@ -441,66 +727,29 @@ function CoverImage:addToMainMenu(menu_items) separator = true, }, -- menu entry: set fallback image - { - text = _("Set fallback image path"), - checked_func = function() - return lfs.attributes(self.cover_image_fallback_path, "mode") == "file" - end, - keep_menu_open = true, - callback = function(menu) - local InputDialog = require("ui/widget/inputdialog") - local sample_input - sample_input = InputDialog:new{ - title = _("Fallback image filename"), - input = self.cover_image_fallback_path, - input_type = "string", - description = _("Leave this empty to remove the cover when the document is closed."), - buttons = { - { - { - text = _("Cancel"), - callback = function() - UIManager:close(sample_input) - end, - }, - { - text = _("Save"), - is_enter_default = true, - callback = function() - self.cover_image_fallback_path = sample_input:getInputText() - G_reader_settings:saveSetting("cover_image_fallback_path", self.cover_image_fallback_path) - if lfs.attributes(self.cover_image_fallback_path, "mode") ~= "file" - and self.cover_image_fallback_path ~= "" then - UIManager:show(InfoMessage:new{ - text = T(_("\"%1\" \nis not a valid image file!\nA valid fallback image is required in Cover-Image"), - self.cover_image_fallback_path), - show_icon = true, - timeout = 10, - }) - end - UIManager:close(sample_input) - menu:updateItems() - end, - }, - } - }, - } - UIManager:show(sample_input) - sample_input:onShowKeyboard() - end, - }, + self:menu_entry_set_path("cover_image_fallback_path", _("Set fallback path"), + _("The fallback image used on document close is:\n%1"), _("You can select a fallback image."), default_fallback_path, false, false), -- menu entry: fallback { text = _("Turn on fallback image"), checked_func = function() - return self:_fallback() + return self:fallbackEnabled() + end, + enabled_func = function() + return lfs.attributes(self.cover_image_fallback_path, "mode") == "file" end, callback = function() self.fallback = not self.fallback + self.fallback = self.fallback and self:fallbackEnabled() G_reader_settings:saveSetting("cover_image_fallback", self.fallback) + if not self:coverEnabled() then + self:cleanUpImage() + end end, separator = true, }, + -- menu entry: Cache settings + self:menu_entry_cache(), }, } end