mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
feat(koreader): rebuild the plugin interface and fix four-way annotation sync
Squash of feat/koreader-widget-board. KOReader plugin 2.0.0 gets the widget-board dashboard, redesigned book details, menus, lists and dialogs, Decor page and reading companions. Highlights, notes and bookmarks now stay in step across web, Kobo, KOReader and the phones (defects D-00 to D-20, migration 0121), and reading progress and narration hand off cleanly between devices.
This commit is contained in:
@@ -302,6 +302,144 @@ function Session:readingSummary()
|
||||
}
|
||||
end
|
||||
|
||||
-- Seconds read per local day since `since`, in one grouped query. Each event
|
||||
-- is capped at a day so a reader left open overnight does not count as hours.
|
||||
function Session:dailySeconds(since)
|
||||
local by_day = {}
|
||||
local res = self:select(
|
||||
"SELECT date(start_time, 'unixepoch', 'localtime'), COALESCE(SUM(MIN(duration, 86400)), 0)"
|
||||
.. " FROM page_stat_data WHERE start_time >= ? GROUP BY 1;",
|
||||
{ since })
|
||||
if res then
|
||||
for i = 1, #res[1] do
|
||||
by_day[res[1][i]] = tonumber(res[2][i]) or 0
|
||||
end
|
||||
end
|
||||
return by_day
|
||||
end
|
||||
|
||||
-- Minutes read in each local hour of the day since `since`, as 24 values for
|
||||
-- hours 0 to 23. Each event is capped at ten minutes, as for pace.
|
||||
function Session:hourMinutes(since)
|
||||
local minutes = {}
|
||||
for i = 1, 24 do minutes[i] = 0 end
|
||||
local res = self:select(
|
||||
"SELECT CAST(strftime('%H', start_time, 'unixepoch', 'localtime') AS INTEGER), COALESCE(SUM(MIN(duration, 600)), 0)"
|
||||
.. " FROM page_stat_data WHERE start_time >= ? GROUP BY 1;",
|
||||
{ since })
|
||||
if res then
|
||||
for i = 1, #res[1] do
|
||||
local hour = tonumber(res[1][i])
|
||||
if hour and hour >= 0 and hour < 24 then
|
||||
minutes[hour + 1] = math.floor((tonumber(res[2][i]) or 0) / 60 + 0.5)
|
||||
end
|
||||
end
|
||||
end
|
||||
return minutes
|
||||
end
|
||||
|
||||
-- Every page turn this device has recorded: the total, the turns since
|
||||
-- `today_start`, and when the first was. One pass over the table.
|
||||
function Session:pageTurns(today_start)
|
||||
local res = self:select(
|
||||
"SELECT COUNT(*), COALESCE(SUM(CASE WHEN start_time >= ? THEN 1 ELSE 0 END), 0), MIN(start_time) FROM page_stat_data;",
|
||||
{ today_start })
|
||||
if not res then return { total = 0, today = 0 } end
|
||||
return {
|
||||
total = tonumber(res[1][1]) or 0,
|
||||
today = tonumber(res[2][1]) or 0,
|
||||
first = tonumber(res[3][1]),
|
||||
}
|
||||
end
|
||||
|
||||
-- Reading sessions since `since`: page events grouped into a session until a
|
||||
-- gap of more than ten minutes. Returns the session count, the longest and
|
||||
-- average session in seconds and the pages turned per hour, or nil when
|
||||
-- there is no reading. One pass over the period's events.
|
||||
function Session:sessionRecords(since)
|
||||
local res = self:select([[
|
||||
WITH ev AS (
|
||||
SELECT start_time AS s, MIN(duration, 600) AS d,
|
||||
CASE WHEN start_time - LAG(start_time + MIN(duration, 600)) OVER (ORDER BY start_time) <= 600 THEN 0 ELSE 1 END AS brk
|
||||
FROM page_stat_data WHERE start_time >= ?
|
||||
), grouped AS (
|
||||
SELECT d, SUM(brk) OVER (ORDER BY s ROWS UNBOUNDED PRECEDING) AS g FROM ev
|
||||
), sessions AS (
|
||||
SELECT SUM(d) AS secs, COUNT(*) AS pages FROM grouped GROUP BY g
|
||||
)
|
||||
SELECT COUNT(*), COALESCE(MAX(secs), 0), COALESCE(AVG(secs), 0), COALESCE(SUM(pages), 0), COALESCE(SUM(secs), 0)
|
||||
FROM sessions WHERE secs >= 60;]], { since })
|
||||
if not res then return nil end
|
||||
local count = tonumber(res[1][1]) or 0
|
||||
if count == 0 then return nil end
|
||||
local total = tonumber(res[5][1]) or 0
|
||||
return {
|
||||
sessions = count,
|
||||
longest_seconds = tonumber(res[2][1]) or 0,
|
||||
average_seconds = tonumber(res[3][1]) or 0,
|
||||
pages_per_hour = total > 0 and (tonumber(res[4][1]) or 0) * 3600 / total or 0,
|
||||
}
|
||||
end
|
||||
|
||||
-- How fast one book is being read: seconds per page from its own events, the
|
||||
-- page count it was last read at, and seconds read over the last `days` days.
|
||||
function Session:bookPace(ids, days)
|
||||
if not ids or #ids == 0 then return nil end
|
||||
local marks = {}
|
||||
for _ = 1, #ids do table.insert(marks, "?") end
|
||||
local id_list = table.concat(marks, ",")
|
||||
local totals = self:select(
|
||||
"SELECT COALESCE(SUM(MIN(duration, 600)), 0), COUNT(DISTINCT page), MAX(total_pages)"
|
||||
.. " FROM page_stat_data WHERE id_book IN (" .. id_list .. ");", ids)
|
||||
if not totals then return nil end
|
||||
local seconds = tonumber(totals[1][1]) or 0
|
||||
local pages = tonumber(totals[2][1]) or 0
|
||||
local total_pages = tonumber(totals[3][1]) or 0
|
||||
local params = {}
|
||||
for _, id in ipairs(ids) do table.insert(params, id) end
|
||||
table.insert(params, os.time() - (days or 14) * 86400)
|
||||
local recent = self:select(
|
||||
"SELECT COALESCE(SUM(MIN(duration, 600)), 0) FROM page_stat_data WHERE id_book IN ("
|
||||
.. id_list .. ") AND start_time >= ?;", params)
|
||||
return {
|
||||
seconds_per_page = pages > 0 and seconds / pages or nil,
|
||||
total_pages = total_pages,
|
||||
recent_seconds = recent and tonumber(recent[1][1]) or 0,
|
||||
}
|
||||
end
|
||||
|
||||
-- One book's reading on this device: when it started and was last read, the
|
||||
-- seconds and distinct days spent on it, and the furthest page reached on
|
||||
-- each day (with the page count then), oldest first, for the reading path.
|
||||
-- Events are capped at ten minutes, as for pace.
|
||||
function Session:bookJournal(ids)
|
||||
if not ids or #ids == 0 then return nil end
|
||||
local marks = {}
|
||||
for _ = 1, #ids do table.insert(marks, "?") end
|
||||
local id_list = table.concat(marks, ",")
|
||||
local totals = self:select(
|
||||
"SELECT MIN(start_time), MAX(start_time), COALESCE(SUM(MIN(duration, 600)), 0),"
|
||||
.. " COUNT(DISTINCT date(start_time, 'unixepoch', 'localtime'))"
|
||||
.. " FROM page_stat_data WHERE id_book IN (" .. id_list .. ");", ids)
|
||||
if not totals or not tonumber(totals[1][1]) then return nil end
|
||||
local days = self:select(
|
||||
"SELECT date(start_time, 'unixepoch', 'localtime'), MAX(page), MAX(total_pages)"
|
||||
.. " FROM page_stat_data WHERE id_book IN (" .. id_list .. ") GROUP BY 1 ORDER BY 1;", ids)
|
||||
local path = {}
|
||||
if days then
|
||||
for i = 1, #days[1] do
|
||||
table.insert(path, { day = days[1][i], page = tonumber(days[2][i]) or 0, total = tonumber(days[3][i]) or 0 })
|
||||
end
|
||||
end
|
||||
return {
|
||||
first_read = tonumber(totals[1][1]),
|
||||
last_read = tonumber(totals[2][1]),
|
||||
seconds = tonumber(totals[3][1]) or 0,
|
||||
days = tonumber(totals[4][1]) or 0,
|
||||
path = path,
|
||||
}
|
||||
end
|
||||
|
||||
local function withSession(fn)
|
||||
local session = BookOrbitStatsReader.openSession()
|
||||
if not session then return nil, "open_failed" end
|
||||
@@ -394,6 +532,42 @@ function BookOrbitStatsReader.getReadingSummary()
|
||||
end)
|
||||
end
|
||||
|
||||
function BookOrbitStatsReader.getDailySeconds(since)
|
||||
return withSession(function(session)
|
||||
return session:dailySeconds(since)
|
||||
end)
|
||||
end
|
||||
|
||||
function BookOrbitStatsReader.getHourMinutes(since)
|
||||
return withSession(function(session)
|
||||
return session:hourMinutes(since)
|
||||
end)
|
||||
end
|
||||
|
||||
function BookOrbitStatsReader.getPageTurns(today_start)
|
||||
return withSession(function(session)
|
||||
return session:pageTurns(today_start)
|
||||
end)
|
||||
end
|
||||
|
||||
function BookOrbitStatsReader.getSessionRecords(since)
|
||||
return withSession(function(session)
|
||||
return session:sessionRecords(since)
|
||||
end)
|
||||
end
|
||||
|
||||
function BookOrbitStatsReader.getBookPace(md5, days)
|
||||
return withSession(function(session)
|
||||
return session:bookPace(session:bookIds(md5), days)
|
||||
end)
|
||||
end
|
||||
|
||||
function BookOrbitStatsReader.getBookJournal(md5)
|
||||
return withSession(function(session)
|
||||
return session:bookJournal(session:bookIds(md5))
|
||||
end)
|
||||
end
|
||||
|
||||
function BookOrbitStatsReader.getEventsAfter(ids, watermark, limit)
|
||||
if not ids or #ids == 0 then return {} end
|
||||
return withSession(function(session)
|
||||
|
||||
Reference in New Issue
Block a user