Files
koreader/frontend/readhistory.lua
NiLuJe e7acec1526 ReaderUI: Saner FM/RD lifecycle
* Ensure that going from one to the other tears down the former and
    its plugins before instantiating the latter and its plugins.

UIManager: Unify Event sending & broadcasting
  * Make the two behave the same way (walk the widget stack from top to
    bottom), and properly handle the window stack shrinking shrinking
    *and* growing.
    Previously, broadcasting happened bottom-to-top and didn't really
    handle the list shrinking/growing, while sending only handled the list
    shrinking by a single element, and hopefully that element being the one
    the event was just sent to.

These two items combined allowed us to optimize suboptimal
refresh behavior with Menu and other Menu classes when
opening/closing a document.
e.g., the "opening document" Notification is now properly regional,
and the "open last doc" option no longer flashes like a crazy person
anymore.

Plugins: Allow optimizing Menu refresh with custom menus, too.

Requires moving Menu's close_callback *after* onMenuSelect, which, eh,
probably makes sense, and is probably harmless in the grand scheme of
things.
2021-05-05 20:37:33 +02:00

308 lines
9.6 KiB
Lua

local DataStorage = require("datastorage")
local DocSettings = require("docsettings")
local dump = require("dump")
local ffiutil = require("ffi/util")
local util = require("util")
local joinPath = ffiutil.joinPath
local lfs = require("libs/libkoreader-lfs")
local realpath = ffiutil.realpath
local history_file = joinPath(DataStorage:getDataDir(), "history.lua")
local ReadHistory = {
hist = {},
last_read_time = 0,
}
local function selectCallback(path)
local ReaderUI = require("apps/reader/readerui")
ReaderUI:showReader(path)
end
local function buildEntry(input_time, input_file)
local file_path = realpath(input_file) or input_file -- keep orig file path of deleted files
local file_exists = lfs.attributes(file_path, "mode") == "file"
return {
time = input_time,
text = input_file:gsub(".*/", ""),
file = file_path,
dim = not file_exists, -- "dim", as expected by Menu
-- mandatory = file_exists and util.getFriendlySize(lfs.attributes(input_file, "size") or 0),
mandatory_func = function() -- Show the last read time (rather than file size)
local readerui_instance = require("apps/reader/readerui"):_getRunningInstance()
local currently_opened_file = readerui_instance and readerui_instance.document.file
local last_read_ts
if file_path == currently_opened_file then
-- Don't use the sidecar file date which is updated regularly while
-- reading: keep showing the opening time for the current document.
last_read_ts = input_time
else
-- For past documents, the last save time of the settings is better
-- as last read time than input_time (its last opening time, that
-- we fallback to it no sidecar file)
last_read_ts = DocSettings:getLastSaveTime(file_path) or input_time
end
return util.secondsToDate(last_read_ts, G_reader_settings:isTrue("twelve_hour_clock"))
end,
select_enabled_func = function()
return lfs.attributes(file_path, "mode") == "file"
end,
callback = function()
selectCallback(input_file)
end
}
end
local function fileFirstOrdering(l, r)
if l.file == r.file then
return l.time > r.time
else
return l.file < r.file
end
end
local function timeFirstOrdering(l, r)
if l.time == r.time then
return l.file < r.file
else
return l.time > r.time
end
end
function ReadHistory:_indexing(start)
assert(self ~= nil)
--- @todo (Hzj_jie): Use binary search to find an item when deleting it.
for i = start, #self.hist, 1 do
self.hist[i].index = i
end
end
function ReadHistory:_sort()
assert(self ~= nil)
local autoremove_deleted_items_from_history =
not G_reader_settings:nilOrFalse("autoremove_deleted_items_from_history")
if autoremove_deleted_items_from_history then
self:clearMissing()
end
table.sort(self.hist, fileFirstOrdering)
--- @todo (zijiehe): Use binary insert instead of a loop to deduplicate.
for i = #self.hist, 2, -1 do
if self.hist[i].file == self.hist[i - 1].file then
table.remove(self.hist, i)
end
end
table.sort(self.hist, timeFirstOrdering)
self:_indexing(1)
end
-- Reduces total count in hist list to a reasonable number by removing last
-- several items.
function ReadHistory:_reduce()
assert(self ~= nil)
while #self.hist > 500 do
table.remove(self.hist, #self.hist)
end
end
-- Flushes current history table into file.
function ReadHistory:_flush()
assert(self ~= nil)
local content = {}
for _, v in ipairs(self.hist) do
table.insert(content, {
time = v.time,
file = v.file
})
end
local f = io.open(history_file, "w")
f:write("return " .. dump(content) .. "\n")
ffiutil.fsyncOpenedFile(f) -- force flush to the storage device
f:close()
end
--- Reads history table from file.
-- @treturn boolean true if the history_file has been updated and reloaded.
function ReadHistory:_read()
assert(self ~= nil)
local history_file_modification_time = lfs.attributes(history_file, "modification")
if history_file_modification_time == nil
or history_file_modification_time <= self.last_read_time then
return false
end
self.last_read_time = history_file_modification_time
local ok, data = pcall(dofile, history_file)
if ok and data then
self.hist = {}
for _, v in ipairs(data) do
table.insert(self.hist, buildEntry(v.time, v.file))
end
end
return true
end
-- Reads history from legacy history folder
function ReadHistory:_readLegacyHistory()
assert(self ~= nil)
local history_dir = DataStorage:getHistoryDir()
for f in lfs.dir(history_dir) do
local path = joinPath(history_dir, f)
if lfs.attributes(path, "mode") == "file" then
path = DocSettings:getPathFromHistory(f)
if path ~= nil and path ~= "" then
local file = DocSettings:getNameFromHistory(f)
if file ~= nil and file ~= "" then
table.insert(
self.hist,
buildEntry(lfs.attributes(joinPath(history_dir, f), "modification"),
joinPath(path, file)))
end
end
end
end
end
function ReadHistory:_init()
assert(self ~= nil)
self:reload()
end
function ReadHistory:ensureLastFile()
local last_existing_file = nil
for i=1, #self.hist do
if lfs.attributes(self.hist[i].file, "mode") == "file" then
last_existing_file = self.hist[i].file
break
end
end
G_reader_settings:saveSetting("lastfile", last_existing_file)
end
function ReadHistory:getLastFile()
self:ensureLastFile()
return G_reader_settings:readSetting("lastfile")
end
function ReadHistory:getPreviousFile(current_file)
-- Get last or previous file in history that is not current_file
-- (self.ui.document.file, probided as current_file, might have
-- been removed from history)
if not current_file then
current_file = G_reader_settings:readSetting("lastfile")
end
for i=1, #self.hist do
-- skip current document and deleted items kept in history
local file = self.hist[i].file
if file ~= current_file and lfs.attributes(file, "mode") == "file" then
return file
end
end
end
function ReadHistory:fileDeleted(path)
if G_reader_settings:isTrue("autoremove_deleted_items_from_history") then
self:removeItemByPath(path)
else
-- Make it dimed
for i=1, #self.hist do
if self.hist[i].file == path then
self.hist[i].dim = true
break
end
end
self:ensureLastFile()
end
end
function ReadHistory:fileSettingsPurged(path)
if G_reader_settings:isTrue("autoremove_deleted_items_from_history") then
-- Also remove it from history on purge when that setting is enabled
self:removeItemByPath(path)
end
end
function ReadHistory:clearMissing()
assert(self ~= nil)
for i = #self.hist, 1, -1 do
if self.hist[i].file == nil or lfs.attributes(self.hist[i].file, "mode") ~= "file" then
self:removeItem(self.hist[i], i)
end
end
self:ensureLastFile()
end
function ReadHistory:removeItemByPath(path)
assert(self ~= nil)
for i = #self.hist, 1, -1 do
if self.hist[i].file == path then
self:removeItem(self.hist[i])
break
end
end
self:ensureLastFile()
end
function ReadHistory:updateItemByPath(old_path, new_path)
assert(self ~= nil)
for i = #self.hist, 1, -1 do
if self.hist[i].file == old_path then
self.hist[i].file = new_path
self.hist[i].text = new_path:gsub(".*/", "")
self:_flush()
self.hist[i].callback = function()
selectCallback(new_path)
end
break
end
end
if G_reader_settings:readSetting("lastfile") == old_path then
G_reader_settings:saveSetting("lastfile", new_path)
end
self:ensureLastFile()
end
function ReadHistory:removeItem(item, idx)
assert(self ~= nil)
table.remove(self.hist, item.index or idx)
os.remove(DocSettings:getHistoryPath(item.file))
self:_indexing(item.index or idx)
self:_flush()
self:ensureLastFile()
end
function ReadHistory:addItem(file, ts)
assert(self ~= nil)
if file ~= nil and lfs.attributes(file, "mode") == "file" then
local now = ts or os.time()
table.insert(self.hist, 1, buildEntry(now, file))
--- @todo (zijiehe): We do not need to sort if we can use binary insert and
-- binary search.
-- util.execute("/bin/touch", "-a", file)
-- This emulates `touch -a` in LuaFileSystem's API, since it may be absent (Android)
-- or provided by busybox, which doesn't support the `-a` flag.
local mtime = lfs.attributes(file, "modification")
lfs.touch(file, now, mtime)
self:_sort()
self:_reduce()
self:_flush()
G_reader_settings:saveSetting("lastfile", file)
end
end
--- Reloads history from history_file.
-- @treturn boolean true if history_file has been updated and reload happened.
function ReadHistory:reload()
assert(self ~= nil)
if self:_read() then
self:_readLegacyHistory()
self:_sort()
self:_reduce()
return true
end
return false
end
ReadHistory:_init()
return ReadHistory