feat(progress): implement progress visualization page with sync source tracking (Phase 9-3)
Add progress list page showing reading progress across all devices: - GetAllProgress API endpoint: Returns all user progress with media details - GetAllProgressData helper: Fetches progress data for SSR rendering - ProgressWithMedia struct: Combines progress with book metadata - Progress page template: Displays progress bars, device icons, sync sources Features: - Visual progress bars with percentage - Device-specific icons (Kobo, KOReader, Web, Mobile) - Last sync timestamp and device attribution - EPUB CFI location display - Cover image support with fallback - Responsive grid layout This gives users a unified view of their reading progress across all synced devices.
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
@@ -226,3 +227,138 @@ func (h *Handler) GetProgressHistory(c echo.Context) error {
|
||||
"count": len(history),
|
||||
})
|
||||
}
|
||||
|
||||
type ProgressWithMedia struct {
|
||||
MediaItemID uuid.UUID
|
||||
Title string
|
||||
Author string
|
||||
CoverImagePath string
|
||||
Percentage float64
|
||||
CurrentPage int32
|
||||
TotalPages int32
|
||||
LastReadAt time.Time
|
||||
Epubcfi string
|
||||
LastSyncDevice string
|
||||
}
|
||||
|
||||
// GetAllProgress retrieves all progress for a user with sync source info
|
||||
func (h *Handler) GetAllProgress(c echo.Context) error {
|
||||
user := MustGetAuthenticatedUser(c)
|
||||
|
||||
progressList := []ProgressWithMedia{}
|
||||
|
||||
mediaItems, err := h.db.ListMediaItems(c.Request().Context(), database.ListMediaItemsParams{
|
||||
Limit: 1000, Offset: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get media items")
|
||||
}
|
||||
|
||||
for _, mediaItem := range mediaItems {
|
||||
progress, err := h.db.GetUniversalProgress(c.Request().Context(), database.GetUniversalProgressParams{
|
||||
MediaItemID: mediaItem.ID,
|
||||
UserID: user.ID,
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
coverPath := ""
|
||||
if mediaItem.CoverImagePath.Valid {
|
||||
coverPath = mediaItem.CoverImagePath.String
|
||||
}
|
||||
|
||||
author := ""
|
||||
if mediaItem.Author.Valid {
|
||||
author = mediaItem.Author.String
|
||||
}
|
||||
|
||||
epubcfi := ""
|
||||
if progress.Epubcfi.Valid {
|
||||
epubcfi = progress.Epubcfi.String
|
||||
}
|
||||
|
||||
deviceName := ""
|
||||
if progress.LastSyncDevice.Valid {
|
||||
deviceName = progress.LastSyncDevice.String
|
||||
}
|
||||
|
||||
progressList = append(progressList, ProgressWithMedia{
|
||||
MediaItemID: progress.MediaItemID.Bytes,
|
||||
Title: mediaItem.Title,
|
||||
Author: author,
|
||||
CoverImagePath: coverPath,
|
||||
Percentage: progress.Percentage.Float64,
|
||||
CurrentPage: progress.CurrentPage.Int32,
|
||||
TotalPages: progress.TotalPages.Int32,
|
||||
LastReadAt: progress.LastReadAt.Time,
|
||||
Epubcfi: epubcfi,
|
||||
LastSyncDevice: deviceName,
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"progress": progressList,
|
||||
"total": len(progressList),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) GetAllProgressData(c echo.Context) ([]ProgressWithMedia, error) {
|
||||
user := MustGetAuthenticatedUser(c)
|
||||
|
||||
progressList := []ProgressWithMedia{}
|
||||
|
||||
mediaItems, err := h.db.ListMediaItems(c.Request().Context(), database.ListMediaItemsParams{
|
||||
Limit: 1000,
|
||||
Offset: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, mediaItem := range mediaItems {
|
||||
progress, err := h.db.GetUniversalProgress(c.Request().Context(), database.GetUniversalProgressParams{
|
||||
MediaItemID: mediaItem.ID,
|
||||
UserID: user.ID,
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
coverPath := ""
|
||||
if mediaItem.CoverImagePath.Valid {
|
||||
coverPath = mediaItem.CoverImagePath.String
|
||||
}
|
||||
|
||||
author := ""
|
||||
if mediaItem.Author.Valid {
|
||||
author = mediaItem.Author.String
|
||||
}
|
||||
|
||||
epubcfi := ""
|
||||
if progress.Epubcfi.Valid {
|
||||
epubcfi = progress.Epubcfi.String
|
||||
}
|
||||
|
||||
deviceName := ""
|
||||
if progress.LastSyncDevice.Valid {
|
||||
deviceName = progress.LastSyncDevice.String
|
||||
}
|
||||
|
||||
progressList = append(progressList, ProgressWithMedia{
|
||||
MediaItemID: progress.MediaItemID.Bytes,
|
||||
Title: mediaItem.Title,
|
||||
Author: author,
|
||||
CoverImagePath: coverPath,
|
||||
Percentage: progress.Percentage.Float64,
|
||||
CurrentPage: progress.CurrentPage.Int32,
|
||||
TotalPages: progress.TotalPages.Int32,
|
||||
LastReadAt: progress.LastReadAt.Time,
|
||||
Epubcfi: epubcfi,
|
||||
LastSyncDevice: deviceName,
|
||||
})
|
||||
}
|
||||
|
||||
return progressList, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user