From d74db03cf3df197f442adb65e8e5a21af06ddc36 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sun, 18 Oct 2026 16:45:48 -0400 Subject: [PATCH] =?UTF-8?q?feat(sync):=20unlinked-books=20queue=20?= =?UTF-8?q?=E2=80=94=20ingest,=20page=20route,=20dashboard=20banner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The identification queue for device files the sync cannot match is now live end-to-end. Previously the whole feature was dark: unmatched device files were silently dropped (losing their reading progress), the queue table sat empty by construction, and the page template had no route. - Ingest: the KOReader sync loop, on an unresolvable file, upserts unlinked_books (one row per device + content id; re-syncs bump last_seen_at and refresh device-reported metadata) instead of dropping the file. Schema gains the (device_id, content_id) unique index the upsert needs. - Page: GET /unlinked-books routes the existing page template, listing the caller's own unresolved rows (ListUnresolvedUnlinkedBooksForUser — devices.user_id scoping; no cross-user leakage). - Dashboard banner: a warning card above the shelves, rendered only when the caller's queue is non-empty (hidden-unless-needed), linking to the page. - Resolve flow unchanged and already gated: MatchingHandler.LinkBook verifies device ownership and target-item visibility (403s) and creates the device file alias, so the next sync matches by path. SyncHandler.LinkBook and Handler.LinkBook are unrouted dead handlers — triage candidates, not part of the live flow. --- database/schema/schema.sql | 2 + internal/database/querier.go | 7 + internal/database/queries.sql.go | 129 ++++ internal/database/queries/queries.sql | 28 + internal/handlers/koreader.go | 44 +- internal/router/frontend.go | 51 +- templates/dashboard.templ | 20 +- templates/dashboard_templ.go | 810 ++++++++++++++------------ templates/partials_render_test.go | 44 ++ templates/unlinked_books_templ.go | 142 ++--- 10 files changed, 823 insertions(+), 454 deletions(-) diff --git a/database/schema/schema.sql b/database/schema/schema.sql index 9385dd8c..4cb8e799 100644 --- a/database/schema/schema.sql +++ b/database/schema/schema.sql @@ -1353,6 +1353,8 @@ CREATE TABLE IF NOT EXISTS unlinked_books ( CREATE INDEX IF NOT EXISTS idx_unlinked_books_device ON unlinked_books(device_id); CREATE INDEX IF NOT EXISTS idx_unlinked_books_content_id ON unlinked_books(content_id); +-- One queue row per (device, document): the sync ingest upserts on this pair. +CREATE UNIQUE INDEX IF NOT EXISTS idx_unlinked_books_device_content ON unlinked_books(device_id, content_id); CREATE INDEX IF NOT EXISTS idx_unlinked_books_resolved ON unlinked_books(resolved); -- Create saved_filters for users to be able to filter different parts of the application diff --git a/internal/database/querier.go b/internal/database/querier.go index 91f7eb47..6cdf8063 100644 --- a/internal/database/querier.go +++ b/internal/database/querier.go @@ -48,6 +48,8 @@ type Querier interface { CountCollectionLiveItems(ctx context.Context, arg CountCollectionLiveItemsParams) (int32, error) // Count unlinked books for a device CountUnlinkedBooks(ctx context.Context, deviceID pgtype.UUID) (int64, error) + // Banner count: unresolved queue rows across the caller's own devices. + CountUnresolvedUnlinkedBooksForUser(ctx context.Context, userID pgtype.UUID) (int64, error) CountUserDevices(ctx context.Context, userID pgtype.UUID) (int64, error) CreateAutoResolvedSyncConflict(ctx context.Context, arg CreateAutoResolvedSyncConflictParams) (SyncConflicts, error) // COLLECTIONS QUERIES @@ -469,6 +471,8 @@ type Querier interface { ListSyncConflictsByUser(ctx context.Context, userID pgtype.UUID) ([]ListSyncConflictsByUserRow, error) // List unresolved unlinked books with pagination ListUnresolvedUnlinkedBooks(ctx context.Context, arg ListUnresolvedUnlinkedBooksParams) ([]ListUnresolvedUnlinkedBooksRow, error) + // Page list: unresolved queue rows for the caller's own devices. + ListUnresolvedUnlinkedBooksForUser(ctx context.Context, userID pgtype.UUID) ([]ListUnresolvedUnlinkedBooksForUserRow, error) ListUsers(ctx context.Context) ([]ListUsersRow, error) // First consecutive scan that cannot find the file on disk. MarkMediaItemMissing(ctx context.Context, id pgtype.UUID) error @@ -607,6 +611,9 @@ type Querier interface { UpsertPanelData(ctx context.Context, arg UpsertPanelDataParams) (PanelData, error) UpsertReaderSettings(ctx context.Context, arg UpsertReaderSettingsParams) (ReaderSettings, error) UpsertSystemSetting(ctx context.Context, arg UpsertSystemSettingParams) (SystemSettings, error) + // Ingest: insert or refresh the queue row for a device file that could not + // be identified. One row per (device, content_id); re-syncs bump last_seen. + UpsertUnlinkedBook(ctx context.Context, arg UpsertUnlinkedBookParams) (UnlinkedBooks, error) } var _ Querier = (*Queries)(nil) diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index adb7c8eb..6d62bc73 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -332,6 +332,21 @@ func (q *Queries) CountUnlinkedBooks(ctx context.Context, deviceID pgtype.UUID) return count, err } +const CountUnresolvedUnlinkedBooksForUser = `-- name: CountUnresolvedUnlinkedBooksForUser :one +SELECT COUNT(*) as count +FROM unlinked_books ub +JOIN devices d ON ub.device_id = d.id +WHERE d.user_id = $1 AND ub.resolved = false +` + +// Banner count: unresolved queue rows across the caller's own devices. +func (q *Queries) CountUnresolvedUnlinkedBooksForUser(ctx context.Context, userID pgtype.UUID) (int64, error) { + row := q.db.QueryRow(ctx, CountUnresolvedUnlinkedBooksForUser, userID) + var count int64 + err := row.Scan(&count) + return count, err +} + const CountUserDevices = `-- name: CountUserDevices :one SELECT COUNT(*) FROM devices WHERE user_id = $1 ` @@ -11365,6 +11380,69 @@ func (q *Queries) ListUnresolvedUnlinkedBooks(ctx context.Context, arg ListUnres return items, nil } +const ListUnresolvedUnlinkedBooksForUser = `-- name: ListUnresolvedUnlinkedBooksForUser :many +SELECT ub.id, ub.device_id, ub.content_id, ub.file_path, ub.title, ub.author, ub.confidence_score, ub.resolved, ub.media_item_id, ub.resolved_at, ub.resolution_method, ub.last_seen_at, ub.created_at, d.device_name, d.device_type +FROM unlinked_books ub +JOIN devices d ON ub.device_id = d.id +WHERE d.user_id = $1 AND ub.resolved = false +ORDER BY ub.last_seen_at DESC +` + +type ListUnresolvedUnlinkedBooksForUserRow struct { + ID pgtype.UUID `db:"id" json:"id"` + DeviceID pgtype.UUID `db:"device_id" json:"device_id"` + ContentID string `db:"content_id" json:"content_id"` + FilePath pgtype.Text `db:"file_path" json:"file_path"` + Title pgtype.Text `db:"title" json:"title"` + Author pgtype.Text `db:"author" json:"author"` + ConfidenceScore pgtype.Float8 `db:"confidence_score" json:"confidence_score"` + Resolved pgtype.Bool `db:"resolved" json:"resolved"` + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + ResolvedAt pgtype.Timestamptz `db:"resolved_at" json:"resolved_at"` + ResolutionMethod pgtype.Text `db:"resolution_method" json:"resolution_method"` + LastSeenAt pgtype.Timestamptz `db:"last_seen_at" json:"last_seen_at"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + DeviceName string `db:"device_name" json:"device_name"` + DeviceType string `db:"device_type" json:"device_type"` +} + +// Page list: unresolved queue rows for the caller's own devices. +func (q *Queries) ListUnresolvedUnlinkedBooksForUser(ctx context.Context, userID pgtype.UUID) ([]ListUnresolvedUnlinkedBooksForUserRow, error) { + rows, err := q.db.Query(ctx, ListUnresolvedUnlinkedBooksForUser, userID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListUnresolvedUnlinkedBooksForUserRow{} + for rows.Next() { + var i ListUnresolvedUnlinkedBooksForUserRow + if err := rows.Scan( + &i.ID, + &i.DeviceID, + &i.ContentID, + &i.FilePath, + &i.Title, + &i.Author, + &i.ConfidenceScore, + &i.Resolved, + &i.MediaItemID, + &i.ResolvedAt, + &i.ResolutionMethod, + &i.LastSeenAt, + &i.CreatedAt, + &i.DeviceName, + &i.DeviceType, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const ListUsers = `-- name: ListUsers :many SELECT u.id, @@ -15382,3 +15460,54 @@ func (q *Queries) UpsertSystemSetting(ctx context.Context, arg UpsertSystemSetti ) return i, err } + +const UpsertUnlinkedBook = `-- name: UpsertUnlinkedBook :one +INSERT INTO unlinked_books (device_id, content_id, file_path, title, author, confidence_score) +VALUES ($1, $2, $3, $4, $5, $6) +ON CONFLICT (device_id, content_id) DO UPDATE +SET file_path = EXCLUDED.file_path, + title = EXCLUDED.title, + author = EXCLUDED.author, + confidence_score = EXCLUDED.confidence_score, + last_seen_at = NOW() +RETURNING id, device_id, content_id, file_path, title, author, confidence_score, resolved, media_item_id, resolved_at, resolution_method, last_seen_at, created_at +` + +type UpsertUnlinkedBookParams struct { + DeviceID pgtype.UUID `db:"device_id" json:"device_id"` + ContentID string `db:"content_id" json:"content_id"` + FilePath pgtype.Text `db:"file_path" json:"file_path"` + Title pgtype.Text `db:"title" json:"title"` + Author pgtype.Text `db:"author" json:"author"` + ConfidenceScore pgtype.Float8 `db:"confidence_score" json:"confidence_score"` +} + +// Ingest: insert or refresh the queue row for a device file that could not +// be identified. One row per (device, content_id); re-syncs bump last_seen. +func (q *Queries) UpsertUnlinkedBook(ctx context.Context, arg UpsertUnlinkedBookParams) (UnlinkedBooks, error) { + row := q.db.QueryRow(ctx, UpsertUnlinkedBook, + arg.DeviceID, + arg.ContentID, + arg.FilePath, + arg.Title, + arg.Author, + arg.ConfidenceScore, + ) + var i UnlinkedBooks + err := row.Scan( + &i.ID, + &i.DeviceID, + &i.ContentID, + &i.FilePath, + &i.Title, + &i.Author, + &i.ConfidenceScore, + &i.Resolved, + &i.MediaItemID, + &i.ResolvedAt, + &i.ResolutionMethod, + &i.LastSeenAt, + &i.CreatedAt, + ) + return i, err +} diff --git a/internal/database/queries/queries.sql b/internal/database/queries/queries.sql index 61384691..37c0d344 100644 --- a/internal/database/queries/queries.sql +++ b/internal/database/queries/queries.sql @@ -2619,6 +2619,34 @@ WHERE ub.resolved = false ORDER BY ub.last_seen_at DESC LIMIT $1 OFFSET $2; +-- Ingest: insert or refresh the queue row for a device file that could not +-- be identified. One row per (device, content_id); re-syncs bump last_seen. +-- name: UpsertUnlinkedBook :one +INSERT INTO unlinked_books (device_id, content_id, file_path, title, author, confidence_score) +VALUES ($1, $2, $3, $4, $5, $6) +ON CONFLICT (device_id, content_id) DO UPDATE +SET file_path = EXCLUDED.file_path, + title = EXCLUDED.title, + author = EXCLUDED.author, + confidence_score = EXCLUDED.confidence_score, + last_seen_at = NOW() +RETURNING *; + +-- Banner count: unresolved queue rows across the caller's own devices. +-- name: CountUnresolvedUnlinkedBooksForUser :one +SELECT COUNT(*) as count +FROM unlinked_books ub +JOIN devices d ON ub.device_id = d.id +WHERE d.user_id = $1 AND ub.resolved = false; + +-- Page list: unresolved queue rows for the caller's own devices. +-- name: ListUnresolvedUnlinkedBooksForUser :many +SELECT ub.*, d.device_name, d.device_type +FROM unlinked_books ub +JOIN devices d ON ub.device_id = d.id +WHERE d.user_id = $1 AND ub.resolved = false +ORDER BY ub.last_seen_at DESC; + -- ============================================ -- CAROUSEL-STYLE DASHBOARD -- ============================================ diff --git a/internal/handlers/koreader.go b/internal/handlers/koreader.go index 3af38369..37b3d646 100644 --- a/internal/handlers/koreader.go +++ b/internal/handlers/koreader.go @@ -451,6 +451,10 @@ func (h *KOReaderHandler) SyncProgress(c *echo.Context) error { for _, book := range req.Books { mediaItemID, _ := h.resolveBookToMediaItem(c, device.ID, pgUserID, book) if !mediaItemID.Valid { + // Unidentified file: queue it for manual identification + // (unlinked-books page) instead of silently dropping the + // file's progress. One row per (device, content id). + h.queueUnlinkedBook(c, device.ID, book) bookResults = append(bookResults, KOReaderBookSyncResult{ SHA256: book.SHA256, Synced: false, @@ -498,9 +502,47 @@ func (h *KOReaderHandler) SyncProgress(c *echo.Context) error { }) } -func (h *KOReaderHandler) resolveBookToMediaItem(c *echo.Context, deviceID pgtype.UUID, userID pgtype.UUID, book KOReaderBookProgress) (pgtype.UUID, float64) { +// queueUnlinkedBook records a device file that could not be identified, so +// the user can resolve it on the unlinked-books page. One row per +// (device, content id); re-syncs refresh the row instead of duplicating it. +// Best-effort: queueing failures never fail the sync response. +func (h *KOReaderHandler) queueUnlinkedBook(c *echo.Context, deviceID pgtype.UUID, book KOReaderBookProgress) { ctx := c.Request().Context() + contentID := book.UUID + if contentID == "" { + contentID = book.SHA256 + } + if contentID == "" { + // Nothing stable to identify the file by — it could never resolve. + return + } + + var title pgtype.Text + if book.Title != "" { + title = pgtype.Text{String: book.Title, Valid: true} + } + var author pgtype.Text + if len(book.Authors) > 0 && book.Authors[0] != "" { + author = pgtype.Text{String: book.Authors[0], Valid: true} + } + var filePath pgtype.Text + if book.FilePath != "" { + filePath = pgtype.Text{String: book.FilePath, Valid: true} + } + + _, _ = h.db.UpsertUnlinkedBook(ctx, database.UpsertUnlinkedBookParams{ + DeviceID: deviceID, + ContentID: contentID, + FilePath: filePath, + Title: title, + Author: author, + ConfidenceScore: pgtype.Float8{Float64: 0, Valid: true}, + }) +} + +func (h *KOReaderHandler) resolveBookToMediaItem(c *echo.Context, deviceID pgtype.UUID, userID pgtype.UUID, book KOReaderBookProgress) (pgtype.UUID, float64) { ctx := c.Request().Context() + // Priority 1: UUID provided (highest confidence - 1.0) if book.UUID != "" { mediaUUID, err := uuid.Parse(book.UUID) diff --git a/internal/router/frontend.go b/internal/router/frontend.go index 3923ee1b..402f4f3d 100644 --- a/internal/router/frontend.go +++ b/internal/router/frontend.go @@ -475,8 +475,18 @@ func registerFrontendRoutes(cfg *Config) { } } + userUUID, userUUIDErr := uuid.Parse(user.ID) + if userUUIDErr != nil { + return renderErrorPage(c, "Error loading user", "user_load_error") + } + unlinkedCount, countErr := cfg.Queries.CountUnresolvedUnlinkedBooksForUser(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true}) + if countErr != nil { + log.Printf("CountUnresolvedUnlinkedBooksForUser failed: %v", countErr) + unlinkedCount = 0 + } + var buf bytes.Buffer - err = templates.Dashboard(user, sectionData, allSectionsData, libRes.Libraries, libraryID, prefs.HiddenCollections, limit, week, activeLibType, errorMsg).Render(c.Request().Context(), &buf) + err = templates.Dashboard(user, sectionData, allSectionsData, libRes.Libraries, libraryID, prefs.HiddenCollections, limit, week, activeLibType, errorMsg, int(unlinkedCount)).Render(c.Request().Context(), &buf) if err != nil { return err } @@ -742,6 +752,45 @@ func registerFrontendRoutes(cfg *Config) { // itself here (htmx) whenever the library switcher dispatches // week-refresh, carrying the current #library-select value. Same // resolution rules as the dashboard page (family-lens safe). + // Unlinked-books page: the identification queue for device files the + // sync could not match (personal data — the caller's own devices only). + frontendProtected.GET("/unlinked-books", func(c *echo.Context) error { + user, err := getTemplateUserWithTheme(c, cfg) + if err != nil { + return renderErrorPage(c, "Error loading user", "user_load_error") + } + userUUID, userUUIDErr := uuid.Parse(user.ID) + if userUUIDErr != nil { + return renderErrorPage(c, "Error loading user", "user_load_error") + } + rows, listErr := cfg.Queries.ListUnresolvedUnlinkedBooksForUser(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true}) + if listErr != nil { + log.Printf("ListUnresolvedUnlinkedBooksForUser failed: %v", listErr) + rows = []database.ListUnresolvedUnlinkedBooksForUserRow{} + } + + books := make([]templates.UnlinkedBookData, 0, len(rows)) + for _, r := range rows { + books = append(books, templates.UnlinkedBookData{ + ProgressID: uuid.UUID(r.ID.Bytes).String(), + DeviceID: uuid.UUID(r.DeviceID.Bytes).String(), + DeviceType: r.DeviceType, + DeviceName: r.DeviceName, + FilePath: utils.PgTextString(r.FilePath), + SHA256: r.ContentID, + TitleFromDevice: utils.PgTextString(r.Title), + LastSyncTime: utils.FormatTimeInTimezone(r.LastSeenAt.Time, user.Timezone), + ConfidenceScore: r.ConfidenceScore.Float64, + }) + } + + var buf bytes.Buffer + if renderErr := templates.UnlinkedBooks(user, books).Render(c.Request().Context(), &buf); renderErr != nil { + return renderErr + } + return c.HTML(http.StatusOK, buf.String()) + }) + // Unlinked-book match results partial: the match cascade for one device // file, rendered server-side and scoped to the requester's visible // libraries (absent-from-results) — rendering-ownership: templ renders, diff --git a/templates/dashboard.templ b/templates/dashboard.templ index 4fb28090..3584dce4 100644 --- a/templates/dashboard.templ +++ b/templates/dashboard.templ @@ -5,7 +5,7 @@ import ( "fmt" ) -templ Dashboard(user User, sections []handlers.SectionData, allSections []handlers.SectionData, libData []LibraryData, currentLibraryID string, hiddenCollections []string, itemsPerSection int, week *WeekStatsData, activeLibType string, errorMessage string) { +templ Dashboard(user User, sections []handlers.SectionData, allSections []handlers.SectionData, libData []LibraryData, currentLibraryID string, hiddenCollections []string, itemsPerSection int, week *WeekStatsData, activeLibType string, errorMessage string, unlinkedCount int) { @@ -23,6 +23,24 @@ templ Dashboard(user User, sections []handlers.SectionData, allSections []handle re-render wipes that container's innerHTML, and anything server-rendered inside it dies on the first library switch. The widget re-fetches itself via htmx instead. --> + if unlinkedCount > 0 { +
+ + + @Icon("alert", "h-4 w-4") + + + { fmt.Sprintf("%d", unlinkedCount) } + { pluralizeBooks(unlinkedCount) } on your devices could not be identified — reading progress for them is paused until they are linked. + + Review → + +
+ } if week != nil {
@DashboardWeekWidget(*week, activeLibType) diff --git a/templates/dashboard_templ.go b/templates/dashboard_templ.go index 39020b09..b8ad4feb 100644 --- a/templates/dashboard_templ.go +++ b/templates/dashboard_templ.go @@ -13,7 +13,7 @@ import ( "fmt" ) -func Dashboard(user User, sections []handlers.SectionData, allSections []handlers.SectionData, libData []LibraryData, currentLibraryID string, hiddenCollections []string, itemsPerSection int, week *WeekStatsData, activeLibType string, errorMessage string) templ.Component { +func Dashboard(user User, sections []handlers.SectionData, allSections []handlers.SectionData, libData []LibraryData, currentLibraryID string, hiddenCollections []string, itemsPerSection int, week *WeekStatsData, activeLibType string, errorMessage string, unlinkedCount int) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -72,8 +72,48 @@ func Dashboard(user User, sections []handlers.SectionData, allSections []handler if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } + if unlinkedCount > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Icon("alert", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", unlinkedCount)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 37, Col: 69} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(pluralizeBooks(unlinkedCount)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 38, Col: 38} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " on your devices could not be identified — reading progress for them is paused until they are linked. Review →
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } if week != nil { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -81,12 +121,12 @@ func Dashboard(user User, sections []handlers.SectionData, allSections []handler if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -96,7 +136,7 @@ func Dashboard(user User, sections []handlers.SectionData, allSections []handler return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -108,7 +148,7 @@ func Dashboard(user User, sections []handlers.SectionData, allSections []handler if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -132,105 +172,105 @@ func CollectionCarousel(section handlers.SectionData) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var4 := templ.GetChildren(ctx) - if templ_7745c5c3_Var4 == nil { - templ_7745c5c3_Var4 = templ.NopComponent + templ_7745c5c3_Var6 := templ.GetChildren(ctx) + if templ_7745c5c3_Var6 == nil { + templ_7745c5c3_Var6 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" data-is-system=\"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var8 string - templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title) + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.IsSystem) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 52, Col: 100} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 64, Col: 35} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\">
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(section.Icon) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 68, Col: 130} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 70, Col: 100} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if section.Description != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var9 string - templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(section.Description) + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(section.Description) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 54, Col: 83} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 72, Col: 83} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if section.ViewAllURL != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "View All") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" class=\"inline-flex items-center gap-1 text-sm font-medium hover:underline transition-colors\" style=\"color: var(--accent);\">View All") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -238,25 +278,25 @@ func CollectionCarousel(section handlers.SectionData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" class=\"carousel-track flex gap-4 overflow-x-auto scroll-smooth snap-x snap-mandatory px-6 pb-2\" style=\"scrollbar-width: none; -ms-overflow-style: none;\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, item := range section.Items { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -303,13 +343,13 @@ func CollectionCarousel(section handlers.SectionData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if len(section.Items) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -317,25 +357,25 @@ func CollectionCarousel(section handlers.SectionData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "

No items in this collection

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "

No items in this collection

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -367,97 +407,97 @@ func BookCard(item handlers.BookInfo) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var15 := templ.GetChildren(ctx) - if templ_7745c5c3_Var15 == nil { - templ_7745c5c3_Var15 = templ.NopComponent + templ_7745c5c3_Var17 := templ.GetChildren(ctx) + if templ_7745c5c3_Var17 == nil { + templ_7745c5c3_Var17 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\" class=\"block h-full\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if item.CoverImagePath != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\"")") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\"")") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if MediaBadgeIcon(item.Badge) != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\" class=\"w-full h-full object-cover\" loading=\"lazy\" onerror=\"this.src='/static/placeholder-book.svg'\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\"")") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if MediaBadgeIcon(item.Badge) != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -465,125 +505,86 @@ func BookCard(item handlers.BookInfo) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var22 string - templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title) + var templ_7745c5c3_Var24 string + templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 147, Col: 17} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 165, Col: 17} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if item.Author != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var24 string - templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 151, Col: 19} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if item.HasConflict { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var26 string - templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue("Resolve progress conflict for " + item.Title) + templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 161, Col: 63} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 169, Col: 19} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\" title=\"Resolve progress conflict\">") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = Icon("book-open", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else if item.FormatGroup == "audiobook" { + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if item.HasConflict { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\" title=\"Resolve progress conflict\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = Icon("headphones", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = Icon("book-open", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -614,7 +615,7 @@ func BookCard(item handlers.BookInfo) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - } else { + } else if item.FormatGroup == "audiobook" { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\" title=\"Listen\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = Icon("book-open", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = Icon("headphones", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -653,8 +654,47 @@ func BookCard(item handlers.BookInfo) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Icon("book-open", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -678,12 +718,12 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [ }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var31 := templ.GetChildren(ctx) - if templ_7745c5c3_Var31 == nil { - templ_7745c5c3_Var31 = templ.NopComponent + templ_7745c5c3_Var33 := templ.GetChildren(ctx) + if templ_7745c5c3_Var33 == nil { + templ_7745c5c3_Var33 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "

Customize Dashboard

Drag to reorder, toggle to show or hide.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, section := range sections { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "\" draggable=\"true\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -730,120 +770,120 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [ if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var34 string - templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(section.Icon) + var templ_7745c5c3_Var36 string + templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(section.Icon) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 216, Col: 43} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 234, Col: 43} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var35 string - templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title) + var templ_7745c5c3_Var37 string + templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 218, Col: 84} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 236, Col: 84} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if section.IsSystem { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "System") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "System") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if section.IsSystem { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "\">Restore ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, ">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "\" class=\"w-full h-2 rounded-lg appearance-none cursor-pointer\" style=\"background-color: var(--border-strong);\" data-input-action=\"update-items-count\" target=\"items-count-display\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -899,12 +939,12 @@ func DashboardWeekWidget(week WeekStatsData, activeLibType string) templ.Compone }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var40 := templ.GetChildren(ctx) - if templ_7745c5c3_Var40 == nil { - templ_7745c5c3_Var40 = templ.NopComponent + templ_7745c5c3_Var42 := templ.GetChildren(ctx) + if templ_7745c5c3_Var42 == nil { + templ_7745c5c3_Var42 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -912,82 +952,82 @@ func DashboardWeekWidget(week WeekStatsData, activeLibType string) templ.Compone if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, " This Week ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var41 string - templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(fmtReadingMinutes(week.WeekMinutes)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 324, Col: 111} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var42 string - templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d pages", week.WeekPages)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 325, Col: 103} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, " This Week ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var43 string - templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d sessions", week.WeekSessions)) + templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(fmtReadingMinutes(week.WeekMinutes)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 326, Col: 109} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 342, Col: 111} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var44 string - templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d books", week.WeekBooks)) + templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d pages", week.WeekPages)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 327, Col: 103} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 343, Col: 103} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var45 string + templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d sessions", week.WeekSessions)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 344, Col: 109} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var46 string + templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d books", week.WeekBooks)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 345, Col: 103} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if week.CurrentStreak > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "🔥 ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "🔥 ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var45 string - templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", week.CurrentStreak)) + var templ_7745c5c3_Var47 string + templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", week.CurrentStreak)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 329, Col: 111} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 347, Col: 111} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "-day streak ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "-day streak ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "Full stats") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "Full stats") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -995,166 +1035,166 @@ func DashboardWeekWidget(week WeekStatsData, activeLibType string) templ.Compone if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if activeLibType == "audiobooks" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "

