commit 1825bb4a51a6423fcd11762bd42d2fc4f99abbb9 Author: Lucas Date: Thu Feb 12 00:40:52 2026 -0700 Upload modified patch with original README from GitHub diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..6ffce1d Binary files /dev/null and b/.DS_Store differ diff --git a/2-browser-folder-cover.lua b/2-browser-folder-cover.lua new file mode 100755 index 0000000..2df8964 --- /dev/null +++ b/2-browser-folder-cover.lua @@ -0,0 +1,382 @@ +--[[ --------------------------------------------------------------------------- +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) +local EDGE_MARGIN = Size.line.medium +local EDGE_COLOR = Blitbuffer.COLOR_GRAY_4 +local EDGE_WIDTH_RATIO = 0.97 + +-- Cover face +local COVER_BORDER_SIZE = Size.border.thin +local COVER_BANNER_ALPHA = 0.6 +local NB_ITEMS_FONT_SIZE = 18 +local NB_ITEMS_MARGIN = Screen:scaleBySize(5) +local DIR_MAX_FONT_SIZE = 20 + +--=============================================================================== + +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, + 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 = Folder.face.nb_items_margin + + nbitems_widget = BottomContainer:new { + -- shrink bottom anchoring area so the badge sits inset upward by `inset` + dimen = { w = dimen.w, h = dimen.h - inset }, + + RightContainer:new { + -- shrink right anchoring area so the badge sits inset leftward by `inset` + dimen = { + w = dimen.w - inset, + h = nb_size + inset * 2 + math.ceil(nb_size * 0.125), + }, + + FrameContainer:new { + padding = Screen:scaleBySize(2), + radius = 6, + background = Blitbuffer.COLOR_WHITE, + CenterContainer:new { dimen = { w = nb_size, h = nb_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) \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2155a7d --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ + ### [🞂 2-browser-folder-cover](2-browser-folder-cover.lua) + +This patch adds images to the mosaic folder entries: it uses the first cover according to the current sorting chosen by the user. + +If you want to use your own folder cover, please add an image file in the folder named `.cover.jpg`, `.cover.jpeg`, `.cover.png`, `.cover.webp`, or `.cover.gif`. + + + +You'll find its options under **🞂 Settings 🞂 Mosaic and detailed list settings 🞂 Folder name centered** and **Crop folder custom image** and **Show folder name** + diff --git a/img/cover_folder.png b/img/cover_folder.png new file mode 100644 index 0000000..1a3a4de Binary files /dev/null and b/img/cover_folder.png differ