Upload modified patch with original README from GitHub
This commit is contained in:
382
2-browser-folder-cover.lua
Executable file
382
2-browser-folder-cover.lua
Executable 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
10
README.md
Normal 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
BIN
img/cover_folder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 185 KiB |
Reference in New Issue
Block a user