Update all handler functions to use *echo.Context (pointer) instead of echo.Context (value) as required by Echo v5. Changes across all handler files: - analytics.go: Update handler signatures - auth.go: Update authentication handler signatures - book_matching.go: Update matching handler signatures - collections.go: Update collection handler signatures - collections_preview_test.go: Update test signatures - commonhandlers.go: Update common handler signatures - conflicts.go: Update conflict handler signatures - context.go: Update context handler signatures - dashboard.go: Update dashboard handler signatures - devices.go: Update device handler signatures - jobs.go: Update job handler signatures - kobo.go: Update Kobo handler signatures - koreader.go: Update Koreader handler signatures - library.go: Update library handler signatures - matching.go: Update matching handler signatures - media.go: Update media handler signatures - opds.go: Update OPDS handler signatures - progress.go: Update progress handler signatures - queue.go: Update queue handler signatures - refresh_token.go: Update token handler signatures - scanner.go: Update scanner handler signatures - sidecar.go: Update sidecar handler signatures - sync.go: Update sync handler signatures - system_settings.go: Update settings handler signatures - websocket.go: Update WebSocket handler signatures All handlers now properly implement Echo v5's pointer-based context pattern. This change is necessary for type safety and compatibility with Echo v5's improved context handling and WebSocket support.
239 lines
7.2 KiB
Go
239 lines
7.2 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bookhoard/internal/database"
|
|
"bookhoard/internal/services"
|
|
"bookhoard/internal/utils"
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/labstack/echo/v5"
|
|
)
|
|
|
|
type DashboardHandler struct {
|
|
db *database.Queries
|
|
dashboardService *services.DashboardService
|
|
}
|
|
|
|
func NewDashboardHandler(db *database.Queries) *DashboardHandler {
|
|
return &DashboardHandler{
|
|
db: db,
|
|
dashboardService: services.NewDashboardService(db),
|
|
}
|
|
}
|
|
|
|
func (h *DashboardHandler) GetSections(c *echo.Context) error {
|
|
user := c.Get("user").(database.Users)
|
|
userUUID := uuid.UUID(user.ID.Bytes)
|
|
|
|
libraryID := c.QueryParam("library_id")
|
|
if libraryID == "" {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "library_id required"})
|
|
}
|
|
libUUID, err := uuid.Parse(libraryID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"})
|
|
}
|
|
|
|
prefs, _ := h.dashboardService.GetDashboardPreferences(c.Request().Context(), userUUID, libUUID)
|
|
|
|
limit := 20
|
|
if prefs.ItemsPerSection.Valid {
|
|
limit = int(prefs.ItemsPerSection.Int32)
|
|
}
|
|
if limitStr := c.QueryParam("limit"); limitStr != "" {
|
|
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
|
|
limit = l
|
|
}
|
|
}
|
|
|
|
sections, err := h.dashboardService.GetDashboardSections(
|
|
c.Request().Context(),
|
|
userUUID,
|
|
libUUID,
|
|
limit,
|
|
prefs.CollectionOrder,
|
|
prefs.HiddenCollections,
|
|
)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to load dashboard sections"})
|
|
}
|
|
|
|
sectionData := BuildSections(sections, libraryID)
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{"sections": sectionData})
|
|
}
|
|
|
|
func (h *DashboardHandler) UpdatePreferences(c *echo.Context) error {
|
|
user := c.Get("user").(database.Users)
|
|
userUUID := uuid.UUID(user.ID.Bytes)
|
|
|
|
var req struct {
|
|
LibraryID string `json:"library_id"`
|
|
HiddenCollections []string `json:"hidden_collections"`
|
|
CollectionOrder []string `json:"collection_order"`
|
|
ItemsPerSection int `json:"items_per_section"`
|
|
}
|
|
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"})
|
|
}
|
|
|
|
libUUID, err := uuid.Parse(req.LibraryID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"})
|
|
}
|
|
|
|
// Sanitize preferences (remove duplicates)
|
|
cleanHidden, cleanOrder := h.dashboardService.SanitizeDashboardPreferences(req.HiddenCollections, req.CollectionOrder)
|
|
req.HiddenCollections = cleanHidden
|
|
req.CollectionOrder = cleanOrder
|
|
|
|
prefs, err := h.dashboardService.UpsertDashboardPreferences(c.Request().Context(), database.UpsertDashboardPreferencesParams{
|
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
|
LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
|
|
HiddenCollections: req.HiddenCollections,
|
|
CollectionOrder: req.CollectionOrder,
|
|
ItemsPerSection: pgtype.Int4{Int32: int32(req.ItemsPerSection), Valid: true},
|
|
})
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to save preferences"})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, prefs)
|
|
}
|
|
|
|
func (h *DashboardHandler) RestoreSystemCollection(c *echo.Context) error {
|
|
user := c.Get("user").(database.Users)
|
|
userUUID := uuid.UUID(user.ID.Bytes)
|
|
|
|
var req struct {
|
|
CollectionName string `form:"collection_name" json:"collection_name"`
|
|
ResetType string `form:"reset_type" json:"reset_type"`
|
|
}
|
|
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"})
|
|
}
|
|
|
|
if req.CollectionName == "" {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "collection_name required"})
|
|
}
|
|
|
|
if req.ResetType == "" {
|
|
req.ResetType = "full"
|
|
}
|
|
|
|
validCollections := map[string]bool{
|
|
"continue-reading": true,
|
|
"recently-added": true,
|
|
"recently-read": true,
|
|
"not-started": true,
|
|
}
|
|
if !validCollections[req.CollectionName] {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid system collection name"})
|
|
}
|
|
|
|
if req.ResetType != "full" && req.ResetType != "keep_books" {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "reset_type must be 'full' or 'keep_books'"})
|
|
}
|
|
|
|
err := h.dashboardService.RestoreSystemCollection(c.Request().Context(), userUUID, req.CollectionName, req.ResetType)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to restore system collection"})
|
|
}
|
|
|
|
// Add redirect header for HTMX requests
|
|
if c.Request().Header.Get("HX-Request") == "true" {
|
|
c.Response().Header().Set("HX-Redirect", "/collections")
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]string{"message": "System collection restored to defaults"})
|
|
}
|
|
|
|
func BuildSections(sections []services.DashboardSection, currentLibraryID string) []SectionData {
|
|
var result []SectionData
|
|
|
|
for _, ds := range sections {
|
|
bookCards := make([]BookInfo, len(ds.Items))
|
|
for i, item := range ds.Items {
|
|
itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16])
|
|
bookCards[i] = BookInfo{
|
|
MediaItemID: itemUUID.String(),
|
|
Title: item.Title,
|
|
Author: textToString(item.Author),
|
|
CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
|
|
}
|
|
}
|
|
|
|
result = append(result, SectionData{
|
|
ID: ds.CollectionName,
|
|
CollectionID: ds.CollectionID.String(),
|
|
IsSystem: ds.IsSystem,
|
|
Title: ds.Title,
|
|
Description: ds.Description,
|
|
Icon: ds.Icon,
|
|
Items: bookCards,
|
|
ViewAllURL: getViewAllURL(ds.CollectionID.String(), currentLibraryID),
|
|
Priority: ds.Priority,
|
|
})
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func getViewAllURL(collectionID string, libraryID string) string {
|
|
if collectionID != "" {
|
|
if libraryID != "" {
|
|
return "/collections/" + collectionID + "?library_id=" + libraryID
|
|
}
|
|
return "/collections/" + collectionID
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (h *DashboardHandler) GetPreferences(c *echo.Context) error {
|
|
user := c.Get("user").(database.Users)
|
|
userUUID := uuid.UUID(user.ID.Bytes)
|
|
libraryID := c.QueryParam("library_id")
|
|
|
|
if libraryID == "" {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "library_id required"})
|
|
}
|
|
|
|
libUUID, err := uuid.Parse(libraryID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"})
|
|
}
|
|
prefs, err := h.dashboardService.GetDashboardPreferences(c.Request().Context(), userUUID, libUUID)
|
|
if err != nil {
|
|
// Return default preferences instead of 404 when none exist
|
|
log.Printf("GetDashboardPreferences failed: %v", err)
|
|
prefs = database.UserDashboardPreferences{
|
|
HiddenCollections: []string{},
|
|
CollectionOrder: []string{},
|
|
ItemsPerSection: pgtype.Int4{Int32: 20, Valid: true},
|
|
}
|
|
}
|
|
|
|
// Normalize nil slices to empty arrays for JSON
|
|
hiddenCollections := prefs.HiddenCollections
|
|
if hiddenCollections == nil {
|
|
hiddenCollections = []string{}
|
|
}
|
|
collectionOrder := prefs.CollectionOrder
|
|
if collectionOrder == nil {
|
|
collectionOrder = []string{}
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"hidden_collections": hiddenCollections,
|
|
"collection_order": collectionOrder,
|
|
"items_per_section": prefs.ItemsPerSection.Int32,
|
|
})
|
|
}
|