// ============================================================ // Media Browser: Navigation, rendering, search, pagination // ============================================================ import { t, showToast, showConfirm, escapeHtml, closeDialog, SEARCH_DEBOUNCE_MS, EMPTY_SVG_FILE, EMPTY_SVG_FOLDER, emptyStateHtml, getAuthHeaders, hasCredentials, } from './core.js'; // Browser state let currentFolderId = null; let currentPath = ''; let currentOffset = 0; let itemsPerPage = parseInt(localStorage.getItem('mediaBrowser.itemsPerPage')) || 100; let totalItems = 0; let mediaFolders = {}; let managementEnabled = false; let viewMode = localStorage.getItem('mediaBrowser.viewMode') || 'grid'; let cachedItems = null; let browserSearchTerm = ''; let browserSearchTimer = null; export const thumbnailCache = new Map(); const THUMBNAIL_CACHE_MAX = 200; // Load media folders on page load export async function loadMediaFolders() { try { if (!hasCredentials()) return; const response = await fetch('/api/browser/folders', { headers: getAuthHeaders() }); if (!response.ok) throw new Error('Failed to load folders'); const data = await response.json(); mediaFolders = data.folders || {}; managementEnabled = data.management_enabled || false; // Show/hide the media folders settings section const section = document.getElementById('mediaFoldersSection'); if (section) { section.style.display = managementEnabled ? '' : 'none'; } // Render folders table in settings if management is enabled if (managementEnabled) { loadFoldersTable(); } // Load last browsed path or show root folder list loadLastBrowserPath(); } catch (error) { console.error('Error loading media folders:', error); showToast(t('browser.error_loading_folders'), 'error'); } } function showRootFolders() { currentFolderId = ''; currentPath = ''; currentOffset = 0; cachedItems = null; // Hide search at root level showBrowserSearch(false); // Render breadcrumb with just "Home" (already at root — not interactive). const breadcrumb = document.getElementById('breadcrumb'); breadcrumb.innerHTML = ''; const root = document.createElement('span'); root.className = 'breadcrumb-item breadcrumb-home'; root.setAttribute('aria-current', 'page'); root.setAttribute('aria-label', t('browser.home') || 'Home'); root.innerHTML = ''; breadcrumb.appendChild(root); // Hide play all button and pagination document.getElementById('playAllBtn').style.display = 'none'; document.getElementById('browserPagination').style.display = 'none'; // Render folders as grid cards const container = document.getElementById('browserGrid'); revokeBlobUrls(container); if (viewMode === 'list') { container.className = 'browser-list'; } else { container.className = 'browser-grid browser-root-grid'; } container.innerHTML = ''; const folderSvg = ''; Object.entries(mediaFolders).forEach(([id, folder]) => { if (!folder.enabled) return; const unavailable = folder.available === false; const unavailableClass = unavailable ? ' unavailable' : ''; if (viewMode === 'list') { const row = document.createElement('div'); row.className = 'browser-list-item' + unavailableClass; if (!unavailable) { row.onclick = () => { currentFolderId = id; browsePath(id, ''); }; } row.innerHTML = `
${folderSvg}
${escapeHtml(folder.label)}${unavailable ? ' ' + t('browser.unavailable') + '' : ''}
`; container.appendChild(row); } else { const card = document.createElement('div'); card.className = 'browser-item browser-root-folder' + unavailableClass; if (!unavailable) { card.onclick = () => { currentFolderId = id; browsePath(id, ''); }; } card.innerHTML = `
${folderSvg}
${escapeHtml(folder.label)}
${unavailable ? '
' + t('browser.unavailable') + '
' : ''}
`; container.appendChild(card); } }); } async function browsePath(folderId, path, offset = 0, nocache = false) { // Clear search when navigating; bump browse generation so in-flight // thumbnail fetches from the previous folder can be discarded. showBrowserSearch(false); bumpBrowseGen(); try { if (!hasCredentials()) return; // Show loading spinner const container = document.getElementById('browserGrid'); container.className = 'browser-grid'; container.innerHTML = '
'; const encodedPath = encodeURIComponent(path); let url = `/api/browser/browse?folder_id=${folderId}&path=${encodedPath}&offset=${offset}&limit=${itemsPerPage}`; if (nocache) url += '&nocache=true'; const response = await fetch( url, { headers: getAuthHeaders() } ); if (!response.ok) { let errorMsg = 'Failed to browse path'; if (response.status === 503) { const errorData = await response.json().catch(() => ({})); errorMsg = errorData.detail || 'Folder is temporarily unavailable (network share not accessible)'; } throw new Error(errorMsg); } const data = await response.json(); currentPath = data.current_path; currentOffset = offset; totalItems = data.total; cachedItems = data.items; renderBreadcrumbs(data.current_path, data.parent_path); renderBrowserItems(cachedItems); renderPagination(); // Show search bar when inside a folder showBrowserSearch(true); // Show/hide Play All button based on whether media items exist const hasMedia = data.items.some(item => item.is_media); document.getElementById('playAllBtn').style.display = hasMedia ? '' : 'none'; // Save last path saveLastBrowserPath(folderId, currentPath); } catch (error) { console.error('Error browsing path:', error); const errorMsg = error.message || t('browser.error_loading'); showToast(errorMsg, 'error'); clearBrowserGrid(); } } function renderBreadcrumbs(currentPathStr, parentPath) { const breadcrumb = document.getElementById('breadcrumb'); breadcrumb.innerHTML = ''; const parts = (currentPathStr || '').split('/').filter(p => p); let path = '/'; // Home link (back to folder list) — use a real `; }).join(''); } export function showAddFolderDialog() { document.getElementById('folderDialogTitle').textContent = t('browser.folder_dialog.title_add'); document.getElementById('folderIsEdit').value = ''; document.getElementById('folderOriginalId').value = ''; document.getElementById('folderId').value = ''; document.getElementById('folderId').disabled = false; document.getElementById('folderLabel').value = ''; document.getElementById('folderPath').value = ''; document.getElementById('folderEnabled').checked = true; document.getElementById('folderDialog').showModal(); } export function showEditFolderDialog(folderId) { const folder = mediaFolders[folderId]; if (!folder) return; document.getElementById('folderDialogTitle').textContent = t('browser.folder_dialog.title_edit'); document.getElementById('folderIsEdit').value = '1'; document.getElementById('folderOriginalId').value = folderId; document.getElementById('folderId').value = folderId; document.getElementById('folderId').disabled = true; document.getElementById('folderLabel').value = folder.label; document.getElementById('folderPath').value = folder.path; document.getElementById('folderEnabled').checked = folder.enabled; document.getElementById('folderDialog').showModal(); } export function closeFolderDialog() { closeDialog(document.getElementById('folderDialog')); } export async function saveFolder(event) { event.preventDefault(); const isEdit = document.getElementById('folderIsEdit').value === '1'; const folderId = isEdit ? document.getElementById('folderOriginalId').value : document.getElementById('folderId').value.trim(); const label = document.getElementById('folderLabel').value.trim(); const path = document.getElementById('folderPath').value.trim(); const enabled = document.getElementById('folderEnabled').checked; if (!folderId || !label || !path) return; const submitBtn = document.querySelector('#folderForm button[type="submit"]'); if (submitBtn) submitBtn.disabled = true; try { let response; if (isEdit) { response = await fetch(`/api/browser/folders/update/${encodeURIComponent(folderId)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', ...getAuthHeaders() }, body: JSON.stringify({ label, path, enabled }), }); } else { response = await fetch('/api/browser/folders/create', { method: 'POST', headers: { 'Content-Type': 'application/json', ...getAuthHeaders() }, body: JSON.stringify({ folder_id: folderId, label, path, enabled }), }); } if (response.ok) { closeFolderDialog(); showToast(t(isEdit ? 'browser.folder_updated' : 'browser.folder_created'), 'success'); await loadMediaFolders(); } else { const result = await response.json().catch(() => ({})); showToast(result.detail || t('browser.folder_save_error'), 'error'); } } catch (error) { console.error('Error saving folder:', error); showToast(t('browser.folder_save_error'), 'error'); } finally { if (submitBtn) submitBtn.disabled = false; } } export async function deleteFolderConfirm(folderId) { if (!await showConfirm(t('browser.folder_confirm_delete', { name: folderId }))) { return; } try { const response = await fetch(`/api/browser/folders/delete/${encodeURIComponent(folderId)}`, { method: 'DELETE', headers: getAuthHeaders(), }); if (response.ok) { showToast(t('browser.folder_deleted'), 'success'); await loadMediaFolders(); } else { const result = await response.json().catch(() => ({})); showToast(result.detail || t('browser.folder_delete_error'), 'error'); } } catch (error) { console.error('Error deleting folder:', error); showToast(t('browser.folder_delete_error'), 'error'); } } // Legacy stub — now handled via settings table export function showManageFoldersDialog() { if (managementEnabled) { // Switch to settings tab and scroll to the folders section const switchTabFn = window.switchTab; if (switchTabFn) switchTabFn('settings'); setTimeout(() => { const section = document.getElementById('mediaFoldersSection'); if (section) { section.setAttribute('open', ''); section.scrollIntoView({ behavior: 'smooth' }); } }, 100); } else { showToast(t('browser.manage_folders_hint'), 'info'); } }