release(v2.3.1): polish file list actions & hover preview peak

This commit is contained in:
Ryan
2025-12-03 00:29:08 -05:00
committed by GitHub
parent e2d1b705bd
commit b417217552
9 changed files with 1469 additions and 204 deletions

View File

@@ -214,6 +214,308 @@ function repaintStripIcon(folder) {
const kind = iconSpan.dataset.kind || 'empty';
iconSpan.innerHTML = folderSVG(kind);
}
const TEXT_PREVIEW_MAX_BYTES = 120 * 1024; // ~120 KB
const _fileSnippetCache = new Map();
function getFileExt(name) {
const dot = name.lastIndexOf(".");
return dot >= 0 ? name.slice(dot + 1).toLowerCase() : "";
}
async function fillFileSnippet(file, snippetEl) {
if (!snippetEl) return;
snippetEl.textContent = "";
snippetEl.style.display = "none";
const folder = file.folder || window.currentFolder || "root";
const key = `${folder}::${file.name}`;
if (!canEditFile(file.name)) {
// No text preview possible for this type cache the fact and bail
_fileSnippetCache.set(key, "");
return;
}
const bytes = Number.isFinite(file.sizeBytes) ? file.sizeBytes : null;
if (bytes != null && bytes > TEXT_PREVIEW_MAX_BYTES) {
// File is too large to safely preview inline
const msg = t("no_preview_available") || "No preview available";
snippetEl.style.display = "block";
snippetEl.textContent = msg;
_fileSnippetCache.set(key, msg);
return;
}
// Use cache if we have it
if (_fileSnippetCache.has(key)) {
const cached = _fileSnippetCache.get(key);
if (cached) {
snippetEl.textContent = cached;
snippetEl.style.display = "block";
}
return;
}
snippetEl.style.display = "block";
snippetEl.textContent = t("loading") || "Loading...";
try {
const url = apiFileUrl(folder, file.name, true);
const res = await fetch(url, { credentials: "include" });
if (!res.ok) throw 0;
const text = await res.text();
const MAX_LINES = 6;
const MAX_CHARS = 600;
const allLines = text.split(/\r?\n/);
let visibleLines = allLines.slice(0, MAX_LINES);
let snippet = visibleLines.join("\n");
let truncated = allLines.length > MAX_LINES;
if (snippet.length > MAX_CHARS) {
snippet = snippet.slice(0, MAX_CHARS);
truncated = true;
}
snippet = snippet.trim();
let finalSnippet = snippet || "(empty file)";
if (truncated) {
finalSnippet += "\n…";
}
_fileSnippetCache.set(key, finalSnippet);
snippetEl.textContent = finalSnippet;
} catch {
snippetEl.textContent = "";
snippetEl.style.display = "none";
_fileSnippetCache.set(key, "");
}
}
function wireEllipsisContextMenu(fileListContent) {
if (!fileListContent) return;
fileListContent
.querySelectorAll(".btn-actions-ellipsis")
.forEach(btn => {
btn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
const row = btn.closest("tr");
if (!row) return;
const rect = btn.getBoundingClientRect();
const evt = new MouseEvent("contextmenu", {
bubbles: true,
cancelable: true,
clientX: rect.left + rect.width / 2,
clientY: rect.bottom
});
row.dispatchEvent(evt);
});
});
}
let hoverPreviewEl = null;
let hoverPreviewTimer = null;
let hoverPreviewActiveRow = null;
let hoverPreviewContext = null;
let hoverPreviewHoveringCard = false;
// Let other modules (drag/drop) kill the hover card instantly.
export function cancelHoverPreview() {
try {
if (hoverPreviewTimer) {
clearTimeout(hoverPreviewTimer);
hoverPreviewTimer = null;
}
} catch {}
hoverPreviewActiveRow = null;
hoverPreviewContext = null;
hoverPreviewHoveringCard = false;
if (hoverPreviewEl) {
hoverPreviewEl.style.display = 'none';
}
}
function isHoverPreviewDisabled() {
// Live flag from user panel
if (window.disableHoverPreview === true) return true;
// Fallback to localStorage (e.g. on first page load)
try {
return localStorage.getItem('disableHoverPreview') === 'true';
} catch {
return false;
}
}
function ensureHoverPreviewEl() {
if (hoverPreviewEl) return hoverPreviewEl;
const el = document.createElement("div");
el.id = "hoverPreview";
el.style.position = "fixed";
el.style.zIndex = "9999";
el.style.display = "none";
el.innerHTML = `
<div class="hover-preview-card">
<div class="hover-preview-grid">
<div class="hover-preview-left">
<div class="hover-preview-thumb"></div>
<pre class="hover-preview-snippet"></pre>
</div>
<div class="hover-preview-right">
<div class="hover-preview-title"></div>
<div class="hover-preview-meta"></div>
<div class="hover-preview-props"></div>
</div>
</div>
</div>
`;
document.body.appendChild(el);
hoverPreviewEl = el;
// ---- Layout + sizing tweaks ---------------------------------
const card = el.querySelector(".hover-preview-card");
const grid = el.querySelector(".hover-preview-grid");
const leftCol = el.querySelector(".hover-preview-left");
const rightCol = el.querySelector(".hover-preview-right");
const thumb = el.querySelector(".hover-preview-thumb");
const snippet = el.querySelector(".hover-preview-snippet");
const titleEl = el.querySelector(".hover-preview-title");
const metaEl = el.querySelector(".hover-preview-meta");
const propsEl = el.querySelector(".hover-preview-props");
if (card) {
card.style.minWidth = "420px";
card.style.maxWidth = "640px";
card.style.minHeight = "220px";
card.style.padding = "10px 12px";
card.style.overflow = "hidden";
}
if (grid) {
grid.style.display = "grid";
grid.style.gridTemplateColumns = "220px minmax(260px, 1fr)";
grid.style.gap = "12px";
grid.style.alignItems = "center";
}
if (leftCol) {
leftCol.style.display = "flex";
leftCol.style.flexDirection = "column";
leftCol.style.justifyContent = "center";
leftCol.style.minWidth = "0";
}
if (rightCol) {
rightCol.style.display = "flex";
rightCol.style.flexDirection = "column";
rightCol.style.justifyContent = "center";
rightCol.style.minWidth = "0";
rightCol.style.overflow = "hidden";
}
if (thumb) {
thumb.style.display = "flex";
thumb.style.alignItems = "center";
thumb.style.justifyContent = "center";
thumb.style.minHeight = "140px";
thumb.style.marginBottom = "6px";
}
if (snippet) {
snippet.style.marginTop = "4px";
snippet.style.maxHeight = "140px";
snippet.style.overflow = "auto";
snippet.style.fontSize = "0.78rem";
snippet.style.whiteSpace = "pre-wrap";
snippet.style.padding = "6px 8px";
snippet.style.borderRadius = "6px";
// Dark-mode friendly styling that still looks OK in light mode
//snippet.style.backgroundColor = "rgba(39, 39, 39, 0.92)";
snippet.style.color = "#e5e7eb";
}
if (titleEl) {
titleEl.style.fontWeight = "600";
titleEl.style.fontSize = "0.95rem";
titleEl.style.marginBottom = "2px";
titleEl.style.whiteSpace = "nowrap";
titleEl.style.overflow = "hidden";
titleEl.style.textOverflow = "ellipsis";
titleEl.style.maxWidth = "100%";
}
if (metaEl) {
metaEl.style.fontSize = "0.8rem";
metaEl.style.opacity = "0.8";
metaEl.style.marginBottom = "6px";
metaEl.style.whiteSpace = "nowrap";
metaEl.style.overflow = "hidden";
metaEl.style.textOverflow = "ellipsis";
metaEl.style.maxWidth = "100%";
}
if (propsEl) {
propsEl.style.fontSize = "0.78rem";
propsEl.style.lineHeight = "1.3";
propsEl.style.maxHeight = "160px";
propsEl.style.overflow = "auto";
propsEl.style.paddingRight = "4px";
propsEl.style.wordBreak = "break-word";
}
// Allow the user to move onto the card without it vanishing
el.addEventListener("mouseenter", () => {
hoverPreviewHoveringCard = true;
});
el.addEventListener("mouseleave", () => {
hoverPreviewHoveringCard = false;
// If we've left both the row and the card, hide after a tiny delay
setTimeout(() => {
if (!hoverPreviewActiveRow && !hoverPreviewHoveringCard) {
hideHoverPreview();
}
}, 120);
});
// Click anywhere on the card = open preview/editor/folder
el.addEventListener("click", (e) => {
e.stopPropagation();
if (!hoverPreviewContext) return;
const ctx = hoverPreviewContext;
// Hide the hover card immediately so it doesn't hang around
hideHoverPreview();
if (ctx.type === "file") {
openDefaultFileFromHover(ctx.file);
} else if (ctx.type === "folder") {
const dest = ctx.folder;
if (dest) {
window.currentFolder = dest;
try { localStorage.setItem("lastOpenedFolder", dest); } catch {}
updateBreadcrumbTitle(dest);
loadFileList(dest);
}
}
});
return el;
}
function hideHoverPreview() {
cancelHoverPreview();
}
function applyFolderStripLayout(strip) {
if (!strip) return;
@@ -316,6 +618,105 @@ function fetchFolderStats(folder) {
return p;
}
// --- Folder "peek" cache (first few child folders/files) ---
const FOLDER_PEEK_MAX_ITEMS = 6;
const _folderPeekCache = new Map();
/**
* Best-effort peek: first few direct child folders + files for a folder.
* Uses existing getFolderList.php + getFileList.php.
*
* Returns: { items: Array<{type,name}>, truncated: boolean }
*/
async function fetchFolderPeek(folder) {
if (!folder) return null;
if (_folderPeekCache.has(folder)) {
return _folderPeekCache.get(folder);
}
const p = (async () => {
try {
// 1) Files in this folder
let files = [];
try {
const res = await fetch(
`/api/file/getFileList.php?folder=${encodeURIComponent(folder)}&recursive=0&t=${Date.now()}`,
{ credentials: "include" }
);
const raw = await safeJson(res);
if (Array.isArray(raw.files)) {
files = raw.files;
} else if (raw.files && typeof raw.files === "object") {
files = Object.entries(raw.files).map(([name, meta]) => ({
...(meta || {}),
name
}));
}
} catch {
// ignore file errors; we can still show folders
}
// 2) Direct subfolders
let subfolderNames = [];
try {
const res2 = await fetch(
`/api/folder/getFolderList.php?folder=${encodeURIComponent(folder)}`,
{ credentials: "include" }
);
const raw2 = await safeJson(res2);
if (Array.isArray(raw2)) {
const allPaths = raw2.map(item => item.folder ?? item);
const depth = folder === "root" ? 1 : folder.split("/").length + 1;
subfolderNames = allPaths
.filter(p => {
if (folder === "root") return p.indexOf("/") === -1;
if (!p.startsWith(folder + "/")) return false;
return p.split("/").length === depth;
})
.map(p => p.split("/").pop() || p);
}
} catch {
// ignore folder errors
}
const items = [];
// Folders first
for (const name of subfolderNames) {
if (!name) continue;
items.push({ type: "folder", name });
if (items.length >= FOLDER_PEEK_MAX_ITEMS) break;
}
// Then a few files
if (items.length < FOLDER_PEEK_MAX_ITEMS && Array.isArray(files)) {
for (const f of files) {
if (!f || !f.name) continue;
items.push({ type: "file", name: f.name });
if (items.length >= FOLDER_PEEK_MAX_ITEMS) break;
}
}
// Were there more candidates than we showed?
const totalCandidates =
(Array.isArray(subfolderNames) ? subfolderNames.length : 0) +
(Array.isArray(files) ? files.length : 0);
const truncated = totalCandidates > items.length;
return { items, truncated };
} catch {
return null;
}
})();
_folderPeekCache.set(folder, p);
return p;
}
/* ===========================================================
SECURITY: build file URLs only via the API (no /uploads)
=========================================================== */
@@ -383,6 +784,258 @@ function wireSelectAll(fileListContent) {
syncHeader();
}
function fillHoverPreviewForRow(row) {
if (isHoverPreviewDisabled()) {
hideHoverPreview();
return;
}
const el = ensureHoverPreviewEl();
const titleEl = el.querySelector(".hover-preview-title");
const metaEl = el.querySelector(".hover-preview-meta");
const thumbEl = el.querySelector(".hover-preview-thumb");
const propsEl = el.querySelector(".hover-preview-props");
const snippetEl = el.querySelector(".hover-preview-snippet");
if (!titleEl || !metaEl || !thumbEl || !propsEl || !snippetEl) return;
// Reset content
thumbEl.innerHTML = "";
propsEl.innerHTML = "";
snippetEl.textContent = "";
snippetEl.style.display = "none";
metaEl.textContent = "";
titleEl.textContent = "";
// Reset per-row sizing (we only make this tall for images)
thumbEl.style.minHeight = "0";
const isFolder = row.classList.contains("folder-row");
if (isFolder) {
// =========================
// FOLDER HOVER PREVIEW
// =========================
const folderPath = row.dataset.folder || "";
const folderName = folderPath.split("/").pop() || folderPath || "(root)";
titleEl.textContent = folderName;
hoverPreviewContext = {
type: "folder",
folder: folderPath
};
// Right column: icon + path
const iconHtml = `
<div class="hover-prop-line" style="display:flex;align-items:center;margin-bottom:4px;">
<span class="hover-preview-icon material-icons" style="margin-right:6px;">folder</span>
<strong>${t("folder") || "Folder"}</strong>
</div>
`;
let propsHtml = iconHtml;
propsHtml += `
<div class="hover-prop-line">
<strong>${t("path") || "Path"}:</strong> ${escapeHTML(folderPath || "root")}
</div>
`;
propsEl.innerHTML = propsHtml;
// Meta: counts + size
fetchFolderStats(folderPath).then(stats => {
if (!stats || !document.body.contains(el)) return;
if (!hoverPreviewContext || hoverPreviewContext.folder !== folderPath) return;
const foldersCount = Number.isFinite(stats.folders) ? stats.folders : 0;
const filesCount = Number.isFinite(stats.files) ? stats.files : 0;
let bytes = null;
const sizeCandidates = [stats.bytes, stats.sizeBytes, stats.size, stats.totalBytes];
for (const v of sizeCandidates) {
const n = Number(v);
if (Number.isFinite(n) && n >= 0) {
bytes = n;
break;
}
}
const pieces = [];
if (foldersCount) pieces.push(`${foldersCount} folder${foldersCount === 1 ? "" : "s"}`);
if (filesCount) pieces.push(`${filesCount} file${filesCount === 1 ? "" : "s"}`);
if (!pieces.length) pieces.push("0 items");
const sizeLabel = bytes != null && bytes >= 0 ? formatSize(bytes) : "";
metaEl.textContent = sizeLabel
? `${pieces.join(", ")}${sizeLabel}`
: pieces.join(", ");
}).catch(() => {});
// Left side: peek inside folder (first few children)
// Left side: peek inside folder (first few children)
fetchFolderPeek(folderPath).then(result => {
if (!document.body.contains(el)) return;
if (!hoverPreviewContext || hoverPreviewContext.folder !== folderPath) return;
if (!result) {
snippetEl.style.display = "none";
return;
}
const { items, truncated } = result;
// If nothing inside, show a friendly message like files do
if (!items || !items.length) {
const msg =
t("no_files_or_folders") ||
t("no_files_found") ||
"No files or folders";
snippetEl.textContent = msg;
snippetEl.style.display = "block";
return;
}
const lines = items.map(it => {
const prefix = it.type === "folder" ? "📁 " : "📄 ";
return prefix + it.name;
});
// If we had to cut the list to FOLDER_PEEK_MAX_ITEMS, turn the LAST line into "…"
if (truncated && lines.length) {
lines[lines.length - 1] = "…";
}
snippetEl.textContent = lines.join("\n");
snippetEl.style.display = "block";
}).catch(() => {});
} else {
// ======================
// FILE HOVER PREVIEW
// ======================
const name = row.getAttribute("data-file-name") || "";
const file = fileData.find(f => f.name === name) || null;
hoverPreviewContext = {
type: "file",
file
};
if (!file) {
titleEl.textContent = name || "(unknown)";
metaEl.textContent = "";
return;
}
titleEl.textContent = file.name;
// IMPORTANT: no duplicate "size • modified • owner" under the title
metaEl.textContent = "";
const ext = getFileExt(file.name);
const lower = file.name.toLowerCase();
const isImage = /\.(jpg|jpeg|png|gif|bmp|webp|ico|tif|tiff|heic)$/i.test(lower);
const isVideo = /\.(mp4|mkv|webm|mov|ogv)$/i.test(lower);
const isAudio = /\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(lower);
const isPdf = /\.pdf$/i.test(lower);
const folder = file.folder || window.currentFolder || "root";
const url = apiFileUrl(folder, file.name, true);
const canTextPreview = canEditFile(file.name);
// Left: image preview OR text snippet OR "No preview"
if (isImage) {
thumbEl.style.minHeight = "140px";
const img = document.createElement("img");
img.src = url;
img.alt = file.name;
img.style.maxWidth = "180px";
img.style.maxHeight = "120px";
img.style.display = "block";
thumbEl.appendChild(img);
}
// Icon type for right column
let iconName = "insert_drive_file";
if (isImage) iconName = "image";
else if (isVideo) iconName = "movie";
else if (isAudio) iconName = "audiotrack";
else if (isPdf) iconName = "picture_as_pdf";
const props = [];
// Icon row at the top of the right column
props.push(`
<div class="hover-prop-line" style="display:flex;align-items:center;margin-bottom:4px;">
<span class="hover-preview-icon material-icons" style="margin-right:6px;">${iconName}</span>
<strong>${escapeHTML(ext || "").toUpperCase() || t("file") || "File"}</strong>
</div>
`);
if (ext) {
props.push(`<div class="hover-prop-line"><strong>${t("extension") || "Ext"}:</strong> .${escapeHTML(ext)}</div>`);
}
if (file.size) {
props.push(`<div class="hover-prop-line"><strong>${t("size") || "Size"}:</strong> ${escapeHTML(file.size)}</div>`);
}
if (file.modified) {
props.push(`<div class="hover-prop-line"><strong>${t("modified") || "Modified"}:</strong> ${escapeHTML(file.modified)}</div>`);
}
if (file.uploaded) {
props.push(`<div class="hover-prop-line"><strong>${t("created") || "Created"}:</strong> ${escapeHTML(file.uploaded)}</div>`);
}
if (file.uploader) {
props.push(`<div class="hover-prop-line"><strong>${t("owner") || "Owner"}:</strong> ${escapeHTML(file.uploader)}</div>`);
}
propsEl.innerHTML = props.join("");
// Text snippet (left) for smaller text/code files
if (canTextPreview) {
fillFileSnippet(file, snippetEl);
} else if (!isImage) {
// Non-image, non-text → explicit "No preview"
const msg = t("no_preview_available") || "No preview available";
thumbEl.innerHTML = `
<div style="
padding:6px 8px;
border-radius:6px;
font-size:0.8rem;
text-align:center;
background-color:rgba(15,23,42,0.92);
color:#e5e7eb;
max-width:100%;
">
${escapeHTML(msg)}
</div>
`;
}
}
}
function positionHoverPreview(x, y) {
const el = ensureHoverPreviewEl();
const CARD_OFFSET_X = 16;
const CARD_OFFSET_Y = 12;
let left = x + CARD_OFFSET_X;
let top = y + CARD_OFFSET_Y;
const rect = el.getBoundingClientRect();
const vw = window.innerWidth;
const vh = window.innerHeight;
if (left + rect.width > vw - 10) {
left = x - rect.width - CARD_OFFSET_X;
}
if (top + rect.height > vh - 10) {
top = y - rect.height - CARD_OFFSET_Y;
}
el.style.left = `${Math.max(4, left)}px`;
el.style.top = `${Math.max(4, top)}px`;
}
// ---- Folder-strip icon helpers (same geometry as tree, but colored inline) ----
function _hexToHsl(hex) {
hex = String(hex || '').replace('#', '');
@@ -932,15 +1585,22 @@ export async function loadFileList(folderParam) {
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);
// If we can't parse a sane size, treat as "unknown" instead of Infinity
if (!Number.isFinite(bytes) || bytes < 0) {
bytes = null;
}
f.sizeBytes = bytes;
// For editing: if size is unknown, assume it's OK and let the editor enforce limits.
const safeForEdit = (bytes == null) || (bytes <= MAX_EDIT_BYTES);
f.editable = canEditFile(f.name) && safeForEdit;
f.folder = folder;
return f;
});
@@ -1256,51 +1916,17 @@ if (headerClass) {
} else if (i === actionsIdx) {
td.classList.add("folder-actions-cell");
const group = document.createElement("div");
group.className = "btn-group btn-group-sm folder-actions-group";
group.setAttribute("role", "group");
group.setAttribute("aria-label", "File actions");
const btn = document.createElement("button");
btn.type = "button";
btn.className = "btn btn-link btn-actions-ellipsis";
btn.title = t("more_actions");
const makeActionBtn = (iconName, titleKey, btnClass, actionKey, handler) => {
const btn = document.createElement("button");
btn.type = "button";
// base classes same size as file actions
btn.className = `btn ${btnClass} py-1`;
// kill any Bootstrap margin helpers that got passed in
btn.classList.remove("ml-2", "mx-2");
btn.setAttribute("data-folder-action", actionKey);
btn.setAttribute("data-i18n-title", titleKey);
btn.title = t(titleKey);
const icon = document.createElement("i");
icon.className = "material-icons";
icon.textContent = iconName;
btn.appendChild(icon);
btn.addEventListener("click", e => {
e.stopPropagation();
window.currentFolder = sf.full;
try { localStorage.setItem("lastOpenedFolder", sf.full); } catch {}
handler();
});
// start disabled; caps logic will enable
btn.disabled = true;
btn.style.pointerEvents = "none";
btn.style.opacity = "0.5";
group.appendChild(btn);
};
const icon = document.createElement("span");
icon.className = "material-icons";
icon.textContent = "more_vert";
makeActionBtn("drive_file_move", "move_folder", "btn-warning folder-move-btn", "move", () => openMoveFolderUI());
makeActionBtn("palette", "color_folder", "btn-color-folder","color", () => openColorFolderModal(sf.full));
makeActionBtn("drive_file_rename_outline", "rename_folder", "btn-warning folder-rename-btn", "rename", () => openRenameFolderModal());
makeActionBtn("share", "share_folder", "btn-secondary", "share", () => openFolderShareModal(sf.full));
td.appendChild(group);
btn.appendChild(icon);
td.appendChild(btn);
}
// IMPORTANT: always append the cell, no matter which column we're in
@@ -1309,22 +1935,27 @@ makeActionBtn("share", "share_folder", "btn-secondary",
// click → navigate, same as before
tr.addEventListener("click", e => {
// If the click came from the 3-dot button, let the context menu logic handle it
if (e.target.closest(".btn-actions-ellipsis")) {
return;
}
if (e.button !== 0) return;
const dest = sf.full;
if (!dest) return;
window.currentFolder = dest;
try { localStorage.setItem("lastOpenedFolder", dest); } catch { }
updateBreadcrumbTitle(dest);
document.querySelectorAll(".folder-option.selected")
.forEach(o => o.classList.remove("selected"));
const treeNode = document.querySelector(
`.folder-option[data-folder="${CSS.escape(dest)}"]`
);
if (treeNode) treeNode.classList.add("selected");
const strip = document.getElementById("folderStripContainer");
if (strip) {
strip.querySelectorAll(".folder-item.selected")
@@ -1334,7 +1965,7 @@ makeActionBtn("share", "share_folder", "btn-secondary",
);
if (stripItem) stripItem.classList.add("selected");
}
loadFileList(dest);
});
@@ -1563,9 +2194,30 @@ function syncFolderIconSizeToRowHeight() {
svg.style.transform = `translateY(${offsetY}px) scale(${scale})`;
});
}
async function openDefaultFileFromHover(file) {
if (!file) return;
const folder = file.folder || window.currentFolder || "root";
try {
if (canEditFile(file.name) && file.editable) {
const m = await import('./fileEditor.js?v={{APP_QVER}}');
m.editFile(file.name, folder);
} else {
const url = apiFileUrl(folder, file.name, true);
const m = await import('./filePreview.js?v={{APP_QVER}}');
m.previewFile(url, file.name);
}
} catch (e) {
console.error("Failed to open hover preview action", e);
}
}
/**
* Render table view
*/
export function renderFileTable(folder, container, subfolders) {
const fileListContent = container || document.getElementById("fileList");
const searchTerm = (window.currentSearchTerm || "").toLowerCase();
@@ -1680,11 +2332,100 @@ export function renderFileTable(folder, container, subfolders) {
fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML;
// Inject inline folder rows for THIS page (Explorer-style)
if (window.showInlineFolders !== false && pageFolders.length) {
injectInlineFolderRows(fileListContent, folder, pageFolders);
}
wireSelectAll(fileListContent);
// ---- MOBILE FIX: show "Size" column for files (Name | Size | Actions) ----
(function fixMobileFileSizeColumn() {
const isMobile = window.innerWidth <= 640;
if (!isMobile) return;
const table = fileListContent.querySelector("table.filr-table");
if (!table || !table.tHead || !table.tBodies.length) return;
const thead = table.tHead;
const tbody = table.tBodies[0];
const headerCells = Array.from(thead.querySelectorAll("th"));
// Find the Size column index by label or data-column
const sizeIdx = headerCells.findIndex(th =>
(th.dataset && (th.dataset.column === "size" || th.dataset.column === "filesize")) ||
/\bsize\b/i.test((th.textContent || "").trim())
);
if (sizeIdx < 0) return;
// Unhide Size header on mobile
const sizeTh = headerCells[sizeIdx];
sizeTh.classList.remove(
"hide-small",
"hide-medium",
"d-none",
"d-sm-table-cell",
"d-md-table-cell",
"d-lg-table-cell",
"d-xl-table-cell"
);
// Unhide the Size cell in every body row (files + folders)
Array.from(tbody.rows).forEach(row => {
if (sizeIdx >= row.cells.length) return;
const td = row.cells[sizeIdx];
if (!td) return;
td.classList.remove(
"hide-small",
"hide-medium",
"d-none",
"d-sm-table-cell",
"d-md-table-cell",
"d-lg-table-cell",
"d-xl-table-cell"
);
});
})();
// Inject inline folder rows for THIS page (Explorer-style) first
if (window.showInlineFolders !== false && pageFolders.length) {
injectInlineFolderRows(fileListContent, folder, pageFolders);
}
// Now wire 3-dot ellipsis so it also picks up folder rows
wireEllipsisContextMenu(fileListContent);
// Hover preview (desktop only, and only if user didnt disable it)
if (window.innerWidth >= 768 && !isHoverPreviewDisabled()) {
fileListContent.querySelectorAll("tbody tr").forEach(row => {
if (row.classList.contains("folder-strip-row")) return;
row.addEventListener("mouseenter", (e) => {
hoverPreviewActiveRow = row;
clearTimeout(hoverPreviewTimer);
hoverPreviewTimer = setTimeout(() => {
if (hoverPreviewActiveRow === row && !isHoverPreviewDisabled()) {
fillHoverPreviewForRow(row);
const el = ensureHoverPreviewEl();
el.style.display = "block";
positionHoverPreview(e.clientX, e.clientY);
}
}, 180);
});
row.addEventListener("mouseleave", () => {
hoverPreviewActiveRow = null;
clearTimeout(hoverPreviewTimer);
setTimeout(() => {
if (!hoverPreviewActiveRow && !hoverPreviewHoveringCard) {
hideHoverPreview();
}
}, 120);
});
row.addEventListener("contextmenu", () => {
hoverPreviewActiveRow = null;
clearTimeout(hoverPreviewTimer);
hideHoverPreview();
});
});
}
wireSelectAll(fileListContent);
// PATCH each row's preview/thumb to use the secure API URLs
// PATCH each row's preview/thumb to use the secure API URLs
@@ -1869,7 +2610,10 @@ export function renderFileTable(folder, container, subfolders) {
document.querySelectorAll(".download-btn, .edit-btn, .rename-btn").forEach(btn => {
btn.addEventListener("click", e => e.stopPropagation());
});
// Right-click context menu stays for power users
bindFileListContextMenu();
refreshViewedBadges(folder).catch(() => { });
}