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.
178 lines
4.5 KiB
Go
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
|
|
}
|