Files
KOReader-Cover-Folder/2-browser-folder-cover.lua

391 lines
16 KiB
Lua
Raw Normal View History

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