document.addEventListener('DOMContentLoaded', () => { // ===== Theme Toggle ===== const themeCheckbox = document.getElementById('theme-toggle-checkbox'); const themeIconLight = document.getElementById('theme-icon-light'); const themeLabelSpan = document.querySelector('.theme-toggle-label span'); function updateTheme(theme) { document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('theme', theme); if (themeCheckbox) { themeCheckbox.checked = theme === 'dark'; } if (themeIconLight) { themeIconLight.className = theme === 'dark' ? 'mdi mdi-weather-night' : 'mdi mdi-weather-sunny'; } if (themeLabelSpan) { themeLabelSpan.textContent = theme === 'dark' ? 'Dark Mode' : 'Light Mode'; } } // Init Theme const savedTheme = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); updateTheme(savedTheme); if (themeCheckbox) { themeCheckbox.addEventListener('change', () => { const next = themeCheckbox.checked ? 'dark' : 'light'; updateTheme(next); }); } // ===== Mobile Sidebar Toggle ===== const sidebar = document.getElementById('sidebar'); const sidebarOverlay = document.getElementById('sidebar-overlay'); const mobileMenuBtn = document.getElementById('mobile-menu-btn'); function toggleSidebar() { sidebar?.classList.toggle('show'); sidebarOverlay?.classList.toggle('show'); } mobileMenuBtn?.addEventListener('click', toggleSidebar); sidebarOverlay?.addEventListener('click', toggleSidebar); // ===== Search Overlay (Spotlight Style) ===== const searchOverlay = document.getElementById('search-overlay'); const searchToggleBtn = document.getElementById('search-toggle-btn'); const searchModalInput = document.getElementById('search-modal-input'); const searchResults = document.getElementById('search-results'); const searchTagsBtn = document.getElementById('search-tags-btn'); const searchAllBtn = document.getElementById('search-all-btn'); const searchForm = document.getElementById('search-form'); let searchMode = 'search'; // 'search' or 'tags' let selectedIndex = -1; let searchTimeout = null; let cachedBookmarks = null; let cachedTags = null; // Highlight matching text with tags function highlightMatch(text, query) { if (!query || query.length === 0) return text; // Escape special regex characters in query const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const regex = new RegExp(`(${escapedQuery})`, 'gi'); return text.replace(regex, '$1'); } // Fuzzy search - matches substring anywhere in text function fuzzyMatch(text, query) { if (!query || query.length === 0) return true; return text.toLowerCase().includes(query.toLowerCase()); } // Fetch all unique tags from the page function fetchTags() { if (cachedTags) return cachedTags; try { const tagElements = document.querySelectorAll('.link-tag, .tag-link, [class*="tag"]'); const tagsSet = new Set(); tagElements.forEach(el => { const tagText = el.textContent.trim(); if (tagText && tagText.length > 0 && !tagText.includes('•')) { tagsSet.add(tagText); } }); // Convert to array and sort alphabetically cachedTags = Array.from(tagsSet).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); return cachedTags; } catch (e) { console.error('Failed to fetch tags:', e); return []; } } // Fetch bookmarks for live search async function fetchBookmarks() { if (cachedBookmarks) return cachedBookmarks; try { // Try to get bookmarks from page (if already loaded) const linkElements = document.querySelectorAll('.link-outer'); if (linkElements.length > 0) { cachedBookmarks = Array.from(linkElements).map(el => ({ id: el.dataset.id, title: el.querySelector('.link-title a')?.textContent || el.querySelector('.link-title')?.textContent || '', url: el.querySelector('.link-url a')?.href || el.querySelector('.link-title a')?.href || '', tags: Array.from(el.querySelectorAll('.link-tag')).map(t => t.textContent.trim()), description: el.querySelector('.link-description')?.textContent || '' })); return cachedBookmarks; } } catch (e) { console.error('Failed to fetch bookmarks:', e); } return []; } // Render search results (tags or bookmarks) function renderResults(results, query, isTagMode = false) { if (!searchResults) return; if (results.length === 0) { if (query && query.length > 0) { searchResults.innerHTML = `
No results found for "${escapeHtml(query)}"
`; } else { searchResults.innerHTML = `
Start typing to see tag suggestions...
`; } return; } let html; if (isTagMode) { // Render tags html = results.slice(0, 15).map((tag, index) => { const highlightedTag = highlightMatch(escapeHtml(tag), query); return `
${highlightedTag}
`; }).join(''); } else { // Render bookmarks html = results.slice(0, 10).map((item, index) => { const highlightedTitle = highlightMatch(escapeHtml(item.title), query); return `
${highlightedTitle}
`; }).join(''); } searchResults.innerHTML = html; // Add click handlers to results searchResults.querySelectorAll('.search-result-item').forEach(item => { item.addEventListener('click', () => { if (isTagMode) { const tag = item.dataset.tag; if (tag) { // Navigate to tag search window.location.href = shaarli.basePath + '/?searchtags=' + encodeURIComponent(tag); } } else { const url = item.dataset.url; if (url) { window.location.href = url; } } }); }); } // Escape HTML to prevent XSS function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Perform live search async function performSearch(query) { const tags = fetchTags(); if (!query || query.length === 0) { // Show all tags when empty renderResults(tags.slice(0, 15), '', true); return; } // Filter tags with fuzzy matching const results = tags.filter(tag => fuzzyMatch(tag, query)); renderResults(results, query, true); } // Update selected result function updateSelection(newIndex) { const items = searchResults?.querySelectorAll('.search-result-item'); if (!items || items.length === 0) return; // Clamp index if (newIndex < 0) newIndex = items.length - 1; if (newIndex >= items.length) newIndex = 0; selectedIndex = newIndex; items.forEach((item, index) => { item.classList.toggle('selected', index === selectedIndex); }); // Scroll into view items[selectedIndex]?.scrollIntoView({ block: 'nearest' }); } // Navigate to selected result function navigateToSelected() { const selected = searchResults?.querySelector('.search-result-item.selected'); if (selected) { // Check if it's a tag const tag = selected.dataset.tag; if (tag) { window.location.href = shaarli.basePath + '/?searchtags=' + encodeURIComponent(tag); return true; } // Otherwise check for URL const url = selected.dataset.url; if (url) { window.location.href = url; return true; } } return false; } function openSearch() { searchOverlay?.classList.add('show'); selectedIndex = -1; setTimeout(() => { searchModalInput?.focus(); // Trigger initial search if there's existing text if (searchModalInput?.value) { performSearch(searchModalInput.value); } }, 100); } function closeSearch() { searchOverlay?.classList.remove('show'); selectedIndex = -1; } // Toggle search mode (tags vs search) function setSearchMode(mode) { searchMode = mode; searchTagsBtn?.classList.toggle('active', mode === 'tags'); searchAllBtn?.classList.toggle('active', mode === 'search'); if (searchModalInput) { searchModalInput.name = mode === 'tags' ? 'searchtags' : 'searchterm'; // Re-run search with new mode performSearch(searchModalInput.value); } } searchToggleBtn?.addEventListener('click', openSearch); // Close search on overlay click searchOverlay?.addEventListener('click', (e) => { if (e.target === searchOverlay) { closeSearch(); } }); // Search mode toggle buttons searchTagsBtn?.addEventListener('click', (e) => { e.preventDefault(); setSearchMode('tags'); }); searchAllBtn?.addEventListener('click', (e) => { // Only prevent default if not submitting if (selectedIndex >= 0) { e.preventDefault(); navigateToSelected(); } }); // Live search on input searchModalInput?.addEventListener('input', (e) => { const query = e.target.value; // Debounce search clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { selectedIndex = -1; performSearch(query); }, 150); }); // Keyboard shortcuts document.addEventListener('keydown', (e) => { const isSearchOpen = searchOverlay?.classList.contains('show'); const isTyping = e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA'; // Handle ESC - always close search/filter if (e.key === 'Escape') { if (isSearchOpen) { closeSearch(); e.preventDefault(); } closeFilterPanel(); return; } // If search is open, handle navigation if (isSearchOpen && e.target === searchModalInput) { switch (e.key) { case 'ArrowDown': e.preventDefault(); updateSelection(selectedIndex + 1); break; case 'ArrowUp': e.preventDefault(); updateSelection(selectedIndex - 1); break; case 'Enter': // If an item is selected, navigate to it if (selectedIndex >= 0 && navigateToSelected()) { e.preventDefault(); } // Otherwise, submit the form normally break; } return; } // S to open search (when not typing) if (!isTyping && (e.key === 's' || e.key === 'S')) { e.preventDefault(); openSearch(); } }); // ===== Filter Panel ===== const filterToggleBtn = document.getElementById('filter-toggle-btn'); const filterPanel = document.getElementById('filter-panel'); const filterCloseBtn = document.getElementById('filter-close-btn'); const filterPrivate = document.getElementById('filter-private'); const filterPublic = document.getElementById('filter-public'); const filterUntagged = document.getElementById('filter-untagged'); function toggleFilterPanel() { filterPanel?.classList.toggle('show'); } function closeFilterPanel() { filterPanel?.classList.remove('show'); } filterToggleBtn?.addEventListener('click', (e) => { e.stopPropagation(); toggleFilterPanel(); }); filterCloseBtn?.addEventListener('click', closeFilterPanel); // Close filter when clicking outside document.addEventListener('click', (e) => { if (filterPanel?.classList.contains('show')) { if (!filterPanel.contains(e.target) && e.target !== filterToggleBtn) { closeFilterPanel(); } } }); // Handle filter toggle switches // Logic: // - Visibility: all (neither checked), private (only private checked), public (only public checked) // - Untagged: can be combined with any visibility function applyFilters() { const isPrivate = filterPrivate?.checked || false; const isPublic = filterPublic?.checked || false; const isUntagged = filterUntagged?.checked || false; let basePath = (typeof shaarli !== 'undefined' && shaarli.basePath) ? shaarli.basePath : ''; // Get current filter state from server-side rendered variables const currentVisibility = (typeof shaarli !== 'undefined' && shaarli.visibility) ? shaarli.visibility : ''; const currentUntagged = (typeof shaarli !== 'undefined' && shaarli.untaggedonly) || false; console.log('[Filter] Applying filters - private:', isPrivate, 'public:', isPublic, 'untagged:', isUntagged); console.log('[Filter] Current state - visibility:', currentVisibility, 'untagged:', currentUntagged); let url = basePath + '/'; // Determine desired visibility let desiredVisibility = 'all'; if (isPrivate) { desiredVisibility = 'private'; } else if (isPublic) { desiredVisibility = 'public'; } // Build URL based on desired state if (desiredVisibility === 'private') { url = basePath + '/admin/visibility/private'; } else if (desiredVisibility === 'public') { url = basePath + '/admin/visibility/public'; } else { // visibility = all - need to clear visibility if it was set if (currentVisibility && currentVisibility !== '') { url = basePath + '/admin/visibility/all'; } else if (isUntagged !== currentUntagged) { // Untagged state changed - /untagged-only works as toggle url = basePath + '/untagged-only'; } else { url = basePath + '/'; } } console.log('[Filter] Navigating to:', url); window.location.href = url; } // Clear all filters - go back to showing all bookmarks function clearAllFilters() { const basePath = (typeof shaarli !== 'undefined' && shaarli.basePath) ? shaarli.basePath : ''; const currentVisibility = (typeof shaarli !== 'undefined' && shaarli.visibility) ? shaarli.visibility : ''; const currentUntagged = (typeof shaarli !== 'undefined' && shaarli.untaggedonly) || false; // If visibility is set, clear it first if (currentVisibility && currentVisibility !== '') { window.location.href = basePath + '/admin/visibility/all'; } else if (currentUntagged) { // Toggle untagged off using /untagged-only window.location.href = basePath + '/untagged-only'; } else { window.location.href = basePath + '/'; } } // Private and public are mutually exclusive filterPrivate?.addEventListener('change', (e) => { if (e.target.checked && filterPublic) { filterPublic.checked = false; } applyFilters(); }); filterPublic?.addEventListener('change', (e) => { if (e.target.checked && filterPrivate) { filterPrivate.checked = false; } applyFilters(); }); filterUntagged?.addEventListener('change', () => { applyFilters(); }); // Initialize filter states from server-side variables (set in includes.html) (function initFilterStates() { console.log('[Filter Debug] ========================================'); // Get filter state from server-side rendered variables const visibility = (typeof shaarli !== 'undefined' && shaarli.visibility) ? shaarli.visibility : ''; const untaggedonly = (typeof shaarli !== 'undefined' && shaarli.untaggedonly) ? shaarli.untaggedonly : false; // Detect active filters from server variables let isPrivateActive = visibility === 'private'; let isPublicActive = visibility === 'public'; let isUntaggedActive = untaggedonly === true; console.log('[Filter Debug] shaarli.visibility:', visibility); console.log('[Filter Debug] shaarli.untaggedonly:', untaggedonly); console.log('[Filter Debug] isPrivateActive:', isPrivateActive); console.log('[Filter Debug] isPublicActive:', isPublicActive); console.log('[Filter Debug] isUntaggedActive:', isUntaggedActive); // Set checkbox states if (filterPrivate && isPrivateActive) { filterPrivate.checked = true; console.log('[Filter Debug] ✓ Set filterPrivate to CHECKED'); } if (filterPublic && isPublicActive) { filterPublic.checked = true; console.log('[Filter Debug] ✓ Set filterPublic to CHECKED'); } if (filterUntagged && isUntaggedActive) { filterUntagged.checked = true; console.log('[Filter Debug] ✓ Set filterUntagged to CHECKED'); } const hasActiveFilter = isPrivateActive || isPublicActive || isUntaggedActive; console.log('[Filter Debug] hasActiveFilter:', hasActiveFilter); console.log('[Filter Debug] ========================================'); // Add/update filter indicator badge on the filter button if (filterToggleBtn && hasActiveFilter) { filterToggleBtn.classList.add('has-active-filter'); console.log('[Filter Debug] Added has-active-filter class to button'); // Add badge indicator if not exists if (!filterToggleBtn.querySelector('.filter-badge')) { const badge = document.createElement('span'); badge.className = 'filter-badge'; filterToggleBtn.appendChild(badge); console.log('[Filter Debug] Added filter badge'); } } // Create and display the filter info banner if (hasActiveFilter && !document.getElementById('filter-info-banner')) { // Get result count from page - try multiple sources let resultCount = 0; const pagingStats = document.querySelector('.paging-stats strong:last-child'); const linkCount = document.querySelectorAll('.link-outer').length; if (pagingStats) { resultCount = parseInt(pagingStats.textContent) || 0; } else if (linkCount > 0) { resultCount = linkCount; } // Check if no results (empty state) const emptyState = document.querySelector('.empty-state'); const isEmptyResults = emptyState !== null || resultCount === 0; // Build the message like the examples: // "5 results without any tag" // "5 results with status private without any tag" // "1 result with status public without any tag" let statusPart = ''; let untaggedPart = ''; if (isPrivateActive) { statusPart = 'private'; } else if (isPublicActive) { statusPart = 'public'; } if (isUntaggedActive) { untaggedPart = 'without any tag'; } // Build the message let message = ''; if (isEmptyResults) { message = 'Nothing found.'; } else { const resultWord = resultCount === 1 ? 'result' : 'results'; if (statusPart && untaggedPart) { message = `${resultCount} ${resultWord} with status ${statusPart} ${untaggedPart}`; } else if (statusPart) { message = `${resultCount} ${resultWord} with status ${statusPart}`; } else if (untaggedPart) { message = `${resultCount} ${resultWord} ${untaggedPart}`; } } console.log('[Filter Debug] resultCount:', resultCount); console.log('[Filter Debug] isEmptyResults:', isEmptyResults); console.log('[Filter Debug] message:', message); if (message) { // Get base path safely const basePath = (typeof shaarli !== 'undefined' && shaarli.basePath) ? shaarli.basePath : ''; const banner = document.createElement('div'); banner.id = 'filter-info-banner'; banner.className = 'filter-info-banner' + (isEmptyResults ? ' empty-results' : ''); banner.innerHTML = `
${message}
`; // Try to find the best insertion point const contentToolbar = document.querySelector('.content-toolbar'); const linklist = document.getElementById('linklist'); const emptyStateDiv = document.querySelector('.empty-state'); console.log('[Filter Debug] contentToolbar:', contentToolbar); console.log('[Filter Debug] linklist:', linklist); // Insert after the content-toolbar (pagination) if (contentToolbar && contentToolbar.parentNode) { contentToolbar.parentNode.insertBefore(banner, contentToolbar.nextSibling); console.log('[Filter Debug] Banner inserted after content-toolbar'); } else if (emptyStateDiv && emptyStateDiv.parentNode) { emptyStateDiv.parentNode.insertBefore(banner, emptyStateDiv); console.log('[Filter Debug] Banner inserted before empty-state'); } else if (linklist) { linklist.insertBefore(banner, linklist.firstChild); console.log('[Filter Debug] Banner inserted at beginning of linklist'); } else { console.log('[Filter Debug] Could not find insertion point for banner'); } // Add click handler to clear button document.getElementById('filter-clear-btn')?.addEventListener('click', () => { console.log('[Filter] Clear button clicked, clearing all filters'); // Clear based on what was active if (isPrivateActive || isPublicActive) { window.location.href = basePath + '/admin/visibility/all'; } else if (isUntaggedActive) { // Toggle untagged off window.location.href = basePath + '/untagged-only'; } else { window.location.href = basePath + '/'; } }); } } })(); // Handle links per page options // Handle custom value form submission const filterInput = document.querySelector('.filter-input'); if (filterInput) { filterInput.closest('form')?.addEventListener('submit', (e) => { e.preventDefault(); const value = filterInput.value; if (value && value > 0) { window.location.href = shaarli.basePath + '/links-per-page?nb=' + value; } }); } // ===== View Toggle (Grid/List/Compact) ===== const linksList = document.getElementById('links-list'); const viewGridBtn = document.getElementById('view-grid-btn'); const viewListBtn = document.getElementById('view-list-btn'); const viewCompactBtn = document.getElementById('view-compact-btn'); function setView(view) { if (!linksList) return; // Remove all view classes linksList.classList.remove('view-grid', 'view-list', 'view-compact'); // Remove active state from all buttons viewGridBtn?.classList.remove('active'); viewListBtn?.classList.remove('active'); viewCompactBtn?.classList.remove('active'); // Apply selected view if (view === 'list') { linksList.classList.add('view-list'); viewListBtn?.classList.add('active'); } else if (view === 'compact') { linksList.classList.add('view-compact'); viewCompactBtn?.classList.add('active'); } else { // Default to grid linksList.classList.add('view-grid'); viewGridBtn?.classList.add('active'); } localStorage.setItem('linksView', view); } // Init view from localStorage const savedView = localStorage.getItem('linksView') || 'grid'; setView(savedView); viewGridBtn?.addEventListener('click', () => setView('grid')); viewListBtn?.addEventListener('click', () => setView('list')); viewCompactBtn?.addEventListener('click', () => setView('compact')); // ===== Multi-Select Mode ===== const selectModeBtn = document.getElementById('select-mode-btn'); const bulkActionsBar = document.getElementById('bulk-actions-bar'); const bulkCount = document.getElementById('bulk-count'); const bulkSelectAll = document.getElementById('bulk-select-all'); const bulkCancel = document.getElementById('bulk-cancel'); const bulkDelete = document.getElementById('bulk-delete'); const bulkPublic = document.getElementById('bulk-public'); const bulkPrivate = document.getElementById('bulk-private'); let selectionMode = false; let selectedIds = new Set(); function updateBulkUI() { if (bulkCount) { bulkCount.textContent = selectedIds.size; } // Update link cards visual state document.querySelectorAll('.link-outer').forEach(card => { const id = card.dataset.id; if (selectedIds.has(id)) { card.classList.add('selected'); } else { card.classList.remove('selected'); } }); // Update checkboxes document.querySelectorAll('.link-checkbox').forEach(cb => { cb.checked = selectedIds.has(cb.dataset.id); }); } function enterSelectionMode() { selectionMode = true; document.body.classList.add('selection-mode'); bulkActionsBar?.classList.add('show'); selectModeBtn?.classList.add('active'); } function exitSelectionMode() { selectionMode = false; selectedIds.clear(); document.body.classList.remove('selection-mode'); bulkActionsBar?.classList.remove('show'); selectModeBtn?.classList.remove('active'); updateBulkUI(); } function toggleSelection(id) { if (selectedIds.has(id)) { selectedIds.delete(id); } else { selectedIds.add(id); } if (selectedIds.size > 0 && !selectionMode) { enterSelectionMode(); } else if (selectedIds.size === 0 && selectionMode) { exitSelectionMode(); } updateBulkUI(); } selectModeBtn?.addEventListener('click', () => { if (selectionMode) { exitSelectionMode(); } else { enterSelectionMode(); } }); bulkCancel?.addEventListener('click', exitSelectionMode); bulkSelectAll?.addEventListener('click', () => { document.querySelectorAll('.link-outer').forEach(card => { if (card.dataset.id) { selectedIds.add(card.dataset.id); } }); updateBulkUI(); }); // Handle checkbox clicks document.addEventListener('change', (e) => { if (e.target.classList.contains('link-checkbox')) { toggleSelection(e.target.dataset.id); } }); // Handle card clicks in selection mode document.addEventListener('click', (e) => { if (!selectionMode) return; const card = e.target.closest('.link-outer'); if (card && card.dataset.id) { // Don't toggle if clicking on actions or links if (e.target.closest('.link-actions') || e.target.closest('.link-hover-actions') || e.target.tagName === 'A') { return; } toggleSelection(card.dataset.id); } }); // Bulk actions bulkDelete?.addEventListener('click', () => { if (selectedIds.size === 0) return; if (!confirm(`Delete ${selectedIds.size} bookmark(s)?`)) return; // Submit form with selected IDs const form = document.createElement('form'); form.method = 'POST'; form.action = shaarli.basePath + '/admin/shaare/delete'; selectedIds.forEach(id => { const input = document.createElement('input'); input.type = 'hidden'; input.name = 'id[]'; input.value = id; form.appendChild(input); }); const tokenInput = document.createElement('input'); tokenInput.type = 'hidden'; tokenInput.name = 'token'; tokenInput.value = document.querySelector('input[name="token"]')?.value || ''; form.appendChild(tokenInput); document.body.appendChild(form); form.submit(); }); bulkPublic?.addEventListener('click', () => { if (selectedIds.size === 0) return; bulkVisibilityChange('public'); }); bulkPrivate?.addEventListener('click', () => { if (selectedIds.size === 0) return; bulkVisibilityChange('private'); }); function bulkVisibilityChange(visibility) { const form = document.createElement('form'); form.method = 'POST'; form.action = shaarli.basePath + '/admin/shaare/visibility'; selectedIds.forEach(id => { const input = document.createElement('input'); input.type = 'hidden'; input.name = 'id[]'; input.value = id; form.appendChild(input); }); const visInput = document.createElement('input'); visInput.type = 'hidden'; visInput.name = 'visibility'; visInput.value = visibility; form.appendChild(visInput); const tokenInput = document.createElement('input'); tokenInput.type = 'hidden'; tokenInput.name = 'token'; tokenInput.value = document.querySelector('input[name="token"]')?.value || ''; form.appendChild(tokenInput); document.body.appendChild(form); form.submit(); } // ===== Thumbnail Update ===== const thumbnailsPage = document.querySelector('.page-thumbnails'); if (thumbnailsPage) { const thumbnailPlaceholder = document.querySelector('.thumbnail-placeholder'); const thumbnailTitle = document.querySelector('.thumbnail-link-title'); const progressCurrent = document.querySelector('.progress-current'); const progressBarActual = document.querySelector('.progress-actual'); const idsInput = document.querySelector('input[name="ids"]'); if (idsInput && idsInput.value) { const thumbnailsIdList = idsInput.value.split(','); const total = thumbnailsIdList.length; let i = 0; const updateThumbnail = function (id) { console.log('Updating thumbnail #' + i + ' with id ' + id); fetch(shaarli.basePath + '/admin/shaare/' + id + '/update-thumbnail', { method: 'PATCH', headers: { 'Accept': 'application/json', } }) .then(response => response.json()) .then(data => { i++; if (thumbnailTitle) { thumbnailTitle.textContent = data.title || 'Processing...'; } if (thumbnailPlaceholder) { if (data.thumbnail) { thumbnailPlaceholder.innerHTML = ''; } else { thumbnailPlaceholder.innerHTML = ''; } } if (progressCurrent) { progressCurrent.textContent = i; } if (progressBarActual) { progressBarActual.style.width = ((i * 100) / total) + '%'; } if (i < total) { updateThumbnail(thumbnailsIdList[i]); } else { if (thumbnailTitle) { thumbnailTitle.textContent = 'Thumbnail update done!'; } } }) .catch(error => { console.error('Failed to update thumbnail:', error); if (thumbnailTitle) { thumbnailTitle.textContent = 'Error updating thumbnail #' + i; } // Continue with next thumbnail even if one fails i++; if (i < total) { updateThumbnail(thumbnailsIdList[i]); } }); }; // Start the update process updateThumbnail(thumbnailsIdList[0]); } } // ===== Fullscreen Description Modal ===== const descModal = document.getElementById('desc-modal'); const descModalBody = document.getElementById('desc-modal-body'); const descModalClose = document.getElementById('desc-modal-close'); function openDescModal(content) { if (descModal && descModalBody) { descModalBody.innerHTML = content; descModal.classList.add('show'); document.body.style.overflow = 'hidden'; // Prevent background scrolling } } function closeDescModal() { if (descModal) { descModal.classList.remove('show'); document.body.style.overflow = ''; } } descModalClose?.addEventListener('click', closeDescModal); descModal?.addEventListener('click', (e) => { if (e.target === descModal) { closeDescModal(); } }); // Press ESC to close description modal document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && descModal?.classList.contains('show')) { closeDescModal(); } }); // Delegate click event for dynamic buttons document.addEventListener('click', (e) => { const btn = e.target.closest('.view-desc-btn'); if (btn) { e.preventDefault(); const id = btn.dataset.id; const card = document.getElementById(id); if (card) { // Try to find description element (even if hidden in compact view) const descEl = card.querySelector('.link-description'); if (descEl) { openDescModal(descEl.innerHTML); } else { openDescModal('

No description available.

'); } } } }); });