--[[ --------------------------------------------------------------------------- Folder Cover patch (custom .cover.* + auto cover fallback) with item-count badge bottom-right and with consistent edge inset (same offset logic in both axes). This version changes ONLY the nbitems_widget placement logic: - Keeps the badge in the bottom-right - Applies Folder.face.nb_items_margin as an inset from BOTH the right and bottom ----------------------------------------------------------------------------- ]] local AlphaContainer = require("ui/widget/container/alphacontainer") local BD = require("ui/bidi") local Blitbuffer = require("ffi/blitbuffer") local BottomContainer = require("ui/widget/container/bottomcontainer") local CenterContainer = require("ui/widget/container/centercontainer") local Device = require("device") local FileChooser = require("ui/widget/filechooser") local Font = require("ui/font") local FrameContainer = require("ui/widget/container/framecontainer") local ImageWidget = require("ui/widget/imagewidget") local LineWidget = require("ui/widget/linewidget") local OverlapGroup = require("ui/widget/overlapgroup") local RightContainer = require("ui/widget/container/rightcontainer") local Size = require("ui/size") local TextBoxWidget = require("ui/widget/textboxwidget") local TextWidget = require("ui/widget/textwidget") local TopContainer = require("ui/widget/container/topcontainer") local VerticalGroup = require("ui/widget/verticalgroup") local VerticalSpan = require("ui/widget/verticalspan") local userpatch = require("userpatch") local util = require("util") local _ = require("gettext") local Screen = Device.screen local logger = require("logger") --========================== [[ Folder Cover Tuning ]] ========================== -- Edge bars (above cover) local EDGE_THICKNESS = Screen:scaleBySize(4.5) -- thickness of the top/bottom edge bars local EDGE_MARGIN = Size.line.medium -- space between edge bars and other elements local EDGE_COLOR = Blitbuffer.COLOR_GRAY_4 -- color of the edge bars local EDGE_WIDTH_RATIO = 0.97 -- width of the edge bars relative to the cover width -- Cover face local COVER_BORDER_SIZE = Size.border.thin -- thickness of the cover border around the image local COVER_BANNER_ALPHA = 0.6 -- transparency of the folder name banner overlay local DIR_MAX_FONT_SIZE = 20 -- maximum font size for the folder name text --========================== [[ Number Badge Tuning ]] ========================== local NB_ITEMS_FONT_SIZE = 18 -- font size of the number badge text local NB_ITEMS_MARGIN = Screen:scaleBySize(5) -- inset distance of number badge from right & bottom edges local NB_BADGE_SIZE = Screen:scaleBySize(18) -- default size of the badge (width and height) local NB_BADGE_PADDING = Screen:scaleBySize(2) -- padding inside the badge frame local NB_BADGE_RADIUS = 6 -- corner radius for the badge local NB_BADGE_BG_COLOR = Blitbuffer.COLOR_WHITE -- background color of the badge local NB_BADGE_MARGIN = Screen:scaleBySize(5) -- inset from right and bottom edges --=============================================================================== local FolderCover = { name = ".cover", exts = { ".jpg", ".jpeg", ".png", ".webp", ".gif" }, } local function findCover(dir_path) local path = dir_path .. "/" .. FolderCover.name for i, ext in ipairs(FolderCover.exts) do local fname = path .. ext if util.fileExists(fname) then return fname end end end local function getMenuItem(menu, ...) -- path local function findItem(sub_items, texts) local find = {} local texts = type(texts) == "table" and texts or { texts } for i, text in ipairs(texts) do find[text] = true end for i, item in ipairs(sub_items) do local text = item.text or (item.text_func and item.text_func()) if text and find[text] then return item end end end local sub_items, item for i, texts in ipairs { ... } do -- walk path sub_items = (item or menu).sub_item_table if not sub_items then return end item = findItem(sub_items, texts) if not item then return end end return item end local function toKey(...) local keys = {} for i, key in pairs { ... } do if type(key) == "table" then table.insert(keys, "table") for k, v in pairs(key) do table.insert(keys, tostring(k)) table.insert(keys, tostring(v)) end else table.insert(keys, tostring(key)) end end return table.concat(keys, "") end local orig_FileChooser_getListItem = FileChooser.getListItem local cached_list = {} function FileChooser:getListItem(dirpath, f, fullpath, attributes, collate) local key = toKey(dirpath, f, fullpath, attributes, collate, self.show_filter.status) cached_list[key] = cached_list[key] or orig_FileChooser_getListItem(self, dirpath, f, fullpath, attributes, collate) return cached_list[key] end local function capitalize(sentence) local words = {} for word in sentence:gmatch("%S+") do table.insert(words, word:sub(1, 1):upper() .. word:sub(2):lower()) end return table.concat(words, " ") end local Folder = { edge = { thick = EDGE_THICKNESS, margin = EDGE_MARGIN, color = EDGE_COLOR, width = EDGE_WIDTH_RATIO, }, face = { border_size = COVER_BORDER_SIZE, alpha = COVER_BANNER_ALPHA, nb_items_font_size = NB_ITEMS_FONT_SIZE, nb_items_margin = NB_ITEMS_MARGIN, dir_max_font_size = DIR_MAX_FONT_SIZE, }, } local function patchCoverBrowser(plugin) local MosaicMenu = require("mosaicmenu") local MosaicMenuItem = userpatch.getUpValue(MosaicMenu._updateItemsBuildUI, "MosaicMenuItem") if not MosaicMenuItem then return end -- Protect against remnants of project title local BookInfoManager = userpatch.getUpValue(MosaicMenuItem.update, "BookInfoManager") local original_update = MosaicMenuItem.update -- setting function BooleanSetting(text, name, default) self = { text = text } self.get = function() local setting = BookInfoManager:getSetting(name) if default then return not setting end -- false is stored as nil, so we need our own logic for boolean default return setting end self.toggle = function() return BookInfoManager:toggleSetting(name) end return self end local settings = { crop_to_fit = BooleanSetting(_("Crop folder custom image"), "folder_crop_custom_image", true), name_centered = BooleanSetting(_("Folder name centered"), "folder_name_centered", true), show_folder_name = BooleanSetting(_("Show folder name"), "folder_name_show", true), } -- cover item function MosaicMenuItem:update(...) original_update(self, ...) if self._foldercover_processed or self.menu.no_refresh_covers or not self.do_cover_image then return end if self.entry.is_file or self.entry.file or not self.mandatory then return end -- it's a file local dir_path = self.entry and self.entry.path if not dir_path then return end self._foldercover_processed = true local cover_file = findCover(dir_path) -- custom if cover_file then local success, w, h = pcall(function() local tmp_img = ImageWidget:new { file = cover_file, scale_factor = 1 } tmp_img:_render() local orig_w = tmp_img:getOriginalWidth() local orig_h = tmp_img:getOriginalHeight() tmp_img:free() return orig_w, orig_h end) if success then self:_setFolderCover { file = cover_file, w = w, h = h, scale_to_fit = settings.crop_to_fit.get() } return end end self.menu._dummy = true local entries = self.menu:genItemTableFromPath(dir_path) -- sorted self.menu._dummy = false if not entries then return end for _, entry in ipairs(entries) do if entry.is_file or entry.file then local bookinfo = BookInfoManager:getBookInfo(entry.path, true) if bookinfo and bookinfo.cover_bb and bookinfo.has_cover and bookinfo.cover_fetched and not bookinfo.ignore_cover and not BookInfoManager.isCachedCoverInvalid(bookinfo, self.menu.cover_specs) then self:_setFolderCover { data = bookinfo.cover_bb, w = bookinfo.cover_w, h = bookinfo.cover_h } break end end end end function MosaicMenuItem:_setFolderCover(img) local top_h = 2 * (Folder.edge.thick + Folder.edge.margin) local target = { w = self.width - 2 * Folder.face.border_size, h = self.height - 2 * Folder.face.border_size - top_h, } local img_options = { file = img.file, image = img.data } if img.scale_to_fit then img_options.scale_factor = math.max(target.w / img.w, target.h / img.h) img_options.width = target.w img_options.height = target.h else img_options.scale_factor = math.min(target.w / img.w, target.h / img.h) end local image = ImageWidget:new(img_options) local size = image:getSize() local dimen = { w = size.w + 2 * Folder.face.border_size, h = size.h + 2 * Folder.face.border_size } local image_widget = FrameContainer:new { padding = 0, bordersize = Folder.face.border_size, image, overlap_align = "center", } local directory, nbitems = self:_getTextBoxes { w = size.w, h = size.h } local size_nb = nbitems:getSize() local nb_size = math.max(size_nb.w, size_nb.h) local folder_name_widget if settings.show_folder_name.get() then folder_name_widget = (settings.name_centered.get() and CenterContainer or TopContainer):new { dimen = dimen, FrameContainer:new { padding = 0, -- Makes text container larger than image if expanded bordersize = Folder.face.border_size, AlphaContainer:new { alpha = Folder.face.alpha, directory }, }, overlap_align = "center", } else folder_name_widget = VerticalSpan:new { width = 0 } end local nbitems_widget if tonumber(nbitems.text) ~= 0 then -- Inset from edges (right AND bottom) using the same margin value local inset = NB_BADGE_MARGIN nbitems_widget = BottomContainer:new { dimen = { w = dimen.w, h = dimen.h }, RightContainer:new { dimen = { w = dimen.w - inset, h = NB_BADGE_SIZE + inset * 2 + math.ceil(NB_BADGE_SIZE * 0.125), }, FrameContainer:new { padding = NB_BADGE_PADDING, radius = NB_BADGE_RADIUS, background = NB_BADGE_BG_COLOR, CenterContainer:new { dimen = { w = NB_BADGE_SIZE, h = NB_BADGE_SIZE }, nbitems }, }, }, overlap_align = "center", } else nbitems_widget = VerticalSpan:new { width = 0 } end local widget = CenterContainer:new { dimen = { w = self.width, h = self.height }, VerticalGroup:new { VerticalSpan:new { width = math.max(0, math.ceil((self.height - (top_h + dimen.h)) * 0.5)) }, LineWidget:new { background = Folder.edge.color, dimen = { w = math.floor(dimen.w * (Folder.edge.width ^ 2)), h = Folder.edge.thick }, }, VerticalSpan:new { width = Folder.edge.margin }, LineWidget:new { background = Folder.edge.color, dimen = { w = math.floor(dimen.w * Folder.edge.width), h = Folder.edge.thick }, }, VerticalSpan:new { width = Folder.edge.margin }, OverlapGroup:new { dimen = { w = self.width, h = self.height - top_h }, image_widget, folder_name_widget, nbitems_widget, }, }, } if self._underline_container[1] then local previous_widget = self._underline_container[1] previous_widget:free() end self._underline_container[1] = widget end function MosaicMenuItem:_getTextBoxes(dimen_in) local nbitems = TextWidget:new { text = self.mandatory:match("(%d+)") or "", -- nb books face = Font:getFace("cfont", Folder.face.nb_items_font_size), bold = true, padding = 0, } local text = self.text if text:match("/$") then text = text:sub(1, -2) end -- remove "/" text = BD.directory(capitalize(text)) local available_height = dimen_in.h - 2 * nbitems:getSize().h local dir_font_size = Folder.face.dir_max_font_size local directory while true do if directory then directory:free(true) end directory = TextBoxWidget:new { text = text, face = Font:getFace("cfont", dir_font_size), width = dimen_in.w, alignment = "center", bold = true, } if directory:getSize().h <= available_height then break end dir_font_size = dir_font_size - 1 if dir_font_size < 10 then -- don't go too low directory:free() directory.height = available_height directory.height_adjust = true directory.height_overflow_show_ellipsis = true directory:init() break end end return directory, nbitems end -- menu local orig_CoverBrowser_addToMainMenu = plugin.addToMainMenu function plugin:addToMainMenu(menu_items) orig_CoverBrowser_addToMainMenu(self, menu_items) if menu_items.filebrowser_settings == nil then return end local item = getMenuItem(menu_items.filebrowser_settings, _("Mosaic and detailed list settings")) if item then item.sub_item_table[#item.sub_item_table].separator = true for i, setting in pairs(settings) do if not getMenuItem(menu_items.filebrowser_settings, _("Mosaic and detailed list settings"), setting.text) then table.insert(item.sub_item_table, { text = setting.text, checked_func = function() return setting.get() end, callback = function() setting.toggle() self.ui.file_chooser:updateItems() end, }) end end end end end userpatch.registerPatchPluginFunc("coverbrowser", patchCoverBrowser)