You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
koreader/frontend/apps/filemanager/filemanagerbookinfo.lua

457 lines
17 KiB
Lua

--[[--
This module provides a way to display book information (filename and book metadata)
]]
local BD = require("ui/bidi")
local ButtonDialog = require("ui/widget/buttondialog")
local DocSettings = require("docsettings")
local Document = require("document/document")
local DocumentRegistry = require("document/documentregistry")
local InfoMessage = require("ui/widget/infomessage")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local ffiutil = require("ffi/util")
local filemanagerutil = require("apps/filemanager/filemanagerutil")
local lfs = require("libs/libkoreader-lfs")
local util = require("util")
local _ = require("gettext")
local Screen = require("device").screen
local BookInfo = WidgetContainer:extend{
props = {
"title",
"authors",
"series",
"series_index",
"language",
"keywords",
"description",
},
}
function BookInfo:init()
if self.ui then -- only for Reader menu
self.ui.menu:registerToMainMenu(self)
end
end
function BookInfo:addToMainMenu(menu_items)
menu_items.book_info = {
text = _("Book information"),
callback = function()
self:onShowBookInfo()
end,
}
end
-- Shows book information.
function BookInfo:show(file, book_props, metadata_updated_caller_callback)
self.updated = nil
local kv_pairs = {}
-- File section
local folder, filename = util.splitFilePathName(file)
local __, filetype = filemanagerutil.splitFileNameType(filename)
local attr = lfs.attributes(file)
local file_size = attr.size or 0
local size_f = util.getFriendlySize(file_size)
local size_b = util.getFormattedSize(file_size)
table.insert(kv_pairs, { _("Filename:"), BD.filename(filename) })
table.insert(kv_pairs, { _("Format:"), filetype:upper() })
table.insert(kv_pairs, { _("Size:"), string.format("%s (%s bytes)", size_f, size_b) })
table.insert(kv_pairs, { _("File date:"), os.date("%Y-%m-%d %H:%M:%S", attr.modification) })
table.insert(kv_pairs, { _("Folder:"), BD.dirpath(filemanagerutil.abbreviate(folder)), separator = true })
-- Book section
-- book_props may be provided if caller already has them available
-- but it may lack "pages", that we may get from sidecar file
if not book_props or not book_props.pages then
book_props = BookInfo.getDocProps(nil, file, book_props)
end
local values_lang
local prop_text = {
title = _("Title:"),
authors = _("Authors:"),
series = _("Series:"),
series_index = _("Series index:"),
pages = _("Pages:"), -- not in document metadata
language = _("Language:"),
keywords = _("Keywords:"),
description = _("Description:"),
}
for _i, prop_key in ipairs(self.props) do
local prop = book_props[prop_key]
if prop == nil or prop == "" then
prop = _("N/A")
elseif prop_key == "title" then
prop = BD.auto(prop)
elseif prop_key == "authors" or prop_key == "keywords" then
if prop:find("\n") then -- BD auto isolate each entry
prop = util.splitToArray(prop, "\n")
for i = 1, #prop do
prop[i] = BD.auto(prop[i])
end
prop = table.concat(prop, "\n")
else
prop = BD.auto(prop)
end
elseif prop_key == "language" then
-- Get a chance to have title, authors... rendered with alternate
-- glyphs for the book language (e.g. japanese book in chinese UI)
values_lang = prop
elseif prop_key == "description" then
-- Description may (often in EPUB, but not always) or may not (rarely in PDF) be HTML
prop = util.htmlToPlainTextIfHtml(prop)
end
table.insert(kv_pairs, { prop_text[prop_key], prop })
if prop_key == "series_index" then
table.insert(kv_pairs, { prop_text["pages"], book_props["pages"] or _("N/A") })
end
end
-- cover image
local is_doc = self.document and true or false
self.custom_book_cover = DocSettings:findCoverFile(file)
table.insert(kv_pairs, {
_("Cover image:"),
_("Tap to display"),
callback = function() self:onShowBookCover(file, true) end,
separator = is_doc and not self.custom_book_cover,
})
-- custom cover image
if self.custom_book_cover then
table.insert(kv_pairs, {
_("Custom cover image:"),
_("Tap to display"),
callback = function() self:onShowBookCover(file) end,
separator = is_doc,
})
end
-- Page section
if is_doc then
local lines_nb, words_nb = self:getCurrentPageLineWordCounts()
if lines_nb == 0 then
lines_nb = _("N/A")
words_nb = _("N/A")
end
table.insert(kv_pairs, { _("Current page lines:"), lines_nb })
table.insert(kv_pairs, { _("Current page words:"), words_nb })
end
local KeyValuePage = require("ui/widget/keyvaluepage")
self.kvp_widget = KeyValuePage:new{
title = _("Book information"),
value_overflow_align = "right",
kv_pairs = kv_pairs,
values_lang = values_lang,
close_callback = function()
self.custom_book_cover = nil
if self.updated then
local FileManager = require("apps/filemanager/filemanager")
local fm_ui = FileManager.instance
local ui = self.ui or fm_ui
if not ui then
local ReaderUI = require("apps/reader/readerui")
ui = ReaderUI.instance
end
if ui and ui.coverbrowser then -- refresh cache db
ui.coverbrowser:deleteBookInfo(file)
end
if fm_ui then
fm_ui:onRefresh()
end
if metadata_updated_caller_callback then
metadata_updated_caller_callback()
end
end
end,
title_bar_left_icon = "appbar.menu",
title_bar_left_icon_tap_callback = function()
self:showCustomMenu(file, book_props, metadata_updated_caller_callback)
end,
}
UIManager:show(self.kvp_widget)
end
-- Returns customized metadata.
function BookInfo.customizeProps(original_props, filepath)
local custom_props = {} -- stub
original_props = original_props or {}
local props = {}
for _i, prop_key in ipairs(BookInfo.props) do
props[prop_key] = custom_props[prop_key] or original_props[prop_key]
end
props.pages = original_props.pages
-- if original title is empty, generate it as filename without extension
props.display_title = props.title or filemanagerutil.splitFileNameType(filepath)
return props
end
-- Returns document metadata (opened document or book (file) metadata or custom metadata).
function BookInfo.getDocProps(ui, file, book_props, no_open_document, no_customize)
local original_props, filepath
if ui then -- currently opened document
original_props = ui.doc_settings:readSetting("doc_props")
filepath = ui.document.file
else -- from file
original_props = BookInfo.getBookProps(file, book_props, no_open_document)
filepath = file
end
return no_customize and original_props or BookInfo.customizeProps(original_props, filepath)
end
-- Returns book (file) metadata, including number of pages.
function BookInfo.getBookProps(file, book_props, no_open_document)
if DocSettings:hasSidecarFile(file) then
local doc_settings = DocSettings:open(file)
if not book_props then
-- Files opened after 20170701 have a "doc_props" setting with
-- complete metadata and "doc_pages" with accurate nb of pages
book_props = doc_settings:readSetting("doc_props")
end
if not book_props then
-- File last opened before 20170701 may have a "stats" setting.
-- with partial metadata, or empty metadata if statistics plugin
-- was not enabled when book was read (we can guess that from
-- the fact that stats.page = 0)
local stats = doc_settings:readSetting("stats")
if stats and stats.pages ~= 0 then
-- title, authors, series, series_index, language
book_props = Document:getProps(stats)
end
end
-- Files opened after 20170701 have an accurate "doc_pages" setting.
local doc_pages = doc_settings:readSetting("doc_pages")
if doc_pages and book_props then
book_props.pages = doc_pages
end
end
-- If still no book_props (book never opened or empty "stats"), open the document to get them
if not book_props and not no_open_document then
local document = DocumentRegistry:openDocument(file)
if document then
local loaded = true
local pages
if document.loadDocument then -- CreDocument
if not document:loadDocument(false) then -- load only metadata
-- failed loading, calling other methods would segfault
loaded = false
end
-- For CreDocument, we would need to call document:render()
-- to get nb of pages, but the nb obtained by simply calling
-- here document:getPageCount() is wrong, often 2 to 3 times
-- the nb of pages we see when opening the document (may be
-- some other cre settings should be applied before calling
-- render() ?)
else
-- for all others than crengine, we seem to get an accurate nb of pages
pages = document:getPageCount()
end
if loaded then
book_props = document:getProps()
book_props.pages = pages
end
document:close()
end
end
-- If still no book_props, fall back to empty ones
return book_props or {}
end
-- Shows book information for currently opened document.
function BookInfo:onShowBookInfo()
if self.document then
self.ui.doc_props.pages = self.ui.doc_settings:readSetting("doc_pages")
self:show(self.document.file, self.ui.doc_props)
end
end
function BookInfo:onShowBookDescription(description, file)
if not description then
if file then
description = BookInfo.getDocProps(nil, file).description
elseif self.document then -- currently opened document
description = self.ui.doc_props.description
end
end
if description and description ~= "" then
-- Description may (often in EPUB, but not always) or may not (rarely
-- in PDF) be HTML.
description = util.htmlToPlainTextIfHtml(description)
local TextViewer = require("ui/widget/textviewer")
UIManager:show(TextViewer:new{
title = _("Description:"),
text = description,
})
else
UIManager:show(InfoMessage:new{
text = _("No book description available."),
})
end
end
function BookInfo:onShowBookCover(file, force_orig)
local cover_bb = self:getCoverImage(self.document, file, force_orig)
if cover_bb then
local ImageViewer = require("ui/widget/imageviewer")
local imgviewer = ImageViewer:new{
image = cover_bb,
with_title_bar = false,
fullscreen = true,
}
UIManager:show(imgviewer)
else
UIManager:show(InfoMessage:new{
text = _("No cover image available."),
})
end
end
function BookInfo:getCoverImage(doc, file, force_orig)
local cover_bb
-- check for a custom cover (orig cover is forcibly requested in "Book information" only)
if not force_orig then
local custom_cover = DocSettings:findCoverFile(file or (doc and doc.file))
if custom_cover then
local cover_doc = DocumentRegistry:openDocument(custom_cover)
if cover_doc then
cover_bb = cover_doc:getCoverPageImage()
cover_doc:close()
return cover_bb, custom_cover
end
end
end
-- orig cover
local is_doc = doc and true or false
if not is_doc then
doc = DocumentRegistry:openDocument(file)
if doc and doc.loadDocument then -- CreDocument
doc:loadDocument(false) -- load only metadata
end
end
if doc then
cover_bb = doc:getCoverPageImage()
if not is_doc then
doc:close()
end
end
return cover_bb
end
function BookInfo:setCustomBookCover(file, book_props, metadata_updated_caller_callback)
local function kvp_update()
if self.ui then
self.ui.doc_settings:getCoverFile(true) -- reset cover file cache
end
self.updated = true
self.kvp_widget:onClose()
self:show(file, book_props, metadata_updated_caller_callback)
end
if self.custom_book_cover then -- reset custom cover
local ConfirmBox = require("ui/widget/confirmbox")
local confirm_box = ConfirmBox:new{
text = _("Reset custom cover?\nImage file will be deleted."),
ok_text = _("Reset"),
ok_callback = function()
if os.remove(self.custom_book_cover) then
DocSettings:removeSidecarDir(file, util.splitFilePathName(self.custom_book_cover))
kvp_update()
end
end,
}
UIManager:show(confirm_box)
else -- choose an image and set custom cover
local PathChooser = require("ui/widget/pathchooser")
local path_chooser = PathChooser:new{
select_directory = false,
file_filter = function(filename)
return DocumentRegistry:isImageFile(filename)
end,
onConfirm = function(image_file)
local sidecar_dir
local sidecar_file = DocSettings:findCoverFile(file) -- existing cover file
if sidecar_file then
os.remove(sidecar_file)
else -- no existing cover, get metadata file path
sidecar_file = DocSettings:hasSidecarFile(file, true) -- new sdr locations only
end
if sidecar_file then
sidecar_dir = util.splitFilePathName(sidecar_file)
else -- no sdr folder, create new
sidecar_dir = DocSettings:getSidecarDir(file) .. "/"
util.makePath(sidecar_dir)
end
local new_cover_file = sidecar_dir .. "cover." .. util.getFileNameSuffix(image_file):lower()
if ffiutil.copyFile(image_file, new_cover_file) == nil then
kvp_update()
end
end,
}
UIManager:show(path_chooser)
end
end
function BookInfo:getCurrentPageLineWordCounts()
local lines_nb, words_nb = 0, 0
if self.ui.rolling then
local res = self.ui.document:getTextFromPositions({x = 0, y = 0},
{x = Screen:getWidth(), y = Screen:getHeight()}, true) -- do not highlight
if res then
lines_nb = #self.ui.document:getScreenBoxesFromPositions(res.pos0, res.pos1, true)
for word in util.gsplit(res.text, "[%s%p]+", false) do
if util.hasCJKChar(word) then
for char in util.gsplit(word, "[\192-\255][\128-\191]+", true) do
words_nb = words_nb + 1
end
else
words_nb = words_nb + 1
end
end
end
else
local page_boxes = self.ui.document:getTextBoxes(self.ui:getCurrentPage())
if page_boxes and page_boxes[1][1].word then
lines_nb = #page_boxes
for _, line in ipairs(page_boxes) do
if #line == 1 and line[1].word == "" then -- empty line
lines_nb = lines_nb - 1
else
words_nb = words_nb + #line
local last_word = line[#line].word
if last_word:sub(-1) == "-" and last_word ~= "-" then -- hyphenated
words_nb = words_nb - 1
end
end
end
end
end
return lines_nb, words_nb
end
function BookInfo:showCustomMenu(file, book_props, metadata_updated_caller_callback)
local button_dialog
local buttons = {{
{
text = self.custom_book_cover and _("Reset cover image") or _("Set cover image"),
align = "left",
callback = function()
UIManager:close(button_dialog)
self:setCustomBookCover(file, book_props, metadata_updated_caller_callback)
end,
},
}}
button_dialog = ButtonDialog:new{
shrink_unneeded_width = true,
buttons = buttons,
anchor = function()
return self.kvp_widget.title_bar.left_button.image.dimen
end,
}
UIManager:show(button_dialog)
end
return BookInfo