- Expand sort dropdown with 12 sorting options - Add collapsible filter panel with 6 filter types - Implement JavaScript filter logic (applyFilters, loadMediaItemsWithFilters, displayActiveFilters, clearFilters) - Add URL state management for shareable filtered/sorted views - Add active filter chips display with clear functionality - Support filtering by author, genre, series, language, year range, has_cover - Restore filters from URL on page load
49 lines
32 KiB
Go
49 lines
32 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 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 - Bookmann</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
|