2020-01-04 00:18:51 +00:00
|
|
|
local BD = require("ui/bidi")
|
2017-09-14 20:29:09 +00:00
|
|
|
local BookStatusWidget = require("ui/widget/bookstatuswidget")
|
2017-08-29 15:34:49 +00:00
|
|
|
local ConfirmBox = require("ui/widget/confirmbox")
|
|
|
|
local DataStorage = require("datastorage")
|
2019-08-01 17:10:46 +00:00
|
|
|
local Device = require("device")
|
2017-08-29 15:34:49 +00:00
|
|
|
local DocSettings = require("docsettings")
|
|
|
|
local InfoMessage = require("ui/widget/infomessage")
|
2016-02-14 21:47:36 +00:00
|
|
|
local KeyValuePage = require("ui/widget/keyvaluepage")
|
2020-05-29 12:22:27 +00:00
|
|
|
local Math = require("optmath")
|
2017-09-16 11:36:54 +00:00
|
|
|
local ReaderFooter = require("apps/reader/modules/readerfooter")
|
2017-08-29 15:34:49 +00:00
|
|
|
local ReaderProgress = require("readerprogress")
|
|
|
|
local ReadHistory = require("readhistory")
|
2017-12-17 17:27:24 +00:00
|
|
|
local Screensaver = require("ui/screensaver")
|
2017-08-29 15:34:49 +00:00
|
|
|
local SQ3 = require("lua-ljsqlite3/init")
|
2015-09-07 17:06:17 +00:00
|
|
|
local TimeVal = require("ui/timeval")
|
2017-08-29 15:34:49 +00:00
|
|
|
local UIManager = require("ui/uimanager")
|
|
|
|
local Widget = require("ui/widget/widget")
|
2015-09-07 17:06:17 +00:00
|
|
|
local lfs = require("libs/libkoreader-lfs")
|
2017-08-29 15:34:49 +00:00
|
|
|
local logger = require("logger")
|
2015-11-27 15:13:01 +00:00
|
|
|
local util = require("util")
|
2017-08-29 15:34:49 +00:00
|
|
|
local _ = require("gettext")
|
|
|
|
local joinPath = require("ffi/util").joinPath
|
|
|
|
local Screen = require("device").screen
|
2019-08-25 13:32:41 +00:00
|
|
|
local N_ = _.ngettext
|
2017-08-29 15:34:49 +00:00
|
|
|
local T = require("ffi/util").template
|
2016-11-04 21:35:20 +00:00
|
|
|
|
2017-08-29 15:34:49 +00:00
|
|
|
local statistics_dir = DataStorage:getDataDir() .. "/statistics/"
|
2017-08-29 18:42:17 +00:00
|
|
|
local db_location = DataStorage:getSettingsDir() .. "/statistics.sqlite3"
|
2017-08-29 15:34:49 +00:00
|
|
|
local PAGE_INSERT = 50
|
|
|
|
local DEFAULT_MIN_READ_SEC = 5
|
|
|
|
local DEFAULT_MAX_READ_SEC = 120
|
2020-02-16 00:03:12 +00:00
|
|
|
local DEFAULT_CALENDAR_START_DAY_OF_WEEK = 2 -- Monday
|
|
|
|
local DEFAULT_CALENDAR_NB_BOOK_SPANS = 3
|
2015-09-07 17:06:17 +00:00
|
|
|
|
2016-12-25 20:13:30 +00:00
|
|
|
local ReaderStatistics = Widget:extend{
|
2018-08-17 18:54:11 +00:00
|
|
|
name = "statistics",
|
2017-08-29 15:34:49 +00:00
|
|
|
page_min_read_sec = DEFAULT_MIN_READ_SEC,
|
|
|
|
page_max_read_sec = DEFAULT_MAX_READ_SEC,
|
2020-02-16 00:03:12 +00:00
|
|
|
calendar_start_day_of_week = DEFAULT_CALENDAR_START_DAY_OF_WEEK,
|
|
|
|
calendar_nb_book_spans = DEFAULT_CALENDAR_NB_BOOK_SPANS,
|
|
|
|
calendar_show_histogram = true,
|
|
|
|
calendar_browse_future_months = false,
|
2017-08-29 15:34:49 +00:00
|
|
|
start_current_period = 0,
|
|
|
|
curr_page = 0,
|
|
|
|
id_curr_book = nil,
|
|
|
|
curr_total_time = 0,
|
|
|
|
curr_total_pages = 0,
|
2015-09-07 17:06:17 +00:00
|
|
|
is_enabled = nil,
|
2020-02-16 00:03:12 +00:00
|
|
|
convert_to_db = nil, -- true when migration to DB has been done
|
2017-08-29 15:34:49 +00:00
|
|
|
total_read_pages = 0,
|
|
|
|
total_read_time = 0,
|
|
|
|
avg_time = nil,
|
|
|
|
pages_stats = {},
|
2015-09-07 17:06:17 +00:00
|
|
|
data = {
|
|
|
|
title = "",
|
|
|
|
authors = "",
|
|
|
|
language = "",
|
|
|
|
series = "",
|
2015-11-23 19:37:36 +00:00
|
|
|
performance_in_pages = {},
|
|
|
|
total_time_in_sec = 0,
|
2015-09-07 17:06:17 +00:00
|
|
|
highlights = 0,
|
|
|
|
notes = 0,
|
|
|
|
pages = 0,
|
2017-08-29 15:34:49 +00:00
|
|
|
md5 = nil,
|
2015-09-07 17:06:17 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2020-02-16 00:03:12 +00:00
|
|
|
local weekDays = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" } -- in Lua wday order
|
|
|
|
|
2017-02-19 21:12:45 +00:00
|
|
|
local shortDayOfWeekTranslation = {
|
|
|
|
["Mon"] = _("Mon"),
|
|
|
|
["Tue"] = _("Tue"),
|
|
|
|
["Wed"] = _("Wed"),
|
|
|
|
["Thu"] = _("Thu"),
|
|
|
|
["Fri"] = _("Fri"),
|
|
|
|
["Sat"] = _("Sat"),
|
|
|
|
["Sun"] = _("Sun"),
|
|
|
|
}
|
|
|
|
|
2020-02-16 00:03:12 +00:00
|
|
|
local longDayOfWeekTranslation = {
|
|
|
|
["Mon"] = _("Monday"),
|
|
|
|
["Tue"] = _("Tuesday"),
|
|
|
|
["Wed"] = _("Wednesday"),
|
|
|
|
["Thu"] = _("Thursday"),
|
|
|
|
["Fri"] = _("Friday"),
|
|
|
|
["Sat"] = _("Saturday"),
|
|
|
|
["Sun"] = _("Sunday"),
|
|
|
|
}
|
|
|
|
|
2017-02-19 21:12:45 +00:00
|
|
|
local monthTranslation = {
|
|
|
|
["January"] = _("January"),
|
|
|
|
["February"] = _("February"),
|
|
|
|
["March"] = _("March"),
|
|
|
|
["April"] = _("April"),
|
|
|
|
["May"] = _("May"),
|
|
|
|
["June"] = _("June"),
|
|
|
|
["July"] = _("July"),
|
|
|
|
["August"] = _("August"),
|
|
|
|
["September"] = _("September"),
|
|
|
|
["October"] = _("October"),
|
|
|
|
["November"] = _("November"),
|
|
|
|
["December"] = _("December"),
|
|
|
|
}
|
|
|
|
|
2017-01-06 09:33:57 +00:00
|
|
|
function ReaderStatistics:isDocless()
|
|
|
|
return self.ui == nil or self.ui.document == nil
|
|
|
|
end
|
|
|
|
|
2015-09-13 19:34:20 +00:00
|
|
|
function ReaderStatistics:init()
|
2017-01-06 09:33:57 +00:00
|
|
|
if not self:isDocless() and self.ui.document.is_pic then
|
2015-09-07 17:06:17 +00:00
|
|
|
return
|
|
|
|
end
|
2017-08-29 15:34:49 +00:00
|
|
|
self.start_current_period = TimeVal:now().sec
|
|
|
|
self.pages_stats = {}
|
2015-09-13 19:34:20 +00:00
|
|
|
local settings = G_reader_settings:readSetting("statistics") or {}
|
2015-09-07 17:06:17 +00:00
|
|
|
self.page_min_read_sec = tonumber(settings.min_sec)
|
|
|
|
self.page_max_read_sec = tonumber(settings.max_sec)
|
2020-02-16 00:03:12 +00:00
|
|
|
self.calendar_start_day_of_week = settings.calendar_start_day_of_week
|
|
|
|
self.calendar_nb_book_spans = settings.calendar_nb_book_spans
|
|
|
|
self.calendar_show_histogram = settings.calendar_show_histogram
|
|
|
|
self.calendar_browse_future_months = settings.calendar_browse_future_months
|
2015-09-07 17:06:17 +00:00
|
|
|
self.is_enabled = not (settings.is_enabled == false)
|
2017-08-29 15:34:49 +00:00
|
|
|
self.convert_to_db = settings.convert_to_db
|
|
|
|
self.ui.menu:registerToMainMenu(self)
|
|
|
|
self:checkInitDatabase()
|
2017-09-14 20:29:09 +00:00
|
|
|
BookStatusWidget.getStats = function()
|
|
|
|
return self:getStatsBookStatus(self.id_curr_book, self.is_enabled)
|
|
|
|
end
|
2017-09-16 11:36:54 +00:00
|
|
|
ReaderFooter.getAvgTimePerPage = function()
|
|
|
|
if self.is_enabled then
|
|
|
|
return self.avg_time
|
|
|
|
end
|
|
|
|
end
|
2017-12-17 17:27:24 +00:00
|
|
|
Screensaver.getReaderProgress = function()
|
|
|
|
local readingprogress
|
|
|
|
self:insertDB(self.id_curr_book)
|
|
|
|
local current_period, current_pages = self:getCurrentBookStats()
|
|
|
|
local today_period, today_pages = self:getTodayBookStats()
|
|
|
|
local dates_stats = self:getReadingProgressStats(7)
|
|
|
|
if dates_stats then
|
|
|
|
readingprogress = ReaderProgress:new{
|
|
|
|
dates = dates_stats,
|
|
|
|
current_period = current_period,
|
|
|
|
current_pages = current_pages,
|
|
|
|
today_period = today_period,
|
|
|
|
today_pages = today_pages,
|
|
|
|
readonly = true,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
return readingprogress
|
|
|
|
end
|
2016-02-14 21:47:36 +00:00
|
|
|
end
|
|
|
|
|
2017-08-29 15:34:49 +00:00
|
|
|
function ReaderStatistics:initData()
|
2017-01-06 09:33:57 +00:00
|
|
|
if self:isDocless() or not self.is_enabled then
|
|
|
|
return
|
|
|
|
end
|
2016-02-14 21:47:36 +00:00
|
|
|
-- first execution
|
2017-01-06 09:33:57 +00:00
|
|
|
if not self.data then
|
|
|
|
self.data = { performance_in_pages= {} }
|
|
|
|
end
|
|
|
|
local book_properties = self:getBookProperties()
|
|
|
|
self.data.title = book_properties.title
|
2017-08-29 15:34:49 +00:00
|
|
|
if self.data.title == nil or self.data.title == "" then
|
|
|
|
self.data.title = self.document.file:match("^.+/(.+)$")
|
|
|
|
end
|
2017-01-06 09:33:57 +00:00
|
|
|
self.data.authors = book_properties.authors
|
|
|
|
self.data.language = book_properties.language
|
|
|
|
self.data.series = book_properties.series
|
2016-02-14 21:47:36 +00:00
|
|
|
|
2017-01-06 09:33:57 +00:00
|
|
|
self.data.pages = self.view.document:getPageCount()
|
2019-10-26 07:08:57 +00:00
|
|
|
if not self.data.md5 then
|
|
|
|
self.data.md5 = self:partialMd5(self.document.file)
|
|
|
|
end
|
2020-05-20 19:40:49 +00:00
|
|
|
-- Update these numbers to what's actually stored in the settings
|
|
|
|
-- (not that "notes" is invalid and does not represent edited highlights)
|
|
|
|
self.data.highlights, self.data.notes = self.ui.bookmark:getNumberOfHighlightsAndNotes()
|
2017-08-29 15:34:49 +00:00
|
|
|
self.curr_total_time = 0
|
|
|
|
self.curr_total_pages = 0
|
|
|
|
self.id_curr_book = self:getIdBookDB()
|
|
|
|
self.total_read_pages, self.total_read_time = self:getPageTimeTotalStats(self.id_curr_book)
|
|
|
|
if self.total_read_pages > 0 then
|
2019-09-10 19:32:02 +00:00
|
|
|
self.avg_time = self.total_read_time / self.total_read_pages
|
2020-03-20 11:01:38 +00:00
|
|
|
else
|
|
|
|
self.avg_time = 0
|
2017-08-29 15:34:49 +00:00
|
|
|
end
|
2015-09-07 17:06:17 +00:00
|
|
|
end
|
|
|
|
|
2017-09-14 20:29:09 +00:00
|
|
|
function ReaderStatistics:getStatsBookStatus(id_curr_book, stat_enable)
|
|
|
|
if not stat_enable or id_curr_book == nil then
|
|
|
|
return {}
|
|
|
|
end
|
|
|
|
|
|
|
|
self:insertDB(self.id_curr_book)
|
|
|
|
local conn = SQ3.open(db_location)
|
|
|
|
local sql_stmt = [[
|
|
|
|
SELECT count(*)
|
|
|
|
FROM (
|
|
|
|
SELECT strftime('%%Y-%%m-%%d', start_time, 'unixepoch', 'localtime') AS dates
|
|
|
|
FROM page_stat
|
|
|
|
WHERE id_book = '%s'
|
|
|
|
GROUP BY dates
|
|
|
|
)
|
|
|
|
]]
|
|
|
|
local total_days = conn:rowexec(string.format(sql_stmt, id_curr_book))
|
|
|
|
sql_stmt = [[
|
|
|
|
SELECT sum(period),
|
|
|
|
count(DISTINCT page)
|
|
|
|
FROM page_stat
|
|
|
|
WHERE id_book = '%s'
|
|
|
|
]]
|
|
|
|
local total_time_book, total_read_pages = conn:rowexec(string.format(sql_stmt, id_curr_book))
|
|
|
|
conn:close()
|
|
|
|
|
|
|
|
if total_time_book == nil then
|
|
|
|
total_time_book = 0
|
|
|
|
end
|
|
|
|
if total_read_pages == nil then
|
|
|
|
total_read_pages = 0
|
|
|
|
end
|
|
|
|
return {
|
|
|
|
days = tonumber(total_days),
|
|
|
|
time = tonumber(total_time_book),
|
|
|
|
pages = tonumber(total_read_pages),
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2017-08-29 15:34:49 +00:00
|
|
|
function ReaderStatistics:checkInitDatabase()
|
|
|
|
local conn = SQ3.open(db_location)
|
2020-02-16 00:03:12 +00:00
|
|
|
if self.convert_to_db then -- if conversion to sqlite DB has already been done
|
2017-08-29 15:34:49 +00:00
|
|
|
if not conn:exec("pragma table_info('book');") then
|
|
|
|
UIManager:show(ConfirmBox:new{
|
2019-08-22 15:11:47 +00:00
|
|
|
text = T(_([[
|
|
|
|
Cannot open database in %1.
|
2017-08-31 04:53:13 +00:00
|
|
|
The database may have been moved or deleted.
|
|
|
|
Do you want to create an empty database?
|
|
|
|
]]),
|
2020-01-04 00:18:51 +00:00
|
|
|
BD.filepath(db_location)),
|
2017-08-29 15:34:49 +00:00
|
|
|
cancel_text = _("Close"),
|
|
|
|
cancel_callback = function()
|
|
|
|
return
|
|
|
|
end,
|
|
|
|
ok_text = _("Create"),
|
|
|
|
ok_callback = function()
|
|
|
|
local conn_new = SQ3.open(db_location)
|
|
|
|
self:createDB(conn_new)
|
|
|
|
conn_new:close()
|
|
|
|
UIManager:show(InfoMessage:new{text =_("A new empty database has been created."), timeout = 3 })
|
|
|
|
self:initData()
|
|
|
|
end,
|
|
|
|
})
|
|
|
|
end
|
2020-02-16 00:03:12 +00:00
|
|
|
else -- Migrate stats for books in history from metadata.lua to sqlite database
|
2017-08-29 15:34:49 +00:00
|
|
|
self.convert_to_db = true
|
|
|
|
if not conn:exec("pragma table_info('book');") then
|
2019-07-17 13:15:21 +00:00
|
|
|
local filename_first_history, quickstart_filename, __
|
|
|
|
if #ReadHistory.hist == 1 then
|
|
|
|
filename_first_history = ReadHistory.hist[1]["text"]
|
|
|
|
local quickstart_path = require("ui/quickstart").quickstart_filename
|
|
|
|
__, quickstart_filename = util.splitFilePathName(quickstart_path)
|
|
|
|
end
|
|
|
|
if #ReadHistory.hist > 1 or (#ReadHistory.hist == 1 and filename_first_history ~= quickstart_filename) then
|
2017-08-29 15:34:49 +00:00
|
|
|
local info = InfoMessage:new{
|
2019-08-22 15:11:47 +00:00
|
|
|
text =_([[
|
|
|
|
New version of statistics plugin detected.
|
2017-08-31 04:53:13 +00:00
|
|
|
Statistics data needs to be converted into the new database format.
|
2017-09-23 20:03:28 +00:00
|
|
|
This may take a few minutes.
|
2017-08-31 04:53:13 +00:00
|
|
|
Please wait…
|
|
|
|
]])}
|
2017-08-29 15:34:49 +00:00
|
|
|
UIManager:show(info)
|
|
|
|
UIManager:forceRePaint()
|
|
|
|
local nr_book = self:migrateToDB(conn)
|
|
|
|
UIManager:close(info)
|
|
|
|
UIManager:forceRePaint()
|
|
|
|
UIManager:show(InfoMessage:new{
|
2017-09-10 11:12:19 +00:00
|
|
|
text =T(_("Conversion completed.\nImported %1 books to database.\nTap to continue."),nr_book) })
|
2017-08-29 15:34:49 +00:00
|
|
|
else
|
|
|
|
self:createDB(conn)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
self:saveSettings()
|
|
|
|
end
|
|
|
|
conn:close()
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderStatistics:partialMd5(file)
|
|
|
|
if file == nil then
|
|
|
|
return nil
|
|
|
|
end
|
2019-03-27 21:50:44 +00:00
|
|
|
local bit = require("bit")
|
2017-08-29 15:34:49 +00:00
|
|
|
local md5 = require("ffi/MD5")
|
|
|
|
local lshift = bit.lshift
|
|
|
|
local step, size = 1024, 1024
|
|
|
|
local m = md5.new()
|
|
|
|
local file_handle = io.open(file, 'rb')
|
|
|
|
for i = -1, 10 do
|
|
|
|
file_handle:seek("set", lshift(step, 2*i))
|
|
|
|
local sample = file_handle:read(size)
|
|
|
|
if sample then
|
|
|
|
m:update(sample)
|
|
|
|
else
|
|
|
|
break
|
|
|
|
end
|
2016-11-04 21:35:20 +00:00
|
|
|
end
|
2017-08-29 15:34:49 +00:00
|
|
|
return m:sum()
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderStatistics:createDB(conn)
|
2019-08-01 17:10:46 +00:00
|
|
|
-- Make it WAL, if possible
|
|
|
|
if Device:canUseWAL() then
|
|
|
|
conn:exec("PRAGMA journal_mode=WAL;")
|
|
|
|
else
|
|
|
|
conn:exec("PRAGMA journal_mode=TRUNCATE;")
|
|
|
|
end
|
2017-08-29 15:34:49 +00:00
|
|
|
local sql_stmt = [[
|
|
|
|
CREATE TABLE IF NOT EXISTS book
|
|
|
|
(
|
|
|
|
id integer PRIMARY KEY autoincrement,
|
|
|
|
title text,
|
|
|
|
authors text,
|
|
|
|
notes integer,
|
|
|
|
last_open integer,
|
|
|
|
highlights integer,
|
|
|
|
pages integer,
|
|
|
|
series text,
|
|
|
|
language text,
|
|
|
|
md5 text,
|
|
|
|
total_read_time integer,
|
|
|
|
total_read_pages integer
|
|
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS page_stat
|
|
|
|
(
|
2017-09-01 20:05:57 +00:00
|
|
|
id_book integer,
|
2017-08-29 15:34:49 +00:00
|
|
|
page integer NOT NULL,
|
|
|
|
start_time integer NOT NULL,
|
|
|
|
period integer NOT NULL,
|
|
|
|
UNIQUE (page, start_time),
|
|
|
|
FOREIGN KEY(id_book) REFERENCES book(id)
|
|
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS info
|
|
|
|
(
|
|
|
|
version integer
|
|
|
|
);
|
2017-09-01 20:05:57 +00:00
|
|
|
CREATE INDEX IF NOT EXISTS page_stat_id_book ON page_stat(id_book);
|
|
|
|
CREATE INDEX IF NOT EXISTS book_title_authors_md5 ON book(title, authors, md5);
|
2017-08-29 15:34:49 +00:00
|
|
|
]]
|
|
|
|
conn:exec(sql_stmt)
|
|
|
|
--DB structure version - now is version 1
|
|
|
|
local stmt = conn:prepare("INSERT INTO info values (?)")
|
|
|
|
stmt:reset():bind("1"):step()
|
|
|
|
stmt:close()
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderStatistics:addBookStatToDB(book_stats, conn)
|
|
|
|
local id_book
|
|
|
|
local last_open_book = 0
|
|
|
|
local start_open_page
|
|
|
|
local diff_time
|
|
|
|
local total_read_pages = 0
|
|
|
|
local total_read_time = 0
|
|
|
|
local sql_stmt
|
|
|
|
if book_stats.total_time_in_sec and book_stats.total_time_in_sec > 0
|
|
|
|
and util.tableSize(book_stats.performance_in_pages) > 0 then
|
|
|
|
local read_pages = util.tableSize(book_stats.performance_in_pages)
|
|
|
|
logger.dbg("Insert to database: " .. book_stats.title)
|
|
|
|
sql_stmt = [[
|
|
|
|
SELECT count(id)
|
|
|
|
FROM book
|
|
|
|
WHERE title = ?
|
|
|
|
AND authors = ?
|
|
|
|
AND md5 = ?
|
|
|
|
]]
|
|
|
|
local stmt = conn:prepare(sql_stmt)
|
|
|
|
local result = stmt:reset():bind(self.data.title, self.data.authors, self.data.md5):step()
|
|
|
|
local nr_id = tonumber(result[1])
|
|
|
|
if nr_id == 0 then
|
|
|
|
stmt = conn:prepare("INSERT INTO book VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
|
|
|
stmt:reset():bind(book_stats.title, book_stats.authors, book_stats.notes,
|
|
|
|
last_open_book, book_stats.highlights, book_stats.pages,
|
|
|
|
book_stats.series, book_stats.language, self:partialMd5(book_stats.file), total_read_time, total_read_pages) :step()
|
|
|
|
sql_stmt = [[
|
|
|
|
SELECT last_insert_rowid() AS num;
|
|
|
|
]]
|
|
|
|
id_book = conn:rowexec(sql_stmt)
|
|
|
|
else
|
|
|
|
sql_stmt = [[
|
|
|
|
SELECT id
|
|
|
|
FROM book
|
|
|
|
WHERE title = ?
|
|
|
|
AND authors = ?
|
|
|
|
AND md5 = ?
|
|
|
|
]]
|
|
|
|
stmt = conn:prepare(sql_stmt)
|
|
|
|
result = stmt:reset():bind(self.data.title, self.data.authors, self.data.md5):step()
|
|
|
|
id_book = result[1]
|
|
|
|
|
|
|
|
end
|
|
|
|
local sorted_performance = {}
|
|
|
|
for k, _ in pairs(book_stats.performance_in_pages) do
|
|
|
|
table.insert(sorted_performance, k)
|
|
|
|
end
|
|
|
|
table.sort(sorted_performance)
|
|
|
|
|
|
|
|
conn:exec('BEGIN')
|
|
|
|
stmt = conn:prepare("INSERT OR IGNORE INTO page_stat VALUES(?, ?, ?, ?)")
|
|
|
|
local avg_time = math.ceil(book_stats.total_time_in_sec / read_pages)
|
2017-09-09 18:18:16 +00:00
|
|
|
if avg_time > self.page_max_read_sec then
|
|
|
|
avg_time = self.page_max_read_sec
|
|
|
|
end
|
2017-08-29 15:34:49 +00:00
|
|
|
local first_read_page = book_stats.performance_in_pages[sorted_performance[1]]
|
|
|
|
if first_read_page > 1 then
|
|
|
|
first_read_page = first_read_page - 1
|
|
|
|
end
|
|
|
|
start_open_page = sorted_performance[1]
|
|
|
|
--first page
|
|
|
|
stmt:reset():bind(id_book, first_read_page, start_open_page - avg_time, avg_time):step()
|
|
|
|
for i=2, #sorted_performance do
|
|
|
|
start_open_page = sorted_performance[i-1]
|
|
|
|
diff_time = sorted_performance[i] - sorted_performance[i-1]
|
|
|
|
if diff_time <= self.page_max_read_sec then
|
|
|
|
stmt:reset():bind(id_book, book_stats.performance_in_pages[sorted_performance[i-1]],
|
|
|
|
start_open_page, diff_time):step()
|
|
|
|
elseif diff_time > self.page_max_read_sec then --and diff_time <= 2 * avg_time then
|
|
|
|
stmt:reset():bind(id_book, book_stats.performance_in_pages[sorted_performance[i-1]],
|
|
|
|
start_open_page, avg_time):step()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
--last page
|
|
|
|
stmt:reset():bind(id_book, book_stats.performance_in_pages[sorted_performance[#sorted_performance]],
|
|
|
|
sorted_performance[#sorted_performance], avg_time):step()
|
|
|
|
--last open book
|
|
|
|
last_open_book = sorted_performance[#sorted_performance] + avg_time
|
|
|
|
conn:exec('COMMIT')
|
|
|
|
sql_stmt = [[
|
|
|
|
SELECT count(DISTINCT page),
|
|
|
|
sum(period)
|
|
|
|
FROM page_stat
|
|
|
|
WHERE id_book = %s;
|
|
|
|
]]
|
|
|
|
total_read_pages, total_read_time = conn:rowexec(string.format(sql_stmt, tonumber(id_book)))
|
|
|
|
sql_stmt = [[
|
|
|
|
UPDATE book
|
|
|
|
SET last_open = ?,
|
|
|
|
total_read_time = ?,
|
|
|
|
total_read_pages = ?
|
|
|
|
WHERE id = ?
|
|
|
|
]]
|
|
|
|
stmt = conn:prepare(sql_stmt)
|
|
|
|
stmt:reset():bind(last_open_book, total_read_time, total_read_pages, id_book):step()
|
|
|
|
stmt:close()
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderStatistics:migrateToDB(conn)
|
|
|
|
self:createDB(conn)
|
|
|
|
local nr_of_conv_books = 0
|
|
|
|
local exclude_titles = {}
|
|
|
|
for _, v in pairs(ReadHistory.hist) do
|
|
|
|
local book_stats = DocSettings:open(v.file):readSetting('stats')
|
|
|
|
if book_stats and book_stats.title == "" then
|
|
|
|
book_stats.title = v.file:match("^.+/(.+)$")
|
|
|
|
end
|
|
|
|
if book_stats then
|
|
|
|
book_stats.file = v.file
|
|
|
|
if self:addBookStatToDB(book_stats, conn) then
|
|
|
|
nr_of_conv_books = nr_of_conv_books + 1
|
|
|
|
exclude_titles[book_stats.title] = true
|
|
|
|
else
|
|
|
|
logger.dbg("Book not converted: " .. book_stats.title)
|
|
|
|
end
|
|
|
|
else
|
|
|
|
logger.dbg("Empty stats for file: ", v.file)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
-- import from stats files (for backward compatibility)
|
|
|
|
if lfs.attributes(statistics_dir, "mode") == "directory" then
|
|
|
|
for curr_file in lfs.dir(statistics_dir) do
|
|
|
|
local path = statistics_dir .. curr_file
|
|
|
|
if lfs.attributes(path, "mode") == "file" then
|
|
|
|
local old_data = self:importFromFile(statistics_dir, curr_file)
|
|
|
|
if old_data and old_data.total_time > 0 and not exclude_titles[old_data.title] then
|
2017-09-09 18:18:16 +00:00
|
|
|
local book_stats = { performance_in_pages= {} }
|
2017-08-29 15:34:49 +00:00
|
|
|
for _, v in pairs(old_data.details) do
|
|
|
|
book_stats.performance_in_pages[v.time] = v.page
|
|
|
|
end
|
|
|
|
book_stats.title = old_data.title
|
|
|
|
book_stats.authors = old_data.authors
|
|
|
|
book_stats.notes = old_data.notes
|
|
|
|
book_stats.highlights = old_data.highlights
|
|
|
|
book_stats.pages = old_data.pages
|
|
|
|
book_stats.series = old_data.series
|
|
|
|
book_stats.language = old_data.language
|
2017-09-09 18:18:16 +00:00
|
|
|
book_stats.total_time_in_sec = old_data.total_time
|
2017-08-29 15:34:49 +00:00
|
|
|
book_stats.file = nil
|
|
|
|
if self:addBookStatToDB(book_stats, conn) then
|
|
|
|
nr_of_conv_books = nr_of_conv_books + 1
|
|
|
|
else
|
|
|
|
logger.dbg("Book not converted (old stats): " .. book_stats.title)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return nr_of_conv_books
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderStatistics:getIdBookDB()
|
|
|
|
local conn = SQ3.open(db_location)
|
|
|
|
local id_book
|
|
|
|
local sql_stmt = [[
|
|
|
|
SELECT count(id)
|
|
|
|
FROM book
|
|
|
|
WHERE title = ?
|
|
|
|
AND authors = ?
|
|
|
|
AND md5 = ?
|
|
|
|
]]
|
|
|
|
local stmt = conn:prepare(sql_stmt)
|
|
|
|
local result = stmt:reset():bind(self.data.title, self.data.authors, self.data.md5):step()
|
|
|
|
local nr_id = tonumber(result[1])
|
|
|
|
if nr_id == 0 then
|
|
|
|
stmt = conn:prepare("INSERT INTO book VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
|
|
|
stmt:reset():bind(self.data.title, self.data.authors, self.data.notes,
|
|
|
|
TimeVal:now().sec, self.data.highlights, self.data.pages,
|
|
|
|
self.data.series, self.data.language, self.data.md5, self.curr_total_time, self.curr_total_pages):step()
|
|
|
|
sql_stmt = [[
|
|
|
|
SELECT last_insert_rowid() AS num;
|
|
|
|
]]
|
|
|
|
id_book = conn:rowexec(sql_stmt)
|
|
|
|
else
|
|
|
|
sql_stmt = [[
|
|
|
|
SELECT id
|
|
|
|
FROM book
|
|
|
|
WHERE title = ?
|
|
|
|
AND authors = ?
|
|
|
|
AND md5 = ?
|
|
|
|
]]
|
|
|
|
stmt = conn:prepare(sql_stmt)
|
|
|
|
result = stmt:reset():bind(self.data.title, self.data.authors, self.data.md5):step()
|
|
|
|
id_book = result[1]
|
|
|
|
end
|
|
|
|
conn:close()
|
|
|
|
return tonumber(id_book)
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderStatistics:insertDB(id_book)
|
|
|
|
self.pages_stats[TimeVal:now().sec] = self.curr_page
|
|
|
|
if id_book == nil or util.tableSize(self.pages_stats) < 2 then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
local diff_time
|
|
|
|
local conn = SQ3.open(db_location)
|
|
|
|
local sorted_performance = {}
|
|
|
|
for time, pages in pairs(self.pages_stats) do
|
|
|
|
table.insert(sorted_performance, time)
|
|
|
|
end
|
|
|
|
table.sort(sorted_performance)
|
|
|
|
conn:exec('BEGIN')
|
|
|
|
local stmt = conn:prepare("INSERT OR IGNORE INTO page_stat VALUES(?, ?, ?, ?)")
|
|
|
|
for i=1, #sorted_performance - 1 do
|
|
|
|
diff_time = sorted_performance[i+1] - sorted_performance[i]
|
|
|
|
if diff_time >= self.page_min_read_sec then
|
|
|
|
stmt:reset():bind(id_book,
|
|
|
|
self.pages_stats[sorted_performance[i]],
|
|
|
|
sorted_performance[i],
|
|
|
|
math.min(diff_time, self.page_max_read_sec)):step()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
conn:exec('COMMIT')
|
|
|
|
local sql_stmt = [[
|
|
|
|
SELECT count(DISTINCT page),
|
|
|
|
sum(period)
|
|
|
|
FROM page_stat
|
|
|
|
WHERE id_book = '%s'
|
|
|
|
]]
|
|
|
|
local total_read_pages, total_read_time = conn:rowexec(string.format(sql_stmt, id_book))
|
|
|
|
sql_stmt = [[
|
|
|
|
UPDATE book
|
|
|
|
SET last_open = ?,
|
|
|
|
notes = ?,
|
|
|
|
highlights = ?,
|
|
|
|
total_read_time = ?,
|
|
|
|
total_read_pages = ?,
|
|
|
|
pages = ?
|
|
|
|
WHERE id = ?
|
|
|
|
]]
|
|
|
|
stmt = conn:prepare(sql_stmt)
|
|
|
|
stmt:reset():bind(TimeVal:now().sec, self.data.notes, self.data.highlights, total_read_time, total_read_pages,
|
|
|
|
self.data.pages, id_book):step()
|
|
|
|
if total_read_pages then
|
|
|
|
self.total_read_pages = tonumber(total_read_pages)
|
|
|
|
else
|
|
|
|
self.total_read_pages = 0
|
|
|
|
end
|
|
|
|
if total_read_time then
|
|
|
|
self.total_read_time = tonumber(total_read_time)
|
|
|
|
else
|
|
|
|
self.total_read_time = 0
|
|
|
|
end
|
|
|
|
self.pages_stats = {}
|
|
|
|
-- last page must be added once more
|
|
|
|
self.pages_stats[TimeVal:now().sec] = self.curr_page
|
|
|
|
conn:close()
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderStatistics:getPageTimeTotalStats(id_book)
|
|
|
|
if id_book == nil then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
local conn = SQ3.open(db_location)
|
|
|
|
local sql_stmt = [[
|
|
|
|
SELECT total_read_pages,
|
|
|
|
total_read_time
|
|
|
|
FROM book
|
|
|
|
WHERE id = '%s'
|
|
|
|
]]
|
|
|
|
local total_pages, total_time = conn:rowexec(string.format(sql_stmt, id_book))
|
|
|
|
if total_pages then
|
|
|
|
total_pages = tonumber(total_pages)
|
|
|
|
else
|
|
|
|
total_pages = 0
|
|
|
|
end
|
|
|
|
if total_time then
|
|
|
|
total_time = tonumber(total_time)
|
|
|
|
else
|
|
|
|
total_time = 0
|
|
|
|
end
|
|
|
|
conn:close()
|
|
|
|
return total_pages, total_time
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderStatistics:getBookProperties()
|
|
|
|
local props = self.view.document:getProps()
|
|
|
|
if props.title == "No document" or props.title == "" then
|
2019-08-23 17:53:53 +00:00
|
|
|
--- @fixme Sometimes crengine returns "No document", try one more time.
|
2017-08-29 15:34:49 +00:00
|
|
|
props = self.view.document:getProps()
|
|
|
|
end
|
|
|
|
return props
|
2016-11-04 21:35:20 +00:00
|
|
|
end
|
|
|
|
|
2016-02-14 21:47:36 +00:00
|
|
|
function ReaderStatistics:getStatisticEnabledMenuItem()
|
2015-09-07 17:06:17 +00:00
|
|
|
return {
|
2016-02-14 21:47:36 +00:00
|
|
|
text = _("Enabled"),
|
2015-09-07 17:06:17 +00:00
|
|
|
checked_func = function() return self.is_enabled end,
|
|
|
|
callback = function()
|
|
|
|
-- if was enabled, have to save data to file
|
2017-08-29 15:34:49 +00:00
|
|
|
if self.is_enabled and not self:isDocless() then
|
|
|
|
self:insertDB(self.id_curr_book)
|
2015-11-23 19:37:36 +00:00
|
|
|
self.ui.doc_settings:saveSetting("stats", self.data)
|
2015-09-07 17:06:17 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
self.is_enabled = not self.is_enabled
|
2017-08-29 15:34:49 +00:00
|
|
|
-- if was disabled have to get data from db
|
2017-01-06 09:33:57 +00:00
|
|
|
if self.is_enabled and not self:isDocless() then
|
2017-08-29 15:34:49 +00:00
|
|
|
self:initData()
|
|
|
|
self.pages_stats = {}
|
|
|
|
self.start_current_period = TimeVal:now().sec
|
2020-03-16 15:52:09 +00:00
|
|
|
self.curr_page = self.ui:getCurrentPage()
|
2017-08-29 15:34:49 +00:00
|
|
|
self.pages_stats[self.start_current_period] = self.curr_page
|
2015-09-07 17:06:17 +00:00
|
|
|
end
|
2015-10-26 14:06:52 +00:00
|
|
|
self:saveSettings()
|
2017-09-16 11:36:54 +00:00
|
|
|
if not self:isDocless() then
|
2020-07-12 18:47:49 +00:00
|
|
|
self.view.footer:onUpdateFooter()
|
2017-09-16 11:36:54 +00:00
|
|
|
end
|
2015-09-07 17:06:17 +00:00
|
|
|
end,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2017-03-04 13:46:38 +00:00
|
|
|
function ReaderStatistics:addToMainMenu(menu_items)
|
|
|
|
menu_items.statistics = {
|
2017-04-09 20:16:56 +00:00
|
|
|
text = _("Reading statistics"),
|
2016-02-14 21:47:36 +00:00
|
|
|
sub_item_table = {
|
|
|
|
self:getStatisticEnabledMenuItem(),
|
|
|
|
{
|
|
|
|
text = _("Settings"),
|
2020-02-16 00:03:12 +00:00
|
|
|
sub_item_table = {
|
|
|
|
{
|
|
|
|
text_func = function()
|
|
|
|
return T(_("Read page duration limits: %1 s / %2 s"),
|
|
|
|
self.page_min_read_sec, self.page_max_read_sec)
|
|
|
|
end,
|
|
|
|
callback = function(touchmenu_instance)
|
|
|
|
local DoubleSpinWidget = require("/ui/widget/doublespinwidget")
|
|
|
|
local durations_widget
|
|
|
|
durations_widget = DoubleSpinWidget:new{
|
|
|
|
left_text = _("Min"),
|
|
|
|
left_value = self.page_min_read_sec,
|
|
|
|
left_default = DEFAULT_MIN_READ_SEC,
|
|
|
|
left_min = 3,
|
|
|
|
left_max = 120,
|
|
|
|
left_step = 1,
|
|
|
|
left_hold_step = 10,
|
|
|
|
right_text = _("Max"),
|
|
|
|
right_value = self.page_max_read_sec,
|
|
|
|
right_default = DEFAULT_MAX_READ_SEC,
|
|
|
|
right_min = 10,
|
|
|
|
right_max = 7200,
|
|
|
|
right_step = 10,
|
|
|
|
right_hold_step = 60,
|
|
|
|
default_values = true,
|
|
|
|
default_text = _("Use defaults"),
|
|
|
|
title_text = _("Read page duration limits"),
|
|
|
|
info_text = _([[
|
|
|
|
Set min and max time spent (in seconds) on a page for it to be counted as read in statistics.
|
|
|
|
The min value ensures pages you quickly browse and skip are not included.
|
|
|
|
The max value ensures a page you stay on for a long time (because you fell asleep or went away) will be included, but with a duration capped to this specified max value.]]),
|
|
|
|
callback = function(min, max)
|
|
|
|
if not min then min = DEFAULT_MIN_READ_SEC end
|
|
|
|
if not max then max = DEFAULT_MAX_READ_SEC end
|
|
|
|
if min > max then
|
|
|
|
min, max = max, min
|
|
|
|
end
|
|
|
|
self.page_min_read_sec = min
|
|
|
|
self.page_max_read_sec = max
|
|
|
|
self:saveSettings()
|
|
|
|
UIManager:close(durations_widget)
|
|
|
|
touchmenu_instance:updateItems()
|
|
|
|
end,
|
|
|
|
}
|
|
|
|
UIManager:show(durations_widget)
|
|
|
|
end,
|
|
|
|
keep_menu_open = true,
|
|
|
|
separator = true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text_func = function()
|
|
|
|
return T(_("Calendar weeks start on %1"),
|
|
|
|
longDayOfWeekTranslation[weekDays[self.calendar_start_day_of_week]])
|
|
|
|
end,
|
|
|
|
sub_item_table = {
|
|
|
|
{ -- Friday (Bangladesh and Maldives)
|
|
|
|
text = longDayOfWeekTranslation[weekDays[6]],
|
|
|
|
checked_func = function() return self.calendar_start_day_of_week == 6 end,
|
|
|
|
callback = function() self.calendar_start_day_of_week = 6 end
|
|
|
|
},
|
|
|
|
{ -- Saturday (some Middle East countries)
|
|
|
|
text = longDayOfWeekTranslation[weekDays[7]],
|
|
|
|
checked_func = function() return self.calendar_start_day_of_week == 7 end,
|
|
|
|
callback = function() self.calendar_start_day_of_week = 7 end
|
|
|
|
},
|
|
|
|
{ -- Sunday
|
|
|
|
text = longDayOfWeekTranslation[weekDays[1]],
|
|
|
|
checked_func = function() return self.calendar_start_day_of_week == 1 end,
|
|
|
|
callback = function() self.calendar_start_day_of_week = 1 end
|
|
|
|
},
|
|
|
|
{ -- Monday
|
|
|
|
text = longDayOfWeekTranslation[weekDays[2]],
|
|
|
|
checked_func = function() return self.calendar_start_day_of_week == 2 end,
|
|
|
|
callback = function() self.calendar_start_day_of_week = 2 end
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text_func = function()
|
|
|
|
return T(_("Books per calendar day: %1"), self.calendar_nb_book_spans)
|
|
|
|
end,
|
|
|
|
callback = function(touchmenu_instance)
|
|
|
|
local SpinWidget = require("ui/widget/spinwidget")
|
|
|
|
UIManager:show(SpinWidget:new{
|
2020-06-12 23:56:36 +00:00
|
|
|
width = math.floor(Screen:getWidth() * 0.6),
|
2020-02-16 00:03:12 +00:00
|
|
|
value = self.calendar_nb_book_spans,
|
|
|
|
value_min = 1,
|
|
|
|
value_max = 5,
|
|
|
|
ok_text = _("Set"),
|
|
|
|
title_text = _("Books per calendar day"),
|
2020-05-09 11:16:09 +00:00
|
|
|
info_text = _("Set the max number of book spans to show for a day"),
|
2020-02-16 00:03:12 +00:00
|
|
|
callback = function(spin)
|
|
|
|
self.calendar_nb_book_spans = spin.value
|
|
|
|
touchmenu_instance:updateItems()
|
|
|
|
end,
|
|
|
|
extra_text = _("Use default"),
|
|
|
|
extra_callback = function()
|
|
|
|
self.calendar_nb_book_spans = DEFAULT_CALENDAR_NB_BOOK_SPANS
|
|
|
|
touchmenu_instance:updateItems()
|
|
|
|
end
|
|
|
|
})
|
|
|
|
end,
|
|
|
|
keep_menu_open = true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text = _("Show hourly histogram in calendar days"),
|
|
|
|
checked_func = function() return self.calendar_show_histogram end,
|
|
|
|
callback = function()
|
|
|
|
self.calendar_show_histogram = not self.calendar_show_histogram
|
|
|
|
end,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text = _("Allow browsing coming months"),
|
|
|
|
checked_func = function() return self.calendar_browse_future_months end,
|
|
|
|
callback = function()
|
|
|
|
self.calendar_browse_future_months = not self.calendar_browse_future_months
|
|
|
|
end,
|
|
|
|
},
|
|
|
|
},
|
2016-02-14 21:47:36 +00:00
|
|
|
},
|
2017-08-29 15:34:49 +00:00
|
|
|
{
|
2020-02-16 00:03:12 +00:00
|
|
|
text = _("Reset statistics"),
|
|
|
|
sub_item_table = self:genResetBookSubItemTable(),
|
|
|
|
separator = true,
|
2017-08-29 15:34:49 +00:00
|
|
|
},
|
2016-02-14 21:47:36 +00:00
|
|
|
{
|
|
|
|
text = _("Current book"),
|
2018-09-04 21:55:58 +00:00
|
|
|
keep_menu_open = true,
|
2016-02-14 21:47:36 +00:00
|
|
|
callback = function()
|
|
|
|
UIManager:show(KeyValuePage:new{
|
2020-05-29 12:22:27 +00:00
|
|
|
title = _("Current statistics"),
|
2017-08-29 15:34:49 +00:00
|
|
|
kv_pairs = self:getCurrentStat(self.id_curr_book),
|
2016-02-14 21:47:36 +00:00
|
|
|
})
|
2017-01-06 09:33:57 +00:00
|
|
|
end,
|
2017-09-12 16:35:23 +00:00
|
|
|
enabled_func = function() return not self:isDocless() and self.is_enabled end,
|
2016-02-14 21:47:36 +00:00
|
|
|
},
|
2016-11-04 21:35:20 +00:00
|
|
|
{
|
|
|
|
text = _("Reading progress"),
|
2018-09-04 21:55:58 +00:00
|
|
|
keep_menu_open = true,
|
2016-11-04 21:35:20 +00:00
|
|
|
callback = function()
|
2017-08-29 15:34:49 +00:00
|
|
|
self:insertDB(self.id_curr_book)
|
|
|
|
local current_period, current_pages = self:getCurrentBookStats()
|
|
|
|
local today_period, today_pages = self:getTodayBookStats()
|
2017-09-01 20:05:57 +00:00
|
|
|
local dates_stats = self:getReadingProgressStats(7)
|
|
|
|
if dates_stats then
|
|
|
|
UIManager:show(ReaderProgress:new{
|
|
|
|
dates = dates_stats,
|
|
|
|
current_period = current_period,
|
|
|
|
current_pages = current_pages,
|
|
|
|
today_period = today_period,
|
|
|
|
today_pages = today_pages,
|
|
|
|
})
|
|
|
|
else
|
|
|
|
UIManager:show(InfoMessage:new{
|
|
|
|
text =T(_("Reading progress unavailable.\nNo data from last %1 days."),7)})
|
|
|
|
end
|
2016-11-04 21:35:20 +00:00
|
|
|
end
|
|
|
|
},
|
2016-10-24 17:15:56 +00:00
|
|
|
{
|
|
|
|
text = _("Time range"),
|
2018-09-04 21:55:58 +00:00
|
|
|
keep_menu_open = true,
|
2017-09-28 13:35:25 +00:00
|
|
|
callback = function()
|
|
|
|
self:statMenu()
|
|
|
|
end
|
2016-10-24 17:15:56 +00:00
|
|
|
},
|
2020-02-12 22:05:18 +00:00
|
|
|
{
|
|
|
|
text = _("Calendar view"),
|
|
|
|
keep_menu_open = true,
|
|
|
|
callback = function()
|
2020-04-10 17:14:04 +00:00
|
|
|
UIManager:show(self:getCalendarView())
|
2020-02-12 22:05:18 +00:00
|
|
|
end,
|
|
|
|
},
|
2016-02-14 21:47:36 +00:00
|
|
|
},
|
2017-02-28 21:46:32 +00:00
|
|
|
}
|
2015-09-07 17:06:17 +00:00
|
|
|
end
|
|
|
|
|
2017-09-28 13:35:25 +00:00
|
|
|
function ReaderStatistics:statMenu()
|
|
|
|
self.kv = KeyValuePage:new{
|
|
|
|
title = _("Time range statistics"),
|
|
|
|
return_button = true,
|
|
|
|
kv_pairs = {
|
|
|
|
{ _("All books"),"",
|
|
|
|
callback = function()
|
|
|
|
local kv = self.kv
|
|
|
|
UIManager:close(self.kv)
|
|
|
|
local total_msg, kv_pairs = self:getTotalStats()
|
|
|
|
self.kv = KeyValuePage:new{
|
|
|
|
title = total_msg,
|
2017-12-17 21:08:13 +00:00
|
|
|
value_align = "right",
|
2017-09-28 13:35:25 +00:00
|
|
|
kv_pairs = kv_pairs,
|
|
|
|
callback_return = function()
|
|
|
|
UIManager:show(kv)
|
|
|
|
self.kv = kv
|
|
|
|
end
|
|
|
|
}
|
|
|
|
UIManager:show(self.kv)
|
|
|
|
end,
|
|
|
|
},
|
2020-05-29 12:22:27 +00:00
|
|
|
{ _("Books by week"),"",
|
2017-09-28 13:35:25 +00:00
|
|
|
callback = function()
|
|
|
|
local kv = self.kv
|
|
|
|
UIManager:close(self.kv)
|
|
|
|
self.kv = KeyValuePage:new{
|
2020-05-29 12:22:27 +00:00
|
|
|
title = _("Books by week"),
|
2017-11-20 17:08:55 +00:00
|
|
|
value_overflow_align = "right",
|
2020-05-29 12:22:27 +00:00
|
|
|
kv_pairs = self:getDatesFromAll(0, "weekly", true),
|
2017-09-28 13:35:25 +00:00
|
|
|
callback_return = function()
|
|
|
|
UIManager:show(kv)
|
|
|
|
self.kv = kv
|
|
|
|
end
|
|
|
|
}
|
|
|
|
UIManager:show(self.kv)
|
|
|
|
end,
|
|
|
|
},
|
2020-05-29 12:22:27 +00:00
|
|
|
{ _("Books by month"),"",
|
2017-09-28 13:35:25 +00:00
|
|
|
callback = function()
|
|
|
|
local kv = self.kv
|
|
|
|
UIManager:close(self.kv)
|
|
|
|
self.kv = KeyValuePage:new{
|
2020-05-29 12:22:27 +00:00
|
|
|
title = _("Books by month"),
|
2017-11-20 17:08:55 +00:00
|
|
|
value_overflow_align = "right",
|
2020-05-29 12:22:27 +00:00
|
|
|
kv_pairs = self:getDatesFromAll(0, "monthly", true),
|
2017-09-28 13:35:25 +00:00
|
|
|
callback_return = function()
|
|
|
|
UIManager:show(kv)
|
|
|
|
self.kv = kv
|
|
|
|
end
|
|
|
|
}
|
|
|
|
UIManager:show(self.kv)
|
|
|
|
end,
|
|
|
|
},
|
2020-05-29 12:22:27 +00:00
|
|
|
"----",
|
|
|
|
{ _("Last week"),"",
|
2017-09-28 13:35:25 +00:00
|
|
|
callback = function()
|
|
|
|
local kv = self.kv
|
|
|
|
UIManager:close(self.kv)
|
|
|
|
self.kv = KeyValuePage:new{
|
2020-05-29 12:22:27 +00:00
|
|
|
title = _("Last week"),
|
2017-11-20 17:08:55 +00:00
|
|
|
value_overflow_align = "right",
|
2020-05-29 12:22:27 +00:00
|
|
|
kv_pairs = self:getDatesFromAll(7, "daily_weekday"),
|
2017-09-28 13:35:25 +00:00
|
|
|
callback_return = function()
|
|
|
|
UIManager:show(kv)
|
|
|
|
self.kv = kv
|
|
|
|
end
|
|
|
|
}
|
|
|
|
UIManager:show(self.kv)
|
|
|
|
end,
|
|
|
|
},
|
2020-05-29 12:22:27 +00:00
|
|
|
{ _("Last month by day"),"",
|
2017-09-28 13:35:25 +00:00
|
|
|
callback = function()
|
|
|
|
local kv = self.kv
|
|
|
|
UIManager:close(self.kv)
|
|
|
|
self.kv = KeyValuePage:new{
|
2020-05-29 12:22:27 +00:00
|
|
|
title = _("Last month by day"),
|
2017-11-20 17:08:55 +00:00
|
|
|
value_overflow_align = "right",
|
2020-05-29 12:22:27 +00:00
|
|
|
kv_pairs = self:getDatesFromAll(30, "daily_weekday"),
|
2017-09-28 13:35:25 +00:00
|
|
|
callback_return = function()
|
|
|
|
UIManager:show(kv)
|
|
|
|
self.kv = kv
|
|
|
|
end
|
|
|
|
}
|
|
|
|
UIManager:show(self.kv)
|
|
|
|
end,
|
|
|
|
},
|
2020-05-29 12:22:27 +00:00
|
|
|
{ _("Last year by day"),"",
|
2017-09-28 13:35:25 +00:00
|
|
|
callback = function()
|
|
|
|
local kv = self.kv
|
|
|
|
UIManager:close(self.kv)
|
|
|
|
self.kv = KeyValuePage:new{
|
2020-05-29 12:22:27 +00:00
|
|
|
title = _("Last year by day"),
|
2017-11-20 17:08:55 +00:00
|
|
|
value_overflow_align = "right",
|
2020-05-29 12:22:27 +00:00
|
|
|
kv_pairs = self:getDatesFromAll(365, "daily"),
|
2017-09-28 13:35:25 +00:00
|
|
|
callback_return = function()
|
|
|
|
UIManager:show(kv)
|
|
|
|
self.kv = kv
|
|
|
|
end
|
|
|
|
}
|
|
|
|
UIManager:show(self.kv)
|
|
|
|
end,
|
|
|
|
},
|
2020-05-29 12:22:27 +00:00
|
|
|
{ _("Last year by week"),"",
|
2017-09-28 13:35:25 +00:00
|
|
|
callback = function()
|
|
|
|
local kv = self.kv
|
|
|
|
UIManager:close(self.kv)
|
|
|
|
self.kv = KeyValuePage:new{
|
2020-05-29 12:22:27 +00:00
|
|
|
title = _("Last year by week"),
|
2017-11-20 17:08:55 +00:00
|
|
|
value_overflow_align = "right",
|
2020-05-29 12:22:27 +00:00
|
|
|
kv_pairs = self:getDatesFromAll(365, "weekly"),
|
2017-09-28 13:35:25 +00:00
|
|
|
callback_return = function()
|
|
|
|
UIManager:show(kv)
|
|
|
|
self.kv = kv
|
|
|
|
end
|
|
|
|
}
|
|
|
|
UIManager:show(self.kv)
|
|
|
|
end,
|
|
|
|
},
|
2020-05-29 12:22:27 +00:00
|
|
|
{ _("All stats by month"),"",
|
2017-09-28 13:35:25 +00:00
|
|
|
callback = function()
|
|
|
|
local kv = self.kv
|
|
|
|
UIManager:close(self.kv)
|
|
|
|
self.kv = KeyValuePage:new{
|
2020-05-29 12:22:27 +00:00
|
|
|
title = _("All stats by month"),
|
2017-11-20 17:08:55 +00:00
|
|
|
value_overflow_align = "right",
|
2020-05-29 12:22:27 +00:00
|
|
|
kv_pairs = self:getDatesFromAll(0, "monthly"),
|
2017-09-28 13:35:25 +00:00
|
|
|
callback_return = function()
|
|
|
|
UIManager:show(kv)
|
|
|
|
self.kv = kv
|
|
|
|
end
|
|
|
|
}
|
|
|
|
UIManager:show(self.kv)
|
|
|
|
end,
|
2020-05-29 12:22:27 +00:00
|
|
|
},
|
2017-09-28 13:35:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
UIManager:show(self.kv)
|
|
|
|
end
|
|
|
|
|
2017-08-29 15:34:49 +00:00
|
|
|
function ReaderStatistics:getTodayBookStats()
|
2016-10-31 18:47:57 +00:00
|
|
|
local now_stamp = os.time()
|
|
|
|
local now_t = os.date("*t")
|
|
|
|
local from_begin_day = now_t.hour * 3600 + now_t.min * 60 + now_t.sec
|
|
|
|
local start_today_time = now_stamp - from_begin_day
|
2017-08-29 15:34:49 +00:00
|
|
|
local conn = SQ3.open(db_location)
|
|
|
|
local sql_stmt = [[
|
|
|
|
SELECT count(*),
|
|
|
|
sum(sum_period)
|
|
|
|
FROM (
|
|
|
|
SELECT sum(period) AS sum_period
|
|
|
|
FROM page_stat
|
|
|
|
WHERE start_time >= '%s'
|
|
|
|
GROUP BY id_book, page
|
2018-04-29 13:15:11 +00:00
|
|
|
)
|
2017-08-29 15:34:49 +00:00
|
|
|
]]
|
|
|
|
local today_pages, today_period = conn:rowexec(string.format(sql_stmt, start_today_time))
|
|
|
|
if today_pages == nil then
|
|
|
|
today_pages = 0
|
2015-09-07 17:06:17 +00:00
|
|
|
end
|
2017-08-29 15:34:49 +00:00
|
|
|
if today_period == nil then
|
|
|
|
today_period = 0
|
|
|
|
end
|
|
|
|
today_period = tonumber(today_period)
|
|
|
|
today_pages = tonumber(today_pages)
|
|
|
|
conn:close()
|
|
|
|
return today_period, today_pages
|
|
|
|
end
|
2016-10-31 18:47:57 +00:00
|
|
|
|
2017-08-29 15:34:49 +00:00
|
|
|
function ReaderStatistics:getCurrentBookStats()
|
|
|
|
local conn = SQ3.open(db_location)
|
|
|
|
local sql_stmt = [[
|
|
|
|
SELECT count(*),
|
|
|
|
sum(sum_period)
|
|
|
|
FROM (
|
|
|
|
SELECT sum(period) AS sum_period
|
|
|
|
FROM page_stat
|
|
|
|
WHERE start_time >= '%s'
|
|
|
|
GROUP BY id_book, page
|
|
|
|
)
|
|
|
|
]]
|
|
|
|
local current_pages, current_period = conn:rowexec(string.format(sql_stmt, self.start_current_period))
|
|
|
|
if current_pages == nil then
|
|
|
|
current_pages = 0
|
2016-10-31 18:47:57 +00:00
|
|
|
end
|
2017-08-29 15:34:49 +00:00
|
|
|
if current_period == nil then
|
|
|
|
current_period = 0
|
|
|
|
end
|
|
|
|
current_period = tonumber(current_period)
|
|
|
|
current_pages = tonumber(current_pages)
|
|
|
|
return current_period, current_pages
|
2015-09-07 17:06:17 +00:00
|
|
|
end
|
|
|
|
|
2017-08-29 15:34:49 +00:00
|
|
|
function ReaderStatistics:getCurrentStat(id_book)
|
|
|
|
if id_book == nil then
|
|
|
|
return
|
2015-09-07 17:06:17 +00:00
|
|
|
end
|
2017-08-29 15:34:49 +00:00
|
|
|
self:insertDB(id_book)
|
|
|
|
local today_period, today_pages = self:getTodayBookStats()
|
|
|
|
local current_period, current_pages = self:getCurrentBookStats()
|
2015-09-07 17:06:17 +00:00
|
|
|
|
2017-08-29 15:34:49 +00:00
|
|
|
local conn = SQ3.open(db_location)
|
2020-05-29 12:22:27 +00:00
|
|
|
local highlights, notes = conn:rowexec(string.format("SELECT highlights, notes FROM book WHERE id = '%s';)", id_book)) -- luacheck: no unused
|
2017-08-29 15:34:49 +00:00
|
|
|
local sql_stmt = [[
|
|
|
|
SELECT count(*)
|
|
|
|
FROM (
|
|
|
|
SELECT strftime('%%Y-%%m-%%d', start_time, 'unixepoch', 'localtime') AS dates
|
|
|
|
FROM page_stat
|
|
|
|
WHERE id_book = '%s'
|
|
|
|
GROUP BY dates
|
|
|
|
)
|
|
|
|
]]
|
|
|
|
local total_days = conn:rowexec(string.format(sql_stmt, id_book))
|
2020-05-29 12:22:27 +00:00
|
|
|
|
2017-08-29 15:34:49 +00:00
|
|
|
sql_stmt = [[
|
|
|
|
SELECT sum(period),
|
2020-05-29 12:22:27 +00:00
|
|
|
count(DISTINCT page),
|
|
|
|
min(start_time)
|
2017-08-29 15:34:49 +00:00
|
|
|
FROM page_stat
|
|
|
|
WHERE id_book = '%s'
|
|
|
|
]]
|
2020-05-29 12:22:27 +00:00
|
|
|
local total_time_book, total_read_pages, first_open = conn:rowexec(string.format(sql_stmt, id_book))
|
2017-08-29 15:34:49 +00:00
|
|
|
conn:close()
|
|
|
|
|
|
|
|
if total_time_book == nil then
|
|
|
|
total_time_book = 0
|
|
|
|
end
|
|
|
|
if total_read_pages == nil then
|
|
|
|
total_read_pages = 0
|
|
|
|
end
|
2020-05-29 12:22:27 +00:00
|
|
|
if first_open == nil then
|
|
|
|
first_open = TimeVal:now().sec
|
|
|
|
end
|
2017-08-29 15:34:49 +00:00
|
|
|
self.data.pages = self.view.document:getPageCount()
|
|
|
|
total_time_book = tonumber(total_time_book)
|
|
|
|
total_read_pages = tonumber(total_read_pages)
|
2019-09-10 19:32:02 +00:00
|
|
|
local time_to_read = (self.data.pages - self.view.state.page) * self.avg_time
|
2018-09-29 17:51:25 +00:00
|
|
|
local estimate_days_to_read = math.ceil(time_to_read/(total_time_book/tonumber(total_days)))
|
|
|
|
local estimate_end_of_read_date = os.date("%Y-%m-%d", tonumber(os.time() + estimate_days_to_read * 86400))
|
2019-09-10 19:32:02 +00:00
|
|
|
local formatstr = "%.0f%%"
|
2017-08-29 15:34:49 +00:00
|
|
|
return {
|
2020-05-29 12:22:27 +00:00
|
|
|
-- Global statistics (may consider other books than current book)
|
|
|
|
-- since last resume
|
2019-09-10 19:32:02 +00:00
|
|
|
{ _("Time spent reading this session"), util.secondsToClock(current_period, false) },
|
|
|
|
{ _("Pages read this session"), tonumber(current_pages) },
|
2020-05-29 12:22:27 +00:00
|
|
|
-- today
|
2019-09-10 19:32:02 +00:00
|
|
|
{ _("Time spent reading today"), util.secondsToClock(today_period, false) },
|
|
|
|
{ _("Pages read today"), tonumber(today_pages) },
|
2020-05-29 12:22:27 +00:00
|
|
|
"----",
|
|
|
|
-- Current book statistics
|
|
|
|
{ _("Time spent reading this book"), util.secondsToClock(total_time_book, false) },
|
|
|
|
-- per days
|
|
|
|
{ _("Reading started"), os.date("%Y-%m-%d (%H:%M)", tonumber(first_open))},
|
|
|
|
{ _("Days reading this book"), tonumber(total_days) },
|
|
|
|
{ _("Average time per day"), util.secondsToClock(total_time_book/tonumber(total_days), false) },
|
|
|
|
-- per page
|
|
|
|
{ _("Pages read"), tonumber(total_read_pages) },
|
2019-09-10 19:32:02 +00:00
|
|
|
{ _("Average time per page"), util.secondsToClock(self.avg_time, false) },
|
2020-05-29 12:22:27 +00:00
|
|
|
-- estimation, from current page to end of book
|
|
|
|
{ _("Current page/Total pages"), self.curr_page .. "/" .. self.data.pages },
|
2019-09-10 19:32:02 +00:00
|
|
|
{ _("Percentage completed"), formatstr:format(self.curr_page/self.data.pages * 100) },
|
2020-05-29 12:22:27 +00:00
|
|
|
{ _("Estimated time to read"), util.secondsToClock(time_to_read, false) },
|
2019-08-24 11:45:07 +00:00
|
|
|
{ _("Estimated reading finished"),
|
2019-08-25 13:32:41 +00:00
|
|
|
T(N_("%1 (1 day)", "%1 (%2 days)", estimate_days_to_read), estimate_end_of_read_date, estimate_days_to_read) },
|
2020-05-29 12:22:27 +00:00
|
|
|
|
|
|
|
{ _("Highlights"), tonumber(highlights) },
|
|
|
|
-- { _("Total notes"), tonumber(notes) }, -- not accurate, don't show it
|
2017-08-29 15:34:49 +00:00
|
|
|
}
|
2015-11-23 19:37:36 +00:00
|
|
|
end
|
|
|
|
|
2017-09-23 17:51:58 +00:00
|
|
|
function ReaderStatistics:getBookStat(id_book)
|
|
|
|
if id_book == nil then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
local conn = SQ3.open(db_location)
|
|
|
|
local sql_stmt = [[
|
2020-05-29 12:22:27 +00:00
|
|
|
SELECT title, authors, pages, last_open, highlights, notes
|
2017-09-23 17:51:58 +00:00
|
|
|
FROM book
|
|
|
|
WHERE id = '%s'
|
|
|
|
]]
|
2020-05-29 12:22:27 +00:00
|
|
|
local title, authors, pages, last_open, highlights, notes = conn:rowexec(string.format(sql_stmt, id_book))
|
|
|
|
|
|
|
|
-- Due to some bug, some books opened around April 2020 might
|
|
|
|
-- have notes and highlight NULL in the DB.
|
|
|
|
-- See: https://github.com/koreader/koreader/issues/6190#issuecomment-633693940
|
|
|
|
-- (We made these last in the SQL so NULL/nil doesn't prevent
|
|
|
|
-- fetching the other fields.)
|
|
|
|
-- Show "?" when these values are not known (they will be
|
|
|
|
-- fixed next time this book is opened).
|
|
|
|
highlights = highlights and tonumber(highlights) or "?"
|
|
|
|
notes = notes and tonumber(notes) or "?" -- luacheck: no unused
|
2017-09-23 17:51:58 +00:00
|
|
|
|
|
|
|
sql_stmt = [[
|
|
|
|
SELECT count(*)
|
|
|
|
FROM (
|
|
|
|
SELECT strftime('%%Y-%%m-%%d', start_time, 'unixepoch', 'localtime') AS dates
|
|
|
|
FROM page_stat
|
|
|
|
WHERE id_book = '%s'
|
|
|
|
GROUP BY dates
|
|
|
|
)
|
|
|
|
]]
|
|
|
|
local total_days = conn:rowexec(string.format(sql_stmt, id_book))
|
|
|
|
|
|
|
|
sql_stmt = [[
|
|
|
|
SELECT sum(period),
|
2020-05-29 12:22:27 +00:00
|
|
|
count(DISTINCT page),
|
|
|
|
min(start_time)
|
2017-09-23 17:51:58 +00:00
|
|
|
FROM page_stat
|
|
|
|
WHERE id_book = '%s'
|
|
|
|
]]
|
2020-05-29 12:22:27 +00:00
|
|
|
local total_time_book, total_read_pages, first_open = conn:rowexec(string.format(sql_stmt, id_book))
|
2017-09-23 17:51:58 +00:00
|
|
|
|
|
|
|
conn:close()
|
|
|
|
|
|
|
|
if total_time_book == nil then
|
|
|
|
total_time_book = 0
|
|
|
|
end
|
|
|
|
if total_read_pages == nil then
|
|
|
|
total_read_pages = 0
|
|
|
|
end
|
2020-05-29 12:22:27 +00:00
|
|
|
if first_open == nil then
|
|
|
|
first_open = TimeVal:now().sec
|
|
|
|
end
|
2017-09-23 17:51:58 +00:00
|
|
|
total_time_book = tonumber(total_time_book)
|
|
|
|
total_read_pages = tonumber(total_read_pages)
|
|
|
|
pages = tonumber(pages)
|
|
|
|
if pages == nil or pages == 0 then
|
|
|
|
pages = 1
|
|
|
|
end
|
|
|
|
local avg_time_per_page = total_time_book / total_read_pages
|
|
|
|
return {
|
|
|
|
{ _("Title"), title},
|
|
|
|
{ _("Authors"), authors},
|
2020-05-29 12:22:27 +00:00
|
|
|
{ _("Reading started"), os.date("%Y-%m-%d (%H:%M)", tonumber(first_open))},
|
|
|
|
{ _("Last read"), os.date("%Y-%m-%d (%H:%M)", tonumber(last_open))},
|
|
|
|
{ _("Days reading this book"), tonumber(total_days) },
|
|
|
|
{ _("Time spent reading this book"), util.secondsToClock(total_time_book, false) },
|
|
|
|
{ _("Average time per day"), util.secondsToClock(total_time_book/tonumber(total_days), false) },
|
2017-09-23 17:51:58 +00:00
|
|
|
{ _("Average time per page"), util.secondsToClock(avg_time_per_page, false) },
|
2020-05-29 12:22:27 +00:00
|
|
|
-- These 2 ones are about page actually read (not the current page and % into book)
|
2017-09-23 17:51:58 +00:00
|
|
|
{ _("Read pages/Total pages"), total_read_pages .. "/" .. pages },
|
2020-05-29 12:22:27 +00:00
|
|
|
{ _("Percentage read"), Math.round(total_read_pages / pages * 100) .. "%" },
|
|
|
|
{ _("Highlights"), highlights },
|
|
|
|
-- { _("Total notes"), notes }, -- not accurate, don't show it
|
2017-09-23 17:51:58 +00:00
|
|
|
"----",
|
|
|
|
{ _("Show days"), _("Tap to display"),
|
|
|
|
callback = function()
|
2017-09-28 13:35:25 +00:00
|
|
|
local kv = self.kv
|
|
|
|
UIManager:close(self.kv)
|
|
|
|
self.kv = KeyValuePage:new{
|
2020-05-29 12:22:27 +00:00
|
|
|
title = T(_("Days reading %1"), title),
|
2017-11-20 17:08:55 +00:00
|
|
|
value_overflow_align = "right",
|
2017-09-23 17:51:58 +00:00
|
|
|
kv_pairs = self:getDatesForBook(id_book),
|
2017-09-28 13:35:25 +00:00
|
|
|
callback_return = function()
|
|
|
|
UIManager:show(kv)
|
|
|
|
self.kv = kv
|
|
|
|
end
|
|
|
|
}
|
|
|
|
UIManager:show(self.kv)
|
2017-09-23 17:51:58 +00:00
|
|
|
end,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2017-09-28 13:35:25 +00:00
|
|
|
local function sqlDaily()
|
|
|
|
return
|
|
|
|
[[
|
2017-08-29 15:34:49 +00:00
|
|
|
SELECT dates,
|
|
|
|
count(*) AS pages,
|
|
|
|
sum(sum_period) AS periods,
|
|
|
|
start_time
|
|
|
|
FROM (
|
|
|
|
SELECT strftime('%%Y-%%m-%%d', start_time, 'unixepoch', 'localtime') AS dates,
|
|
|
|
sum(period) AS sum_period,
|
|
|
|
start_time
|
|
|
|
FROM page_stat
|
|
|
|
WHERE start_time >= '%s'
|
|
|
|
GROUP BY id_book, page, dates
|
|
|
|
)
|
|
|
|
GROUP BY dates
|
|
|
|
ORDER BY dates DESC
|
2017-09-28 13:35:25 +00:00
|
|
|
]]
|
|
|
|
end
|
|
|
|
|
|
|
|
local function sqlWeekly()
|
|
|
|
return
|
|
|
|
[[
|
2017-08-29 15:34:49 +00:00
|
|
|
SELECT dates,
|
|
|
|
count(*) AS pages,
|
|
|
|
sum(sum_period) AS periods,
|
|
|
|
start_time
|
|
|
|
FROM (
|
|
|
|
SELECT strftime('%%Y-%%W', start_time, 'unixepoch', 'localtime') AS dates,
|
|
|
|
sum(period) AS sum_period,
|
|
|
|
start_time
|
|
|
|
FROM page_stat
|
|
|
|
WHERE start_time >= '%s'
|
|
|
|
GROUP BY id_book, page, dates
|
|
|
|
)
|
|
|
|
GROUP BY dates
|
|
|
|
ORDER BY dates DESC
|
2017-09-28 13:35:25 +00:00
|
|
|
]]
|
|
|
|
end
|
|
|
|
|
|
|
|
local function sqlMonthly()
|
|
|
|
return
|
|
|
|
[[
|
2017-08-29 15:34:49 +00:00
|
|
|
SELECT dates,
|
|
|
|
count(*) AS pages,
|
|
|
|
sum(sum_period) AS periods,
|
|
|
|
start_time
|
|
|
|
FROM (
|
|
|
|
SELECT strftime('%%Y-%%m', start_time, 'unixepoch', 'localtime') AS dates,
|
|
|
|
sum(period) AS sum_period,
|
|
|
|
start_time
|
|
|
|
FROM page_stat
|
2017-09-28 13:35:25 +00:00
|
|
|
WHERE start_time >= '%s'
|
2017-08-29 15:34:49 +00:00
|
|
|
GROUP BY id_book, page, dates
|
|
|
|
)
|
|
|
|
GROUP BY dates
|
|
|
|
ORDER BY dates DESC
|
2017-09-28 13:35:25 +00:00
|
|
|
]]
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderStatistics:callbackMonthly(begin, finish, date_text, book_mode)
|
|
|
|
local kv = self.kv
|
|
|
|
UIManager:close(kv)
|
|
|
|
if book_mode then
|
|
|
|
self.kv = KeyValuePage:new{
|
2020-05-29 12:22:27 +00:00
|
|
|
title = T(_("Books read in %1"), date_text),
|
2017-12-17 21:08:13 +00:00
|
|
|
value_align = "right",
|
2017-09-28 13:35:25 +00:00
|
|
|
kv_pairs = self:getBooksFromPeriod(begin, finish),
|
|
|
|
callback_return = function()
|
|
|
|
UIManager:show(kv)
|
|
|
|
self.kv = kv
|
|
|
|
end
|
|
|
|
}
|
|
|
|
else
|
|
|
|
self.kv = KeyValuePage:new{
|
|
|
|
title = date_text,
|
2017-12-17 21:08:13 +00:00
|
|
|
value_align = "right",
|
2017-09-28 13:35:25 +00:00
|
|
|
kv_pairs = self:getDaysFromPeriod(begin, finish),
|
|
|
|
callback_return = function()
|
|
|
|
UIManager:show(kv)
|
|
|
|
self.kv = kv
|
|
|
|
end
|
|
|
|
}
|
|
|
|
end
|
|
|
|
UIManager:show(self.kv)
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderStatistics:callbackWeekly(begin, finish, date_text, book_mode)
|
|
|
|
local kv = self.kv
|
|
|
|
UIManager:close(kv)
|
|
|
|
if book_mode then
|
|
|
|
self.kv = KeyValuePage:new{
|
2020-05-29 12:22:27 +00:00
|
|
|
title = T(_("Books read in %1"), date_text),
|
2017-12-17 21:08:13 +00:00
|
|
|
value_align = "right",
|
2017-09-28 13:35:25 +00:00
|
|
|
kv_pairs = self:getBooksFromPeriod(begin, finish),
|
|
|
|
callback_return = function()
|
|
|
|
UIManager:show(kv)
|
|
|
|
self.kv = kv
|
|
|
|
end
|
|
|
|
}
|
|
|
|
else
|
|
|
|
self.kv = KeyValuePage:new{
|
|
|
|
title = date_text,
|
2017-12-17 21:08:13 +00:00
|
|
|
value_align = "right",
|
2017-09-28 13:35:25 +00:00
|
|
|
kv_pairs = self:getDaysFromPeriod(begin, finish),
|
|
|
|
callback_return = function()
|
|
|
|
UIManager:show(kv)
|
|
|
|
self.kv = kv
|
|
|
|
end
|
|
|
|
}
|
|
|
|
end
|
|
|
|
UIManager:show(self.kv)
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderStatistics:callbackDaily(begin, finish, date_text)
|
|
|
|
local kv = self.kv
|
|
|
|
UIManager:close(kv)
|
|
|
|
self.kv = KeyValuePage:new{
|
|
|
|
title = date_text,
|
2017-12-17 21:08:13 +00:00
|
|
|
value_align = "right",
|
2017-09-28 13:35:25 +00:00
|
|
|
kv_pairs = self:getBooksFromPeriod(begin, finish),
|
|
|
|
callback_return = function()
|
|
|
|
UIManager:show(kv)
|
|
|
|
self.kv = kv
|
|
|
|
end
|
|
|
|
}
|
|
|
|
UIManager:show(self.kv)
|
|
|
|
end
|
|
|
|
|
|
|
|
-- sdays -> number of days to show
|
|
|
|
-- ptype -> daily - show daily without weekday name
|
|
|
|
-- daily_weekday - show daily with weekday name
|
|
|
|
-- weekly - show weekly
|
|
|
|
-- monthly - show monthly
|
|
|
|
-- book_mode = if true than show book in this period
|
|
|
|
function ReaderStatistics:getDatesFromAll(sdays, ptype, book_mode)
|
|
|
|
local results = {}
|
|
|
|
local year_begin, year_end, month_begin, month_end
|
|
|
|
local timestamp
|
|
|
|
local now_t = os.date("*t")
|
|
|
|
local from_begin_day = now_t.hour *3600 + now_t.min*60 + now_t.sec
|
|
|
|
local now_stamp = os.time()
|
|
|
|
local one_day = 86400 -- one day in seconds
|
|
|
|
local sql_stmt_res_book
|
|
|
|
local period_begin = 0
|
|
|
|
if sdays > 0 then
|
|
|
|
period_begin = now_stamp - ((sdays-1) * one_day) - from_begin_day
|
|
|
|
end
|
|
|
|
if ptype == "daily" or ptype == "daily_weekday" then
|
|
|
|
sql_stmt_res_book = sqlDaily()
|
|
|
|
elseif ptype == "weekly" then
|
|
|
|
sql_stmt_res_book = sqlWeekly()
|
|
|
|
elseif ptype == "monthly" then
|
|
|
|
sql_stmt_res_book = sqlMonthly()
|
2017-08-29 15:34:49 +00:00
|
|
|
end
|
|
|
|
self:insertDB(self.id_curr_book)
|
|
|
|
local conn = SQ3.open(db_location)
|
|
|
|
local result_book = conn:exec(string.format(sql_stmt_res_book, period_begin))
|
|
|
|
conn:close()
|
|
|
|
if result_book == nil then
|
|
|
|
return {}
|
|
|
|
end
|
|
|
|
for i=1, #result_book.dates do
|
|
|
|
local date_text
|
2017-09-28 13:35:25 +00:00
|
|
|
timestamp = tonumber(result_book[4][i])
|
2017-08-29 15:34:49 +00:00
|
|
|
if ptype == "daily_weekday" then
|
|
|
|
date_text = string.format("%s (%s)",
|
2017-09-28 13:35:25 +00:00
|
|
|
os.date("%Y-%m-%d", timestamp),
|
|
|
|
shortDayOfWeekTranslation[os.date("%a", timestamp)])
|
2017-08-29 15:34:49 +00:00
|
|
|
elseif ptype == "daily" then
|
|
|
|
date_text = result_book[1][i]
|
|
|
|
elseif ptype == "weekly" then
|
2017-09-28 13:35:25 +00:00
|
|
|
date_text = T(_("%1 Week %2"), os.date("%Y", timestamp), os.date(" %W", timestamp))
|
2017-08-29 15:34:49 +00:00
|
|
|
elseif ptype == "monthly" then
|
2017-09-28 13:35:25 +00:00
|
|
|
date_text = monthTranslation[os.date("%B", timestamp)] .. os.date(" %Y", timestamp)
|
2015-11-23 19:37:36 +00:00
|
|
|
else
|
2017-08-29 15:34:49 +00:00
|
|
|
date_text = result_book[1][i]
|
2015-11-23 19:37:36 +00:00
|
|
|
end
|
2017-09-09 18:19:05 +00:00
|
|
|
if ptype == "monthly" then
|
2017-09-28 13:35:25 +00:00
|
|
|
year_begin = tonumber(os.date("%Y", timestamp))
|
|
|
|
month_begin = tonumber(os.date("%m", timestamp))
|
2017-09-09 18:19:05 +00:00
|
|
|
if month_begin == 12 then
|
|
|
|
year_end = year_begin + 1
|
|
|
|
month_end = 1
|
|
|
|
else
|
|
|
|
year_end = year_begin
|
|
|
|
month_end = month_begin + 1
|
|
|
|
end
|
|
|
|
local start_month = os.time{year=year_begin, month=month_begin, day=1, hour=0, min=0 }
|
|
|
|
local stop_month = os.time{year=year_end, month=month_end, day=1, hour=0, min=0 }
|
|
|
|
table.insert(results, {
|
|
|
|
date_text,
|
|
|
|
T(_("Pages: (%1) Time: %2"), tonumber(result_book[2][i]), util.secondsToClock(tonumber(result_book[3][i]), false)),
|
|
|
|
callback = function()
|
2017-09-28 13:35:25 +00:00
|
|
|
self:callbackMonthly(start_month, stop_month, date_text, book_mode)
|
|
|
|
end,
|
|
|
|
})
|
|
|
|
elseif ptype == "weekly" then
|
|
|
|
local time_book = os.date("%H%M%S%w", timestamp)
|
|
|
|
local begin_week = tonumber(result_book[4][i]) - 3600 * tonumber(string.sub(time_book,1,2))
|
|
|
|
- 60 * tonumber(string.sub(time_book,3,4)) - tonumber(string.sub(time_book,5,6))
|
|
|
|
local weekday = tonumber(string.sub(time_book,7,8))
|
|
|
|
if weekday == 0 then weekday = 6 else weekday = weekday - 1 end
|
|
|
|
begin_week = begin_week - weekday * 86400
|
|
|
|
table.insert(results, {
|
|
|
|
date_text,
|
|
|
|
T(_("Pages: (%1) Time: %2"), tonumber(result_book[2][i]), util.secondsToClock(tonumber(result_book[3][i]), false)),
|
|
|
|
callback = function()
|
|
|
|
self:callbackWeekly(begin_week, begin_week + 7 * 86400, date_text, book_mode)
|
2017-09-09 18:19:05 +00:00
|
|
|
end,
|
|
|
|
})
|
|
|
|
else
|
2017-09-28 13:35:25 +00:00
|
|
|
local time_book = os.date("%H%M%S", timestamp)
|
|
|
|
local begin_day = tonumber(result_book[4][i]) - 3600 * tonumber(string.sub(time_book,1,2))
|
|
|
|
- 60 * tonumber(string.sub(time_book,3,4)) - tonumber(string.sub(time_book,5,6))
|
2017-09-09 18:19:05 +00:00
|
|
|
table.insert(results, {
|
|
|
|
date_text,
|
2017-09-28 13:35:25 +00:00
|
|
|
T(_("Pages: (%1) Time: %2"), tonumber(result_book[2][i]), util.secondsToClock(tonumber(result_book[3][i]), false)),
|
|
|
|
callback = function()
|
|
|
|
self:callbackDaily(begin_day, begin_day + 86400, date_text)
|
|
|
|
end,
|
2017-09-09 18:19:05 +00:00
|
|
|
})
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return results
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderStatistics:getDaysFromPeriod(period_begin, period_end)
|
|
|
|
local results = {}
|
|
|
|
local sql_stmt_res_book = [[
|
|
|
|
SELECT dates,
|
|
|
|
count(*) AS pages,
|
|
|
|
sum(sum_period) AS periods,
|
|
|
|
start_time
|
|
|
|
FROM (
|
|
|
|
SELECT strftime('%%Y-%%m-%%d', start_time, 'unixepoch', 'localtime') AS dates,
|
|
|
|
sum(period) AS sum_period,
|
|
|
|
start_time
|
|
|
|
FROM page_stat
|
|
|
|
WHERE start_time >= '%s' AND start_time < '%s'
|
|
|
|
GROUP BY id_book, page, dates
|
|
|
|
)
|
|
|
|
GROUP BY dates
|
|
|
|
ORDER BY dates DESC
|
|
|
|
]]
|
|
|
|
local conn = SQ3.open(db_location)
|
|
|
|
local result_book = conn:exec(string.format(sql_stmt_res_book, period_begin, period_end))
|
|
|
|
conn:close()
|
|
|
|
if result_book == nil then
|
|
|
|
return {}
|
|
|
|
end
|
|
|
|
for i=1, #result_book.dates do
|
2017-09-28 13:35:25 +00:00
|
|
|
local time_begin = os.time{year=string.sub(result_book[1][i],1,4), month=string.sub(result_book[1][i],6,7),
|
|
|
|
day=string.sub(result_book[1][i],9,10), hour=0, min=0, sec=0 }
|
2017-08-29 15:34:49 +00:00
|
|
|
table.insert(results, {
|
2017-09-09 18:19:05 +00:00
|
|
|
result_book[1][i],
|
2017-09-28 13:35:25 +00:00
|
|
|
T(_("Pages: (%1) Time: %2"), tonumber(result_book[2][i]), util.secondsToClock(tonumber(result_book[3][i]), false)),
|
|
|
|
callback = function()
|
|
|
|
local kv = self.kv
|
|
|
|
UIManager:close(kv)
|
|
|
|
self.kv = KeyValuePage:new{
|
2020-05-29 12:22:27 +00:00
|
|
|
title = T(_("Books read %1"), result_book[1][i]),
|
2017-11-20 17:08:55 +00:00
|
|
|
value_overflow_align = "right",
|
2017-09-28 13:35:25 +00:00
|
|
|
kv_pairs = self:getBooksFromPeriod(time_begin, time_begin + 86400),
|
|
|
|
callback_return = function()
|
|
|
|
UIManager:show(kv)
|
|
|
|
self.kv = kv
|
|
|
|
end
|
|
|
|
}
|
|
|
|
UIManager:show(self.kv)
|
|
|
|
end,
|
|
|
|
})
|
|
|
|
end
|
|
|
|
return results
|
|
|
|
end
|
|
|
|
|
2020-05-29 12:22:27 +00:00
|
|
|
function ReaderStatistics:getBooksFromPeriod(period_begin, period_end, callback_shows_days)
|
2017-09-28 13:35:25 +00:00
|
|
|
local results = {}
|
|
|
|
local sql_stmt_res_book = [[
|
|
|
|
SELECT book_tbl.title AS title,
|
|
|
|
sum(page_stat_tbl.period),
|
|
|
|
count(distinct page_stat_tbl.page),
|
|
|
|
book_tbl.id
|
|
|
|
FROM page_stat AS page_stat_tbl, book AS book_tbl
|
|
|
|
WHERE page_stat_tbl.id_book=book_tbl.id AND page_stat_tbl.start_time > '%s' AND page_stat_tbl.start_time <= '%s'
|
|
|
|
GROUP BY book_tbl.id
|
|
|
|
ORDER BY book_tbl.last_open DESC
|
|
|
|
]]
|
|
|
|
local conn = SQ3.open(db_location)
|
|
|
|
local result_book = conn:exec(string.format(sql_stmt_res_book, period_begin, period_end))
|
|
|
|
conn:close()
|
|
|
|
if result_book == nil then
|
|
|
|
return {}
|
|
|
|
end
|
|
|
|
for i=1, #result_book.title do
|
|
|
|
table.insert(results, {
|
|
|
|
result_book[1][i],
|
|
|
|
T(_("%1 (%2)"), util.secondsToClock(tonumber(result_book[2][i]), false), tonumber(result_book[3][i])),
|
|
|
|
callback = function()
|
|
|
|
local kv = self.kv
|
|
|
|
UIManager:close(self.kv)
|
2020-05-29 12:22:27 +00:00
|
|
|
if callback_shows_days then -- not used currently by any code
|
|
|
|
self.kv = KeyValuePage:new{
|
|
|
|
title = T(_("Days reading %1"), result_book[1][i]),
|
|
|
|
kv_pairs = self:getDatesForBook(tonumber(result_book[4][i])),
|
|
|
|
value_overflow_align = "right",
|
|
|
|
callback_return = function()
|
|
|
|
UIManager:show(kv)
|
|
|
|
self.kv = kv
|
|
|
|
end
|
|
|
|
}
|
|
|
|
else
|
|
|
|
self.kv = KeyValuePage:new{
|
|
|
|
title = result_book[1][i],
|
|
|
|
kv_pairs = self:getBookStat(tonumber(result_book[4][i])),
|
|
|
|
value_overflow_align = "right",
|
|
|
|
callback_return = function()
|
|
|
|
UIManager:show(kv)
|
|
|
|
self.kv = kv
|
|
|
|
end
|
|
|
|
}
|
|
|
|
end
|
2017-09-28 13:35:25 +00:00
|
|
|
UIManager:show(self.kv)
|
|
|
|
end,
|
2017-08-29 15:34:49 +00:00
|
|
|
})
|
2015-11-23 19:37:36 +00:00
|
|
|
end
|
2017-08-29 15:34:49 +00:00
|
|
|
return results
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderStatistics:getReadingProgressStats(sdays)
|
|
|
|
local results = {}
|
|
|
|
local pages, period, date_read
|
|
|
|
local now_t = os.date("*t")
|
|
|
|
local from_begin_day = now_t.hour *3600 + now_t.min*60 + now_t.sec
|
|
|
|
local now_stamp = os.time()
|
|
|
|
local one_day = 86400 -- one day in seconds
|
|
|
|
local period_begin = now_stamp - ((sdays-1) * one_day) - from_begin_day
|
|
|
|
local conn = SQ3.open(db_location)
|
|
|
|
local sql_stmt = [[
|
|
|
|
SELECT dates,
|
|
|
|
count(*) AS pages,
|
|
|
|
sum(sum_period) AS periods,
|
|
|
|
start_time
|
|
|
|
FROM (
|
|
|
|
SELECT strftime('%%Y-%%m-%%d', start_time, 'unixepoch', 'localtime') AS dates,
|
|
|
|
sum(period) AS sum_period,
|
|
|
|
start_time
|
|
|
|
FROM page_stat
|
|
|
|
WHERE start_time >= '%s'
|
|
|
|
GROUP BY id_book, page, dates
|
|
|
|
)
|
|
|
|
GROUP BY dates
|
|
|
|
ORDER BY dates DESC
|
|
|
|
]]
|
|
|
|
local result_book = conn:exec(string.format(sql_stmt, period_begin))
|
2017-09-01 20:05:57 +00:00
|
|
|
if not result_book then return end
|
2017-08-29 15:34:49 +00:00
|
|
|
for i = 1, sdays do
|
|
|
|
pages = tonumber(result_book[2][i])
|
|
|
|
period = tonumber(result_book[3][i])
|
|
|
|
date_read = result_book[1][i]
|
|
|
|
if pages == nil then pages = 0 end
|
|
|
|
if period == nil then period = 0 end
|
|
|
|
table.insert(results, {
|
|
|
|
pages,
|
|
|
|
period,
|
|
|
|
date_read
|
|
|
|
})
|
|
|
|
end
|
|
|
|
conn:close()
|
|
|
|
return results
|
|
|
|
end
|
|
|
|
|
2017-09-23 17:51:58 +00:00
|
|
|
function ReaderStatistics:getDatesForBook(id_book)
|
2017-08-29 15:34:49 +00:00
|
|
|
local results = {}
|
|
|
|
local conn = SQ3.open(db_location)
|
|
|
|
local sql_stmt = [[
|
|
|
|
SELECT date(start_time, 'unixepoch', 'localtime') AS dates,
|
|
|
|
count(DISTINCT page) AS pages,
|
|
|
|
sum(period) AS periods
|
|
|
|
FROM page_stat
|
|
|
|
WHERE id_book = '%s'
|
|
|
|
GROUP BY Date(start_time, 'unixepoch', 'localtime')
|
|
|
|
ORDER BY dates DESC
|
|
|
|
]]
|
|
|
|
local result_book = conn:exec(string.format(sql_stmt, id_book))
|
|
|
|
conn:close()
|
|
|
|
if result_book == nil then
|
|
|
|
return {}
|
|
|
|
end
|
|
|
|
for i=1, #result_book.dates do
|
|
|
|
table.insert(results, {
|
|
|
|
result_book[1][i],
|
|
|
|
T(_("Pages: (%1) Time: %2"), tonumber(result_book[2][i]), util.secondsToClock(tonumber(result_book[3][i]), false))
|
|
|
|
})
|
|
|
|
end
|
|
|
|
return results
|
2015-09-07 17:06:17 +00:00
|
|
|
end
|
|
|
|
|
2016-02-15 10:18:10 +00:00
|
|
|
function ReaderStatistics:getTotalStats()
|
2017-08-29 15:34:49 +00:00
|
|
|
self:insertDB(self.id_curr_book)
|
|
|
|
local conn = SQ3.open(db_location)
|
|
|
|
local sql_stmt = [[
|
|
|
|
SELECT sum(period)
|
|
|
|
FROM page_stat
|
|
|
|
]]
|
|
|
|
local total_books_time = conn:rowexec(sql_stmt)
|
|
|
|
if total_books_time == nil then
|
|
|
|
total_books_time = 0
|
|
|
|
end
|
2017-01-06 09:33:57 +00:00
|
|
|
local total_stats = {}
|
2017-08-29 15:34:49 +00:00
|
|
|
sql_stmt = [[
|
|
|
|
SELECT id
|
|
|
|
FROM book
|
|
|
|
ORDER BY last_open DESC
|
|
|
|
]]
|
|
|
|
local id_book_tbl = conn:exec(sql_stmt)
|
|
|
|
local nr_books
|
|
|
|
if id_book_tbl ~= nil then
|
|
|
|
nr_books = #id_book_tbl.id
|
|
|
|
else
|
|
|
|
nr_books = 0
|
2017-01-06 09:33:57 +00:00
|
|
|
end
|
2015-10-02 20:18:18 +00:00
|
|
|
|
2017-08-29 15:34:49 +00:00
|
|
|
local total_time_book
|
|
|
|
for i=1, nr_books do
|
|
|
|
local id_book = tonumber(id_book_tbl[1][i])
|
|
|
|
sql_stmt = [[
|
|
|
|
SELECT title
|
|
|
|
FROM book
|
|
|
|
WHERE id = '%s'
|
|
|
|
]]
|
|
|
|
local book_title = conn:rowexec(string.format(sql_stmt, id_book))
|
|
|
|
sql_stmt = [[
|
|
|
|
SELECT sum(period)
|
|
|
|
FROM page_stat
|
|
|
|
WHERE id_book = '%s'
|
|
|
|
]]
|
|
|
|
total_time_book = conn:rowexec(string.format(sql_stmt,id_book))
|
|
|
|
if total_time_book == nil then
|
|
|
|
total_time_book = 0
|
|
|
|
end
|
|
|
|
table.insert(total_stats, {
|
|
|
|
book_title,
|
|
|
|
util.secondsToClock(total_time_book, false),
|
|
|
|
callback = function()
|
2017-09-28 13:35:25 +00:00
|
|
|
local kv = self.kv
|
|
|
|
UIManager:close(self.kv)
|
|
|
|
|
|
|
|
self.kv = KeyValuePage:new{
|
2017-08-29 15:34:49 +00:00
|
|
|
title = book_title,
|
2017-09-23 17:51:58 +00:00
|
|
|
kv_pairs = self:getBookStat(id_book),
|
2020-05-29 12:22:27 +00:00
|
|
|
value_overflow_align = "right",
|
2017-09-28 13:35:25 +00:00
|
|
|
callback_return = function()
|
|
|
|
UIManager:show(kv)
|
|
|
|
self.kv = kv
|
|
|
|
end
|
|
|
|
}
|
|
|
|
UIManager:show(self.kv)
|
2017-08-29 15:34:49 +00:00
|
|
|
end,
|
|
|
|
})
|
|
|
|
end
|
|
|
|
conn:close()
|
2020-05-29 12:22:27 +00:00
|
|
|
return T(_("Total time spent reading: %1"), util.secondsToClock(total_books_time, false)), total_stats
|
2015-09-07 17:06:17 +00:00
|
|
|
end
|
|
|
|
|
2020-02-16 00:03:12 +00:00
|
|
|
function ReaderStatistics:genResetBookSubItemTable()
|
|
|
|
local sub_item_table = {}
|
|
|
|
table.insert(sub_item_table, {
|
|
|
|
text = _("Reset statistics per book"),
|
|
|
|
keep_menu_open = true,
|
|
|
|
callback = function()
|
|
|
|
self:resetBook()
|
|
|
|
end,
|
|
|
|
separator = true,
|
|
|
|
})
|
|
|
|
local reset_minutes = { 1, 5, 15, 30, 60 }
|
|
|
|
for _, minutes in ipairs(reset_minutes) do
|
|
|
|
local text = T(N_("Reset stats for books read for < 1 m",
|
|
|
|
"Reset stats for books read for < %1 m",
|
|
|
|
minutes), minutes)
|
|
|
|
table.insert(sub_item_table, {
|
|
|
|
text = text,
|
|
|
|
keep_menu_open = true,
|
|
|
|
callback = function()
|
|
|
|
self:deleteBooksByTotalDuration(minutes)
|
|
|
|
end,
|
|
|
|
})
|
|
|
|
end
|
|
|
|
return sub_item_table
|
|
|
|
end
|
|
|
|
|
2017-08-29 15:34:49 +00:00
|
|
|
function ReaderStatistics:resetBook()
|
|
|
|
local total_stats = {}
|
|
|
|
local kv_reset_book
|
|
|
|
|
|
|
|
self:insertDB(self.id_curr_book)
|
|
|
|
local conn = SQ3.open(db_location)
|
|
|
|
local sql_stmt = [[
|
|
|
|
SELECT id
|
|
|
|
FROM book
|
|
|
|
ORDER BY last_open DESC
|
|
|
|
]]
|
|
|
|
local id_book_tbl = conn:exec(sql_stmt)
|
|
|
|
local nr_books
|
|
|
|
if id_book_tbl ~= nil then
|
|
|
|
nr_books = #id_book_tbl.id
|
|
|
|
else
|
|
|
|
nr_books = 0
|
|
|
|
end
|
|
|
|
|
|
|
|
local total_time_book
|
|
|
|
for i=1, nr_books do
|
|
|
|
local id_book = tonumber(id_book_tbl[1][i])
|
|
|
|
sql_stmt = [[
|
|
|
|
SELECT title
|
|
|
|
FROM book
|
|
|
|
WHERE id = '%s'
|
|
|
|
]]
|
|
|
|
local book_title = conn:rowexec(string.format(sql_stmt, id_book))
|
|
|
|
sql_stmt = [[
|
|
|
|
SELECT sum(period)
|
|
|
|
FROM page_stat
|
|
|
|
WHERE id_book = '%s'
|
|
|
|
]]
|
|
|
|
total_time_book = conn:rowexec(string.format(sql_stmt,id_book))
|
|
|
|
if total_time_book == nil then
|
|
|
|
total_time_book = 0
|
2017-01-24 17:51:42 +00:00
|
|
|
end
|
2017-08-29 15:34:49 +00:00
|
|
|
|
|
|
|
if id_book ~= self.id_curr_book then
|
2016-10-24 17:15:56 +00:00
|
|
|
table.insert(total_stats, {
|
2017-08-29 15:34:49 +00:00
|
|
|
book_title,
|
|
|
|
util.secondsToClock(total_time_book, false),
|
|
|
|
id_book,
|
2016-10-24 17:15:56 +00:00
|
|
|
callback = function()
|
2017-08-29 15:34:49 +00:00
|
|
|
UIManager:show(ConfirmBox:new{
|
|
|
|
text = T(_("Do you want to reset statistics for book:\n%1"), book_title),
|
|
|
|
cancel_text = _("Cancel"),
|
|
|
|
cancel_callback = function()
|
|
|
|
return
|
|
|
|
end,
|
|
|
|
ok_text = _("Reset"),
|
|
|
|
ok_callback = function()
|
|
|
|
for j=1, #total_stats do
|
|
|
|
if total_stats[j][3] == id_book then
|
|
|
|
self:deleteBook(id_book)
|
|
|
|
table.remove(total_stats, j)
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
--refresh window after delete item
|
|
|
|
kv_reset_book:_populateItems()
|
|
|
|
end,
|
2016-10-24 17:15:56 +00:00
|
|
|
})
|
|
|
|
end,
|
|
|
|
})
|
2015-11-23 19:37:36 +00:00
|
|
|
end
|
|
|
|
end
|
2017-08-29 15:34:49 +00:00
|
|
|
kv_reset_book = KeyValuePage:new{
|
|
|
|
title = _("Reset book statistics"),
|
2017-12-17 21:08:13 +00:00
|
|
|
value_align = "right",
|
2017-08-29 15:34:49 +00:00
|
|
|
kv_pairs = total_stats,
|
|
|
|
}
|
|
|
|
UIManager:show(kv_reset_book)
|
|
|
|
conn:close()
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderStatistics:deleteBook(id_book)
|
|
|
|
local conn = SQ3.open(db_location)
|
|
|
|
local sql_stmt = [[
|
|
|
|
DELETE from book
|
|
|
|
WHERE id = ?
|
|
|
|
]]
|
|
|
|
local stmt = conn:prepare(sql_stmt)
|
|
|
|
stmt:reset():bind(id_book):step()
|
|
|
|
|
|
|
|
sql_stmt = [[
|
|
|
|
DELETE from page_stat
|
|
|
|
WHERE id_book = ?
|
|
|
|
]]
|
|
|
|
stmt = conn:prepare(sql_stmt)
|
|
|
|
stmt:reset():bind(id_book):step()
|
|
|
|
stmt:close()
|
|
|
|
conn:close()
|
2015-09-07 17:06:17 +00:00
|
|
|
end
|
|
|
|
|
2020-02-16 00:03:12 +00:00
|
|
|
function ReaderStatistics:deleteBooksByTotalDuration(max_total_duration_mn)
|
|
|
|
local max_total_duration_sec = max_total_duration_mn * 60
|
|
|
|
UIManager:show(ConfirmBox:new{
|
|
|
|
text = T(N_("Permanently remove statistics for books read for less than 1 minute?",
|
|
|
|
"Permanently remove statistics for books read for less than %1 minutes?",
|
|
|
|
max_total_duration_mn), max_total_duration_mn),
|
|
|
|
ok_text = _("Remove"),
|
|
|
|
ok_callback = function()
|
2020-02-17 15:53:09 +00:00
|
|
|
-- Allow following SQL statements to work even when doc less by
|
|
|
|
-- using -1 as the book id, as real book ids are positive.
|
|
|
|
local id_curr_book = self.id_curr_book or -1
|
2020-02-16 00:03:12 +00:00
|
|
|
local conn = SQ3.open(db_location)
|
|
|
|
local sql_stmt = [[
|
|
|
|
DELETE from page_stat
|
|
|
|
WHERE id_book in (
|
|
|
|
select id from book where id != ? and (total_read_time is NULL or total_read_time < ?)
|
|
|
|
)
|
|
|
|
]]
|
|
|
|
local stmt = conn:prepare(sql_stmt)
|
2020-02-17 15:53:09 +00:00
|
|
|
stmt:reset():bind(id_curr_book, max_total_duration_sec):step()
|
2020-02-16 00:03:12 +00:00
|
|
|
sql_stmt = [[
|
|
|
|
DELETE from book
|
|
|
|
WHERE id != ? and (total_read_time is NULL or total_read_time < ?)
|
|
|
|
]]
|
|
|
|
stmt = conn:prepare(sql_stmt)
|
2020-02-17 15:53:09 +00:00
|
|
|
stmt:reset():bind(id_curr_book, max_total_duration_sec):step()
|
2020-02-16 00:03:12 +00:00
|
|
|
stmt:close()
|
|
|
|
-- Get nb of deleted books
|
|
|
|
sql_stmt = [[
|
|
|
|
SELECT changes()
|
|
|
|
]]
|
|
|
|
local nb_deleted = conn:rowexec(sql_stmt)
|
|
|
|
nb_deleted = nb_deleted and tonumber(nb_deleted) or 0
|
|
|
|
if max_total_duration_mn >= 30 and nb_deleted >= 10 then
|
|
|
|
-- Do a VACUUM to reduce db size (but not worth doing if not much was removed)
|
|
|
|
conn:exec("PRAGMA temp_store = 2") -- use memory for temp files
|
|
|
|
local ok, errmsg = pcall(conn.exec, conn, "VACUUM") -- this may take some time
|
|
|
|
if not ok then
|
|
|
|
logger.warn("Failed compacting statistics database:", errmsg)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
conn:close()
|
|
|
|
UIManager:show(InfoMessage:new{
|
|
|
|
text = nb_deleted > 0 and T(N_("Statistics for 1 book removed.",
|
|
|
|
"Statistics for %1 books removed.",
|
|
|
|
nb_deleted), nb_deleted)
|
|
|
|
or T(_("No statistics removed."))
|
|
|
|
})
|
|
|
|
end,
|
|
|
|
})
|
|
|
|
end
|
|
|
|
|
|
|
|
|
2017-10-09 18:22:34 +00:00
|
|
|
function ReaderStatistics:onPosUpdate(pos, pageno)
|
2017-10-10 06:49:13 +00:00
|
|
|
if self.curr_page ~= pageno then
|
|
|
|
self:onPageUpdate(pageno)
|
|
|
|
end
|
2017-10-05 19:49:59 +00:00
|
|
|
end
|
|
|
|
|
2015-09-13 19:34:20 +00:00
|
|
|
function ReaderStatistics:onPageUpdate(pageno)
|
2017-01-06 09:33:57 +00:00
|
|
|
if self:isDocless() or not self.is_enabled then
|
|
|
|
return
|
|
|
|
end
|
2017-08-29 15:34:49 +00:00
|
|
|
self.curr_page = pageno
|
|
|
|
self.pages_stats[TimeVal:now().sec] = pageno
|
|
|
|
local mem_read_pages = 0
|
|
|
|
local mem_read_time = 0
|
|
|
|
if util.tableSize(self.pages_stats) > 1 then
|
|
|
|
mem_read_pages = util.tableSize(self.pages_stats) - 1
|
|
|
|
local sorted_performance = {}
|
|
|
|
for time, page in pairs(self.pages_stats) do
|
|
|
|
table.insert(sorted_performance, time)
|
|
|
|
end
|
|
|
|
table.sort(sorted_performance)
|
|
|
|
local diff_time
|
|
|
|
for i=1, #sorted_performance - 1 do
|
|
|
|
diff_time = sorted_performance[i + 1] - sorted_performance[i]
|
|
|
|
if diff_time <= self.page_max_read_sec and diff_time >= self.page_min_read_sec then
|
|
|
|
mem_read_time = mem_read_time + diff_time
|
|
|
|
elseif diff_time > self.page_max_read_sec then
|
|
|
|
mem_read_time = mem_read_time + self.page_max_read_sec
|
|
|
|
end
|
|
|
|
end
|
2017-01-06 09:33:57 +00:00
|
|
|
end
|
2017-08-29 15:34:49 +00:00
|
|
|
-- every 50 pages we write stats to database
|
|
|
|
if util.tableSize(self.pages_stats) % PAGE_INSERT == 0 then
|
|
|
|
self:insertDB(self.id_curr_book)
|
|
|
|
mem_read_pages = 0
|
|
|
|
mem_read_time = 0
|
2015-09-07 17:06:17 +00:00
|
|
|
end
|
2017-08-29 15:34:49 +00:00
|
|
|
if self.total_read_pages > 0 or mem_read_pages > 0 then
|
2019-09-10 19:32:02 +00:00
|
|
|
self.avg_time = (self.total_read_time + mem_read_time) / (self.total_read_pages + mem_read_pages)
|
2015-11-23 19:37:36 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- For backward compatibility
|
|
|
|
function ReaderStatistics:importFromFile(base_path, item)
|
2016-02-14 21:47:36 +00:00
|
|
|
item = string.gsub(item, "^%s*(.-)%s*$", "%1") -- trim
|
2016-02-15 13:58:46 +00:00
|
|
|
if item ~= ".stat" then
|
|
|
|
local statistic_file = joinPath(base_path, item)
|
|
|
|
if lfs.attributes(statistic_file, "mode") == "directory" then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
local ok, stored = pcall(dofile, statistic_file)
|
|
|
|
if ok then
|
|
|
|
return stored
|
|
|
|
end
|
2015-09-07 17:06:17 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2015-09-13 19:34:20 +00:00
|
|
|
function ReaderStatistics:onCloseDocument()
|
2017-08-29 15:34:49 +00:00
|
|
|
if not self:isDocless() and self.is_enabled then
|
2015-11-23 19:37:36 +00:00
|
|
|
self.ui.doc_settings:saveSetting("stats", self.data)
|
2017-10-10 06:49:13 +00:00
|
|
|
self:insertDB(self.id_curr_book)
|
2015-09-07 17:06:17 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2015-09-13 19:34:20 +00:00
|
|
|
function ReaderStatistics:onAddHighlight()
|
2015-09-07 17:06:17 +00:00
|
|
|
self.data.highlights = self.data.highlights + 1
|
2017-08-29 15:34:49 +00:00
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderStatistics:onDelHighlight()
|
|
|
|
if self.data.highlights > 0 then
|
|
|
|
self.data.highlights = self.data.highlights - 1
|
|
|
|
end
|
|
|
|
return true
|
2015-09-07 17:06:17 +00:00
|
|
|
end
|
|
|
|
|
2015-09-13 19:34:20 +00:00
|
|
|
function ReaderStatistics:onAddNote()
|
2015-09-07 17:06:17 +00:00
|
|
|
self.data.notes = self.data.notes + 1
|
|
|
|
end
|
|
|
|
|
2015-10-26 14:06:52 +00:00
|
|
|
function ReaderStatistics:onSaveSettings()
|
|
|
|
self:saveSettings()
|
2017-01-06 09:33:57 +00:00
|
|
|
if not self:isDocless() then
|
|
|
|
self.ui.doc_settings:saveSetting("stats", self.data)
|
2017-08-29 15:34:49 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- in case when screensaver starts
|
|
|
|
function ReaderStatistics:onSuspend()
|
|
|
|
if not self:isDocless() then
|
|
|
|
self.ui.doc_settings:saveSetting("stats", self.data)
|
|
|
|
self:insertDB(self.id_curr_book)
|
2017-01-06 09:33:57 +00:00
|
|
|
end
|
2015-09-07 17:06:17 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
-- screensaver off
|
2015-09-13 19:34:20 +00:00
|
|
|
function ReaderStatistics:onResume()
|
2017-08-29 15:34:49 +00:00
|
|
|
self.start_current_period = TimeVal:now().sec
|
|
|
|
self.pages_stats = {}
|
|
|
|
self.pages_stats[self.start_current_period] = self.curr_page
|
2015-09-07 17:06:17 +00:00
|
|
|
end
|
|
|
|
|
2020-02-16 00:03:12 +00:00
|
|
|
function ReaderStatistics:saveSettings()
|
2015-09-07 17:06:17 +00:00
|
|
|
local settings = {
|
|
|
|
min_sec = self.page_min_read_sec,
|
|
|
|
max_sec = self.page_max_read_sec,
|
|
|
|
is_enabled = self.is_enabled,
|
2020-02-16 00:03:12 +00:00
|
|
|
convert_to_db = self.convert_to_db,
|
|
|
|
calendar_start_day_of_week = self.calendar_start_day_of_week,
|
|
|
|
calendar_nb_book_spans = self.calendar_nb_book_spans,
|
|
|
|
calendar_show_histogram = self.calendar_show_histogram,
|
|
|
|
calendar_browse_future_months = self.calendar_browse_future_months,
|
2015-09-07 17:06:17 +00:00
|
|
|
}
|
2015-09-13 19:34:20 +00:00
|
|
|
G_reader_settings:saveSetting("statistics", settings)
|
2015-09-07 17:06:17 +00:00
|
|
|
end
|
|
|
|
|
2015-11-23 19:37:36 +00:00
|
|
|
function ReaderStatistics:onReadSettings(config)
|
2020-04-15 06:52:16 +00:00
|
|
|
self.data = config.data.stats or {}
|
2015-11-23 19:37:36 +00:00
|
|
|
end
|
2015-09-07 17:06:17 +00:00
|
|
|
|
2016-02-12 14:55:02 +00:00
|
|
|
function ReaderStatistics:onReaderReady()
|
2016-02-07 22:03:53 +00:00
|
|
|
-- we have correct page count now, do the actual initialization work
|
|
|
|
self:initData()
|
2020-07-12 18:47:49 +00:00
|
|
|
self.view.footer:onUpdateFooter()
|
2016-02-07 22:03:53 +00:00
|
|
|
end
|
2015-09-07 17:06:17 +00:00
|
|
|
|
2020-04-10 17:14:04 +00:00
|
|
|
function ReaderStatistics:getCalendarView()
|
|
|
|
self:insertDB(self.id_curr_book)
|
|
|
|
local CalendarView = require("calendarview")
|
|
|
|
return CalendarView:new{
|
|
|
|
reader_statistics = self,
|
|
|
|
monthTranslation = monthTranslation,
|
|
|
|
shortDayOfWeekTranslation = shortDayOfWeekTranslation,
|
|
|
|
longDayOfWeekTranslation = longDayOfWeekTranslation,
|
|
|
|
start_day_of_week = self.calendar_start_day_of_week,
|
|
|
|
nb_book_spans = self.calendar_nb_book_spans,
|
|
|
|
show_hourly_histogram = self.calendar_show_histogram,
|
|
|
|
browse_future_months = self.calendar_browse_future_months,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2020-02-12 22:05:18 +00:00
|
|
|
-- Used by calendarview.lua CalendarView
|
|
|
|
function ReaderStatistics:getFirstTimestamp()
|
|
|
|
local sql_stmt = [[
|
|
|
|
SELECT min(start_time)
|
|
|
|
FROM page_stat
|
|
|
|
]]
|
|
|
|
local conn = SQ3.open(db_location)
|
|
|
|
local first_ts = conn:rowexec(sql_stmt)
|
|
|
|
conn:close()
|
|
|
|
return first_ts and tonumber(first_ts) or nil
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderStatistics:getReadingRatioPerHourByDay(month)
|
|
|
|
local sql_stmt = [[
|
|
|
|
SELECT
|
|
|
|
strftime('%Y-%m-%d', start_time, 'unixepoch', 'localtime') day,
|
|
|
|
strftime('%H', start_time, 'unixepoch', 'localtime') hour,
|
|
|
|
sum(period)/3600.0 ratio
|
|
|
|
FROM page_stat
|
|
|
|
WHERE strftime('%Y-%m', start_time, 'unixepoch', 'localtime') = ?
|
|
|
|
GROUP BY
|
|
|
|
strftime('%Y-%m-%d', start_time, 'unixepoch', 'localtime'),
|
|
|
|
strftime('%H', start_time, 'unixepoch', 'localtime')
|
|
|
|
ORDER BY day, hour
|
|
|
|
]]
|
|
|
|
local conn = SQ3.open(db_location)
|
|
|
|
local stmt = conn:prepare(sql_stmt)
|
|
|
|
local res, nb = stmt:reset():bind(month):resultset("i")
|
|
|
|
stmt:close()
|
|
|
|
conn:close()
|
|
|
|
local per_day = {}
|
|
|
|
for i=1, nb do
|
|
|
|
local day, hour, ratio = res[1][i], res[2][i], res[3][i]
|
|
|
|
if not per_day[day] then
|
|
|
|
per_day[day] = {}
|
|
|
|
end
|
|
|
|
-- +1 as histogram starts counting at 1
|
|
|
|
per_day[day][tonumber(hour)+1] = ratio
|
|
|
|
end
|
|
|
|
return per_day
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderStatistics:getReadBookByDay(month)
|
|
|
|
local sql_stmt = [[
|
|
|
|
SELECT
|
|
|
|
strftime('%Y-%m-%d', start_time, 'unixepoch', 'localtime') day,
|
|
|
|
sum(period) duration,
|
|
|
|
id_book book_id,
|
|
|
|
book.title book_title
|
|
|
|
FROM page_stat
|
|
|
|
JOIN book on book.id = page_stat.id_book
|
|
|
|
WHERE strftime('%Y-%m', start_time, 'unixepoch', 'localtime') = ?
|
|
|
|
GROUP BY
|
|
|
|
strftime('%Y-%m-%d', start_time, 'unixepoch', 'localtime'),
|
|
|
|
id_book,
|
|
|
|
title
|
|
|
|
ORDER BY day, duration desc, book_id, book_title
|
|
|
|
]]
|
|
|
|
local conn = SQ3.open(db_location)
|
|
|
|
local stmt = conn:prepare(sql_stmt)
|
|
|
|
local res, nb = stmt:reset():bind(month):resultset("i")
|
|
|
|
stmt:close()
|
|
|
|
conn:close()
|
|
|
|
local per_day = {}
|
|
|
|
for i=1, nb do
|
|
|
|
-- (We don't care about the duration, we just needed it
|
|
|
|
-- to have the books in decreasing duration order)
|
|
|
|
local day, duration, book_id, book_title = res[1][i], res[2][i], res[3][i], res[4][i] -- luacheck: no unused
|
|
|
|
if not per_day[day] then
|
|
|
|
per_day[day] = {}
|
|
|
|
end
|
|
|
|
table.insert(per_day[day], { id = tonumber(book_id), title = tostring(book_title) })
|
|
|
|
end
|
|
|
|
return per_day
|
|
|
|
end
|
|
|
|
|
2020-07-12 18:47:49 +00:00
|
|
|
function ReaderStatistics:onShowReaderProgress()
|
|
|
|
local readingprogress
|
|
|
|
self:insertDB(self.id_curr_book)
|
|
|
|
local current_period, current_pages = self:getCurrentBookStats()
|
|
|
|
local today_period, today_pages = self:getTodayBookStats()
|
|
|
|
local dates_stats = self:getReadingProgressStats(7)
|
|
|
|
if dates_stats then
|
|
|
|
readingprogress = ReaderProgress:new{
|
|
|
|
dates = dates_stats,
|
|
|
|
current_period = current_period,
|
|
|
|
current_pages = current_pages,
|
|
|
|
today_period = today_period,
|
|
|
|
today_pages = today_pages,
|
|
|
|
--readonly = true,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
UIManager:show(readingprogress)
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderStatistics:onShowBookStats()
|
|
|
|
if self:isDocless() or not self.is_enabled then return end
|
|
|
|
local stats = KeyValuePage:new{
|
|
|
|
title = _("Current statistics"),
|
|
|
|
kv_pairs = self:getCurrentStat(self.id_curr_book),
|
|
|
|
}
|
|
|
|
UIManager:show(stats)
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderStatistics:onShowCalendarView()
|
|
|
|
UIManager:show(self:getCalendarView())
|
|
|
|
end
|
|
|
|
|
2016-02-07 22:03:53 +00:00
|
|
|
return ReaderStatistics
|