diff --git a/plugins/exporter.koplugin/base.lua b/plugins/exporter.koplugin/base.lua index 26cc464c5..2a69dbaa2 100644 --- a/plugins/exporter.koplugin/base.lua +++ b/plugins/exporter.koplugin/base.lua @@ -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 diff --git a/plugins/exporter.koplugin/clip.lua b/plugins/exporter.koplugin/clip.lua index 7ef219cd8..41f005337 100644 --- a/plugins/exporter.koplugin/clip.lua +++ b/plugins/exporter.koplugin/clip.lua @@ -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 diff --git a/plugins/exporter.koplugin/main.lua b/plugins/exporter.koplugin/main.lua index 8c45e079b..e537f3259 100644 --- a/plugins/exporter.koplugin/main.lua +++ b/plugins/exporter.koplugin/main.lua @@ -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 diff --git a/spec/unit/exporter_plugin_main_spec.lua b/spec/unit/exporter_plugin_main_spec.lua index c968a2681..50c564d75 100644 --- a/spec/unit/exporter_plugin_main_spec.lua +++ b/spec/unit/exporter_plugin_main_spec.lua @@ -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"))