--[[ 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"), 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() local docless = self.ui == nil or self.ui.document == nil or self.view == nil return not docless 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:exportCurrentNotes() local clippings = self.parser:parseCurrentDoc(self.view) 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() for k, v in pairs(self.targets) do if v:isEnabled() then v.timestamp = timestamp v:export(exportables) v.timestamp = nil end end 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 = {} for k, v in pairs(self.targets) do submenu[#submenu + 1] = v:getMenuTable() end table.sort(submenu, function(v1, v2) return v1.text < v2.text end) menu_items.exporter = { text = _("Export highlights"), sub_item_table = { { text = _("Export all notes in this book"), enabled_func = function() return self:isDocReady() 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 = true, }, { text = _("Choose formats and services"), sub_item_table = submenu, separator = true, }, } } end return Exporter