// fileListView.js import { escapeHTML, debounce, buildSearchAndPaginationControls, buildFileTableHeader, buildFileTableRow, buildBottomControls, updateFileActionButtons, showToast, updateRowHighlight, toggleRowSelection, attachEnterKeyListener } from './domUtils.js?v={{APP_QVER}}'; import { t } from './i18n.js?v={{APP_QVER}}'; import { bindFileListContextMenu } from './fileMenu.js?v={{APP_QVER}}'; import { openDownloadModal } from './fileActions.js?v={{APP_QVER}}'; import { openTagModal, openMultiTagModal } from './fileTags.js?v={{APP_QVER}}'; import { getParentFolder, updateBreadcrumbTitle, setupBreadcrumbDelegation, showFolderManagerContextMenu, hideFolderManagerContextMenu, openRenameFolderModal, openDeleteFolderModal } from './folderManager.js?v={{APP_QVER}}'; import { openFolderShareModal } from './folderShareModal.js?v={{APP_QVER}}'; import { folderDragOverHandler, folderDragLeaveHandler, folderDropHandler } from './fileDragDrop.js?v={{APP_QVER}}'; export let fileData = []; export let sortOrder = { column: "uploaded", ascending: true }; // Hide "Edit" for files >10 MiB const MAX_EDIT_BYTES = 10 * 1024 * 1024; // Latest-response-wins guard (prevents double render/flicker if loadFileList gets called twice) let __fileListReqSeq = 0; window.itemsPerPage = parseInt( localStorage.getItem('itemsPerPage') || window.itemsPerPage || '10', 10 ); window.currentPage = window.currentPage || 1; window.viewMode = localStorage.getItem("viewMode") || "table"; // Global flag for advanced search mode. window.advancedSearchEnabled = false; /* =========================================================== SECURITY: build file URLs only via the API (no /uploads) =========================================================== */ function apiFileUrl(folder, name, inline = false) { const f = folder && folder !== "root" ? folder : "root"; const q = new URLSearchParams({ folder: f, file: name, inline: inline ? "1" : "0", t: String(Date.now()) // cache-bust }); return `/api/file/download.php?${q.toString()}`; } // Wire "select all" header checkbox for the current table render function wireSelectAll(fileListContent) { // Be flexible about how the header checkbox is identified const selectAll = fileListContent.querySelector( 'thead input[type="checkbox"].select-all, ' + 'thead .select-all input[type="checkbox"], ' + 'thead input#selectAll, ' + 'thead input#selectAllCheckbox, ' + 'thead input[data-select-all]' ); if (!selectAll) return; const getRowCbs = () => Array.from(fileListContent.querySelectorAll('tbody .file-checkbox')) .filter(cb => !cb.disabled); // Toggle all rows when the header checkbox changes selectAll.addEventListener('change', () => { const checked = selectAll.checked; getRowCbs().forEach(cb => { cb.checked = checked; updateRowHighlight(cb); }); updateFileActionButtons(); // No indeterminate state when explicitly toggled selectAll.indeterminate = false; }); // Keep header checkbox state in sync with row selections const syncHeader = () => { const cbs = getRowCbs(); const total = cbs.length; const checked = cbs.filter(cb => cb.checked).length; if (!total) { selectAll.checked = false; selectAll.indeterminate = false; return; } selectAll.checked = checked === total; selectAll.indeterminate = checked > 0 && checked < total; }; // Listen for any row checkbox changes to refresh header state fileListContent.addEventListener('change', (e) => { if (e.target && e.target.classList.contains('file-checkbox')) { syncHeader(); } }); // Initial sync on mount syncHeader(); } /* ----------------------------- Helper: robust JSON handling ----------------------------- */ // Parse JSON if possible; throw on non-2xx with useful message & status async function safeJson(res) { const text = await res.text(); let body = null; try { body = text ? JSON.parse(text) : null; } catch { /* ignore */ } if (!res.ok) { const msg = (body && (body.error || body.message)) || (text && text.trim()) || `HTTP ${res.status}`; const err = new Error(msg); err.status = res.status; throw err; } return body ?? {}; } /** * Convert a file size string (e.g. "456.9KB", "1.2 MB", "1024") into bytes. */ function parseSizeToBytes(sizeStr) { if (!sizeStr) return 0; let s = sizeStr.trim(); let value = parseFloat(s); let upper = s.toUpperCase(); if (upper.includes("KB")) { value *= 1024; } else if (upper.includes("MB")) { value *= 1024 * 1024; } else if (upper.includes("GB")) { value *= 1024 * 1024 * 1024; } return value; } /** * Format the total bytes as a human-readable string. */ function formatSize(totalBytes) { if (totalBytes < 1024) { return totalBytes + " Bytes"; } else if (totalBytes < 1024 * 1024) { return (totalBytes / 1024).toFixed(2) + " KB"; } else if (totalBytes < 1024 * 1024 * 1024) { return (totalBytes / (1024 * 1024)).toFixed(2) + " MB"; } else { return (totalBytes / (1024 * 1024 * 1024)).toFixed(2) + " GB"; } } /** * Build the folder summary HTML using the filtered file list. */ function buildFolderSummary(filteredFiles) { const totalFiles = filteredFiles.length; const totalBytes = filteredFiles.reduce((sum, file) => { return sum + parseSizeToBytes(file.size); }, 0); const sizeStr = formatSize(totalBytes); return `${t('total_files')}: ${totalFiles}  |  ${t('total_size')}: ${sizeStr}`; } /** * Advanced Search toggle */ function toggleAdvancedSearch() { window.advancedSearchEnabled = !window.advancedSearchEnabled; const advancedBtn = document.getElementById("advancedSearchToggle"); if (advancedBtn) { advancedBtn.textContent = window.advancedSearchEnabled ? "Basic Search" : "Advanced Search"; } renderFileTable(window.currentFolder); } window.imageCache = window.imageCache || {}; function cacheImage(imgElem, key) { window.imageCache[key] = imgElem.src; } window.cacheImage = cacheImage; /** * Fuse.js fuzzy search helper */ // --- Lazy Fuse loader (drop-in, CSP-safe, no inline) --- const FUSE_SRC = '/vendor/fuse/6.6.2/fuse.min.js?v={{APP_QVER}}'; let _fuseLoadingPromise = null; function loadScriptOnce(src) { // cache by src so we don't append multiple