mirror of
https://github.com/koreader/koreader.git
synced 2025-12-13 20:36:53 +01:00
Export highlights: customizable export filename (#14212)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
|
||||
Reference in New Issue
Block a user