Listening Time

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var46 string - templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(fmtReadingMinutes(week.WeekMinutes)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 341, Col: 108} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var47 string - templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(weekVsPrevLabel(week.WeekMinutes, week.PrevMinutes)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 342, Col: 122} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "

Sessions

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "

Listening Time

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var48 string - templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", week.WeekSessions)) + templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(fmtReadingMinutes(week.WeekMinutes)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 346, Col: 109} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 359, Col: 108} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "

Books Touched

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var49 string - templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", week.WeekBooks)) + templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(weekVsPrevLabel(week.WeekMinutes, week.PrevMinutes)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 350, Col: 106} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 360, Col: 122} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "

Reading Time

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "

Sessions

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var50 string - templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(fmtReadingMinutes(week.WeekMinutes)) + templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", week.WeekSessions)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 355, Col: 108} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 364, Col: 109} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "

Books Touched

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var51 string - templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(weekVsPrevLabel(week.WeekMinutes, week.PrevMinutes)) + templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", week.WeekBooks)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 356, Col: 122} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 368, Col: 106} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "

Pages

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "

Reading Time

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var52 string - templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", week.WeekPages)) + templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(fmtReadingMinutes(week.WeekMinutes)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 360, Col: 106} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 373, Col: 108} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "

Sessions

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var53 string - templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", week.WeekSessions)) + templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(weekVsPrevLabel(week.WeekMinutes, week.PrevMinutes)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 364, Col: 109} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 374, Col: 122} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "

Books Touched

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "

Pages

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var54 string - templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", week.WeekBooks)) + templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", week.WeekPages)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 368, Col: 106} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 378, Col: 106} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "

Sessions

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var55 string + templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", week.WeekSessions)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 382, Col: 109} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "

Books Touched

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var56 string + templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", week.WeekBooks)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 386, Col: 106} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "

Synced from your reading devices ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "

Synced from your reading devices ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if week.CurrentStreak > 0 { if week.ActiveToday { - var templ_7745c5c3_Var55 string - templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(" · 🔥 %d-day streak — keep it going today", week.CurrentStreak)) + var templ_7745c5c3_Var57 string + templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(" · 🔥 %d-day streak — keep it going today", week.CurrentStreak)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 376, Col: 89} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 394, Col: 89} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - var templ_7745c5c3_Var56 string - templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(" · 🔥 %d-day streak — read today to keep it", week.CurrentStreak)) + var templ_7745c5c3_Var58 string + templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(" · 🔥 %d-day streak — read today to keep it", week.CurrentStreak)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 378, Col: 91} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 396, Col: 91} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/templates/partials_render_test.go b/templates/partials_render_test.go index 22dfd0c1..8880fa54 100644 --- a/templates/partials_render_test.go +++ b/templates/partials_render_test.go @@ -47,3 +47,47 @@ func TestRenderingPartialsRender(t *testing.T) { } } } + +// The unlinked-books queue banner renders only when the queue is +// non-empty (hidden-unless-needed). +func TestDashboardUnlinkedBanner(t *testing.T) { + render := func(count int) string { + var b strings.Builder + err := Dashboard(User{ID: "u1"}, nil, nil, nil, "", nil, 0, nil, "", "", count).Render(context.Background(), &b) + if err != nil { + t.Fatalf("render: %v", err) + } + return b.String() + } + if out := render(0); strings.Contains(out, "/unlinked-books") { + t.Error("banner rendered with empty queue") + } + out := render(2) + if !strings.Contains(out, "/unlinked-books") || !strings.Contains(out, "could not be identified") { + t.Error("banner missing with non-empty queue") + } +} + +// The unlinked-books page renders queue rows standalone. +func TestUnlinkedBooksPageRender(t *testing.T) { + var b strings.Builder + user := User{ID: "u1", Timezone: "UTC"} + books := []UnlinkedBookData{{ + ProgressID: "ub1", + DeviceName: "Kobo Clara", + DeviceType: "kobo", + FilePath: "/mnt/onboard/book.epub", + SHA256: "abc123", + TitleFromDevice: "Some Sideloaded Book", + LastSyncTime: "2026-10-18 12:00", + }} + if err := UnlinkedBooks(user, books).Render(context.Background(), &b); err != nil { + t.Fatalf("render: %v", err) + } + out := b.String() + for _, want := range []string{"Some Sideloaded Book", "abc123", `data-progress-id="ub1"`, "searchMatches"} { + if !strings.Contains(out, want) { + t.Errorf("unlinked page missing %q", want) + } + } +} diff --git a/templates/unlinked_books_templ.go b/templates/unlinked_books_templ.go index 65d1aa69..c76e229b 100644 --- a/templates/unlinked_books_templ.go +++ b/templates/unlinked_books_templ.go @@ -111,46 +111,56 @@ func UnlinkedMatchCards(progressId string, matches []services.BookMatch) templ.C if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "

by ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var7 string - templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(match.Author) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 35, Col: 79} + if match.Author != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "

by ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(match.Author) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 36, Col: 80} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var8 string templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f%% confidence", match.Confidence*100)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 38, Col: 64} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 40, Col: 64} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var9 string templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(match.MatchMethod) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 40, Col: 85} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 42, Col: 85} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -181,7 +191,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component templ_7745c5c3_Var10 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "Unlinked Books - Bookhoard") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "Unlinked Books - Bookhoard") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -190,7 +200,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" x-data=\"unlinkedBooks\" x-init=\"setupEventDelegation()\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -211,7 +221,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -219,12 +229,12 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "

Unlinked Books

Resolve books that couldn't be automatically matched between devices

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "

Unlinked Books

Resolve books that couldn't be automatically matched between devices

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(unlinkedBooks) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
0 selected
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "Bulk Manual Link") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(unlinkedBooks) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -266,39 +276,39 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "

All Books Linked!

There are no unlinked books that need manual resolution.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "

All Books Linked!

There are no unlinked books that need manual resolution.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } for _, book := range unlinkedBooks { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" onchange=\"updateSelectedCount()\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -318,46 +328,46 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var15 string templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(book.TitleFromDevice) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 123, Col: 111} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 125, Col: 111} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "

Device: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "

Device: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var16 string templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(book.DeviceName) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 125, Col: 37} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 127, Col: 37} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " (") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " (") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var17 string templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(book.DeviceType) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 125, Col: 58} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 127, Col: 58} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, ")

File Path: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, ")

File Path: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -368,98 +378,98 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component var templ_7745c5c3_Var18 string templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(book.FilePath) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 134, Col: 27} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 136, Col: 27} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if book.SHA256 != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
SHA-256: SHA-256: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var20 string templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(book.SHA256) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 140, Col: 84} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 142, Col: 84} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "
Last Synced: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "
Last Synced: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var21 string templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(book.LastSyncTime) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 145, Col: 71} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 147, Col: 71} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if book.ConfidenceScore > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "
Auto-Link Confidence: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "
Auto-Link Confidence: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var22 string templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(book.ConfidenceScore) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 151, Col: 35} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 153, Col: 35} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "% confidence
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "% confidence
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "

Potential Matches

Potential Matches

Click \"Find Potential Matches\" to search

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\" class=\"space-y-3\">

Click \"Find Potential Matches\" to search

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err }