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