You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
koreader/plugins/statistics.koplugin/main.lua

425 lines
13 KiB
Lua

local InputContainer = require("ui/widget/container/inputcontainer")
local MultiInputDialog = require("ui/widget/multiinputdialog")
local CenterContainer = require("ui/widget/container/centercontainer")
local UIManager = require("ui/uimanager")
local Screen = require("device").screen
local Menu = require("ui/widget/menu")
local Font = require("ui/font")
local TimeVal = require("ui/timeval")
local dump = require("dump")
local lfs = require("libs/libkoreader-lfs")
local DEBUG = require("dbg")
local T = require("ffi/util").template
local _ = require("gettext")
local tableutil = require("tableutil")
local statistics_dir = "./statistics"
local ReaderStatistics = InputContainer:new {
last_time = nil,
page_min_read_sec = 5,
page_max_read_sec = 90,
current_period = 0,
is_enabled = nil,
data = {
title = "",
authors = "",
language = "",
series = "",
details = {},
total_time = 0,
highlights = 0,
notes = 0,
pages = 0,
},
}
function ReaderStatistics:init()
if self.ui.document.is_djvu or self.ui.document.is_pdf or self.ui.document.is_pic then
return
end
self.ui.menu:registerToMainMenu(self)
self.current_period = 0
local settings = G_reader_settings:readSetting("statistics") or {}
self.page_min_read_sec = tonumber(settings.min_sec)
self.page_max_read_sec = tonumber(settings.max_sec)
self.is_enabled = not (settings.is_enabled == false)
self.last_time = TimeVal:now()
UIManager:scheduleIn(0.1, function() self:initData() end)
end
function ReaderStatistics:initData()
--first execution
if self.is_enabled then
local book_properties = self:getBookProperties()
self.data = self:importFromFile(book_properties.title .. ".stat")
self:savePropertiesInToData(book_properties)
self.data.pages = self.view.document:getPageCount()
return
end
end
function ReaderStatistics:addToMainMenu(tab_item_table)
table.insert(tab_item_table.plugins, {
text = _("Statistics"),
sub_item_table = {
self:getStatisticEnabledMenuTable(),
self:getStatisticSettingsMenuTable(),
self:getStatisticForCurrentBookMenuTable(),
self:getStatisticTotalStatisticMenuTable(),
}
})
end
function ReaderStatistics:getStatisticEnabledMenuTable()
return {
text_func = function()
return _("Enabled")
end,
checked_func = function() return self.is_enabled end,
callback = function()
-- if was enabled, have to save data to file
if self.last_time and self.is_enabled then
self:exportToFile(self:getBookProperties())
end
self.is_enabled = not self.is_enabled
-- if was disabled have to get data from file
if self.is_enabled then
self:initData()
end
self:onSaveSettings()
end,
}
end
function ReaderStatistics:getStatisticSettingsMenuTable()
return {
text_func = function()
return _("Settings")
end,
checked_func = function() return false end,
callback = function()
self:updateSettings()
end,
}
end
function ReaderStatistics:updateSettings()
self.settings_dialog = MultiInputDialog:new {
title = _("Statistics settings"),
fields = {
{
text = "",
input_type = "number",
hint = _("Min seconds, default is 5"),
},
{
text = "",
input_type = "number",
hint = _("Max seconds, default is 90"),
},
},
buttons = {
{
{
text = _("Cancel"),
callback = function()
self.settings_dialog:onClose()
UIManager:close(self.settings_dialog)
end
},
{
text = _("Apply"),
callback = function()
self.settings_dialog:onClose()
UIManager:close(self.settings_dialog)
self:onSaveSettings(MultiInputDialog:getFields())
end
},
},
},
width = Screen:getWidth() * 0.95,
height = Screen:getHeight() * 0.2,
input_type = "number",
}
self.settings_dialog:onShowKeyboard()
UIManager:show(self.settings_dialog)
end
function ReaderStatistics:getStatisticForCurrentBookMenuTable()
self.status_menu = {}
local book_status = Menu:new {
title = _("Status"),
item_table = self:updateCurrentStat(),
is_borderless = true,
is_popout = false,
is_enable_shortcut = false,
width = Screen:getWidth(),
height = Screen:getHeight(),
cface = Font:getFace("cfont", 20),
}
self.status_menu = CenterContainer:new {
dimen = Screen:getSize(),
book_status,
}
book_status.close_callback = function()
UIManager:close(self.status_menu)
end
book_status.show_parent = self.status_menu
return {
text = _("Current"),
enabled_func = function() return true end,
checked_func = function() return false end,
callback = function()
book_status:swithItemTable(nil, self:updateCurrentStat())
UIManager:show(self.status_menu)
return true
end
}
end
function ReaderStatistics:getStatisticTotalStatisticMenuTable()
self.total_status = Menu:new {
title = _("Total"),
item_table = self:updateTotalStat(),
is_borderless = true,
is_popout = false,
is_enable_shortcut = false,
width = Screen:getWidth(),
height = Screen:getHeight(),
cface = Font:getFace("cfont", 20),
}
self.total_menu = CenterContainer:new {
dimen = Screen:getSize(),
self.total_status,
}
self.total_status.close_callback = function()
UIManager:close(self.total_menu)
end
self.total_status.show_parent = self.total_menu
return {
text = _("Total"),
callback = function()
self.total_status:swithItemTable(nil, self:updateTotalStat())
UIManager:show(self.total_menu)
return true
end
}
end
function ReaderStatistics:updateCurrentStat()
local stats = {}
local dates = {}
for k, v in pairs(self.data.details) do
dates[os.date("%Y-%m-%d", v.time)] = ""
end
table.insert(stats, { text = _("Current period"), mandatory = os.date("!%X", self.current_period) })
table.insert(stats, { text = _("Total time"), mandatory = os.date("!%X", self.data.total_time) })
table.insert(stats, { text = _("Total highlights"), mandatory = self.data.highlights })
table.insert(stats, { text = _("Total notes"), mandatory = self.data.notes })
table.insert(stats, { text = _("Total days"), mandatory = tableutil.tablelength(dates) })
table.insert(stats, { text = _("Average time per page"), mandatory = os.date("!%X", self.data.total_time / tableutil.tablelength(self.data.details)) })
table.insert(stats, { text = _("Read pages/Total pages"), mandatory = tableutil.tablelength(self.data.details) .. "/" .. self.data.pages })
return stats
end
function ReaderStatistics:getDatesForBook(book)
local dates = {}
local result = {}
for k, v in pairs(book.details) do
local date_text = os.date("%Y-%m-%d", v.time)
if not dates[date_text] then
dates[date_text] = {
date = v.time,
read = v.read,
count = 1
}
else
dates[date_text] = {
read = dates[date_text].read + v.read,
count = dates[date_text].count + 1,
date = dates[date_text].date
}
end
end
table.insert(result, { text = book.title })
for k, v in tableutil.spairs(dates, function(t, a, b) return t[b].date > t[a].date end) do
table.insert(result, { text = k, mandatory = T(_("Pages (%1) Time: %2"), v.count, os.date("!%X", v.read)) })
end
return result
end
function ReaderStatistics:updateTotalStat()
local total_stats = {}
local total_books_time = 0
for curr_file in lfs.dir(statistics_dir) do
local path = statistics_dir .. "/" .. curr_file
if lfs.attributes(path, "mode") == "file" then
local book_result = self:importFromFile(curr_file)
if book_result and book_result.title ~= self.data.title then
table.insert(total_stats, {
text = book_result.title,
mandatory = os.date("!%X", tonumber(book_result.total_time)),
callback = function()
self.total_status:swithItemTable(nil, self:getDatesForBook(book_result))
UIManager:show(self.total_menu)
return true
end,
})
total_books_time = total_books_time + book_result.total_time
end
end
end
total_books_time = total_books_time + tonumber(self.data.total_time)
table.insert(total_stats, 1, { text = _("All time"), mandatory = os.date("!%X", total_books_time) })
table.insert(total_stats, 2, { text = _("----------------------------------------------------") })
table.insert(total_stats, 3, {
text = self.data.title,
mandatory = os.date("!%X", tonumber(self.data.total_time)),
callback = function()
self.total_status:swithItemTable(nil, self:getDatesForBook(self.data))
UIManager:show(self.total_menu)
return true
end,
})
return total_stats
end
function ReaderStatistics:getBookProperties()
local props = self.view.document:getProps()
if props.title == "No document" or props.title == "" then --sometime crengine returns "No document" try to get one more time
props = self.view.document:getProps()
end
return props
end
function ReaderStatistics:onPageUpdate(pageno)
if self.is_enabled then
local curr_time = TimeVal:now()
local diff_time = curr_time.sec - self.last_time.sec
-- if last update was more then 10 minutes then current period set to 0
if (diff_time > 600) then
self.current_period = 0
end
if diff_time >= self.page_min_read_sec and diff_time <= self.page_max_read_sec then
self.current_period = self.current_period + diff_time
self.data.total_time = self.data.total_time + diff_time
local timeData = {
time = curr_time.sec,
read = diff_time,
page = pageno
}
table.insert(self.data.details, timeData)
end
self.last_time = curr_time
end
end
function ReaderStatistics:exportToFile(book_properties)
if book_properties then
self:savePropertiesInToData(book_properties)
end
local statistics = io.open(statistics_dir .. "/" .. self.data.title .. ".stat", "w")
if statistics then
local current_locale = os.setlocale()
os.setlocale("C")
local data = dump(self.data)
statistics:write("return ")
statistics:write(data)
statistics:write("\n")
statistics:close()
os.setlocale(current_locale)
end
end
function ReaderStatistics:savePropertiesInToData(item)
self.data.title = item.title
self.data.authors = item.authors
self.data.language = item.language
self.data.series = item.series
end
function ReaderStatistics:importFromFile(item)
item = string.gsub(item, "^%s*(.-)%s*$", "%1") --trim
if lfs.attributes(statistics_dir, "mode") ~= "directory" then
lfs.mkdir("statistics")
end
local statisticFile = statistics_dir .. "/" .. item
local ok, stored = pcall(dofile, statisticFile)
if ok then
return stored
else
DEBUG(stored)
end
end
function ReaderStatistics:onCloseDocument()
if self.last_time and self.is_enabled then
self:exportToFile()
end
end
function ReaderStatistics:onAddHighlight()
self.data.highlights = self.data.highlights + 1
end
function ReaderStatistics:onAddNote()
self.data.notes = self.data.notes + 1
end
-- in case when screensaver starts
function ReaderStatistics:onFlushSettings()
self:onSaveSettings()
self:exportToFile()
self.current_period = 0
return true
end
-- screensaver off
function ReaderStatistics:onResume()
self.current_period = 0
return true
end
function ReaderStatistics:onSaveSettings(fields)
if fields then
self.page_min_read_sec = tonumber(fields[1])
self.page_max_read_sec = tonumber(fields[2])
end
local settings = {
min_sec = self.page_min_read_sec,
max_sec = self.page_max_read_sec,
is_enabled = self.is_enabled,
}
G_reader_settings:saveSetting("statistics", settings)
end
return ReaderStatistics