Files
bookhoard/internal/router/helpers.go
T
john-okeefe 05370d236a feat(ui): display media counts in library switcher
The UI had no surface showing how many media items have been imported.
Surface the total in the library switcher shown on the Dashboard, Series,
and Collections pages (via the LibrarySwitcher component) and in the
Bookshelf's inline library filter.

- Add a MediaCount field to LibraryData and a TotalMediaCount helper to
  sum counts for the "All Libraries" / "All Books" option.
- resolveLibrary() now fetches per-library counts (one query) and maps
  them onto each LibraryData entry, so the switcher reflects the active
  scope without changing the component's signature.
- Each library option renders "(N)" and the "All" option renders the
  grand total across the user's visible libraries.

The "All" total is the sum of the user's visible libraries, correctly
respecting per-user library visibility rather than a raw global count.

Regenerated templ files for library_switcher and bookshelf.
2026-07-30 11:41:13 -04:00

178 lines
4.5 KiB
Go

package router
import (
"context"
"log"
"net/url"
"time"
"bookhoard/internal/database"
"bookhoard/templates"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/labstack/echo/v5"
)
func getTemplateUserWithTheme(c *echo.Context, cfg *Config) (templates.User, error) {
userID := c.Get("user_id").(string)
userEmail := c.Get("user_email").(string)
userUsername := c.Get("user_username").(string)
userRole := c.Get("user_role").(string)
userUUID, err := uuid.Parse(userID)
if err != nil {
log.Printf("getTemplateUserWithTheme failed: invalid UUID '%s': %v", userID, err)
return templates.User{}, err
}
userDB, err := cfg.Queries.GetUser(c.Request().Context(), uuidToPGType(userUUID))
if err != nil {
log.Printf("getTemplateUserWithTheme failed: database query error for user ID %s: %v", userID, err)
return templates.User{}, err
}
userTheme := "tokyo-night"
if userDB.Theme.Valid {
userTheme = userDB.Theme.String
}
userTimezone := "UTC"
if userDB.Timezone.Valid {
userTimezone = userDB.Timezone.String
}
// Extract JWT token for WebSocket authentication
token := ""
if cookie, err := c.Cookie("token"); err == nil {
token = cookie.Value
}
return templates.User{
ID: userID,
Email: userEmail,
Username: userUsername,
Role: userRole,
Theme: userTheme,
Token: token,
Timezone: userTimezone,
}, nil
}
func convertPending(pending []map[string]interface{}) []templates.PendingRegistrationData {
result := make([]templates.PendingRegistrationData, len(pending))
for i, p := range pending {
result[i] = templates.PendingRegistrationData{
RegistrationID: p["registration_id"].(string),
DeviceName: p["device_name"].(string),
DeviceType: p["device_type"].(string),
ExpiresAt: p["expires_at"].(time.Time).Format(time.RFC3339),
}
}
return result
}
func pingDB(cfg *Config, ctx context.Context) error {
if cfg.DBPool != nil {
if pool, ok := cfg.DBPool.(*pgxpool.Pool); ok {
return pool.Ping(ctx)
}
}
return nil
}
func parseUUID(s string) (uuid.UUID, error) {
return uuid.Parse(s)
}
func uuidToPGType(u uuid.UUID) pgtype.UUID {
return pgtype.UUID{Bytes: u, Valid: true}
}
const selectedLibraryCookie = "selectedLibrary"
const allLibrariesSentinel = "__all__"
type LibraryResolution struct {
LibraryID string
IsAll bool
LibUUID pgtype.UUID
Libraries []templates.LibraryData
FirstID string
}
func getText(t pgtype.Text) string {
if t.Valid {
return t.String
}
return ""
}
func resolveLibrary(c *echo.Context, cfg *Config, userUUID string) LibraryResolution {
res := LibraryResolution{}
userU, _ := uuid.Parse(userUUID)
libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userU))
if err != nil {
log.Printf("GetUserVisibleLibraries failed: %v", err)
libraries = []database.GetUserVisibleLibrariesRow{}
}
counts, countErr := cfg.Queries.GetVisibleLibraryMediaCounts(c.Request().Context(), uuidToPGType(userU))
if countErr != nil {
log.Printf("GetVisibleLibraryMediaCounts failed: %v", countErr)
counts = []database.GetVisibleLibraryMediaCountsRow{}
}
countMap := make(map[string]int64, len(counts))
for _, mc := range counts {
mcUUID, _ := uuid.FromBytes(mc.ID.Bytes[0:16])
countMap[mcUUID.String()] = mc.MediaCount
}
res.Libraries = make([]templates.LibraryData, len(libraries))
for i, lib := range libraries {
libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16])
res.Libraries[i] = templates.LibraryData{
ID: libUUID.String(),
Name: lib.Name,
Description: getText(lib.Description),
TypeName: lib.TypeName,
MediaCount: countMap[libUUID.String()],
}
}
if len(libraries) > 0 {
libUUID, _ := uuid.FromBytes(libraries[0].ID.Bytes[0:16])
res.FirstID = libUUID.String()
}
libraryID := c.QueryParam("library_id")
if libraryID == "" {
if cookie, err := c.Cookie(selectedLibraryCookie); err == nil {
val, _ := url.QueryUnescape(cookie.Value)
if val == allLibrariesSentinel {
res.IsAll = true
res.LibraryID = ""
return res
}
if _, parseErr := uuid.Parse(val); parseErr == nil {
libraryID = val
}
}
}
if libraryID == "" {
res.LibraryID = res.FirstID
if res.LibraryID != "" {
parsed, _ := uuid.Parse(res.LibraryID)
res.LibUUID = pgtype.UUID{Bytes: parsed, Valid: true}
}
return res
}
res.LibraryID = libraryID
parsed, _ := uuid.Parse(libraryID)
res.LibUUID = pgtype.UUID{Bytes: parsed, Valid: true}
return res
}