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 ```