diff --git a/internal/handlers/collections.go b/internal/handlers/collections.go index dee8431f..9523b8e9 100644 --- a/internal/handlers/collections.go +++ b/internal/handlers/collections.go @@ -5,6 +5,7 @@ import ( "bookhoard/internal/services" wsync "bookhoard/internal/sync" "bookhoard/internal/utils" + "context" "encoding/json" "errors" "fmt" @@ -77,6 +78,40 @@ type BookInfo struct { HasConflict bool `json:"has_conflict"` // Media-type identifier for card badges/hover icons (A2c) FormatGroup string `json:"format_group"` + // Resolved cover-badge glyph: "audiobook" | "comic" | "manga" | "". + // comic vs manga rides the LIBRARY type (format_group only knows + // comic_archive — manga_type is unreliable across sources). + Badge string `json:"badge"` +} + +// LibraryTypeMap builds libraryID→library-type-name for badge resolution +// (one small query per request; libraries are few). +func LibraryTypeMap(ctx context.Context, db *database.Queries) map[string]string { + m := map[string]string{} + libs, err := db.ListLibraries(ctx) + if err != nil { + return m + } + for _, l := range libs { + if l.ID.Valid { + m[uuid.UUID(l.ID.Bytes).String()] = l.TypeName + } + } + return m +} + +// MediaBadgeKind resolves the badge glyph per media type from the row's +// format group and its library's type name. +func MediaBadgeKind(formatGroup, libraryTypeName string) string { + switch { + case formatGroup == "audiobook": + return "audiobook" + case formatGroup == "comic_archive" && libraryTypeName == "manga": + return "manga" + case formatGroup == "comic_archive": + return "comic" + } + return "" } type SectionData struct { @@ -280,12 +315,14 @@ func (h *CollectionHandler) GetCollection(c *echo.Context) error { bookList = make([]BookInfo, 0, len(matches)) for _, item := range matches { itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16]) + libTypes := LibraryTypeMap(c.Request().Context(), h.db) bookList = append(bookList, BookInfo{ MediaItemID: itemUUID.String(), Title: item.Title, Author: textToString(item.Author), CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath), FormatGroup: item.FormatGroup, + Badge: MediaBadgeKind(item.FormatGroup, libTypes[uuid.UUID(item.LibraryID.Bytes).String()]), }) } } else if collection.IsSystemCollection.Bool && collection.QueryType.Valid && collection.QueryType.String != "" && collection.QueryType.String != "filter" { @@ -328,6 +365,7 @@ func (h *CollectionHandler) GetCollection(c *echo.Context) error { validItems = append(validItems, item) } } + libTypes := LibraryTypeMap(c.Request().Context(), h.db) bookCards := make([]BookInfo, len(validItems)) for i, item := range validItems { itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16]) @@ -337,6 +375,7 @@ func (h *CollectionHandler) GetCollection(c *echo.Context) error { Author: textToString(item.Author), CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath), FormatGroup: item.FormatGroup, + Badge: MediaBadgeKind(item.FormatGroup, libTypes[uuid.UUID(item.LibraryID.Bytes).String()]), } } bookList = bookCards @@ -348,12 +387,14 @@ func (h *CollectionHandler) GetCollection(c *echo.Context) error { } bookList = make([]BookInfo, 0, len(books)) for _, book := range books { + libTypes := LibraryTypeMap(c.Request().Context(), h.db) bookList = append(bookList, BookInfo{ MediaItemID: uuid.UUID(book.MediaItemID.Bytes).String(), Title: book.Title, Author: textToString(book.Author), CoverImagePath: utils.ResolveMediaURL(book.LibraryID, book.CoverImagePath), FormatGroup: book.FormatGroup, + Badge: MediaBadgeKind(book.FormatGroup, libTypes[uuid.UUID(book.LibraryID.Bytes).String()]), }) } } diff --git a/internal/handlers/dashboard.go b/internal/handlers/dashboard.go index f361cb2f..81610b2f 100644 --- a/internal/handlers/dashboard.go +++ b/internal/handlers/dashboard.go @@ -73,8 +73,9 @@ func (h *DashboardHandler) GetSections(c *echo.Context) error { } } } + libTypes := LibraryTypeMap(c.Request().Context(), h.db) - sectionData := BuildSections(sections, libraryID, libraryType) + sectionData := BuildSections(sections, libraryID, libraryType, libTypes) sectionData = MarkActiveConflictsSections(c.Request().Context(), h.db, user.ID, sectionData) return c.JSON(http.StatusOK, map[string]interface{}{"sections": sectionData}) @@ -182,7 +183,7 @@ func (h *DashboardHandler) RestoreSystemCollection(c *echo.Context) error { // active library is audiobooks, system-collection titles/descriptions // swap to the listening wording at display time — the stored names stay // generic so restores and settings keep working across library types. -func BuildSections(sections []services.DashboardSection, currentLibraryID, libraryType string) []SectionData { +func BuildSections(sections []services.DashboardSection, currentLibraryID, libraryType string, libTypes map[string]string) []SectionData { var result []SectionData audioScope := libraryType == "audiobooks" @@ -208,6 +209,7 @@ func BuildSections(sections []services.DashboardSection, currentLibraryID, libra Author: textToString(item.Author), CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath), FormatGroup: item.FormatGroup, + Badge: MediaBadgeKind(item.FormatGroup, libTypes[uuid.UUID(item.LibraryID.Bytes).String()]), } } diff --git a/internal/handlers/series.go b/internal/handlers/series.go index 0b820480..18a36dec 100644 --- a/internal/handlers/series.go +++ b/internal/handlers/series.go @@ -15,11 +15,13 @@ import ( type SeriesHandler struct { seriesService *services.SeriesService + db *database.Queries } func NewSeriesHandler(db *database.Queries) *SeriesHandler { return &SeriesHandler{ seriesService: services.NewSeriesService(db), + db: db, } } @@ -154,12 +156,15 @@ func (h *SeriesHandler) GetSeriesBooks(c *echo.Context) error { bookCards := make([]BookInfo, 0, len(books)) for _, item := range books { itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16]) + libTypes := LibraryTypeMap(c.Request().Context(), h.db) + _ = libTypes bookCards = append(bookCards, BookInfo{ MediaItemID: itemUUID.String(), Title: item.Title, Author: textToString(item.Author), CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath), FormatGroup: item.FormatGroup, + Badge: MediaBadgeKind(item.FormatGroup, libTypes[uuid.UUID(item.LibraryID.Bytes).String()]), }) } diff --git a/internal/router/frontend.go b/internal/router/frontend.go index 784a2abb..e85bc709 100644 --- a/internal/router/frontend.go +++ b/internal/router/frontend.go @@ -482,9 +482,14 @@ func registerFrontendRoutes(cfg *Config) { } } + libTypes := map[string]string{} + for _, ld := range libRes.Libraries { + libTypes[ld.ID] = ld.TypeValue + } + userPgID := pgtype.UUID{Bytes: userUUID, Valid: true} - sectionData := handlers.MarkActiveConflictsSections(c.Request().Context(), cfg.Queries, userPgID, handlers.BuildSections(visibleSections, libraryID, activeLibType)) - allSectionsData := handlers.BuildSections(allSections, libraryID, activeLibType) + sectionData := handlers.MarkActiveConflictsSections(c.Request().Context(), cfg.Queries, userPgID, handlers.BuildSections(visibleSections, libraryID, activeLibType, libTypes)) + allSectionsData := handlers.BuildSections(allSections, libraryID, activeLibType, libTypes) // "This week" widget: the user's rolling 7-day reading pulse for // the CURRENTLY SELECTED library (the same resolution the sections diff --git a/internal/router/search.go b/internal/router/search.go index d3cd95e5..e40d3ad5 100644 --- a/internal/router/search.go +++ b/internal/router/search.go @@ -110,6 +110,8 @@ func handleSearchHTML(c *echo.Context, cfg *Config) error { Title: book.Title, Author: textToString(book.Author), CoverImagePath: utils.ResolveMediaURL(pgtype.UUID{Bytes: bookLibUUID, Valid: true}, book.CoverImagePath), + FormatGroup: book.FormatGroup, + Badge: handlers.MediaBadgeKind(book.FormatGroup, book.LibraryTypeName), } } // Stamp active conflict flags so cards route the play action correctly diff --git a/templates/annotations_banner.templ b/templates/annotations_banner.templ index a8d19ae8..736f99d2 100644 --- a/templates/annotations_banner.templ +++ b/templates/annotations_banner.templ @@ -29,7 +29,7 @@ templ AnnotationsBanner(book handlers.MediaDetail) { aria-label="Annotations" >
Loading…
- No notes or highlights on this book yet. + No notes, highlights or bookmarks on this book yet.
@@ -64,6 +64,12 @@ templ AnnotationsBanner(book handlers.MediaDetail) { +No chapters detected for this audiobook.
No bookmarks yet — tap the
Bookmark at 0:00