Files
bookhoard/templates/dashboard_templ.go
T
john-okeefe bb713f6300 Rename frontend template references: Bookmann → Bookhoard
Template changes:
- Update page titles: "Bookmann" → "Bookhoard"
- Update header branding and navigation text
- Regenerate compiled .go templates from .templ sources
- Update all UI references in HTML templates

This is part 3 of the project rename to Bookhoard.
2026-02-01 16:11:59 -05:00

49 lines
32 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 Dashboard(user User) 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>Dashboard - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><script src=\"/static/toast.js\"></script><script src=\"/static/header.js\"></script><script src=\"/static/search.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><style>\n :root {\n --bg-primary: #1a1b26;\n --bg-secondary: #16161e;\n --text-primary: #a9b1d6;\n --text-secondary: #565f89;\n --accent: #7aa2f7;\n --border: #414868;\n }\n .theme-tokyo-night {\n --bg-primary: #1a1b26;\n --bg-secondary: #16161e;\n --text-primary: #a9b1d6;\n --text-secondary: #565f89;\n --accent: #7aa2f7;\n --border: #414868;\n }\n body {\n background-color: var(--bg-primary);\n color: var(--text-primary);\n }\n .card {\n background-color: var(--bg-secondary);\n border-color: var(--border);\n }\n .btn-primary {\n background-color: var(--accent);\n color: var(--bg-primary);\n }\n .btn-primary:hover {\n opacity: 0.8;\n }\n .rating-star {\n display: inline-block;\n transition: all 0.2s ease;\n cursor: pointer;\n position: relative;\n }\n .rating-star:hover {\n transform: scale(1.1);\n }\n .half-star {\n position: relative;\n display: inline-block;\n }\n .half-star::before {\n content: '★';\n position: absolute;\n left: 0;\n top: 0;\n width: 50%;\n overflow: hidden;\n color: #facc15; /* yellow-400 */\n }\n .rating-text {\n font-size: 0.875rem;\n margin-left: 0.5rem;\n opacity: 0.8;\n }\n </style></head><body class=\"theme-tokyo-night\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Header(user, "/dashboard").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\"><!-- Header Section --><div class=\"mb-8\"><div><h2 class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">Your Media Library</h2><p style=\"color: var(--text-secondary)\">Browse and manage your media collections</p></div></div><!-- Libraries Section --><div id=\"libraries-container\" class=\"mb-8\"><!-- Libraries will be loaded here --></div><!-- Media Items Section --><div id=\"media-section\" class=\"hidden\"><!-- Search and Filter --><div class=\"mb-6\"><div class=\"flex flex-col sm:flex-row gap-4\"><button onclick=\"backToLibraries()\" class=\"btn-secondary px-4 py-2 rounded-lg\">← Back to Libraries</button> <input type=\"text\" id=\"search-input\" placeholder=\"Search media...\" class=\"flex-1 px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)\" onkeyup=\"filterMedia()\"> <select id=\"sort-select\" class=\"px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)\" onchange=\"loadMediaItems()\"><option value=\"created_at DESC\">Newest Added</option> <option value=\"created_at ASC\">Oldest Added</option> <option value=\"title ASC\">Title A-Z</option> <option value=\"title DESC\">Title Z-A</option> <option value=\"author ASC\">Author A-Z</option> <option value=\"author DESC\">Author Z-A</option> <option value=\"series ASC\">Series Order</option> <option value=\"date_published DESC\">Newest Published</option> <option value=\"copyright_year DESC\">Copyright Year (Newest)</option> <option value=\"page_count ASC\">Shortest First</option> <option value=\"page_count DESC\">Longest First</option> <option value=\"genre ASC\">Genre A-Z</option></select></div></div><!-- Filters Panel --><div class=\"filter-panel mb-6\"><details><summary class=\"cursor-pointer font-semibold mb-4 flex items-center gap-2\" style=\"color: var(--text-primary)\"><svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z\"></path></svg> Filters</summary><div class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4\"><div><label class=\"block text-sm mb-1\" style=\"color: var(--text-secondary)\">Author</label> <select id=\"filter-author\" class=\"w-full px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)\"><option value=\"\">All Authors</option></select></div><div><label class=\"block text-sm mb-1\" style=\"color: var(--text-secondary)\">Genre</label> <select id=\"filter-genre\" class=\"w-full px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)\"><option value=\"\">All Genres</option></select></div><div><label class=\"block text-sm mb-1\" style=\"color: var(--text-secondary)\">Series</label> <select id=\"filter-series\" class=\"w-full px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)\"><option value=\"\">All Series</option></select></div><div><label class=\"block text-sm mb-1\" style=\"color: var(--text-secondary)\">Language</label> <select id=\"filter-language\" class=\"w-full px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)\"><option value=\"\">All Languages</option> <option value=\"en\">English</option> <option value=\"es\">Spanish</option> <option value=\"fr\">French</option> <option value=\"de\">German</option> <option value=\"ja\">Japanese</option> <option value=\"zh\">Chinese</option> <option value=\"ko\">Korean</option> <option value=\"ru\">Russian</option> <option value=\"it\">Italian</option> <option value=\"pt\">Portuguese</option></select></div><div><label class=\"block text-sm mb-1\" style=\"color: var(--text-secondary)\">Year Range</label><div class=\"flex gap-2\"><input type=\"number\" id=\"filter-year-min\" placeholder=\"From\" class=\"w-1/2 px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)\" min=\"1800\" max=\"2100\"> <input type=\"number\" id=\"filter-year-max\" placeholder=\"To\" class=\"w-1/2 px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)\" min=\"1800\" max=\"2100\"></div></div><div class=\"flex items-end\"><label class=\"flex items-center gap-2 cursor-pointer\"><input type=\"checkbox\" id=\"filter-has-cover\" class=\"w-4 h-4 rounded\"> <span class=\"text-sm\" style=\"color: var(--text-secondary)\">Has Cover Image</span></label></div></div><div class=\"flex gap-2\"><button onclick=\"applyFilters()\" class=\"btn-primary px-4 py-2 rounded-lg font-medium\">Apply Filters</button> <button onclick=\"clearFilters()\" class=\"btn-secondary px-4 py-2 rounded-lg font-medium\">Clear All</button></div></details></div><!-- Active Filters Display --><div id=\"active-filters\" class=\"hidden mb-4\"><div class=\"flex flex-wrap gap-2\" id=\"active-filters-container\"><!-- Active filter chips added here dynamically --></div></div><!-- Media Grid --><div id=\"media-container\" class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\"><!-- Media items will be loaded here --></div></div><!-- Loading indicator --><div id=\"loading\" class=\"text-center py-8\" style=\"color: var(--text-secondary)\">Loading your media libraries...</div></div><script>\n let libraries = [];\n let currentLibrary = null;\n let mediaItems = [];\n\n function loadLibraries() {\n const loading = document.getElementById('loading');\n const librariesContainer = document.getElementById('libraries-container');\n \n loading.style.display = 'block';\n librariesContainer.innerHTML = '';\n\n fetch('/api/libraries/visible', {\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 libraries = data;\n renderLibraries();\n })\n .catch(error => {\n loading.style.display = 'none';\n console.error('Error loading libraries:', error);\n librariesContainer.innerHTML = '<div class=\"text-center py-8\" style=\"color: var(--text-secondary)\">Error loading libraries. Please try again.</div>';\n });\n }\n\n function renderLibraries() {\n const container = document.getElementById('libraries-container');\n \n if (libraries.length === 0) {\n container.innerHTML = '<div class=\"text-center py-8\" style=\"color: var(--text-secondary)\">No libraries available. Please contact an administrator.</div>';\n return;\n }\n\n container.innerHTML = '<div class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">' +\n libraries.map(library => {\n const icon = getLibraryIcon(library.type_name);\n return `<div class=\"card p-6 rounded-lg border hover:shadow-lg transition-shadow cursor-pointer\" onclick=\"selectLibrary('${library.id}')\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\">\n <div class=\"text-center\">\n <div class=\"text-4xl mb-3\">${icon}</div>\n <h3 class=\"font-semibold text-lg mb-2\" style=\"color: var(--text-primary)\">${library.name}</h3>\n <p class=\"text-sm mb-3\" style=\"color: var(--text-secondary)\">${library.description || ''}</p>\n <span class=\"inline-block px-3 py-1 text-sm rounded\" style=\"background-color: var(--accent); color: var(--bg-primary)\">${library.type_name}</span>\n </div>\n </div>`;\n }).join('') +\n '</div>';\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(libraryId) {\n currentLibrary = libraries.find(l => l.id === libraryId);\n if (!currentLibrary) return;\n\n const librariesContainer = document.getElementById('libraries-container');\n const mediaSection = document.getElementById('media-section');\n const loading = document.getElementById('loading');\n\n // Hide libraries, show media section\n librariesContainer.classList.add('hidden');\n mediaSection.classList.remove('hidden');\n loading.style.display = 'block';\n\n loadMediaItems();\n }\n\n function backToLibraries() {\n const librariesContainer = document.getElementById('libraries-container');\n const mediaSection = document.getElementById('media-section');\n\n librariesContainer.classList.remove('hidden');\n mediaSection.classList.add('hidden');\n currentLibrary = null;\n }\n\n function loadMediaItems() {\n if (!currentLibrary) return;\n\n const loading = document.getElementById('loading');\n const container = document.getElementById('media-container');\n\n loading.style.display = 'block';\n container.innerHTML = '';\n\n const sort = document.getElementById('sort-select').value;\n\n fetch(`/api/media-items?library_id=${currentLibrary.id}&limit=12&offset=0&sort=${sort}`, {\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 renderMediaItems();\n })\n .catch(error => {\n loading.style.display = 'none';\n console.error('Error loading media items:', error);\n container.innerHTML = '<div class=\"col-span-full text-center py-8\" style=\"color: var(--text-secondary)\">Error loading media items. Please try again.</div>';\n });\n }\n\n async function renderMediaItems() {\n const container = document.getElementById('media-container');\n container.innerHTML = '';\n\n if (mediaItems.length === 0) {\n container.innerHTML = '<div class=\"col-span-full text-center py-8\" style=\"color: var(--text-secondary)\">No media items found in this library.</div>';\n return;\n }\n\n for (const item of mediaItems) {\n const card = document.createElement('div');\n card.className = 'card p-4 rounded-lg border hover:shadow-lg transition-shadow';\n card.style.cssText = 'background-color: var(--bg-secondary); border-color: var(--border);';\n\n const coverUrl = item.cover_image_path ? item.cover_image_path : '/static/placeholder-book.svg';\n \n // Get user's rating for this media item\n const rating = await getMediaRating(item.id);\n const ratingStars = renderRatingStars(item.id, rating);\n\n card.innerHTML = '<div class=\"flex flex-col h-full\"><div class=\"flex-1\"><div class=\"aspect-w-3 aspect-h-4 mb-3 overflow-hidden rounded\"><img src=\"' + coverUrl + '\" alt=\"Cover\" class=\"w-full h-48 object-cover rounded\" onerror=\"this.src=\\'/static/placeholder-book.svg\\'\"></div><h3 class=\"font-semibold text-lg mb-1 line-clamp-2\" style=\"color: var(--text-primary)\">' + item.title + '</h3>' + (item.author ? '<p class=\"text-sm mb-2\" style=\"color: var(--text-secondary)\">by ' + item.author + '</p>' : '') + '<div class=\"mb-2\" id=\"rating-' + item.id + '\">' + ratingStars + '</div><div class=\"flex justify-between items-center mt-4\"><div class=\"flex space-x-2\"><button onclick=\"viewMediaItem(\\'' + item.id + '\\')\" class=\"px-3 py-1 text-sm border rounded hover:opacity-80\" style=\"border-color: var(--border); color: var(--text-secondary)\">View</button></div></div></div>';\n\n container.appendChild(card);\n }\n }\n\n async function getMediaRating(mediaId) {\n try {\n const response = await fetch(`/api/media-items/${mediaId}/rating`, {\n headers: {\n 'Authorization': 'Bearer ' + localStorage.getItem('token'),\n 'Content-Type': 'application/json'\n }\n });\n \n if (response.ok) {\n const data = await response.json();\n return data.rating || 0;\n }\n return 0;\n } catch (error) {\n console.error('Error fetching rating:', error);\n return 0;\n }\n }\n\n // Convert 10-point rating to 5-point with half-star precision\n function convertToFivePoint(rating) {\n return rating / 2;\n }\n\n // Convert 5-point with half-stars back to 10-point\n function convertToTenPoint(fivePointRating) {\n return Math.round(fivePointRating * 2);\n }\n\n function renderRatingStars(mediaId, currentRating) {\n const fivePointRating = convertToFivePoint(currentRating);\n let stars = '';\n \n for (let i = 1; i <= 5; i++) {\n const filled = i <= Math.floor(fivePointRating);\n const halfFilled = !filled && (i - 0.5) <= fivePointRating;\n let starHtml = '';\n \n if (filled) {\n starHtml = '★';\n } else if (halfFilled) {\n starHtml = '⭐'; // Using different symbol for half-star\n } else {\n starHtml = '☆';\n }\n \n const starClass = filled ? 'text-yellow-400' : \n halfFilled ? 'text-yellow-200' : 'text-gray-400';\n \n stars += `<span onclick=\"openRatingDialog('${mediaId}', ${currentRating})\" class=\"cursor-pointer text-xl ${starClass} hover:text-yellow-300 transition-colors\" title=\"Click to rate (supports half-stars)\">${starHtml}</span>`;\n }\n \n // Add rating text\n stars += `<span class=\"text-sm ml-2\" style=\"color: var(--text-secondary)\">${currentRating > 0 ? fivePointRating.toFixed(1) + '/5' : 'Not rated'}</span>`;\n \n return stars;\n }\n\n function openRatingDialog(mediaId, currentRating) {\n const fivePointRating = convertToFivePoint(currentRating);\n const newRating = prompt(`Rate this media (1-5, supports 0.5 increments):`, currentRating > 0 ? fivePointRating.toFixed(1) : '');\n \n if (newRating === null) return; // User cancelled\n \n const rating = parseFloat(newRating);\n if (isNaN(rating) || rating < 0.5 || rating > 5) {\n alert('Please enter a valid rating between 0.5 and 5');\n return;\n }\n \n // Round to nearest 0.5\n const roundedRating = Math.round(rating * 2) / 2;\n setRating(mediaId, convertToTenPoint(roundedRating));\n }\n\n async function setRating(mediaId, rating) {\n try {\n const response = await fetch(`/api/media-items/${mediaId}/rating`, {\n method: 'POST',\n headers: {\n 'Authorization': 'Bearer ' + localStorage.getItem('token'),\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({ rating: rating })\n });\n \n if (response.ok) {\n const data = await response.json();\n const ratingContainer = document.getElementById(`rating-${mediaId}`);\n if (ratingContainer) {\n ratingContainer.innerHTML = renderRatingStars(mediaId, data.rating);\n }\n } else {\n const errorText = await response.text();\n console.error('Error setting rating:', errorText);\n alert('Failed to set rating: ' + errorText);\n }\n } catch (error) {\n console.error('Error setting rating:', error);\n alert('Failed to set rating due to network error');\n }\n }\n\n function filterMedia() {\n const searchTerm = document.getElementById('search-input').value.toLowerCase();\n const filteredItems = mediaItems.filter(item =>\n item.title.toLowerCase().includes(searchTerm) ||\n (item.author && item.author.toLowerCase().includes(searchTerm))\n );\n\n // Temporarily replace mediaItems and re-render\n const tempItems = mediaItems;\n mediaItems = filteredItems;\n renderMediaItems();\n mediaItems = tempItems;\n }\n\n function applyFilters() {\n const filters = {\n author: document.getElementById('filter-author').value,\n genre: document.getElementById('filter-genre').value,\n series: document.getElementById('filter-series').value,\n language: document.getElementById('filter-language').value,\n yearMin: parseInt(document.getElementById('filter-year-min').value) || 0,\n yearMax: parseInt(document.getElementById('filter-year-max').value) || 0,\n hasCover: document.getElementById('filter-has-cover').checked\n };\n\n const sort = document.getElementById('sort-select').value;\n\n // Update URL for shareability\n const params = new URLSearchParams();\n params.set('library_id', currentLibrary.id);\n params.set('sort', sort);\n Object.entries(filters).forEach(([key, value]) => {\n if (value && value !== 0) {\n if (key === 'hasCover') {\n if (value === true) params.set(`has_cover`, 'true');\n } else {\n params.set(`${key}_filter`, value);\n }\n }\n });\n\n window.history.pushState({}, '', `?${params.toString()}`);\n\n loadMediaItemsWithFilters(filters, sort);\n displayActiveFilters(filters);\n }\n\n function loadMediaItemsWithFilters(filters, sort) {\n if (!currentLibrary) return;\n\n const loading = document.getElementById('loading');\n const container = document.getElementById('media-container');\n\n loading.style.display = 'block';\n container.innerHTML = '';\n\n const params = new URLSearchParams();\n params.set('library_id', currentLibrary.id);\n params.set('sort', sort);\n params.set('limit', 50);\n params.set('offset', 0);\n\n // Only add filter params if they have values\n if (filters.author) params.set('author_filter', filters.author);\n if (filters.series) params.set('series_filter', filters.series);\n if (filters.genre) params.set('genre_filter', filters.genre);\n if (filters.language) params.set('language_filter', filters.language);\n if (filters.yearMin) params.set('year_min', filters.yearMin);\n if (filters.yearMax) params.set('year_max', filters.yearMax);\n if (filters.hasCover) params.set('has_cover', 'true');\n\n fetch(`/api/media-items/filtered?${params.toString()}`, {\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.data || [];\n renderMediaItems();\n })\n .catch(error => {\n loading.style.display = 'none';\n console.error('Error loading filtered media items:', error);\n container.innerHTML = '<div class=\"col-span-full text-center py-8\" style=\"color: var(--text-secondary)\">Error loading media items. Please try again.</div>';\n });\n }\n\n function displayActiveFilters(filters) {\n const container = document.getElementById('active-filters');\n const chipsContainer = document.getElementById('active-filters-container');\n chipsContainer.innerHTML = '';\n\n let hasFilters = false;\n\n const filterLabels = {\n author: 'Author',\n genre: 'Genre',\n series: 'Series',\n language: 'Language',\n yearMin: 'Year From',\n yearMax: 'Year To',\n hasCover: 'Has Cover'\n };\n\n Object.entries(filters).forEach(([key, value]) => {\n if (value && value !== 0) {\n hasFilters = true;\n const label = filterLabels[key] || key;\n const displayValue = key === 'hasCover' ? 'Yes' : value;\n\n const chip = document.createElement('span');\n chip.className = 'px-3 py-1 rounded-full text-sm flex items-center gap-2 cursor-pointer';\n chip.style.cssText = 'background-color: var(--accent); color: var(--bg-primary)';\n chip.innerHTML = `${label}: ${displayValue} <button class=\"font-bold hover:opacity-80\">×</button>`;\n chip.onclick = () => clearFilter(key);\n chipsContainer.appendChild(chip);\n }\n });\n\n container.classList.toggle('hidden', !hasFilters);\n }\n\n function clearFilter(filterKey) {\n switch (filterKey) {\n case 'author':\n document.getElementById('filter-author').value = '';\n break;\n case 'genre':\n document.getElementById('filter-genre').value = '';\n break;\n case 'series':\n document.getElementById('filter-series').value = '';\n break;\n case 'language':\n document.getElementById('filter-language').value = '';\n break;\n case 'yearMin':\n document.getElementById('filter-year-min').value = '';\n break;\n case 'yearMax':\n document.getElementById('filter-year-max').value = '';\n break;\n case 'hasCover':\n document.getElementById('filter-has-cover').checked = false;\n break;\n }\n applyFilters();\n }\n\n function clearFilters() {\n document.getElementById('filter-author').value = '';\n document.getElementById('filter-genre').value = '';\n document.getElementById('filter-series').value = '';\n document.getElementById('filter-language').value = '';\n document.getElementById('filter-year-min').value = '';\n document.getElementById('filter-year-max').value = '';\n document.getElementById('filter-has-cover').checked = false;\n\n // Clear URL params\n const params = new URLSearchParams();\n params.set('library_id', currentLibrary.id);\n window.history.pushState({}, '', `?${params.toString()}`);\n\n loadMediaItems();\n document.getElementById('active-filters').classList.add('hidden');\n }\n\n function viewMediaItem(mediaId) {\n // TODO: Implement detailed media view\n alert('Media viewer coming soon!');\n }\n\n // Load filters from URL on page load\n document.addEventListener('DOMContentLoaded', function() {\n loadTheme();\n loadLibraries();\n\n // Restore filters from URL\n const params = new URLSearchParams(window.location.search);\n if (params.has('author_filter')) {\n document.getElementById('filter-author').value = params.get('author_filter');\n }\n if (params.has('genre_filter')) {\n document.getElementById('filter-genre').value = params.get('genre_filter');\n }\n if (params.has('series_filter')) {\n document.getElementById('filter-series').value = params.get('series_filter');\n }\n if (params.has('language_filter')) {\n document.getElementById('filter-language').value = params.get('language_filter');\n }\n if (params.has('year_min')) {\n document.getElementById('filter-year-min').value = params.get('year_min');\n }\n if (params.has('year_max')) {\n document.getElementById('filter-year-max').value = params.get('year_max');\n }\n if (params.has('has_cover')) {\n document.getElementById('filter-has-cover').checked = params.get('has_cover') === 'true';\n }\n if (params.has('sort')) {\n document.getElementById('sort-select').value = params.get('sort');\n }\n });\n </script></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate