From ed2ea6803f4b9be6b065f43590229dea4fe51426 Mon Sep 17 00:00:00 2001 From: hius07 <62179190+hius07@users.noreply.github.com> Date: Fri, 1 Sep 2023 08:07:29 +0300 Subject: [PATCH] Custom metadata (#10861) --- .../apps/filemanager/filemanagerbookinfo.lua | 405 +++++++++++------- .../filemanager/filemanagerfilesearcher.lua | 2 +- .../apps/reader/modules/readertypography.lua | 2 +- frontend/apps/reader/modules/readerview.lua | 46 +- frontend/apps/reader/readerui.lua | 5 +- frontend/docsettings.lua | 240 ++++++++--- frontend/ui/screensaver.lua | 2 +- .../coverbrowser.koplugin/bookinfomanager.lua | 2 +- 8 files changed, 473 insertions(+), 231 deletions(-) diff --git a/frontend/apps/filemanager/filemanagerbookinfo.lua b/frontend/apps/filemanager/filemanagerbookinfo.lua index 565d44f5f..d2b65952c 100644 --- a/frontend/apps/filemanager/filemanagerbookinfo.lua +++ b/frontend/apps/filemanager/filemanagerbookinfo.lua @@ -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) +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 = {{ + 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, + { + 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 diff --git a/frontend/apps/filemanager/filemanagerfilesearcher.lua b/frontend/apps/filemanager/filemanagerfilesearcher.lua index 5f63f8db2..6b304d1fe 100644 --- a/frontend/apps/filemanager/filemanagerfilesearcher.lua +++ b/frontend/apps/filemanager/filemanagerfilesearcher.lua @@ -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] diff --git a/frontend/apps/reader/modules/readertypography.lua b/frontend/apps/reader/modules/readertypography.lua index 9e9c15a5c..7049374c6 100644 --- a/frontend/apps/reader/modules/readertypography.lua +++ b/frontend/apps/reader/modules/readertypography.lua @@ -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 diff --git a/frontend/apps/reader/modules/readerview.lua b/frontend/apps/reader/modules/readerview.lua index a904403e2..d76f45992 100644 --- a/frontend/apps/reader/modules/readerview.lua +++ b/frontend/apps/reader/modules/readerview.lua @@ -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 diff --git a/frontend/apps/reader/readerui.lua b/frontend/apps/reader/readerui.lua index cef2d032f..23b832bf2 100644 --- a/frontend/apps/reader/readerui.lua +++ b/frontend/apps/reader/readerui.lua @@ -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") diff --git a/frontend/docsettings.lua b/frontend/docsettings.lua index 81e6fde9e..b1c82344f 100644 --- a/frontend/docsettings.lua +++ b/frontend/docsettings.lua @@ -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 diff --git a/frontend/ui/screensaver.lua b/frontend/ui/screensaver.lua index b87f888c8..ded637f2c 100644 --- a/frontend/ui/screensaver.lua +++ b/frontend/ui/screensaver.lua @@ -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 diff --git a/plugins/coverbrowser.koplugin/bookinfomanager.lua b/plugins/coverbrowser.koplugin/bookinfomanager.lua index d8205a5ee..d15731856 100644 --- a/plugins/coverbrowser.koplugin/bookinfomanager.lua +++ b/plugins/coverbrowser.koplugin/bookinfomanager.lua @@ -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