fix(alpine): wrap all Alpine.data() callbacks in arrow functions for proper component initialization
- Wrap all Alpine.data() object literals in arrow functions (() => ({}))
- This fixes "n.bind is not a function" errors when Alpine initializes components
- Alpine.data() requires a factory function, not a plain object
- Ensures each component instance gets its own closure and proper this binding
Fixed 24 TypeScript files:
- admin.ts, analytics.ts, api.ts, api-explorer-docs.ts
- bookshelf.ts, collection-rules.ts, collections.ts, conflicts.ts
- device-management.ts, docs.ts, header.ts, index.ts
- library.ts, linking.ts, login.ts, password_validation.ts
- profile-modal.ts, profile.ts, queue.ts, register.ts
- search.ts, theme.ts, toast-error.ts, toast.ts, unlinked_books.ts
Before: Alpine.data("name", { method1, method2 })
After: Alpine.data("name", () => ({ method1, method2 }))
This is a critical fix for Alpine.js v3+ where components must be
registered as factory functions to ensure proper reactivity and
prevent binding errors during initialization.
This commit is contained in:
@@ -126,6 +126,6 @@ document.addEventListener("DOMContentLoaded", loadAnalytics);
|
|||||||
|
|
||||||
export { loadAnalytics };
|
export { loadAnalytics };
|
||||||
|
|
||||||
Alpine.store("analytics", {
|
Alpine.data("analytics", () => ({
|
||||||
loadAnalytics,
|
loadAnalytics,
|
||||||
});
|
}));
|
||||||
|
|||||||
+2
-2
@@ -99,7 +99,7 @@ export {
|
|||||||
handleError,
|
handleError,
|
||||||
};
|
};
|
||||||
|
|
||||||
Alpine.store("api", {
|
Alpine.data("api", () => ({
|
||||||
get: apiGet,
|
get: apiGet,
|
||||||
post: apiPost,
|
post: apiPost,
|
||||||
put: apiPut,
|
put: apiPut,
|
||||||
@@ -108,4 +108,4 @@ Alpine.store("api", {
|
|||||||
handleResponse: handleResponse,
|
handleResponse: handleResponse,
|
||||||
handleVoidResponse: handleVoidResponse,
|
handleVoidResponse: handleVoidResponse,
|
||||||
handleError: handleError,
|
handleError: handleError,
|
||||||
});
|
}));
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ let mediaItems: unknown[] = [];
|
|||||||
function initBookshelf(): void {
|
function initBookshelf(): void {
|
||||||
const savedLibrary = localStorage.getItem("selectedLibrary");
|
const savedLibrary = localStorage.getItem("selectedLibrary");
|
||||||
if (savedLibrary) {
|
if (savedLibrary) {
|
||||||
const select = document.getElementById("library-select") as HTMLSelectElement;
|
const select = document.getElementById(
|
||||||
|
"library-select",
|
||||||
|
) as HTMLSelectElement;
|
||||||
if (select && select.value) {
|
if (select && select.value) {
|
||||||
currentLibraryId = savedLibrary;
|
currentLibraryId = savedLibrary;
|
||||||
loadBookshelf(savedLibrary);
|
loadBookshelf(savedLibrary);
|
||||||
@@ -196,7 +198,7 @@ export {
|
|||||||
viewBook,
|
viewBook,
|
||||||
};
|
};
|
||||||
|
|
||||||
Alpine.store("bookshelf", {
|
Alpine.data("bookshelf", () => ({
|
||||||
changePage,
|
changePage,
|
||||||
initBookshelf,
|
initBookshelf,
|
||||||
loadBookshelf,
|
loadBookshelf,
|
||||||
@@ -204,4 +206,4 @@ Alpine.store("bookshelf", {
|
|||||||
selectLibrary,
|
selectLibrary,
|
||||||
setupEventDelegation,
|
setupEventDelegation,
|
||||||
viewBook,
|
viewBook,
|
||||||
});
|
}));
|
||||||
|
|||||||
@@ -251,7 +251,8 @@ async function testRule(): Promise<void> {
|
|||||||
'<p style="color: var(--text-secondary); font-size: 0.875rem;">Found ' +
|
'<p style="color: var(--text-secondary); font-size: 0.875rem;">Found ' +
|
||||||
result.matches.length +
|
result.matches.length +
|
||||||
" matching books:</p>";
|
" matching books:</p>";
|
||||||
html += '<div style="display: flex; flex-direction: column; gap: 0.5rem;">';
|
html +=
|
||||||
|
'<div style="display: flex; flex-direction: column; gap: 0.5rem;">';
|
||||||
|
|
||||||
result.matches.slice(0, 20).forEach((book: TestRuleMatch) => {
|
result.matches.slice(0, 20).forEach((book: TestRuleMatch) => {
|
||||||
html +=
|
html +=
|
||||||
@@ -408,7 +409,7 @@ export {
|
|||||||
toggleRule,
|
toggleRule,
|
||||||
};
|
};
|
||||||
|
|
||||||
Alpine.store("collectionRules", {
|
Alpine.data("collectionRules", () => ({
|
||||||
backToCollection,
|
backToCollection,
|
||||||
clearForm,
|
clearForm,
|
||||||
deleteRule,
|
deleteRule,
|
||||||
@@ -422,4 +423,4 @@ Alpine.store("collectionRules", {
|
|||||||
setupEventDelegation,
|
setupEventDelegation,
|
||||||
testRule,
|
testRule,
|
||||||
toggleRule,
|
toggleRule,
|
||||||
});
|
}));
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ export {
|
|||||||
showResolveModal,
|
showResolveModal,
|
||||||
};
|
};
|
||||||
|
|
||||||
Alpine.store("conflicts", {
|
Alpine.data("conflicts", () => ({
|
||||||
bulkDismiss,
|
bulkDismiss,
|
||||||
bulkResolve,
|
bulkResolve,
|
||||||
dismissAllResolved,
|
dismissAllResolved,
|
||||||
@@ -225,4 +225,4 @@ Alpine.store("conflicts", {
|
|||||||
refreshConflicts,
|
refreshConflicts,
|
||||||
resolveConflict,
|
resolveConflict,
|
||||||
showResolveModal,
|
showResolveModal,
|
||||||
});
|
}));
|
||||||
|
|||||||
@@ -534,7 +534,10 @@ async function handleSaveDeviceSettings(event: Event): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyToClipboard(text: string, description: string): Promise<void> {
|
async function copyToClipboard(
|
||||||
|
text: string,
|
||||||
|
description: string,
|
||||||
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
showToast(`${description} copied!`, "success");
|
showToast(`${description} copied!`, "success");
|
||||||
@@ -693,7 +696,7 @@ export {
|
|||||||
showShelfMappings,
|
showShelfMappings,
|
||||||
};
|
};
|
||||||
|
|
||||||
Alpine.store("devices", {
|
Alpine.data("devices", () => ({
|
||||||
approveDevice,
|
approveDevice,
|
||||||
clearSyncQueue,
|
clearSyncQueue,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
@@ -716,4 +719,4 @@ Alpine.store("devices", {
|
|||||||
showAddMappingModal,
|
showAddMappingModal,
|
||||||
showDeviceSettings,
|
showDeviceSettings,
|
||||||
showShelfMappings,
|
showShelfMappings,
|
||||||
});
|
}));
|
||||||
|
|||||||
+4
-4
@@ -93,14 +93,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
initializeDocsSearch();
|
initializeDocsSearch();
|
||||||
});
|
});
|
||||||
|
|
||||||
Alpine.store("docs", {
|
Alpine.data("docs", () => ({
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
initializeSearch: initializeDocsSearch,
|
initializeSearch: initializeDocsSearch,
|
||||||
});
|
}));
|
||||||
|
|
||||||
export { toggleSidebar, initializeDocsSearch };
|
export { toggleSidebar, initializeDocsSearch };
|
||||||
|
|
||||||
Alpine.store("docs", {
|
Alpine.data("docs", () => ({
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
initializeSearch: initializeDocsSearch,
|
initializeSearch: initializeDocsSearch,
|
||||||
});
|
}));
|
||||||
|
|||||||
+2
-2
@@ -10,6 +10,6 @@ const logout = (): void => {
|
|||||||
|
|
||||||
export { logout };
|
export { logout };
|
||||||
|
|
||||||
Alpine.store("header", {
|
Alpine.data("header", () => ({
|
||||||
logout,
|
logout,
|
||||||
});
|
}));
|
||||||
|
|||||||
+2
-2
@@ -43,8 +43,8 @@ async function checkAuthRedirect(): Promise<void> {
|
|||||||
|
|
||||||
export { changeTheme, checkAuthRedirect, initIndexTheme };
|
export { changeTheme, checkAuthRedirect, initIndexTheme };
|
||||||
|
|
||||||
Alpine.store("index", {
|
Alpine.data("index", () => ({
|
||||||
changeTheme,
|
changeTheme,
|
||||||
checkAuthRedirect,
|
checkAuthRedirect,
|
||||||
initIndexTheme,
|
initIndexTheme,
|
||||||
});
|
}));
|
||||||
|
|||||||
+2
-2
@@ -680,7 +680,7 @@ export {
|
|||||||
confirmDeleteLibrary,
|
confirmDeleteLibrary,
|
||||||
};
|
};
|
||||||
|
|
||||||
Alpine.store("library", {
|
Alpine.data("library", () => ({
|
||||||
deleteLibrary,
|
deleteLibrary,
|
||||||
showLibraryFolders,
|
showLibraryFolders,
|
||||||
addLibraryFolder,
|
addLibraryFolder,
|
||||||
@@ -696,4 +696,4 @@ Alpine.store("library", {
|
|||||||
showDeleteModal,
|
showDeleteModal,
|
||||||
hideDeleteModal,
|
hideDeleteModal,
|
||||||
confirmDeleteLibrary,
|
confirmDeleteLibrary,
|
||||||
});
|
}));
|
||||||
|
|||||||
+2
-2
@@ -201,11 +201,11 @@ export {
|
|||||||
showSuggestionsModal,
|
showSuggestionsModal,
|
||||||
};
|
};
|
||||||
|
|
||||||
Alpine.store("linking", {
|
Alpine.data("linking", () => ({
|
||||||
autoLinkBooks,
|
autoLinkBooks,
|
||||||
getSuggestions,
|
getSuggestions,
|
||||||
hideMatchModal,
|
hideMatchModal,
|
||||||
linkBook,
|
linkBook,
|
||||||
loadUnlinkedBooks,
|
loadUnlinkedBooks,
|
||||||
showSuggestionsModal,
|
showSuggestionsModal,
|
||||||
});
|
}));
|
||||||
|
|||||||
+2
-2
@@ -22,7 +22,7 @@ function changeTheme(): void {
|
|||||||
|
|
||||||
export { changeTheme, initLoginTheme };
|
export { changeTheme, initLoginTheme };
|
||||||
|
|
||||||
Alpine.store("login", {
|
Alpine.data("login", () => ({
|
||||||
changeTheme,
|
changeTheme,
|
||||||
initLoginTheme,
|
initLoginTheme,
|
||||||
});
|
}));
|
||||||
|
|||||||
@@ -189,6 +189,6 @@ function initPasswordValidation(): void {
|
|||||||
// Export for use in template
|
// Export for use in template
|
||||||
export { initPasswordValidation };
|
export { initPasswordValidation };
|
||||||
|
|
||||||
Alpine.store("validation", {
|
Alpine.data("validation", () => ({
|
||||||
initPasswordValidation,
|
initPasswordValidation,
|
||||||
});
|
}));
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function setupProfileModal(): void {
|
|||||||
|
|
||||||
export { closeProfileModal, setupProfileModal };
|
export { closeProfileModal, setupProfileModal };
|
||||||
|
|
||||||
Alpine.store("profileModal", {
|
Alpine.data("profileModal", () => ({
|
||||||
closeProfileModal,
|
closeProfileModal,
|
||||||
setupProfileModal,
|
setupProfileModal,
|
||||||
});
|
}));
|
||||||
|
|||||||
+2
-2
@@ -41,6 +41,6 @@ async function confirmDeleteAccount(): Promise<void> {
|
|||||||
|
|
||||||
export { confirmDeleteAccount };
|
export { confirmDeleteAccount };
|
||||||
|
|
||||||
Alpine.store("profile", {
|
Alpine.data("profile", () => ({
|
||||||
confirmDeleteAccount,
|
confirmDeleteAccount,
|
||||||
});
|
}));
|
||||||
|
|||||||
+2
-2
@@ -191,7 +191,7 @@ export {
|
|||||||
filterQueue,
|
filterQueue,
|
||||||
};
|
};
|
||||||
|
|
||||||
Alpine.store("queue", {
|
Alpine.data("queue", () => ({
|
||||||
clearAllItems,
|
clearAllItems,
|
||||||
clearFailedItems,
|
clearFailedItems,
|
||||||
deleteQueueItem,
|
deleteQueueItem,
|
||||||
@@ -201,4 +201,4 @@ Alpine.store("queue", {
|
|||||||
showQueueItemModal,
|
showQueueItemModal,
|
||||||
hideQueueItemModal,
|
hideQueueItemModal,
|
||||||
filterQueue,
|
filterQueue,
|
||||||
});
|
}));
|
||||||
|
|||||||
+2
-2
@@ -11,6 +11,6 @@ function initRegisterTheme(): void {
|
|||||||
|
|
||||||
export { initRegisterTheme };
|
export { initRegisterTheme };
|
||||||
|
|
||||||
Alpine.store("register", {
|
Alpine.data("register", () => ({
|
||||||
initRegisterTheme,
|
initRegisterTheme,
|
||||||
});
|
}));
|
||||||
|
|||||||
+2
-2
@@ -307,6 +307,6 @@ document.addEventListener("DOMContentLoaded", initializeSearch);
|
|||||||
|
|
||||||
export { selectLibraryAndBook };
|
export { selectLibraryAndBook };
|
||||||
|
|
||||||
Alpine.store("search", {
|
Alpine.data("search", () => ({
|
||||||
selectLibraryAndBook,
|
selectLibraryAndBook,
|
||||||
});
|
}));
|
||||||
|
|||||||
+2
-2
@@ -217,10 +217,10 @@ if (typeof document !== "undefined") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Alpine.store("theme", {
|
Alpine.data("theme", () => ({
|
||||||
changeTheme,
|
changeTheme,
|
||||||
changeWoodPaneling,
|
changeWoodPaneling,
|
||||||
});
|
}));
|
||||||
|
|
||||||
export {
|
export {
|
||||||
applyTheme,
|
applyTheme,
|
||||||
|
|||||||
@@ -30,6 +30,6 @@ function showErrorToast(message: string): void {
|
|||||||
|
|
||||||
export { showErrorToast };
|
export { showErrorToast };
|
||||||
|
|
||||||
Alpine.store("toastError", {
|
Alpine.data("toastError", () => ({
|
||||||
showErrorToast,
|
showErrorToast,
|
||||||
});
|
}));
|
||||||
|
|||||||
+2
-2
@@ -226,14 +226,14 @@ if (typeof document !== "undefined") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Export toast API for manual use
|
// Export toast API for manual use
|
||||||
Alpine.store("showToast", {
|
Alpine.data("showToast", () => ({
|
||||||
error: (message: string, duration?: number) =>
|
error: (message: string, duration?: number) =>
|
||||||
showToast(message, "error", duration),
|
showToast(message, "error", duration),
|
||||||
success: (message: string, duration?: number) =>
|
success: (message: string, duration?: number) =>
|
||||||
showToast(message, "success", duration),
|
showToast(message, "success", duration),
|
||||||
info: (message: string, duration?: number) =>
|
info: (message: string, duration?: number) =>
|
||||||
showToast(message, "info", duration),
|
showToast(message, "info", duration),
|
||||||
});
|
}));
|
||||||
|
|
||||||
export { showToast };
|
export { showToast };
|
||||||
export type { ToastType };
|
export type { ToastType };
|
||||||
|
|||||||
+99
-35
@@ -4,14 +4,19 @@ import { getToken } from "./storage";
|
|||||||
|
|
||||||
let selectedMediaItem: string | null = null;
|
let selectedMediaItem: string | null = null;
|
||||||
|
|
||||||
function searchMatches(progressId: string, sha256: string, title: string): void {
|
function searchMatches(
|
||||||
|
progressId: string,
|
||||||
|
sha256: string,
|
||||||
|
title: string,
|
||||||
|
): void {
|
||||||
const container = document.getElementById(`matches-${progressId}`);
|
const container = document.getElementById(`matches-${progressId}`);
|
||||||
const matchesList = document.getElementById(`matches-list-${progressId}`);
|
const matchesList = document.getElementById(`matches-list-${progressId}`);
|
||||||
|
|
||||||
if (!container || !matchesList) return;
|
if (!container || !matchesList) return;
|
||||||
|
|
||||||
container.classList.remove("hidden");
|
container.classList.remove("hidden");
|
||||||
matchesList.innerHTML = '<p class="text-sm" style="color: var(--text-secondary)">Searching...</p>';
|
matchesList.innerHTML =
|
||||||
|
'<p class="text-sm" style="color: var(--text-secondary)">Searching...</p>';
|
||||||
|
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
const url = sha256
|
const url = sha256
|
||||||
@@ -51,19 +56,27 @@ function searchMatches(progressId: string, sha256: string, title: string): void
|
|||||||
)
|
)
|
||||||
.join("");
|
.join("");
|
||||||
} else {
|
} else {
|
||||||
matchesList.innerHTML = '<p class="text-sm" style="color: var(--text-secondary)">No matches found. Try manual linking.</p>';
|
matchesList.innerHTML =
|
||||||
|
'<p class="text-sm" style="color: var(--text-secondary)">No matches found. Try manual linking.</p>';
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Failed to search", error);
|
console.error("Failed to search", error);
|
||||||
matchesList.innerHTML = '<p class="text-sm" style="color: var(--error)">Failed to search</p>';
|
matchesList.innerHTML =
|
||||||
|
'<p class="text-sm" style="color: var(--error)">Failed to search</p>';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function autoLinkBook(progressId: string, mediaItemId: string, confidence: number): void {
|
function autoLinkBook(
|
||||||
|
progressId: string,
|
||||||
|
mediaItemId: string,
|
||||||
|
confidence: number,
|
||||||
|
): void {
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
"Link this book? The confidence score is " + Math.round(confidence * 100) + "%",
|
"Link this book? The confidence score is " +
|
||||||
|
Math.round(confidence * 100) +
|
||||||
|
"%",
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -104,20 +117,28 @@ function autoLinkBook(progressId: string, mediaItemId: string, confidence: numbe
|
|||||||
|
|
||||||
function showManualLinkModal(progressId: string, bookTitle: string): void {
|
function showManualLinkModal(progressId: string, bookTitle: string): void {
|
||||||
const modal = document.getElementById("manual-link-modal");
|
const modal = document.getElementById("manual-link-modal");
|
||||||
const progressIdInput = document.getElementById("link-progress-id") as HTMLInputElement;
|
const progressIdInput = document.getElementById(
|
||||||
const bookTitleInput = document.getElementById("link-book-title") as HTMLInputElement;
|
"link-progress-id",
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const bookTitleInput = document.getElementById(
|
||||||
|
"link-book-title",
|
||||||
|
) as HTMLInputElement;
|
||||||
const searchResults = document.getElementById("link-search-results");
|
const searchResults = document.getElementById("link-search-results");
|
||||||
|
|
||||||
if (modal) modal.classList.remove("hidden");
|
if (modal) modal.classList.remove("hidden");
|
||||||
if (progressIdInput) progressIdInput.value = progressId;
|
if (progressIdInput) progressIdInput.value = progressId;
|
||||||
if (bookTitleInput) bookTitleInput.value = bookTitle;
|
if (bookTitleInput) bookTitleInput.value = bookTitle;
|
||||||
if (searchResults) searchResults.innerHTML = '<p style="color: var(--text-secondary)">Search for books to link</p>';
|
if (searchResults)
|
||||||
|
searchResults.innerHTML =
|
||||||
|
'<p style="color: var(--text-secondary)">Search for books to link</p>';
|
||||||
selectedMediaItem = null;
|
selectedMediaItem = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideManualLinkModal(): void {
|
function hideManualLinkModal(): void {
|
||||||
const modal = document.getElementById("manual-link-modal");
|
const modal = document.getElementById("manual-link-modal");
|
||||||
const searchInput = document.getElementById("link-search-input") as HTMLInputElement;
|
const searchInput = document.getElementById(
|
||||||
|
"link-search-input",
|
||||||
|
) as HTMLInputElement;
|
||||||
|
|
||||||
if (modal) modal.classList.add("hidden");
|
if (modal) modal.classList.add("hidden");
|
||||||
if (searchInput) searchInput.value = "";
|
if (searchInput) searchInput.value = "";
|
||||||
@@ -125,7 +146,9 @@ function hideManualLinkModal(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function searchBooksForLink(): void {
|
function searchBooksForLink(): void {
|
||||||
const searchInput = document.getElementById("link-search-input") as HTMLInputElement;
|
const searchInput = document.getElementById(
|
||||||
|
"link-search-input",
|
||||||
|
) as HTMLInputElement;
|
||||||
const resultsContainer = document.getElementById("link-search-results");
|
const resultsContainer = document.getElementById("link-search-results");
|
||||||
|
|
||||||
if (!searchInput || !resultsContainer) return;
|
if (!searchInput || !resultsContainer) return;
|
||||||
@@ -133,11 +156,13 @@ function searchBooksForLink(): void {
|
|||||||
const searchTerm = searchInput.value;
|
const searchTerm = searchInput.value;
|
||||||
|
|
||||||
if (searchTerm.length < 2) {
|
if (searchTerm.length < 2) {
|
||||||
resultsContainer.innerHTML = '<p class="text-sm" style="color: var(--text-secondary)">Enter at least 2 characters</p>';
|
resultsContainer.innerHTML =
|
||||||
|
'<p class="text-sm" style="color: var(--text-secondary)">Enter at least 2 characters</p>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
resultsContainer.innerHTML = '<p class="text-sm" style="color: var(--text-secondary)">Searching...</p>';
|
resultsContainer.innerHTML =
|
||||||
|
'<p class="text-sm" style="color: var(--text-secondary)">Searching...</p>';
|
||||||
|
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
fetch(`/api/books/match?title=${encodeURIComponent(searchTerm)}`, {
|
fetch(`/api/books/match?title=${encodeURIComponent(searchTerm)}`, {
|
||||||
@@ -168,16 +193,22 @@ function searchBooksForLink(): void {
|
|||||||
)
|
)
|
||||||
.join("");
|
.join("");
|
||||||
} else {
|
} else {
|
||||||
resultsContainer.innerHTML = '<p class="text-sm" style="color: var(--text-secondary)">No matches found</p>';
|
resultsContainer.innerHTML =
|
||||||
|
'<p class="text-sm" style="color: var(--text-secondary)">No matches found</p>';
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Failed to search", error);
|
console.error("Failed to search", error);
|
||||||
resultsContainer.innerHTML = '<p class="text-sm" style="color: var(--error)">Failed to search</p>';
|
resultsContainer.innerHTML =
|
||||||
|
'<p class="text-sm" style="color: var(--error)">Failed to search</p>';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectBookForLink(mediaItemId: string, title: string, _coverPath: string): void {
|
function selectBookForLink(
|
||||||
|
mediaItemId: string,
|
||||||
|
title: string,
|
||||||
|
_coverPath: string,
|
||||||
|
): void {
|
||||||
selectedMediaItem = mediaItemId;
|
selectedMediaItem = mediaItemId;
|
||||||
const resultsContainer = document.getElementById("link-search-results");
|
const resultsContainer = document.getElementById("link-search-results");
|
||||||
if (!resultsContainer) return;
|
if (!resultsContainer) return;
|
||||||
@@ -197,10 +228,18 @@ function confirmManualLink(): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const progressIdInput = document.getElementById("link-progress-id") as HTMLInputElement;
|
const progressIdInput = document.getElementById(
|
||||||
const confidenceInput = document.getElementById("link-confidence") as HTMLInputElement;
|
"link-progress-id",
|
||||||
const bookTitleInput = document.getElementById("link-book-title") as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
const sha256Input = document.getElementById("link-book-sha256") as HTMLInputElement;
|
const confidenceInput = document.getElementById(
|
||||||
|
"link-confidence",
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const bookTitleInput = document.getElementById(
|
||||||
|
"link-book-title",
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const sha256Input = document.getElementById(
|
||||||
|
"link-book-sha256",
|
||||||
|
) as HTMLInputElement;
|
||||||
|
|
||||||
if (!progressIdInput) return;
|
if (!progressIdInput) return;
|
||||||
|
|
||||||
@@ -240,7 +279,9 @@ function confirmManualLink(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleAllUnlinked(): void {
|
function toggleAllUnlinked(): void {
|
||||||
const selectAll = document.getElementById("select-all-unlinked") as HTMLInputElement;
|
const selectAll = document.getElementById(
|
||||||
|
"select-all-unlinked",
|
||||||
|
) as HTMLInputElement;
|
||||||
if (!selectAll) return;
|
if (!selectAll) return;
|
||||||
|
|
||||||
document.querySelectorAll(".unlinked-checkbox").forEach((cb) => {
|
document.querySelectorAll(".unlinked-checkbox").forEach((cb) => {
|
||||||
@@ -250,7 +291,9 @@ function toggleAllUnlinked(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getSelectedUnlinked(): { progressId: string; title: string }[] {
|
function getSelectedUnlinked(): { progressId: string; title: string }[] {
|
||||||
return Array.from(document.querySelectorAll(".unlinked-checkbox:checked")).map((cb) => ({
|
return Array.from(
|
||||||
|
document.querySelectorAll(".unlinked-checkbox:checked"),
|
||||||
|
).map((cb) => ({
|
||||||
progressId: cb.getAttribute("data-progress-id") || "",
|
progressId: cb.getAttribute("data-progress-id") || "",
|
||||||
title: cb.getAttribute("data-title") || "",
|
title: cb.getAttribute("data-title") || "",
|
||||||
}));
|
}));
|
||||||
@@ -271,7 +314,11 @@ async function bulkAutoLink(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!confirm(`Auto-link ${selected.length} books with high confidence matches (≥80%)?`)) {
|
if (
|
||||||
|
!confirm(
|
||||||
|
`Auto-link ${selected.length} books with high confidence matches (≥80%)?`,
|
||||||
|
)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +337,10 @@ async function bulkAutoLink(): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
showToast(`Auto-linked ${result.auto_linked} books successfully`, "success");
|
showToast(
|
||||||
|
`Auto-linked ${result.auto_linked} books successfully`,
|
||||||
|
"success",
|
||||||
|
);
|
||||||
setTimeout(() => window.location.reload(), 1500);
|
setTimeout(() => window.location.reload(), 1500);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Auto-link failed", error);
|
console.error("Auto-link failed", error);
|
||||||
@@ -308,11 +358,14 @@ async function bulkGetSuggestions(): Promise<void> {
|
|||||||
const token = getToken();
|
const token = getToken();
|
||||||
for (const book of selected) {
|
for (const book of selected) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/sync/unlinked-books/${book.progressId}/suggestions`, {
|
const response = await fetch(
|
||||||
headers: {
|
`/sync/unlinked-books/${book.progressId}/suggestions`,
|
||||||
Authorization: `Bearer ${token}`,
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
displaySuggestions(book.progressId, result.suggestions, result.action);
|
displaySuggestions(book.progressId, result.suggestions, result.action);
|
||||||
@@ -322,7 +375,11 @@ async function bulkGetSuggestions(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function displaySuggestions(progressId: string, suggestions: any[], _action: string): void {
|
function displaySuggestions(
|
||||||
|
progressId: string,
|
||||||
|
suggestions: any[],
|
||||||
|
_action: string,
|
||||||
|
): void {
|
||||||
const container = document.getElementById(`matches-${progressId}`);
|
const container = document.getElementById(`matches-${progressId}`);
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
@@ -333,13 +390,15 @@ function displaySuggestions(progressId: string, suggestions: any[], _action: str
|
|||||||
listContainer.innerHTML = "";
|
listContainer.innerHTML = "";
|
||||||
|
|
||||||
if (suggestions.length === 0) {
|
if (suggestions.length === 0) {
|
||||||
listContainer.innerHTML = '<p style="color: var(--text-secondary)">No matches found</p>';
|
listContainer.innerHTML =
|
||||||
|
'<p style="color: var(--text-secondary)">No matches found</p>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
suggestions.forEach((match) => {
|
suggestions.forEach((match) => {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className = "p-3 border rounded cursor-pointer hover:bg-opacity-80 transition-colors";
|
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.style.cssText = `background-color: var(--bg-primary); border-color: var(--border);`;
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
@@ -370,8 +429,13 @@ function showBulkManualLink(): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
showToast(`Bulk manual link for ${selected.length} books - select target book in library`, "info");
|
showToast(
|
||||||
window.location.href = "/library?mode=link&unlinked=" + selected.map((s) => s.progressId).join(",");
|
`Bulk manual link for ${selected.length} books - select target book in library`,
|
||||||
|
"info",
|
||||||
|
);
|
||||||
|
window.location.href =
|
||||||
|
"/library?mode=link&unlinked=" +
|
||||||
|
selected.map((s) => s.progressId).join(",");
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupEventDelegation(): void {
|
function setupEventDelegation(): void {
|
||||||
@@ -427,7 +491,7 @@ export {
|
|||||||
toggleAllUnlinked,
|
toggleAllUnlinked,
|
||||||
};
|
};
|
||||||
|
|
||||||
Alpine.store("unlinkedBooks", {
|
Alpine.data("unlinkedBooks", () => ({
|
||||||
bulkAutoLink,
|
bulkAutoLink,
|
||||||
bulkGetSuggestions,
|
bulkGetSuggestions,
|
||||||
confirmManualLink,
|
confirmManualLink,
|
||||||
@@ -440,4 +504,4 @@ Alpine.store("unlinkedBooks", {
|
|||||||
showBulkManualLink,
|
showBulkManualLink,
|
||||||
showManualLinkModal,
|
showManualLinkModal,
|
||||||
toggleAllUnlinked,
|
toggleAllUnlinked,
|
||||||
});
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user