mirror of
https://github.com/koreader/koreader.git
synced 2025-08-10 00:52:38 +00:00
add Evernote plugin to export highlights and notes
The "My Clipping" file that storing highlights and notes for Kindle native readers could also be parsed and exported. The parser is implemented in `evernote.koplugin/clip.lua`. Parsed highlights and notes in one book will be packed and rendered into html node with a slt2 template `note.tpl` that complies with evernote markup language(ENML). Finally the evernote client will create or update note entries and push them to Evernote cloud.
This commit is contained in:
@@ -18,3 +18,10 @@ trim_trailing_whitespace = false
|
||||
indent_style = tab
|
||||
indent_size = 8
|
||||
|
||||
[Makefile.def]
|
||||
indent_style = tab
|
||||
indent_size = 8
|
||||
|
||||
[*.{js,json,css,scss,sass,html,handlebars,tpl}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -10,6 +10,7 @@ git-rev
|
||||
*.o
|
||||
tags
|
||||
test/*
|
||||
*.tar
|
||||
|
||||
emu
|
||||
|
||||
@@ -18,4 +19,6 @@ koreader-*.zip
|
||||
/.cproject
|
||||
/.project
|
||||
|
||||
koreader-arm-linux-gnueabi
|
||||
koreader-x86_64-linux-gnu
|
||||
|
||||
|
||||
2
Makefile
2
Makefile
@@ -39,6 +39,8 @@ endif
|
||||
for f in $(INSTALL_FILES); do \
|
||||
ln -sf ../../$$f $(INSTALL_DIR)/koreader/; \
|
||||
done
|
||||
# install plugins
|
||||
cp -r plugins/* $(INSTALL_DIR)/koreader/plugins/
|
||||
cp -rpL resources/fonts/* $(INSTALL_DIR)/koreader/fonts/
|
||||
mkdir -p $(INSTALL_DIR)/koreader/screenshots
|
||||
mkdir -p $(INSTALL_DIR)/koreader/data/dict
|
||||
|
||||
@@ -18,13 +18,13 @@ local InputDialog = InputContainer:new{
|
||||
buttons = nil,
|
||||
input_type = nil,
|
||||
enter_callback = nil,
|
||||
|
||||
|
||||
width = nil,
|
||||
height = nil,
|
||||
|
||||
|
||||
title_face = Font:getFace("tfont", 22),
|
||||
input_face = Font:getFace("cfont", 20),
|
||||
|
||||
|
||||
title_padding = Screen:scaleByDPI(5),
|
||||
title_margin = Screen:scaleByDPI(2),
|
||||
input_padding = Screen:scaleByDPI(10),
|
||||
@@ -53,21 +53,21 @@ function InputDialog:init()
|
||||
scroll = false,
|
||||
parent = self,
|
||||
}
|
||||
local button_table = ButtonTable:new{
|
||||
self.button_table = ButtonTable:new{
|
||||
width = self.width,
|
||||
button_font_face = "cfont",
|
||||
button_font_size = 20,
|
||||
buttons = self.buttons,
|
||||
zero_sep = true,
|
||||
}
|
||||
local title_bar = LineWidget:new{
|
||||
self.title_bar = LineWidget:new{
|
||||
--background = 8,
|
||||
dimen = Geom:new{
|
||||
w = button_table:getSize().w + self.button_padding,
|
||||
w = self.button_table:getSize().w + self.button_padding,
|
||||
h = Screen:scaleByDPI(2),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
self.dialog_frame = FrameContainer:new{
|
||||
radius = 8,
|
||||
bordersize = 3,
|
||||
@@ -77,11 +77,11 @@ function InputDialog:init()
|
||||
VerticalGroup:new{
|
||||
align = "left",
|
||||
self.title,
|
||||
title_bar,
|
||||
self.title_bar,
|
||||
-- input
|
||||
CenterContainer:new{
|
||||
dimen = Geom:new{
|
||||
w = title_bar:getSize().w,
|
||||
w = self.title_bar:getSize().w,
|
||||
h = self.input:getSize().h,
|
||||
},
|
||||
self.input,
|
||||
@@ -89,14 +89,14 @@ function InputDialog:init()
|
||||
-- buttons
|
||||
CenterContainer:new{
|
||||
dimen = Geom:new{
|
||||
w = title_bar:getSize().w,
|
||||
h = button_table:getSize().h,
|
||||
w = self.title_bar:getSize().w,
|
||||
h = self.button_table:getSize().h,
|
||||
},
|
||||
button_table,
|
||||
self.button_table,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
self[1] = CenterContainer:new{
|
||||
dimen = Geom:new{
|
||||
w = Screen:getWidth(),
|
||||
|
||||
@@ -3,9 +3,13 @@ local ScrollTextWidget = require("ui/widget/scrolltextwidget")
|
||||
local TextBoxWidget = require("ui/widget/textboxwidget")
|
||||
local FrameContainer = require("ui/widget/container/framecontainer")
|
||||
local VirtualKeyboard = require("ui/widget/virtualkeyboard")
|
||||
local Font = require("ui/font")
|
||||
local Screen = require("ui/screen")
|
||||
local GestureRange = require("ui/gesturerange")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local Geom = require("ui/geometry")
|
||||
local Device = require("ui/device")
|
||||
local Screen = require("ui/screen")
|
||||
local Font = require("ui/font")
|
||||
local DEBUG = require("dbg")
|
||||
|
||||
local InputText = InputContainer:new{
|
||||
text = "",
|
||||
@@ -13,40 +17,51 @@ local InputText = InputContainer:new{
|
||||
charlist = {}, -- table to store input string
|
||||
charpos = 1,
|
||||
input_type = nil,
|
||||
|
||||
text_type = nil,
|
||||
|
||||
width = nil,
|
||||
height = nil,
|
||||
face = Font:getFace("cfont", 22),
|
||||
|
||||
|
||||
padding = 5,
|
||||
margin = 5,
|
||||
bordersize = 2,
|
||||
|
||||
|
||||
parent = nil, -- parent dialog that will be set dirty
|
||||
scroll = false,
|
||||
focused = true,
|
||||
}
|
||||
|
||||
function InputText:init()
|
||||
self:StringToCharlist(self.text)
|
||||
self:initTextBox()
|
||||
self:initKeyboard()
|
||||
if Device:isTouchDevice() then
|
||||
self.ges_events = {
|
||||
TapTextBox = {
|
||||
GestureRange:new{
|
||||
ges = "tap",
|
||||
range = self.dimen
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
function InputText:initTextBox()
|
||||
local bgcolor = nil
|
||||
local fgcolor = nil
|
||||
if self.text == "" then
|
||||
self.text = self.hint
|
||||
bgcolor = 0.0
|
||||
fgcolor = 0.5
|
||||
else
|
||||
bgcolor = 0.0
|
||||
fgcolor = 1.0
|
||||
end
|
||||
local bgcolor, fgcolor = 0.0, self.text == "" and 0.5 or 1.0
|
||||
|
||||
local text_widget = nil
|
||||
local show_text = self.text
|
||||
if self.text_type == "password" and show_text ~= "" then
|
||||
show_text = self.text:gsub("(.-).", function() return "*" end)
|
||||
show_text = show_text:gsub("(.)$", function() return self.text:sub(-1) end)
|
||||
elseif show_text == "" then
|
||||
show_text = self.hint
|
||||
end
|
||||
if self.scroll then
|
||||
text_widget = ScrollTextWidget:new{
|
||||
text = self.text,
|
||||
text = show_text,
|
||||
face = self.face,
|
||||
bgcolor = bgcolor,
|
||||
fgcolor = fgcolor,
|
||||
@@ -55,7 +70,7 @@ function InputText:initTextBox()
|
||||
}
|
||||
else
|
||||
text_widget = TextBoxWidget:new{
|
||||
text = self.text,
|
||||
text = show_text,
|
||||
face = self.face,
|
||||
bgcolor = bgcolor,
|
||||
fgcolor = fgcolor,
|
||||
@@ -67,6 +82,7 @@ function InputText:initTextBox()
|
||||
bordersize = self.bordersize,
|
||||
padding = self.padding,
|
||||
margin = self.margin,
|
||||
color = self.focused and 15 or 8,
|
||||
text_widget,
|
||||
}
|
||||
self.dimen = self[1]:getSize()
|
||||
@@ -85,6 +101,22 @@ function InputText:initKeyboard()
|
||||
}
|
||||
end
|
||||
|
||||
function InputText:onTapTextBox()
|
||||
if self.parent.onSwitchFocus then
|
||||
self.parent:onSwitchFocus(self)
|
||||
end
|
||||
end
|
||||
|
||||
function InputText:unfocus()
|
||||
self.focused = false
|
||||
self[1].color = 8
|
||||
end
|
||||
|
||||
function InputText:focus()
|
||||
self.focused = true
|
||||
self[1].color = 15
|
||||
end
|
||||
|
||||
function InputText:onShowKeyboard()
|
||||
UIManager:show(self.keyboard)
|
||||
end
|
||||
|
||||
113
frontend/ui/widget/logindialog.lua
Normal file
113
frontend/ui/widget/logindialog.lua
Normal file
@@ -0,0 +1,113 @@
|
||||
local FrameContainer = require("ui/widget/container/framecontainer")
|
||||
local CenterContainer = require("ui/widget/container/centercontainer")
|
||||
local VerticalGroup = require("ui/widget/verticalgroup")
|
||||
local InputDialog = require("ui/widget/inputdialog")
|
||||
local InputText = require("ui/widget/inputtext")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local Geom = require("ui/geometry")
|
||||
local Screen = require("ui/screen")
|
||||
local DEBUG = require("dbg")
|
||||
local _ = require("gettext")
|
||||
|
||||
local LoginDialog = InputDialog:extend{
|
||||
username = "",
|
||||
username_hint = "username",
|
||||
password = "",
|
||||
password_hint = "password",
|
||||
}
|
||||
|
||||
function LoginDialog:init()
|
||||
-- init title and buttons in base class
|
||||
InputDialog.init(self)
|
||||
self.input_username = InputText:new{
|
||||
text = self.username,
|
||||
hint = self.username_hint,
|
||||
face = self.input_face,
|
||||
width = self.width * 0.9,
|
||||
focused = true,
|
||||
scroll = false,
|
||||
parent = self,
|
||||
}
|
||||
|
||||
self.input_password = InputText:new{
|
||||
text = self.password,
|
||||
hint = self.password_hint,
|
||||
face = self.input_face,
|
||||
width = self.width * 0.9,
|
||||
text_type = "password",
|
||||
focused = false,
|
||||
scroll = false,
|
||||
parent = self,
|
||||
}
|
||||
|
||||
self.dialog_frame = FrameContainer:new{
|
||||
radius = 8,
|
||||
bordersize = 3,
|
||||
padding = 0,
|
||||
margin = 0,
|
||||
background = 0,
|
||||
VerticalGroup:new{
|
||||
align = "left",
|
||||
self.title,
|
||||
self.title_bar,
|
||||
-- username input
|
||||
CenterContainer:new{
|
||||
dimen = Geom:new{
|
||||
w = self.title_bar:getSize().w,
|
||||
h = self.input_username:getSize().h,
|
||||
},
|
||||
self.input_username,
|
||||
},
|
||||
-- password input
|
||||
CenterContainer:new{
|
||||
dimen = Geom:new{
|
||||
w = self.title_bar:getSize().w,
|
||||
h = self.input_password:getSize().h,
|
||||
},
|
||||
self.input_password,
|
||||
},
|
||||
-- buttons
|
||||
CenterContainer:new{
|
||||
dimen = Geom:new{
|
||||
w = self.title_bar:getSize().w,
|
||||
h = self.button_table:getSize().h,
|
||||
},
|
||||
self.button_table,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.input = self.input_username
|
||||
|
||||
self[1] = CenterContainer:new{
|
||||
dimen = Geom:new{
|
||||
w = Screen:getWidth(),
|
||||
h = Screen:getHeight() - self.input:getKeyboardDimen().h,
|
||||
},
|
||||
self.dialog_frame,
|
||||
}
|
||||
UIManager.repaint_all = true
|
||||
UIManager.full_refresh = true
|
||||
end
|
||||
|
||||
function LoginDialog:getCredential()
|
||||
local username = self.input_username:getText()
|
||||
local password = self.input_password:getText()
|
||||
return username, password
|
||||
end
|
||||
|
||||
function LoginDialog:onSwitchFocus(inputbox)
|
||||
-- unfocus current inputbox
|
||||
self.input:unfocus()
|
||||
self.input:onCloseKeyboard()
|
||||
|
||||
-- focus new inputbox
|
||||
self.input = inputbox
|
||||
self.input:focus()
|
||||
self.input:onShowKeyboard()
|
||||
|
||||
UIManager:show(self)
|
||||
end
|
||||
|
||||
return LoginDialog
|
||||
|
||||
Submodule koreader-base updated: c087744408...4e15f4085f
262
plugins/evernote.koplugin/clip.lua
Normal file
262
plugins/evernote.koplugin/clip.lua
Normal file
@@ -0,0 +1,262 @@
|
||||
-- lfs
|
||||
|
||||
local MyClipping = {
|
||||
my_clippings = "/mnt/us/documents/My Clippings.txt",
|
||||
history_dir = "./history",
|
||||
}
|
||||
|
||||
function MyClipping:new(o)
|
||||
o = o or {}
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
return o
|
||||
end
|
||||
|
||||
--[[
|
||||
-- clippings: main table to store parsed highlights and notes entries
|
||||
-- {
|
||||
-- ["Title(Author Name)"] = {
|
||||
-- {
|
||||
-- {
|
||||
-- ["page"] = 123,
|
||||
-- ["time"] = 1398127554,
|
||||
-- ["text"] = "Games of all sorts were played in homes and fields."
|
||||
-- },
|
||||
-- {
|
||||
-- ["page"] = 156,
|
||||
-- ["time"] = 1398128287,
|
||||
-- ["text"] = "There Spenser settled down to gentleman farming.",
|
||||
-- ["note"] = "This is a sample note.",
|
||||
-- },
|
||||
-- ["title"] = "Chapter I"
|
||||
-- },
|
||||
-- }
|
||||
-- }
|
||||
-- ]]
|
||||
function MyClipping:parseMyClippings()
|
||||
-- My Clippings format:
|
||||
-- Title(Author Name)
|
||||
-- Your Highlight on Page 123 | Added on Monday, April 21, 2014 10:08:07 PM
|
||||
--
|
||||
-- This is a sample highlight.
|
||||
-- ==========
|
||||
local file = io.open(self.my_clippings, "r")
|
||||
local clippings = {}
|
||||
if file then
|
||||
local index = 1
|
||||
local corrupted = false
|
||||
local title, author, info, text
|
||||
for line in file:lines() do
|
||||
line = line:match("^%s*(.-)%s*$") or ""
|
||||
if index == 1 then
|
||||
title, author = self:getTitle(line)
|
||||
clippings[title] = clippings[title] or {
|
||||
title = title,
|
||||
author = author,
|
||||
}
|
||||
elseif index == 2 then
|
||||
info = self:getInfo(line)
|
||||
elseif index == 3 then
|
||||
-- should be a blank line, we skip this line
|
||||
elseif index == 4 then
|
||||
text = self:getText(line)
|
||||
end
|
||||
if line == "==========" then
|
||||
if index == 5 then
|
||||
-- entry ends normally
|
||||
local clipping = {
|
||||
page = info.page or info.location,
|
||||
sort = info.sort,
|
||||
time = info.time,
|
||||
text = text,
|
||||
}
|
||||
-- we cannot extract chapter info so just insert clipping
|
||||
-- to a place holder chapter
|
||||
table.insert(clippings[title], { clipping })
|
||||
end
|
||||
index = 0
|
||||
end
|
||||
index = index + 1
|
||||
end
|
||||
end
|
||||
|
||||
return clippings
|
||||
end
|
||||
|
||||
local extensions = {
|
||||
[".pdf"] = true,
|
||||
[".djvu"] = true,
|
||||
[".epub"] = true,
|
||||
[".fb2"] = true,
|
||||
[".mobi"] = true,
|
||||
[".txt"] = true,
|
||||
[".html"] = true,
|
||||
[".doc"] = true,
|
||||
}
|
||||
|
||||
-- remove file extensions added by former Koreader
|
||||
-- extract author name in "Title(Author)" format
|
||||
-- extract author name in "Title - Author" format
|
||||
function MyClipping:getTitle(line)
|
||||
line = line:match("^%s*(.-)%s*$") or ""
|
||||
if extensions[line:sub(-4):lower()] then
|
||||
line = line:sub(1, -5)
|
||||
elseif extensions[line:sub(-5):lower()] then
|
||||
line = line:sub(1, -6)
|
||||
end
|
||||
local _, _, title, author = line:find("(.-)%s*%((.*)%)")
|
||||
if not author then
|
||||
_, _, title, author = line:find("(.-)%s*-%s*(.*)")
|
||||
end
|
||||
if not title then title = line end
|
||||
return title:match("^%s*(.-)%s*$"), author
|
||||
end
|
||||
|
||||
local keywords = {
|
||||
["highlight"] = {
|
||||
"Highlight",
|
||||
"标注",
|
||||
},
|
||||
["note"] = {
|
||||
"Note",
|
||||
"笔记",
|
||||
},
|
||||
["bookmark"] = {
|
||||
"Bookmark",
|
||||
"书签",
|
||||
},
|
||||
}
|
||||
|
||||
local months = {
|
||||
["Jan"] = 1,
|
||||
["Feb"] = 2,
|
||||
["Mar"] = 3,
|
||||
["Apr"] = 4,
|
||||
["May"] = 5,
|
||||
["Jun"] = 6,
|
||||
["Jul"] = 7,
|
||||
["Aug"] = 8,
|
||||
["Sep"] = 9,
|
||||
["Oct"] = 10,
|
||||
["Nov"] = 11,
|
||||
["Dec"] = 12
|
||||
}
|
||||
|
||||
local pms = {
|
||||
["PM"] = 12,
|
||||
["下午"] = 12,
|
||||
}
|
||||
|
||||
function MyClipping:getTime(line)
|
||||
if not line then return end
|
||||
local _, _, year, month, day = line:find("(%d+)年(%d+)月(%d+)日")
|
||||
if not year or not month or not day then
|
||||
_, _, year, month, day = line:find("(%d%d%d%d)-(%d%d)-(%d%d)")
|
||||
end
|
||||
if not year or not month or not day then
|
||||
for k, v in pairs(months) do
|
||||
if line:find(k) then
|
||||
month = v
|
||||
_, _, day = line:find(" (%d%d),")
|
||||
_, _, year = line:find(" (%d%d%d%d)")
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local _, _, hour, minute, second = line:find("(%d+):(%d+):(%d+)")
|
||||
if year and month and day and hour and minute and second then
|
||||
for k, v in pairs(pms) do
|
||||
if line:find(k) then hour = hour + v end
|
||||
break
|
||||
end
|
||||
local time = os.time({
|
||||
year = year, month = month, day = day,
|
||||
hour = hour, min = minute, sec = second,
|
||||
})
|
||||
|
||||
return time
|
||||
end
|
||||
end
|
||||
|
||||
function MyClipping:getInfo(line)
|
||||
local info = {}
|
||||
line = line or ""
|
||||
local _, _, part1, part2 = line:find("(.+)%s*|%s*(.+)")
|
||||
|
||||
-- find entry type and location
|
||||
for sort, words in pairs(keywords) do
|
||||
for _, word in ipairs(words) do
|
||||
if part1 and part1:find(word) then
|
||||
info.sort = sort
|
||||
info.location = part1:match("(%d+-?%d+)")
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- find entry created time
|
||||
info.time = self:getTime(part2 or "")
|
||||
|
||||
return info
|
||||
end
|
||||
|
||||
function MyClipping:getText(line)
|
||||
line = line or ""
|
||||
return line:match("^%s*(.-)%s*$") or ""
|
||||
end
|
||||
|
||||
function MyClipping:parseHighlight(highlights, book)
|
||||
for page, items in pairs(highlights) do
|
||||
for _, item in ipairs(items) do
|
||||
local clipping = {}
|
||||
clipping.page = page
|
||||
clipping.sort = "highlight"
|
||||
clipping.time = self:getTime(item.datetime or "")
|
||||
clipping.text = self:getText(item.text)
|
||||
-- TODO: store chapter info when exporting highlights
|
||||
if clipping.text and clipping.text ~= "" then
|
||||
table.insert(book, { clipping })
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(book, function(v1, v2) return v1[1].page < v2[1].page end)
|
||||
end
|
||||
|
||||
function MyClipping:parseHistory()
|
||||
local clippings = {}
|
||||
for f in lfs.dir(self.history_dir) do
|
||||
local path = self.history_dir.."/"..f
|
||||
if lfs.attributes(path, "mode") == "file" and path:find(".+%.lua$") then
|
||||
local ok, stored = pcall(dofile, path)
|
||||
if ok and stored.highlight then
|
||||
local _, _, docname = path:find("%[.*%](.*)%.lua$")
|
||||
local title, author = self:getTitle(docname)
|
||||
clippings[title] = {
|
||||
title = title,
|
||||
author = author,
|
||||
}
|
||||
self:parseHighlight(stored.highlight, clippings[title])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return clippings
|
||||
end
|
||||
|
||||
function MyClipping:parseCurrentDoc(view)
|
||||
local clippings = {}
|
||||
local path = view.document.file
|
||||
local _, _, docname = path:find(".*/(.*)")
|
||||
local title, author = self:getTitle(docname)
|
||||
clippings[title] = {
|
||||
title = title,
|
||||
author = author,
|
||||
}
|
||||
self:parseHighlight(view.highlight.saved, clippings[title])
|
||||
|
||||
return clippings
|
||||
end
|
||||
|
||||
return MyClipping
|
||||
|
||||
270
plugins/evernote.koplugin/main.lua
Normal file
270
plugins/evernote.koplugin/main.lua
Normal file
@@ -0,0 +1,270 @@
|
||||
local InputContainer = require("ui/widget/container/inputcontainer")
|
||||
local LoginDialog = require("ui/widget/logindialog")
|
||||
local InfoMessage = require("ui/widget/infomessage")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local Screen = require("ui/screen")
|
||||
local Event = require("ui/event")
|
||||
local DEBUG = require("dbg")
|
||||
local _ = require("gettext")
|
||||
|
||||
local slt2 = require('slt2')
|
||||
local MyClipping = require("clip")
|
||||
local EvernoteOAuth = require("EvernoteOAuth")
|
||||
local EvernoteClient = require("EvernoteClient")
|
||||
|
||||
local EvernoteExporter = InputContainer:new{
|
||||
login_title = _("Login to Evernote"),
|
||||
notebook_name = _("Koreader Notes"),
|
||||
--evernote_domain = "sandbox",
|
||||
|
||||
evernote_token,
|
||||
notebook_guid,
|
||||
}
|
||||
|
||||
function EvernoteExporter:init()
|
||||
self.ui.menu:registerToMainMenu(self)
|
||||
|
||||
local settings = G_reader_settings:readSetting("evernote") or {}
|
||||
self.evernote_username = settings.username or ""
|
||||
self.evernote_token = settings.token
|
||||
self.notebook_guid = settings.notebook
|
||||
|
||||
self.parser = MyClipping:new{
|
||||
my_clippings = "/mnt/us/documents/My Clippings.txt",
|
||||
history_dir = "./history",
|
||||
}
|
||||
self.template = slt2.loadfile(self.path.."/note.tpl")
|
||||
end
|
||||
|
||||
function EvernoteExporter:addToMainMenu(tab_item_table)
|
||||
table.insert(tab_item_table.plugins, {
|
||||
text = _("Evernote"),
|
||||
sub_item_table = {
|
||||
{
|
||||
text_func = function()
|
||||
return self.evernote_token and _("Logout") or _("Login")
|
||||
end,
|
||||
callback = function()
|
||||
if self.evernote_token then
|
||||
self:logout()
|
||||
else
|
||||
self:login()
|
||||
end
|
||||
end
|
||||
},
|
||||
{
|
||||
text = _("Export all notes in this book"),
|
||||
callback = function()
|
||||
UIManager:scheduleIn(0.5, function()
|
||||
self:exportCurrentNotes(self.view)
|
||||
end)
|
||||
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = _("This may take several seconds..."),
|
||||
timeout = 3,
|
||||
})
|
||||
end
|
||||
},
|
||||
{
|
||||
text = _("Export all notes in your library"),
|
||||
callback = function()
|
||||
UIManager:scheduleIn(0.5, function()
|
||||
self:exportAllNotes()
|
||||
end)
|
||||
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = _("This may take several minutes..."),
|
||||
timeout = 3,
|
||||
})
|
||||
end
|
||||
},
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
function EvernoteExporter:login()
|
||||
self.login_dialog = LoginDialog:new{
|
||||
title = self.login_title,
|
||||
username = self.evernote_username or "",
|
||||
buttons = {
|
||||
{
|
||||
{
|
||||
text = _("Cancel"),
|
||||
enabled = true,
|
||||
callback = function()
|
||||
self:closeDialog()
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Login"),
|
||||
enabled = true,
|
||||
callback = function()
|
||||
local username, password = self:getCredential()
|
||||
self:closeDialog()
|
||||
UIManager:scheduleIn(0.5, function()
|
||||
self:doLogin(username, password)
|
||||
end)
|
||||
end,
|
||||
},
|
||||
},
|
||||
},
|
||||
width = Screen:getWidth() * 0.8,
|
||||
height = Screen:getHeight() * 0.4,
|
||||
}
|
||||
|
||||
self.login_dialog:onShowKeyboard()
|
||||
UIManager:show(self.login_dialog)
|
||||
end
|
||||
|
||||
function EvernoteExporter:closeDialog()
|
||||
self.login_dialog:onClose()
|
||||
UIManager:close(self.login_dialog)
|
||||
end
|
||||
|
||||
function EvernoteExporter:getCredential()
|
||||
return self.login_dialog:getCredential()
|
||||
end
|
||||
|
||||
function EvernoteExporter:doLogin(username, password)
|
||||
self:closeDialog()
|
||||
|
||||
local oauth = EvernoteOAuth:new{
|
||||
domain = self.evernote_domain,
|
||||
username = username,
|
||||
password = password,
|
||||
}
|
||||
self.evernote_username = username
|
||||
local ok, token = pcall(oauth.getToken, oauth)
|
||||
if not ok or not token then
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = _("Error occurs when login:") .. "\n" .. token,
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
local client = EvernoteClient:new{
|
||||
domain = self.evernote_domain,
|
||||
authToken = token,
|
||||
}
|
||||
local ok, guid = pcall(self.getExportNotebook, self, client)
|
||||
if not ok or not guid then
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = _("Error occurs when login:") .. "\n" .. guid,
|
||||
})
|
||||
elseif guid then
|
||||
self.evernote_token = token
|
||||
self.notebook_guid = guid
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = _("Login to Evernote successfully"),
|
||||
})
|
||||
end
|
||||
|
||||
self:saveSettings()
|
||||
end
|
||||
|
||||
function EvernoteExporter:logout()
|
||||
self.evernote_token = nil
|
||||
self.notebook_guid = nil
|
||||
self:saveSettings()
|
||||
end
|
||||
|
||||
function EvernoteExporter:saveSettings()
|
||||
local settings = {
|
||||
username = self.evernote_username,
|
||||
token = self.evernote_token,
|
||||
notebook = self.notebook_guid,
|
||||
}
|
||||
G_reader_settings:saveSetting("evernote", settings)
|
||||
end
|
||||
|
||||
function EvernoteExporter:getExportNotebook(client)
|
||||
local name = self.notebook_name
|
||||
return client:findNotebookByTitle(name) or client:createNotebook(name).guid
|
||||
end
|
||||
|
||||
function EvernoteExporter:exportCurrentNotes(view)
|
||||
local client = EvernoteClient:new{
|
||||
domain = self.evernote_domain,
|
||||
authToken = self.evernote_token,
|
||||
}
|
||||
|
||||
local clippings = self.parser:parseCurrentDoc(view)
|
||||
self:exportClippings(client, clippings)
|
||||
end
|
||||
|
||||
function EvernoteExporter:exportAllNotes()
|
||||
local client = EvernoteClient:new{
|
||||
domain = self.evernote_domain,
|
||||
authToken = self.evernote_token,
|
||||
}
|
||||
|
||||
local clippings = self.parser:parseMyClippings()
|
||||
if next(clippings) == nil then
|
||||
clippings = self.parser:parseHistory()
|
||||
end
|
||||
-- remove blank entries
|
||||
for title, booknotes in pairs(clippings) do
|
||||
-- chapter number is zero
|
||||
if #booknotes == 0 then
|
||||
clippings[title] = nil
|
||||
end
|
||||
end
|
||||
--DEBUG("clippings", clippings)
|
||||
self:exportClippings(client, clippings)
|
||||
end
|
||||
|
||||
function EvernoteExporter:exportClippings(client, clippings)
|
||||
local export_count, error_count = 0, 0
|
||||
local export_title, error_title
|
||||
for title, booknotes in pairs(clippings) do
|
||||
local ok, err = pcall(self.exportBooknotes, self, client, title, booknotes)
|
||||
|
||||
-- error reporting
|
||||
if not ok then
|
||||
DEBUG("Error occurs when exporting book:", title, err)
|
||||
error_count = error_count + 1
|
||||
error_title = title
|
||||
else
|
||||
DEBUG("Exported notes in book:", title)
|
||||
export_count = export_count + 1
|
||||
export_title = title
|
||||
end
|
||||
end
|
||||
|
||||
local msg = ""
|
||||
local all_count = export_count + error_count
|
||||
if export_count > 0 and error_count == 0 then
|
||||
if all_count == 1 then
|
||||
msg = _("Exported notes in book:") .. "\n" .. export_title
|
||||
else
|
||||
msg = _("Exported notes in book:") .. "\n" .. export_title
|
||||
msg = msg .. "\n" .. _("and ") .. all_count-1 .. _("others.")
|
||||
end
|
||||
elseif error_count > 0 then
|
||||
if all_count == 1 then
|
||||
msg = _("Error occurs when exporting book:") .. "\n" .. error_title
|
||||
else
|
||||
msg = _("Errors occur when exporting book:") .. "\n" .. error_title
|
||||
msg = msg .. "\n" .. _("and ") .. error_count-1 .. ("others.")
|
||||
end
|
||||
end
|
||||
UIManager:show(InfoMessage:new{ text = msg })
|
||||
|
||||
end
|
||||
|
||||
function EvernoteExporter:exportBooknotes(client, title, booknotes)
|
||||
local content = slt2.render(self.template, {
|
||||
booknotes = booknotes,
|
||||
notemarks = _("Note: "),
|
||||
})
|
||||
--DEBUG("content", content)
|
||||
local note_guid = client:findNoteByTitle(title, self.notebook_guid)
|
||||
if not note_guid then
|
||||
client:createNote(title, content, {}, self.notebook_guid)
|
||||
else
|
||||
client:updateNote(note_guid, title, content, {}, self.notebook_guid)
|
||||
end
|
||||
end
|
||||
|
||||
return EvernoteExporter
|
||||
|
||||
57
plugins/evernote.koplugin/note.tpl
Normal file
57
plugins/evernote.koplugin/note.tpl
Normal file
@@ -0,0 +1,57 @@
|
||||
#{
|
||||
-- helper function to map time to JET color
|
||||
function timecolor(time)
|
||||
local r,g,b
|
||||
local year = 3600*24*30*12
|
||||
local lapse = os.time() - time
|
||||
if lapse <= 1*year then
|
||||
r,g,b = 255, 255*(year-lapse)/year, 0
|
||||
elseif lapse > 1*year and lapse < 2*year then
|
||||
r,g,b = 255*(lapse-year)/year, 255, 255*(2*year-lapse)/year
|
||||
elseif lapse >= 2*year then
|
||||
r,g,b = 0, 255*(lapse-2*year)/year, 255
|
||||
end
|
||||
r = r > 255 and 255 or math.floor(r)
|
||||
r = r < 0 and 0 or math.floor(r)
|
||||
g = g > 255 and 255 or math.floor(g)
|
||||
g = g < 0 and 0 or math.floor(g)
|
||||
b = b > 255 and 255 or math.floor(b)
|
||||
b = b < 0 and 0 or math.floor(b)
|
||||
|
||||
return r..','..g..','..b
|
||||
end
|
||||
|
||||
function htmlescape(text)
|
||||
if text == nil then return "" end
|
||||
|
||||
local esc, _ = text:gsub('&', '&'):gsub('<', '<'):gsub('>', '>')
|
||||
return esc
|
||||
end
|
||||
}#
|
||||
<div style="width:90%; max-width:600px; margin:0px auto; padding:5px; font-size:12pt; font-family:Georgia">
|
||||
<h2 style="font-size:18pt; text-align:right;">#{= htmlescape(booknotes.title) }#</h2>
|
||||
<h5 style="font-size:12pt; text-align:right; color:gray;">#{= htmlescape(booknotes.author) }#</h5>
|
||||
#{ for _, chapter in ipairs(booknotes) do }#
|
||||
#{ if chapter.title then }#
|
||||
<div style="font-size:14pt; font-weight:bold; text-align:center; margin:0.5em;"><span>#{= htmlescape(chapter.title) }#</span></div>
|
||||
#{ end }#
|
||||
#{ for index, clipping in ipairs(chapter) do }#
|
||||
<div style="padding-top:0.5em; padding-bottom:0.5em;#{ if index > 1 then }# border-top:1px dotted lightgray;#{ end }#">
|
||||
<div style="font-size:10pt; margin-bottom:0.2em; color:darkgray">
|
||||
<div style="display:inline-block; width:0.2em; height:0.9em; margin-right:0.2em; background-color:rgb(#{= timecolor(clipping.time)}#);"></div>
|
||||
<span>#{= os.date("%x", clipping.time) }#</span><span style="float:right">#{= clipping.page }#</span>
|
||||
</div>
|
||||
<div style="font-size:12pt">
|
||||
<span>#{= htmlescape(clipping.text) }#</span>
|
||||
</div>
|
||||
#{ if clipping.note then }#
|
||||
<div style="font-size:11pt; margin-top:0.2em;">
|
||||
<span style="font-weight:bold;">#{= htmlescape(notemarks) }#</span>
|
||||
<span style="color:#888888">#{= htmlescape(clipping.note) }#</span>
|
||||
</div>
|
||||
#{ end }#
|
||||
</div>
|
||||
#{ end }#
|
||||
#{ end }#
|
||||
</div>
|
||||
|
||||
175
plugins/evernote.koplugin/slt2.lua
Normal file
175
plugins/evernote.koplugin/slt2.lua
Normal file
@@ -0,0 +1,175 @@
|
||||
--[[
|
||||
-- slt2 - Simple Lua Template 2
|
||||
--
|
||||
-- Project page: https://github.com/henix/slt2
|
||||
--
|
||||
-- @License
|
||||
-- MIT License
|
||||
--]]
|
||||
|
||||
local slt2 = {}
|
||||
|
||||
-- a tree fold on inclusion tree
|
||||
-- @param init_func: must return a new value when called
|
||||
local function include_fold(template, start_tag, end_tag, fold_func, init_func)
|
||||
local result = init_func()
|
||||
|
||||
start_tag = start_tag or '#{'
|
||||
end_tag = end_tag or '}#'
|
||||
local start_tag_inc = start_tag..'include:'
|
||||
|
||||
local start1, end1 = string.find(template, start_tag_inc, 1, true)
|
||||
local start2 = nil
|
||||
local end2 = 0
|
||||
|
||||
while start1 ~= nil do
|
||||
if start1 > end2 + 1 then -- for beginning part of file
|
||||
result = fold_func(result, string.sub(template, end2 + 1, start1 - 1))
|
||||
end
|
||||
start2, end2 = string.find(template, end_tag, end1 + 1, true)
|
||||
assert(start2, 'end tag "'..end_tag..'" missing')
|
||||
do -- recursively include the file
|
||||
local filename = assert(loadstring('return '..string.sub(template, end1 + 1, start2 - 1)))()
|
||||
assert(filename)
|
||||
local fin = assert(io.open(filename))
|
||||
-- TODO: detect cyclic inclusion?
|
||||
result = fold_func(result, include_fold(fin:read('*a'), start_tag, end_tag, fold_func, init_func), filename)
|
||||
fin:close()
|
||||
end
|
||||
start1, end1 = string.find(template, start_tag_inc, end2 + 1, true)
|
||||
end
|
||||
result = fold_func(result, string.sub(template, end2 + 1))
|
||||
return result
|
||||
end
|
||||
|
||||
-- preprocess included files
|
||||
-- @return string
|
||||
function slt2.precompile(template, start_tag, end_tag)
|
||||
return table.concat(include_fold(template, start_tag, end_tag, function(acc, v)
|
||||
if type(v) == 'string' then
|
||||
table.insert(acc, v)
|
||||
elseif type(v) == 'table' then
|
||||
table.insert(acc, table.concat(v))
|
||||
else
|
||||
error('Unknown type: '..type(v))
|
||||
end
|
||||
return acc
|
||||
end, function() return {} end))
|
||||
end
|
||||
|
||||
-- unique a list, preserve order
|
||||
local function stable_uniq(t)
|
||||
local existed = {}
|
||||
local res = {}
|
||||
for _, v in ipairs(t) do
|
||||
if not existed[v] then
|
||||
table.insert(res, v)
|
||||
existed[v] = true
|
||||
end
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
-- @return { string }
|
||||
function slt2.get_dependency(template, start_tag, end_tag)
|
||||
return stable_uniq(include_fold(template, start_tag, end_tag, function(acc, v, name)
|
||||
if type(v) == 'string' then
|
||||
elseif type(v) == 'table' then
|
||||
if name ~= nil then
|
||||
table.insert(acc, name)
|
||||
end
|
||||
for _, subname in ipairs(v) do
|
||||
table.insert(acc, subname)
|
||||
end
|
||||
else
|
||||
error('Unknown type: '..type(v))
|
||||
end
|
||||
return acc
|
||||
end, function() return {} end))
|
||||
end
|
||||
|
||||
-- @return { name = string, code = string / function}
|
||||
function slt2.loadstring(template, start_tag, end_tag, tmpl_name)
|
||||
-- compile it to lua code
|
||||
local lua_code = {}
|
||||
|
||||
start_tag = start_tag or '#{'
|
||||
end_tag = end_tag or '}#'
|
||||
|
||||
local output_func = "coroutine.yield"
|
||||
|
||||
template = slt2.precompile(template, start_tag, end_tag)
|
||||
|
||||
local start1, end1 = string.find(template, start_tag, 1, true)
|
||||
local start2 = nil
|
||||
local end2 = 0
|
||||
|
||||
local cEqual = string.byte('=', 1)
|
||||
|
||||
while start1 ~= nil do
|
||||
if start1 > end2 + 1 then
|
||||
table.insert(lua_code, output_func..'('..string.format("%q", string.sub(template, end2 + 1, start1 - 1))..')')
|
||||
end
|
||||
start2, end2 = string.find(template, end_tag, end1 + 1, true)
|
||||
assert(start2, 'end_tag "'..end_tag..'" missing')
|
||||
if string.byte(template, end1 + 1) == cEqual then
|
||||
table.insert(lua_code, output_func..'('..string.sub(template, end1 + 2, start2 - 1)..')')
|
||||
else
|
||||
table.insert(lua_code, string.sub(template, end1 + 1, start2 - 1))
|
||||
end
|
||||
start1, end1 = string.find(template, start_tag, end2 + 1, true)
|
||||
end
|
||||
table.insert(lua_code, output_func..'('..string.format("%q", string.sub(template, end2 + 1))..')')
|
||||
|
||||
local ret = { name = tmpl_name or '=(slt2.loadstring)' }
|
||||
if setfenv == nil then -- lua 5.2
|
||||
ret.code = table.concat(lua_code, '\n')
|
||||
else -- lua 5.1
|
||||
ret.code = assert(loadstring(table.concat(lua_code, '\n'), ret.name))
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
-- @return { name = string, code = string / function }
|
||||
function slt2.loadfile(filename, start_tag, end_tag)
|
||||
local fin = assert(io.open(filename))
|
||||
local all = fin:read('*a')
|
||||
fin:close()
|
||||
return slt2.loadstring(all, start_tag, end_tag, filename)
|
||||
end
|
||||
|
||||
local mt52 = { __index = _ENV }
|
||||
local mt51 = { __index = _G }
|
||||
|
||||
-- @return a coroutine function
|
||||
function slt2.render_co(t, env)
|
||||
local f
|
||||
if setfenv == nil then -- lua 5.2
|
||||
if env ~= nil then
|
||||
setmetatable(env, mt52)
|
||||
end
|
||||
f = assert(load(t.code, t.name, 't', env or _ENV))
|
||||
else -- lua 5.1
|
||||
if env ~= nil then
|
||||
setmetatable(env, mt51)
|
||||
end
|
||||
f = setfenv(t.code, env or _G)
|
||||
end
|
||||
return f
|
||||
end
|
||||
|
||||
-- @return string
|
||||
function slt2.render(t, env)
|
||||
local result = {}
|
||||
local co = coroutine.create(slt2.render_co(t, env))
|
||||
while coroutine.status(co) ~= 'dead' do
|
||||
local ok, chunk = coroutine.resume(co)
|
||||
if not ok then
|
||||
error(chunk)
|
||||
end
|
||||
table.insert(result, chunk)
|
||||
end
|
||||
return table.concat(result)
|
||||
end
|
||||
|
||||
return slt2
|
||||
Reference in New Issue
Block a user