diff --git a/plugins/exporter.koplugin/main.lua b/plugins/exporter.koplugin/main.lua index c153e25cd..12780f574 100644 --- a/plugins/exporter.koplugin/main.lua +++ b/plugins/exporter.koplugin/main.lua @@ -39,7 +39,7 @@ local _ = require("gettext") -- migrate settings from old "evernote.koplugin" or from previous (monolithic) "exporter.koplugin" local function migrateSettings() - local formats = { "flomo", "html", "joplin", "json", "memos", "my_clippings", "readwise", "text" } + local formats = { "flomo", "html", "joplin", "json", "memos", "my_clippings", "readwise", "text", "xmnote" } local settings = G_reader_settings:readSetting("exporter") if not settings then @@ -107,6 +107,7 @@ local Exporter = WidgetContainer:extend{ my_clippings = require("target/my_clippings"), readwise = require("target/readwise"), text = require("target/text"), + xmnote = require("target/xmnote"), }, } diff --git a/plugins/exporter.koplugin/target/xmnote.lua b/plugins/exporter.koplugin/target/xmnote.lua new file mode 100644 index 000000000..1e80a72ff --- /dev/null +++ b/plugins/exporter.koplugin/target/xmnote.lua @@ -0,0 +1,166 @@ +local InputDialog = require("ui/widget/inputdialog") +local UIManager = require("ui/uimanager") +local InfoMessage = require("ui/widget/infomessage") +local BD = require("ui/bidi") +local DataStorage = require("datastorage") +local http = require("socket.http") +local json = require("json") +local logger = require("logger") +local ltn12 = require("ltn12") +local socket = require("socket") +local socketutil = require("socketutil") +local util = require("ffi/util") +local T = util.template +local _ = require("gettext") + +-- xmnote exporter +local XMNoteExporter = require("base"):new { + name = "xmnote", + is_remote = true, + server_port = 8080 +} + +function XMNoteExporter:getMenuTable() + return { + text = _("XMNote"), + checked_func = function() return self:isEnabled() end, + sub_item_table = { + { + text = _("Set XMNote IP"), + keep_menu_open = true, + callback = function() + local url_dialog + url_dialog = InputDialog:new { + title = _("Set XMNote IP"), + input = self.settings.ip, + buttons = { + { + { + text = _("Cancel"), + callback = function() + UIManager:close(url_dialog) + end + }, + { + text = _("Set IP"), + callback = function() + local ip = url_dialog:getInputText() + self.settings.ip = ip + self:saveSettings() + UIManager:close(url_dialog) + end + } + } + } + } + UIManager:show(url_dialog) + url_dialog:onShowKeyboard() + end + } , + { + text = _("Export to XMNote"), + checked_func = function() return self:isEnabled() end, + callback = function() self:toggleEnabled() end, + }, + { + text = _("Help"), + keep_menu_open = true, + callback = function() + UIManager:show(InfoMessage:new { + text = T(_([[Before starting the export process, please make sure that your mobile and KOReader are connected to the same local network. Open XMNote and go to "My" - "Import Highlights" - "Import via API". At the bottom of the interface, you will find the IP address of your mobile device. Enter this IP address into KOReader to complete the configuration.]]) + , BD.dirpath(DataStorage:getDataDir())) + }) + end + } + } + } +end + +function XMNoteExporter:createRequestBody(booknotes) + local book = { + title = booknotes.title or "", + type = 1, + locationUnit = 1, + } + local entries = {} + for _, chapter in ipairs(booknotes) do + for _, clipping in ipairs(chapter) do + local entry = { + text = clipping.text, + note = clipping.note or "", + chapter = clipping.chapter, + time = clipping.time + } + local page = tonumber(clipping.page) + if page ~= nil then + entry.page = page + end + table.insert(entries, entry) + end + end + book.entries = entries + return book +end + +function XMNoteExporter:makeRequest(endpoint, method, request_body) + local sink = {} + local request_body_json = json.encode(request_body) + local source = ltn12.source.string(request_body_json) + socketutil:set_timeout(socketutil.LARGE_BLOCK_TIMEOUT, socketutil.LARGE_TOTAL_TIMEOUT) + local url = "http://".. self.settings.ip .. ":" .. self.server_port .. endpoint + local request = { + url = url, + method = method, + sink = ltn12.sink.table(sink), + source = source, + headers = { + ["Content-Length"] = #request_body_json, + ["Content-Type"] = "application/json" + }, + } + local code, headers, status = socket.skip(1, http.request(request)) + socketutil:reset_timeout() + + if code ~= 200 then + logger.warn("XMNoteClient: HTTP response code <> 200. Response status: ", status) + logger.dbg("Response headers:", headers) + return nil, status + end + + local response = json.decode(sink[1]) + local api_code = response["code"] + if api_code ~= nil and api_code ~= 200 then + logger.warn("XMNoteClient: response code <> 200. message: ", response["message"]) + logger.dbg("Response headers:", headers) + return nil, status + end + return response +end + +function XMNoteExporter:createHighlights(booknotes) + local body = self:createRequestBody(booknotes) + local result, err = self:makeRequest("/send", "POST", body) + if not result then + logger.warn("error creating highlights", err) + return false + end + + logger.dbg("createHighlights result", result) + return true +end + + +function XMNoteExporter:isReadyToExport() + return true +end + +function XMNoteExporter:export(t) + for _, booknotes in ipairs(t) do + local ok = self:createHighlights(booknotes) + if not ok then return false end + end + return true +end + +return XMNoteExporter +