Compare commits

...

1 Commits

13 changed files with 189 additions and 151 deletions

View File

@ -126,12 +126,12 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
} }
statistics := gin.H{ statistics := gin.H{
"TotalTimeLeftSeconds": (document.TotalPages - document.CurrentPage) * document.SecondsPerPage, "TotalTimeLeftSeconds": (document.Pages - document.Page) * document.SecondsPerPage,
"WordsPerMinute": "N/A", "WordsPerMinute": "N/A",
} }
if document.Words != nil && *document.Words != 0 { if document.Words != nil && *document.Words != 0 {
statistics["WordsPerMinute"] = (*document.Words / document.TotalPages * document.ReadPages) / (document.TotalTimeSeconds / 60.0) statistics["WordsPerMinute"] = (*document.Words / document.Pages * document.ReadPages) / (document.TotalTimeSeconds / 60.0)
} }
templateVars["RelBase"] = "../" templateVars["RelBase"] = "../"
@ -513,12 +513,12 @@ func (api *API) identifyDocument(c *gin.Context) {
} }
statistics := gin.H{ statistics := gin.H{
"TotalTimeLeftSeconds": (document.TotalPages - document.CurrentPage) * document.SecondsPerPage, "TotalTimeLeftSeconds": (document.Pages - document.Page) * document.SecondsPerPage,
"WordsPerMinute": "N/A", "WordsPerMinute": "N/A",
} }
if document.Words != nil && *document.Words != 0 { if document.Words != nil && *document.Words != 0 {
statistics["WordsPerMinute"] = (*document.Words / document.TotalPages * document.ReadPages) / (document.TotalTimeSeconds / 60.0) statistics["WordsPerMinute"] = (*document.Words / document.Pages * document.ReadPages) / (document.TotalTimeSeconds / 60.0)
} }
templateVars["Data"] = document templateVars["Data"] = document

View File

