2
0
mirror of https://github.com/koreader/koreader synced 2024-11-20 03:25:34 +00:00
koreader/plugins/exporter.koplugin/main.lua
2022-06-04 22:10:07 +02:00

284 lines
9.3 KiB
Lua

--[[
Export highlights to different targets.
Some conventions:
- Target: each local format or remote service this plugin can translate to.
Each new target should inherit from "formats/base" and implement *at least* an export function.
- Highlight: Text or image in document. Stored in "highlights" table of documents sidecar file.
Parser uses this table.
If highlight._._.text field is empty the parser uses highlight._._.pboxes field to get an image instead.
- Bookmarks: Data in bookmark explorer. Stored in "bookmarks" table of documents sidecar file.
Every field in bookmarks._ has "text" and "notes" fields.
When user edits a highlight or "renames" bookmark the text field is created or updated.
The parser looks to bookmarks._.text field for edited notes. bookmarks._.notes isn't used for exporting operations.
- Clippings: Parsed form of highlights. Single table for all documents.
- Booknotes: Every table in clippings table. clippings = {"title" = booknotes}
--]]
local DataStorage = require("datastorage")
local Device = require("device")
local InfoMessage = require("ui/widget/infomessage")
local InputContainer = require("ui/widget/container/inputcontainer")
local MyClipping = require("clip")
local NetworkMgr = require("ui/network/manager")
local UIManager = require("ui/uimanager")
local logger = require("logger")
local _ = require("gettext")
-- migrate settings from old "evernote.koplugin" or from previous (monolithic) "exporter.koplugin"
local function migrateSettings()
local formats = { "html", "joplin", "json", "readwise", "text" }
local settings = G_reader_settings:readSetting("exporter")
if not settings then
settings = G_reader_settings:readSetting("evernote")
end
if type(settings) == "table" then
for _, fmt in ipairs(formats) do
if type(settings[fmt]) == "table" then return end
end
local new_settings = {}
for _, fmt in ipairs(formats) do
new_settings[fmt] = { enabled = false }
end
new_settings["joplin"].ip = settings.joplin_IP
new_settings["joplin"].port = settings.joplin_port
new_settings["joplin"].token = settings.joplin_token
new_settings["readwise"].token = settings.readwise_token
G_reader_settings:saveSetting("exporter", new_settings)
end
end
-- update clippings from history clippings
local function updateHistoryClippings(clippings, new_clippings)
for title, booknotes in pairs(new_clippings) do
for chapter_index, chapternotes in ipairs(booknotes) do
for note_index, note in ipairs(chapternotes) do
if clippings[title] == nil or clippings[title][chapter_index] == nil
or clippings[title][chapter_index][note_index] == nil
or clippings[title][chapter_index][note_index].page ~= note.page
or clippings[title][chapter_index][note_index].time ~= note.time
or clippings[title][chapter_index][note_index].text ~= note.text
or clippings[title][chapter_index][note_index].note ~= note.note then
logger.dbg("found new notes in history", booknotes.title)
clippings[title] = booknotes
end
end
end
end
return clippings
end
-- update clippings from Kindle annotation system
local function updateMyClippings(clippings, new_clippings)
-- only new titles or new notes in My clippings are updated to clippings
-- since appending is the only way to modify notes in My Clippings
for title, booknotes in pairs(new_clippings) do
if clippings[title] == nil or #clippings[title] < #booknotes then
logger.dbg("found new notes in MyClipping", booknotes.title)
clippings[title] = booknotes
end
end
return clippings
end
local Exporter = InputContainer:new {
name = "exporter",
clipping_dir = DataStorage:getDataDir() .. "/clipboard",
targets = {
html = require("target/html"),
joplin = require("target/joplin"),
json = require("target/json"),
markdown = require("target/markdown"),
readwise = require("target/readwise"),
text = require("target/text"),
},
}
function Exporter:init()
migrateSettings()
self.parser = MyClipping:new {
history_dir = DataStorage:getDataDir() .. "/history",
}
for k, _ in pairs(self.targets) do
self.targets[k].path = self.path
end
self.ui.menu:registerToMainMenu(self)
end
function Exporter:isReady()
for k, v in pairs(self.targets) do
if v:isEnabled() then
return true
end
end
return false
end
function Exporter:isDocReady()
return self.ui and self.ui.document and self.view
end
function Exporter:isReadyToExport()
return self:isDocReady() and self:isReady()
end
function Exporter:requiresNetwork()
for k, v in pairs(self.targets) do
if v:isEnabled() then
if v.is_remote then
return true
end
end
end
end
function Exporter:getDocumentClippings()
return self.parser:parseCurrentDoc(self.view) or {}
end
function Exporter:exportCurrentNotes()
local clippings = self:getDocumentClippings()
self:exportClippings(clippings)
end
function Exporter:exportAllNotes()
local clippings = {}
clippings = updateHistoryClippings(clippings, self.parser:parseHistory())
if Device:isKindle() then
clippings = updateMyClippings(clippings, self.parser:parseMyClippings())
end
for title, booknotes in pairs(clippings) do
-- chapter number is zero
if #booknotes == 0 then
clippings[title] = nil
end
end
self:exportClippings(clippings)
end
function Exporter:exportClippings(clippings)
if type(clippings) ~= "table" then return end
local exportables = {}
for _title, booknotes in pairs(clippings) do
table.insert(exportables, booknotes)
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
v.timestamp = timestamp
local status = v:export(exportables)
if status then
if v.is_remote then
table.insert(statuses, _(v.name .. ": Exported successfully."))
else
table.insert(statuses, _(v.name .. ": Exported to " ) .. v:getFilePath(exportables))
end
else
table.insert(statuses, _(v.name .. ": Failed to export."))
end
v.timestamp = nil
end
end
UIManager:show(InfoMessage:new{
text = table.concat(statuses, "\n"),
timeout = 3,
})
end)
UIManager:show(InfoMessage:new {
text = _("Exporting may take several seconds…"),
timeout = 1,
})
end
if self:requiresNetwork() then
NetworkMgr:runWhenOnline(export_callback)
else
export_callback()
end
end
function Exporter:addToMainMenu(menu_items)
local submenu = {}
local sharemenu = {}
for k, v in pairs(self.targets) do
submenu[#submenu + 1] = v:getMenuTable()
if v.shareable then
sharemenu[#sharemenu + 1] = { text = _("Share as " .. v.name), callback = function()
local clippings = self:getDocumentClippings()
local document
for _, notes in pairs(clippings) do
document = notes or {}
end
if #document > 0 then
v:share(document)
end
end
}
end
end
table.sort(submenu, function(v1, v2)
return v1.text < v2.text
end)
local menu = {
text = _("Export highlights"),
sub_item_table = {
{
text = _("Export all notes in this book"),
enabled_func = function()
return self:isReadyToExport()
end,
callback = function()
self:exportCurrentNotes()
end,
},
{
text = _("Export all notes in your library"),
enabled_func = function()
return self:isReady()
end,
callback = function()
self:exportAllNotes()
end,
separator = #sharemenu == 0,
},
{
text = _("Choose formats and services"),
sub_item_table = submenu,
separator = true,
},
}
}
if #sharemenu > 0 then
table.sort(sharemenu, function(v1, v2)
return v1.text < v2.text
end)
table.insert(menu.sub_item_table, 3, {
text = _("Share all notes in this book"),
enabled_func = function()
return self:isDocReady()
end,
sub_item_table = sharemenu,
separator = true,
})
end
menu_items.exporter = menu
end
return Exporter