Update all page templates to accept an optional error message parameter and display it via the ErrorToast component when data loading fails.
206 lines
30 KiB
Go
206 lines
30 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 UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) 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>Unlinked Books - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><script src=\"/static/toast.js\"></script><script src=\"/static/header.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, "/unlinked").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=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8\"><h1 class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">Unlinked Books</h1><p style=\"color: var(--text-secondary)\">Resolve books that couldn't be automatically matched between devices</p></div>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if len(unlinkedBooks) > 0 {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"flex justify-between items-center mb-6 p-4 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex items-center gap-4\"><input type=\"checkbox\" id=\"select-all-unlinked\" onchange=\"toggleAllUnlinked()\" class=\"w-5 h-5\"> <label for=\"select-all-unlinked\" class=\"cursor-pointer\" style=\"color: var(--text-primary)\">Select All</label> <span id=\"selected-count\" class=\"text-sm\" style=\"color: var(--text-secondary)\">0 selected</span></div><div class=\"flex gap-2\"><button onclick=\"bulkAutoLink()\" class=\"btn-secondary px-4 py-2 rounded-lg text-sm\">🤖 Auto-Link Selected (High Confidence)</button> <button onclick=\"bulkGetSuggestions()\" class=\"btn-secondary px-4 py-2 rounded-lg text-sm\">💡 Get Suggestions for Selected</button> <button onclick=\"showBulkManualLink()\" class=\"btn-primary px-4 py-2 rounded-lg text-sm\">🔗 Bulk Manual Link</button></div></div>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div id=\"unlinked-container\" class=\"space-y-6\">")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if len(unlinkedBooks) == 0 {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"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)\">All Books Linked!</h3><p>There are no unlinked books that need manual resolution.</p></div>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
for _, book := range unlinkedBooks {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex gap-6\"><div class=\"flex items-start pt-2\"><input type=\"checkbox\" class=\"unlinked-checkbox w-5 h-5\" data-progress-id=\"{ book.ProgressID }\" data-title=\"{ book.TitleFromDevice }\" onchange=\"updateSelectedCount()\"></div><div class=\"flex-1\"><div class=\"flex items-center gap-3 mb-4\"><div class=\"text-4xl\">")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if book.DeviceType == "koreader" {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "📖")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
} else if book.DeviceType == "kobo" {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "📚")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
} else {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "📱")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div><div><h3 class=\"font-semibold text-lg\" style=\"color: var(--text-primary)\">")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var2 string
|
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(book.TitleFromDevice)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 72, Col: 131}
|
|
}
|
|
_, 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, 11, "</h3><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Device: ")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var3 string
|
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(book.DeviceName)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 74, Col: 69}
|
|
}
|
|
_, 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, 12, " (")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var4 string
|
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(book.DeviceType)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 74, Col: 90}
|
|
}
|
|
_, 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, 13, ")</p></div></div><div class=\"space-y-2 text-sm mb-4\"><div><span style=\"color: var(--text-secondary)\">File Path:</span> <code style=\"color: var(--text-primary); background-color: var(--bg-primary); padding: 2px 6px; border-radius: 4px;\">")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var5 string
|
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(book.FilePath)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 83, Col: 59}
|
|
}
|
|
_, 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, 14, "</code></div>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if book.SHA256 != "" {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div><span style=\"color: var(--text-secondary)\">SHA-256:</span> <code style=\"color: var(--text-primary); background-color: var(--bg-primary); padding: 2px 6px; border-radius: 4px; font-size: 11px;\">")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var6 string
|
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(book.SHA256)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 90, Col: 61}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</code></div>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div><span style=\"color: var(--text-secondary)\">Last Synced:</span> <span style=\"color: var(--text-primary)\">")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var7 string
|
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(book.LastSyncTime)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 96, Col: 100}
|
|
}
|
|
_, 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, 18, "</span></div>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if book.ConfidenceScore > 0 {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div><span style=\"color: var(--text-secondary)\">Auto-Link Confidence:</span> <span style=\"color: var(--text-primary)\">")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var8 string
|
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(book.ConfidenceScore)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 102, Col: 70}
|
|
}
|
|
_, 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, 20, "% confidence</span></div>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div><div class=\"mb-4\"><button onclick=\"searchMatches('{ book.ProgressID }', '{ book.SHA256 }', '{ book.TitleFromDevice }')\" class=\"btn-secondary px-4 py-2 rounded-lg text-sm\">🔍 Find Potential Matches</button> <button onclick=\"showManualLinkModal('{ book.ProgressID }', '{ book.TitleFromDevice }')\" class=\"btn-primary px-4 py-2 rounded-lg text-sm\">🔗 Manually Link</button></div><div id=\"matches-{ book.ProgressID }\" class=\"hidden\"><div class=\"border-t pt-4\" style=\"border-color: var(--border);\"><h4 class=\"font-semibold mb-3\" style=\"color: var(--text-primary)\">Potential Matches</h4><div id=\"matches-list-{ book.ProgressID }\" class=\"space-y-3\"><p style=\"color: var(--text-secondary)\">Click \"Find Potential Matches\" to search</p></div></div></div></div></div></div>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div></div><!-- Manual Link Modal --><div id=\"manual-link-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center\" style=\"background-color: rgba(0, 0, 0, 0.7);\"><div class=\"card rounded-lg p-6 w-full max-w-md mx-4\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Manually Link Book</h2><button onclick=\"hideManualLinkModal()\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-primary)\">✕</button></div><input type=\"hidden\" id=\"link-progress-id\"> <input type=\"hidden\" id=\"link-book-sha256\"><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Book Title from Device</label> <input type=\"text\" id=\"link-book-title\" readonly class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-secondary); border-color: var(--border);\"></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Search for Book to Link</label><div class=\"flex gap-2\"><input type=\"text\" id=\"link-search-input\" placeholder=\"Search by title, author...\" class=\"flex-1 px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"> <button type=\"button\" onclick=\"searchBooksForLink()\" class=\"btn-primary px-4 py-2 rounded-lg\">Search</button></div></div><div id=\"link-search-results\" class=\"mb-4 max-h-48 overflow-y-auto space-y-2\"><p style=\"color: var(--text-secondary)\">Search for books to link</p></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Confidence Score</label> <input type=\"number\" id=\"link-confidence\" min=\"0\" max=\"1\" step=\"0.1\" value=\"1.0\" class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><p class=\"text-xs mt-1\" style=\"color: var(--text-secondary)\">1.0 = manual override (highest confidence)</p></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" onclick=\"hideManualLinkModal()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Cancel</button> <button type=\"button\" onclick=\"confirmManualLink()\" class=\"btn-primary px-4 py-2 rounded-lg\">Link Book</button></div></div></div><script>\n let selectedMediaItem = null;\n\n function searchMatches(progressId, sha256, title) {\n const container = document.getElementById(`matches-${progressId}`);\n const matchesList = document.getElementById(`matches-list-${progressId}`);\n\n container.classList.remove('hidden');\n matchesList.innerHTML = '<p class=\"text-sm\" style=\"color: var(--text-secondary)\">Searching...</p>';\n\n const url = sha256 ? `/api/books/match?sha256=${sha256}` : `/api/books/match?title=${encodeURIComponent(title)}`;\n\n fetch(url, {\n headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }\n })\n .then(response => response.json())\n .then(result => {\n if (result.matches && result.matches.length > 0) {\n matchesList.innerHTML = result.matches.map(match => `\n <div class=\"card p-4 rounded-lg border cursor-pointer hover:shadow-lg transition-shadow\"\n style=\"background-color: var(--bg-primary); border-color: var(--border);\"\n onclick=\"autoLinkBook('${progressId}', '${match.media_item_id}', ${match.confidence})\">\n <div class=\"flex gap-4\">\n <img src=\"${match.cover_image_path || '/static/placeholder-book.svg'}\"\n alt=\"Cover\" class=\"w-16 h-24 object-cover rounded\">\n <div>\n <h5 class=\"font-semibold\" style=\"color: var(--text-primary)\">${match.title}</h5>\n <p class=\"text-sm\" style=\"color: var(--text-secondary)\">by ${match.author || 'Unknown'}</p>\n <div class=\"mt-2 flex items-center gap-2\">\n <span class=\"text-xs px-2 py-1 rounded\" style=\"background-color: var(--accent); color: var(--bg-primary);\">\n ${Math.round(match.confidence * 100)}% confidence\n </span>\n <span class=\"text-xs\" style=\"color: var(--text-secondary)\">${match.match_method}</span>\n </div>\n </div>\n </div>\n </div>\n `).join('');\n } else {\n matchesList.innerHTML = '<p class=\"text-sm\" style=\"color: var(--text-secondary)\">No matches found. Try manual linking.</p>';\n }\n })\n .catch(error => {\n matchesList.innerHTML = '<p class=\"text-sm\" style=\"color: var(--error)\">Failed to search</p>';\n });\n }\n\n function autoLinkBook(progressId, mediaItemId, confidence) {\n if (!confirm('Link this book? The confidence score is ' + Math.round(confidence * 100) + '%')) return;\n\n const data = {\n device_file: {\n file_path: document.getElementById(`matches-${progressId}`).querySelector('code').textContent,\n sha256: document.getElementById(`matches-${progressId}`).querySelector('[title=\"SHA-256\"]')?.textContent || ''\n },\n media_item_id: mediaItemId,\n confidence_score: confidence\n };\n\n fetch(`/api/devices/${deviceId}/sync/link-book`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + localStorage.getItem('token')\n },\n body: JSON.stringify(data)\n })\n .then(response => response.json())\n .then(result => {\n showToast('Book linked successfully', 'success');\n location.reload();\n })\n .catch(error => {\n showToast('Failed to link book', 'error');\n });\n }\n\n function showManualLinkModal(progressId, bookTitle) {\n document.getElementById('link-progress-id').value = progressId;\n document.getElementById('link-book-title').value = bookTitle;\n document.getElementById('link-search-results').innerHTML = '<p style=\"color: var(--text-secondary)\">Search for books to link</p>';\n selectedMediaItem = null;\n document.getElementById('manual-link-modal').classList.remove('hidden');\n }\n\n function hideManualLinkModal() {\n document.getElementById('manual-link-modal').classList.add('hidden');\n document.getElementById('link-search-input').value = '';\n selectedMediaItem = null;\n }\n\n function searchBooksForLink() {\n const searchTerm = document.getElementById('link-search-input').value;\n const resultsContainer = document.getElementById('link-search-results');\n\n if (searchTerm.length < 2) {\n resultsContainer.innerHTML = '<p class=\"text-sm\" style=\"color: var(--text-secondary)\">Enter at least 2 characters</p>';\n return;\n }\n\n resultsContainer.innerHTML = '<p class=\"text-sm\" style=\"color: var(--text-secondary)\">Searching...</p>';\n\n fetch(`/api/books/match?title=${encodeURIComponent(searchTerm)}`, {\n headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }\n })\n .then(response => response.json())\n .then(result => {\n if (result.matches && result.matches.length > 0) {\n resultsContainer.innerHTML = result.matches.map(match => `\n <div class=\"card p-3 rounded-lg border cursor-pointer ${selectedMediaItem === match.media_item_id ? 'border-2 border-blue-500' : ''}\"\n style=\"background-color: var(--bg-primary); border-color: var(--border);\"\n onclick=\"selectBookForLink('${match.media_item_id}', '${match.title}', '${match.cover_image_path || ''}')\">\n <div class=\"flex gap-3\">\n <img src=\"${match.cover_image_path || '/static/placeholder-book.svg'}\"\n alt=\"Cover\" class=\"w-12 h-16 object-cover rounded\">\n <div>\n <h5 class=\"font-semibold text-sm\" style=\"color: var(--text-primary)\">${match.title}</h5>\n <p class=\"text-xs\" style=\"color: var(--text-secondary)\">by ${match.author || 'Unknown'}</p>\n <p class=\"text-xs\" style=\"color: var(--accent)\">${Math.round(match.confidence * 100)}% confidence</p>\n </div>\n </div>\n </div>\n `).join('');\n } else {\n resultsContainer.innerHTML = '<p class=\"text-sm\" style=\"color: var(--text-secondary)\">No matches found</p>';\n }\n })\n .catch(error => {\n resultsContainer.innerHTML = '<p class=\"text-sm\" style=\"color: var(--error)\">Failed to search</p>';\n });\n }\n\n function selectBookForLink(mediaItemId, title, coverPath) {\n selectedMediaItem = mediaItemId;\n const resultsContainer = document.getElementById('link-search-results');\n const cards = resultsContainer.querySelectorAll('.card');\n cards.forEach(card => {\n card.classList.remove('border-2', 'border-blue-500');\n if (card.getAttribute('onclick').includes(mediaItemId)) {\n card.classList.add('border-2', 'border-blue-500');\n }\n });\n }\n\n function confirmManualLink() {\n if (!selectedMediaItem) {\n showToast('Please select a book to link', 'error');\n return;\n }\n\n const progressId = document.getElementById('link-progress-id').value;\n const confidence = parseFloat(document.getElementById('link-confidence').value);\n\n const data = {\n device_file: {\n file_path: document.getElementById('link-book-title').value,\n sha256: document.getElementById('link-book-sha256').value\n },\n media_item_id: selectedMediaItem,\n confidence_score: confidence\n };\n\n fetch(`/api/devices/${deviceId}/sync/link-book`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + localStorage.getItem('token')\n },\n body: JSON.stringify(data)\n })\n .then(response => response.json())\n .then(result => {\n showToast('Book linked successfully', 'success');\n hideManualLinkModal();\n location.reload();\n })\n .catch(error => {\n showToast('Failed to link book', 'error');\n });\n }\n\n function logout() {\n localStorage.removeItem('token');\n window.location.href = '/login';\n }\n\n // Bulk Operations Functions\n\n function toggleAllUnlinked() {\n const selectAll = document.getElementById('select-all-unlinked');\n document.querySelectorAll('.unlinked-checkbox').forEach(cb => {\n cb.checked = selectAll.checked;\n });\n updateSelectedCount();\n }\n\n function getSelectedUnlinked() {\n return Array.from(document.querySelectorAll('.unlinked-checkbox:checked'))\n .map(cb => ({\n progressId: cb.getAttribute('data-progress-id'),\n title: cb.getAttribute('data-title')\n }));\n }\n\n function updateSelectedCount() {\n const count = document.querySelectorAll('.unlinked-checkbox:checked').length;\n document.getElementById('selected-count').textContent = `${count} selected`;\n }\n\n async function bulkAutoLink() {\n const selected = getSelectedUnlinked();\n if (selected.length === 0) {\n showToast('Please select at least one book', 'error');\n return;\n }\n\n if (!confirm(`Auto-link ${selected.length} books with high confidence matches (≥80%)?`)) {\n return;\n }\n\n try {\n const response = await fetch('/sync/auto-link-books', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + localStorage.getItem('token')\n },\n body: JSON.stringify({\n confidence_threshold: 0.8,\n limit: selected.length\n })\n });\n\n const result = await response.json();\n showToast(`Auto-linked ${result.auto_linked} books successfully`, 'success');\n setTimeout(() => location.reload(), 1500);\n } catch (error) {\n showToast('Auto-link failed: ' + error.message, 'error');\n }\n }\n\n async function bulkGetSuggestions() {\n const selected = getSelectedUnlinked();\n if (selected.length === 0) {\n showToast('Please select at least one book', 'error');\n return;\n }\n\n for (const book of selected) {\n try {\n const response = await fetch(`/sync/unlinked-books/${book.progressId}/suggestions`, {\n headers: {\n 'Authorization': 'Bearer ' + localStorage.getItem('token')\n }\n });\n\n const result = await response.json();\n displaySuggestions(book.progressId, result.suggestions, result.action);\n } catch (error) {\n console.error('Failed to get suggestions:', error);\n }\n }\n }\n\n function displaySuggestions(progressId, suggestions, action) {\n const container = document.getElementById(`matches-${progressId}`);\n if (!container) return;\n\n container.classList.remove('hidden');\n const listContainer = container.querySelector('.matches-list');\n listContainer.innerHTML = '';\n\n if (suggestions.length === 0) {\n listContainer.innerHTML = '<p style=\"color: var(--text-secondary)\">No matches found</p>';\n return;\n }\n\n suggestions.forEach(match => {\n const div = document.createElement('div');\n div.className = 'p-3 border rounded cursor-pointer hover:bg-opacity-80 transition-colors';\n div.style.cssText = `background-color: var(--bg-primary); border-color: var(--border);`;\n div.innerHTML = `\n <div class=\"flex justify-between items-center\">\n <div>\n <h4 class=\"font-semibold\" style=\"color: var(--text-primary)\">${match.title}</h4>\n <p class=\"text-sm\" style=\"color: var(--text-secondary)\">Author: ${match.author || 'Unknown'}</p>\n </div>\n <div class=\"text-right\">\n <div class=\"text-sm font-semibold\" style=\"color: var(--text-primary)\">\n ${(match.confidence * 100).toFixed(0)}% confidence\n </div>\n <div class=\"text-xs\" style=\"color: var(--text-secondary)\">${match.match_method}</div>\n </div>\n </div>\n `;\n div.onclick = () => selectMatchForLink(progressId, match);\n listContainer.appendChild(div);\n });\n }\n\n function showBulkManualLink() {\n const selected = getSelectedUnlinked();\n if (selected.length === 0) {\n showToast('Please select at least one book', 'error');\n return;\n }\n\n showToast(`Bulk manual link for ${selected.length} books - select target book in library`, 'info');\n // For now, redirect to manual linking. In the future, this could open a modal\n window.location.href = '/library?mode=link&unlinked=' + selected.map(s => s.progressId).join(',');\n }\n </script></body></html>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
var _ = templruntime.GeneratedTemplate
|