// fileListView.js import { escapeHTML, debounce, buildSearchAndPaginationControls, buildFileTableHeader, buildFileTableRow, buildBottomControls, updateFileActionButtons, showToast, updateRowHighlight, toggleRowSelection, attachEnterKeyListener } from './domUtils.js'; import { t } from './i18n.js'; import { bindFileListContextMenu } from './fileMenu.js'; import { openDownloadModal } from './fileActions.js'; import { openTagModal, openMultiTagModal } from './fileTags.js'; import { getParentFolder, updateBreadcrumbTitle, setupBreadcrumbDelegation, showFolderManagerContextMenu, hideFolderManagerContextMenu, openRenameFolderModal, openDeleteFolderModal } from './folderManager.js'; import { openFolderShareModal } from './folderShareModal.js'; import { folderDragOverHandler, folderDragLeaveHandler, folderDropHandler } from './fileDragDrop.js'; 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 */ function searchFiles(searchTerm) { if (!searchTerm) return fileData; let keys = [ { name: 'name', weight: 0.1 }, { name: 'uploader', weight: 0.1 }, { name: 'tags.name', weight: 0.1 } ]; if (window.advancedSearchEnabled) { keys.push({ name: 'content', weight: 0.7 }); } const options = { keys: keys, threshold: 0.4, minMatchCharLength: 2, ignoreLocation: true }; const fuse = new Fuse(fileData, options); let results = fuse.search(searchTerm); return results.map(result => result.item); } /** * View mode toggle */ export function createViewToggleButton() { let toggleBtn = document.getElementById("toggleViewBtn"); if (!toggleBtn) { toggleBtn = document.createElement("button"); toggleBtn.id = "toggleViewBtn"; toggleBtn.classList.add("btn", "btn-toggleview"); if (window.viewMode === "gallery") { toggleBtn.innerHTML = 'view_list'; toggleBtn.title = t("switch_to_table_view"); } else { toggleBtn.innerHTML = 'view_module'; toggleBtn.title = t("switch_to_gallery_view"); } const headerButtons = document.querySelector(".header-buttons"); if (headerButtons && headerButtons.lastElementChild) { headerButtons.insertBefore(toggleBtn, headerButtons.lastElementChild); } else if (headerButtons) { headerButtons.appendChild(toggleBtn); } } toggleBtn.onclick = () => { window.viewMode = window.viewMode === "gallery" ? "table" : "gallery"; localStorage.setItem("viewMode", window.viewMode); loadFileList(window.currentFolder); if (window.viewMode === "gallery") { toggleBtn.innerHTML = 'view_list'; toggleBtn.title = t("switch_to_table_view"); } else { toggleBtn.innerHTML = 'view_module'; toggleBtn.title = t("switch_to_gallery_view"); } }; return toggleBtn; } export function formatFolderName(folder) { if (folder === "root") return "(Root)"; return folder .replace(/[_-]+/g, " ") .replace(/\b\w/g, char => char.toUpperCase()); } // Expose inline DOM helpers. window.toggleRowSelection = toggleRowSelection; window.updateRowHighlight = updateRowHighlight; export async function loadFileList(folderParam) { const reqId = ++__fileListReqSeq; // latest call wins const folder = folderParam || "root"; const fileListContainer = document.getElementById("fileList"); const actionsContainer = document.getElementById("fileListActions"); // 1) show loader (only this request is allowed to render) fileListContainer.style.visibility = "hidden"; fileListContainer.innerHTML = "
Loading files...
"; try { // Kick off both in parallel, but render as soon as FILES are ready const filesPromise = fetch( `/api/file/getFileList.php?folder=${encodeURIComponent(folder)}&recursive=1&t=${Date.now()}`, { credentials: 'include' } ); const foldersPromise = fetch( `/api/folder/getFolderList.php?folder=${encodeURIComponent(folder)}`, { credentials: 'include' } ); // ----- FILES FIRST ----- const filesRes = await filesPromise; if (filesRes.status === 401) { // session expired — bounce to logout window.location.href = "/api/auth/logout.php"; throw new Error("Unauthorized"); } if (filesRes.status === 403) { // forbidden — friendly message, keep UI responsive fileListContainer.innerHTML = `
${t("no_access_to_resource") || "You don't have access to this folder."}
`; showToast(t("no_access_to_resource") || "You don't have access to this folder.", "error"); return []; } const data = await safeJson(filesRes); if (data.error) { throw new Error(typeof data.error === 'string' ? data.error : 'Server returned an error.'); } // If another loadFileList ran after this one, bail before touching the DOM if (reqId !== __fileListReqSeq) return []; // 3) clear loader fileListContainer.innerHTML = ""; // 4) handle “no files” case if (!data.files || Object.keys(data.files).length === 0) { if (reqId !== __fileListReqSeq) return []; fileListContainer.innerHTML = `
${t("no_files_found")}
${t("no_folder_access_yet") || "No folder access has been assigned to your account yet."}
`; const summaryElem = document.getElementById("fileSummary"); if (summaryElem) summaryElem.style.display = "none"; const sliderContainer = document.getElementById("viewSliderContainer"); if (sliderContainer) sliderContainer.style.display = "none"; const strip = document.getElementById("folderStripContainer"); if (strip) strip.style.display = "none"; updateFileActionButtons(); fileListContainer.style.visibility = "visible"; // We still try to populate the folder strip below } // 5) normalize files array if (!Array.isArray(data.files)) { data.files = Object.entries(data.files).map(([name, meta]) => { meta.name = name; return meta; }); } data.files = data.files.map(f => { f.fullName = (f.path || f.name).trim().toLowerCase(); // Prefer numeric size if API provides it; otherwise parse the "1.2 MB" string let bytes = Number.isFinite(f.sizeBytes) ? f.sizeBytes : parseSizeToBytes(String(f.size || "")); if (!Number.isFinite(bytes)) bytes = Infinity; f.editable = canEditFile(f.name) && (bytes <= MAX_EDIT_BYTES); f.folder = folder; return f; }); fileData = data.files; if (reqId !== __fileListReqSeq) return []; // 6) inject summary + slider if (actionsContainer) { // a) summary let summaryElem = document.getElementById("fileSummary"); if (!summaryElem) { summaryElem = document.createElement("div"); summaryElem.id = "fileSummary"; summaryElem.style.cssText = "float:right; margin:0 60px 0 auto; font-size:0.9em;"; actionsContainer.appendChild(summaryElem); } summaryElem.style.display = "block"; summaryElem.innerHTML = buildFolderSummary(fileData); // b) slider const viewMode = window.viewMode || "table"; let sliderContainer = document.getElementById("viewSliderContainer"); if (!sliderContainer) { sliderContainer = document.createElement("div"); sliderContainer.id = "viewSliderContainer"; sliderContainer.style.cssText = "display:inline-flex; align-items:center; margin-right:auto; font-size:0.9em;"; actionsContainer.insertBefore(sliderContainer, summaryElem); } else { sliderContainer.style.display = "inline-flex"; } if (viewMode === "gallery") { const w = window.innerWidth; let maxCols; if (w < 600) maxCols = 1; else if (w < 900) maxCols = 2; else if (w < 1200) maxCols = 4; else maxCols = 6; const currentCols = Math.min( parseInt(localStorage.getItem("galleryColumns") || "3", 10), maxCols ); sliderContainer.innerHTML = ` ${currentCols} `; const gallerySlider = document.getElementById("galleryColumnsSlider"); const galleryValue = document.getElementById("galleryColumnsValue"); gallerySlider.oninput = e => { const v = +e.target.value; localStorage.setItem("galleryColumns", v); galleryValue.textContent = v; document.querySelector(".gallery-container") ?.style.setProperty("grid-template-columns", `repeat(${v},1fr)`); }; } else { const currentHeight = parseInt(localStorage.getItem("rowHeight") || "48", 10); sliderContainer.innerHTML = ` ${currentHeight}px `; const rowSlider = document.getElementById("rowHeightSlider"); const rowValue = document.getElementById("rowHeightValue"); rowSlider.oninput = e => { const v = e.target.value; document.documentElement.style.setProperty("--file-row-height", v + "px"); localStorage.setItem("rowHeight", v); rowValue.textContent = v + "px"; }; } } // 7) render files if (reqId !== __fileListReqSeq) return []; if (window.viewMode === "gallery") { renderGalleryView(folder); } else { renderFileTable(folder); } updateFileActionButtons(); fileListContainer.style.visibility = "visible"; // ----- FOLDERS NEXT (populate strip when ready; doesn't block rows) ----- try { const foldersRes = await foldersPromise; // If folders API forbids, just skip the strip; keep file rows rendered if (foldersRes.status === 403) { const strip = document.getElementById("folderStripContainer"); if (strip) strip.style.display = "none"; return data.files; } const folderRaw = await safeJson(foldersRes).catch(() => []); // don't block file render on strip issues if (reqId !== __fileListReqSeq) return data.files; // --- build ONLY the *direct* children of current folder --- let subfolders = []; const hidden = new Set(["profile_pics", "trash"]); if (Array.isArray(folderRaw)) { const allPaths = folderRaw.map(item => item.folder ?? item); const depth = folder === "root" ? 1 : folder.split("/").length + 1; subfolders = allPaths .filter(p => { if (folder === "root") return p.indexOf("/") === -1; if (!p.startsWith(folder + "/")) return false; return p.split("/").length === depth; }) .map(p => ({ name: p.split("/").pop(), full: p })); } subfolders = subfolders.filter(sf => !hidden.has(sf.name)); let strip = document.getElementById("folderStripContainer"); if (!strip) { strip = document.createElement("div"); strip.id = "folderStripContainer"; strip.className = "folder-strip-container"; actionsContainer.parentNode.insertBefore(strip, actionsContainer); } if (window.showFoldersInList && subfolders.length) { strip.innerHTML = subfolders.map(sf => `
folder
${escapeHTML(sf.name)}
`).join(""); strip.style.display = "flex"; strip.querySelectorAll(".folder-item").forEach(el => { // 1) click to navigate el.addEventListener("click", () => { const dest = el.dataset.folder; window.currentFolder = dest; localStorage.setItem("lastOpenedFolder", dest); updateBreadcrumbTitle(dest); document.querySelectorAll(".folder-option.selected").forEach(o => o.classList.remove("selected")); document.querySelector(`.folder-option[data-folder="${dest}"]`)?.classList.add("selected"); loadFileList(dest); }); // 2) drag & drop el.addEventListener("dragover", folderDragOverHandler); el.addEventListener("dragleave", folderDragLeaveHandler); el.addEventListener("drop", folderDropHandler); // 3) right-click context menu el.addEventListener("contextmenu", e => { e.preventDefault(); e.stopPropagation(); const dest = el.dataset.folder; window.currentFolder = dest; localStorage.setItem("lastOpenedFolder", dest); strip.querySelectorAll(".folder-item.selected").forEach(i => i.classList.remove("selected")); el.classList.add("selected"); const menuItems = [ { label: t("create_folder"), action: () => document.getElementById("createFolderModal").style.display = "block" }, { label: t("rename_folder"), action: () => openRenameFolderModal() }, { label: t("folder_share"), action: () => openFolderShareModal(dest) }, { label: t("delete_folder"), action: () => openDeleteFolderModal() } ]; showFolderManagerContextMenu(e.pageX, e.pageY, menuItems); }); }); document.addEventListener("click", hideFolderManagerContextMenu); } else { strip.style.display = "none"; } } catch { // ignore folder errors; rows already rendered } return data.files; } catch (err) { console.error("Error loading file list:", err); if (err.status === 403) { showToast(t("no_access_to_resource") || "You don't have access to this folder.", "error"); const fileListContainer = document.getElementById("fileList"); if (fileListContainer) fileListContainer.textContent = t("no_access_to_resource") || "You don't have access to this folder."; } else if (err.message !== "Unauthorized") { const fileListContainer = document.getElementById("fileList"); if (fileListContainer) fileListContainer.textContent = "Error loading files."; } return []; } finally { if (reqId === __fileListReqSeq) { fileListContainer.style.visibility = "visible"; } } } /** * Render table view */ export function renderFileTable(folder, container, subfolders) { const fileListContent = container || document.getElementById("fileList"); const searchTerm = (window.currentSearchTerm || "").toLowerCase(); const itemsPerPageSetting = parseInt(localStorage.getItem("itemsPerPage") || "10", 10); let currentPage = window.currentPage || 1; const filteredFiles = searchFiles(searchTerm); const totalFiles = filteredFiles.length; const totalPages = Math.ceil(totalFiles / itemsPerPageSetting); if (currentPage > totalPages) { currentPage = totalPages > 0 ? totalPages : 1; window.currentPage = currentPage; } // We pass a harmless "base" string to keep buildFileTableRow happy, // then we will FIX the preview/thumbnail URLs to the API below. const fakeBase = "#/"; const topControlsHTML = buildSearchAndPaginationControls({ currentPage, totalPages, searchTerm: window.currentSearchTerm || "" }); const combinedTopHTML = topControlsHTML; let headerHTML = buildFileTableHeader(sortOrder); const startIndex = (currentPage - 1) * itemsPerPageSetting; const endIndex = Math.min(startIndex + itemsPerPageSetting, totalFiles); let rowsHTML = ""; if (totalFiles > 0) { filteredFiles.slice(startIndex, endIndex).forEach((file, idx) => { // Build row with a neutral base, then correct the links/preview below. let rowHTML = buildFileTableRow(file, fakeBase); // Give the row an ID so we can patch attributes safely rowHTML = rowHTML.replace(" 0) { tagBadgesHTML = '
'; file.tags.forEach(tag => { tagBadgesHTML += `${escapeHTML(tag.name)}`; }); tagBadgesHTML += "
"; } rowsHTML += rowHTML.replace(/()(.*?)(<\/td>)/, (match, p1, p2, p3) => { return p1 + p2 + tagBadgesHTML + p3; }); }); } else { rowsHTML += `No files found.`; } rowsHTML += ""; const bottomControlsHTML = buildBottomControls(itemsPerPageSetting); fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML; wireSelectAll(fileListContent); // PATCH each row's preview/thumb to use the secure API URLs if (totalFiles > 0) { filteredFiles.slice(startIndex, endIndex).forEach((file, idx) => { const rowEl = document.getElementById(`file-row-${encodeURIComponent(file.name)}-${startIndex + idx}`); if (!rowEl) return; const previewUrl = apiFileUrl(file.folder || folder, file.name, true); // Preview button dataset const previewBtn = rowEl.querySelector(".preview-btn"); if (previewBtn) { previewBtn.dataset.previewUrl = previewUrl; previewBtn.dataset.previewName = file.name; } // Thumbnail (if present) const thumbImg = rowEl.querySelector("img"); if (thumbImg) { thumbImg.src = previewUrl; thumbImg.setAttribute("data-cache-key", previewUrl); } // Any anchor that might have been built to point at a file path rowEl.querySelectorAll('a[href]').forEach(a => { // Only rewrite obvious file anchors (ignore actions with '#', 'javascript:', etc.) if (/^#|^javascript:/i.test(a.getAttribute('href') || '')) return; a.href = previewUrl; }); }); } fileListContent.querySelectorAll('.folder-item').forEach(el => { el.addEventListener('click', () => loadFileList(el.dataset.folder)); }); // pagination clicks const prevBtn = document.getElementById("prevPageBtn"); if (prevBtn) prevBtn.addEventListener("click", () => { if (window.currentPage > 1) { window.currentPage--; renderFileTable(folder, container); } }); const nextBtn = document.getElementById("nextPageBtn"); if (nextBtn) nextBtn.addEventListener("click", () => { if (window.currentPage < totalPages) { window.currentPage++; renderFileTable(folder, container); } }); // advanced search toggle const advToggle = document.getElementById("advancedSearchToggle"); if (advToggle) advToggle.addEventListener("click", () => { toggleAdvancedSearch(); }); // items-per-page selector const itemsSelect = document.getElementById("itemsPerPageSelect"); if (itemsSelect) itemsSelect.addEventListener("change", e => { window.itemsPerPage = parseInt(e.target.value, 10); localStorage.setItem("itemsPerPage", window.itemsPerPage); window.currentPage = 1; renderFileTable(folder, container); }); // Row-select fileListContent.querySelectorAll("tbody tr").forEach(row => { row.addEventListener("click", e => { const cb = row.querySelector(".file-checkbox"); if (!cb) return; toggleRowSelection(e, cb.value); }); }); // Download buttons fileListContent.querySelectorAll(".download-btn").forEach(btn => { btn.addEventListener("click", e => { e.stopPropagation(); openDownloadModal(btn.dataset.downloadName, btn.dataset.downloadFolder); }); }); // Edit buttons fileListContent.querySelectorAll(".edit-btn").forEach(btn => { btn.addEventListener("click", async e => { e.stopPropagation(); const m = await import('./fileEditor.js'); m.editFile(btn.dataset.editName, btn.dataset.editFolder); }); }); // Rename buttons fileListContent.querySelectorAll(".rename-btn").forEach(btn => { btn.addEventListener("click", async e => { e.stopPropagation(); const m = await import('./fileActions.js'); m.renameFile(btn.dataset.renameName, btn.dataset.renameFolder); }); }); // Preview buttons fileListContent.querySelectorAll(".preview-btn").forEach(btn => { btn.addEventListener("click", async e => { e.stopPropagation(); const m = await import('./filePreview.js'); m.previewFile(btn.dataset.previewUrl, btn.dataset.previewName); }); }); createViewToggleButton(); // search input const newSearchInput = document.getElementById("searchInput"); if (newSearchInput) { newSearchInput.addEventListener("input", debounce(function () { window.currentSearchTerm = newSearchInput.value; window.currentPage = 1; renderFileTable(folder, container); setTimeout(() => { const freshInput = document.getElementById("searchInput"); if (freshInput) { freshInput.focus(); const len = freshInput.value.length; freshInput.setSelectionRange(len, len); } }, 0); }, 300)); } const slider = document.getElementById('rowHeightSlider'); const valueDisplay = document.getElementById('rowHeightValue'); if (slider) { slider.addEventListener('input', e => { const v = +e.target.value; // slider value in px document.documentElement.style.setProperty('--file-row-height', v + 'px'); localStorage.setItem('rowHeight', v); valueDisplay.textContent = v + 'px'; }); } document.querySelectorAll("table.table thead th[data-column]").forEach(cell => { cell.addEventListener("click", function () { const column = this.getAttribute("data-column"); sortFiles(column, folder); }); }); document.querySelectorAll("#fileList .file-checkbox").forEach(checkbox => { checkbox.addEventListener("change", function (e) { updateRowHighlight(e.target); updateFileActionButtons(); }); }); document.querySelectorAll(".share-btn").forEach(btn => { btn.addEventListener("click", function (e) { e.stopPropagation(); const fileName = this.getAttribute("data-file"); const file = fileData.find(f => f.name === fileName); if (file) { import('./filePreview.js').then(module => { module.openShareModal(file, folder); }); } }); }); updateFileActionButtons(); document.querySelectorAll("#fileList tbody tr").forEach(row => { row.setAttribute("draggable", "true"); import('./fileDragDrop.js').then(module => { row.addEventListener("dragstart", module.fileDragStartHandler); }); }); document.querySelectorAll(".download-btn, .edit-btn, .rename-btn").forEach(btn => { btn.addEventListener("click", e => e.stopPropagation()); }); bindFileListContextMenu(); } // A helper to compute the max image height based on the current column count. function getMaxImageHeight() { const columns = parseInt(window.galleryColumns || 3, 10); return 150 * (7 - columns); } export function renderGalleryView(folder, container) { const fileListContent = container || document.getElementById("fileList"); const searchTerm = (window.currentSearchTerm || "").toLowerCase(); const filteredFiles = searchFiles(searchTerm); // API preview base (we’ll build per-file URLs) const apiBase = `/api/file/download.php?folder=${encodeURIComponent(folder)}&file=`; // pagination settings const itemsPerPage = window.itemsPerPage; let currentPage = window.currentPage || 1; const totalFiles = filteredFiles.length; const totalPages = Math.ceil(totalFiles / itemsPerPage); if (currentPage > totalPages) { currentPage = totalPages || 1; window.currentPage = currentPage; } // --- Top controls: search + pagination + items-per-page --- let galleryHTML = buildSearchAndPaginationControls({ currentPage, totalPages, searchTerm: window.currentSearchTerm || "" }); // wire up search input just like table view setTimeout(() => { const searchInput = document.getElementById("searchInput"); if (searchInput) { searchInput.addEventListener("input", debounce(() => { window.currentSearchTerm = searchInput.value; window.currentPage = 1; renderGalleryView(folder); setTimeout(() => { const f = document.getElementById("searchInput"); if (f) { f.focus(); const len = f.value.length; f.setSelectionRange(len, len); } }, 0); }, 300)); } }, 0); // determine column max by screen size const numColumns = window.galleryColumns || 3; const w = window.innerWidth; let maxCols = 6; if (w < 600) maxCols = 1; else if (w < 900) maxCols = 2; const startCols = Math.min(numColumns, maxCols); window.galleryColumns = startCols; // --- Start gallery grid --- galleryHTML += ` `; // end gallery-container // bottom controls galleryHTML += buildBottomControls(itemsPerPage); // render fileListContent.innerHTML = galleryHTML; // pagination buttons for gallery const prevBtn = document.getElementById("prevPageBtn"); if (prevBtn) prevBtn.addEventListener("click", () => { if (window.currentPage > 1) { window.currentPage--; renderGalleryView(folder, container); } }); const nextBtn = document.getElementById("nextPageBtn"); if (nextBtn) nextBtn.addEventListener("click", () => { if (window.currentPage < totalPages) { window.currentPage++; renderGalleryView(folder, container); } }); // advanced search toggle const advToggle = document.getElementById("advancedSearchToggle"); if (advToggle) advToggle.addEventListener("click", () => { toggleAdvancedSearch(); }); // context menu in gallery bindFileListContextMenu(); // items-per-page selector for gallery const itemsSelect = document.getElementById("itemsPerPageSelect"); if (itemsSelect) itemsSelect.addEventListener("change", e => { window.itemsPerPage = parseInt(e.target.value, 10); localStorage.setItem("itemsPerPage", window.itemsPerPage); window.currentPage = 1; renderGalleryView(folder, container); }); // cache images on load fileListContent.querySelectorAll('.gallery-thumbnail').forEach(img => { const key = img.dataset.cacheKey; img.addEventListener('load', () => cacheImage(img, key)); }); // preview clicks (dynamic import to avoid global dependency) fileListContent.querySelectorAll(".gallery-preview").forEach(el => { el.addEventListener("click", async () => { const m = await import('./filePreview.js'); m.previewFile(el.dataset.previewUrl, el.dataset.previewName); }); }); // download clicks fileListContent.querySelectorAll(".download-btn").forEach(btn => { btn.addEventListener("click", e => { e.stopPropagation(); openDownloadModal(btn.dataset.downloadName, btn.dataset.downloadFolder); }); }); // edit clicks fileListContent.querySelectorAll(".edit-btn").forEach(btn => { btn.addEventListener("click", async e => { e.stopPropagation(); const m = await import('./fileEditor.js'); m.editFile(btn.dataset.editName, btn.dataset.editFolder); }); }); // rename clicks fileListContent.querySelectorAll(".rename-btn").forEach(btn => { btn.addEventListener("click", async e => { e.stopPropagation(); const m = await import('./fileActions.js'); m.renameFile(btn.dataset.renameName, btn.dataset.renameFolder); }); }); // share clicks fileListContent.querySelectorAll(".share-btn").forEach(btn => { btn.addEventListener("click", e => { e.stopPropagation(); const fileName = btn.dataset.file; const fileObj = fileData.find(f => f.name === fileName); if (fileObj) { import('./filePreview.js').then(m => m.openShareModal(fileObj, folder)); } }); }); // checkboxes fileListContent.querySelectorAll(".file-checkbox").forEach(cb => { cb.addEventListener("change", () => updateFileActionButtons()); }); // slider const slider = document.getElementById("galleryColumnsSlider"); if (slider) { slider.addEventListener("input", () => { const v = +slider.value; document.getElementById("galleryColumnsValue").textContent = v; window.galleryColumns = v; document.querySelector(".gallery-container") .style.gridTemplateColumns = `repeat(${v},1fr)`; document.querySelectorAll(".gallery-thumbnail") .forEach(img => img.style.maxHeight = getMaxImageHeight() + "px"); }); } // pagination helpers window.changePage = newPage => { window.currentPage = newPage; if (window.viewMode === "gallery") renderGalleryView(folder); else renderFileTable(folder); }; window.changeItemsPerPage = cnt => { window.itemsPerPage = +cnt; localStorage.setItem("itemsPerPage", cnt); window.currentPage = 1; if (window.viewMode === "gallery") renderGalleryView(folder); else renderFileTable(folder); }; updateFileActionButtons(); createViewToggleButton(); } // Responsive slider constraints based on screen size. function updateSliderConstraints() { const slider = document.getElementById("galleryColumnsSlider"); if (!slider) return; const width = window.innerWidth; let min = 1; let max; if (width < 600) { max = 1; } else if (width < 1024) { max = 3; } else if (width < 1440) { max = 4; } else { max = 6; } let currentVal = parseInt(slider.value, 10); if (currentVal > max) { currentVal = max; slider.value = max; } slider.min = min; slider.max = max; document.getElementById("galleryColumnsValue").textContent = currentVal; const galleryContainer = document.querySelector(".gallery-container"); if (galleryContainer) { galleryContainer.style.gridTemplateColumns = `repeat(${currentVal}, 1fr)`; } } window.addEventListener('load', updateSliderConstraints); window.addEventListener('resize', updateSliderConstraints); export function sortFiles(column, folder) { if (sortOrder.column === column) { sortOrder.ascending = !sortOrder.ascending; } else { sortOrder.column = column; sortOrder.ascending = true; } fileData.sort((a, b) => { let valA = a[column] || ""; let valB = b[column] || ""; if (column === "modified" || column === "uploaded") { const parsedA = parseCustomDate(valA); const parsedB = parseCustomDate(valB); valA = parsedA; valB = parsedB; } else if (typeof valA === "string") { valA = valA.toLowerCase(); valB = valB.toLowerCase(); } if (valA < valB) return sortOrder.ascending ? -1 : 1; if (valA > valB) return sortOrder.ascending ? 1 : -1; return 0; }); if (window.viewMode === "gallery") { renderGalleryView(folder); } else { renderFileTable(folder); } } function parseCustomDate(dateStr) { dateStr = dateStr.replace(/\s+/g, " ").trim(); const parts = dateStr.split(" "); if (parts.length !== 2) { return new Date(dateStr).getTime(); } const datePart = parts[0]; const timePart = parts[1]; const dateComponents = datePart.split("/"); if (dateComponents.length !== 3) { return new Date(dateStr).getTime(); } let month = parseInt(dateComponents[0], 10); let day = parseInt(dateComponents[1], 10); let year = parseInt(dateComponents[2], 10); if (year < 100) { year += 2000; } const timeRegex = /^(\d{1,2}):(\d{2})(AM|PM)$/i; const match = timePart.match(timeRegex); if (!match) { return new Date(dateStr).getTime(); } let hour = parseInt(match[1], 10); const minute = parseInt(match[2], 10); const period = match[3].toUpperCase(); if (period === "PM" && hour !== 12) { hour += 12; } if (period === "AM" && hour === 12) { hour = 0; } return new Date(year, month - 1, day, hour, minute).getTime(); } export function canEditFile(fileName) { if (!fileName || typeof fileName !== "string") return false; const dot = fileName.lastIndexOf("."); if (dot < 0) return false; const ext = fileName.slice(dot + 1).toLowerCase(); const allowedExtensions = [ "txt", "text", "md", "markdown", "rst", "html", "htm", "xhtml", "shtml", "css", "scss", "sass", "less", "js", "mjs", "cjs", "jsx", "ts", "tsx", "json", "jsonc", "ndjson", "yml", "yaml", "toml", "xml", "plist", "ini", "conf", "config", "cfg", "cnf", "properties", "props", "rc", "env", "dotenv", "csv", "tsv", "tab", "log", "sh", "bash", "zsh", "ksh", "fish", "bat", "cmd", "ps1", "psm1", "psd1", "py", "pyw", "rb", "pl", "pm", "go", "rs", "java", "kt", "kts", "scala", "sc", "groovy", "gradle", "c", "h", "cpp", "cxx", "cc", "hpp", "hh", "hxx", "m", "mm", "swift", "cs", "fs", "fsx", "dart", "lua", "r", "rmd", "sql", "vue", "svelte", "twig", "mustache", "hbs", "handlebars", "ejs", "pug", "jade" ]; return allowedExtensions.includes(ext); } // Expose global functions for pagination and preview. window.changePage = function (newPage) { window.currentPage = newPage; if (window.viewMode === 'gallery') { renderGalleryView(window.currentFolder); } else { renderFileTable(window.currentFolder); } }; window.changeItemsPerPage = function (newCount) { window.itemsPerPage = parseInt(newCount, 10); localStorage.setItem('itemsPerPage', newCount); window.currentPage = 1; if (window.viewMode === 'gallery') { renderGalleryView(window.currentFolder); } else { renderFileTable(window.currentFolder); } }; // fileListView.js (bottom) window.loadFileList = loadFileList; window.renderFileTable = renderFileTable; window.renderGalleryView = renderGalleryView; window.sortFiles = sortFiles; window.toggleAdvancedSearch = toggleAdvancedSearch;