fix(search): link results to book detail page and add cover thumbnails
Release / build-and-push (push) Successful in 2m24s

Search results navigated to /bookshelf with no filters instead of the
selected book's page. Results now link to /media/:id and display cover
thumbnails, with cover URLs resolved server-side via ResolveMediaURL.
Removes the dead selectedBook localStorage plumbing.
This commit is contained in:
2026-07-30 14:55:00 -04:00
parent bf83492bf7
commit 5ac407057e
3 changed files with 14 additions and 30 deletions
+4
View File
@@ -1791,6 +1791,10 @@ func (mh *MediaHandler) SearchMediaItems(c *echo.Context) error {
"results": []interface{}{}, "results": []interface{}{},
}) })
} }
for i := range results {
resolved := utils.ResolveMediaURL(results[i].LibraryID, results[i].CoverImagePath)
results[i].CoverImagePath = pgtype.Text{String: resolved, Valid: resolved != ""}
}
return c.JSON(http.StatusOK, results) return c.JSON(http.StatusOK, results)
} }
+10 -20
View File
@@ -1,5 +1,4 @@
import { Alpine } from "./alpine"; import { Alpine } from "./alpine";
import { setSelectedLibrary } from "./storage";
let searchInputTimeout: ReturnType<typeof setTimeout> | null = null; let searchInputTimeout: ReturnType<typeof setTimeout> | null = null;
const SEARCH_DEBOUNCE_MS = 300; const SEARCH_DEBOUNCE_MS = 300;
@@ -190,12 +189,6 @@ function showSearchResults(results: MediaItemSummary[], query: string, activeId?
searchResults.dataset.selectedIndex = "-1"; searchResults.dataset.selectedIndex = "-1";
const libraryIconMap: Record<string, string> = {
ebooks: "📚",
comics: "📖",
manga: "🗾",
};
let html = ` let html = `
<div class="p-3 border-b" style="border-color: var(--border)"> <div class="p-3 border-b" style="border-color: var(--border)">
<p class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-secondary)"> <p class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">
@@ -206,19 +199,23 @@ function showSearchResults(results: MediaItemSummary[], query: string, activeId?
`; `;
results.forEach((item, index) => { results.forEach((item, index) => {
const icon = libraryIconMap[item.library_type_name] || "📁";
const titleHtml = highlightMatch(item.title, query); const titleHtml = highlightMatch(item.title, query);
const authorHtml = item.author ? highlightMatch(item.author, query) : ""; const authorHtml = item.author ? highlightMatch(item.author, query) : "";
const coverUrl = item.cover_image_path || "/static/placeholder-book.svg";
html += ` html += `
<div class="search-result-item p-3 border-b hover:bg-opacity-50 transition-colors cursor-pointer" <div class="search-result-item p-3 border-b hover:bg-opacity-50 transition-colors cursor-pointer"
style="border-color: var(--border); background-color: var(--bg-secondary)" style="border-color: var(--border); background-color: var(--bg-secondary)"
data-index="${index}"> data-index="${index}">
<a href="/bookshelf" <a href="/media/${item.id}" class="block">
class="block"
onclick="window.selectLibraryAndBook('${item.library_id}', '${item.id}')">
<div class="flex items-start space-x-3"> <div class="flex items-start space-x-3">
<div class="text-2xl">${icon}</div> <div class="w-10 h-14 flex-shrink-0 rounded overflow-hidden bg-gradient-to-br from-gray-700 to-gray-900">
<img src="${coverUrl}"
alt="${searchEscapeHtml(item.title)}"
class="w-full h-full object-cover"
loading="lazy"
onerror="this.src='/static/placeholder-book.svg'">
</div>
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<h4 class="text-sm font-medium truncate" style="color: var(--text-primary)"> <h4 class="text-sm font-medium truncate" style="color: var(--text-primary)">
${titleHtml} ${titleHtml}
@@ -323,15 +320,8 @@ function searchEscapeHtml(text: string): string {
return div.innerHTML; return div.innerHTML;
} }
function selectLibraryAndBook(libraryId: string, bookId: string): void { export { initializeSearch };
setSelectedLibrary(libraryId);
localStorage.setItem("selectedBook", bookId);
hideSearchResults();
}
export { selectLibraryAndBook, initializeSearch };
Alpine.data("search", () => ({ Alpine.data("search", () => ({
selectLibraryAndBook,
initializeSearch, initializeSearch,
})); }));
-10
View File
@@ -45,14 +45,6 @@ function setSelectedLibrary(libraryId: string): void {
document.cookie = `selectedLibrary=${encodeURIComponent(value)};path=/;max-age=${365 * 24 * 60 * 60};samesite=lax`; document.cookie = `selectedLibrary=${encodeURIComponent(value)};path=/;max-age=${365 * 24 * 60 * 60};samesite=lax`;
} }
function getSelectedBook(): string | null {
return localStorage.getItem("selectedBook");
}
function setSelectedBook(bookId: string): void {
localStorage.setItem("selectedBook", bookId);
}
function clearAll(): void { function clearAll(): void {
localStorage.clear(); localStorage.clear();
} }
@@ -61,14 +53,12 @@ export {
ALL_LIBRARIES, ALL_LIBRARIES,
clearAll, clearAll,
getRefreshToken, getRefreshToken,
getSelectedBook,
getSelectedLibrary, getSelectedLibrary,
getTheme, getTheme,
getToken, getToken,
removeRefreshToken, removeRefreshToken,
removeToken, removeToken,
setRefreshToken, setRefreshToken,
setSelectedBook,
setSelectedLibrary, setSelectedLibrary,
setTheme, setTheme,
setToken setToken