[feat, plugin] Vocabulary builder (#9132)

Made the old dictionary lookup history into a flashcard-ish vocabulary builder.
pull/9158/head
weijiuqiao 2 years ago committed by GitHub
parent 5a033f1221
commit e1b137339c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -3,6 +3,7 @@ local ConfirmBox = require("ui/widget/confirmbox")
local DataStorage = require("datastorage")
local Device = require("device")
local DictQuickLookup = require("ui/widget/dictquicklookup")
local Event = require("ui/event")
local Geom = require("ui/geometry")
local InfoMessage = require("ui/widget/infomessage")
local InputContainer = require("ui/widget/container/inputcontainer")
@ -401,7 +402,7 @@ function ReaderDictionary:addToMainMenu(menu_items)
end
end
function ReaderDictionary:onLookupWord(word, is_sane, boxes, highlight, link)
function ReaderDictionary:onLookupWord(word, is_sane, boxes, highlight, link, tweak_buttons_func)
logger.dbg("dict lookup word:", word, boxes)
-- escape quotes and other funny characters in word
word = self:cleanSelection(word, is_sane)
@ -411,7 +412,7 @@ function ReaderDictionary:onLookupWord(word, is_sane, boxes, highlight, link)
-- Wrapped through Trapper, as we may be using Trapper:dismissablePopen() in it
Trapper:wrap(function()
self:stardictLookup(word, self.enabled_dict_names, not self.disable_fuzzy_search, boxes, link)
self:stardictLookup(word, self.enabled_dict_names, not self.disable_fuzzy_search, boxes, link, tweak_buttons_func)
end)
return true
end
@ -882,12 +883,11 @@ function ReaderDictionary:startSdcv(word, dict_names, fuzzy_search)
return results
end
function ReaderDictionary:stardictLookup(word, dict_names, fuzzy_search, boxes, link)
function ReaderDictionary:stardictLookup(word, dict_names, fuzzy_search, boxes, link, tweak_buttons_func)
if word == "" then
return
end
if not self.disable_lookup_history then
local book_title = self.ui.doc_settings and self.ui.doc_settings:readSetting("doc_props").title or _("Dictionary lookup")
if book_title == "" then -- no or empty metadata title
if self.ui.document and self.ui.document.file then
@ -895,6 +895,10 @@ function ReaderDictionary:stardictLookup(word, dict_names, fuzzy_search, boxes,
book_title = util.splitFileNameSuffix(filename)
end
end
-- Event for plugin to catch lookup with book title
self.ui:handleEvent(Event:new("WordLookedUp", word, book_title))
if not self.disable_lookup_history then
lookup_history:addTableItem("lookup_history", {
book_title = book_title,
time = os.time(),
@ -945,16 +949,17 @@ function ReaderDictionary:stardictLookup(word, dict_names, fuzzy_search, boxes,
return
end
self:showDict(word, tidyMarkup(results), boxes, link)
self:showDict(word, tidyMarkup(results), boxes, link, tweak_buttons_func)
end
function ReaderDictionary:showDict(word, results, boxes, link)
function ReaderDictionary:showDict(word, results, boxes, link, tweak_buttons_func)
if results and results[1] then
logger.dbg("showing quick lookup window", #self.dict_window_list+1, ":", word, results)
self.dict_window = DictQuickLookup:new{
window_list = self.dict_window_list,
ui = self.ui,
highlight = self.highlight,
tweak_buttons_func = tweak_buttons_func,
dialog = self.dialog,
-- original lookup word
word = word,

@ -139,6 +139,7 @@ local order = {
search = {
"dictionary_lookup",
"dictionary_lookup_history",
"vocabulary_builder",
"dictionary_settings",
"----------------------------",
"wikipedia_lookup",

@ -187,6 +187,7 @@ local order = {
search = {
"dictionary_lookup",
"dictionary_lookup_history",
"vocabulary_builder",
"dictionary_settings",
"----------------------------",
"wikipedia_lookup",

@ -505,7 +505,9 @@ function DictQuickLookup:init()
})
end
end
if self.tweak_buttons_func then
self.tweak_buttons_func(buttons)
end
-- Bottom buttons get a bit less padding so their line separators
-- reach out from the content to the borders a bit more
local buttons_padding = Size.padding.default

@ -0,0 +1,6 @@
local _ = require("gettext")
return {
name = "vocabulary_builder",
fullname = _("Vocabulary builder"),
description = _([[This plugin processes dictionary word lookups and uses spaced repetition to help you remember new words.]]),
}

@ -0,0 +1,248 @@
local DataStorage = require("datastorage")
local Device = require("device")
local SQ3 = require("lua-ljsqlite3/init")
local LuaData = require("luadata")
local db_location = DataStorage:getSettingsDir() .. "/vocabulary_builder.sqlite3"
local DB_SCHEMA_VERSION = 20220522
local VOCABULARY_DB_SCHEMA = [[
-- To store looked up words
CREATE TABLE IF NOT EXISTS "vocabulary" (
"word" TEXT NOT NULL UNIQUE,
"book_title" TEXT DEFAULT '',
"create_time" INTEGER NOT NULL,
"review_time" INTEGER,
"due_time" INTEGER NOT NULL,
"review_count" INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY("word")
);
CREATE INDEX IF NOT EXISTS due_time_index ON vocabulary(due_time);
]]
local VocabularyBuilder = {
count = 0,
}
function VocabularyBuilder:init()
VocabularyBuilder:createDB()
end
function VocabularyBuilder:hasItems()
if self.count > 0 then
return true
end
self.count = self:selectCount()
return self.count > 0
end
function VocabularyBuilder:selectCount(conn)
if conn then
return tonumber(conn:rowexec("SELECT count(0) FROM vocabulary;"))
else
local db_conn = SQ3.open(db_location)
local count = tonumber(db_conn:rowexec("SELECT count(0) FROM vocabulary;"))
db_conn:close()
return count
end
end
function VocabularyBuilder:createDB()
local db_conn = SQ3.open(db_location)
-- Make it WAL, if possible
if Device:canUseWAL() then
db_conn:exec("PRAGMA journal_mode=WAL;")
else
db_conn:exec("PRAGMA journal_mode=TRUNCATE;")
end
-- Create db
db_conn:exec(VOCABULARY_DB_SCHEMA)
-- Check version
local db_version = tonumber(db_conn:rowexec("PRAGMA user_version;"))
if db_version < DB_SCHEMA_VERSION then
if db_version == 0 then
self:insertLookupData(db_conn)
end
-- Update version
db_conn:exec(string.format("PRAGMA user_version=%d;", DB_SCHEMA_VERSION))
end
db_conn:close()
end
function VocabularyBuilder:insertLookupData(db_conn)
local file_path = DataStorage:getSettingsDir() .. "/lookup_history.lua"
local lookup_history = LuaData:open(file_path, { name = "LookupHistory" })
if lookup_history:has("lookup_history") then
local lookup_history_table = lookup_history:readSetting("lookup_history")
local words = {}
for i = #lookup_history_table, 1, -1 do
local value = lookup_history_table[i]
if not words[value.word] then
local insert_sql = [[INSERT OR REPLACE INTO vocabulary
(word, book_title, create_time, due_time) values
(?, ?, ?, ?);
]]
local stmt = db_conn:prepare(insert_sql)
stmt:bind(value.word, value.book_title or "", value.time, value.time + 5*60)
stmt:step()
stmt:clearbind():reset()
words[value.word] = true
end
end
end
end
function VocabularyBuilder:_select_items(items, start_idx)
local conn = SQ3.open(db_location)
local sql = string.format("SELECT * FROM vocabulary ORDER BY due_time limit %d OFFSET %d;", 32, start_idx-1)
local results = conn:exec(sql)
conn:close()
if not results then return end
local current_time = os.time()
for i = 1, #results.word do
local item = items[start_idx+i-1]
if item then
item.word = results.word[i]
item.review_count = math.max(0, math.min(8, tonumber(results.review_count[i])))
item.book_title = results.book_title[i] or ""
item.create_time = tonumber( results.create_time[i])
item.review_time = nil --use this field to flag change
item.due_time = tonumber(results.due_time[i])
item.is_dim = tonumber(results.due_time[i]) > current_time
item.got_it_callback = function(item_input)
VocabularyBuilder:gotOrForgot(item_input, true)
end
item.forgot_callback = function(item_input)
VocabularyBuilder:gotOrForgot(item_input, false)
end
item.remove_callback = function(item_input)
VocabularyBuilder:remove(item_input)
end
end
end
end
function VocabularyBuilder:select_items(items, start_idx, end_idx)
local start_cursor
if #items == 0 then
start_cursor = 0
else
for i = start_idx+1, end_idx do
if not items[i].word then
start_cursor = i
break
end
end
end
if not start_cursor then return end
self:_select_items(items, start_cursor)
end
function VocabularyBuilder:gotOrForgot(item, isGot)
local current_time = os.time()
local due_time
local target_count = math.min(math.max(item.review_count + (isGot and 1 or -1), 0), 8)
if target_count == 0 then
due_time = current_time + 5 * 60
elseif target_count == 1 then
due_time = current_time + 30 * 60
elseif target_count == 2 then
due_time = current_time + 12 * 3600
elseif target_count == 3 then
due_time = current_time + 24 * 3600
elseif target_count == 4 then
due_time = current_time + 48 * 3600
elseif target_count == 5 then
due_time = current_time + 96 * 3600
elseif target_count == 6 then
due_time = current_time + 24 * 7 * 3600
elseif target_count == 7 then
due_time = current_time + 24 * 15 * 3600
else
due_time = current_time + 24 * 30 * 3600
end
item.review_count = target_count
item.review_time = current_time
item.due_time = due_time
end
function VocabularyBuilder:batchUpdateItems(items)
local sql = [[UPDATE vocabulary
SET review_count = ?,
review_time = ?,
due_time = ?
WHERE word = ?;]]
local conn = SQ3.open(db_location)
local stmt = conn:prepare(sql)
for _, item in ipairs(items) do
if item.review_time then
stmt:bind(item.review_count, item.review_time, item.due_time, item.word)
stmt:step()
stmt:clearbind():reset()
end
end
conn:close()
end
function VocabularyBuilder:insertOrUpdate(entry)
local conn = SQ3.open(db_location)
conn:exec(string.format([[INSERT INTO vocabulary (word, book_title, create_time, due_time)
VALUES ('%s', '%s', %d, %d)
ON CONFLICT(word) DO UPDATE SET book_title = excluded.book_title,
create_time = excluded.create_time,
review_count = MAX(review_count-1, 0),
due_time = %d;
]], entry.word, entry.book_title, entry.time, entry.time+300, entry.time+300))
self.count = tonumber(conn:rowexec("SELECT count(0) from vocabulary;"))
conn:close()
end
function VocabularyBuilder:remove(item)
local conn = SQ3.open(db_location)
conn:exec(string.format("DELETE FROM vocabulary WHERE word = '%s' ;", item.word))
self.count = self.count - 1
conn:close()
end
function VocabularyBuilder:resetProgress()
local conn = SQ3.open(db_location)
local due_time = os.time()
conn:exec(string.format("UPDATE vocabulary SET review_count = 0, due_time = %d;", due_time))
conn:close()
end
function VocabularyBuilder:resetWordProgress(word)
local conn = SQ3.open(db_location)
local due_time = os.time()
conn:exec(string.format("UPDATE vocabulary SET review_count = 0, due_time = %d WHERE word = '%s';", due_time, word))
conn:close()
end
function VocabularyBuilder:purge()
local conn = SQ3.open(db_location)
conn:exec("DELETE FROM vocabulary;")
self.count = 0
conn:close()
end
VocabularyBuilder:init()
return VocabularyBuilder

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save