2016-04-18 22:38:14 +00:00
|
|
|
local DataStorage = require("datastorage")
|
|
|
|
local DocSettings = require("docsettings")
|
|
|
|
local dump = require("dump")
|
2019-12-10 22:00:08 +00:00
|
|
|
local ffiutil = require("ffi/util")
|
2020-08-29 16:25:36 +00:00
|
|
|
local util = require("util")
|
2019-12-10 22:00:08 +00:00
|
|
|
local joinPath = ffiutil.joinPath
|
2017-07-19 12:56:17 +00:00
|
|
|
local lfs = require("libs/libkoreader-lfs")
|
2019-12-10 22:00:08 +00:00
|
|
|
local realpath = ffiutil.realpath
|
2016-04-18 22:38:14 +00:00
|
|
|
|
|
|
|
local history_file = joinPath(DataStorage:getDataDir(), "history.lua")
|
|
|
|
|
|
|
|
local ReadHistory = {
|
|
|
|
hist = {},
|
2017-07-19 12:56:17 +00:00
|
|
|
last_read_time = 0,
|
2016-04-18 22:38:14 +00:00
|
|
|
}
|
|
|
|
|
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-01 16:53:04 +00:00
|
|
|
local function selectCallback(path)
|
|
|
|
local ReaderUI = require("apps/reader/readerui")
|
|
|
|
ReaderUI:showReader(path)
|
|
|
|
end
|
|
|
|
|
2016-04-18 22:38:14 +00:00
|
|
|
local function buildEntry(input_time, input_file)
|
2020-08-29 16:25:36 +00:00
|
|
|
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"
|
2016-04-18 22:38:14 +00:00
|
|
|
return {
|
|
|
|
time = input_time,
|
|
|
|
text = input_file:gsub(".*/", ""),
|
2020-08-29 16:25:36 +00:00
|
|
|
file = file_path,
|
2019-01-01 08:44:21 +00:00
|
|
|
dim = not file_exists, -- "dim", as expected by Menu
|
2020-09-15 18:39:32 +00:00
|
|
|
-- mandatory = file_exists and util.getFriendlySize(lfs.attributes(input_file, "size") or 0),
|
2020-08-29 16:25:36 +00:00
|
|
|
mandatory_func = function() -- Show the last read time (rather than file size)
|
|
|
|
local readerui_instance = require("apps/reader/readerui"):_getRunningInstance()
|
2021-05-18 16:20:34 +00:00
|
|
|
local currently_opened_file = readerui_instance and readerui_instance.document and readerui_instance.document.file
|
2020-08-29 16:25:36 +00:00
|
|
|
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
|
ReaderFooter: Don't duplicate a 12h clock time format option (#6973)
* ReaderFooter:
* Honor the global twelve_hour_clock setting, instead of
duplicating a local one.
(Re #6969)
* os.date is a thin wrapper around strftime, so we might be able to get
away with some not-quite-standard extensions...
These are *definitely* supported on Linux, but are *NOT* the glibc
extension (that'd be e.g., %-I), so, hopefully, they're somewhat
portable...
They are also supported on BSD/macOS.
They are *not* supported by the MS UCRT. That means MinGW-w64, too.
This *appears* to be supported on current Bionic (it might even support
said glibc format altering extensions).
* And of course, Windows is terrible, so, make this terribly ugly to not
break it there...
* Turns out BSD also supports the dash trim format extension, so, leave
the trimming to the libc, and handle the special-casing in a way that
doesn't create stupid locals.
* Random unrelated cleanup ^^.
(https://gitter.im/koreader/koreader?at=5fd24be492aa1c4ef5d11f31)
* Update the testsuite
(Because the default used to be 24h clock).
Changed the default to 24h clock ;p.
* Explain why we don't try to fix it in Lua
2020-12-12 09:44:35 +00:00
|
|
|
return util.secondsToDate(last_read_ts, G_reader_settings:isTrue("twelve_hour_clock"))
|
2020-08-29 16:25:36 +00:00
|
|
|
end,
|
2021-02-20 18:05:10 +00:00
|
|
|
select_enabled_func = function()
|
|
|
|
return lfs.attributes(file_path, "mode") == "file"
|
|
|
|
end,
|
2016-04-18 22:38:14 +00:00
|
|
|
callback = function()
|
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-01 16:53:04 +00:00
|
|
|
selectCallback(input_file)
|
2016-04-18 22:38:14 +00:00
|
|
|
end
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2016-09-13 03:02:48 +00:00
|
|
|
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
|
|
|
|
|
2016-07-19 05:37:57 +00:00
|
|
|
function ReadHistory:_indexing(start)
|
2017-07-19 12:56:17 +00:00
|
|
|
assert(self ~= nil)
|
2019-08-23 17:53:53 +00:00
|
|
|
--- @todo (Hzj_jie): Use binary search to find an item when deleting it.
|
2016-07-19 05:37:57 +00:00
|
|
|
for i = start, #self.hist, 1 do
|
|
|
|
self.hist[i].index = i
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-04-18 22:38:14 +00:00
|
|
|
function ReadHistory:_sort()
|
2017-07-19 12:56:17 +00:00
|
|
|
assert(self ~= nil)
|
|
|
|
local autoremove_deleted_items_from_history =
|
|
|
|
not G_reader_settings:nilOrFalse("autoremove_deleted_items_from_history")
|
2017-02-25 21:16:19 +00:00
|
|
|
if autoremove_deleted_items_from_history then
|
|
|
|
self:clearMissing()
|
|
|
|
end
|
2016-09-13 03:02:48 +00:00
|
|
|
table.sort(self.hist, fileFirstOrdering)
|
2019-08-23 17:53:53 +00:00
|
|
|
--- @todo (zijiehe): Use binary insert instead of a loop to deduplicate.
|
2016-04-18 22:38:14 +00:00
|
|
|
for i = #self.hist, 2, -1 do
|
|
|
|
if self.hist[i].file == self.hist[i - 1].file then
|
2016-09-13 03:02:48 +00:00
|
|
|
table.remove(self.hist, i)
|
2016-04-18 22:38:14 +00:00
|
|
|
end
|
|
|
|
end
|
2016-09-13 03:02:48 +00:00
|
|
|
table.sort(self.hist, timeFirstOrdering)
|
2016-07-19 05:37:57 +00:00
|
|
|
self:_indexing(1)
|
2016-04-18 22:38:14 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
-- Reduces total count in hist list to a reasonable number by removing last
|
|
|
|
-- several items.
|
|
|
|
function ReadHistory:_reduce()
|
2017-07-19 12:56:17 +00:00
|
|
|
assert(self ~= nil)
|
2016-04-18 22:38:14 +00:00
|
|
|
while #self.hist > 500 do
|
|
|
|
table.remove(self.hist, #self.hist)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Flushes current history table into file.
|
|
|
|
function ReadHistory:_flush()
|
2017-07-19 12:56:17 +00:00
|
|
|
assert(self ~= nil)
|
2016-04-18 22:38:14 +00:00
|
|
|
local content = {}
|
2020-05-29 12:30:39 +00:00
|
|
|
for _, v in ipairs(self.hist) do
|
|
|
|
table.insert(content, {
|
2016-04-18 22:38:14 +00:00
|
|
|
time = v.time,
|
|
|
|
file = v.file
|
2020-05-29 12:30:39 +00:00
|
|
|
})
|
2016-04-18 22:38:14 +00:00
|
|
|
end
|
|
|
|
local f = io.open(history_file, "w")
|
|
|
|
f:write("return " .. dump(content) .. "\n")
|
2019-12-10 22:00:08 +00:00
|
|
|
ffiutil.fsyncOpenedFile(f) -- force flush to the storage device
|
2016-04-18 22:38:14 +00:00
|
|
|
f:close()
|
|
|
|
end
|
|
|
|
|
2017-07-19 12:56:17 +00:00
|
|
|
--- Reads history table from file.
|
|
|
|
-- @treturn boolean true if the history_file has been updated and reloaded.
|
2016-04-18 22:38:14 +00:00
|
|
|
function ReadHistory:_read()
|
2017-07-19 12:56:17 +00:00
|
|
|
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
|
2016-04-18 22:38:14 +00:00
|
|
|
local ok, data = pcall(dofile, history_file)
|
2016-09-13 03:02:48 +00:00
|
|
|
if ok and data then
|
2020-05-29 12:30:39 +00:00
|
|
|
self.hist = {}
|
|
|
|
for _, v in ipairs(data) do
|
2016-04-18 22:38:14 +00:00
|
|
|
table.insert(self.hist, buildEntry(v.time, v.file))
|
|
|
|
end
|
|
|
|
end
|
2017-07-19 12:56:17 +00:00
|
|
|
return true
|
2016-04-18 22:38:14 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
-- Reads history from legacy history folder
|
|
|
|
function ReadHistory:_readLegacyHistory()
|
2017-07-19 12:56:17 +00:00
|
|
|
assert(self ~= nil)
|
2016-04-18 22:38:14 +00:00
|
|
|
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
|
2016-07-19 05:37:57 +00:00
|
|
|
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,
|
2016-09-13 03:02:48 +00:00
|
|
|
buildEntry(lfs.attributes(joinPath(history_dir, f), "modification"),
|
2016-07-19 05:37:57 +00:00
|
|
|
joinPath(path, file)))
|
|
|
|
end
|
|
|
|
end
|
2016-04-18 22:38:14 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReadHistory:_init()
|
2017-07-19 12:56:17 +00:00
|
|
|
assert(self ~= nil)
|
|
|
|
self:reload()
|
2016-04-18 22:38:14 +00:00
|
|
|
end
|
|
|
|
|
2020-05-06 19:11:34 +00:00
|
|
|
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)
|
2021-03-06 21:44:18 +00:00
|
|
|
if G_reader_settings:isTrue("autoremove_deleted_items_from_history") then
|
2020-05-06 19:11:34 +00:00
|
|
|
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)
|
2021-03-06 21:44:18 +00:00
|
|
|
if G_reader_settings:isTrue("autoremove_deleted_items_from_history") then
|
2020-05-06 19:11:34 +00:00
|
|
|
-- Also remove it from history on purge when that setting is enabled
|
|
|
|
self:removeItemByPath(path)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-02-25 18:22:04 +00:00
|
|
|
function ReadHistory:clearMissing()
|
2017-07-19 12:56:17 +00:00
|
|
|
assert(self ~= nil)
|
2017-02-25 18:22:04 +00:00
|
|
|
for i = #self.hist, 1, -1 do
|
|
|
|
if self.hist[i].file == nil or lfs.attributes(self.hist[i].file, "mode") ~= "file" then
|
2020-08-24 23:47:04 +00:00
|
|
|
self:removeItem(self.hist[i], i)
|
2017-02-25 18:22:04 +00:00
|
|
|
end
|
|
|
|
end
|
2020-05-06 19:11:34 +00:00
|
|
|
self:ensureLastFile()
|
2017-02-25 18:22:04 +00:00
|
|
|
end
|
|
|
|
|
2017-02-25 21:16:19 +00:00
|
|
|
function ReadHistory:removeItemByPath(path)
|
2017-07-19 12:56:17 +00:00
|
|
|
assert(self ~= nil)
|
2017-02-25 21:16:19 +00:00
|
|
|
for i = #self.hist, 1, -1 do
|
|
|
|
if self.hist[i].file == path then
|
|
|
|
self:removeItem(self.hist[i])
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
2020-05-06 19:11:34 +00:00
|
|
|
self:ensureLastFile()
|
2017-02-25 21:16:19 +00:00
|
|
|
end
|
|
|
|
|
2018-01-16 17:20:25 +00:00
|
|
|
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
|
2020-05-06 19:11:34 +00:00
|
|
|
self.hist[i].text = new_path:gsub(".*/", "")
|
2018-01-16 17:20:25 +00:00
|
|
|
self:_flush()
|
|
|
|
self.hist[i].callback = function()
|
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-01 16:53:04 +00:00
|
|
|
selectCallback(new_path)
|
2018-01-16 17:20:25 +00:00
|
|
|
end
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
2020-05-06 19:11:34 +00:00
|
|
|
if G_reader_settings:readSetting("lastfile") == old_path then
|
|
|
|
G_reader_settings:saveSetting("lastfile", new_path)
|
|
|
|
end
|
|
|
|
self:ensureLastFile()
|
2018-01-16 17:20:25 +00:00
|
|
|
end
|
|
|
|
|
2020-08-24 23:47:04 +00:00
|
|
|
function ReadHistory:removeItem(item, idx)
|
2017-07-19 12:56:17 +00:00
|
|
|
assert(self ~= nil)
|
2020-08-24 23:47:04 +00:00
|
|
|
table.remove(self.hist, item.index or idx)
|
2016-04-18 22:38:14 +00:00
|
|
|
os.remove(DocSettings:getHistoryPath(item.file))
|
2020-08-24 23:47:04 +00:00
|
|
|
self:_indexing(item.index or idx)
|
2016-04-18 22:38:14 +00:00
|
|
|
self:_flush()
|
2020-05-06 19:11:34 +00:00
|
|
|
self:ensureLastFile()
|
2016-04-18 22:38:14 +00:00
|
|
|
end
|
|
|
|
|
2020-12-15 20:10:36 +00:00
|
|
|
function ReadHistory:addItem(file, ts)
|
2017-07-19 12:56:17 +00:00
|
|
|
assert(self ~= nil)
|
2016-04-18 22:38:14 +00:00
|
|
|
if file ~= nil and lfs.attributes(file, "mode") == "file" then
|
2020-12-15 20:10:36 +00:00
|
|
|
local now = ts or os.time()
|
2018-05-30 04:50:15 +00:00
|
|
|
table.insert(self.hist, 1, buildEntry(now, file))
|
2019-08-23 17:53:53 +00:00
|
|
|
--- @todo (zijiehe): We do not need to sort if we can use binary insert and
|
2016-04-18 22:38:14 +00:00
|
|
|
-- binary search.
|
2018-05-30 04:50:15 +00:00
|
|
|
-- 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)
|
2016-04-18 22:38:14 +00:00
|
|
|
self:_sort()
|
|
|
|
self:_reduce()
|
|
|
|
self:_flush()
|
2020-05-06 19:11:34 +00:00
|
|
|
G_reader_settings:saveSetting("lastfile", file)
|
2017-09-22 16:24:38 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-07-19 12:56:17 +00:00
|
|
|
--- 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
|
|
|
|
|
2016-04-18 22:38:14 +00:00
|
|
|
ReadHistory:_init()
|
|
|
|
|
|
|
|
return ReadHistory
|