Files
bookhoard/templates/bookshelf_templ.go
T
john-okeefe c6fa217092 feat: Add library ID support to media scanner and worker
Add default library ID functionality to improve library targeting
during media scans.

Service changes in internal/services/media_scanner.go:
- Add defaultLibraryID field to MediaScanner struct
- Add SetLibraryID() method to set default library
- Modify processMediaFile() to use defaultLibraryID when set
  - Prioritizes defaultLibraryID over folder-based library detection
  - Provides explicit library targeting for scans

Service changes in internal/services/worker.go:
- Add libraryUUID conversion from string to pgtype.UUID
- Call scanner.SetLibraryID() before ScanFolders()
  - Ensures scanner respects the job's library ID

These changes enable more precise library targeting during media scans,
allowing scans to be directed to specific libraries rather than relying
solely on folder-based detection.
2026-03-01 00:29:39 -05:00

92 lines
14 KiB
Go

// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.977
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func BookShelf(user User, libraries []LibraryData) 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 {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Library - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><script src=\"/static/toast.js\"></script><script src=\"/static/search.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-{ user.Theme }\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Header(user, "/bookshelf").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"w-full px-4 sm:px-6 lg:8 py-8\"><!-- Library Selector --><div class=\"mb-8\"><div class=\"flex flex-col sm:flex-row gap-4 items-center justify-between\"><div class=\"flex-1 w-full\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Select Library</label> <select id=\"library-select\" onchange=\"selectLibrary()\" class=\"w-full px-4 py-3 border rounded-lg text-lg\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(libraries) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<option value=\"\">No libraries available</option>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
for _, lib := range libraries {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<option value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(lib.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 28, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 28, Col: 71}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</option>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</select></div><div class=\"flex gap-2\"><button onclick=\"loadBookshelf()\" class=\"btn-primary px-6 py-3 rounded-lg font-medium\">Refresh</button></div></div></div><!-- Bookshelf Container --><div id=\"bookshelf-container\"><!-- Loading state --><div id=\"loading\" class=\"text-center py-16\" style=\"color: var(--text-secondary)\"><div class=\"loading-spinner mx-auto mb-4\"></div><p>Loading your library...</p></div><!-- Empty state --><div id=\"empty-state\" class=\"hidden text-center py-16\" style=\"color: var(--text-secondary)\"><div class=\"text-6xl mb-4\">📚</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Books Yet</h3><p>Select a library to view your collection</p></div><!-- Books Grid --><div id=\"books-grid\" class=\"hidden\"><!-- Books will be loaded here on shelves --></div></div></div><script src=\"/static/theme.js\"></script><script>\n // Libraries already loaded server-side\n const libraries = [\n if len(libraries) > 0 {\n for _, lib := range libraries {\n {\n id: \"{ lib.ID }\",\n name: \"{ lib.Name }\",\n description: \"{ lib.Description }\",\n type_name: \"{ lib.TypeName }\"\n },\n }\n }\n ];\n let currentLibrary = null;\n let mediaItems = [];\n\n function selectLibrary() {\n const librarySelect = document.getElementById('library-select');\n const selectedId = librarySelect.value;\n currentLibrary = libraries.find(lib => lib.id === selectedId);\n if (currentLibrary) {\n loadBookshelf();\n }\n }\n\n // Auto-select first library if available\n document.addEventListener('DOMContentLoaded', function() {\n loadTheme();\n if (libraries.length > 0) {\n const librarySelect = document.getElementById('library-select');\n currentLibrary = libraries[0];\n librarySelect.value = currentLibrary.id;\n loadBookshelf();\n } else {\n showEmptyState();\n }\n });\n })\n .then(response => response.json())\n .then(data => {\n loading.style.display = 'none';\n libraries = data;\n populateLibrarySelect();\n \n // Auto-select first library if available\n if (libraries.length > 0 && !currentLibrary) {\n currentLibrary = libraries[0];\n librarySelect.value = currentLibrary.id;\n loadBookshelf();\n }\n })\n .catch(error => {\n loading.style.display = 'none';\n console.error('Error loading libraries:', error);\n showToast('Error loading libraries', 'error');\n });\n }\n\n function populateLibrarySelect() {\n const librarySelect = document.getElementById('library-select');\n \n if (libraries.length === 0) {\n librarySelect.innerHTML = '<option value=\"\">No libraries available</option>';\n return;\n }\n\n librarySelect.innerHTML = '<option value=\"\">Select a library...</option>' +\n libraries.map(library => {\n const icon = getLibraryIcon(library.type_name);\n return `<option value=\"${library.id}\">${icon} ${library.name}</option>`;\n }).join('');\n }\n\n function getLibraryIcon(typeName) {\n switch (typeName) {\n case 'ebooks': return '📚';\n case 'comics': return '📖';\n case 'manga': return '🗾';\n default: return '📁';\n }\n }\n\n function selectLibrary() {\n const librarySelect = document.getElementById('library-select');\n const libraryId = librarySelect.value;\n \n currentLibrary = libraries.find(l => l.id === libraryId);\n loadBookshelf();\n }\n\n function loadBookshelf() {\n if (!currentLibrary) {\n showEmptyState();\n return;\n }\n\n const loading = document.getElementById('loading');\n const booksGrid = document.getElementById('books-grid');\n const emptyState = document.getElementById('empty-state');\n\n loading.style.display = 'block';\n booksGrid.classList.add('hidden');\n emptyState.classList.add('hidden');\n\n fetch(`/api/media-items?library_id=${currentLibrary.id}&limit=100&offset=0`, {\n headers: {\n 'Authorization': 'Bearer ' + localStorage.getItem('token'),\n 'Content-Type': 'application/json'\n }\n })\n .then(response => response.json())\n .then(data => {\n loading.style.display = 'none';\n mediaItems = data;\n \n if (mediaItems.length === 0) {\n showEmptyState();\n } else {\n renderBookshelf();\n }\n })\n .catch(error => {\n loading.style.display = 'none';\n console.error('Error loading bookshelf:', error);\n showToast('Error loading books', 'error');\n });\n }\n\n function showEmptyState() {\n const booksGrid = document.getElementById('books-grid');\n const emptyState = document.getElementById('empty-state');\n const loading = document.getElementById('loading');\n \n loading.style.display = 'none';\n booksGrid.classList.add('hidden');\n emptyState.classList.remove('hidden');\n }\n\n function renderBookshelf() {\n const booksGrid = document.getElementById('books-grid');\n const emptyState = document.getElementById('empty-state');\n const loading = document.getElementById('loading');\n \n loading.style.display = 'none';\n emptyState.classList.add('hidden');\n booksGrid.classList.remove('hidden');\n\n // Group books into shelves (6 books per shelf for better display)\n const booksPerShelf = 6;\n const shelves = [];\n \n for (let i = 0; i < mediaItems.length; i += booksPerShelf) {\n shelves.push(mediaItems.slice(i, i + booksPerShelf));\n }\n\n let html = '';\n shelves.forEach((shelfBooks, index) => {\n html += `<div class=\"relative bg-gradient-to-b from-transparent to-black/10 p-8 mb-4 rounded-lg\" style=\"padding-bottom: 3rem;\">\n <div class=\"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4\">\n ${shelfBooks.map(book => renderBookCard(book)).join('')}\n </div>\n <div class=\"absolute bottom-0 left-0 right-0 h-3 rounded-b-lg\" style=\"background: linear-gradient(to bottom, rgba(107, 68, 35, 0.3) 0%, rgba(107, 68, 35, 0.5) 50%, rgba(107, 68, 35, 0.3) 100%); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\"></div>\n </div>`;\n });\n\n booksGrid.innerHTML = html;\n }\n\n function renderBookCard(book) {\n const coverUrl = book.cover_image_path || '/static/placeholder-book.svg';\n const authorHtml = book.author ? `<p class=\"text-xs\" style=\"color: var(--text-secondary)\">${book.author}</p>` : '';\n\n return `<div class=\"relative transition-all duration-200 ease hover:-translate-y-2 hover:-rotate-2 hover:shadow-2xl hover:z-10 cursor-pointer\" onclick=\"viewBook('${book.id}')\">\n <div class=\"aspect-[2/3] overflow-hidden rounded shadow-[2px_2px_4px_rgba(0,0,0,0.2),-1px_-1px_2px_rgba(255,255,255,0.1)_inset] relative\">\n <div class=\"absolute left-0 top-0 bottom-0 w-1\" style=\"background: linear-gradient(to right, rgba(0, 0, 0, 0.3) 0%, rgba(255, 255, 255, 0.1) 50%, transparent 100%);\"></div>\n <img src=\"${coverUrl}\"\n alt=\"${book.title}\"\n class=\"w-full h-full object-cover\"\n onerror=\"this.src='/static/placeholder-book.svg'\"\n >\n </div>\n <div class=\"mt-2\">\n <h3 class=\"text-sm font-semibold line-clamp-2\" style=\"color: var(--text-primary)\">${book.title}</h3>\n ${authorHtml}\n </div>\n </div>`;\n }\n\n function viewBook(bookId) {\n // TODO: Navigate to book detail view\n showToast('Book viewer coming soon!', 'info');\n }\n\n // Initialize\n document.addEventListener('DOMContentLoaded', function() {\n loadTheme();\n loadLibraries();\n });\n </script></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate