I like to track [[Read|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|KOReader user patches]] to achieve that goal. The patch:
- Adds a new footer item that shows `✓ N` where 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()` and `onPosUpdate()` 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_minute` settings 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 (the `MODE` table is local and inaccessible from patches), making it the default visible item
- Force-enables itself on init because saved settings can persist a `false` value across reboots
## Full Source
```lua
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
```