2023-05-19 19:43:12 +00:00
|
|
|
--[[--
|
|
|
|
Export highlights to different targets.
|
2019-09-29 21:09:58 +00:00
|
|
|
|
2023-05-19 19:43:12 +00:00
|
|
|
Some conventions:
|
2014-04-23 14:19:29 +00:00
|
|
|
|
2023-05-19 19:43:12 +00:00
|
|
|
- Target: each local format or remote service this plugin can translate to.
|
2017-02-27 07:41:06 +00:00
|
|
|
|
2023-05-19 19:43:12 +00:00
|
|
|
Each new target should inherit from "formats/base" and implement *at least* an export function.
|
2016-07-05 14:15:52 +00:00
|
|
|
|
2023-05-19 19:43:12 +00:00
|
|
|
- Highlight: Text or image in document. Stored in "highlights" table of documents sidecar file.
|
2017-01-09 08:32:10 +00:00
|
|
|
|
2023-05-19 19:43:12 +00:00
|
|
|
Parser uses this table.
|
|
|
|
If highlight._._.text field is empty the parser uses highlight._._.pboxes field to get an image instead.
|
2017-01-09 08:32:10 +00:00
|
|
|
|
2023-05-19 19:43:12 +00:00
|
|
|
- Bookmarks: Data in bookmark explorer. Stored in "bookmarks" table of documents sidecar file.
|
2014-04-23 14:19:29 +00:00
|
|
|
|
2023-05-19 19:43:12 +00:00
|
|
|
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.
|
2019-09-29 21:09:58 +00:00
|
|
|
|
2023-05-19 19:43:12 +00:00
|
|
|
- Clippings: Parsed form of highlights. Single table for all documents.
|
2019-09-29 21:09:58 +00:00
|
|
|
|
2023-05-19 19:43:12 +00:00
|
|
|
- Booknotes: Every table in clippings table. clippings = {"title" = booknotes}
|
2014-04-23 14:19:29 +00:00
|
|
|
|
2023-05-19 19:43:12 +00:00
|
|
|
@module koplugin.exporter
|
|
|
|
--]]--
|
2022-01-04 20:21:58 +00:00
|
|
|
|
2022-05-06 20:44:28 +00:00
|
|
|
local DataStorage = require("datastorage")
|
|
|
|
local Device = require("device")
|
|
|
|
local InfoMessage = require("ui/widget/infomessage")
|
|
|
|
local MyClipping = require("clip")
|
|
|
|
local NetworkMgr = require("ui/network/manager")
|
|
|
|
local UIManager = require("ui/uimanager")
|
Clarify our OOP semantics across the codebase (#9586)
Basically:
* Use `extend` for class definitions
* Use `new` for object instantiations
That includes some minor code cleanups along the way:
* Updated `Widget`'s docs to make the semantics clearer.
* Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283)
* Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass).
* Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events.
* Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier.
* Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references.
* ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak).
* Terminal: Make sure the shell is killed on plugin teardown.
* InputText: Fix Home/End/Del physical keys to behave sensibly.
* InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...).
* OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of.
* ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed!
* Kobo: Minor code cleanups.
2022-10-06 00:14:48 +00:00
|
|
|
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
2023-12-16 08:36:57 +00:00
|
|
|
local filemanagerutil = require("apps/filemanager/filemanagerutil")
|
2023-02-24 23:05:26 +00:00
|
|
|
local T = require("ffi/util").template
|
2022-05-06 20:44:28 +00:00
|
|
|
local logger = require("logger")
|
|
|
|
local _ = require("gettext")
|
2014-04-23 14:19:29 +00:00
|
|
|
|
|
|
|
|
2022-05-06 20:44:28 +00:00
|
|
|
-- migrate settings from old "evernote.koplugin" or from previous (monolithic) "exporter.koplugin"
|
|
|
|
local function migrateSettings()
|
2023-11-19 09:32:27 +00:00
|
|
|
local formats = { "flomo", "html", "joplin", "json", "memos", "my_clippings", "readwise", "text", "xmnote" }
|
2014-04-23 14:19:29 +00:00
|
|
|
|
2022-05-06 20:44:28 +00:00
|
|
|
local settings = G_reader_settings:readSetting("exporter")
|
|
|
|
if not settings then
|
|
|
|
settings = G_reader_settings:readSetting("evernote")
|
|
|
|
end
|
2014-04-23 14:19:29 +00:00
|
|
|
|
2022-05-06 20:44:28 +00:00
|
|
|
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
|
2014-04-23 14:19:29 +00:00
|
|
|
end
|
|
|
|
|
2022-05-06 20:44:28 +00:00
|
|
|
-- update clippings from history clippings
|
|
|
|
local function updateHistoryClippings(clippings, new_clippings)
|
2014-05-12 10:07:20 +00:00
|
|
|
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
|
2020-06-27 15:35:25 +00:00
|
|
|
logger.dbg("found new notes in history", booknotes.title)
|
2014-05-12 10:07:20 +00:00
|
|
|
clippings[title] = booknotes
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return clippings
|
|
|
|
end
|
|
|
|
|
2022-05-06 20:44:28 +00:00
|
|
|
-- update clippings from Kindle annotation system
|
|
|
|
local function updateMyClippings(clippings, new_clippings)
|
2014-05-12 13:19:17 +00:00
|
|
|
-- 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
|
2020-06-27 15:35:25 +00:00
|
|
|
logger.dbg("found new notes in MyClipping", booknotes.title)
|
2014-05-12 13:19:17 +00:00
|
|
|
clippings[title] = booknotes
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return clippings
|
|
|
|
end
|
|
|
|
|
Clarify our OOP semantics across the codebase (#9586)
Basically:
* Use `extend` for class definitions
* Use `new` for object instantiations
That includes some minor code cleanups along the way:
* Updated `Widget`'s docs to make the semantics clearer.
* Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283)
* Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass).
* Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events.
* Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier.
* Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references.
* ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak).
* Terminal: Make sure the shell is killed on plugin teardown.
* InputText: Fix Home/End/Del physical keys to behave sensibly.
* InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...).
* OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of.
* ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed!
* Kobo: Minor code cleanups.
2022-10-06 00:14:48 +00:00
|
|
|
local Exporter = WidgetContainer:extend{
|
2022-05-06 20:44:28 +00:00
|
|
|
name = "exporter",
|
|
|
|
targets = {
|
2023-10-12 12:54:31 +00:00
|
|
|
flomo = require("target/flomo"),
|
2022-05-06 20:44:28 +00:00
|
|
|
html = require("target/html"),
|
|
|
|
joplin = require("target/joplin"),
|
|
|
|
json = require("target/json"),
|
2022-05-28 08:32:36 +00:00
|
|
|
markdown = require("target/markdown"),
|
2023-05-18 11:10:09 +00:00
|
|
|
memos = require("target/memos"),
|
2023-04-28 23:19:13 +00:00
|
|
|
my_clippings = require("target/my_clippings"),
|
2023-10-12 12:54:31 +00:00
|
|
|
readwise = require("target/readwise"),
|
|
|
|
text = require("target/text"),
|
2023-11-19 09:32:27 +00:00
|
|
|
xmnote = require("target/xmnote"),
|
2022-05-06 20:44:28 +00:00
|
|
|
},
|
|
|
|
}
|
2020-01-25 16:28:13 +00:00
|
|
|
|
2022-05-06 20:44:28 +00:00
|
|
|
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
|
2014-04-23 14:19:29 +00:00
|
|
|
end
|
2022-05-06 20:44:28 +00:00
|
|
|
self.ui.menu:registerToMainMenu(self)
|
2014-04-23 14:19:29 +00:00
|
|
|
end
|
|
|
|
|
2022-05-06 20:44:28 +00:00
|
|
|
function Exporter:isReady()
|
|
|
|
for k, v in pairs(self.targets) do
|
|
|
|
if v:isEnabled() then
|
|
|
|
return true
|
2014-05-15 09:27:05 +00:00
|
|
|
end
|
2017-01-21 09:32:42 +00:00
|
|
|
end
|
2022-05-06 20:44:28 +00:00
|
|
|
return false
|
2014-04-23 14:19:29 +00:00
|
|
|
end
|
|
|
|
|
2022-05-06 20:44:28 +00:00
|
|
|
function Exporter:isDocReady()
|
2022-11-06 10:01:59 +00:00
|
|
|
return self.ui and self.ui.document and self.view or false
|
2022-06-03 17:56:10 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function Exporter:isReadyToExport()
|
|
|
|
return self:isDocReady() and self:isReady()
|
2022-03-19 08:59:04 +00:00
|
|
|
end
|
|
|
|
|
2022-05-06 20:44:28 +00:00
|
|
|
function Exporter:requiresNetwork()
|
|
|
|
for k, v in pairs(self.targets) do
|
|
|
|
if v:isEnabled() then
|
|
|
|
if v.is_remote then
|
|
|
|
return true
|
2022-03-19 08:59:04 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-06-03 17:56:10 +00:00
|
|
|
function Exporter:getDocumentClippings()
|
|
|
|
return self.parser:parseCurrentDoc(self.view) or {}
|
|
|
|
end
|
|
|
|
|
2023-05-19 07:55:49 +00:00
|
|
|
--- Parse and export highlights from the currently opened document.
|
2022-05-06 20:44:28 +00:00
|
|
|
function Exporter:exportCurrentNotes()
|
2024-07-24 15:35:48 +00:00
|
|
|
self.ui.annotation:updatePageNumbers(true)
|
2022-06-03 17:56:10 +00:00
|
|
|
local clippings = self:getDocumentClippings()
|
2022-05-06 20:44:28 +00:00
|
|
|
self:exportClippings(clippings)
|
2020-04-26 17:36:56 +00:00
|
|
|
end
|
|
|
|
|
2023-05-19 07:55:49 +00:00
|
|
|
--- Parse and export highlights from all the documents in History
|
|
|
|
-- and from the Kindle "My Clippings.txt".
|
2022-05-06 20:44:28 +00:00
|
|
|
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
|
2017-01-21 09:32:42 +00:00
|
|
|
end
|
|
|
|
end
|
2022-05-06 20:44:28 +00:00
|
|
|
self:exportClippings(clippings)
|
2017-01-21 09:32:42 +00:00
|
|
|
end
|
|
|
|
|
2023-05-19 07:55:49 +00:00
|
|
|
--- Parse and export highlights from selected documents.
|
2023-05-19 19:43:12 +00:00
|
|
|
-- @tparam table files list of files as a table of {[file_path] = true}
|
2023-05-19 07:55:49 +00:00
|
|
|
function Exporter:exportFilesNotes(files)
|
|
|
|
local clippings = self.parser:parseFiles(files)
|
|
|
|
for title, booknotes in pairs(clippings) do
|
|
|
|
-- chapter number is zero
|
|
|
|
if #booknotes == 0 then
|
|
|
|
clippings[title] = nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
self:exportClippings(clippings)
|
|
|
|
end
|
|
|
|
|
2022-05-06 20:44:28 +00:00
|
|
|
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()
|
2022-06-04 07:37:26 +00:00
|
|
|
local statuses = {}
|
2022-05-06 20:44:28 +00:00
|
|
|
for k, v in pairs(self.targets) do
|
|
|
|
if v:isEnabled() then
|
|
|
|
v.timestamp = timestamp
|
2022-06-04 07:37:26 +00:00
|
|
|
local status = v:export(exportables)
|
|
|
|
if status then
|
2022-06-04 20:10:07 +00:00
|
|
|
if v.is_remote then
|
2023-02-24 23:05:26 +00:00
|
|
|
table.insert(statuses, T(_("%1: Exported successfully."), v.name))
|
2022-06-04 20:10:07 +00:00
|
|
|
else
|
2023-02-24 23:05:26 +00:00
|
|
|
table.insert(statuses, T(_("%1: Exported to %2."), v.name, v:getFilePath(exportables)))
|
2022-06-04 20:10:07 +00:00
|
|
|
end
|
2022-06-04 07:37:26 +00:00
|
|
|
else
|
2023-02-24 23:05:26 +00:00
|
|
|
table.insert(statuses, T(_("%1: Failed to export."), v.name))
|
2022-06-04 07:37:26 +00:00
|
|
|
end
|
2022-05-06 20:44:28 +00:00
|
|
|
v.timestamp = nil
|
|
|
|
end
|
2021-12-25 09:24:48 +00:00
|
|
|
end
|
2022-06-04 07:37:26 +00:00
|
|
|
UIManager:show(InfoMessage:new{
|
|
|
|
text = table.concat(statuses, "\n"),
|
|
|
|
timeout = 3,
|
|
|
|
})
|
2022-05-06 20:44:28 +00:00
|
|
|
end)
|
2019-09-29 21:09:58 +00:00
|
|
|
|
2022-05-06 20:44:28 +00:00
|
|
|
UIManager:show(InfoMessage:new {
|
|
|
|
text = _("Exporting may take several seconds…"),
|
|
|
|
timeout = 1,
|
|
|
|
})
|
|
|
|
end
|
|
|
|
if self:requiresNetwork() then
|
|
|
|
NetworkMgr:runWhenOnline(export_callback)
|
2019-09-29 21:09:58 +00:00
|
|
|
else
|
2022-05-06 20:44:28 +00:00
|
|
|
export_callback()
|
2019-09-29 21:09:58 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-05-06 20:44:28 +00:00
|
|
|
function Exporter:addToMainMenu(menu_items)
|
|
|
|
local submenu = {}
|
2022-06-03 17:56:10 +00:00
|
|
|
local sharemenu = {}
|
2022-05-06 20:44:28 +00:00
|
|
|
for k, v in pairs(self.targets) do
|
|
|
|
submenu[#submenu + 1] = v:getMenuTable()
|
2022-06-03 17:56:10 +00:00
|
|
|
if v.shareable then
|
2023-02-24 23:05:26 +00:00
|
|
|
sharemenu[#sharemenu + 1] = { text = T(_("Share as %1."), v.name), callback = function()
|
2022-06-03 17:56:10 +00:00
|
|
|
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
|
2022-05-06 20:44:28 +00:00
|
|
|
end
|
|
|
|
table.sort(submenu, function(v1, v2)
|
|
|
|
return v1.text < v2.text
|
|
|
|
end)
|
2022-06-03 17:56:10 +00:00
|
|
|
local menu = {
|
2022-05-06 20:44:28 +00:00
|
|
|
text = _("Export highlights"),
|
|
|
|
sub_item_table = {
|
|
|
|
{
|
|
|
|
text = _("Export all notes in this book"),
|
|
|
|
enabled_func = function()
|
2022-06-03 17:56:10 +00:00
|
|
|
return self:isReadyToExport()
|
2022-05-06 20:44:28 +00:00
|
|
|
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,
|
2022-06-03 17:56:10 +00:00
|
|
|
separator = #sharemenu == 0,
|
2022-05-06 20:44:28 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
text = _("Choose formats and services"),
|
|
|
|
sub_item_table = submenu,
|
2023-05-18 20:11:35 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
text = _("Choose export folder"),
|
|
|
|
keep_menu_open = true,
|
|
|
|
callback = function()
|
|
|
|
self:chooseFolder()
|
|
|
|
end,
|
2022-05-06 20:44:28 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
2022-06-03 17:56:10 +00:00
|
|
|
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
|
2021-12-25 09:24:48 +00:00
|
|
|
end
|
|
|
|
|
2023-05-18 20:11:35 +00:00
|
|
|
function Exporter:chooseFolder()
|
2023-12-16 08:36:57 +00:00
|
|
|
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 caller_callback = function(path)
|
|
|
|
settings.clipping_dir = path
|
|
|
|
for _, target in pairs(self.targets) do
|
|
|
|
target.clipping_dir = path
|
2023-05-18 20:11:35 +00:00
|
|
|
end
|
|
|
|
end
|
2023-12-16 08:36:57 +00:00
|
|
|
filemanagerutil.showChooseDialog(title_header, caller_callback, current_path, default_path)
|
2023-05-18 20:11:35 +00:00
|
|
|
end
|
|
|
|
|
2021-08-30 07:11:23 +00:00
|
|
|
return Exporter
|