mirror of
https://github.com/koreader/koreader.git
synced 2025-12-13 20:36:53 +01:00
Add LuaData and Dictionary Lookup History (#3161)
* Add dictionary history Fixes #2033, fixes #2998. * Add LuaData * table handling in base settings * Add LuaData spec
This commit is contained in:
@@ -5,6 +5,8 @@ local DictQuickLookup = require("ui/widget/dictquicklookup")
|
||||
local InfoMessage = require("ui/widget/infomessage")
|
||||
local InputContainer = require("ui/widget/container/inputcontainer")
|
||||
local JSON = require("json")
|
||||
local KeyValuePage = require("ui/widget/keyvaluepage")
|
||||
local LuaData = require("luadata")
|
||||
local Trapper = require("ui/trapper")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local logger = require("logger")
|
||||
@@ -16,6 +18,7 @@ local T = require("ffi/util").template
|
||||
-- We'll store the list of available dictionaries as a module local
|
||||
-- so we only have to look for them on the first :init()
|
||||
local available_ifos = nil
|
||||
local lookup_history = nil
|
||||
|
||||
local function getIfosInDir(path)
|
||||
-- Get all the .ifo under directory path.
|
||||
@@ -50,7 +53,8 @@ end
|
||||
local ReaderDictionary = InputContainer:new{
|
||||
data_dir = nil,
|
||||
dict_window_list = {},
|
||||
lookup_msg = _("Searching dictionary for:\n%1")
|
||||
disable_lookup_history = G_reader_settings:isTrue("disable_lookup_history"),
|
||||
lookup_msg = _("Searching dictionary for:\n%1"),
|
||||
}
|
||||
|
||||
function ReaderDictionary:init()
|
||||
@@ -96,6 +100,9 @@ function ReaderDictionary:init()
|
||||
end
|
||||
-- Prepare the -u options to give to sdcv if some dictionaries are disabled
|
||||
self:updateSdcvDictNamesOptions()
|
||||
if not lookup_history then
|
||||
lookup_history = LuaData:open(DataStorage:getSettingsDir() .. "/lookup_history.lua", { name = "LookupHistory" })
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderDictionary:updateSdcvDictNamesOptions()
|
||||
@@ -142,6 +149,35 @@ function ReaderDictionary:addToMainMenu(menu_items)
|
||||
end,
|
||||
},
|
||||
}
|
||||
menu_items.dictionary_lookup_history = {
|
||||
text = _("Dictionary lookup history"),
|
||||
enabled_func = function()
|
||||
return lookup_history:has("lookup_history")
|
||||
end,
|
||||
callback = function()
|
||||
local lookup_history_table = lookup_history:readSetting("lookup_history")
|
||||
local kv_pairs = {}
|
||||
local previous_title
|
||||
for i = #lookup_history_table, 1, -1 do
|
||||
local value = lookup_history_table[i]
|
||||
if value.book_title ~= previous_title then
|
||||
table.insert(kv_pairs, { value.book_title..":", "" })
|
||||
end
|
||||
previous_title = value.book_title
|
||||
table.insert(kv_pairs, {
|
||||
os.date("%Y-%m-%d %H:%M:%S", value.time),
|
||||
value.word,
|
||||
callback = function()
|
||||
self:onLookupWord(value.word)
|
||||
end
|
||||
})
|
||||
end
|
||||
UIManager:show(KeyValuePage:new{
|
||||
title = _("Dictionary lookup history"),
|
||||
kv_pairs = kv_pairs,
|
||||
})
|
||||
end,
|
||||
}
|
||||
menu_items.dictionary_settings = {
|
||||
text = _("Dictionary settings"),
|
||||
sub_item_table = {
|
||||
@@ -182,6 +218,29 @@ If you'd like to change the order in which dictionaries are queried (and their r
|
||||
self:makeDisableFuzzyDefault(self.disable_fuzzy_search)
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Disable dictionary lookup history"),
|
||||
checked_func = function()
|
||||
return self.disable_lookup_history
|
||||
end,
|
||||
callback = function()
|
||||
self.disable_lookup_history = not self.disable_lookup_history
|
||||
G_reader_settings:saveSetting("disable_lookup_history", self.disable_lookup_history)
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Clean dictionary lookup history"),
|
||||
callback = function()
|
||||
UIManager:show(ConfirmBox:new{
|
||||
text = _("Clean dictionary lookup history?"),
|
||||
ok_text = _("Clean"),
|
||||
ok_callback = function()
|
||||
-- empty data table to replace current one
|
||||
lookup_history:reset{}
|
||||
end,
|
||||
})
|
||||
end,
|
||||
},
|
||||
{ -- setting used by dictquicklookup
|
||||
text = _("Justify text"),
|
||||
checked_func = function()
|
||||
@@ -329,6 +388,16 @@ function ReaderDictionary:stardictLookup(word, box, link)
|
||||
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")
|
||||
lookup_history:addTableItem("lookup_history", {
|
||||
book_title = book_title,
|
||||
time = os.time(),
|
||||
word = word,
|
||||
})
|
||||
end
|
||||
|
||||
if not self.disable_fuzzy_search then
|
||||
self:showLookupInfo(word)
|
||||
end
|
||||
@@ -503,6 +572,11 @@ function ReaderDictionary:makeDisableFuzzyDefault(disable_fuzzy_search)
|
||||
and _("Disable fuzzy search by default?")
|
||||
or _("Enable fuzzy search by default?")
|
||||
),
|
||||
ok_text = T(
|
||||
disable_fuzzy_search
|
||||
and _("Disable")
|
||||
or _("Enable")
|
||||
),
|
||||
ok_callback = function()
|
||||
G_reader_settings:saveSetting("disable_fuzzy_search", disable_fuzzy_search)
|
||||
end,
|
||||
|
||||
165
frontend/luadata.lua
Normal file
165
frontend/luadata.lua
Normal file
@@ -0,0 +1,165 @@
|
||||
--[[--
|
||||
Handles append-mostly data such as KOReader's bookmarks and dictionary search history.
|
||||
]]
|
||||
|
||||
local LuaSettings = require("luasettings")
|
||||
local dbg = require("dbg")
|
||||
local dump = require("dump")
|
||||
local logger = require("logger")
|
||||
local util = require("util")
|
||||
|
||||
local LuaData = LuaSettings:new{
|
||||
name = "",
|
||||
max_backups = 9,
|
||||
}
|
||||
|
||||
--- Creates a new LuaData instance.
|
||||
function LuaData:open(file_path, o) -- luacheck: ignore 312
|
||||
if o and type(o) ~= "table" then
|
||||
if dbg.is_on then
|
||||
error("LuaData: got "..type(o)..", table expected")
|
||||
else
|
||||
o = {}
|
||||
end
|
||||
end
|
||||
-- always initiate a new instance
|
||||
-- careful, `o` is already a table so we use parentheses
|
||||
self = LuaData:new(o)
|
||||
|
||||
local new = {file=file_path, data={}}
|
||||
|
||||
-- some magic to allow for self-describing function names
|
||||
local _local = {}
|
||||
_local.__index = _local
|
||||
setmetatable(_G, _local)
|
||||
_local[self.name.."Entry"] = function(table)
|
||||
if table.index then
|
||||
-- we've got a deleted setting, overwrite with nil
|
||||
if not table.data then new.data[table.index] = nil end
|
||||
new.data[table.index] = new.data[table.index] or {}
|
||||
local size = util.tableSize(table.data)
|
||||
if size == 1 then
|
||||
for key, value in pairs(table.data) do
|
||||
new.data[table.index][key] = value
|
||||
end
|
||||
else
|
||||
new.data[table.index] = table.data
|
||||
end
|
||||
-- we've got it all at once
|
||||
else
|
||||
new.data = table
|
||||
end
|
||||
end
|
||||
|
||||
local ok = pcall(dofile, new.file)
|
||||
|
||||
if ok then
|
||||
logger.dbg("data is read from ", new.file)
|
||||
else
|
||||
logger.dbg(new.file, " is invalid, remove.")
|
||||
os.remove(new.file)
|
||||
for i=1, self.max_backups, 1 do
|
||||
local backup_file = new.file..".old."..i
|
||||
if pcall(dofile, backup_file) then
|
||||
logger.dbg("data is read from ", backup_file)
|
||||
break
|
||||
else
|
||||
logger.dbg(backup_file, " is invalid, remove.")
|
||||
os.remove(backup_file)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return setmetatable(new, {__index = self})
|
||||
end
|
||||
|
||||
--- Saves a setting.
|
||||
function LuaData:saveSetting(key, value)
|
||||
self.data[key] = value
|
||||
self:append{
|
||||
index = key,
|
||||
data = value,
|
||||
}
|
||||
return self
|
||||
end
|
||||
|
||||
--- Deletes a setting.
|
||||
function LuaData:delSetting(key)
|
||||
self.data[key] = nil
|
||||
self:append{
|
||||
index = key,
|
||||
}
|
||||
return self
|
||||
end
|
||||
|
||||
--- Adds item to table.
|
||||
function LuaData:addTableItem(table_name, value)
|
||||
local settings_table = self:has(table_name) and self:readSetting(table_name) or {}
|
||||
table.insert(settings_table, value)
|
||||
self.data[table_name] = settings_table
|
||||
self:append{
|
||||
index = table_name,
|
||||
data = {[#settings_table] = value},
|
||||
}
|
||||
end
|
||||
|
||||
local _orig_removeTableItem = LuaSettings.removeTableItem
|
||||
--- Removes index from table.
|
||||
function LuaData:removeTableItem(key, index)
|
||||
_orig_removeTableItem(self, key, index)
|
||||
self:flush()
|
||||
return self
|
||||
end
|
||||
|
||||
--- Appends settings to disk.
|
||||
function LuaData:append(data)
|
||||
if not self.file then return end
|
||||
local f_out = io.open(self.file, "a")
|
||||
if f_out ~= nil then
|
||||
os.setlocale('C', 'numeric')
|
||||
f_out:write(self.name.."Entry")
|
||||
f_out:write(dump(data))
|
||||
f_out:write("\n")
|
||||
f_out:close()
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Replaces existing settings with table.
|
||||
function LuaData:reset(table)
|
||||
self.data = table
|
||||
self:flush()
|
||||
return self
|
||||
end
|
||||
|
||||
--- Writes all settings to disk (does not append).
|
||||
function LuaData:flush()
|
||||
if not self.file then return end
|
||||
|
||||
if lfs.attributes(self.file, "mode") == "file" then
|
||||
for i=1, self.max_backups, 1 do
|
||||
if lfs.attributes(self.file..".old."..i, "mode") == "file" then
|
||||
logger.dbg("LuaData: Rename ", self.file .. ".old." .. i, " to ", self.file .. ".old." .. i+1)
|
||||
os.rename(self.file, self.file .. ".old." .. i+1)
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
logger.dbg("LuaData: Rename ", self.file, " to ", self.file .. ".old.1")
|
||||
os.rename(self.file, self.file .. ".old.1")
|
||||
end
|
||||
|
||||
logger.dbg("LuaData: Write to ", self.file)
|
||||
local f_out = io.open(self.file, "w")
|
||||
if f_out ~= nil then
|
||||
os.setlocale('C', 'numeric')
|
||||
f_out:write("-- we can read Lua syntax here!\n")
|
||||
f_out:write(self.name.."Entry")
|
||||
f_out:write(dump(self.data))
|
||||
f_out:write("\n")
|
||||
f_out:close()
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
return LuaData
|
||||
@@ -6,6 +6,13 @@ local dump = require("dump")
|
||||
|
||||
local LuaSettings = {}
|
||||
|
||||
function LuaSettings:new(o)
|
||||
o = o or {}
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
return o
|
||||
end
|
||||
|
||||
--- Opens a settings file.
|
||||
function LuaSettings:open(file_path)
|
||||
local new = {file=file_path}
|
||||
@@ -133,6 +140,22 @@ function LuaSettings:flipFalse(key)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Adds item to table.
|
||||
function LuaSettings:addTableItem(key, value)
|
||||
local settings_table = self:has(key) and self:readSetting(key) or {}
|
||||
table.insert(settings_table, value)
|
||||
self:saveSetting(key, settings_table)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Removes index from table.
|
||||
function LuaSettings:removeTableItem(key, index)
|
||||
local settings_table = self:has(key) and self:readSetting(key) or {}
|
||||
table.remove(settings_table, index)
|
||||
self:saveSetting(key, settings_table)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Replaces existing settings with table.
|
||||
function LuaSettings:reset(table)
|
||||
self.data = table
|
||||
|
||||
@@ -53,6 +53,7 @@ local order = {
|
||||
},
|
||||
search = {
|
||||
"dictionary_lookup",
|
||||
"dictionary_lookup_history",
|
||||
"dictionary_settings",
|
||||
"----------------------------",
|
||||
"wikipedia_lookup",
|
||||
|
||||
@@ -71,6 +71,7 @@ local order = {
|
||||
},
|
||||
search = {
|
||||
"dictionary_lookup",
|
||||
"dictionary_lookup_history",
|
||||
"dictionary_settings",
|
||||
"----------------------------",
|
||||
"wikipedia_lookup",
|
||||
|
||||
145
spec/unit/luadata_spec.lua
Normal file
145
spec/unit/luadata_spec.lua
Normal file
@@ -0,0 +1,145 @@
|
||||
describe("luadata module", function()
|
||||
local Settings
|
||||
setup(function()
|
||||
require("commonrequire")
|
||||
Settings = require("frontend/luadata"):open("this-is-not-a-valid-file")
|
||||
end)
|
||||
|
||||
it("should handle undefined keys", function()
|
||||
Settings:delSetting("abc")
|
||||
|
||||
assert.True(Settings:hasNot("abc"))
|
||||
assert.True(Settings:nilOrTrue("abc"))
|
||||
assert.False(Settings:isTrue("abc"))
|
||||
Settings:saveSetting("abc", true)
|
||||
assert.True(Settings:has("abc"))
|
||||
assert.True(Settings:nilOrTrue("abc"))
|
||||
assert.True(Settings:isTrue("abc"))
|
||||
end)
|
||||
|
||||
it("should flip bool values", function()
|
||||
Settings:delSetting("abc")
|
||||
|
||||
assert.True(Settings:hasNot("abc"))
|
||||
Settings:flipNilOrTrue("abc")
|
||||
assert.False(Settings:nilOrTrue("abc"))
|
||||
assert.True(Settings:has("abc"))
|
||||
assert.False(Settings:isTrue("abc"))
|
||||
Settings:flipNilOrTrue("abc")
|
||||
assert.True(Settings:nilOrTrue("abc"))
|
||||
assert.True(Settings:hasNot("abc"))
|
||||
assert.False(Settings:isTrue("abc"))
|
||||
|
||||
Settings:flipTrue("abc")
|
||||
assert.True(Settings:has("abc"))
|
||||
assert.True(Settings:isTrue("abc"))
|
||||
assert.True(Settings:nilOrTrue("abc"))
|
||||
Settings:flipTrue("abc")
|
||||
assert.False(Settings:has("abc"))
|
||||
assert.False(Settings:isTrue("abc"))
|
||||
assert.True(Settings:nilOrTrue("abc"))
|
||||
end)
|
||||
|
||||
it("should create child settings", function()
|
||||
Settings:delSetting("key")
|
||||
|
||||
Settings:saveSetting("key", {
|
||||
a = "b",
|
||||
c = "true",
|
||||
d = false,
|
||||
})
|
||||
|
||||
local child = Settings:child("key")
|
||||
|
||||
assert.is_not_nil(child)
|
||||
assert.True(child:has("a"))
|
||||
assert.are.equal(child:readSetting("a"), "b")
|
||||
assert.True(child:has("c"))
|
||||
assert.True(child:isTrue("c"))
|
||||
assert.True(child:has("d"))
|
||||
assert.True(child:isFalse("d"))
|
||||
assert.False(child:isTrue("e"))
|
||||
child:flipTrue("e")
|
||||
child:close()
|
||||
|
||||
child = Settings:child("key")
|
||||
assert.True(child:isTrue("e"))
|
||||
end)
|
||||
|
||||
describe("table wrapper", function()
|
||||
Settings:delSetting("key")
|
||||
|
||||
it("should add item to table", function()
|
||||
Settings:addTableItem("key", 1)
|
||||
Settings:addTableItem("key", 2)
|
||||
Settings:addTableItem("key", 3)
|
||||
|
||||
assert.are.equal(1, Settings:readSetting("key")[1])
|
||||
assert.are.equal(2, Settings:readSetting("key")[2])
|
||||
assert.are.equal(3, Settings:readSetting("key")[3])
|
||||
end)
|
||||
|
||||
it("should remove item from table", function()
|
||||
Settings:removeTableItem("key", 1)
|
||||
|
||||
assert.are.equal(2, Settings:readSetting("key")[1])
|
||||
assert.are.equal(3, Settings:readSetting("key")[2])
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("backup data file", function()
|
||||
local file = "dummy-test-file"
|
||||
local d = Settings:open(file)
|
||||
it("should generate data file", function()
|
||||
d:saveSetting("a", "a")
|
||||
assert.Equals("file", lfs.attributes(d.file, "mode"))
|
||||
end)
|
||||
it("should generate backup data file on flush", function()
|
||||
d:flush()
|
||||
-- file and file.old.1 should be generated.
|
||||
assert.Equals("file", lfs.attributes(d.file, "mode"))
|
||||
assert.Equals("file", lfs.attributes(d.file .. ".old.1", "mode"))
|
||||
d:close()
|
||||
end)
|
||||
it("should remove garbage data file", function()
|
||||
-- write some garbage to sidecar-file.
|
||||
local f_out = io.open(d.file, "w")
|
||||
f_out:write("bla bla bla")
|
||||
f_out:close()
|
||||
|
||||
d = Settings:open(file)
|
||||
-- file should be removed.
|
||||
assert.are.not_equal("file", lfs.attributes(d.file, "mode"))
|
||||
assert.Equals("file", lfs.attributes(d.file .. ".old.2", "mode"))
|
||||
assert.Equals("a", d:readSetting("a"))
|
||||
d:saveSetting("a", "b")
|
||||
d:close()
|
||||
-- backup should be generated.
|
||||
assert.Equals("file", lfs.attributes(d.file, "mode"))
|
||||
assert.Equals("file", lfs.attributes(d.file .. ".old.1", "mode"))
|
||||
-- The contents in file and file.old.1 are different.
|
||||
-- a:b v.s. a:a
|
||||
end)
|
||||
it("should open backup data file after garbage removal", function()
|
||||
d = Settings:open(file)
|
||||
-- We should get the right result.
|
||||
assert.Equals("b", d:readSetting("a"))
|
||||
-- write some garbage to file.
|
||||
local f_out = io.open(d.file, "w")
|
||||
f_out:write("bla bla bla")
|
||||
f_out:close()
|
||||
|
||||
-- do not flush the result, open docsettings again.
|
||||
d = Settings:open(file)
|
||||
-- data file should be removed.
|
||||
assert.are.not_equal("file", lfs.attributes(d.file, "mode"))
|
||||
assert.Equals("file", lfs.attributes(d.file .. ".old.2", "mode"))
|
||||
-- The content should come from file.old.2.
|
||||
assert.Equals("a", d:readSetting("a"))
|
||||
d:close()
|
||||
-- data file should be generated and last good backup should not change name.
|
||||
assert.Equals("file", lfs.attributes(d.file, "mode"))
|
||||
assert.Equals("file", lfs.attributes(d.file .. ".old.2", "mode"))
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
@@ -41,6 +41,8 @@ describe("luasettings module", function()
|
||||
end)
|
||||
|
||||
it("should create child settings", function()
|
||||
Settings:delSetting("key")
|
||||
|
||||
Settings:saveSetting("key", {
|
||||
a = "b",
|
||||
c = "true",
|
||||
@@ -63,4 +65,25 @@ describe("luasettings module", function()
|
||||
child = Settings:child("key")
|
||||
assert.True(child:isTrue("e"))
|
||||
end)
|
||||
|
||||
describe("table wrapper", function()
|
||||
Settings:delSetting("key")
|
||||
|
||||
it("should add item to table", function()
|
||||
Settings:addTableItem("key", 1)
|
||||
Settings:addTableItem("key", 2)
|
||||
Settings:addTableItem("key", 3)
|
||||
|
||||
assert.are.equal(1, Settings:readSetting("key")[1])
|
||||
assert.are.equal(2, Settings:readSetting("key")[2])
|
||||
assert.are.equal(3, Settings:readSetting("key")[3])
|
||||
end)
|
||||
|
||||
it("should remove item from table", function()
|
||||
Settings:removeTableItem("key", 1)
|
||||
|
||||
assert.are.equal(2, Settings:readSetting("key")[1])
|
||||
assert.are.equal(3, Settings:readSetting("key")[2])
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
Reference in New Issue
Block a user