local BookStatusWidget = require ( " ui/widget/bookstatuswidget " )
local ConfirmBox = require ( " ui/widget/confirmbox " )
local DataStorage = require ( " datastorage " )
local Device = require ( " device " )
local DocSettings = require ( " docsettings " )
local InfoMessage = require ( " ui/widget/infomessage " )
local KeyValuePage = require ( " ui/widget/keyvaluepage " )
local MultiInputDialog = require ( " ui/widget/multiinputdialog " )
local ReaderFooter = require ( " apps/reader/modules/readerfooter " )
local ReaderProgress = require ( " readerprogress " )
local ReadHistory = require ( " readhistory " )
local Screensaver = require ( " ui/screensaver " )
local SQ3 = require ( " lua-ljsqlite3/init " )
local TimeVal = require ( " ui/timeval " )
local UIManager = require ( " ui/uimanager " )
local Widget = require ( " ui/widget/widget " )
local lfs = require ( " libs/libkoreader-lfs " )
local logger = require ( " logger " )
local util = require ( " util " )
local _ = require ( " gettext " )
local joinPath = require ( " ffi/util " ) . joinPath
local Screen = require ( " device " ) . screen
local T = require ( " ffi/util " ) . template
local statistics_dir = DataStorage : getDataDir ( ) .. " /statistics/ "
local db_location = DataStorage : getSettingsDir ( ) .. " /statistics.sqlite3 "
local PAGE_INSERT = 50
local DEFAULT_MIN_READ_SEC = 5
local DEFAULT_MAX_READ_SEC = 120
local ReaderStatistics = Widget : extend {
name = " statistics " ,
page_min_read_sec = DEFAULT_MIN_READ_SEC ,
page_max_read_sec = DEFAULT_MAX_READ_SEC ,
start_current_period = 0 ,
curr_page = 0 ,
id_curr_book = nil ,
curr_total_time = 0 ,
curr_total_pages = 0 ,
is_enabled = nil ,
convert_to_db = nil ,
total_read_pages = 0 ,
total_read_time = 0 ,
avg_time = nil ,
pages_stats = { } ,
data = {
title = " " ,
authors = " " ,
language = " " ,
series = " " ,
performance_in_pages = { } ,
total_time_in_sec = 0 ,
highlights = 0 ,
notes = 0 ,
pages = 0 ,
md5 = nil ,
} ,
}
local shortDayOfWeekTranslation = {
[ " Mon " ] = _ ( " Mon " ) ,
[ " Tue " ] = _ ( " Tue " ) ,
[ " Wed " ] = _ ( " Wed " ) ,
[ " Thu " ] = _ ( " Thu " ) ,
[ " Fri " ] = _ ( " Fri " ) ,
[ " Sat " ] = _ ( " Sat " ) ,
[ " Sun " ] = _ ( " Sun " ) ,
}
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 " ) ,
}
function ReaderStatistics : isDocless ( )
return self.ui == nil or self.ui . document == nil
end
function ReaderStatistics : init ( )
if not self : isDocless ( ) and self.ui . document.is_pic then
return
end
self.start_current_period = TimeVal : now ( ) . sec
self.pages_stats = { }
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.convert_to_db = settings.convert_to_db
self.ui . menu : registerToMainMenu ( self )
self : checkInitDatabase ( )
BookStatusWidget.getStats = function ( )
return self : getStatsBookStatus ( self.id_curr_book , self.is_enabled )
end
ReaderFooter.getAvgTimePerPage = function ( )
if self.is_enabled then
return self.avg_time
end
end
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
local ReaderGesture = require ( " apps/reader/modules/readergesture " )
ReaderGesture.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
ReaderGesture.getBookStats = function ( )
if self : isDocless ( ) or not self.is_enabled then return end
local stats = KeyValuePage : new {
title = _ ( " Statistics " ) ,
kv_pairs = self : getCurrentStat ( self.id_curr_book ) ,
}
return stats
end
end
function ReaderStatistics : initData ( )
if self : isDocless ( ) or not self.is_enabled then
return
end
-- first execution
if not self.data then
self.data = { performance_in_pages = { } }
end
local book_properties = self : getBookProperties ( )
self.data . title = book_properties.title
if self.data . title == nil or self.data . title == " " then
self.data . title = self.document . file : match ( " ^.+/(.+)$ " )
end
self.data . authors = book_properties.authors
self.data . language = book_properties.language
self.data . series = book_properties.series
self.data . pages = self.view . document : getPageCount ( )
self.data . md5 = self : partialMd5 ( self.document . file )
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
self.avg_time = math.ceil ( self.total_read_time / self.total_read_pages )
end
end
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
function ReaderStatistics : checkInitDatabase ( )
local conn = SQ3.open ( db_location )
if self.convert_to_db then -- if conversion to sqlite was doing earlier
if not conn : exec ( " pragma table_info('book'); " ) then
UIManager : show ( ConfirmBox : new {
text = T ( _ ( [ [
Cannot open database in % 1.
The database may have been moved or deleted .
Do you want to create an empty database ?
] ] ) ,
db_location ) ,
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
else -- first time convertion to sqlite database
self.convert_to_db = true
if not conn : exec ( " pragma table_info('book'); " ) then
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
local info = InfoMessage : new {
text = _ ( [ [
New version of statistics plugin detected .
Statistics data needs to be converted into the new database format .
This may take a few minutes .
Please wait …
] ] ) }
UIManager : show ( info )
UIManager : forceRePaint ( )
local nr_book = self : migrateToDB ( conn )
UIManager : close ( info )
UIManager : forceRePaint ( )
UIManager : show ( InfoMessage : new {
text = T ( _ ( " Conversion completed. \n Imported %1 books to database. \n Tap to continue. " ) , nr_book ) } )
else
self : createDB ( conn )
end
end
self : saveSettings ( )
end
conn : close ( )
end
function ReaderStatistics : partialMd5 ( file )
if file == nil then
return nil
end
local bit = require ( " bit " )
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
end
return m : sum ( )
end
function ReaderStatistics : createDB ( conn )
-- Make it WAL, if possible
if Device : canUseWAL ( ) then
conn : exec ( " PRAGMA journal_mode=WAL; " )
else
conn : exec ( " PRAGMA journal_mode=TRUNCATE; " )
end
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
(
id_book integer ,
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
) ;
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 ) ;
] ]
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 )
if avg_time > self.page_max_read_sec then
avg_time = self.page_max_read_sec
end
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
local book_stats = { performance_in_pages = { } }
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
book_stats.total_time_in_sec = old_data.total_time
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
--- @fixme Sometimes crengine returns "No document", try one more time.
props = self.view . document : getProps ( )
end
return props
end
function ReaderStatistics : getStatisticEnabledMenuItem ( )
return {
text = _ ( " Enabled " ) ,
checked_func = function ( ) return self.is_enabled end ,
callback = function ( )
-- if was enabled, have to save data to file
if self.is_enabled and not self : isDocless ( ) then
self : insertDB ( self.id_curr_book )
self.ui . doc_settings : saveSetting ( " stats " , self.data )
end
self.is_enabled = not self.is_enabled
-- if was disabled have to get data from db
if self.is_enabled and not self : isDocless ( ) then
self : initData ( )
self.pages_stats = { }
self.start_current_period = TimeVal : now ( ) . sec
if self.document . info.has_pages then
self.curr_page = self.ui . paging.current_page
else
self.curr_page = self.document : getCurrentPage ( )
end
self.pages_stats [ self.start_current_period ] = self.curr_page
end
self : saveSettings ( )
if not self : isDocless ( ) then
self.view . footer : updateFooter ( )
end
end ,
}
end
function ReaderStatistics : updateSettings ( )
self.settings_dialog = MultiInputDialog : new {
title = _ ( " Statistics settings " ) ,
fields = {
{
text = self.page_min_read_sec ,
description = T ( _ ( " Min seconds, default is %1 " ) , DEFAULT_MIN_READ_SEC ) ,
input_type = " number " ,
} ,
{
text = self.page_max_read_sec ,
description = T ( _ ( " Max seconds, default is %1 " ) , DEFAULT_MAX_READ_SEC ) ,
input_type = " number " ,
} ,
} ,
buttons = {
{
{
text = _ ( " Cancel " ) ,
callback = function ( )
self.settings_dialog : onClose ( )
UIManager : close ( self.settings_dialog )
end
} ,
{
text = _ ( " Apply " ) ,
callback = function ( )
self : saveSettings ( MultiInputDialog : getFields ( ) )
self.settings_dialog : onClose ( )
UIManager : close ( self.settings_dialog )
end
} ,
} ,
} ,
width = Screen : getWidth ( ) * 0.95 ,
height = Screen : getHeight ( ) * 0.2 ,
input_type = " number " ,
}
UIManager : show ( self.settings_dialog )
self.settings_dialog : onShowKeyboard ( )
end
function ReaderStatistics : addToMainMenu ( menu_items )
menu_items.statistics = {
text = _ ( " Reading statistics " ) ,
sub_item_table = {
self : getStatisticEnabledMenuItem ( ) ,
{
text = _ ( " Settings " ) ,
keep_menu_open = true ,
callback = function ( ) self : updateSettings ( ) end ,
} ,
{
text = _ ( " Reset book statistics " ) ,
keep_menu_open = true ,
callback = function ( )
self : resetBook ( )
end
} ,
{
text = _ ( " Current book " ) ,
keep_menu_open = true ,
callback = function ( )
UIManager : show ( KeyValuePage : new {
title = _ ( " Statistics " ) ,
kv_pairs = self : getCurrentStat ( self.id_curr_book ) ,
} )
end ,
enabled_func = function ( ) return not self : isDocless ( ) and self.is_enabled end ,
} ,
{
text = _ ( " Reading progress " ) ,
keep_menu_open = true ,
callback = function ( )
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
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. \n No data from last %1 days. " ) , 7 ) } )
end
end
} ,
{
text = _ ( " Time range " ) ,
keep_menu_open = true ,
callback = function ( )
self : statMenu ( )
end
} ,
} ,
}
end
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 ,
value_align = " right " ,
kv_pairs = kv_pairs ,
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
end ,
} ,
" ---- " ,
{ _ ( " Last week " ) , " " ,
callback = function ( )
local kv = self.kv
UIManager : close ( self.kv )
self.kv = KeyValuePage : new {
title = _ ( " Last week " ) ,
value_overflow_align = " right " ,
kv_pairs = self : getDatesFromAll ( 7 , " daily_weekday " ) ,
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
end ,
} ,
{ _ ( " Last month by day " ) , " " ,
callback = function ( )
local kv = self.kv
UIManager : close ( self.kv )
self.kv = KeyValuePage : new {
title = _ ( " Last month by day " ) ,
value_overflow_align = " right " ,
kv_pairs = self : getDatesFromAll ( 30 , " daily_weekday " ) ,
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
end ,
} ,
{ _ ( " Last year by day " ) , " " ,
callback = function ( )
local kv = self.kv
UIManager : close ( self.kv )
self.kv = KeyValuePage : new {
title = _ ( " Last year by day " ) ,
value_overflow_align = " right " ,
kv_pairs = self : getDatesFromAll ( 365 , " daily " ) ,
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
end ,
} ,
{ _ ( " Last year by week " ) , " " ,
callback = function ( )
local kv = self.kv
UIManager : close ( self.kv )
self.kv = KeyValuePage : new {
title = _ ( " Last year by week " ) ,
value_overflow_align = " right " ,
kv_pairs = self : getDatesFromAll ( 365 , " weekly " ) ,
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
end ,
} ,
{ _ ( " All stats by month " ) , " " ,
callback = function ( )
local kv = self.kv
UIManager : close ( self.kv )
self.kv = KeyValuePage : new {
title = _ ( " All stats by month " ) ,
value_overflow_align = " right " ,
kv_pairs = self : getDatesFromAll ( 0 , " monthly " ) ,
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
end ,
} ,
" ---- " ,
{ _ ( " Books by week " ) , " " ,
callback = function ( )
local kv = self.kv
UIManager : close ( self.kv )
self.kv = KeyValuePage : new {
title = _ ( " Books by week " ) ,
value_overflow_align = " right " ,
kv_pairs = self : getDatesFromAll ( 0 , " weekly " , true ) ,
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
end ,
} ,
{ _ ( " Books by month " ) , " " ,
callback = function ( )
local kv = self.kv
UIManager : close ( self.kv )
self.kv = KeyValuePage : new {
title = _ ( " Books by month " ) ,
value_overflow_align = " right " ,
kv_pairs = self : getDatesFromAll ( 0 , " monthly " , true ) ,
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
end ,
}
}
}
UIManager : show ( self.kv )
end
function ReaderStatistics : getTodayBookStats ( )
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
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 today_pages , today_period = conn : rowexec ( string.format ( sql_stmt , start_today_time ) )
if today_pages == nil then
today_pages = 0
end
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
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
end
if current_period == nil then
current_period = 0
end
current_period = tonumber ( current_period )
current_pages = tonumber ( current_pages )
return current_period , current_pages
end
function ReaderStatistics : getCurrentStat ( id_book )
if id_book == nil then
return
end
self : insertDB ( id_book )
local today_period , today_pages = self : getTodayBookStats ( )
local current_period , current_pages = self : getCurrentBookStats ( )
local conn = SQ3.open ( db_location )
local notes , highlights = conn : rowexec ( string.format ( " SELECT notes, highlights FROM book WHERE id = '%s';) " , id_book ) )
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 ) )
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_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
self.data . pages = self.view . document : getPageCount ( )
total_time_book = tonumber ( total_time_book )
total_read_pages = tonumber ( total_read_pages )
local avg_time_per_page = total_time_book / total_read_pages
local time_to_read = ( self.data . pages - self.view . state.page ) * avg_time_per_page
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 ) )
return {
{ _ ( " Current period " ) , util.secondsToClock ( current_period , false ) } ,
{ _ ( " Current pages " ) , tonumber ( current_pages ) } ,
{ _ ( " Today period " ) , util.secondsToClock ( today_period , false ) } ,
{ _ ( " Today pages " ) , tonumber ( today_pages ) } ,
{ _ ( " Time to read " ) , util.secondsToClock ( time_to_read ) , false } ,
{ _ ( " Total time " ) , util.secondsToClock ( total_time_book , false ) } ,
{ _ ( " Total highlights " ) , tonumber ( highlights ) } ,
{ _ ( " Total notes " ) , tonumber ( notes ) } ,
{ _ ( " Total days " ) , tonumber ( total_days ) } ,
{ _ ( " Average time per page " ) , util.secondsToClock ( avg_time_per_page , false ) } ,
{ _ ( " Read pages/Total pages " ) , total_read_pages .. " / " .. self.data . pages } ,
-- adding 0.5 rounds to nearest integer with math.floor
{ _ ( " Percentage completed " ) , math.floor ( total_read_pages / self.data . pages * 100 + 0.5 ) .. " % " } ,
{ _ ( " Average time per day " ) , util.secondsToClock ( total_time_book / tonumber ( total_days ) ) , false } ,
{ _ ( " Estimated reading finished " ) ,
--- @todo Plurals for translations.
-- @translators Estimated reading finished date (%1), estimated remaining days to read (%2). Please see https://github.com/koreader/koreader/issues/5249 to track proper plural translation.
T ( _ ( " %1 (%2 day(s)) " ) , estimate_end_of_read_date , estimate_days_to_read ) } ,
}
end
function ReaderStatistics : getBookStat ( id_book )
if id_book == nil then
return
end
local conn = SQ3.open ( db_location )
local sql_stmt = [ [
SELECT title , authors , notes , highlights , pages , last_open
FROM book
WHERE id = ' %s '
] ]
local title , authors , notes , highlights , pages , last_open = conn : rowexec ( string.format ( sql_stmt , id_book ) )
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 ) ,
count ( DISTINCT page )
FROM page_stat
WHERE id_book = ' %s '
] ]
local total_time_book , total_read_pages = conn : rowexec ( string.format ( sql_stmt , id_book ) )
sql_stmt = [ [
SELECT min ( start_time )
FROM page_stat
WHERE id_book = ' %s '
] ]
local first_open = conn : rowexec ( string.format ( sql_stmt , id_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
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 } ,
{ _ ( " First opened " ) , os.date ( " %Y-%m-%d (%H:%M) " , tonumber ( first_open ) ) } ,
{ _ ( " Last opened " ) , os.date ( " %Y-%m-%d (%H:%M) " , tonumber ( last_open ) ) } ,
{ _ ( " Total time " ) , util.secondsToClock ( total_time_book , false ) } ,
{ _ ( " Total highlights " ) , tonumber ( highlights ) } ,
{ _ ( " Total notes " ) , tonumber ( notes ) } ,
{ _ ( " Total days " ) , tonumber ( total_days ) } ,
{ _ ( " Average time per page " ) , util.secondsToClock ( avg_time_per_page , false ) } ,
{ _ ( " Read pages/Total pages " ) , total_read_pages .. " / " .. pages } ,
-- adding 0.5 rounds to nearest integer with math.floor
{ _ ( " Percentage completed " ) , math.floor ( total_read_pages / pages * 100 + 0.5 ) .. " % " } ,
" ---- " ,
{ _ ( " Show days " ) , _ ( " Tap to display " ) ,
callback = function ( )
local kv = self.kv
UIManager : close ( self.kv )
self.kv = KeyValuePage : new {
title = _ ( " Read in days " ) ,
value_overflow_align = " right " ,
kv_pairs = self : getDatesForBook ( id_book ) ,
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
end ,
}
}
end
local function sqlDaily ( )
return
[ [
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
] ]
end
local function sqlWeekly ( )
return
[ [
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
] ]
end
local function sqlMonthly ( )
return
[ [
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
WHERE start_time >= ' %s '
GROUP BY id_book , page , dates
)
GROUP BY dates
ORDER BY dates DESC
] ]
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 {
title = T ( _ ( " Books from: %1 " ) , date_text ) ,
value_align = " right " ,
kv_pairs = self : getBooksFromPeriod ( begin , finish ) ,
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
else
self.kv = KeyValuePage : new {
title = date_text ,
value_align = " right " ,
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 {
title = T ( _ ( " Books from: %1 " ) , date_text ) ,
value_align = " right " ,
kv_pairs = self : getBooksFromPeriod ( begin , finish ) ,
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
else
self.kv = KeyValuePage : new {
title = date_text ,
value_align = " right " ,
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 ,
value_align = " right " ,
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 ( )
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
timestamp = tonumber ( result_book [ 4 ] [ i ] )
if ptype == " daily_weekday " then
date_text = string.format ( " %s (%s) " ,
os.date ( " %Y-%m-%d " , timestamp ) ,
shortDayOfWeekTranslation [ os.date ( " %a " , timestamp ) ] )
elseif ptype == " daily " then
date_text = result_book [ 1 ] [ i ]
elseif ptype == " weekly " then
date_text = T ( _ ( " %1 Week %2 " ) , os.date ( " %Y " , timestamp ) , os.date ( " %W " , timestamp ) )
elseif ptype == " monthly " then
date_text = monthTranslation [ os.date ( " %B " , timestamp ) ] .. os.date ( " %Y " , timestamp )
else
date_text = result_book [ 1 ] [ i ]
end
if ptype == " monthly " then
year_begin = tonumber ( os.date ( " %Y " , timestamp ) )
month_begin = tonumber ( os.date ( " %m " , timestamp ) )
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 ( )
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 )
end ,
} )
else
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 ) )
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 : callbackDaily ( begin_day , begin_day + 86400 , date_text )
end ,
} )
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
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 }
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 ) ) ,
callback = function ( )
local kv = self.kv
UIManager : close ( kv )
self.kv = KeyValuePage : new {
title = T ( _ ( " Books in %1 " ) , result_book [ 1 ] [ i ] ) ,
value_overflow_align = " right " ,
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
function ReaderStatistics : getBooksFromPeriod ( period_begin , period_end )
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 )
self.kv = KeyValuePage : new {
title = _ ( " Read in days " ) ,
value_overflow_align = " right " ,
kv_pairs = self : getDatesForBook ( tonumber ( result_book [ 4 ] [ i ] ) ) ,
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
end ,
} )
end
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 ) )
if not result_book then return end
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
function ReaderStatistics : getDatesForBook ( id_book )
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
end
function ReaderStatistics : getTotalStats ( )
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
local total_stats = { }
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
end
table.insert ( total_stats , {
book_title ,
util.secondsToClock ( total_time_book , false ) ,
callback = function ( )
local kv = self.kv
UIManager : close ( self.kv )
self.kv = KeyValuePage : new {
title = book_title ,
value_overflow_align = " right " ,
kv_pairs = self : getBookStat ( id_book ) ,
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
end ,
} )
end
conn : close ( )
return T ( _ ( " Total hours read %1 " ) , util.secondsToClock ( total_books_time , false ) ) , total_stats
end
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
end
if id_book ~= self.id_curr_book then
table.insert ( total_stats , {
book_title ,
util.secondsToClock ( total_time_book , false ) ,
id_book ,
callback = function ( )
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 ,
} )
end ,
} )
end
end
kv_reset_book = KeyValuePage : new {
title = _ ( " Reset book statistics " ) ,
value_align = " right " ,
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 ( )
end
function ReaderStatistics : onPosUpdate ( pos , pageno )
if self.curr_page ~= pageno then
self : onPageUpdate ( pageno )
end
end
function ReaderStatistics : onPageUpdate ( pageno )
if self : isDocless ( ) or not self.is_enabled then
return
end
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
end
-- 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
end
if self.total_read_pages > 0 or mem_read_pages > 0 then
self.avg_time = math.ceil ( ( self.total_read_time + mem_read_time ) / ( self.total_read_pages + mem_read_pages ) )
end
end
-- For backward compatibility
function ReaderStatistics : importFromFile ( base_path , item )
item = string.gsub ( item , " ^%s*(.-)%s*$ " , " %1 " ) -- trim
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
end
end
function ReaderStatistics : onCloseDocument ( )
if not self : isDocless ( ) and self.is_enabled then
self.ui . doc_settings : saveSetting ( " stats " , self.data )
self : insertDB ( self.id_curr_book )
end
end
function ReaderStatistics : onAddHighlight ( )
self.data . highlights = self.data . highlights + 1
return true
end
function ReaderStatistics : onDelHighlight ( )
if self.data . highlights > 0 then
self.data . highlights = self.data . highlights - 1
end
return true
end
function ReaderStatistics : onAddNote ( )
self.data . notes = self.data . notes + 1
end
function ReaderStatistics : onSaveSettings ( )
self : saveSettings ( )
if not self : isDocless ( ) then
self.ui . doc_settings : saveSetting ( " stats " , self.data )
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 )
end
end
-- screensaver off
function ReaderStatistics : onResume ( )
self.start_current_period = TimeVal : now ( ) . sec
self.pages_stats = { }
self.pages_stats [ self.start_current_period ] = self.curr_page
end
function ReaderStatistics : saveSettings ( 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 ,
convert_to_db = self.convert_to_db
}
G_reader_settings : saveSetting ( " statistics " , settings )
end
function ReaderStatistics : onReadSettings ( config )
self.data = config.data . stats
end
function ReaderStatistics : onReaderReady ( )
-- we have correct page count now, do the actual initialization work
self : initData ( )
self.view . footer : updateFooter ( )
end
return ReaderStatistics