// 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 }; // onnlyoffice let OO_ENABLED = false; let OO_EXTS = new Set(); export async function initOnlyOfficeCaps() { try { const r = await fetch('/api/onlyoffice/status.php', { credentials: 'include' }); if (!r.ok) throw 0; const j = await r.json(); OO_ENABLED = !!j.enabled; OO_EXTS = new Set(Array.isArray(j.exts) ? j.exts : []); } catch { OO_ENABLED = false; OO_EXTS = new Set(); } } // 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 ?? {}; } // ---- Viewed badges (table + gallery) ---- // ---------- Badge factory (center text vertically) ---------- function makeBadge(state) { if (!state) return null; const el = document.createElement('span'); el.className = 'status-badge'; el.style.cssText = [ 'display:inline-flex', 'align-items:center', 'justify-content:center', 'vertical-align:middle', 'margin-left:6px', 'padding:2px 8px', 'min-height:18px', 'line-height:1', 'border-radius:999px', 'font-size:.78em', 'border:1px solid rgba(0,0,0,.2)', 'background:rgba(0,0,0,.06)' ].join(';'); if (state.completed) { el.classList.add('watched'); el.textContent = (t('watched') || t('viewed') || 'Watched'); el.style.borderColor = 'rgba(34,197,94,.45)'; el.style.background = 'rgba(34,197,94,.15)'; el.style.color = '#22c55e'; return el; } if (Number.isFinite(state.seconds) && Number.isFinite(state.duration) && state.duration > 0) { const pct = Math.max(1, Math.min(99, Math.round((state.seconds / state.duration) * 100))); el.classList.add('progress'); el.textContent = `${pct}%`; el.style.borderColor = 'rgba(234,88,12,.55)'; el.style.background = 'rgba(234,88,12,.18)'; el.style.color = '#ea580c'; return el; } return null; } // ---------- Public: set/clear badges for one file (table + gallery) ---------- function applyBadgeToDom(name, state) { const safe = CSS.escape(name); // Table document.querySelectorAll(`tr[data-file-name="${safe}"] .name-cell, tr[data-file-name="${safe}"] .file-name-cell`) .forEach(cell => { cell.querySelector('.status-badge')?.remove(); const b = makeBadge(state); if (b) cell.appendChild(b); }); // Gallery document.querySelectorAll(`.gallery-card[data-file-name="${safe}"] .gallery-file-name`) .forEach(title => { title.querySelector('.status-badge')?.remove(); const b = makeBadge(state); if (b) title.appendChild(b); }); } export function setFileWatchedBadge(name, watched = true) { applyBadgeToDom(name, watched ? { completed: true } : null); } export function setFileProgressBadge(name, seconds, duration) { if (duration > 0 && seconds >= 0) { applyBadgeToDom(name, { seconds, duration, completed: seconds >= duration - 1 }); } else { applyBadgeToDom(name, null); } } export async function refreshViewedBadges(folder) { let map = null; try { const res = await fetch(`/api/media/getViewedMap.php?folder=${encodeURIComponent(folder)}&t=${Date.now()}`, { credentials: 'include' }); const j = await res.json(); map = j?.map || null; } catch { /* ignore */ } // Clear any existing badges document.querySelectorAll( '#fileList tr[data-file-name] .file-name-cell .status-badge, ' + '#fileList tr[data-file-name] .name-cell .status-badge, ' + '.gallery-card[data-file-name] .gallery-file-name .status-badge' ).forEach(n => n.remove()); if (!map) return; // Table rows document.querySelectorAll('#fileList tr[data-file-name]').forEach(tr => { const name = tr.getAttribute('data-file-name'); const state = map[name]; if (!state) return; const cell = tr.querySelector('.name-cell, .file-name-cell'); if (!cell) return; const badge = makeBadge(state); if (badge) cell.appendChild(badge); }); // Gallery cards document.querySelectorAll('.gallery-card[data-file-name]').forEach(card => { const name = card.getAttribute('data-file-name'); const state = map[name]; if (!state) return; const title = card.querySelector('.gallery-file-name'); if (!title) return; const badge = makeBadge(state); if (badge) title.appendChild(badge); }); } /** * 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