feat(annotations + player): bookmark rows in the banner, stacked volume panel, badge parity for comics/manga
Annotations banner: audio/text bookmarks join notes and highlights — third row kind (bookmark glyph indicator, H:MM:SS / Page N / Chapter N friendly labels), edit-note/delete actions branch by kind, and the banner's export now covers all annotations. The archived ledger (ListDeletedAnnotationsForBook) already tombstoned bookmarks equally and Restore/Purge take annotation_type=bookmark — verified live: delete → restore brings the row back with its note intact. Volume: the dragger sits BELOW the mute icon in the panel (stacked flyout shape) with the horizontal slider from the last round. Badges: the real fix for comics/manga — format_group only knows 'comic_archive' (manga_type is unreliable across sources), so the comic/manga split is resolved from the LIBRARY type server-side. handlers.MediaBadgeKind + BookInfo.Badge at every construction site (dashboard SSR + sections API, collections, series, and the bookshelf search grid — which was also missing FormatGroup entirely, so no card ever badged there). Both renderers (templ + dashboard.ts) switch on the resolved badge. Verified: All-Books grid renders 2 Comic + 3 Manga badges, dashboard the audiobook one. Infra: header.templ main.js src gains ?v=<stamp> — browsers keyed an intermediate bundle by URL and served it across rebuilds (the stale 'Add note' the user saw); bump the stamp when JS stale-shadows.
This commit is contained in:
1 parent
2c714dcb60
commit
89665fb2bc
19 files changed
+273
-92
No files matched your search
@@ -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()]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user