diff --git a/templates/unlinked_books.templ b/templates/unlinked_books.templ index 090fc5e..a061904 100644 --- a/templates/unlinked_books.templ +++ b/templates/unlinked_books.templ @@ -21,6 +21,27 @@ templ UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) {

Resolve books that couldn't be automatically matched between devices

+ if len(unlinkedBooks) > 0 { +
+
+ + + 0 selected +
+
+ + + +
+
+ } +
if len(unlinkedBooks) == 0 {
@@ -33,6 +54,9 @@ templ UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) { for _, book := range unlinkedBooks {
+
+ +
@@ -346,6 +370,132 @@ templ UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) { localStorage.removeItem('token'); window.location.href = '/login'; } + + // Bulk Operations Functions + + function toggleAllUnlinked() { + const selectAll = document.getElementById('select-all-unlinked'); + document.querySelectorAll('.unlinked-checkbox').forEach(cb => { + cb.checked = selectAll.checked; + }); + updateSelectedCount(); + } + + function getSelectedUnlinked() { + return Array.from(document.querySelectorAll('.unlinked-checkbox:checked')) + .map(cb => ({ + progressId: cb.getAttribute('data-progress-id'), + title: cb.getAttribute('data-title') + })); + } + + function updateSelectedCount() { + const count = document.querySelectorAll('.unlinked-checkbox:checked').length; + document.getElementById('selected-count').textContent = `${count} selected`; + } + + async function bulkAutoLink() { + const selected = getSelectedUnlinked(); + if (selected.length === 0) { + showToast('Please select at least one book', 'error'); + return; + } + + if (!confirm(`Auto-link ${selected.length} books with high confidence matches (≥80%)?`)) { + return; + } + + try { + const response = await fetch('/sync/auto-link-books', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + localStorage.getItem('token') + }, + body: JSON.stringify({ + confidence_threshold: 0.8, + limit: selected.length + }) + }); + + const result = await response.json(); + showToast(`Auto-linked ${result.auto_linked} books successfully`, 'success'); + setTimeout(() => location.reload(), 1500); + } catch (error) { + showToast('Auto-link failed: ' + error.message, 'error'); + } + } + + async function bulkGetSuggestions() { + const selected = getSelectedUnlinked(); + if (selected.length === 0) { + showToast('Please select at least one book', 'error'); + return; + } + + for (const book of selected) { + try { + const response = await fetch(`/sync/unlinked-books/${book.progressId}/suggestions`, { + headers: { + 'Authorization': 'Bearer ' + localStorage.getItem('token') + } + }); + + const result = await response.json(); + displaySuggestions(book.progressId, result.suggestions, result.action); + } catch (error) { + console.error('Failed to get suggestions:', error); + } + } + } + + function displaySuggestions(progressId, suggestions, action) { + const container = document.getElementById(`matches-${progressId}`); + if (!container) return; + + container.classList.remove('hidden'); + const listContainer = container.querySelector('.matches-list'); + listContainer.innerHTML = ''; + + if (suggestions.length === 0) { + listContainer.innerHTML = '

No matches found

'; + return; + } + + suggestions.forEach(match => { + const div = document.createElement('div'); + div.className = 'p-3 border rounded cursor-pointer hover:bg-opacity-80 transition-colors'; + div.style.cssText = `background-color: var(--bg-primary); border-color: var(--border);`; + div.innerHTML = ` +
+
+

${match.title}

+

Author: ${match.author || 'Unknown'}

+
+
+
+ ${(match.confidence * 100).toFixed(0)}% confidence +
+
${match.match_method}
+
+
+ `; + div.onclick = () => selectMatchForLink(progressId, match); + listContainer.appendChild(div); + }); + } + + function showBulkManualLink() { + const selected = getSelectedUnlinked(); + if (selected.length === 0) { + showToast('Please select at least one book', 'error'); + return; + } + + showToast(`Bulk manual link for ${selected.length} books - select target book in library`, 'info'); + // For now, redirect to manual linking. In the future, this could open a modal + window.location.href = '/library?mode=link&unlinked=' + selected.map(s => s.progressId).join(','); + } diff --git a/templates/unlinked_books_templ.go b/templates/unlinked_books_templ.go index 030869e..c2afca9 100644 --- a/templates/unlinked_books_templ.go +++ b/templates/unlinked_books_templ.go @@ -37,154 +37,164 @@ 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, 2, "

Unlinked Books

Resolve books that couldn't be automatically matched between devices

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

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, 3, "
0 selected
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(unlinkedBooks) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "

All Books Linked!

There are no unlinked books that need manual resolution.

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

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, 4, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if book.DeviceType == "koreader" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "📖") + 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, 6, "📚") + 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, 7, "📱") + 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, 8, "

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

") 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: `unlinked_books.templ`, Line: 48, Col: 131} + 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, 9, "

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

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: `unlinked_books.templ`, Line: 50, Col: 69} + 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, 10, " (") + 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: `unlinked_books.templ`, Line: 50, Col: 90} + 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, 11, ")

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

File Path: ") 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: `unlinked_books.templ`, Line: 59, Col: 59} + 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, 12, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if book.SHA256 != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
SHA-256: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
SHA-256: ") 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: `unlinked_books.templ`, Line: 66, Col: 61} + 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, 14, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
Last Synced: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
Last Synced: ") 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: `unlinked_books.templ`, Line: 72, Col: 100} + 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, 16, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if book.ConfidenceScore > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
Auto-Link Confidence: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
Auto-Link Confidence: ") 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: `unlinked_books.templ`, Line: 78, Col: 70} + 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, 18, "% confidence
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "% confidence
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "

Potential Matches

Click \"Find Potential Matches\" to search

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

Potential Matches

Click \"Find Potential Matches\" to search

") 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 }