mirror of
https://github.com/koreader/koreader.git
synced 2025-12-18 12:02:09 +01:00
1099 lines
44 KiB
Lua
1099 lines
44 KiB
Lua
--[[--
|
|
This module provides a way to display book information (filename and book metadata)
|
|
]]
|
|
|
|
local BD = require("ui/bidi")
|
|
local BookList = require("ui/widget/booklist")
|
|
local ButtonDialog = require("ui/widget/buttondialog")
|
|
local ConfirmBox = require("ui/widget/confirmbox")
|
|
local Device = require("device")
|
|
local DocSettings = require("docsettings")
|
|
local Document = require("document/document")
|
|
local DocumentRegistry = require("document/documentregistry")
|
|
local Event = require("ui/event")
|
|
local InfoMessage = require("ui/widget/infomessage")
|
|
local InputDialog = require("ui/widget/inputdialog")
|
|
local Math = require("optmath")
|
|
local Notification = require("ui/widget/notification")
|
|
local TextViewer = require("ui/widget/textviewer")
|
|
local UIManager = require("ui/uimanager")
|
|
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
|
local datetime = require("datetime")
|
|
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 N_ = _.ngettext
|
|
local Screen = Device.screen
|
|
local T = ffiUtil.template
|
|
|
|
local BookInfo = WidgetContainer:extend{
|
|
title = _("Book information"),
|
|
props = {
|
|
"title",
|
|
"authors",
|
|
"series",
|
|
"series_index",
|
|
"language",
|
|
"keywords",
|
|
"description",
|
|
},
|
|
prop_text = {
|
|
cover = _("Cover image:"),
|
|
title = _("Title:"),
|
|
authors = _("Author(s):"),
|
|
series = _("Series:"),
|
|
series_index = _("Series index:"),
|
|
language = _("Language:"),
|
|
keywords = _("Keywords:"),
|
|
description = _("Description:"),
|
|
pages = _("Pages:"),
|
|
},
|
|
rating_max = 5,
|
|
}
|
|
|
|
function BookInfo:init()
|
|
if self.document then -- only for Reader menu
|
|
self.ui.menu:registerToMainMenu(self)
|
|
end
|
|
end
|
|
|
|
function BookInfo:addToMainMenu(menu_items)
|
|
menu_items.book_info = {
|
|
text = self.title,
|
|
callback = function()
|
|
self:onShowBookInfo()
|
|
end,
|
|
}
|
|
end
|
|
|
|
-- Shows book information.
|
|
function BookInfo:show(doc_settings_or_file, book_props)
|
|
self.prop_updated = nil
|
|
self.summary_updated = nil
|
|
local kv_pairs = {}
|
|
|
|
-- File section
|
|
local has_sidecar = type(doc_settings_or_file) == "table"
|
|
local file = has_sidecar and doc_settings_or_file:readSetting("doc_path") or doc_settings_or_file
|
|
self.is_current_doc = self.document and self.document.file == file
|
|
if not has_sidecar and self.is_current_doc then
|
|
doc_settings_or_file = self.ui.doc_settings
|
|
has_sidecar = true
|
|
end
|
|
if not has_sidecar and BookList.hasBookBeenOpened(file) then
|
|
doc_settings_or_file = BookList.getDocSettings(file)
|
|
has_sidecar = true
|
|
end
|
|
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
|
|
book_props = book_props or self:getDocProps(file, book_props)
|
|
book_props.pages = book_props.pages or BookList.getBookInfo(file).pages
|
|
-- cover image
|
|
self.custom_book_cover = DocSettings:findCustomCoverFile(file)
|
|
local key_text = self.prop_text["cover"]
|
|
if self.custom_book_cover then
|
|
key_text = "\u{F040} " .. key_text
|
|
end
|
|
table.insert(kv_pairs, { key_text, _("Tap to display"),
|
|
callback = function()
|
|
self:onShowBookCover(file)
|
|
end,
|
|
hold_callback = function()
|
|
self:showCustomDialog(file, book_props)
|
|
end,
|
|
separator = true,
|
|
})
|
|
-- metadata
|
|
local n_a = _("N/A")
|
|
local custom_props
|
|
local custom_metadata_file = DocSettings:findCustomMetadataFile(file)
|
|
if custom_metadata_file then
|
|
self.custom_doc_settings = DocSettings.openSettingsFile(custom_metadata_file)
|
|
custom_props = self.custom_doc_settings:readSetting("custom_props")
|
|
end
|
|
local values_lang, callback
|
|
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)
|
|
callback = function() -- proper text_type in TextViewer
|
|
self:showBookProp("description", prop)
|
|
end
|
|
end
|
|
key_text = self.prop_text[prop_key]
|
|
if custom_props and custom_props[prop_key] then -- customized
|
|
key_text = "\u{F040} " .. key_text
|
|
end
|
|
table.insert(kv_pairs, { key_text, prop,
|
|
callback = callback,
|
|
hold_callback = function()
|
|
self:showCustomDialog(file, book_props, prop_key)
|
|
end,
|
|
})
|
|
end
|
|
-- pages
|
|
local pages = self.is_current_doc
|
|
and self.ui.pagemap and self.ui.pagemap:wantsPageLabels() and self.ui.pagemap:getLastPageLabel(true)
|
|
table.insert(kv_pairs, { self.prop_text["pages"], pages or book_props.pages or n_a, separator = true })
|
|
|
|
-- Current page
|
|
if self.document then
|
|
local lines_nb, words_nb = self.ui.view:getCurrentPageLineWordCounts()
|
|
local text = lines_nb == 0 and _("number of lines and words not available")
|
|
or T(N_("1 line", "%1 lines", lines_nb), lines_nb) .. ", " .. T(N_("1 word", "%1 words", words_nb), words_nb)
|
|
table.insert(kv_pairs, { _("Current page:"), text, separator = true })
|
|
end
|
|
|
|
-- Summary section
|
|
local summary = has_sidecar and doc_settings_or_file:readSetting("summary") or {}
|
|
local rating = summary.rating or 0
|
|
local summary_hold_callback = function()
|
|
self:editSummary(doc_settings_or_file, book_props)
|
|
end
|
|
table.insert(kv_pairs, { _("Rating:"), ("★"):rep(rating) .. ("☆"):rep(self.rating_max - rating),
|
|
hold_callback = summary_hold_callback })
|
|
table.insert(kv_pairs, { _("Review:"), summary.note or n_a,
|
|
hold_callback = summary_hold_callback, separator = true })
|
|
|
|
-- Notebook file
|
|
local notebook_file = self:getNotebookFile(doc_settings_or_file)
|
|
local notebook_file_callback = function()
|
|
self:showNotebookFileDialog(notebook_file, doc_settings_or_file, book_props)
|
|
end
|
|
table.insert(kv_pairs, { _("Notebook file:"), notebook_file:gsub(".*/", ""),
|
|
callback = notebook_file_callback })
|
|
|
|
local KeyValuePage = require("ui/widget/keyvaluepage")
|
|
self.kvp_widget = KeyValuePage:new{
|
|
title = self.title,
|
|
value_overflow_align = "right",
|
|
kv_pairs = kv_pairs,
|
|
values_lang = values_lang,
|
|
close_callback = function()
|
|
self.custom_doc_settings = nil
|
|
self.custom_book_cover = nil
|
|
if self.prop_updated then
|
|
UIManager:broadcastEvent(Event:new("InvalidateMetadataCache", file))
|
|
UIManager:broadcastEvent(Event:new("BookMetadataChanged", self.prop_updated))
|
|
end
|
|
if self.summary_updated then -- refresh file browser, sdr folder may appear
|
|
UIManager:broadcastEvent(Event:new("BookMetadataChanged"))
|
|
end
|
|
end,
|
|
}
|
|
UIManager:show(self.kvp_widget)
|
|
end
|
|
|
|
function BookInfo.getCustomProp(prop_key, filepath)
|
|
local custom_metadata_file = DocSettings:findCustomMetadataFile(filepath)
|
|
return custom_metadata_file
|
|
and DocSettings.openSettingsFile(custom_metadata_file):readSetting("custom_props")[prop_key]
|
|
end
|
|
|
|
-- Returns extended and customized metadata.
|
|
function BookInfo.extendProps(original_props, filepath)
|
|
-- do not customize if filepath is not passed (eg from covermenu)
|
|
local custom_metadata_file = filepath and DocSettings:findCustomMetadataFile(filepath)
|
|
local custom_props = custom_metadata_file
|
|
and DocSettings.openSettingsFile(custom_metadata_file):readSetting("custom_props") or {}
|
|
original_props = original_props or {}
|
|
|
|
local props = {}
|
|
for _, 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 customized document metadata, including number of pages.
|
|
function BookInfo:getDocProps(file, book_props, no_open_document)
|
|
if self.ui.coverbrowser then
|
|
book_props = self.ui.coverbrowser.getDocProps(file)
|
|
if book_props ~= nil then -- already customized
|
|
book_props.display_title = book_props.title or filemanagerutil.splitFileNameType(file)
|
|
return book_props
|
|
end
|
|
end
|
|
|
|
if BookList.hasBookBeenOpened(file) then
|
|
local doc_settings = BookList.getDocSettings(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"),
|
|
-- but custom metadata exists, it has a copy of original doc_props
|
|
if not book_props then
|
|
local custom_metadata_file = DocSettings:findCustomMetadataFile(file)
|
|
if custom_metadata_file then
|
|
book_props = DocSettings.openSettingsFile(custom_metadata_file):readSetting("doc_props")
|
|
end
|
|
end
|
|
|
|
-- If still no book_props, open the document to get them
|
|
if not book_props and not no_open_document then
|
|
local document = DocumentRegistry:hasProvider(file) and 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
|
|
|
|
return BookInfo.extendProps(book_props, file)
|
|
end
|
|
|
|
function BookInfo:findInProps(book_props, search_string, case_sensitive)
|
|
for _, key in ipairs(self.props) do
|
|
local prop = book_props[key]
|
|
if prop then
|
|
if key == "series_index" then
|
|
prop = tostring(prop)
|
|
elseif key == "description" then
|
|
prop = util.htmlToPlainTextIfHtml(prop)
|
|
end
|
|
if util.stringSearch(prop, search_string, case_sensitive) ~= 0 then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
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.ui.doc_settings, self.ui.doc_props)
|
|
end
|
|
end
|
|
|
|
function BookInfo:showBookProp(prop_key, prop_text)
|
|
UIManager:show(TextViewer:new{
|
|
title = self.prop_text[prop_key],
|
|
text = prop_text,
|
|
text_type = prop_key == "description" and "book_info" or nil,
|
|
})
|
|
end
|
|
|
|
function BookInfo:onShowBookDescription(description, file)
|
|
if not description then
|
|
if file then
|
|
description = self:getDocProps(file).description
|
|
elseif self.document then -- currently opened document
|
|
description = self.ui.doc_props.description
|
|
end
|
|
end
|
|
if description then
|
|
self:showBookProp("description", util.htmlToPlainTextIfHtml(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(document, file, force_orig)
|
|
local curr_file = document and document.file
|
|
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:findCustomCoverFile(file or curr_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 doc
|
|
local do_open = file ~= nil and file ~= curr_file
|
|
if do_open then
|
|
doc = DocumentRegistry:openDocument(file)
|
|
if doc and doc.loadDocument then -- CreDocument
|
|
doc:loadDocument(false) -- load only metadata
|
|
end
|
|
else
|
|
doc = document
|
|
end
|
|
if doc then
|
|
cover_bb = doc:getCoverPageImage()
|
|
if do_open then
|
|
doc:close()
|
|
end
|
|
end
|
|
return cover_bb
|
|
end
|
|
|
|
function BookInfo:updateBookInfo(file, book_props, prop_updated, prop_value_old)
|
|
if self.document and prop_updated == "cover" then
|
|
self.ui.doc_settings:getCustomCoverFile(true) -- reset cover file cache
|
|
end
|
|
self.prop_updated = {
|
|
filepath = file,
|
|
doc_props = book_props,
|
|
metadata_key_updated = prop_updated,
|
|
metadata_value_old = prop_value_old,
|
|
}
|
|
self.kvp_widget:onClose()
|
|
self:show(file, book_props)
|
|
end
|
|
|
|
function BookInfo:setCustomCover(file, book_props)
|
|
if self.custom_book_cover then -- reset custom cover
|
|
if os.remove(self.custom_book_cover) then
|
|
DocSettings.removeSidecarDir(util.splitFilePathName(self.custom_book_cover))
|
|
self:updateBookInfo(file, book_props, "cover")
|
|
end
|
|
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)
|
|
if DocSettings:flushCustomCover(file, image_file) then
|
|
self:updateBookInfo(file, book_props, "cover")
|
|
end
|
|
end,
|
|
}
|
|
UIManager:show(path_chooser)
|
|
end
|
|
end
|
|
|
|
function BookInfo:setCustomCoverFromImage(file, image_file)
|
|
local custom_book_cover = DocSettings:findCustomCoverFile(file)
|
|
if custom_book_cover then
|
|
os.remove(custom_book_cover)
|
|
end
|
|
DocSettings:flushCustomCover(file, image_file)
|
|
if self.ui.doc_settings then
|
|
self.ui.doc_settings:getCustomCoverFile(true) -- reset cover file cache
|
|
end
|
|
UIManager:broadcastEvent(Event:new("InvalidateMetadataCache", file))
|
|
UIManager:broadcastEvent(Event:new("BookMetadataChanged"))
|
|
end
|
|
|
|
function BookInfo:setCustomMetadata(file, book_props, prop_key, prop_value)
|
|
-- in file
|
|
local custom_doc_settings, custom_props, display_title, no_custom_metadata
|
|
if self.custom_doc_settings then
|
|
custom_doc_settings = self.custom_doc_settings
|
|
else -- no custom metadata file, create new
|
|
custom_doc_settings = DocSettings.openSettingsFile()
|
|
display_title = book_props.display_title -- backup
|
|
book_props.display_title = nil
|
|
custom_doc_settings:saveSetting("doc_props", book_props) -- save a copy of original props
|
|
end
|
|
custom_props = custom_doc_settings:readSetting("custom_props", {})
|
|
local prop_value_old = custom_props[prop_key] or book_props[prop_key]
|
|
custom_props[prop_key] = prop_value -- nil when resetting a custom prop
|
|
if next(custom_props) == nil then -- no more custom metadata
|
|
os.remove(custom_doc_settings.sidecar_file)
|
|
DocSettings.removeSidecarDir(util.splitFilePathName(custom_doc_settings.sidecar_file))
|
|
no_custom_metadata = true
|
|
else
|
|
if book_props.pages then -- keep a copy of original 'pages' up to date
|
|
local original_props = custom_doc_settings:readSetting("doc_props")
|
|
original_props.pages = book_props.pages
|
|
end
|
|
custom_doc_settings:flushCustomMetadata(file)
|
|
end
|
|
book_props.display_title = book_props.display_title or display_title -- restore
|
|
-- in memory
|
|
prop_value = prop_value or custom_doc_settings:readSetting("doc_props")[prop_key] -- set custom or restore original
|
|
book_props[prop_key] = prop_value
|
|
if prop_key == "title" then -- generate when resetting the customized title and original is empty
|
|
book_props.display_title = book_props.title or filemanagerutil.splitFileNameType(file)
|
|
end
|
|
if self.is_current_doc then
|
|
self.ui.doc_props[prop_key] = prop_value
|
|
if prop_key == "title" then
|
|
self.ui.doc_props.display_title = book_props.display_title
|
|
end
|
|
if no_custom_metadata then
|
|
self.ui.doc_settings:getCustomMetadataFile(true) -- reset metadata file cache
|
|
end
|
|
end
|
|
self:updateBookInfo(file, book_props, prop_key, prop_value_old)
|
|
end
|
|
|
|
function BookInfo:showCustomEditDialog(file, book_props, prop_key)
|
|
local prop = book_props[prop_key]
|
|
if prop and prop_key == "description" then
|
|
prop = util.htmlToPlainTextIfHtml(prop)
|
|
end
|
|
local input_dialog
|
|
input_dialog = InputDialog:new{
|
|
title = _("Edit book metadata:") .. " " .. self.prop_text[prop_key]:gsub(":", ""),
|
|
input = prop,
|
|
input_type = prop_key == "series_index" and "number",
|
|
allow_newline = prop_key == "authors" or prop_key == "keywords" or prop_key == "description",
|
|
buttons = {
|
|
{
|
|
{
|
|
text = _("Cancel"),
|
|
id = "close",
|
|
callback = function()
|
|
UIManager:close(input_dialog)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Save"),
|
|
callback = function()
|
|
local prop_value = input_dialog:getInputValue()
|
|
if prop_value and prop_value ~= "" then
|
|
UIManager:close(input_dialog)
|
|
self:setCustomMetadata(file, book_props, prop_key, prop_value)
|
|
end
|
|
end,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
UIManager:show(input_dialog)
|
|
input_dialog:onShowKeyboard()
|
|
end
|
|
|
|
function BookInfo:showCustomDialog(file, book_props, prop_key)
|
|
local original_prop, custom_prop, prop_is_cover
|
|
if prop_key then -- metadata
|
|
if self.custom_doc_settings then
|
|
original_prop = self.custom_doc_settings:readSetting("doc_props")[prop_key]
|
|
custom_prop = self.custom_doc_settings:readSetting("custom_props")[prop_key]
|
|
else
|
|
original_prop = book_props[prop_key]
|
|
end
|
|
if original_prop and prop_key == "description" then
|
|
original_prop = util.htmlToPlainTextIfHtml(original_prop)
|
|
end
|
|
prop_is_cover = false
|
|
else -- cover
|
|
prop_key = "cover"
|
|
prop_is_cover = true
|
|
end
|
|
|
|
local button_dialog
|
|
local buttons = {
|
|
{
|
|
{
|
|
text = _("Copy original"),
|
|
enabled = original_prop ~= nil and Device:hasClipboard(),
|
|
callback = function()
|
|
UIManager:close(button_dialog)
|
|
Device.input.setClipboardText(original_prop)
|
|
end,
|
|
},
|
|
{
|
|
text = _("View original"),
|
|
enabled = original_prop ~= nil or prop_is_cover,
|
|
callback = function()
|
|
if prop_is_cover then
|
|
self:onShowBookCover(file, true)
|
|
else
|
|
self:showBookProp(prop_key, original_prop)
|
|
end
|
|
end,
|
|
},
|
|
},
|
|
{
|
|
{
|
|
text = _("Reset custom"),
|
|
enabled = custom_prop ~= nil or (prop_is_cover and self.custom_book_cover ~= nil),
|
|
callback = function()
|
|
local confirm_box = ConfirmBox:new{
|
|
text = prop_is_cover and _("Reset custom cover?\nImage file will be deleted.")
|
|
or _("Reset custom book metadata field?"),
|
|
ok_text = _("Reset"),
|
|
ok_callback = function()
|
|
UIManager:close(button_dialog)
|
|
if prop_is_cover then
|
|
self:setCustomCover(file, book_props)
|
|
else
|
|
self:setCustomMetadata(file, book_props, prop_key)
|
|
end
|
|
end,
|
|
}
|
|
UIManager:show(confirm_box)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Set custom"),
|
|
enabled = not prop_is_cover or (prop_is_cover and self.custom_book_cover == nil),
|
|
callback = function()
|
|
UIManager:close(button_dialog)
|
|
if prop_is_cover then
|
|
self:setCustomCover(file, book_props)
|
|
else
|
|
self:showCustomEditDialog(file, book_props, prop_key)
|
|
end
|
|
end,
|
|
},
|
|
},
|
|
}
|
|
button_dialog = ButtonDialog:new{
|
|
title = _("Book metadata:") .. " " .. self.prop_text[prop_key]:gsub(":", ""),
|
|
title_align = "center",
|
|
buttons = buttons,
|
|
}
|
|
UIManager:show(button_dialog)
|
|
end
|
|
|
|
function BookInfo:editSummary(doc_settings_or_file, book_props)
|
|
local has_sidecar = type(doc_settings_or_file) == "table"
|
|
local summary = has_sidecar and doc_settings_or_file:readSetting("summary") or {}
|
|
local rating = summary.rating or 0
|
|
local input_dialog
|
|
local rating_buttons_row = {}
|
|
for i = -1, self.rating_max + 2 do -- 2 empty buttons on each side
|
|
if i < 1 or i > self.rating_max then
|
|
table.insert(rating_buttons_row, {
|
|
text = "",
|
|
no_vertical_sep = true,
|
|
enabled = false,
|
|
})
|
|
else
|
|
table.insert(rating_buttons_row, {
|
|
text = i <= rating and "★" or "☆",
|
|
no_vertical_sep = true,
|
|
callback = function()
|
|
UIManager:close(input_dialog)
|
|
local note = input_dialog:getInputText()
|
|
summary.note = note ~= "" and note or nil
|
|
summary.rating = (i == 1 and summary.rating == 1) and 0 or i
|
|
doc_settings_or_file = filemanagerutil.saveSummary(doc_settings_or_file, summary)
|
|
self.summary_updated = true
|
|
self.kvp_widget:onClose()
|
|
self:show(doc_settings_or_file, book_props)
|
|
end,
|
|
})
|
|
end
|
|
end
|
|
input_dialog = InputDialog:new{
|
|
title = _("Edit book review"),
|
|
input = summary.note,
|
|
text_height = Screen:scaleBySize(160),
|
|
allow_newline = true,
|
|
buttons = {
|
|
rating_buttons_row,
|
|
{
|
|
{
|
|
text = _("Cancel"),
|
|
id = "close",
|
|
callback = function()
|
|
UIManager:close(input_dialog)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Save review"),
|
|
callback = function()
|
|
UIManager:close(input_dialog)
|
|
local note = input_dialog:getInputText()
|
|
summary.note = note ~= "" and note or nil
|
|
doc_settings_or_file = filemanagerutil.saveSummary(doc_settings_or_file, summary)
|
|
self.summary_updated = true
|
|
self.kvp_widget:onClose()
|
|
self:show(doc_settings_or_file, book_props)
|
|
end,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
UIManager:show(input_dialog)
|
|
input_dialog:onShowKeyboard(true)
|
|
end
|
|
|
|
-- notebook file
|
|
|
|
function BookInfo:getNotebookFile(doc_settings_or_file)
|
|
local notebook_file
|
|
if type(doc_settings_or_file) == "table" then
|
|
notebook_file = doc_settings_or_file:readSetting("notebook_file")
|
|
end
|
|
if notebook_file == nil then
|
|
notebook_file = G_reader_settings:readSetting("notebook_file")
|
|
if notebook_file == nil then
|
|
if type(doc_settings_or_file) == "table" then
|
|
notebook_file = doc_settings_or_file:readSetting("doc_path") .. ".txt"
|
|
elseif type(doc_settings_or_file) == "string" then
|
|
notebook_file = doc_settings_or_file .. ".txt"
|
|
else
|
|
local home_folder = G_reader_settings:readSetting("home_dir") or filemanagerutil.getDefaultDir()
|
|
notebook_file = ffiUtil.realpath(home_folder) .. "/notebook.txt"
|
|
end
|
|
end
|
|
end
|
|
return notebook_file
|
|
end
|
|
|
|
function BookInfo:showNotebookFileDialog(notebook_file, doc_settings_or_file, book_props)
|
|
local has_sidecar = type(doc_settings_or_file) == "table"
|
|
local file = has_sidecar and doc_settings_or_file:readSetting("doc_path") or doc_settings_or_file
|
|
local function saveNotebookFile(new_notebook_file)
|
|
if not has_sidecar then
|
|
doc_settings_or_file = BookList.getDocSettings(doc_settings_or_file)
|
|
end
|
|
doc_settings_or_file:saveSetting("notebook_file", new_notebook_file)
|
|
if not self.is_current_doc then
|
|
if new_notebook_file or doc_settings_or_file:readSetting("summary") then
|
|
doc_settings_or_file:flush()
|
|
else -- remove empty sidecar
|
|
doc_settings_or_file:purge(nil, { doc_settings = true }) -- keep custom
|
|
doc_settings_or_file = file -- to reopen bookinfo
|
|
end
|
|
self.summary_updated = true -- refresh FM
|
|
end
|
|
self.kvp_widget:onClose()
|
|
self:show(doc_settings_or_file, book_props)
|
|
end
|
|
|
|
local button_dialog
|
|
local local_notebook_file = file .. ".txt"
|
|
local default_notebook_file = G_reader_settings:readSetting("notebook_file")
|
|
local buttons = {
|
|
{
|
|
{
|
|
text = _("Use default"),
|
|
enabled = default_notebook_file ~= nil and notebook_file ~= default_notebook_file,
|
|
callback = function()
|
|
UIManager:close(button_dialog)
|
|
saveNotebookFile(default_notebook_file)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Reset default"),
|
|
enabled = default_notebook_file ~= nil,
|
|
callback = function()
|
|
UIManager:close(button_dialog)
|
|
G_reader_settings:delSetting("notebook_file")
|
|
Notification:notify(_("Notebook file default location reset"), Notification.SOURCE_ALWAYS_SHOW)
|
|
end,
|
|
},
|
|
},
|
|
{
|
|
{
|
|
text = _("Use local"),
|
|
enabled = notebook_file ~= local_notebook_file,
|
|
callback = function()
|
|
UIManager:close(button_dialog)
|
|
saveNotebookFile(local_notebook_file)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Set as default"),
|
|
enabled = notebook_file ~= default_notebook_file,
|
|
callback = function()
|
|
UIManager:close(button_dialog)
|
|
G_reader_settings:saveSetting("notebook_file", notebook_file)
|
|
Notification:notify(_("Notebook file default location saved"), Notification.SOURCE_ALWAYS_SHOW)
|
|
end,
|
|
},
|
|
},
|
|
{
|
|
{
|
|
text = _("Choose"),
|
|
callback = function()
|
|
UIManager:close(button_dialog)
|
|
local PathChooser = require("ui/widget/pathchooser")
|
|
local path_chooser = PathChooser:new{
|
|
path = notebook_file:match("(.*)/"),
|
|
select_directory = false,
|
|
onConfirm = saveNotebookFile,
|
|
}
|
|
UIManager:show(path_chooser)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Create"),
|
|
enabled = self.ui.texteditor ~= nil,
|
|
callback = function()
|
|
UIManager:close(button_dialog)
|
|
self.ui.texteditor:newFile(notebook_file, saveNotebookFile)
|
|
end,
|
|
},
|
|
},
|
|
{}, -- separator
|
|
{
|
|
{
|
|
text = _("View"),
|
|
enabled = lfs.attributes(notebook_file, "mode") == "file",
|
|
callback = function()
|
|
UIManager:close(button_dialog)
|
|
TextViewer.openFile(notebook_file)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Edit"),
|
|
enabled = self.ui.texteditor ~= nil,
|
|
callback = function()
|
|
UIManager:close(button_dialog)
|
|
self.ui.texteditor:openFile(notebook_file, saveNotebookFile)
|
|
end,
|
|
},
|
|
},
|
|
}
|
|
button_dialog = ButtonDialog:new{
|
|
title = notebook_file,
|
|
title_align = "center",
|
|
buttons = buttons,
|
|
}
|
|
UIManager:show(button_dialog)
|
|
end
|
|
|
|
function BookInfo:onShowNotebookFile()
|
|
local notebook_file = self:getNotebookFile(self.ui.doc_settings)
|
|
if self.ui.texteditor then
|
|
local function saveNotebookFile(new_notebook_file)
|
|
if self.ui.doc_settings ~= nil then
|
|
self.ui.doc_settings:saveSetting("notebook_file", new_notebook_file)
|
|
end
|
|
end
|
|
self.ui.texteditor:openFile(notebook_file, saveNotebookFile)
|
|
elseif lfs.attributes(notebook_file, "mode") == "file" then
|
|
TextViewer.openFile(notebook_file)
|
|
end
|
|
end
|
|
|
|
-- book metadata (sdr)
|
|
|
|
function BookInfo:moveBookMetadata()
|
|
-- called by filemanagermenu only
|
|
local file_chooser = self.ui.file_chooser
|
|
local function scanPath()
|
|
local sys_folders = { -- do not scan sys_folders
|
|
["/dev"] = true,
|
|
["/proc"] = true,
|
|
["/sys"] = true,
|
|
}
|
|
local books_to_move = {}
|
|
local dirs = { file_chooser.path }
|
|
while #dirs ~= 0 do
|
|
local new_dirs = {}
|
|
for _, d in ipairs(dirs) do
|
|
local ok, iter, dir_obj = pcall(lfs.dir, d)
|
|
if ok then
|
|
for f in iter, dir_obj do
|
|
local fullpath = "/" .. f
|
|
if d ~= "/" then
|
|
fullpath = d .. fullpath
|
|
end
|
|
local attributes = lfs.attributes(fullpath) or {}
|
|
if attributes.mode == "directory" and f ~= "." and f ~= ".."
|
|
and file_chooser:show_dir(f) and not sys_folders[fullpath] then
|
|
table.insert(new_dirs, fullpath)
|
|
elseif attributes.mode == "file" and not util.stringStartsWith(f, "._")
|
|
and file_chooser:show_file(f)
|
|
and DocSettings.isSidecarFileNotInPreferredLocation(fullpath) then
|
|
table.insert(books_to_move, fullpath)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
dirs = new_dirs
|
|
end
|
|
return books_to_move
|
|
end
|
|
UIManager:show(ConfirmBox:new{
|
|
text = _("Scan books in current folder and subfolders for their metadata location?"),
|
|
ok_text = _("Scan"),
|
|
ok_callback = function()
|
|
local books_to_move = scanPath()
|
|
local books_to_move_nb = #books_to_move
|
|
if books_to_move_nb == 0 then
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("No books with metadata not in your preferred location found."),
|
|
})
|
|
else
|
|
UIManager:show(ConfirmBox:new{
|
|
text = T(N_("1 book with metadata not in your preferred location found.",
|
|
"%1 books with metadata not in your preferred location found.",
|
|
books_to_move_nb), books_to_move_nb) .. "\n" ..
|
|
_("Move book metadata to your preferred location?"),
|
|
ok_text = _("Move"),
|
|
ok_callback = function()
|
|
UIManager:close(self.menu_container)
|
|
for _, book in ipairs(books_to_move) do
|
|
DocSettings.updateLocation(book, book)
|
|
end
|
|
file_chooser:refreshPath()
|
|
end,
|
|
})
|
|
end
|
|
end,
|
|
})
|
|
end
|
|
|
|
function BookInfo.showBooksWithHashBasedMetadata()
|
|
local header = T(_("Hash-based metadata has been saved in %1 for the following documents. Hash-based storage may slow down file browser navigation in large directories. Thus, if not using hash-based metadata storage, it is recommended to open the associated documents in KOReader to automatically migrate their metadata to the preferred storage location, or to delete %1, which will speed up file browser navigation."),
|
|
DocSettings.getSidecarStorage("hash"))
|
|
local file_info = { header .. "\n" }
|
|
local sdrs = DocSettings.findSidecarFilesInHashLocation()
|
|
for i, sdr in ipairs(sdrs) do
|
|
local sidecar_file, custom_metadata_file = unpack(sdr)
|
|
local doc_settings = DocSettings.openSettingsFile(sidecar_file)
|
|
local doc_props = doc_settings:readSetting("doc_props")
|
|
local custom_props = custom_metadata_file
|
|
and DocSettings.openSettingsFile(custom_metadata_file):readSetting("custom_props") or {}
|
|
local doc_path = doc_settings:readSetting("doc_path")
|
|
local title = custom_props.title or doc_props.title or filemanagerutil.splitFileNameType(doc_path)
|
|
local author = custom_props.authors or doc_props.authors or _("N/A")
|
|
doc_path = lfs.attributes(doc_path, "mode") == "file" and doc_path or _("N/A")
|
|
local text = T(_("%1. Title: %2; Author: %3\nDocument: %4"), i, title, author, doc_path)
|
|
table.insert(file_info, text)
|
|
end
|
|
local doc_nb = #file_info - 1
|
|
UIManager:show(TextViewer:new{
|
|
title = T(N_("1 document with hash-based metadata", "%1 documents with hash-based metadata", doc_nb), doc_nb),
|
|
title_multilines = true,
|
|
text = table.concat(file_info, "\n"),
|
|
})
|
|
end
|
|
|
|
function BookInfo:expandString(str, file, timestamp)
|
|
if self == nil then
|
|
UIManager:show(InfoMessage:new{
|
|
text = _([[
|
|
%T title
|
|
%A author
|
|
%S series
|
|
%t total pages
|
|
%c current page
|
|
%l pages left in chapter
|
|
%p book percentage read
|
|
%H time left in book
|
|
%C chapter title
|
|
%P chapter percentage read
|
|
%h time left in chapter
|
|
%F file path
|
|
%f file name
|
|
%b battery level
|
|
%B battery symbol
|
|
%r separator
|
|
%D current date (yyyy-mm-dd)
|
|
%d current date (mm-dd)
|
|
%m current time (hh:mm)
|
|
%M current time (hh-mm-ss)]]),
|
|
monospace_font = true,
|
|
})
|
|
return
|
|
end
|
|
|
|
if not (str and str:find("%%")) then
|
|
return str
|
|
end
|
|
|
|
local n_a = _("N/A")
|
|
local doc_patterns, is_doc_required = "%T%A%S%t%c%p%H%C%l%P%h"
|
|
local patterns = {}
|
|
for p in str:gmatch("%%%a") do
|
|
patterns[p] = n_a -- calculate only needed items
|
|
if not is_doc_required and doc_patterns:find(p, 1, true) then
|
|
is_doc_required = true
|
|
end
|
|
end
|
|
|
|
file = file or G_reader_settings:readSetting("lastfile")
|
|
if file then
|
|
if is_doc_required then
|
|
local props
|
|
local doc = self.document and self.document.file == file and self.document
|
|
if doc then -- Reader, currently opened file
|
|
props = self.ui.doc_props
|
|
local footer = self.ui.view.footer
|
|
local pageno = footer.pageno
|
|
if patterns["%t"] or patterns["%c"] then
|
|
if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then
|
|
patterns["%t"] = patterns["%t"] and self.ui.pagemap:getLastPageLabel(true)
|
|
patterns["%c"] = patterns["%c"] and self.ui.pagemap:getCurrentPageLabel(true)
|
|
elseif doc:hasHiddenFlows() then
|
|
patterns["%t"] = patterns["%t"] and doc:getTotalPagesInFlow(doc:getPageFlow(pageno))
|
|
patterns["%c"] = patterns["%c"] and doc:getPageNumberInFlow(pageno)
|
|
else
|
|
patterns["%t"] = patterns["%t"] and footer.pages
|
|
patterns["%c"] = patterns["%c"] and pageno
|
|
end
|
|
end
|
|
patterns["%p"] = patterns["%p"] and Math.round(footer:getBookProgress() * 100)
|
|
if patterns["%C"] then
|
|
local title = self.ui.toc:getTocTitleByPage(pageno)
|
|
if title and title ~= "" then
|
|
patterns["%C"] = title
|
|
end
|
|
end
|
|
if patterns["%l"] then
|
|
local pages_left_in_chapter = self.ui.toc:getChapterPagesLeft(pageno) or doc:getTotalPagesLeft(pageno)
|
|
if pages_left_in_chapter then
|
|
if footer.settings.pages_left_includes_current_page then
|
|
pages_left_in_chapter = pages_left_in_chapter + 1
|
|
end
|
|
patterns["%l"] = pages_left_in_chapter
|
|
end
|
|
end
|
|
patterns["%P"] = patterns["%P"] and Math.round(footer:getChapterProgress(true) * 100)
|
|
if (patterns["%H"] or patterns["%h"]) and self.ui.statistics then
|
|
local pages = doc:getTotalPagesLeft(pageno)
|
|
if patterns["%H"] then
|
|
local time_left = self.ui.statistics:getTimeForPages(pages)
|
|
if time_left then
|
|
patterns["%H"] = time_left
|
|
end
|
|
end
|
|
if patterns["%h"] then
|
|
pages = self.ui.toc:getChapterPagesLeft(pageno) or pages
|
|
local time_left = self.ui.statistics:getTimeForPages(pages)
|
|
if time_left then
|
|
patterns["%h"] = time_left
|
|
end
|
|
end
|
|
end
|
|
elseif BookList.hasBookBeenOpened(file) then -- do not open book, use sdr only
|
|
local doc_settings = BookList.getDocSettings(file)
|
|
props = BookInfo.extendProps(doc_settings:readSetting("doc_props"), file)
|
|
if patterns["%t"] or patterns["%c"] or patterns["%p"] then
|
|
local pages = doc_settings:readSetting("doc_pages")
|
|
if pages then
|
|
patterns["%t"] = patterns["%t"] and pages
|
|
local percent = doc_settings:readSetting("percent_finished")
|
|
if percent then
|
|
patterns["%c"] = patterns["%c"] and Math.round(percent * pages)
|
|
patterns["%p"] = patterns["%p"] and Math.round(percent * 100)
|
|
end
|
|
end
|
|
end
|
|
-- %H %C %P %h unavailable
|
|
end
|
|
if props then
|
|
patterns["%T"] = patterns["%T"] and props.display_title
|
|
if patterns["%A"] and props.authors then
|
|
patterns["%A"] = props.authors
|
|
end
|
|
if patterns["%S"] and props.series then
|
|
patterns["%S"] = props.series_index and props.series .. " #" .. props.series_index or props.series
|
|
end
|
|
end
|
|
end
|
|
|
|
patterns["%F"] = patterns["%F"] and file
|
|
patterns["%f"] = patterns["%f"] and file:gsub(".*/", "")
|
|
end
|
|
if patterns["%r"] and self.document then
|
|
patterns["%r"] = self.ui.view.footer:genSeparator()
|
|
end
|
|
if (patterns["%b"] or patterns["%B"]) and Device:hasBattery() then
|
|
local powerd = Device:getPowerDevice()
|
|
local batt_lvl = powerd:getCapacity()
|
|
if Device:hasAuxBattery() and powerd:isAuxBatteryConnected() then
|
|
batt_lvl = batt_lvl + powerd:getAuxCapacity()
|
|
patterns["%B"] = patterns["%B"] and powerd:getBatterySymbol(powerd:isAuxCharged(), powerd:isAuxCharging(), batt_lvl / 2)
|
|
else
|
|
patterns["%B"] = patterns["%B"] and powerd:getBatterySymbol(powerd:isCharged(), powerd:isCharging(), batt_lvl)
|
|
end
|
|
patterns["%b"] = patterns["%b"] and batt_lvl
|
|
end
|
|
|
|
timestamp = timestamp or os.time()
|
|
patterns["%D"] = patterns["%D"] and os.date("%Y-%m-%d", timestamp)
|
|
patterns["%d"] = patterns["%d"] and os.date("%m-%d", timestamp)
|
|
patterns["%m"] = patterns["%m"] and datetime.secondsToHour(timestamp, G_reader_settings:isTrue("twelve_hour_clock"))
|
|
patterns["%M"] = patterns["%M"] and os.date("%H-%M-%S", timestamp)
|
|
|
|
return str:gsub("(%%%a)", patterns)
|
|
end
|
|
|
|
return BookInfo
|