mirror of
https://github.com/koreader/koreader.git
synced 2025-08-10 00:52:38 +00:00
Custom metadata (#10861)
This commit is contained in:
@@ -4,18 +4,20 @@ This module provides a way to display book information (filename and book metada
|
||||
|
||||
local BD = require("ui/bidi")
|
||||
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 InfoMessage = require("ui/widget/infomessage")
|
||||
local InputDialog = require("ui/widget/inputdialog")
|
||||
local TextViewer = require("ui/widget/textviewer")
|
||||
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 = {
|
||||
@@ -27,6 +29,17 @@ local BookInfo = WidgetContainer:extend{
|
||||
"keywords",
|
||||
"description",
|
||||
},
|
||||
prop_text = {
|
||||
cover = _("Cover image:"),
|
||||
title = _("Title:"),
|
||||
authors = _("Authors:"),
|
||||
series = _("Series:"),
|
||||
series_index = _("Series index:"),
|
||||
language = _("Language:"),
|
||||
keywords = _("Keywords:"),
|
||||
description = _("Description:"),
|
||||
pages = _("Pages:"),
|
||||
},
|
||||
}
|
||||
|
||||
function BookInfo:init()
|
||||
@@ -46,7 +59,7 @@ end
|
||||
|
||||
-- Shows book information.
|
||||
function BookInfo:show(file, book_props, metadata_updated_caller_callback)
|
||||
self.updated = nil
|
||||
self.prop_updated = nil
|
||||
local kv_pairs = {}
|
||||
|
||||
-- File section
|
||||
@@ -66,19 +79,31 @@ function BookInfo:show(file, book_props, metadata_updated_caller_callback)
|
||||
-- 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)
|
||||
book_props = BookInfo.getDocProps(file, book_props)
|
||||
end
|
||||
-- cover image
|
||||
self.custom_book_cover = DocSettings:findCoverFile(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, metadata_updated_caller_callback)
|
||||
end,
|
||||
separator = true,
|
||||
})
|
||||
-- metadata
|
||||
local custom_props
|
||||
local custom_metadata_file = DocSettings:getCustomMetadataFile(file)
|
||||
if custom_metadata_file then
|
||||
self.custom_doc_settings = DocSettings:openCustomMetadata(custom_metadata_file)
|
||||
custom_props = self.custom_doc_settings:readSetting("custom_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
|
||||
@@ -103,33 +128,23 @@ function BookInfo:show(file, book_props, metadata_updated_caller_callback)
|
||||
-- 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") })
|
||||
key_text = self.prop_text[prop_key]
|
||||
if custom_props and custom_props[prop_key] then -- customized
|
||||
key_text = "\u{F040} " .. key_text
|
||||
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,
|
||||
table.insert(kv_pairs, { key_text, prop,
|
||||
hold_callback = function()
|
||||
self:showCustomDialog(file, book_props, metadata_updated_caller_callback, prop_key)
|
||||
end,
|
||||
})
|
||||
end
|
||||
-- pages
|
||||
local is_doc = self.document and true or false
|
||||
table.insert(kv_pairs, { self.prop_text["pages"], book_props["pages"] or _("N/A"), separator = is_doc })
|
||||
|
||||
-- Page section
|
||||
if is_doc then
|
||||
local lines_nb, words_nb = self:getCurrentPageLineWordCounts()
|
||||
local lines_nb, words_nb = self.ui.view:getCurrentPageLineWordCounts()
|
||||
if lines_nb == 0 then
|
||||
lines_nb = _("N/A")
|
||||
words_nb = _("N/A")
|
||||
@@ -145,16 +160,19 @@ function BookInfo:show(file, book_props, metadata_updated_caller_callback)
|
||||
kv_pairs = kv_pairs,
|
||||
values_lang = values_lang,
|
||||
close_callback = function()
|
||||
self.custom_doc_settings = nil
|
||||
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
|
||||
if self.prop_updated then
|
||||
local ui, fm_ui
|
||||
if self.ui then
|
||||
if self.prop_updated == "title" then
|
||||
self.ui.view.footer:updateFooterText() -- in case the title changed
|
||||
end
|
||||
else
|
||||
fm_ui = require("apps/filemanager/filemanager").instance
|
||||
end
|
||||
if ui and ui.coverbrowser then -- refresh cache db
|
||||
ui = self.ui or fm_ui
|
||||
if ui.coverbrowser then -- refresh cache db
|
||||
ui.coverbrowser:deleteBookInfo(file)
|
||||
end
|
||||
if fm_ui then
|
||||
@@ -165,21 +183,19 @@ function BookInfo:show(file, book_props, 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
|
||||
-- Returns extended and customized metadata.
|
||||
function BookInfo.extendProps(original_props, filepath)
|
||||
local custom_metadata_file = DocSettings:getCustomMetadataFile(filepath)
|
||||
local custom_props = custom_metadata_file
|
||||
and DocSettings:openCustomMetadata(custom_metadata_file):readSetting("custom_props") or {}
|
||||
original_props = original_props or {}
|
||||
|
||||
local props = {}
|
||||
for _i, prop_key in ipairs(BookInfo.props) do
|
||||
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
|
||||
@@ -188,21 +204,8 @@ function BookInfo.customizeProps(original_props, 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)
|
||||
-- Returns customized document metadata, including number of pages.
|
||||
function BookInfo.getDocProps(file, book_props, no_open_document)
|
||||
if DocSettings:hasSidecarFile(file) then
|
||||
local doc_settings = DocSettings:open(file)
|
||||
if not book_props then
|
||||
@@ -228,7 +231,16 @@ function BookInfo.getBookProps(file, book_props, no_open_document)
|
||||
end
|
||||
end
|
||||
|
||||
-- If still no book_props (book never opened or empty "stats"), open the document to get them
|
||||
-- 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:getCustomMetadataFile(file)
|
||||
if custom_metadata_file then
|
||||
book_props = DocSettings:openCustomMetadata(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:openDocument(file)
|
||||
if document then
|
||||
@@ -257,8 +269,7 @@ function BookInfo.getBookProps(file, book_props, no_open_document)
|
||||
end
|
||||
end
|
||||
|
||||
-- If still no book_props, fall back to empty ones
|
||||
return book_props or {}
|
||||
return BookInfo.extendProps(book_props, file)
|
||||
end
|
||||
|
||||
-- Shows book information for currently opened document.
|
||||
@@ -269,23 +280,26 @@ function BookInfo:onShowBookInfo()
|
||||
end
|
||||
end
|
||||
|
||||
function BookInfo:showBookProp(prop_key, prop_text)
|
||||
if prop_key == "description" then
|
||||
prop_text = util.htmlToPlainTextIfHtml(prop_text)
|
||||
end
|
||||
UIManager:show(TextViewer:new{
|
||||
title = self.prop_text[prop_key],
|
||||
text = prop_text,
|
||||
})
|
||||
end
|
||||
|
||||
function BookInfo:onShowBookDescription(description, file)
|
||||
if not description then
|
||||
if file then
|
||||
description = BookInfo.getDocProps(nil, file).description
|
||||
description = BookInfo.getDocProps(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,
|
||||
})
|
||||
if description then
|
||||
self:showBookProp("description", description)
|
||||
else
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = _("No book description available."),
|
||||
@@ -341,28 +355,21 @@ function BookInfo:getCoverImage(doc, file, force_orig)
|
||||
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)
|
||||
function BookInfo:updateBookInfo(file, book_props, metadata_updated_caller_callback, prop_updated)
|
||||
if prop_updated == "cover" and self.ui then
|
||||
self.ui.doc_settings:getCoverFile(true) -- reset cover file cache
|
||||
end
|
||||
self.prop_updated = prop_updated
|
||||
self.kvp_widget:onClose()
|
||||
self:show(file, book_props, metadata_updated_caller_callback)
|
||||
end
|
||||
|
||||
function BookInfo:setCustomBookCover(file, book_props, metadata_updated_caller_callback)
|
||||
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)
|
||||
if os.remove(self.custom_book_cover) then
|
||||
DocSettings:removeSidecarDir(file, util.splitFilePathName(self.custom_book_cover))
|
||||
self:updateBookInfo(file, book_props, metadata_updated_caller_callback, "cover")
|
||||
end
|
||||
else -- choose an image and set custom cover
|
||||
local PathChooser = require("ui/widget/pathchooser")
|
||||
local path_chooser = PathChooser:new{
|
||||
@@ -371,22 +378,8 @@ function BookInfo:setCustomBookCover(file, book_props, metadata_updated_caller_c
|
||||
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()
|
||||
if DocSettings:flushCustomCover(file, image_file) then
|
||||
self:updateBookInfo(file, book_props, metadata_updated_caller_callback, "cover")
|
||||
end
|
||||
end,
|
||||
}
|
||||
@@ -394,61 +387,153 @@ function BookInfo:setCustomBookCover(file, book_props, metadata_updated_caller_c
|
||||
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
|
||||
function BookInfo:setCustomMetadata(file, book_props, metadata_updated_caller_callback, prop_key, prop_value)
|
||||
-- in file
|
||||
local custom_doc_settings, custom_props, display_title
|
||||
if self.custom_doc_settings then
|
||||
custom_doc_settings = self.custom_doc_settings
|
||||
custom_props = custom_doc_settings:readSetting("custom_props")
|
||||
else -- no custom metadata file, create new
|
||||
custom_doc_settings = DocSettings:openCustomMetadata()
|
||||
custom_props = {}
|
||||
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[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.custom_metadata_file)
|
||||
DocSettings:removeSidecarDir(file, util.splitFilePathName(custom_doc_settings.custom_metadata_file))
|
||||
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
|
||||
custom_doc_settings:saveSetting("custom_props", custom_props)
|
||||
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 self.ui then -- currently opened document
|
||||
self.ui.doc_props[prop_key] = prop_value
|
||||
if prop_key == "title" then -- generate if original is empty
|
||||
self.ui.doc_props.display_title = prop_value or filemanagerutil.splitFileNameType(file)
|
||||
end
|
||||
end
|
||||
return lines_nb, words_nb
|
||||
self:updateBookInfo(file, book_props, metadata_updated_caller_callback, prop_key)
|
||||
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,
|
||||
function BookInfo:showCustomEditDialog(file, book_props, metadata_updated_caller_callback, prop_key)
|
||||
local input_dialog
|
||||
input_dialog = InputDialog:new{
|
||||
title = _("Edit book property:") .. " " .. self.prop_text[prop_key]:gsub(":", ""),
|
||||
input = book_props[prop_key],
|
||||
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, metadata_updated_caller_callback, prop_key, prop_value)
|
||||
end
|
||||
end,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
}
|
||||
UIManager:show(input_dialog)
|
||||
input_dialog:onShowKeyboard()
|
||||
end
|
||||
|
||||
function BookInfo:showCustomDialog(file, book_props, metadata_updated_caller_callback, 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 property?"),
|
||||
ok_text = _("Reset"),
|
||||
ok_callback = function()
|
||||
UIManager:close(button_dialog)
|
||||
if prop_is_cover then
|
||||
self:setCustomBookCover(file, book_props, metadata_updated_caller_callback)
|
||||
else
|
||||
self:setCustomMetadata(file, book_props, metadata_updated_caller_callback, 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:setCustomBookCover(file, book_props, metadata_updated_caller_callback)
|
||||
else
|
||||
self:showCustomEditDialog(file, book_props, metadata_updated_caller_callback, prop_key)
|
||||
end
|
||||
end,
|
||||
},
|
||||
},
|
||||
}
|
||||
button_dialog = ButtonDialog:new{
|
||||
shrink_unneeded_width = true,
|
||||
title = _("Book property:") .. " " .. self.prop_text[prop_key]:gsub(":", ""),
|
||||
title_align = "center",
|
||||
buttons = buttons,
|
||||
anchor = function()
|
||||
return self.kvp_widget.title_bar.left_button.image.dimen
|
||||
end,
|
||||
}
|
||||
UIManager:show(button_dialog)
|
||||
end
|
||||
|
||||
@@ -193,7 +193,7 @@ function FileSearcher:isFileMatch(filename, fullpath, keywords, is_file)
|
||||
end
|
||||
if self.include_metadata and is_file and DocumentRegistry:hasProvider(fullpath) then
|
||||
local book_props = self.ui.coverbrowser:getBookInfo(fullpath) or
|
||||
FileManagerBookInfo.getDocProps(nil, fullpath, nil, true)
|
||||
FileManagerBookInfo.getDocProps(fullpath, nil, true) -- do not open the document
|
||||
if next(book_props) ~= nil then
|
||||
for _, key in ipairs(FileManagerBookInfo.props) do
|
||||
local prop = book_props[key]
|
||||
|
||||
@@ -778,7 +778,7 @@ function ReaderTypography:onPreRenderDocument(config)
|
||||
-- This is called after the document has been loaded,
|
||||
-- when we know and can access the document language.
|
||||
local props = self.ui.document:getProps()
|
||||
local doc_language = FileManagerBookInfo.customizeProps(props, self.ui.document.file).language
|
||||
local doc_language = FileManagerBookInfo.extendProps(props, self.ui.document.file).language
|
||||
self.book_lang_tag = self:fixLangTag(doc_language)
|
||||
|
||||
local is_known_lang_tag = self.book_lang_tag and LANG_TAG_TO_LANG_NAME[self.book_lang_tag] ~= nil
|
||||
|
||||
@@ -22,6 +22,7 @@ local logger = require("logger")
|
||||
local optionsutil = require("ui/data/optionsutil")
|
||||
local Size = require("ui/size")
|
||||
local time = require("ui/time")
|
||||
local util = require("util")
|
||||
local _ = require("gettext")
|
||||
local Screen = Device.screen
|
||||
local T = require("ffi/util").template
|
||||
@@ -636,7 +637,7 @@ function ReaderView:drawHighlightRect(bb, _x, _y, rect, drawer, draw_note_mark)
|
||||
else
|
||||
local note_mark_pos_x
|
||||
if self.ui.paging or
|
||||
(self.ui.document:getVisiblePageCount() == 1) or -- one-page mode
|
||||
(self.document:getVisiblePageCount() == 1) or -- one-page mode
|
||||
(x < Screen:getWidth() / 2) then -- page 1 in two-page mode
|
||||
note_mark_pos_x = self.note_mark_pos_x1
|
||||
else
|
||||
@@ -1247,17 +1248,17 @@ function ReaderView:setupNoteMarkPosition()
|
||||
self.note_mark_pos_x1 = screen_w - sign_gap - sign_w
|
||||
end
|
||||
else
|
||||
local doc_margins = self.ui.document:getPageMargins()
|
||||
local doc_margins = self.document:getPageMargins()
|
||||
local pos_x_r = screen_w - doc_margins["right"] + sign_gap -- mark in the right margin
|
||||
local pos_x_l = doc_margins["left"] - sign_gap - sign_w -- mark in the left margin
|
||||
if self.ui.document:getVisiblePageCount() == 1 then
|
||||
if self.document:getVisiblePageCount() == 1 then
|
||||
if BD.mirroredUILayout() then
|
||||
self.note_mark_pos_x1 = pos_x_l
|
||||
else
|
||||
self.note_mark_pos_x1 = pos_x_r
|
||||
end
|
||||
else -- two-page mode
|
||||
local page2_x = self.ui.document:getPageOffsetX(self.ui.document:getCurrentPage(true)+1)
|
||||
local page2_x = self.document:getPageOffsetX(self.document:getCurrentPage(true)+1)
|
||||
if BD.mirroredUILayout() then
|
||||
self.note_mark_pos_x1 = pos_x_l
|
||||
self.note_mark_pos_x2 = pos_x_l + page2_x
|
||||
@@ -1270,4 +1271,41 @@ function ReaderView:setupNoteMarkPosition()
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderView:getCurrentPageLineWordCounts()
|
||||
local lines_nb, words_nb = 0, 0
|
||||
if self.ui.rolling then
|
||||
local res = self.document:getTextFromPositions({x = 0, y = 0},
|
||||
{x = Screen:getWidth(), y = Screen:getHeight()}, true) -- do not highlight
|
||||
if res then
|
||||
lines_nb = #self.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.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
|
||||
|
||||
return ReaderView
|
||||
|
||||
@@ -456,9 +456,10 @@ function ReaderUI:init()
|
||||
-- Now that document is loaded, store book metadata in settings
|
||||
-- (so that filemanager can use it from sideCar file to display
|
||||
-- Book information).
|
||||
self.doc_settings:saveSetting("doc_props", self.document:getProps())
|
||||
local props = self.document:getProps()
|
||||
self.doc_settings:saveSetting("doc_props", props)
|
||||
-- And have an extended and customized copy in memory for quick access.
|
||||
self.doc_props = FileManagerBookInfo.getDocProps(self)
|
||||
self.doc_props = FileManagerBookInfo.extendProps(props, self.document.file)
|
||||
|
||||
-- Set "reading" status if there is no status.
|
||||
local summary = self.doc_settings:readSetting("summary")
|
||||
|
||||
@@ -16,6 +16,7 @@ local DocSettings = LuaSettings:extend{}
|
||||
|
||||
local HISTORY_DIR = DataStorage:getHistoryDir()
|
||||
local DOCSETTINGS_DIR = DataStorage:getDocSettingsDir()
|
||||
local custom_metadata_filename = "custom_metadata.lua"
|
||||
|
||||
local function buildCandidates(list)
|
||||
local candidates = {}
|
||||
@@ -146,41 +147,6 @@ function DocSettings:getFileFromHistory(hist_name)
|
||||
end
|
||||
end
|
||||
|
||||
--- Returns path to book custom cover file if it exists, or nil.
|
||||
function DocSettings:findCoverFile(doc_path)
|
||||
local location = G_reader_settings:readSetting("document_metadata_folder", "doc")
|
||||
local sidecar_dir = self:getSidecarDir(doc_path, location)
|
||||
local cover_file = self:_findCoverFileInDir(sidecar_dir)
|
||||
if not cover_file then
|
||||
location = location == "doc" and "dir" or "doc"
|
||||
sidecar_dir = self:getSidecarDir(doc_path, location)
|
||||
cover_file = self:_findCoverFileInDir(sidecar_dir)
|
||||
end
|
||||
return cover_file
|
||||
end
|
||||
|
||||
function DocSettings:_findCoverFileInDir(dir)
|
||||
local ok, iter, dir_obj = pcall(lfs.dir, dir)
|
||||
if ok then
|
||||
for f in iter, dir_obj do
|
||||
if util.splitFileNameSuffix(f) == "cover" then
|
||||
return dir .. "/" .. f
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function DocSettings:getCoverFile(reset_cache)
|
||||
if reset_cache then
|
||||
self.cover_file = nil
|
||||
else
|
||||
if self.cover_file == nil then
|
||||
self.cover_file = DocSettings:findCoverFile(self.data.doc_path) or false
|
||||
end
|
||||
return self.cover_file
|
||||
end
|
||||
end
|
||||
|
||||
--- Opens a document's individual settings (font, margin, dictionary, etc.)
|
||||
-- @string doc_path path to the document (e.g., `/foo/bar.pdf`)
|
||||
-- @treturn DocSettings object
|
||||
@@ -252,8 +218,16 @@ function DocSettings:open(doc_path)
|
||||
return new
|
||||
end
|
||||
|
||||
function DocSettings.writeFile(f_out, s_out)
|
||||
f_out:write("-- we can read Lua syntax here!\nreturn ")
|
||||
f_out:write(s_out)
|
||||
f_out:write("\n")
|
||||
ffiutil.fsyncOpenedFile(f_out) -- force flush to the storage device
|
||||
f_out:close()
|
||||
end
|
||||
|
||||
--- Serializes settings and writes them to `metadata.lua`.
|
||||
function DocSettings:flush(data, no_cover)
|
||||
function DocSettings:flush(data, no_custom_metadata)
|
||||
-- Depending on the settings, doc_settings are saved to the book folder or
|
||||
-- to koreader/docsettings folder. The latter is also a fallback for read-only book storage.
|
||||
local serials = G_reader_settings:readSetting("document_metadata_folder", "doc") == "doc"
|
||||
@@ -281,28 +255,35 @@ function DocSettings:flush(data, no_cover)
|
||||
logger.dbg("DocSettings: Writing to", sidecar_file)
|
||||
local f_out = io.open(sidecar_file, "w")
|
||||
if f_out ~= nil then
|
||||
f_out:write("-- we can read Lua syntax here!\nreturn ")
|
||||
f_out:write(s_out)
|
||||
f_out:write("\n")
|
||||
ffiutil.fsyncOpenedFile(f_out) -- force flush to the storage device
|
||||
f_out:close()
|
||||
DocSettings.writeFile(f_out, s_out)
|
||||
|
||||
if directory_updated then
|
||||
-- Ensure the file renaming is flushed to storage device
|
||||
ffiutil.fsyncDirectory(sidecar_file)
|
||||
end
|
||||
|
||||
-- move cover file to the metadata file location
|
||||
if not no_cover then
|
||||
local cover_file = self:getCoverFile()
|
||||
if cover_file then
|
||||
local filepath, filename = util.splitFilePathName(cover_file)
|
||||
-- move custom cover file and custom metadata file to the metadata file location
|
||||
if not no_custom_metadata then
|
||||
local metadata_file, filepath, filename
|
||||
-- custom cover
|
||||
metadata_file = self:getCoverFile()
|
||||
if metadata_file then
|
||||
filepath, filename = util.splitFilePathName(metadata_file)
|
||||
if filepath ~= sidecar_dir .. "/" then
|
||||
ffiutil.copyFile(cover_file, sidecar_dir .. "/" .. filename)
|
||||
os.remove(cover_file)
|
||||
ffiutil.copyFile(metadata_file, sidecar_dir .. "/" .. filename)
|
||||
os.remove(metadata_file)
|
||||
self:getCoverFile(true) -- reset cache
|
||||
end
|
||||
end
|
||||
-- custom metadata
|
||||
metadata_file = self:getCustomMetadataFile()
|
||||
if metadata_file then
|
||||
filepath, filename = util.splitFilePathName(metadata_file)
|
||||
if filepath ~= sidecar_dir .. "/" then
|
||||
ffiutil.copyFile(metadata_file, sidecar_dir .. "/" .. filename)
|
||||
os.remove(metadata_file)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self:purge(sidecar_file) -- remove old candidates and empty sidecar folders
|
||||
@@ -330,13 +311,21 @@ function DocSettings:purge(sidecar_to_keep)
|
||||
|
||||
local custom_metadata_purged
|
||||
if not sidecar_to_keep then
|
||||
local cover_file = self:getCoverFile()
|
||||
if cover_file then
|
||||
os.remove(cover_file)
|
||||
-- custom cover
|
||||
local metadata_file = self:getCoverFile()
|
||||
if metadata_file then
|
||||
os.remove(metadata_file)
|
||||
self:getCoverFile(true) -- reset cache
|
||||
custom_metadata_purged = true
|
||||
end
|
||||
-- custom metadata
|
||||
metadata_file = self:getCustomMetadataFile()
|
||||
if metadata_file then
|
||||
os.remove(metadata_file)
|
||||
custom_metadata_purged = true
|
||||
end
|
||||
end
|
||||
|
||||
if lfs.attributes(self.doc_sidecar_dir, "mode") == "directory" then
|
||||
os.remove(self.doc_sidecar_dir) -- keep parent folders
|
||||
end
|
||||
@@ -361,11 +350,11 @@ function DocSettings:updateLocation(doc_path, new_doc_path, copy)
|
||||
local doc_settings, new_sidecar_dir
|
||||
|
||||
-- update metadata
|
||||
if self:hasSidecarFile(doc_path) then
|
||||
if DocSettings:hasSidecarFile(doc_path) then
|
||||
doc_settings = DocSettings:open(doc_path)
|
||||
if new_doc_path then
|
||||
local new_doc_settings = DocSettings:open(new_doc_path)
|
||||
-- save doc settings to the new location, no cover file yet
|
||||
-- save doc settings to the new location, no custom metadata yet
|
||||
new_sidecar_dir = new_doc_settings:flush(doc_settings.data, true)
|
||||
else
|
||||
local cache_file_path = doc_settings:readSetting("cache_file_path")
|
||||
@@ -375,26 +364,155 @@ function DocSettings:updateLocation(doc_path, new_doc_path, copy)
|
||||
end
|
||||
end
|
||||
|
||||
-- update cover file
|
||||
-- update custom metadata
|
||||
if not doc_settings then
|
||||
doc_settings = DocSettings:open(doc_path)
|
||||
end
|
||||
local cover_file = doc_settings:getCoverFile()
|
||||
if cover_file and new_doc_path then
|
||||
if not new_sidecar_dir then
|
||||
new_sidecar_dir = self:getSidecarDir(new_doc_path)
|
||||
util.makePath(new_sidecar_dir)
|
||||
if new_doc_path then
|
||||
-- custom cover
|
||||
if cover_file then
|
||||
if not new_sidecar_dir then
|
||||
new_sidecar_dir = DocSettings:getSidecarDir(new_doc_path)
|
||||
util.makePath(new_sidecar_dir)
|
||||
end
|
||||
local _, filename = util.splitFilePathName(cover_file)
|
||||
ffiutil.copyFile(cover_file, new_sidecar_dir .. "/" .. filename)
|
||||
end
|
||||
-- custom metadata
|
||||
local metadata_file = self:getCustomMetadataFile(doc_path)
|
||||
if metadata_file then
|
||||
if not new_sidecar_dir then
|
||||
new_sidecar_dir = DocSettings:getSidecarDir(new_doc_path)
|
||||
util.makePath(new_sidecar_dir)
|
||||
end
|
||||
ffiutil.copyFile(metadata_file, new_sidecar_dir .. "/" .. custom_metadata_filename)
|
||||
end
|
||||
local _, filename = util.splitFilePathName(cover_file)
|
||||
ffiutil.copyFile(cover_file, new_sidecar_dir .. "/" .. filename)
|
||||
end
|
||||
|
||||
if not copy then
|
||||
doc_settings:purge()
|
||||
end
|
||||
if cover_file then
|
||||
|
||||
if cover_file then -- after purge because purge uses cover file cache
|
||||
doc_settings:getCoverFile(true) -- reset cache
|
||||
end
|
||||
end
|
||||
|
||||
-- custom cover
|
||||
|
||||
--- Returns path to book custom cover file if it exists, or nil.
|
||||
function DocSettings:findCoverFile(doc_path)
|
||||
doc_path = doc_path or self.data.doc_path
|
||||
local location = G_reader_settings:readSetting("document_metadata_folder", "doc")
|
||||
local sidecar_dir = self:getSidecarDir(doc_path, location)
|
||||
local cover_file = DocSettings._findCoverFileInDir(sidecar_dir)
|
||||
if not cover_file then
|
||||
location = location == "doc" and "dir" or "doc"
|
||||
sidecar_dir = self:getSidecarDir(doc_path, location)
|
||||
cover_file = DocSettings._findCoverFileInDir(sidecar_dir)
|
||||
end
|
||||
return cover_file
|
||||
end
|
||||
|
||||
function DocSettings._findCoverFileInDir(dir)
|
||||
local ok, iter, dir_obj = pcall(lfs.dir, dir)
|
||||
if ok then
|
||||
for f in iter, dir_obj do
|
||||
if util.splitFileNameSuffix(f) == "cover" then
|
||||
return dir .. "/" .. f
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function DocSettings:getCoverFile(reset_cache)
|
||||
if reset_cache then
|
||||
self.cover_file = nil
|
||||
else
|
||||
if self.cover_file == nil then -- fill empty cache
|
||||
self.cover_file = self:findCoverFile() or false
|
||||
end
|
||||
return self.cover_file
|
||||
end
|
||||
end
|
||||
|
||||
function DocSettings:getCustomCandidateSidecarDirs(doc_path)
|
||||
local sidecar_file = self:hasSidecarFile(doc_path, true) -- new locations only
|
||||
if sidecar_file then -- book was opened, write custom metadata to its sidecar dir
|
||||
local sidecar_dir = util.splitFilePathName(sidecar_file):sub(1, -2)
|
||||
return { sidecar_dir }
|
||||
end
|
||||
-- new book, create sidecar dir in accordance with sdr location setting
|
||||
local dir_sidecar_dir = self:getSidecarDir(doc_path, "dir")
|
||||
if G_reader_settings:readSetting("document_metadata_folder", "doc") == "doc" then
|
||||
local doc_sidecar_dir = self:getSidecarDir(doc_path, "doc")
|
||||
return { doc_sidecar_dir, dir_sidecar_dir } -- fallback in case of readonly book storage
|
||||
end
|
||||
return { dir_sidecar_dir }
|
||||
end
|
||||
|
||||
function DocSettings:flushCustomCover(doc_path, image_file)
|
||||
local sidecar_dirs = self:getCustomCandidateSidecarDirs(doc_path)
|
||||
local new_cover_filename = "/cover." .. util.getFileNameSuffix(image_file):lower()
|
||||
for _, sidecar_dir in ipairs(sidecar_dirs) do
|
||||
util.makePath(sidecar_dir)
|
||||
local new_cover_file = sidecar_dir .. new_cover_filename
|
||||
if ffiutil.copyFile(image_file, new_cover_file) == nil then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- custom metadata
|
||||
|
||||
--- Returns path to book custom metadata file if it exists, or nil.
|
||||
function DocSettings:getCustomMetadataFile(doc_path)
|
||||
doc_path = doc_path or self.data.doc_path
|
||||
for _, mode in ipairs({"doc", "dir"}) do
|
||||
local file = self:getSidecarDir(doc_path, mode) .. "/" .. custom_metadata_filename
|
||||
if lfs.attributes(file, "mode") == "file" then
|
||||
return file
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function DocSettings:openCustomMetadata(custom_metadata_file)
|
||||
local new = DocSettings:extend{}
|
||||
local ok, stored
|
||||
if custom_metadata_file then
|
||||
ok, stored = pcall(dofile, custom_metadata_file)
|
||||
end
|
||||
if ok and next(stored) ~= nil then
|
||||
new.data = stored
|
||||
else
|
||||
new.data = {}
|
||||
end
|
||||
new.custom_metadata_file = custom_metadata_file
|
||||
return new
|
||||
end
|
||||
|
||||
function DocSettings:flushCustomMetadata(doc_path)
|
||||
local sidecar_dirs = self:getCustomCandidateSidecarDirs(doc_path)
|
||||
local new_sidecar_dir
|
||||
local s_out = dump(self.data, nil, true)
|
||||
for _, sidecar_dir in ipairs(sidecar_dirs) do
|
||||
util.makePath(sidecar_dir)
|
||||
local f_out = io.open(sidecar_dir .. "/" .. custom_metadata_filename, "w")
|
||||
if f_out ~= nil then
|
||||
DocSettings.writeFile(f_out, s_out)
|
||||
new_sidecar_dir = sidecar_dir .. "/"
|
||||
break
|
||||
end
|
||||
end
|
||||
-- remove old custom metadata file if it was in alternative location
|
||||
if self.custom_metadata_file then
|
||||
local old_sidecar_dir = util.splitFilePathName(self.custom_metadata_file)
|
||||
if old_sidecar_dir ~= new_sidecar_dir then
|
||||
os.remove(self.custom_metadata_file)
|
||||
self:removeSidecarDir(doc_path, old_sidecar_dir)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return DocSettings
|
||||
|
||||
@@ -182,7 +182,7 @@ function Screensaver:expandSpecial(message, fallback)
|
||||
percent = doc_settings:readSetting("percent_finished") or percent
|
||||
currentpage = Math.round(percent * totalpages)
|
||||
percent = Math.round(percent * 100)
|
||||
props = FileManagerBookInfo.customizeProps(doc_settings:readSetting("doc_props"), lastfile)
|
||||
props = FileManagerBookInfo.extendProps(doc_settings:readSetting("doc_props"), lastfile)
|
||||
-- Unable to set time_left_chapter and time_left_document without ReaderUI, so leave N/A
|
||||
end
|
||||
if props then
|
||||
|
||||
@@ -492,7 +492,7 @@ function BookInfoManager:extractBookInfo(filepath, cover_specs)
|
||||
end
|
||||
if loaded then
|
||||
dbrow.pages = pages
|
||||
local props = FileManagerBookInfo.customizeProps(document:getProps(), filepath)
|
||||
local props = FileManagerBookInfo.extendProps(document:getProps(), filepath)
|
||||
if next(props) then -- there's at least one item
|
||||
dbrow.has_meta = 'Y'
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user