Upload modified patch with original README from GitHub

This commit is contained in:
Lucas
2026-02-12 00:40:52 -07:00
commit 1825bb4a51
4 changed files with 392 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

382
2-browser-folder-cover.lua Executable file
View File

@@ -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)

10
README.md Normal file
View File

@@ -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`.
<img src="img/cover_folder.png" style="width:40%; height:auto;">
You'll find its options under <sub><img src="img/appbar.navigation.svg" style="width:2%; height:auto;"></sub> **🞂 Settings 🞂 Mosaic and detailed list settings 🞂 Folder name centered** and **Crop folder custom image** and **Show folder name**

BIN
img/cover_folder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB