Export highlights: customizable export filename (#14212)

This commit is contained in:
hius07
2025-08-26 09:41:09 +03:00
committed by GitHub
parent a7afd230ba
commit 826455961d
4 changed files with 138 additions and 69 deletions

View File

@@ -6,22 +6,15 @@ Each target should inherit from this class and implement *at least* an `export`
@module baseexporter
]]
local DataStorage = require("datastorage")
local Device = require("device")
local http = require("socket.http")
local ltn12 = require("ltn12")
local rapidjson = require("rapidjson")
local socket = require("socket")
local socketutil = require("socketutil")
local util = require("util")
local _ = require("gettext")
local msg_failed = "json request failed: %s"
local BaseExporter = {
clipping_dir = DataStorage:getFullDataDir() .. "/clipboard"
}
local BaseExporter = {}
function BaseExporter:new(o)
o = o or {}
@@ -37,7 +30,7 @@ function BaseExporter:_init()
self.version = self.version or "1.0.0"
self.shareable = self.is_remote and nil or Device:canShareText()
self:loadSettings()
if type(self.init_callback) == "function" then
if self.init_callback then
local changed, settings = self:init_callback(self.settings)
if changed then
self.settings = settings
@@ -95,24 +88,10 @@ function BaseExporter:export(t) end
--[[--
File path where the exporter writes its output
@param t table of booknotes
@treturn string absolute path or nil
]]
function BaseExporter:getFilePath(t)
if self.is_remote then return end
local plugin_settings = G_reader_settings:readSetting("exporter") or {}
local clipping_dir = plugin_settings.clipping_dir or self.clipping_dir
local title
if #t == 1 then
title = t[1].output_filename
if plugin_settings.clipping_dir_book then
clipping_dir = util.splitFilePathName(t[1].file):sub(1, -2)
end
else
title = self.all_books_title or "all-books"
end
local filename = string.format("%s-%s.%s", self:getTimeStamp(), title, self.extension)
return clipping_dir .. "/" .. util.getSafeFilename(filename)
function BaseExporter:getFilePath()
return self.filepath and self.filepath .. "." .. self.extension
end
--[[--
@@ -180,6 +159,7 @@ Makes a json request against a remote endpoint
]]
function BaseExporter:makeJsonRequest(endpoint, method, body, headers)
local msg_failed = "json request failed: %s"
local sink = {}
local extra_headers = headers or {}
local body_json, response, err

View File

@@ -1,15 +1,10 @@
local BookList = require("ui/widget/booklist")
local DocumentRegistry = require("document/documentregistry")
local DocSettings = require("docsettings")
local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo")
local ffiutil = require("ffi/util")
local md5 = require("ffi/sha2").md5
local util = require("util")
local _ = require("gettext")
local T = ffiutil.template
local T = require("ffi/util").template
local MyClipping = {
my_clippings = "/mnt/us/documents/My Clippings.txt",
}
local MyClipping = {}
function MyClipping:new(o)
if o == nil then o = {} end
@@ -18,7 +13,6 @@ function MyClipping:new(o)
return o
end
--[[
-- clippings: main table to store parsed highlights and notes entries
-- {
-- ["Title(Author Name)"] = {
@@ -38,7 +32,7 @@ end
-- },
-- }
-- }
-- ]]
function MyClipping:parseMyClippings()
-- My Clippings format:
-- Title(Author Name)
@@ -46,7 +40,7 @@ function MyClipping:parseMyClippings()
--
-- This is a sample highlight.
-- ==========
local file = io.open(self.my_clippings, "r")
local file = io.open("/mnt/us/documents/My Clippings.txt", "r")
local clippings = {}
if file then
local index = 1
@@ -70,7 +64,7 @@ function MyClipping:parseMyClippings()
if index == 5 then
-- entry ends normally
local clipping = {
page = info.page or info.location,
page = info.page or info.location or _("N/A"),
sort = info.sort,
time = info.time,
text = text,
@@ -85,7 +79,6 @@ function MyClipping:parseMyClippings()
end
file:close()
end
return clippings
end
@@ -338,7 +331,7 @@ function MyClipping:getTitleAuthor(filepath, props)
end
function MyClipping:getClippingsFromBook(clippings, doc_path)
local doc_settings = DocSettings:open(doc_path)
local doc_settings = BookList.getDocSettings(doc_path)
local highlights, bookmarks
local annotations = doc_settings:readSetting("annotations")
if annotations == nil then
@@ -347,7 +340,7 @@ function MyClipping:getClippingsFromBook(clippings, doc_path)
bookmarks = doc_settings:readSetting("bookmarks")
end
local props = doc_settings:readSetting("doc_props")
props = FileManagerBookInfo.extendProps(props, doc_path)
props = self.ui.bookinfo.extendProps(props, doc_path)
local title, author = self:getTitleAuthor(doc_path, props)
clippings[title] = {
file = doc_path,
@@ -365,7 +358,7 @@ end
function MyClipping:parseHistory()
local clippings = {}
for _, item in ipairs(require("readhistory").hist) do
if not item.dim and DocSettings:hasSidecarFile(item.file) then
if not item.dim and BookList.hasBookBeenOpened(item.file) then
self:getClippingsFromBook(clippings, item.file)
end
end
@@ -375,25 +368,23 @@ end
function MyClipping:parseFiles(files)
local clippings = {}
for file in pairs(files) do
if DocSettings:hasSidecarFile(file) then
if BookList.hasBookBeenOpened(file) then
self:getClippingsFromBook(clippings, file)
end
end
return clippings
end
function MyClipping:parseCurrentDoc(view)
function MyClipping:parseCurrentDoc()
local clippings = {}
local title, author = self:getTitleAuthor(view.document.file, view.ui.doc_props)
local title, author = self:getTitleAuthor(self.ui.document.file, self.ui.doc_props)
clippings[title] = {
file = view.document.file,
file = self.ui.document.file,
title = title,
author = author,
-- Replaces characters that are invalid in filenames.
output_filename = util.getSafeFilename(title),
number_of_pages = view.document.info.number_of_pages,
number_of_pages = self.ui.view.footer.pages,
}
self:parseAnnotations(view.ui.annotation.annotations, clippings[title])
self:parseAnnotations(self.ui.annotation.annotations, clippings[title])
return clippings
end

View File

@@ -36,8 +36,10 @@ local ReaderHighlight = require("apps/reader/modules/readerhighlight")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local filemanagerutil = require("apps/filemanager/filemanagerutil")
local T = require("ffi/util").template
local ffiUtil = require("ffi/util")
local logger = require("logger")
local util = require("util")
local T = ffiUtil.template
local _ = require("gettext")
-- update clippings from history clippings
@@ -104,10 +106,14 @@ end
local Exporter = WidgetContainer:extend{
name = "exporter",
default_clipping_dir = DataStorage:getFullDataDir() .. "/clipboard",
default_clipping_filename_single = "%D-%M %A - %T", -- "yyyy-mm-dd-hh-mm-ss author - title"
default_clipping_filename_multiple = "%D-%M all-books", -- see patterns in FileManagerBookInfo:expandString()
}
function Exporter:init()
self.parser = MyClipping:new{}
self.settings = G_reader_settings:readSetting("exporter", {})
self.parser = MyClipping:new{ ui = self.ui }
self.targets = genExportersTable(self.path)
self.ui.menu:registerToMainMenu(self)
self:onDispatcherRegisterActions()
@@ -130,13 +136,21 @@ function Exporter:isReady()
end
function Exporter:isDocReady()
return self.ui.document and true or false
return self.document and self.ui.annotation:hasAnnotations() and true or false
end
function Exporter:isReadyToExport()
return self:isDocReady() and self:isReady()
end
function Exporter:requiresFile()
for _, v in pairs(self.targets) do
if v:isEnabled() and not v.is_remote then
return true
end
end
end
function Exporter:requiresNetwork()
for k, v in pairs(self.targets) do
if v:isEnabled() then
@@ -147,15 +161,11 @@ function Exporter:requiresNetwork()
end
end
function Exporter:getDocumentClippings()
return self.parser:parseCurrentDoc(self.view) or {}
end
--- Parse and export highlights from the currently opened document.
function Exporter:onExportCurrentNotes()
if not self:isReadyToExport() then return end
self.ui.annotation:updatePageNumbers(true)
local clippings = self:getDocumentClippings()
local clippings = self.parser:parseCurrentDoc()
self:exportClippings(clippings)
end
@@ -196,19 +206,42 @@ function Exporter:exportClippings(clippings)
for _title, booknotes in pairs(clippings) do
table.insert(exportables, booknotes)
end
if #exportables == 0 then
UIManager:show(InfoMessage:new{ text = _("No highlights to export") })
return
end
local timestamp = os.time()
local clipping_filepath
if self:requiresFile() then
local file, clipping_dir, clipping_filename
if #exportables == 1 then
file = exportables[1].file
clipping_dir = self.settings.clipping_dir_book and ffiUtil.dirname(file)
or self.settings.clipping_dir or self.default_clipping_dir
clipping_filename = self.settings.clipping_filename_single or self.default_clipping_filename_single
else
clipping_dir = self.settings.clipping_dir or self.default_clipping_dir
clipping_filename = self.settings.clipping_filename_multiple or self.default_clipping_filename_multiple
end
-- full file path without extension
clipping_filepath = clipping_dir .. "/" ..
util.getSafeFilename(self.ui.bookinfo:expandString(clipping_filename, file, timestamp), nil, nil, -1)
end
local export_callback = function()
UIManager:nextTick(function()
local timestamp = os.time()
local statuses = {}
for k, v in pairs(self.targets) do
if v:isEnabled() then
if not v.is_remote then
v.filepath = clipping_filepath
end
v.timestamp = timestamp
local status = v:export(exportables)
if status then
if v.is_remote then
table.insert(statuses, T(_("%1: Exported successfully."), v.name))
else
table.insert(statuses, T(_("%1: Exported to %2."), v.name, v:getFilePath(exportables)))
table.insert(statuses, T(_("%1: Exported to %2."), v.name, v:getFilePath()))
end
else
table.insert(statuses, T(_("%1: Failed to export."), v.name))
@@ -218,7 +251,6 @@ function Exporter:exportClippings(clippings)
end
UIManager:show(InfoMessage:new{
text = table.concat(statuses, "\n"),
timeout = 3,
})
end)
@@ -243,7 +275,7 @@ function Exporter:addToMainMenu(menu_items)
share_submenu[#share_submenu + 1] = {
text = T(_("Share as %1"), v.name),
callback = function()
local clippings = self:getDocumentClippings()
local clippings = self.parser:parseCurrentDoc()
local document
for _, notes in pairs(clippings) do
document = notes or {}
@@ -258,7 +290,7 @@ function Exporter:addToMainMenu(menu_items)
table.sort(formats_submenu, function(v1, v2)
return v1.text < v2.text
end)
local settings = G_reader_settings:readSetting("exporter", {})
local settings = self.settings
for i, v in ipairs(ReaderHighlight.getHighlightStyles()) do
local style = v[2]
styles_submenu[i] = {
@@ -310,6 +342,13 @@ function Exporter:addToMainMenu(menu_items)
sub_item_table = styles_submenu,
separator = true,
},
{
text = _("Set export filename"),
keep_menu_open = true,
callback = function()
self:setFilename()
end,
},
{
text = _("Choose export folder"),
keep_menu_open = true,
@@ -344,13 +383,73 @@ function Exporter:addToMainMenu(menu_items)
menu_items.exporter = menu
end
function Exporter:setFilename()
local MultiInputDialog = require("ui/widget/multiinputdialog")
local dialog
dialog = MultiInputDialog:new{
title = _("Set export filename"),
fields = {
{
text = self.settings.clipping_filename_single or self.default_clipping_filename_single,
hint = self.default_clipping_filename_single,
description = _("Single book"),
},
{
text = self.settings.clipping_filename_multiple or self.default_clipping_filename_multiple,
hint = self.default_clipping_filename_multiple,
description = _("Multiple books"),
},
},
buttons = {
{
{
text = _("Info"),
callback = self.ui.bookinfo.expandString,
},
{
text = _("Default"),
callback = function()
dialog._input_widget:setText(dialog._input_widget.hint)
dialog._input_widget:goToEnd()
end,
},
},
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(dialog)
end,
},
{
text = _("Set"),
callback = function()
UIManager:close(dialog)
local filename_single, filename_multiple = unpack(dialog:getFields())
if filename_single == "" or filename_single == self.default_clipping_filename_single then
filename_single = nil
end
self.settings.clipping_filename_single = filename_single
if filename_multiple == "" or filename_multiple == self.default_clipping_filename_multiple then
filename_multiple = nil
end
self.settings.clipping_filename_multiple = filename_multiple
end,
},
},
},
}
UIManager:show(dialog)
dialog:onShowKeyboard()
end
function Exporter:chooseFolder()
local settings = G_reader_settings:readSetting("exporter", {})
local title_header = _("Current export folder:")
local current_path = settings.clipping_dir
local default_path = DataStorage:getFullDataDir() .. "/clipboard"
local current_path = self.settings.clipping_dir
local default_path = self.default_clipping_dir
local caller_callback = function(path)
settings.clipping_dir = path
self.settings.clipping_dir = path
end
filemanagerutil.showChooseDialog(title_header, caller_callback, current_path, default_path)
end

View File

@@ -78,10 +78,9 @@ describe("Exporter plugin module", function()
end)
it("should write clippings to a timestamped txt file", function()
local timestamp = os.time()
readerui.exporter.targets["text"].timestamp = timestamp
readerui.exporter.targets["text"].filepath = readerui.exporter.targets["text"]:getTimeStamp()
local exportable = { sample_clippings.Title1 }
local file_path = readerui.exporter.targets["text"]:getFilePath(exportable)
local file_path = readerui.exporter.targets["text"]:getFilePath()
readerui.exporter.targets["text"]:export(exportable)
local f = io.open(file_path, "r")
assert.is.truthy(string.find(f:read("*all"), "Some important stuff 1"))