@ -22,11 +22,11 @@ import (
) )
type activityItem struct { type activityItem struct {
DocumentID string `json:"document"` DocumentID string `json:"document"`
StartTime int64 `json:"start_time"` StartTime int64 `json:"start_time"`
Duration int64 `json:"duration"` Duration int64 `json:"duration"`
CurrentPage int64 `json:"current_page"` Page int64 `json:"page"`
TotalPages int64 `json:"total_pages"` Pages int64 `json:"pages"`
} }
type requestActivity struct { type requestActivity struct {
@ -256,13 +256,13 @@ func (api *API) addActivities(c *gin.Context) {
// Add All Activity // Add All Activity
for _, item := range rActivity.Activity { for _, item := range rActivity.Activity {
if _, err := qtx.AddActivity(api.DB.Ctx, database.AddActivityParams{ if _, err := qtx.AddActivity(api.DB.Ctx, database.AddActivityParams{
UserID: rUser.(string), UserID: rUser.(string),
DocumentID: item.DocumentID, DocumentID: item.DocumentID,
DeviceID: rActivity.DeviceID, DeviceID: rActivity.DeviceID,
StartTime: time.Unix(int64(item.StartTime), 0).UTC(), StartTime: time.Unix(int64(item.StartTime), 0).UTC(),
Duration: int64(item.Duration), Duration: int64(item.Duration),
CurrentPage: int64(item.CurrentPage), Page: int64(item.Page),
TotalPages: int64(item.TotalPages), Pages: int64(item.Pages),
}); err != nil { }); err != nil {
log.Error("[addActivities] AddActivity DB Error:", err) log.Error("[addActivities] AddActivity DB Error:", err)
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Activity"}) c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Activity"})

0
book_manager.db Normal file
View File

View File

@ -919,8 +919,8 @@ function SyncNinja:getStatisticsActivity(timestamp)
document = rows[1][i], document = rows[1][i],
start_time = tonumber(rows[2][i]), start_time = tonumber(rows[2][i]),
duration = tonumber(rows[3][i]), duration = tonumber(rows[3][i]),
current_page = tonumber(rows[4][i]), page = tonumber(rows[4][i]),
total_pages = tonumber(rows[5][i]) pages = tonumber(rows[5][i])
}) })
end end

View File

@ -9,15 +9,15 @@ import (
) )
type Activity struct { type Activity struct {
ID int64 `json:"id"` ID int64 `json:"id"`
UserID string `json:"user_id"` UserID string `json:"user_id"`
DocumentID string `json:"document_id"` DocumentID string `json:"document_id"`
DeviceID string `json:"device_id"` DeviceID string `json:"device_id"`
StartTime time.Time `json:"start_time"` StartTime time.Time `json:"start_time"`
Duration int64 `json:"duration"` Duration int64 `json:"duration"`
CurrentPage int64 `json:"current_page"` Page int64 `json:"page"`
TotalPages int64 `json:"total_pages"` Pages int64 `json:"pages"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
} }
type Device struct { type Device struct {
@ -85,6 +85,7 @@ type RescaledActivity struct {
DeviceID string `json:"device_id"` DeviceID string `json:"device_id"`
UserID string `json:"user_id"` UserID string `json:"user_id"`
StartTime time.Time `json:"start_time"` StartTime time.Time `json:"start_time"`
Pages int64 `json:"pages"`
Page int64 `json:"page"` Page int64 `json:"page"`
Duration int64 `json:"duration"` Duration int64 `json:"duration"`
} }

View File

@ -141,8 +141,8 @@ INSERT INTO activity (
device_id, device_id,
start_time, start_time,
duration, duration,
current_page, page,
total_pages pages
) )
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
RETURNING *; RETURNING *;
@ -192,15 +192,15 @@ WITH true_progress AS (
start_time AS last_read, start_time AS last_read,
SUM(duration) AS total_time_seconds, SUM(duration) AS total_time_seconds,
document_id, document_id,
current_page, page,
total_pages, pages,
-- Determine Read Pages -- Determine Read Pages
COUNT(DISTINCT current_page) AS read_pages, COUNT(DISTINCT page) AS read_pages,
-- Derive Percentage of Book -- Derive Percentage of Book
ROUND(CAST(current_page AS REAL) / CAST(total_pages AS REAL) * 100, 2) AS percentage ROUND(CAST(page AS REAL) / CAST(pages AS REAL) * 100, 2) AS percentage
FROM activity FROM rescaled_activity
WHERE user_id = $user_id WHERE user_id = $user_id
AND document_id = $document_id AND document_id = $document_id
GROUP BY document_id GROUP BY document_id
@ -210,8 +210,8 @@ WITH true_progress AS (
SELECT SELECT
documents.*, documents.*,
CAST(IFNULL(current_page, 0) AS INTEGER) AS current_page, CAST(IFNULL(page, 0) AS INTEGER) AS page,
CAST(IFNULL(total_pages, 0) AS INTEGER) AS total_pages, CAST(IFNULL(pages, 0) AS INTEGER) AS pages,
CAST(IFNULL(total_time_seconds, 0) AS INTEGER) AS total_time_seconds, CAST(IFNULL(total_time_seconds, 0) AS INTEGER) AS total_time_seconds,
CAST(DATETIME(IFNULL(last_read, "1970-01-01"), time_offset) AS TEXT) AS last_read, CAST(DATETIME(IFNULL(last_read, "1970-01-01"), time_offset) AS TEXT) AS last_read,
CAST(IFNULL(read_pages, 0) AS INTEGER) AS read_pages, CAST(IFNULL(read_pages, 0) AS INTEGER) AS read_pages,
@ -244,9 +244,9 @@ WITH true_progress AS (
start_time AS last_read, start_time AS last_read,
SUM(duration) AS total_time_seconds, SUM(duration) AS total_time_seconds,
document_id, document_id,
current_page, page,
total_pages, pages,
ROUND(CAST(current_page AS REAL) / CAST(total_pages AS REAL) * 100, 2) AS percentage ROUND(CAST(page AS REAL) / CAST(pages AS REAL) * 100, 2) AS percentage
FROM activity FROM activity
WHERE user_id = $user_id WHERE user_id = $user_id
GROUP BY document_id GROUP BY document_id
@ -255,8 +255,8 @@ WITH true_progress AS (
SELECT SELECT
documents.*, documents.*,
CAST(IFNULL(current_page, 0) AS INTEGER) AS current_page, CAST(IFNULL(page, 0) AS INTEGER) AS page,
CAST(IFNULL(total_pages, 0) AS INTEGER) AS total_pages, CAST(IFNULL(pages, 0) AS INTEGER) AS pages,
CAST(IFNULL(total_time_seconds, 0) AS INTEGER) AS total_time_seconds, CAST(IFNULL(total_time_seconds, 0) AS INTEGER) AS total_time_seconds,
CAST(DATETIME(IFNULL(last_read, "1970-01-01"), time_offset) AS TEXT) AS last_read, CAST(DATETIME(IFNULL(last_read, "1970-01-01"), time_offset) AS TEXT) AS last_read,
@ -295,8 +295,8 @@ SELECT
title, title,
author, author,
duration, duration,
current_page, page,
total_pages pages
FROM activity FROM activity
LEFT JOIN documents ON documents.id = activity.document_id LEFT JOIN documents ON documents.id = activity.document_id
LEFT JOIN users ON users.id = activity.user_id LEFT JOIN users ON users.id = activity.user_id
@ -324,8 +324,8 @@ GROUP BY activity.device_id;
-- name: GetDocumentReadStats :one -- name: GetDocumentReadStats :one
SELECT SELECT
count(DISTINCT page) AS pages_read, COUNT(DISTINCT page) AS pages_read,
sum(duration) AS total_time SUM(duration) AS total_time
FROM rescaled_activity FROM rescaled_activity
WHERE document_id = $document_id WHERE document_id = $document_id
AND user_id = $user_id AND user_id = $user_id
@ -333,7 +333,7 @@ AND start_time >= $start_time;
-- name: GetDocumentReadStatsCapped :one -- name: GetDocumentReadStatsCapped :one
WITH capped_stats AS ( WITH capped_stats AS (
SELECT min(sum(duration), CAST($page_duration_cap AS INTEGER)) AS durations SELECT MIN(SUM(duration), CAST($page_duration_cap AS INTEGER)) AS durations
FROM rescaled_activity FROM rescaled_activity
WHERE document_id = $document_id WHERE document_id = $document_id
AND user_id = $user_id AND user_id = $user_id
@ -341,20 +341,20 @@ WITH capped_stats AS (
GROUP BY page GROUP BY page
) )
SELECT SELECT
CAST(count(*) AS INTEGER) AS pages_read, CAST(COUNT(*) AS INTEGER) AS pages_read,
CAST(sum(durations) AS INTEGER) AS total_time CAST(SUM(durations) AS INTEGER) AS total_time
FROM capped_stats; FROM capped_stats;
-- name: GetDocumentDaysRead :one -- name: GetDocumentDaysRead :one
WITH document_days AS ( WITH document_days AS (
SELECT DATE(start_time, time_offset) AS dates SELECT DATE(start_time, time_offset) AS dates
FROM rescaled_activity FROM activity
JOIN users ON users.id = rescaled_activity.user_id JOIN users ON users.id = activity.user_id
WHERE document_id = $document_id WHERE document_id = $document_id
AND user_id = $user_id AND user_id = $user_id
GROUP BY dates GROUP BY dates
) )
SELECT CAST(count(*) AS INTEGER) AS days_read SELECT CAST(COUNT(*) AS INTEGER) AS days_read
FROM document_days; FROM document_days;
-- name: GetUserWindowStreaks :one -- name: GetUserWindowStreaks :one
@ -381,7 +381,7 @@ partitions AS (
), ),
streaks AS ( streaks AS (
SELECT SELECT
count(*) AS streak, COUNT(*) AS streak,
MIN(read_window) AS start_date, MIN(read_window) AS start_date,
MAX(read_window) AS end_date, MAX(read_window) AS end_date,
time_offset time_offset
@ -431,10 +431,10 @@ LIMIT 1;
-- name: GetDatabaseInfo :one -- name: GetDatabaseInfo :one
SELECT SELECT
(SELECT count(rowid) FROM activity WHERE activity.user_id = $user_id) AS activity_size, (SELECT COUNT(rowid) FROM activity WHERE activity.user_id = $user_id) AS activity_size,
(SELECT count(rowid) FROM documents) AS documents_size, (SELECT COUNT(rowid) FROM documents) AS documents_size,
(SELECT count(rowid) FROM document_progress WHERE document_progress.user_id = $user_id) AS progress_size, (SELECT COUNT(rowid) FROM document_progress WHERE document_progress.user_id = $user_id) AS progress_size,
(SELECT count(rowid) FROM devices WHERE devices.user_id = $user_id) AS devices_size (SELECT COUNT(rowid) FROM devices WHERE devices.user_id = $user_id) AS devices_size
LIMIT 1; LIMIT 1;
-- name: GetDailyReadStats :many -- name: GetDailyReadStats :many
@ -448,7 +448,7 @@ WITH RECURSIVE last_30_days AS (
), ),
activity_records AS ( activity_records AS (
SELECT SELECT
sum(duration) AS seconds_read, SUM(duration) AS seconds_read,
DATE(start_time, time_offset) AS day DATE(start_time, time_offset) AS day
FROM activity FROM activity
LEFT JOIN users ON users.id = activity.user_id LEFT JOIN users ON users.id = activity.user_id

View File

@ -19,21 +19,21 @@ INSERT INTO activity (
device_id, device_id,
start_time, start_time,
duration, duration,
current_page, page,
total_pages pages
) )
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
RETURNING id, user_id, document_id, device_id, start_time, duration, current_page, total_pages, created_at RETURNING id, user_id, document_id, device_id, start_time, duration, page, pages, created_at
` `
type AddActivityParams struct { type AddActivityParams struct {
UserID string `json:"user_id"` UserID string `json:"user_id"`
DocumentID string `json:"document_id"` DocumentID string `json:"document_id"`
DeviceID string `json:"device_id"` DeviceID string `json:"device_id"`
StartTime time.Time `json:"start_time"` StartTime time.Time `json:"start_time"`
Duration int64 `json:"duration"` Duration int64 `json:"duration"`
CurrentPage int64 `json:"current_page"` Page int64 `json:"page"`
TotalPages int64 `json:"total_pages"` Pages int64 `json:"pages"`
} }
func (q *Queries) AddActivity(ctx context.Context, arg AddActivityParams) (Activity, error) { func (q *Queries) AddActivity(ctx context.Context, arg AddActivityParams) (Activity, error) {
@ -43,8 +43,8 @@ func (q *Queries) AddActivity(ctx context.Context, arg AddActivityParams) (Activ
arg.DeviceID, arg.DeviceID,
arg.StartTime, arg.StartTime,
arg.Duration, arg.Duration,
arg.CurrentPage, arg.Page,
arg.TotalPages, arg.Pages,
) )
var i Activity var i Activity
err := row.Scan( err := row.Scan(
@ -54,8 +54,8 @@ func (q *Queries) AddActivity(ctx context.Context, arg AddActivityParams) (Activ
&i.DeviceID, &i.DeviceID,
&i.StartTime, &i.StartTime,
&i.Duration, &i.Duration,
&i.CurrentPage, &i.Page,
&i.TotalPages, &i.Pages,
&i.CreatedAt, &i.CreatedAt,
) )
return i, err return i, err
@ -155,8 +155,8 @@ SELECT
title, title,
author, author,
duration, duration,
current_page, page,
total_pages pages
FROM activity FROM activity
LEFT JOIN documents ON documents.id = activity.document_id LEFT JOIN documents ON documents.id = activity.document_id
LEFT JOIN users ON users.id = activity.user_id LEFT JOIN users ON users.id = activity.user_id
@ -181,13 +181,13 @@ type GetActivityParams struct {
} }
type GetActivityRow struct { type GetActivityRow struct {
DocumentID string `json:"document_id"` DocumentID string `json:"document_id"`
StartTime string `json:"start_time"` StartTime string `json:"start_time"`
Title *string `json:"title"` Title *string `json:"title"`
Author *string `json:"author"` Author *string `json:"author"`
Duration int64 `json:"duration"` Duration int64 `json:"duration"`
CurrentPage int64 `json:"current_page"` Page int64 `json:"page"`
TotalPages int64 `json:"total_pages"` Pages int64 `json:"pages"`
} }
func (q *Queries) GetActivity(ctx context.Context, arg GetActivityParams) ([]GetActivityRow, error) { func (q *Queries) GetActivity(ctx context.Context, arg GetActivityParams) ([]GetActivityRow, error) {
@ -211,8 +211,8 @@ func (q *Queries) GetActivity(ctx context.Context, arg GetActivityParams) ([]Get
&i.Title, &i.Title,
&i.Author, &i.Author,
&i.Duration, &i.Duration,
&i.CurrentPage, &i.Page,
&i.TotalPages, &i.Pages,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -238,7 +238,7 @@ WITH RECURSIVE last_30_days AS (
), ),
activity_records AS ( activity_records AS (
SELECT SELECT
sum(duration) AS seconds_read, SUM(duration) AS seconds_read,
DATE(start_time, time_offset) AS day DATE(start_time, time_offset) AS day
FROM activity FROM activity
LEFT JOIN users ON users.id = activity.user_id LEFT JOIN users ON users.id = activity.user_id
@ -290,10 +290,10 @@ func (q *Queries) GetDailyReadStats(ctx context.Context, userID string) ([]GetDa
const getDatabaseInfo = `-- name: GetDatabaseInfo :one const getDatabaseInfo = `-- name: GetDatabaseInfo :one
SELECT SELECT
(SELECT count(rowid) FROM activity WHERE activity.user_id = ?1) AS activity_size, (SELECT COUNT(rowid) FROM activity WHERE activity.user_id = ?1) AS activity_size,
(SELECT count(rowid) FROM documents) AS documents_size, (SELECT COUNT(rowid) FROM documents) AS documents_size,
(SELECT count(rowid) FROM document_progress WHERE document_progress.user_id = ?1) AS progress_size, (SELECT COUNT(rowid) FROM document_progress WHERE document_progress.user_id = ?1) AS progress_size,
(SELECT count(rowid) FROM devices WHERE devices.user_id = ?1) AS devices_size (SELECT COUNT(rowid) FROM devices WHERE devices.user_id = ?1) AS devices_size
LIMIT 1 LIMIT 1
` `
@ -451,13 +451,13 @@ func (q *Queries) GetDocument(ctx context.Context, documentID string) (Document,
const getDocumentDaysRead = `-- name: GetDocumentDaysRead :one const getDocumentDaysRead = `-- name: GetDocumentDaysRead :one
WITH document_days AS ( WITH document_days AS (
SELECT DATE(start_time, time_offset) AS dates SELECT DATE(start_time, time_offset) AS dates
FROM rescaled_activity FROM activity
JOIN users ON users.id = rescaled_activity.user_id JOIN users ON users.id = activity.user_id
WHERE document_id = ?1 WHERE document_id = ?1
AND user_id = ?2 AND user_id = ?2
GROUP BY dates GROUP BY dates
) )
SELECT CAST(count(*) AS INTEGER) AS days_read SELECT CAST(COUNT(*) AS INTEGER) AS days_read
FROM document_days FROM document_days
` `
@ -475,8 +475,8 @@ func (q *Queries) GetDocumentDaysRead(ctx context.Context, arg GetDocumentDaysRe
const getDocumentReadStats = `-- name: GetDocumentReadStats :one const getDocumentReadStats = `-- name: GetDocumentReadStats :one
SELECT SELECT
count(DISTINCT page) AS pages_read, COUNT(DISTINCT page) AS pages_read,
sum(duration) AS total_time SUM(duration) AS total_time
FROM rescaled_activity FROM rescaled_activity
WHERE document_id = ?1 WHERE document_id = ?1
AND user_id = ?2 AND user_id = ?2
@ -503,7 +503,7 @@ func (q *Queries) GetDocumentReadStats(ctx context.Context, arg GetDocumentReadS
const getDocumentReadStatsCapped = `-- name: GetDocumentReadStatsCapped :one const getDocumentReadStatsCapped = `-- name: GetDocumentReadStatsCapped :one
WITH capped_stats AS ( WITH capped_stats AS (
SELECT min(sum(duration), CAST(?1 AS INTEGER)) AS durations SELECT MIN(SUM(duration), CAST(?1 AS INTEGER)) AS durations
FROM rescaled_activity FROM rescaled_activity
WHERE document_id = ?2 WHERE document_id = ?2
AND user_id = ?3 AND user_id = ?3
@ -511,8 +511,8 @@ WITH capped_stats AS (
GROUP BY page GROUP BY page
) )
SELECT SELECT
CAST(count(*) AS INTEGER) AS pages_read, CAST(COUNT(*) AS INTEGER) AS pages_read,
CAST(sum(durations) AS INTEGER) AS total_time CAST(SUM(durations) AS INTEGER) AS total_time
FROM capped_stats FROM capped_stats
` `
@ -546,15 +546,15 @@ WITH true_progress AS (
start_time AS last_read, start_time AS last_read,
SUM(duration) AS total_time_seconds, SUM(duration) AS total_time_seconds,
document_id, document_id,
current_page, page,
total_pages, pages,
-- Determine Read Pages -- Determine Read Pages
COUNT(DISTINCT current_page) AS read_pages, COUNT(DISTINCT page) AS read_pages,
-- Derive Percentage of Book -- Derive Percentage of Book
ROUND(CAST(current_page AS REAL) / CAST(total_pages AS REAL) * 100, 2) AS percentage ROUND(CAST(page AS REAL) / CAST(pages AS REAL) * 100, 2) AS percentage
FROM activity FROM rescaled_activity
WHERE user_id = ?1 WHERE user_id = ?1
AND document_id = ?2 AND document_id = ?2
GROUP BY document_id GROUP BY document_id
@ -564,8 +564,8 @@ WITH true_progress AS (
SELECT SELECT
documents.id, documents.md5, documents.filepath, documents.coverfile, documents.title, documents.author, documents.series, documents.series_index, documents.lang, documents.description, documents.words, documents.gbid, documents.olid, documents.isbn10, documents.isbn13, documents.synced, documents.deleted, documents.updated_at, documents.created_at, documents.id, documents.md5, documents.filepath, documents.coverfile, documents.title, documents.author, documents.series, documents.series_index, documents.lang, documents.description, documents.words, documents.gbid, documents.olid, documents.isbn10, documents.isbn13, documents.synced, documents.deleted, documents.updated_at, documents.created_at,
CAST(IFNULL(current_page, 0) AS INTEGER) AS current_page, CAST(IFNULL(page, 0) AS INTEGER) AS page,
CAST(IFNULL(total_pages, 0) AS INTEGER) AS total_pages, CAST(IFNULL(pages, 0) AS INTEGER) AS pages,
CAST(IFNULL(total_time_seconds, 0) AS INTEGER) AS total_time_seconds, CAST(IFNULL(total_time_seconds, 0) AS INTEGER) AS total_time_seconds,
CAST(DATETIME(IFNULL(last_read, "1970-01-01"), time_offset) AS TEXT) AS last_read, CAST(DATETIME(IFNULL(last_read, "1970-01-01"), time_offset) AS TEXT) AS last_read,
CAST(IFNULL(read_pages, 0) AS INTEGER) AS read_pages, CAST(IFNULL(read_pages, 0) AS INTEGER) AS read_pages,
@ -618,8 +618,8 @@ type GetDocumentWithStatsRow struct {
Deleted bool `json:"-"` Deleted bool `json:"-"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
CurrentPage int64 `json:"current_page"` Page int64 `json:"page"`
TotalPages int64 `json:"total_pages"` Pages int64 `json:"pages"`
TotalTimeSeconds int64 `json:"total_time_seconds"` TotalTimeSeconds int64 `json:"total_time_seconds"`
LastRead string `json:"last_read"` LastRead string `json:"last_read"`
ReadPages int64 `json:"read_pages"` ReadPages int64 `json:"read_pages"`
@ -650,8 +650,8 @@ func (q *Queries) GetDocumentWithStats(ctx context.Context, arg GetDocumentWithS
&i.Deleted, &i.Deleted,
&i.UpdatedAt, &i.UpdatedAt,
&i.CreatedAt, &i.CreatedAt,
&i.CurrentPage, &i.Page,
&i.TotalPages, &i.Pages,
&i.TotalTimeSeconds, &i.TotalTimeSeconds,
&i.LastRead, &i.LastRead,
&i.ReadPages, &i.ReadPages,
@ -722,9 +722,9 @@ WITH true_progress AS (
start_time AS last_read, start_time AS last_read,
SUM(duration) AS total_time_seconds, SUM(duration) AS total_time_seconds,
document_id, document_id,
current_page, page,
total_pages, pages,
ROUND(CAST(current_page AS REAL) / CAST(total_pages AS REAL) * 100, 2) AS percentage ROUND(CAST(page AS REAL) / CAST(pages AS REAL) * 100, 2) AS percentage
FROM activity FROM activity
WHERE user_id = ?1 WHERE user_id = ?1
GROUP BY document_id GROUP BY document_id
@ -733,8 +733,8 @@ WITH true_progress AS (
SELECT SELECT
documents.id, documents.md5, documents.filepath, documents.coverfile, documents.title, documents.author, documents.series, documents.series_index, documents.lang, documents.description, documents.words, documents.gbid, documents.olid, documents.isbn10, documents.isbn13, documents.synced, documents.deleted, documents.updated_at, documents.created_at, documents.id, documents.md5, documents.filepath, documents.coverfile, documents.title, documents.author, documents.series, documents.series_index, documents.lang, documents.description, documents.words, documents.gbid, documents.olid, documents.isbn10, documents.isbn13, documents.synced, documents.deleted, documents.updated_at, documents.created_at,
CAST(IFNULL(current_page, 0) AS INTEGER) AS current_page, CAST(IFNULL(page, 0) AS INTEGER) AS page,
CAST(IFNULL(total_pages, 0) AS INTEGER) AS total_pages, CAST(IFNULL(pages, 0) AS INTEGER) AS pages,
CAST(IFNULL(total_time_seconds, 0) AS INTEGER) AS total_time_seconds, CAST(IFNULL(total_time_seconds, 0) AS INTEGER) AS total_time_seconds,
CAST(DATETIME(IFNULL(last_read, "1970-01-01"), time_offset) AS TEXT) AS last_read, CAST(DATETIME(IFNULL(last_read, "1970-01-01"), time_offset) AS TEXT) AS last_read,
@ -779,8 +779,8 @@ type GetDocumentsWithStatsRow struct {
Deleted bool `json:"-"` Deleted bool `json:"-"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
CurrentPage int64 `json:"current_page"` Page int64 `json:"page"`
TotalPages int64 `json:"total_pages"` Pages int64 `json:"pages"`
TotalTimeSeconds int64 `json:"total_time_seconds"` TotalTimeSeconds int64 `json:"total_time_seconds"`
LastRead string `json:"last_read"` LastRead string `json:"last_read"`
Percentage float64 `json:"percentage"` Percentage float64 `json:"percentage"`
@ -815,8 +815,8 @@ func (q *Queries) GetDocumentsWithStats(ctx context.Context, arg GetDocumentsWit
&i.Deleted, &i.Deleted,
&i.UpdatedAt, &i.UpdatedAt,
&i.CreatedAt, &i.CreatedAt,
&i.CurrentPage, &i.Page,
&i.TotalPages, &i.Pages,
&i.TotalTimeSeconds, &i.TotalTimeSeconds,
&i.LastRead, &i.LastRead,
&i.Percentage, &i.Percentage,
@ -1002,7 +1002,7 @@ partitions AS (
), ),
streaks AS ( streaks AS (
SELECT SELECT
count(*) AS streak, COUNT(*) AS streak,
MIN(read_window) AS start_date, MIN(read_window) AS start_date,
MAX(read_window) AS end_date, MAX(read_window) AS end_date,
time_offset time_offset

View File

@ -110,8 +110,8 @@ CREATE TABLE IF NOT EXISTS activity (
start_time DATETIME NOT NULL, start_time DATETIME NOT NULL,
duration INTEGER NOT NULL, duration INTEGER NOT NULL,
current_page INTEGER NOT NULL, page INTEGER NOT NULL,
total_pages INTEGER NOT NULL, pages INTEGER NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id), FOREIGN KEY (user_id) REFERENCES users (id),
@ -119,6 +119,13 @@ CREATE TABLE IF NOT EXISTS activity (
FOREIGN KEY (device_id) REFERENCES devices (id) FOREIGN KEY (device_id) REFERENCES devices (id)
); );
-- Indexes
CREATE INDEX IF NOT EXISTS activity_start_time ON activity (start_time);
CREATE INDEX IF NOT EXISTS activity_user_id_document_id ON activity (
user_id,
document_id
);
-- Update Trigger -- Update Trigger
CREATE TRIGGER IF NOT EXISTS update_documents_updated_at CREATE TRIGGER IF NOT EXISTS update_documents_updated_at
BEFORE UPDATE ON documents BEGIN BEFORE UPDATE ON documents BEGIN
@ -130,20 +137,21 @@ END;
-- Rescaled Activity View (Adapted from KOReader) -- Rescaled Activity View (Adapted from KOReader)
CREATE VIEW IF NOT EXISTS rescaled_activity AS CREATE VIEW IF NOT EXISTS rescaled_activity AS
WITH RECURSIVE numbers (idx) AS ( WITH RECURSIVE nums (idx) AS (
SELECT 1 AS idx SELECT 1 AS idx
UNION ALL UNION ALL
SELECT idx + 1 SELECT idx + 1
FROM numbers FROM nums
LIMIT 1000 LIMIT 1000
), ),
total_pages AS ( current_pages AS (
SELECT SELECT
document_id, document_id,
total_pages AS pages user_id,
pages
FROM activity FROM activity
GROUP BY document_id GROUP BY document_id, user_id
HAVING MAX(start_time) HAVING MAX(start_time)
ORDER BY start_time DESC ORDER BY start_time DESC
), ),
@ -153,25 +161,50 @@ intermediate AS (
activity.document_id, activity.document_id,
activity.device_id, activity.device_id,
activity.user_id, activity.user_id,
activity.current_page,
activity.total_pages,
total_pages.pages,
activity.start_time, activity.start_time,
activity.duration, activity.duration,
numbers.idx, activity.page,
-- Derive First Page current_pages.pages,
((activity.current_page - 1) * total_pages.pages) / activity.total_pages
-- Derive first page
((activity.page - 1) * current_pages.pages) / activity.pages
+ 1 AS first_page, + 1 AS first_page,
-- Derive Last Page
-- Derive last page
MAX( MAX(
((activity.current_page - 1) * total_pages.pages) ((activity.page - 1) * current_pages.pages)
/ activity.total_pages / activity.pages
+ 1, + 1,
(activity.current_page * total_pages.pages) / activity.total_pages (activity.page * current_pages.pages) / activity.pages
) AS last_page ) AS last_page
FROM activity FROM activity
INNER JOIN total_pages ON total_pages.document_id = activity.document_id INNER JOIN current_pages ON
INNER JOIN numbers ON numbers.idx <= (last_page - first_page + 1) current_pages.document_id = activity.document_id
AND current_pages.user_id = activity.user_id
),
-- Improves performance
num_limit AS (
SELECT * FROM nums
LIMIT (SELECT MAX(last_page - first_page + 1) FROM intermediate)
),
rescaled_raw AS (
SELECT
document_id,
device_id,
user_id,
start_time,
last_page,
pages,
first_page + num_limit.idx - 1 AS page,
duration / (
last_page - first_page + 1.0
) AS duration
FROM intermediate
JOIN num_limit ON
num_limit.idx <= (last_page - first_page + 1)
) )
SELECT SELECT
@ -179,6 +212,13 @@ SELECT
device_id, device_id,
user_id, user_id,
start_time, start_time,
first_page + idx - 1 AS page, pages,
duration / (last_page - first_page + 1) AS duration page,
FROM intermediate;
-- Round up if last page (maintains total duration)
CAST(CASE
WHEN page = last_page AND duration != CAST(duration AS INTEGER)
THEN duration + 1
ELSE duration
END AS INTEGER) AS duration
FROM rescaled_raw;

View File

@ -45,7 +45,7 @@
<p>{{ $activity.Duration }}</p> <p>{{ $activity.Duration }}</p>
</td> </td>
<td class="p-3 border-b border-gray-200"> <td class="p-3 border-b border-gray-200">
<p>{{ $activity.CurrentPage }} / {{ $activity.TotalPages }}</p> <p>{{ $activity.Page }} / {{ $activity.Pages }}</p>
</td> </td>
</tr> </tr>
{{end}} {{end}}

View File

@ -137,7 +137,7 @@
<div> <div>
<p class="text-gray-400">Progress</p> <p class="text-gray-400">Progress</p>
<p class="font-medium text-lg"> <p class="font-medium text-lg">
{{ .Data.CurrentPage }} / {{ .Data.TotalPages }} ({{ .Data.Percentage }}%) {{ .Data.Page }} / {{ .Data.Pages }} ({{ .Data.Percentage }}%)
</p> </p>
</div> </div>
<div> <div>

View File

@ -344,7 +344,7 @@
<div> <div>
<p class="text-gray-500">Progress</p> <p class="text-gray-500">Progress</p>
<p class="font-medium text-lg"> <p class="font-medium text-lg">
{{ .Data.CurrentPage }} / {{ .Data.TotalPages }} ({{ .Data.Percentage }}%) {{ .Data.Page }} / {{ .Data.Pages }} ({{ .Data.Percentage }}%)
</p> </p>
</div> </div>
<!-- <!--

View File

@ -10,8 +10,8 @@
<div class="grid grid-cols-1 gap-4 mb-4 md:grid-cols-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-4 mb-4 md:grid-cols-2 lg:grid-cols-3">
{{range $doc := .Data }} {{range $doc := .Data }}
<div class="w-full relative"> <div class="w-full relative">
<div class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded"> <div class="flex gap-4 w-full h-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded">
<div class="min-w-fit h-48 relative"> <div class="min-w-fit my-auto h-48 relative">
<a href="./documents/{{$doc.ID}}"> <a href="./documents/{{$doc.ID}}">
<img class="rounded object-cover h-full" src="./documents/{{$doc.ID}}/cover"></img> <img class="rounded object-cover h-full" src="./documents/{{$doc.ID}}/cover"></img>
</a> </a>
@ -37,7 +37,7 @@
<div> <div>
<p class="text-gray-400">Progress</p> <p class="text-gray-400">Progress</p>
<p class="font-medium"> <p class="font-medium">
{{ $doc.CurrentPage }} / {{ $doc.TotalPages }} ({{ $doc.Percentage }}%) {{ $doc.Page }} / {{ $doc.Pages }} ({{ $doc.Percentage }}%)
</p> </p>
</div> </div>
</div> </div>

View File

@ -100,7 +100,7 @@
</div> </div>
<div class="grid grid-cols-2 gap-4 my-4 md:grid-cols-4"> <div class="grid grid-cols-2 gap-4 my-4 md:grid-cols-4">
<div class="w-full"> <a href="./documents" class="w-full">
<div <div
class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded" class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded"
> >
@ -111,9 +111,8 @@
<p class="text-sm text-gray-400">Documents</p> <p class="text-sm text-gray-400">Documents</p>
</div> </div>
</div> </div>
</div> </a>
<a href="./activity" class="w-full">
<div class="w-full">
<div <div
class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded" class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded"
> >
@ -124,8 +123,7 @@
<p class="text-sm text-gray-400">Activity Records</p> <p class="text-sm text-gray-400">Activity Records</p>
</div> </div>
</div> </div>
</div> </a>
<div class="w-full"> <div class="w-full">
<div <div
class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded" class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded"
@ -138,7 +136,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="w-full"> <div class="w-full">
<div <div
class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded" class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded"