Properties3
| Type | Project |
| Note created | Mar 16, 2026 |
| Last updated | Mar 18, 2026 |
I like to track the number of pages I read per day, even though it is a wildly inconsistent metric that depends on the format: it’s not the same in a coffee table book or in my Kobo Libra Color. KOReader shows different metrics in the toolbar, but the closest to this one is the total number of pages read vs the total number of pages in the book. I can get the number I want by substracting, but I would prefer to get a number for that.
This note contains the code that is loaded via KOReader user patches to achieve that goal. The patch:
- Adds a new footer item that shows
✓ Nwhere N is unique pages read today - A page counts as “read” when you turn forward past it — opening a book shows 0, going backwards doesn’t count
- On first render, queries the statistics SQLite database directly to seed the count with pages already read earlier today
- Wraps
ReaderFooter.onPageUpdate()andonPosUpdate()to track the previous page in an in-memory set when advancing, updating the counter instantly - Uses a shared set (
pages_today_set) to deduplicate: re-visiting a page you already read today doesn’t increment the counter - Respects the
calendar_day_start_hour/calendar_day_start_minutesettings from the Statistics plugin (configurable in Reading Statistics > Calendar > “Daily timeline starts at”), so reading at 2 AM counts as the previous day if set to e.g. 05:00 - Resets automatically when a new day starts (based on the configured day boundary)
- Monkey-patches
ReaderFooter.init()to inject the mode at position 1 in the footer’s mode list (theMODEtable is local and inaccessible from patches), making it the default visible item - Force-enables itself on init because saved settings can persist a
falsevalue across reboots
Full Source
local ReaderFooter = require("apps/reader/modules/readerfooter")
local SQ3 = require("lua-ljsqlite3/init")
local DataStorage = require("datastorage")
local MODE_NAME = "session_pages"
local MODE_PREFIX = "✓"
local pages_today_set = {}
local pages_today_count = 0
local db_loaded = false
local loaded_day = nil
local function getStartOfDay()
local day_start_hour = 0
local day_start_minute = 0
local stats_settings = G_reader_settings:readSetting("statistics")
if stats_settings then
day_start_hour = stats_settings.calendar_day_start_hour or 0
day_start_minute = stats_settings.calendar_day_start_minute or 0
end
local now = os.time()
local now_t = os.date("*t")
local seconds_since_midnight = now_t.hour * 3600 + now_t.min * 60 + now_t.sec
local day_start_offset = day_start_hour * 3600 + day_start_minute * 60
local start_of_day = now - seconds_since_midnight + day_start_offset
if seconds_since_midnight < day_start_offset then
start_of_day = start_of_day - 86400
end
return start_of_day
end
local function getCurrentDay()
local start = getStartOfDay()
return os.date("%Y-%m-%d", start)
end
local function resetState()
pages_today_set = {}
pages_today_count = 0
db_loaded = false
loaded_day = nil
end
local function loadTodayPagesFromDB()
local db_path = DataStorage:getSettingsDir() .. "/statistics.sqlite3"
local ok, conn = pcall(SQ3.open, db_path)
if not ok or not conn then return end
local start_of_day = getStartOfDay()
local sql = string.format([[
SELECT id_book, page
FROM page_stat
WHERE start_time >= %d
GROUP BY id_book, page
]], start_of_day)
local rows = conn:exec(sql)
if rows then
local id_books = rows[1] or {}
local pages = rows[2] or {}
for i = 1, #id_books do
local key = tostring(id_books[i]) .. ":" .. tostring(pages[i])
if not pages_today_set[key] then
pages_today_set[key] = true
pages_today_count = pages_today_count + 1
end
end
end
conn:close()
end
local function ensureLoaded(footer)
local today = getCurrentDay()
if loaded_day and loaded_day ~= today then
resetState()
end
if not db_loaded then
local stats = footer.ui and footer.ui.statistics
if stats then stats:insertDB() end
loadTodayPagesFromDB()
db_loaded = true
loaded_day = today
end
end
local function trackPage(self, pageno)
if not pageno then return end
local prev = self.pageno
if not prev or pageno <= prev then return end
local stats = self.ui and self.ui.statistics
if not stats or not stats.id_curr_book then return end
local key = tostring(stats.id_curr_book) .. ":" .. tostring(prev)
if not pages_today_set[key] then
pages_today_set[key] = true
pages_today_count = pages_today_count + 1
end
end
ReaderFooter.textGeneratorMap[MODE_NAME] = function(footer)
ensureLoaded(footer)
if footer.settings.all_at_once
and footer.settings.hide_empty_generators
and pages_today_count == 0 then
return ""
end
return MODE_PREFIX .. " " .. tostring(pages_today_count)
end
ReaderFooter.default_settings[MODE_NAME] = true
local orig_onPageUpdate = ReaderFooter.onPageUpdate
ReaderFooter.onPageUpdate = function(self, pageno, ...)
trackPage(self, pageno)
return orig_onPageUpdate(self, pageno, ...)
end
local orig_onPosUpdate = ReaderFooter.onPosUpdate
ReaderFooter.onPosUpdate = function(self, pos, pageno, ...)
trackPage(self, pageno)
return orig_onPosUpdate(self, pos, pageno, ...)
end
local orig_init = ReaderFooter.init
ReaderFooter.init = function(self, ...)
orig_init(self, ...)
local already_present = false
for _, name in pairs(self.mode_index) do
if name == MODE_NAME then
already_present = true
break
end
end
if not already_present then
for i = self.mode_nb, 1, -1 do
self.mode_index[i] = self.mode_index[i - 1]
end
self.mode_index[1] = MODE_NAME
self.mode_nb = self.mode_nb + 1
self.mode_list = {}
for i = 0, self.mode_nb - 1 do
self.mode_list[self.mode_index[i]] = i
end
end
self.settings[MODE_NAME] = true
self:updateFooterTextGenerator()
end
local orig_textOptionTitles = ReaderFooter.textOptionTitles
ReaderFooter.textOptionTitles = function(self, option, ...)
if option == MODE_NAME then
return "Pages read today (" .. MODE_PREFIX .. ")"
end
return orig_textOptionTitles(self, option, ...)
end