// fileListView.js
import {
  escapeHTML,
  debounce,
  buildSearchAndPaginationControls,
  buildFileTableHeader,
  buildFileTableRow,
  buildBottomControls,
  updateFileActionButtons,
  showToast,
  updateRowHighlight,
  toggleRowSelection,
  attachEnterKeyListener
} from './domUtils.js?v={{APP_QVER}}';
import { withBase } from './basePath.js?v={{APP_QVER}}';
import { t } from './i18n.js?v={{APP_QVER}}';
import { bindFileListContextMenu } from './fileMenu.js?v={{APP_QVER}}';
import {
  openDownloadModal,
  handleCopySelected,
  handleMoveSelected,
  handleDeleteSelected,
  handleRenameSelected
} from './fileActions.js?v={{APP_QVER}}';
import { openTagModal, openMultiTagModal } from './fileTags.js?v={{APP_QVER}}';
import {
  getParentFolder,
  updateBreadcrumbTitle,
  setupBreadcrumbDelegation,
  showFolderManagerContextMenu,
  hideFolderManagerContextMenu,
  openRenameFolderModal,
  renameFolderInline,
  openDeleteFolderModal,
  refreshFolderIcon,
  openColorFolderModal,
  openMoveFolderUI,
  startInlineRenameInTree,
  startFolderCryptoJobFlow,
  folderSVG,
  expandTreePath,
  expandTreePathAsync,
  loadFolderTree,
  resetFolderTreeCaches,
  syncFolderTreeSelection,
  syncTreeAfterFolderMove
} from './folderManager.js?v={{APP_QVER}}';
import { openFolderShareModal } from './folderShareModal.js?v={{APP_QVER}}';
import {
  folderDragOverHandler,
  folderDragLeaveHandler,
  folderDropHandler
} from './fileDragDrop.js?v={{APP_QVER}}';
import { runTransferJob } from './transferJobs.js?v={{APP_QVER}}';

export let fileData = [];
export let sortOrder = { column: "modified", ascending: false };

let searchEverywhereBtn = null;
let searchEverywhereModal = null;
let searchEverywhereCard = null;
let searchEverywhereResultsEl = null;
let searchEverywhereInputEl = null;
let searchEverywhereLimitEl = null;
let searchEverywhereSourceEl = null;
let searchEverywhereStatusEl = null;
let searchEverywhereListEl = null;
let searchEverywhereRunId = 0;
let pendingSearchSelection = null;

try {
  window.addEventListener('folderCapsUpdated', (e) => {
    const d = (e && e.detail) || {};
    const folder = d.folder || window.currentFolder || 'root';
    // Banner is only relevant for the currently viewed folder, but icons in the list may reference
    // other folders (e.g. decrypting from a parent folder view), so always refresh icons.
    if (folder === (window.currentFolder || 'root')) {
      updateEncryptedFolderBanner(folder);
      applyEncryptedFolderUiRestrictions();
    }

    // Best-effort: update icons for this specific folder if it's visible in the list/strip.
    try {
      const stripItem = document.querySelector(`#folderStripContainer .folder-item[data-folder="${CSS.escape(folder)}"]`);
      if (stripItem) attachStripIconAsync(stripItem, folder, 48);
    } catch (e2) { /* ignore */ }
    try {
      const row = document.querySelector(`#fileList tr.folder-row[data-folder="${CSS.escape(folder)}"]`);
      if (row) attachStripIconAsync(row, folder, 28);
    } catch (e3) { /* ignore */ }

    refreshEncryptedFolderIconsInList();
  });
} catch (e) { /* ignore */ }

function ensureEncryptedFolderBanner() {
  const titleEl = document.getElementById('fileListTitle');
  if (!titleEl) return null;
  let el = document.getElementById('frEncryptedFolderBanner');
  if (el) {
    titleEl.insertAdjacentElement('afterend', el);
    return el;
  }

  el = document.createElement('div');
  el.id = 'frEncryptedFolderBanner';
  el.className = 'fr-enc-banner';
  el.style.display = 'none';
  titleEl.insertAdjacentElement('afterend', el);
  return el;
}

function updateEncryptedFolderBanner(folder) {
  const el = ensureEncryptedFolderBanner();
  if (!el) return;

  const enc = window.currentFolderCaps?.encryption || null;
  const isEncrypted = !!enc?.encrypted;
  if (!isEncrypted) {
    el.style.display = 'none';
    el.textContent = '';
    return;
  }

  const inherited = !!enc?.inherited;
  const root = (enc && enc.root) ? String(enc.root) : (folder || 'root');
  const rootLabel = inherited && root ? ` (inherited from ${root})` : '';

  el.style.display = 'flex';
  el.textContent = '';

  const pill = document.createElement('span');
  pill.className = 'fr-enc-pill';
  pill.textContent = 'Encrypted';

  const text = document.createElement('div');
  text.className = 'fr-enc-text';
  text.textContent =
    `This folder${rootLabel} is encrypted. ` +
    'Video/audio previews, WebDAV, ONLYOFFICE, and archive create/extract are disabled.';

  el.append(pill, text);
}

function applyEncryptedFolderUiRestrictions() {
  const caps = window.currentFolderCaps || null;
  const inEncrypted = !!(caps && caps.encryption && caps.encryption.encrypted);
  const allowShare = !inEncrypted && (caps ? !!(caps.canShareFile || caps.canShare) : true);

  try {
    document.querySelectorAll('#fileList .share-btn').forEach(btn => {
      btn.style.display = allowShare ? '' : 'none';
    });
  } catch (e) { /* ignore */ }
}

function isEncryptedForFolderIcon(folder) {
  try {
    const enc = window.currentFolderCaps?.encryption || null;
    if (enc && enc.encrypted) return true;
  } catch (e) { /* ignore */ }
  try {
    const el = document.querySelector(`.folder-option[data-folder="${CSS.escape(folder)}"]`);
    return !!(el && el.classList.contains('encrypted'));
  } catch (e) {
    return false;
  }
}

function refreshEncryptedFolderIconsInList() {
  // Folder strip icons
  try {
    const strip = document.getElementById('folderStripContainer');
    if (strip) {
      strip.querySelectorAll('.folder-item').forEach(item => {
        const folder = item.getAttribute('data-folder') || '';
        if (!folder) return;
        const iconSpan = item.querySelector('.folder-svg');
        if (!iconSpan) return;
        const kind = iconSpan.dataset.kind || 'empty';
        iconSpan.innerHTML = folderSVG(kind, { encrypted: isEncryptedForFolderIcon(folder) });
      });
    }
  } catch (e) { /* best effort */ }

  // Inline folder rows
  try {
    document.querySelectorAll('#fileList tr.folder-row[data-folder], #fileListSecondary tr.folder-row[data-folder]').forEach(row => {
      const folder = row.getAttribute('data-folder') || '';
      if (!folder) return;
      const iconSpan = row.querySelector('.folder-svg');
      if (!iconSpan) return;
      const kind = iconSpan.dataset.kind || 'empty';
      iconSpan.innerHTML = folderSVG(kind, { encrypted: isEncryptedForFolderIcon(folder) });
    });
  } catch (e) { /* best effort */ }
}

function decodeHtmlEntities(str) {
  if (!str) return "";
  try {
    const input = String(str);
    return input.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (match, code) => {
      if (!code) return match;
      if (code[0] === '#') {
        const isHex = code[1] === 'x' || code[1] === 'X';
        const num = isHex ? parseInt(code.slice(2), 16) : parseInt(code.slice(1), 10);
        if (!Number.isFinite(num)) return match;
        try { return String.fromCodePoint(num); } catch (e) { return match; }
      }
      const table = {
        amp: '&',
        lt: '<',
        gt: '>',
        quot: '"',
        apos: "'",
        nbsp: '\u00A0',
        colon: ':',
        slash: '/',
        backslash: '\\'
      };
      const lower = code.toLowerCase();
      return Object.prototype.hasOwnProperty.call(table, lower) ? table[lower] : match;
    });
  } catch (e) {
    return String(str);
  }
}

function compareSemverLite(a, b) {
  const pa = String(a || '').split('.').map(n => parseInt(n, 10) || 0);
  const pb = String(b || '').split('.').map(n => parseInt(n, 10) || 0);
  const len = Math.max(pa.length, pb.length);
  for (let i = 0; i < len; i++) {
    const na = pa[i] || 0;
    const nb = pb[i] || 0;
    if (na > nb) return 1;
    if (na < nb) return -1;
  }
  return 0;
}


const FOLDER_STRIP_PAGE_SIZE = 50;
const SLOW_LOAD_TOAST_DELAY_MS = 8000;
const SLOW_LOAD_TOAST_COOLDOWN_MS = 30000;
let _lastSlowLoadToastAt = 0;
// onnlyoffice
let OO_ENABLED = false;
let OO_EXTS = new Set();

// Late-import safety: many legacy modules bind on DOMContentLoaded/load.
// If this file is imported after those events already fired, replay them once
// so modal buttons (delete/move/rename/etc.) wire up correctly.
(function ensureLegacyReadyReplay() {
  if (window.__FR_READY_REPLAYED__) return;
  if (document.readyState === 'loading') return; // native event still pending
  window.__FR_READY_REPLAYED__ = true;
  try { document.dispatchEvent(new Event('DOMContentLoaded')); } catch (e) { }
  try { window.dispatchEvent(new Event('load')); } catch (e) { }
})();

export async function initOnlyOfficeCaps() {
  if (window.__FR_OO_PROMISE) return window.__FR_OO_PROMISE;
  window.__FR_OO_PROMISE = (async () => {
    try {
      const r = await fetch(withBase('/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 (e) {
      OO_ENABLED = false;
      OO_EXTS = new Set();
    } finally {
      window.__FR_OO_PROMISE = null;
    }
  })();
  return window.__FR_OO_PROMISE;
}

function wireFolderStripItems(strip, sourceId = '') {
  if (!strip) return;
  const paneSourceId = sourceId || getActivePaneSourceId();

  // Click / DnD / context menu
  strip.querySelectorAll(".folder-item").forEach(el => {
    // 1) click to navigate
    el.addEventListener("click", () => {
      const dest = el.dataset.folder;
      if (!dest) return;

      setCurrentFolderContext(dest, { resetPage: true });

      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", async e => {
      e.preventDefault();
      e.stopPropagation();

      const dest = el.dataset.folder;
      if (!dest) return;

      strip.querySelectorAll(".folder-item.selected")
        .forEach(i => i.classList.remove("selected"));
      el.classList.add("selected");

      let caps = null;
      try {
        caps = await fetchFolderCaps(dest, paneSourceId);
      } catch (e2) { /* ignore */ }
      const enc = (caps && caps.encryption) ? caps.encryption : {};
      const canEncrypt = !!enc.canEncrypt;
      const canDecrypt = !!enc.canDecrypt;

      const menuItems = [
        {
          label: t("create_folder"),
          action: () => {
            // Create inside this folder (without changing the current view yet)
            window.currentFolder = dest;
            const modal = document.getElementById("createFolderModal");
            if (modal) modal.style.display = "block";
            const input = document.getElementById("newFolderName");
            if (input) input.focus();
          }
        },
        {
          label: t("move_folder"),
          action: () => openMoveFolderUI(dest)
        },
        {
          label: t("rename_folder"),
          action: () => {
            window.currentFolder = dest;
            openRenameFolderModal();
          }
        },
        {
          label: t("color_folder"),
          action: () => openColorFolderModal(dest)
        },
        ...(canEncrypt ? [{
          label: 'Encrypt folder',
          icon: 'lock',
          action: () => startFolderCryptoJobFlow(dest, 'encrypt')
        }] : []),
        ...(canDecrypt ? [{
          label: 'Decrypt folder',
          icon: 'lock_open',
          action: () => startFolderCryptoJobFlow(dest, 'decrypt')
        }] : []),
        {
          label: t("folder_share"),
          action: () => openFolderShareModal(dest)
        },
        {
          label: t("delete_folder"),
          action: () => {
            window.currentFolder = dest;
            openDeleteFolderModal();
          }
        }
      ];
      showFolderManagerContextMenu(e.clientX, e.clientY, menuItems);

      // Defensive: some browsers/styles occasionally blank the SVG after a contextmenu.
      // If it happens, repaint just this icon.
      try {
        queueMicrotask(() => {
          const iconSpan = el.querySelector('.folder-svg');
          if (iconSpan && String(iconSpan.innerHTML || '').trim() === '') {
            attachStripIconAsync(el, dest, 48, { sourceId: paneSourceId });
          }
        });
      } catch (e2) { /* ignore */ }
    });
  });

  // Close menu when clicking elsewhere
  document.removeEventListener("click", hideFolderManagerContextMenu);
  document.addEventListener("click", hideFolderManagerContextMenu);

  // Folder icons
  strip.querySelectorAll(".folder-item").forEach(el => {
    const full = el.getAttribute('data-folder');
    if (full) attachStripIconAsync(el, full, 48, { sourceId: paneSourceId });
  });
}

function renderFolderStripPaged(strip, subfolders, sourceId = '') {
  if (!strip) return;

  if (!window.showFoldersInList || !subfolders.length) {
    strip.style.display = "none";
    strip.innerHTML = "";
    return;
  }

  const total = subfolders.length;
  const pageSize = FOLDER_STRIP_PAGE_SIZE;
  const totalPages = Math.ceil(total / pageSize);

  function drawPage(page) {
    const endIdx = Math.min(page * pageSize, total);
    const visible = subfolders.slice(0, endIdx);

    const badgeText = formatSourceBadgeText(getSourceMetaById(sourceId));
    const badgeHtml = badgeText
      ? `<div class="folder-strip-badge fr-source-badge" title="Source: ${escapeHTML(badgeText)}">${escapeHTML(badgeText)}</div>`
      : '';

    let html = badgeHtml + visible.map(sf => `
      <div class="folder-item"
           data-folder="${sf.full}"
           draggable="true">
        <span class="folder-svg"></span>
        <div class="folder-name">
          ${escapeHTML(sf.name)}
        </div>
      </div>
    `).join("");

    if (endIdx < total) {
      html += `
        <button type="button"
                class="folder-strip-load-more">
          ${t('load_more_folders') || t('load_more') || 'Load more folders'}
        </button>
      `;
    }

    strip.innerHTML = html;

    applyFolderStripLayout(strip);
    wireFolderStripItems(strip, sourceId);

    const loadMoreBtn = strip.querySelector(".folder-strip-load-more");
    if (loadMoreBtn) {
      loadMoreBtn.addEventListener("click", e => {
        e.preventDefault();
        e.stopPropagation();
        drawPage(page + 1);
      });
    }
  }

  drawPage(1);
}

function _trimLabel(str, max = 40) {
  if (!str) return "";
  const s = String(str);
  if (s.length <= max) return s;
  return s.slice(0, max - 1) + "…";
}

// helper to repaint one strip item quickly
function repaintStripIcon(folder) {
  const el = document.querySelector(`#folderStripContainer .folder-item[data-folder="${CSS.escape(folder)}"]`);
  if (!el) return;
  const iconSpan = el.querySelector('.folder-svg');
  if (!iconSpan) return;

  const hex = (window.folderColorMap && window.folderColorMap[folder]) || '#f6b84e';
  const front = hex;
  const back = _lighten(hex, 14);
  const stroke = _darken(hex, 22);
  el.style.setProperty('--filr-folder-front', front);
  el.style.setProperty('--filr-folder-back', back);
  el.style.setProperty('--filr-folder-stroke', stroke);

  const kind = iconSpan.dataset.kind || 'empty';
  iconSpan.innerHTML = folderSVG(kind, { encrypted: isEncryptedForFolderIcon(folder) });
}
const TEXT_PREVIEW_MAX_BYTES = 512 * 1024;
const DEFAULT_HOVER_PREVIEW_MAX_MB = 8;
const MIN_HOVER_PREVIEW_MAX_MB = 1;
const MAX_HOVER_PREVIEW_MAX_MB = 50;
const DEFAULT_HOVER_PREVIEW_MAX_VIDEO_MB = 200;
const MIN_HOVER_PREVIEW_MAX_VIDEO_MB = 1;
const MAX_HOVER_PREVIEW_MAX_VIDEO_MB = 2048;
const DEFAULT_HOVER_PREVIEW_DELAY_MS = 180;
const MIN_HOVER_PREVIEW_DELAY_MS = 0;
const MAX_HOVER_PREVIEW_DELAY_MS = 2500;
const HOVER_PREVIEW_DELAY_STORAGE_KEY = 'hoverPreviewDelayMs';
const DEFAULT_FILE_LIST_SUMMARY_DEPTH = 2;
const MIN_FILE_LIST_SUMMARY_DEPTH = 0;
const MAX_FILE_LIST_SUMMARY_DEPTH = 10;
const OFFICE_SNIPPET_EXTS = new Set([
  'doc', 'docx', 'docm', 'dotx',
  'xls', 'xlsx', 'xlsm', 'xltx',
  'ppt', 'pptx', 'pptm', 'potx'
]);
const _fileSnippetCache = new Map();

function getMaxImagePreviewBytes() {
  const cfg = window.__FR_SITE_CFG__ || window.siteConfig || {};
  const display = (cfg && typeof cfg.display === 'object') ? cfg.display : {};
  const raw = parseInt(display.hoverPreviewMaxImageMb, 10);
  const mb = Number.isFinite(raw) ? raw : DEFAULT_HOVER_PREVIEW_MAX_MB;
  const clamped = Math.max(MIN_HOVER_PREVIEW_MAX_MB, Math.min(MAX_HOVER_PREVIEW_MAX_MB, mb));
  return clamped * 1024 * 1024;
}

function getMaxVideoPreviewBytes() {
  const cfg = window.__FR_SITE_CFG__ || window.siteConfig || {};
  const display = (cfg && typeof cfg.display === 'object') ? cfg.display : {};
  const raw = parseInt(display.hoverPreviewMaxVideoMb, 10);
  const mb = Number.isFinite(raw) ? raw : DEFAULT_HOVER_PREVIEW_MAX_VIDEO_MB;
  const clamped = Math.max(MIN_HOVER_PREVIEW_MAX_VIDEO_MB, Math.min(MAX_HOVER_PREVIEW_MAX_VIDEO_MB, mb));
  return clamped * 1024 * 1024;
}

function arePdfThumbnailsEnabled() {
  const cfg = window.__FR_SITE_CFG__ || window.siteConfig || {};
  const display = (cfg && typeof cfg.display === 'object') ? cfg.display : {};
  if (!Object.prototype.hasOwnProperty.call(display, 'enablePdfThumbnails')) {
    return false;
  }
  return !!display.enablePdfThumbnails;
}

function getFileListSummaryDepth() {
  const cfg = window.__FR_SITE_CFG__ || window.siteConfig || {};
  const display = (cfg && typeof cfg.display === 'object') ? cfg.display : {};
  const raw = parseInt(display.fileListSummaryDepth, 10);
  const depth = Number.isFinite(raw) ? raw : DEFAULT_FILE_LIST_SUMMARY_DEPTH;
  return Math.max(MIN_FILE_LIST_SUMMARY_DEPTH, Math.min(MAX_FILE_LIST_SUMMARY_DEPTH, depth));
}

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}`;
  const ext    = getFileExt(file.name || "");
  const bytes  = Number.isFinite(file.sizeBytes) ? file.sizeBytes : null;
  const isOffice = OFFICE_SNIPPET_EXTS.has(ext);

  // Reuse cache if we have it
  if (_fileSnippetCache.has(key)) {
    const cached = _fileSnippetCache.get(key);
    if (cached) {
      snippetEl.textContent = cached;
      snippetEl.style.display = "block";
    }
    return;
  }

  // ============================
  // OFFICE DOCS (DOCX/XLSX/PPTX)
  // ============================
  if (isOffice) {
    // Size guard (avoid parsing massive Office files)
    const MAX_OFFICE_BYTES = 20 * 1024 * 1024; // 20 MiB
    if (bytes != null && bytes > MAX_OFFICE_BYTES) {
      const msg = t("no_preview_available") || "No preview available";
      snippetEl.style.display = "block";
      snippetEl.textContent   = msg;
      _fileSnippetCache.set(key, msg);
      return;
    }

    snippetEl.style.display = "block";
    snippetEl.textContent   = t("loading") || "Loading...";

    try {
      const url = withBase(`/api/file/snippet.php?folder=${encodeURIComponent(folder)}&file=${encodeURIComponent(file.name)}&t=${Date.now()}`);
      const res = await fetch(url, { credentials: "include" });
      if (!res.ok) throw 0;

      const j = await res.json().catch(() => ({}));
      let text = (j && typeof j.snippet === "string") ? j.snippet : "";
      text = text || "";

      if (!text) {
        snippetEl.textContent = "";
        snippetEl.style.display = "none";
        _fileSnippetCache.set(key, "");
        return;
      }

      // Same visual rule as before: 6 lines, 600 chars, but let lines be a bit wider
      const MAX_LINES       = 6;
      const MAX_CHARS_TOTAL = 600;
      const MAX_LINE_CHARS  = 60;

      const rawLines = text.split(/\r?\n/);

      let visibleLines = rawLines.slice(0, MAX_LINES).map(line =>
        _trimLabel(line, MAX_LINE_CHARS)
      );

      let truncated =
        rawLines.length > MAX_LINES ||
        visibleLines.some((line, idx) => {
          const orig = rawLines[idx] || "";
          return orig.length > MAX_LINE_CHARS;
        });

      let snippet = visibleLines.join("\n");

      if (snippet.length > MAX_CHARS_TOTAL) {
        snippet = snippet.slice(0, MAX_CHARS_TOTAL);
        truncated = true;
      }

      snippet = snippet.trim();
      let finalSnippet = snippet || "(empty file)";
      if (truncated || j.truncated === true) {
        finalSnippet += "\n…";
      }

      _fileSnippetCache.set(key, finalSnippet);
      snippetEl.textContent = finalSnippet;

    } catch (e) {
      snippetEl.textContent = "";
      snippetEl.style.display = "none";
      _fileSnippetCache.set(key, "");
    }
    return;
  }

  // ============================
  // EXISTING TEXT FILE BEHAVIOR
  // ============================
  if (!canEditFile(file.name)) {
    // No text preview possible for this type – cache the fact and bail
    _fileSnippetCache.set(key, "");
    return;
  }

  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;
  }

  snippetEl.style.display = "block";
  snippetEl.textContent = t("loading") || "Loading...";

  try {
    const sourceId = String(file.sourceId || getActivePaneSourceId() || '').trim();
    const url = apiFileUrl(folder, file.name, true, sourceId);
    const res = await fetch(url, { credentials: "include" });
    if (!res.ok) throw 0;
    const text = await res.text();

    const MAX_LINES       = 6;
    const MAX_CHARS_TOTAL = 600;
    const MAX_LINE_CHARS  = 20;

    const allLines = text.split(/\r?\n/);

    let visibleLines = allLines.slice(0, MAX_LINES).map(line =>
      _trimLabel(line, MAX_LINE_CHARS)
    );

    let truncated =
      allLines.length > MAX_LINES ||
      visibleLines.some((line, idx) => {
        const orig = allLines[idx] || "";
        return orig.length > MAX_LINE_CHARS;
      });

    let snippet = visibleLines.join("\n");

    if (snippet.length > MAX_CHARS_TOTAL) {
      snippet = snippet.slice(0, MAX_CHARS_TOTAL);
      truncated = true;
    }

    snippet = snippet.trim();
    let finalSnippet = snippet || "(empty file)";
    if (truncated) {
      finalSnippet += "\n…";
    }

    _fileSnippetCache.set(key, finalSnippet);
    snippetEl.textContent = finalSnippet;

  } catch (e) {
    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 inlineRenameState = null;

function clearInlineRenameState({ restore = true } = {}) {
  const state = inlineRenameState;
  if (!state) return;

  inlineRenameState = null;

  try {
    if (state.input && state.input.parentNode) {
      state.input.parentNode.removeChild(state.input);
    }
  } catch (e) { /* ignore */ }

  if (restore) {
    if (state.type === 'file' && state.nameCell) {
      state.nameCell.innerHTML = state.originalHtml;
    } else if (state.type === 'folder' && state.nameSpan) {
      state.nameSpan.style.display = '';
      state.nameSpan.textContent = state.originalName;
    }
  }

  if (state.row) {
    state.row.classList.remove('inline-rename-active');
    if (state.prevDraggable == null) {
      state.row.removeAttribute('draggable');
    } else {
      state.row.setAttribute('draggable', state.prevDraggable);
    }
  }
}

function focusRenameInput(input, name, selectBase) {
  try {
    input.focus();
    if (selectBase) {
      const lastDot = String(name || '').lastIndexOf('.');
      if (lastDot > 0) {
        input.setSelectionRange(0, lastDot);
      } else {
        input.select();
      }
    } else {
      input.select();
    }
  } catch (e) { /* ignore */ }
}

function startInlineRenameForFileRow(row, file, folder) {
  if (!row || !file) return false;
  if (window.viewMode !== 'table') return false;

  const nameCell = row.querySelector('.file-name-cell');
  if (!nameCell) return false;

  clearInlineRenameState();
  hideHoverPreview();

  const oldName = file.name || '';
  const originalHtml = nameCell.innerHTML;

  nameCell.textContent = '';

  const input = document.createElement('input');
  input.type = 'text';
  input.value = oldName;
  input.className = 'inline-rename-input form-control';
  input.setAttribute('aria-label', t('rename') || 'Rename');
  input.style.width = '100%';
  input.style.maxWidth = '100%';
  input.style.fontSize = 'inherit';
  input.style.padding = '2px 6px';
  input.style.height = 'auto';
  input.style.boxSizing = 'border-box';

  nameCell.appendChild(input);

  const state = {
    type: 'file',
    row,
    input,
    nameCell,
    originalHtml,
    oldName,
    folder: folder || window.currentFolder || 'root',
    submitting: false,
    prevDraggable: row.getAttribute('draggable')
  };
  inlineRenameState = state;
  row.setAttribute('draggable', 'false');
  row.classList.add('inline-rename-active');

  const commit = async () => {
    if (!inlineRenameState || inlineRenameState !== state || state.submitting) return;
    const newName = String(input.value || '').trim();
    if (!newName || newName === oldName) {
      clearInlineRenameState();
      return;
    }

    state.submitting = true;
    input.disabled = true;

    try {
      const res = await fetch(withBase('/api/file/renameFile.php'), {
        method: 'POST',
        credentials: 'include',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRF-Token': window.csrfToken
        },
        body: JSON.stringify({
          folder: state.folder,
          oldName: oldName,
          newName: newName
        })
      });
      const data = await res.json().catch(() => ({}));
      if (data && data.success) {
        showToast(t('rename_file_success'), 'success');
        clearInlineRenameState({ restore: false });
        loadFileList(state.folder);
        return;
      }
      const errMsg = data.error || t('unknown_error');
      showToast(t('rename_file_error', { error: errMsg }), 'error');
    } catch (err) {
      console.error('Error renaming file:', err);
      showToast(t('rename_file_error_generic'), 'error');
    }

    if (inlineRenameState === state) {
      state.submitting = false;
      input.disabled = false;
      focusRenameInput(input, newName, true);
    }
  };

  const cancel = () => {
    clearInlineRenameState();
  };

  const stop = (e) => { e.stopPropagation(); };
  input.addEventListener('mousedown', stop);
  input.addEventListener('click', stop);
  input.addEventListener('dragstart', stop);
  input.addEventListener('keydown', (e) => {
    if (e.key === 'Enter') {
      e.preventDefault();
      e.stopPropagation();
      commit();
    } else if (e.key === 'Escape') {
      e.preventDefault();
      e.stopPropagation();
      cancel();
    }
  });
  input.addEventListener('blur', () => {
    if (inlineRenameState === state && !state.submitting) {
      commit();
    }
  });

  requestAnimationFrame(() => {
    focusRenameInput(input, oldName, true);
  });

  return true;
}

function startInlineRenameForFolderRow(row, folderPath) {
  if (!row || !folderPath) return false;
  if (window.viewMode !== 'table') return false;

  const wrap = row.querySelector('.folder-row-inner');
  const nameSpan = row.querySelector('.folder-row-name');
  if (!wrap || !nameSpan) return false;

  clearInlineRenameState();
  hideHoverPreview();

  const oldName = nameSpan.textContent || (String(folderPath).split('/').pop() || '');
  const originalName = oldName;

  const input = document.createElement('input');
  input.type = 'text';
  input.value = oldName;
  input.className = 'inline-rename-input form-control';
  input.setAttribute('aria-label', t('rename_folder') || 'Rename folder');
  input.style.width = '100%';
  input.style.maxWidth = '100%';
  input.style.fontSize = 'inherit';
  input.style.padding = '2px 6px';
  input.style.height = 'auto';
  input.style.boxSizing = 'border-box';

  const metaSpan = wrap.querySelector('.folder-row-meta');
  nameSpan.style.display = 'none';
  if (metaSpan) {
    wrap.insertBefore(input, metaSpan);
  } else {
    wrap.appendChild(input);
  }

  const state = {
    type: 'folder',
    row,
    input,
    nameSpan,
    originalName,
    folderPath,
    submitting: false,
    prevDraggable: row.getAttribute('draggable')
  };
  inlineRenameState = state;
  row.setAttribute('draggable', 'false');
  row.classList.add('inline-rename-active');

  const commit = async () => {
    if (!inlineRenameState || inlineRenameState !== state || state.submitting) return;
    const newName = String(input.value || '').trim();
    if (!newName || newName === oldName) {
      clearInlineRenameState();
      return;
    }

    state.submitting = true;
    input.disabled = true;

    try {
      const result = await renameFolderInline(folderPath, newName, { selectAfter: false });
      if (result && result.success) {
        clearInlineRenameState({ restore: false });
        const reloadFolder = window.currentFolder || getParentFolder(folderPath);
        loadFileList(reloadFolder);
        return;
      }
    } catch (err) {
      console.error('Error renaming folder:', err);
    }

    if (inlineRenameState === state) {
      state.submitting = false;
      input.disabled = false;
      focusRenameInput(input, newName, false);
    }
  };

  const cancel = () => {
    clearInlineRenameState();
  };

  const stop = (e) => { e.stopPropagation(); };
  input.addEventListener('mousedown', stop);
  input.addEventListener('click', stop);
  input.addEventListener('dragstart', stop);
  input.addEventListener('keydown', (e) => {
    if (e.key === 'Enter') {
      e.preventDefault();
      e.stopPropagation();
      commit();
    } else if (e.key === 'Escape') {
      e.preventDefault();
      e.stopPropagation();
      cancel();
    }
  });
  input.addEventListener('blur', () => {
    if (inlineRenameState === state && !state.submitting) {
      commit();
    }
  });

  requestAnimationFrame(() => {
    focusRenameInput(input, oldName, false);
  });

  return true;
}

export function startInlineRenameFromContext(file, row) {
  if (!file || !row) return false;
  if (row.classList.contains('folder-row')) return false;
  return startInlineRenameForFileRow(row, file, file.folder || window.currentFolder || 'root');
}

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 (e) {}

  hoverPreviewActiveRow = null;
  hoverPreviewContext = null;
  hoverPreviewHoveringCard = false;

  if (hoverPreviewEl) {
    hoverPreviewEl.style.display = 'none';
  }
}

function isHoverPreviewDisabled() {
  if (window.disableHoverPreview === true) return true;

  // Keep disabled on touch-only/coarse-only devices, but allow hybrid devices
  // (for example iPad + trackpad/mouse) that expose any hover-capable pointer.
  try {
    if (window.matchMedia) {
      const hasHoverPointer =
        window.matchMedia('(any-hover: hover)').matches ||
        window.matchMedia('(hover: hover)').matches ||
        window.matchMedia('(any-pointer: fine)').matches ||
        window.matchMedia('(pointer: fine)').matches;
      const touchOnlyLikely =
        (
          window.matchMedia('(pointer: coarse)').matches ||
          window.matchMedia('(any-pointer: coarse)').matches ||
          window.matchMedia('(hover: none)').matches ||
          window.matchMedia('(any-hover: none)').matches
        ) &&
        !hasHoverPointer;
      if (touchOnlyLikely) return true;
    }
  } catch (e) {}

  try {
    return localStorage.getItem("disableHoverPreview") === "true";
  } catch (e) {
    return false;
  }
}

function normalizeHoverPreviewDelayMs(value) {
  const parsed = parseInt(value, 10);
  const delay = Number.isFinite(parsed) ? parsed : DEFAULT_HOVER_PREVIEW_DELAY_MS;
  return Math.max(MIN_HOVER_PREVIEW_DELAY_MS, Math.min(MAX_HOVER_PREVIEW_DELAY_MS, delay));
}

function getHoverPreviewDelayMs() {
  const runtimeValue = parseInt(window.hoverPreviewDelayMs, 10);
  if (Number.isFinite(runtimeValue)) {
    return normalizeHoverPreviewDelayMs(runtimeValue);
  }

  try {
    const stored = localStorage.getItem(HOVER_PREVIEW_DELAY_STORAGE_KEY);
    if (stored !== null && stored !== '') {
      const normalized = normalizeHoverPreviewDelayMs(stored);
      window.hoverPreviewDelayMs = normalized;
      return normalized;
    }
  } catch (e) {}

  const cfg = window.__FR_SITE_CFG__ || window.siteConfig || {};
  const display = (cfg && typeof cfg.display === 'object') ? cfg.display : {};
  const normalized = normalizeHoverPreviewDelayMs(display.hoverPreviewDelayMs);
  window.hoverPreviewDelayMs = normalized;
  return normalized;
}

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  = "380px";   // was 420
    card.style.maxWidth  = "600px";   // was 640
    card.style.minHeight = "200px";   // was 220
    card.style.padding   = "8px 10px"; // slightly tighter padding
    card.style.overflow  = "hidden";
  }

  if (grid) {
    grid.style.display             = "grid";
    grid.style.gridTemplateColumns = "200px minmax(240px, 1fr)"; // both columns ~9% smaller
    grid.style.gap                 = "10px";
    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      = "120px"; // was 140
    thumb.style.marginBottom   = "4px";   // slightly tighter
  }
  

  if (snippet) {
    snippet.style.marginTop    = "4px";
    snippet.style.maxHeight    = "120px";
    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.76rem";
    propsEl.style.lineHeight = "1.3";
    propsEl.style.maxHeight  = "140px";
    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) {
        setCurrentFolderContext(dest, { resetPage: true });
        loadFileList(dest);
      }
    }
  });

  return el;
}

function hideHoverPreview() {
  cancelHoverPreview();
}

// allow ESC to quickly dismiss the hover preview
window.addEventListener("keydown", (e) => {
  if (e.key === "Escape" || e.key === "Esc") {
    hideHoverPreview();
  }
});

function parentFolderOf(path) {
  if (!path || path === 'root') return 'root';
  const parts = String(path).split('/').filter(Boolean);
  if (parts.length <= 1) return 'root';
  parts.pop();
  return parts.join('/');
}

function normalizeFolderToastKey(path) {
  const raw = String(path || '').trim();
  if (!raw || raw.toLowerCase() === 'root') return 'root';
  return raw;
}

function formatFolderPathForToast(path) {
  const key = normalizeFolderToastKey(path);
  return key === 'root' ? (t('root_folder') || 'Root') : key;
}

function movedFolderNameForToast(path) {
  const key = normalizeFolderToastKey(path);
  if (key === 'root') return t('root_folder') || 'Root';
  const parts = key.split('/').filter(Boolean);
  return parts.length ? parts[parts.length - 1] : key;
}

function buildMoveFolderSuccessToast(sourceFolder, destinationFolder) {
  const movedName = movedFolderNameForToast(sourceFolder);
  const sourceParent = normalizeFolderToastKey(parentFolderOf(sourceFolder));
  const destFolder = normalizeFolderToastKey(destinationFolder);
  const destLabel = formatFolderPathForToast(destFolder);
  if (sourceParent !== destFolder) {
    const sourceLabel = formatFolderPathForToast(sourceParent);
    return t('move_folder_success_named_from_to', { name: movedName, source: sourceLabel, folder: destLabel });
  }
  return t('move_folder_success_named_to', { name: movedName, folder: destLabel });
}

function invalidateFolderStats(folders, sourceId = '') {
  try {
    const arr = Array.isArray(folders) ? folders : [folders];
    window.dispatchEvent(new CustomEvent('folderStatsInvalidated', {
      detail: { folders: arr, sourceId }
    }));
  } catch (e) {
    // best effort only
  }
}

function applyFolderStripLayout(strip) {
  if (!strip) return;
  const hasItems = strip.querySelector('.folder-item') !== null;
  if (!hasItems) {
    strip.style.display = 'none';
    strip.classList.remove('folder-strip-mobile', 'folder-strip-desktop');
    return;
  }

  const isMobile = window.innerWidth <= 640; // tweak breakpoint if you want

  strip.classList.add('folder-strip-container');
  strip.classList.toggle('folder-strip-mobile', isMobile);
  strip.classList.toggle('folder-strip-desktop', !isMobile);

  strip.style.display = isMobile ? 'block' : 'flex';
  strip.style.overflowX = isMobile ? 'visible' : 'auto';
  strip.style.overflowY = isMobile ? 'auto' : 'hidden';
}

window.addEventListener('resize', () => {
  const strip = document.getElementById('folderStripContainer');
  if (strip) applyFolderStripLayout(strip);
});

// Clamp gallery columns when viewport shrinks
window.addEventListener('resize', () => {
  const maxCols = getGalleryMaxColumns();
  if ((window.galleryColumns || 0) > maxCols) {
    applyGalleryColumns(maxCols);
  }
});

window.addEventListener('keydown', async (e) => {
  if (window.__frIsModalOpen && window.__frIsModalOpen()) return;
  if (isTextEntryTarget(e.target)) return;

  const key = e.key;

  if (key === 'F2' && !(e.ctrlKey || e.altKey || e.metaKey)) {
    e.preventDefault();
    e.stopPropagation();

    if (inlineRenameState && inlineRenameState.input) {
      try {
        inlineRenameState.input.focus();
        inlineRenameState.input.select();
      } catch (e2) { /* ignore */ }
      return;
    }

    const folderCb = document.querySelector('#fileList .folder-checkbox:checked');
    if (folderCb) {
      const folder = folderCb.dataset.folder || '';
      const row = folderCb.closest('tr');
      if (folder && row) {
        startInlineRenameForFolderRow(row, folder);
        return;
      }
    }

    const fileCbs = document.querySelectorAll('#fileList .file-checkbox:checked');
    if (fileCbs.length) {
      handleRenameSelected(new Event('click'));
      return;
    }

    const treeFolder = getSelectedTreeFolderPath() || window.currentFolder || 'root';
    try {
      await startInlineRenameInTree(treeFolder);
    } catch (e2) { /* ignore */ }
    return;
  }

  if (e.ctrlKey || e.altKey || e.metaKey) return;

  if (!['F3', 'F4', 'F5', 'F6', 'F7', 'F8'].includes(key)) return;
  e.preventDefault();
  e.stopPropagation();

  if (key === 'F5') return handleCopySelected(new Event('click'));
  if (key === 'F6') return handleMoveSelected(new Event('click'));
  if (key === 'F8') return handleDeleteSelected(new Event('click'));

  if (key === 'F7') {
    const btn = document.getElementById('createFolderBtn');
    if (btn && !btn.disabled) btn.click();
    return;
  }

  const files = getActiveSelectedFileObjects();
  if (!files.length) {
    showToast(t('no_files_selected') || 'No files selected.', 'warning');
    return;
  }
  const file = files[0];
  const folder = file.folder || window.currentFolder || 'root';
  const sourceId = String(file.sourceId || '').trim() || getActivePaneSourceId();

  if (key === 'F3') {
    try {
      const url = apiFileUrl(folder, file.name, true, sourceId);
      const m = await import('./filePreview.js?v={{APP_QVER}}');
      m.previewFile(url, file.name);
    } catch (err) {
      console.error('Failed to open preview', err);
    }
    return;
  }

  if (key === 'F4') {
    if (!canEditFile(file.name) || !file.editable) {
      showToast(t('file_not_editable') || 'File is not editable.', 'warning');
      return;
    }
    try {
      const m = await import('./fileEditor.js?v={{APP_QVER}}');
      m.editFile(file.name, folder, sourceId, file.sizeBytes);
    } catch (err) {
      console.error('Failed to open editor', err);
    }
  }
});

window.addEventListener('keydown', (e) => {
  if (window.__frIsModalOpen && window.__frIsModalOpen()) return;
  if (isTextEntryTarget(e.target)) return;

  const isShortcut = (e.key === 'n' || e.key === 'N') && e.shiftKey && (e.ctrlKey || e.metaKey) && !e.altKey;
  if (!isShortcut) return;

  e.preventDefault();
  e.stopPropagation();

  const folderCb = document.querySelector('#fileList .folder-checkbox:checked');
  const targetFolder =
    (folderCb && folderCb.dataset.folder) ||
    getSelectedTreeFolderPath() ||
    window.currentFolder ||
    'root';

  if (targetFolder) {
    window.currentFolder = targetFolder;
  }

  const btn = document.getElementById('createFolderBtn');
  if (btn && !btn.disabled) {
    btn.click();
    return;
  }

  const modal = document.getElementById('createFolderModal');
  if (modal) {
    modal.style.display = 'block';
    const input = document.getElementById('newFolderName');
    if (input) input.focus();
  }
});

window.addEventListener('keydown', (e) => {
  if (window.__frIsModalOpen && window.__frIsModalOpen()) return;
  if (isTextEntryTarget(e.target)) return;
  if (e.ctrlKey || e.altKey || e.metaKey) return;

  const key = e.key;

  if (key === '/' && !e.shiftKey) {
    const input = document.getElementById('searchInput');
    if (input) {
      e.preventDefault();
      e.stopPropagation();
      input.focus();
      input.select();
    }
    return;
  }

  if (key === '?' || (key === '/' && e.shiftKey)) {
    e.preventDefault();
    e.stopPropagation();
    showToast(t('keyboard_shortcuts_help'), 6000, 'info');
    return;
  }

  if (key === 'Delete') {
    e.preventDefault();
    e.stopPropagation();
    const selectedFolder = getSelectedFolderPath();
    if (selectedFolder) {
      setCurrentFolderContext(selectedFolder);
      openDeleteFolderModal();
      return;
    }
    handleDeleteSelected(new Event('click'));
  }
});

// Listen once: update strip + tree + inline rows when folder color changes
window.addEventListener('folderColorChanged', (e) => {
  const { folder } = e.detail || {};
  if (!folder) return;

  // 1) Update the strip (if that folder is currently shown)
  repaintStripIcon(folder);

  // 2) Refresh the tree icon (existing function)
  try { refreshFolderIcon(folder); } catch (e) { }

  // 3) Repaint any inline folder rows in the file table
  try {
    const safeFolder = CSS.escape(folder);
    const activeSourceId = getActivePaneSourceId();
    document
      .querySelectorAll(`#fileList tr.folder-row[data-folder="${safeFolder}"]`)
      .forEach(row => {
        // reuse the same helper we used when injecting inline rows
        attachStripIconAsync(row, folder, 28, { sourceId: activeSourceId });
      });
  } catch (e) {
    // CSS.escape might not exist on very old browsers; fail silently
  }
});

// Hide "Edit" for files >10 MiB
const MAX_EDIT_BYTES = 10 * 1024 * 1024;

// Max number of files allowed for non-ZIP multi-download
export const MAX_NONZIP_MULTI_DOWNLOAD = 20;

// Global queue + panel ref for stepper-style downloads
window.__nonZipDownloadQueue = window.__nonZipDownloadQueue || [];
window.__nonZipDownloadPanel = window.__nonZipDownloadPanel || null;

// Latest-response-wins guard (prevents double render/flicker if loadFileList gets called twice)
let __fileListReqSeq = { primary: 0, secondary: 0 };
let __dualPaneRestoreRunning = false;

window.itemsPerPage = parseInt(
  localStorage.getItem('itemsPerPage') || window.itemsPerPage || '50',
  10
);
window.currentPage = window.currentPage || 1;
window.viewMode = localStorage.getItem("viewMode") || "table";
window.currentSubfolders = window.currentSubfolders || [];
window.currentSubfoldersSourceId = window.currentSubfoldersSourceId || '';
window.currentSubfoldersFolder = window.currentSubfoldersFolder || '';
window.currentFileListPaging = window.currentFileListPaging || null;

const FILE_LIST_CURSOR_PAGE_SIZE_MIN = 10;
const FILE_LIST_CURSOR_PAGE_SIZE_MAX = 500;

const PANE_FOLDER_STORAGE_KEYS = {
  primary: 'frPaneFolderPrimary',
  secondary: 'frPaneFolderSecondary'
};
const PANE_SOURCE_STORAGE_KEYS = {
  primary: 'frPaneSourcePrimary',
  secondary: 'frPaneSourceSecondary'
};

function normalizePaneKey(pane) {
  return pane === 'secondary' ? 'secondary' : 'primary';
}

function getGlobalActiveSourceId() {
  try {
    if (typeof window.__frGetActiveSourceId === 'function') {
      return window.__frGetActiveSourceId() || '';
    }
  } catch (e) { /* ignore */ }
  const sel = document.getElementById('sourceSelector');
  if (sel && sel.value) return sel.value;
  try {
    const stored = localStorage.getItem('fr_active_source');
    if (stored) return stored;
  } catch (e) { /* ignore */ }
  return '';
}

function getSourceNameById(sourceId) {
  const id = String(sourceId || '').trim();
  if (!id) return '';
  try {
    if (typeof window.__frGetSourceNameById === 'function') {
      return String(window.__frGetSourceNameById(id) || '');
    }
  } catch (e) { /* ignore */ }
  const sel = document.getElementById('sourceSelector');
  if (sel) {
    const opt = Array.from(sel.options).find(o => o.value === id);
    if (opt) return String(opt.dataset?.sourceName || '');
  }
  return '';
}

function getSourceTypeById(sourceId) {
  const id = String(sourceId || '').trim();
  if (!id) return '';
  try {
    if (typeof window.__frGetSourceMetaById === 'function') {
      const meta = window.__frGetSourceMetaById(id);
      if (meta && typeof meta === 'object' && meta.type) return String(meta.type || '');
    }
  } catch (e) { /* ignore */ }
  try {
    if (typeof window.__frGetSourceTypeById === 'function') {
      return String(window.__frGetSourceTypeById(id) || '');
    }
  } catch (e) { /* ignore */ }
  const sel = document.getElementById('sourceSelector');
  if (sel) {
    const opt = Array.from(sel.options).find(o => o.value === id);
    if (opt) return String(opt.dataset?.sourceType || '');
  }
  return '';
}

function isFtpSource(sourceId = '') {
  const type = String(getSourceTypeById(sourceId || getGlobalActiveSourceId()) || '').toLowerCase();
  return type === 'ftp';
}

function isLocalSourceLike(sourceId = '') {
  const id = String(sourceId || '').trim().toLowerCase();
  if (!id || id === 'local') return true;
  const type = String(getSourceTypeById(sourceId) || '').toLowerCase();
  return type === 'local';
}

function isSlowRemoteSource(sourceId = '') {
  const type = String(getSourceTypeById(sourceId || getGlobalActiveSourceId()) || '').toLowerCase();
  if (!type || type === 'local') return false;
  return ['ftp', 'sftp', 'webdav'].includes(type);
}

function getSourceMetaById(sourceId) {
  return {
    name: getSourceNameById(sourceId),
    type: getSourceTypeById(sourceId)
  };
}

function getRootLabel(sourceId = '') {
  const id = sourceId || getGlobalActiveSourceId();
  const name = getSourceNameById(id);
  return name ? `(${name})` : '(Root)';
}

function getRootCrumbLabel(sourceId = '') {
  const id = sourceId || getGlobalActiveSourceId();
  const name = getSourceNameById(id);
  return name || 'root';
}

function getLastOpenedFolderKey(sourceId = '') {
  const id = String(sourceId || '').trim();
  return id ? `lastOpenedFolder.${id}` : 'lastOpenedFolder';
}

function getLastOpenedFolder(sourceId = '') {
  const key = getLastOpenedFolderKey(sourceId);
  try {
    const val = String(localStorage.getItem(key) || '').trim();
    return val;
  } catch (e) {
    return '';
  }
}

function setLastOpenedFolder(folder, sourceId = '') {
  if (!folder) return;
  const key = getLastOpenedFolderKey(sourceId);
  try {
    localStorage.setItem(key, folder);
    if (key !== 'lastOpenedFolder') {
      localStorage.setItem('lastOpenedFolder', folder);
    }
  } catch (e) { /* ignore */ }
}

function formatSourceTypeLabel(type) {
  const t = String(type || '').trim();
  if (!t) return '';
  if (t.toLowerCase() === 'local') return 'Local';
  if (t.length <= 5) return t.toUpperCase();
  return t;
}

function formatSourceBadgeText(meta) {
  const name = String(meta?.name || '').trim();
  const typeLabel = formatSourceTypeLabel(meta?.type || '');
  if (typeLabel && name) {
    if (typeLabel.toLowerCase() === name.toLowerCase()) return name;
    return `${typeLabel}: ${name}`;
  }
  return name || typeLabel;
}

function setSourceBadge(titleEl, sourceId) {
  if (!titleEl) return;
  const meta = getSourceMetaById(sourceId);
  const label = formatSourceBadgeText(meta);
  const existing = titleEl.querySelector('.fr-source-badge');
  if (!label) {
    if (existing) existing.remove();
    return;
  }
  const badge = existing || document.createElement('span');
  if (!existing) {
    badge.className = 'fr-source-badge fr-source-badge--title';
    titleEl.appendChild(badge);
  }
  if (existing) {
    badge.classList.add('fr-source-badge', 'fr-source-badge--title');
  }
  badge.textContent = label;
  badge.title = `Source: ${label}`;
}

function refreshSourceBadges() {
  ensurePaneState();
  const active = normalizePaneKey(window.activePane);
  const other = active === 'secondary' ? 'primary' : 'secondary';
  setSourceBadge(document.getElementById('fileListTitle'), getPaneSourceId(active));
  setSourceBadge(document.getElementById('fileListTitleSecondary'), getPaneSourceId(other));
}

try { window.__frRefreshSourceBadges = refreshSourceBadges; } catch (e) { /* ignore */ }

function readStoredPaneFolder(pane, sourceId = '') {
  const key = PANE_FOLDER_STORAGE_KEYS[normalizePaneKey(pane)];
  if (!key) return "";
  const scopedKey = sourceId ? `${key}.${sourceId}` : key;
  try {
    if (sourceId) {
      return String(localStorage.getItem(scopedKey) || '').trim();
    }
    return String(localStorage.getItem(scopedKey) || '').trim();
  } catch (e) {
    return "";
  }
}

function readStoredPaneSource(pane) {
  const key = PANE_SOURCE_STORAGE_KEYS[normalizePaneKey(pane)];
  if (!key) return "";
  try {
    return String(localStorage.getItem(key) || '').trim();
  } catch (e) {
    return "";
  }
}

function persistPaneFolder(pane, folder, sourceId = '') {
  const key = PANE_FOLDER_STORAGE_KEYS[normalizePaneKey(pane)];
  if (!key || !folder) return;
  const scopedKey = sourceId ? `${key}.${sourceId}` : key;
  try {
    localStorage.setItem(scopedKey, folder);
  } catch (e) { /* ignore */ }
}

function persistPaneSource(pane, sourceId) {
  const key = PANE_SOURCE_STORAGE_KEYS[normalizePaneKey(pane)];
  if (!key) return;
  try {
    if (!sourceId) {
      localStorage.removeItem(key);
      return;
    }
    localStorage.setItem(key, sourceId);
  } catch (e) { /* ignore */ }
}

function getPaneSourceId(pane) {
  try {
    const state = getPaneState(pane);
    if (state && state.sourceId) return state.sourceId;
  } catch (e) { /* ignore */ }
  const stored = readStoredPaneSource(pane);
  if (stored) return stored;
  return getGlobalActiveSourceId();
}

function getActivePaneSourceId() {
  return getPaneSourceId(normalizePaneKey(window.activePane));
}

function getPaneKeyForElement(el) {
  const pane = el && el.closest ? el.closest('.file-list-pane') : null;
  if (pane && pane.classList.contains('secondary-pane')) return 'secondary';
  if (pane && pane.classList.contains('primary-pane')) return 'primary';
  return normalizePaneKey(window.activePane);
}

function getPaneSourceIdForElement(el) {
  return getPaneSourceId(getPaneKeyForElement(el));
}

(function seedLastOpenedFolderFromPaneStorage() {
  try {
    const dualEnabled = localStorage.getItem('dualPaneMode') === 'true';
    if (!dualEnabled) return;
    const active = normalizePaneKey(localStorage.getItem('activePane'));
    const sourceId = readStoredPaneSource(active) || getGlobalActiveSourceId();
    const folder = readStoredPaneFolder(active, sourceId);
    if (folder) setLastOpenedFolder(folder, sourceId);
  } catch (e) { /* ignore */ }
})();

function ensurePaneState() {
  if (window.__frPaneState) return;
  const storedPrimarySource = readStoredPaneSource('primary');
  const storedSecondarySource = readStoredPaneSource('secondary');
  const activeSource = getGlobalActiveSourceId();
  const primarySourceId = storedPrimarySource || activeSource;
  const secondarySourceId = storedSecondarySource || primarySourceId || activeSource;
  const storedPrimary = readStoredPaneFolder('primary', primarySourceId);
  const storedSecondary = readStoredPaneFolder('secondary', secondarySourceId);
  const initialFolder =
    storedPrimary ||
    getLastOpenedFolder(primarySourceId) ||
    (primarySourceId ? "root" : (window.currentFolder || "root"));
  window.__frPaneState = {
    primary: {
      currentFolder: initialFolder,
      sourceId: primarySourceId,
      currentSubfolders: Array.isArray(window.currentSubfolders) ? window.currentSubfolders : [],
      currentPage: window.currentPage || 1,
      currentSearchTerm: window.currentSearchTerm || "",
      currentFolderCaps: window.currentFolderCaps || null,
      selectedFolderCaps: window.selectedFolderCaps || null,
      fileData: Array.isArray(fileData) ? fileData : [],
      fileListPaging: window.currentFileListPaging || null,
      hasLoaded: false,
      needsReload: false
    },
    secondary: {
      currentFolder: storedSecondary || null,
      sourceId: secondarySourceId,
      currentSubfolders: [],
      currentPage: 1,
      currentSearchTerm: "",
      currentFolderCaps: null,
      selectedFolderCaps: null,
      fileData: [],
      fileListPaging: null,
      hasLoaded: false,
      needsReload: false
    }
  };
}

function getPaneState(pane) {
  ensurePaneState();
  return window.__frPaneState[normalizePaneKey(pane)];
}

function savePaneState(pane, patch) {
  const state = getPaneState(pane);
  Object.assign(state, patch || {});
  if (patch && Object.prototype.hasOwnProperty.call(patch, 'currentFolder')) {
    const sourceId = state.sourceId || getPaneSourceId(pane);
    persistPaneFolder(pane, state.currentFolder, sourceId);
  }
  if (patch && Object.prototype.hasOwnProperty.call(patch, 'sourceId')) {
    persistPaneSource(pane, state.sourceId || '');
  }
}

function syncPaneStateFromGlobals(pane) {
  savePaneState(pane, {
    currentFolder: window.currentFolder || "root",
    sourceId: getGlobalActiveSourceId(),
    currentSubfolders: Array.isArray(window.currentSubfolders) ? window.currentSubfolders : [],
    currentSubfoldersSourceId: String(window.currentSubfoldersSourceId || ''),
    currentSubfoldersFolder: String(window.currentSubfoldersFolder || ''),
    currentPage: window.currentPage || 1,
    currentSearchTerm: window.currentSearchTerm || "",
    currentFolderCaps: window.currentFolderCaps || null,
    selectedFolderCaps: window.selectedFolderCaps || null,
    fileData: Array.isArray(fileData) ? fileData : [],
    fileListPaging: window.currentFileListPaging || null
  });
}

function syncGlobalsFromPaneState(pane) {
  const state = getPaneState(pane);
  window.currentFolder = state.currentFolder || "root";
  window.currentSubfolders = Array.isArray(state.currentSubfolders) ? state.currentSubfolders : [];
  window.currentSubfoldersSourceId = String(state.currentSubfoldersSourceId || '');
  window.currentSubfoldersFolder = String(state.currentSubfoldersFolder || '');
  window.currentPage = state.currentPage || 1;
  window.currentSearchTerm = state.currentSearchTerm || "";
  window.currentFolderCaps = state.currentFolderCaps || null;
  window.selectedFolderCaps = state.selectedFolderCaps || null;
  fileData = Array.isArray(state.fileData) ? state.fileData : [];
  window.currentFileListPaging = state.fileListPaging || null;
}

function getFileListCursorPageSize() {
  const raw = parseInt(window.itemsPerPage || localStorage.getItem('itemsPerPage') || '50', 10);
  const value = Number.isFinite(raw) ? raw : 50;
  return Math.max(FILE_LIST_CURSOR_PAGE_SIZE_MIN, Math.min(FILE_LIST_CURSOR_PAGE_SIZE_MAX, value));
}

function getPaneFileListPaging(pane) {
  const state = getPaneState(pane);
  const paging = state && state.fileListPaging ? state.fileListPaging : null;
  return (paging && paging.enabled) ? paging : null;
}

function setPaneFileListPaging(pane, paging) {
  const normalized = normalizePaneKey(pane);
  const next = (paging && paging.enabled) ? paging : null;
  savePaneState(normalized, { fileListPaging: next });
  if (normalized === normalizePaneKey(window.activePane)) {
    window.currentFileListPaging = next;
  }
}

function shouldUseServerFilePagingForRequest(options = {}) {
  if (options && options.forceLegacy) {
    return false;
  }
  if (window.advancedSearchEnabled) {
    return false;
  }
  const term = String(window.currentSearchTerm || '').trim();
  return term === '';
}

function parseCursorOffset(cursor) {
  const text = String(cursor == null ? '' : cursor).trim();
  if (!/^\d+$/.test(text)) return 0;
  const value = parseInt(text, 10);
  return Number.isFinite(value) && value > 0 ? value : 0;
}

function getSubfoldersForPagingContext(pane, folder, sourceId = '') {
  const paneKey = normalizePaneKey(pane);
  const folderKey = String(folder || 'root');
  const sourceKey = String(sourceId || '').trim();

  const state = getPaneState(paneKey);
  const stateSource = String(state?.currentSubfoldersSourceId || '').trim();
  const stateFolder = String(state?.currentSubfoldersFolder || '');
  if (stateSource === sourceKey && stateFolder === folderKey && Array.isArray(state?.currentSubfolders)) {
    return state.currentSubfolders;
  }

  if (paneKey === normalizePaneKey(window.activePane)) {
    const winSource = String(window.currentSubfoldersSourceId || '').trim();
    const winFolder = String(window.currentSubfoldersFolder || '');
    if (winSource === sourceKey && winFolder === folderKey && Array.isArray(window.currentSubfolders)) {
      return window.currentSubfolders;
    }
  }

  return [];
}

function getCombinedPageLayout({ totalFolders, totalFiles, itemsPerPage, targetPage }) {
  const folderCount = Number.isFinite(Number(totalFolders)) ? Math.max(0, Number(totalFolders)) : 0;
  const fileCount = Number.isFinite(Number(totalFiles)) ? Math.max(0, Number(totalFiles)) : 0;
  const itemsRaw = Number(itemsPerPage);
  const pageSize = Number.isFinite(itemsRaw) && itemsRaw > 0 ? Math.max(1, Math.floor(itemsRaw)) : 50;
  const totalRows = folderCount + fileCount;
  const totalPages = Math.max(1, Math.ceil(totalRows / pageSize) || 1);
  const targetRaw = Number(targetPage);
  const page = Number.isFinite(targetRaw)
    ? Math.max(1, Math.min(totalPages, Math.floor(targetRaw)))
    : 1;
  const startRow = (page - 1) * pageSize;
  const endRow = Math.min(totalRows, startRow + pageSize);

  const folderStart = Math.min(folderCount, startRow);
  const folderEnd = Math.min(folderCount, endRow);
  const folderCountOnPage = Math.max(0, folderEnd - folderStart);

  const fileStart = Math.max(0, startRow - folderCount);
  const fileEnd = Math.max(0, endRow - folderCount);
  const fileCountOnPage = Math.max(0, fileEnd - fileStart);

  return {
    page,
    totalPages,
    pageSize,
    totalRows,
    folderStart,
    folderCount: folderCountOnPage,
    fileStart,
    fileCount: fileCountOnPage
  };
}

function resolveServerPagingCursorForLoad({ pane, folder, sourceId, requestedCursor }) {
  if (requestedCursor != null) {
    return String(requestedCursor);
  }
  if (window.viewMode !== 'table' || window.showInlineFolders === false) {
    return '';
  }

  const paneKey = normalizePaneKey(pane);
  const state = getPaneState(paneKey);
  const pageRaw = Number(state?.currentPage || window.currentPage || 1);
  const targetPage = Number.isFinite(pageRaw) ? Math.max(1, Math.floor(pageRaw)) : 1;
  if (targetPage <= 1) {
    return '';
  }

  const subfolders = getSubfoldersForPagingContext(paneKey, folder, sourceId);
  const totalFolders = Array.isArray(subfolders) ? subfolders.length : 0;
  if (totalFolders <= 0) {
    return '';
  }
  const pageSize = getFileListCursorPageSize();
  const fileOffset = Math.max(0, ((targetPage - 1) * pageSize) - totalFolders);
  return String(fileOffset);
}

let __frTreeSourceId = null;

function syncActiveSourceToPane(paneKey, sourceId) {
  const targetId = sourceId || getPaneSourceId(paneKey);
  const currentId = getGlobalActiveSourceId();
  if (!targetId || targetId === currentId) return Promise.resolve(false);

  if (typeof window.__frApplyActiveSource === 'function') {
    return window.__frApplyActiveSource(targetId, { skipEvent: true, origin: 'pane' });
  }

  const sel = document.getElementById('sourceSelector');
  if (sel) sel.value = targetId;
  try { localStorage.setItem('fr_active_source', targetId); } catch (e) { /* ignore */ }
  return Promise.resolve(true);
}

function refreshTreeForSource(folder, sourceId) {
  const nextId = sourceId || getGlobalActiveSourceId();
  if (!nextId) return;
  if (__frTreeSourceId === nextId) return;
  __frTreeSourceId = nextId;
  try { resetFolderTreeCaches(); } catch (e) { /* ignore */ }
  try { loadFolderTree(folder); } catch (e) { /* ignore */ }
}

function setPaneHasContent(pane, hasContent) {
  const selector = normalizePaneKey(pane) === 'secondary'
    ? '.secondary-pane'
    : '.primary-pane';
  const el = document.querySelector(selector);
  if (!el) return;
  el.classList.toggle('fr-pane-has-content', !!hasContent);
}

function getPaneLayoutMode(targetEl) {
  const pane = targetEl && targetEl.closest ? targetEl.closest('.file-list-pane') : null;
  const width = pane ? pane.getBoundingClientRect().width : window.innerWidth;
  if (width <= 720) return 'narrow';
  if (width <= 980) return 'medium';
  return 'wide';
}

function updatePaneWidthClasses() {
  const panes = document.querySelectorAll('.file-list-pane');
  panes.forEach(pane => {
    const width = pane.getBoundingClientRect().width;
    if (!width) return;
    const mode = width <= 720 ? 'narrow' : (width <= 980 ? 'medium' : 'wide');
    pane.classList.toggle('fr-pane-narrow', mode === 'narrow');
    pane.classList.toggle('fr-pane-medium', mode === 'medium');
  });
  updatePaneStickyClasses();
}

function updatePaneStickyClasses() {
  const panes = document.querySelectorAll('.file-list-pane');
  panes.forEach(pane => pane.classList.remove('fr-pane-sticky'));

  if (!window.dualPaneEnabled || panes.length < 2) return;

  const paneList = Array.from(panes);
  const heights = paneList.map(pane => pane.getBoundingClientRect().height || 0);
  const viewport = window.innerHeight || 0;
  const buffer = 24;

  if (!viewport || heights.every(h => h <= 0)) return;

  const canStick = heights.map(h => h + buffer <= viewport);

  if (canStick[0] && canStick[1]) {
    paneList.forEach(pane => pane.classList.add('fr-pane-sticky'));
    return;
  }

  if (canStick[0] && heights[0] < heights[1]) {
    paneList[0].classList.add('fr-pane-sticky');
  }
  if (canStick[1] && heights[1] < heights[0]) {
    paneList[1].classList.add('fr-pane-sticky');
  }
}

function ensureDualPaneTargetHint() {
  const actions = document.getElementById('fileListActions');
  if (!actions) return null;
  let hint = document.getElementById('dualPaneTargetHint');
  if (!hint) {
    hint = document.createElement('div');
    hint.id = 'dualPaneTargetHint';
    hint.className = 'file-list-dual-hint fr-dual-target-hint';
    actions.appendChild(hint);
  }
  return hint;
}

function updateDualPaneTargetHint() {
  const hint = ensureDualPaneTargetHint();
  if (!hint) return;
  if (!window.dualPaneEnabled || !window.__frPaneState) {
    hint.style.display = 'none';
    return;
  }
  const active = normalizePaneKey(window.activePane);
  const other = active === 'secondary' ? 'primary' : 'secondary';
  const otherFolder = window.__frPaneState?.[other]?.currentFolder || '';
  if (!otherFolder) {
    hint.style.display = 'none';
    return;
  }
  const otherSourceId = window.__frPaneState?.[other]?.sourceId || '';
  const sideLabel = other === 'secondary' ? 'Right' : 'Left';
  const folderLabel = otherFolder === 'root' ? getRootLabel(otherSourceId) : otherFolder;
  hint.textContent = `Target: ${sideLabel} -> ${folderLabel}`;
  hint.title = `Copy/move target is the ${sideLabel.toLowerCase()} pane folder.`;
  hint.style.display = 'inline-flex';
}

function syncFolderStripWithPaneState(paneKey) {
  const strip = document.getElementById('folderStripContainer');
  if (!strip) return;
  if (!window.showFoldersInList) {
    strip.innerHTML = '';
    strip.style.display = 'none';
    return;
  }

  const state = getPaneState(paneKey);
  const sourceId = String((state && state.sourceId) || '');
  const folder = String((state && state.currentFolder) || 'root');
  const subfolders = Array.isArray(state?.currentSubfolders) ? state.currentSubfolders : [];
  const subfoldersSourceId = String(state?.currentSubfoldersSourceId || '');
  const subfoldersFolder = String(state?.currentSubfoldersFolder || '');
  const matchesContext = subfoldersSourceId === sourceId && subfoldersFolder === folder;

  if (!matchesContext || !subfolders.length) {
    strip.innerHTML = '';
    strip.style.display = 'none';
    return;
  }

  renderFolderStripPaged(strip, subfolders, sourceId);
}

function updateInactivePanePlaceholder(inactiveKey) {
  if (!window.dualPaneEnabled) return;
  const container = document.getElementById('fileListContainerSecondary');
  if (!container) return;

  const ensurePlaceholderElements = () => {
    let hintEl = container.querySelector('.file-list-dual-hint');
    if (!hintEl) {
      let actionsWrap = container.querySelector('.file-list-actions');
      const hasToolbar = actionsWrap && actionsWrap.querySelector('#fileListActions');
      if (!actionsWrap || hasToolbar) {
        actionsWrap = document.createElement('div');
        actionsWrap.className = 'file-list-actions';
        const title = container.querySelector('#fileListTitleSecondary');
        if (title && title.parentNode === container) {
          title.insertAdjacentElement('afterend', actionsWrap);
        } else {
          container.insertBefore(actionsWrap, container.firstChild);
        }
      }

      hintEl = document.createElement('div');
      hintEl.className = 'file-list-dual-hint';
      hintEl.setAttribute('data-i18n-key', 'dual_pane_secondary_hint');
      hintEl.textContent = t('dual_pane_secondary_hint') || 'Select a folder to open here.';
      actionsWrap.appendChild(hintEl);
    }

    let emptyEl = container.querySelector('#fileListSecondary .empty-state');
    const listEl = container.querySelector('#fileListSecondary');
    if (!emptyEl && listEl && listEl.childElementCount === 0) {
      listEl.classList.add('file-list-secondary-empty');
      emptyEl = document.createElement('div');
      emptyEl.className = 'empty-state';
      emptyEl.setAttribute('data-i18n-key', 'dual_pane_secondary_empty');
      emptyEl.textContent = t('dual_pane_secondary_empty') || 'Select a folder to open in the right pane.';
      listEl.appendChild(emptyEl);
    }

    return { hintEl, emptyEl };
  };

  const state = getPaneState(inactiveKey);
  const paneSourceId = (state && typeof state.sourceId === 'string')
    ? state.sourceId.trim()
    : '';
  const labelSourceId = paneSourceId || readStoredPaneSource(inactiveKey);
  let folder = (state && typeof state.currentFolder === 'string')
    ? state.currentFolder.trim()
    : '';
  if (!folder) {
    folder = readStoredPaneFolder(inactiveKey, labelSourceId);
  }
  const label = folder === 'root' ? getRootCrumbLabel(labelSourceId) : folder;

  const ensured = ensurePlaceholderElements();

  const titleEl = document.getElementById('fileListTitleSecondary');
  if (titleEl && !titleEl.dataset.baseText) {
    titleEl.dataset.baseText = titleEl.textContent || '';
  }

  const hintEl = ensured.hintEl || container.querySelector('.file-list-dual-hint');
  if (hintEl && !hintEl.dataset.baseText) {
    hintEl.dataset.baseText = hintEl.textContent || (t('dual_pane_secondary_hint') || 'Select a folder to open here.');
  }

  const emptyEl = ensured.emptyEl || container.querySelector('#fileListSecondary .empty-state');
  if (emptyEl && !emptyEl.dataset.baseText) {
    emptyEl.dataset.baseText = emptyEl.textContent || (t('dual_pane_secondary_empty') || 'Select a folder to open in the right pane.');
  }

  if (!folder) {
    if (titleEl && titleEl.dataset.baseText) titleEl.textContent = titleEl.dataset.baseText;
    if (hintEl && hintEl.dataset.baseText) hintEl.textContent = hintEl.dataset.baseText;
    if (emptyEl && emptyEl.dataset.baseText) emptyEl.textContent = emptyEl.dataset.baseText;
    refreshSourceBadges();
    return;
  }

  if (titleEl) {
    const prefix = t('files_in') || 'Files in';
    titleEl.textContent = `${prefix} (${formatBreadcrumbText(folder, labelSourceId)})`;
  }
  if (hintEl) {
    hintEl.textContent = `${hintEl.dataset.baseText} (${label})`;
  }
  if (emptyEl) {
    emptyEl.textContent = `${emptyEl.dataset.baseText} (${label})`;
  }
  refreshSourceBadges();
}

function resetInactivePaneBaseText() {
  const titleEl = document.getElementById('fileListTitleSecondary');
  if (titleEl && titleEl.dataset) {
    delete titleEl.dataset.baseText;
  }

  const container = document.getElementById('fileListContainerSecondary');
  if (!container) return;

  const hintEl = container.querySelector('.file-list-dual-hint');
  if (hintEl && hintEl.dataset) {
    delete hintEl.dataset.baseText;
  }

  const emptyEl = container.querySelector('#fileListSecondary .empty-state');
  if (emptyEl && emptyEl.dataset) {
    delete emptyEl.dataset.baseText;
  }
}

function formatBreadcrumbText(folder, sourceId = '') {
  const path = (typeof folder === 'string' && folder.length) ? folder : 'root';
  const rootLabel = getRootCrumbLabel(sourceId);
  if (path === 'root') return rootLabel;
  const parts = path.split('/').filter(Boolean);
  return [rootLabel, ...parts].join(' › ');
}

function resolvePaneFolderForTitle(paneKey, { fallbackToCurrent = false } = {}) {
  if (!paneKey) return '';
  let folder = '';
  let sourceId = '';
  try {
    const state = getPaneState(paneKey);
    folder = (state && typeof state.currentFolder === 'string')
      ? state.currentFolder.trim()
      : '';
    sourceId = (state && typeof state.sourceId === 'string')
      ? state.sourceId.trim()
      : '';
  } catch (e) {
    folder = '';
  }

  if (!folder) {
    const id = sourceId || getPaneSourceId(paneKey);
    folder = readStoredPaneFolder(paneKey, id);
  }

  if (!folder && fallbackToCurrent) {
    const id = sourceId || getPaneSourceId(paneKey);
    if (id) {
      folder = getLastOpenedFolder(id) || 'root';
    } else {
      folder = window.currentFolder || getLastOpenedFolder(id) || 'root';
    }
  }

  return folder || '';
}

function refreshPaneTitlesFromState() {
  ensurePaneState();
  let storedActive = null;
  try { storedActive = localStorage.getItem('activePane'); } catch (e) { storedActive = null; }

  const active = normalizePaneKey(window.activePane || storedActive || 'primary');
  const activeFolder = resolvePaneFolderForTitle(active, { fallbackToCurrent: true });
  if (activeFolder) {
    updateBreadcrumbTitle(activeFolder);
  }

  if (window.dualPaneEnabled) {
    const other = active === 'secondary' ? 'primary' : 'secondary';
    updateInactivePanePlaceholder(other);
    updateDualPaneTargetHint();
  }
  refreshSourceBadges();
}

// Default folder display settings from localStorage
try {
  const storedStrip  = localStorage.getItem('showFoldersInList');
  const storedInline = localStorage.getItem('showInlineFolders');

  window.showFoldersInList = storedStrip === null ? true : storedStrip === 'true';
  window.showInlineFolders = storedInline === null ? true : storedInline === 'true';
} catch (e) {
  // if localStorage blows up, fall back to both enabled
  window.showFoldersInList = true;
  window.showInlineFolders = true;
}

// Dual pane mode (UI-only scaffolding)
function setActivePane(pane, opts) {
  ensurePaneState();
  const next = normalizePaneKey(pane);
  const current = normalizePaneKey(window.activePane);

  if (current !== next) {
    syncPaneStateFromGlobals(current);

    const activeContainer = document.getElementById('fileListContainer');
    const inactiveContainer = document.getElementById('fileListContainerSecondary');
    if (activeContainer && inactiveContainer) {
      activeContainer.id = 'fileListContainerSecondary';
      inactiveContainer.id = 'fileListContainer';
    }

    const activeTitle = document.getElementById('fileListTitle');
    const inactiveTitle = document.getElementById('fileListTitleSecondary');
    if (activeTitle && inactiveTitle) {
      activeTitle.id = 'fileListTitleSecondary';
      inactiveTitle.id = 'fileListTitle';
    }

    const activeList = document.getElementById('fileList');
    const inactiveList = document.getElementById('fileListSecondary');
    if (activeList && inactiveList) {
      activeList.id = 'fileListSecondary';
      inactiveList.id = 'fileList';
    }
  }

  window.activePane = next;
  const state = getPaneState(next);
  const paneSourceId = getPaneSourceId(next);
  const globalSourceId = getGlobalActiveSourceId();
  if (__frTreeSourceId === null) {
    __frTreeSourceId = globalSourceId || paneSourceId || null;
  }
  if (!state.currentFolder) {
    const fallbackFolder = readStoredPaneFolder(next, paneSourceId) || getLastOpenedFolder(paneSourceId) || 'root';
    savePaneState(next, { currentFolder: fallbackFolder });
    state.currentFolder = fallbackFolder;
  }
  if (paneSourceId && paneSourceId !== globalSourceId) {
    syncActiveSourceToPane(next, paneSourceId).then(ok => {
      if (ok) {
        const targetFolder = state.currentFolder || window.currentFolder || 'root';
        const treeWillReload = __frTreeSourceId !== paneSourceId;
        if (treeWillReload && state.hasLoaded && !state.needsReload) {
          window.__frSkipListReload = {
            folder: targetFolder,
            sourceId: paneSourceId
          };
        }
        refreshTreeForSource(targetFolder, paneSourceId);
      }
    }).catch(() => {});
  }

  const activeContainer = document.getElementById('fileListContainer');
  const inactiveContainer = document.getElementById('fileListContainerSecondary');
  if (activeContainer) activeContainer.classList.add('fr-pane-active');
  if (inactiveContainer) inactiveContainer.classList.remove('fr-pane-active');

  const actions = document.getElementById('fileListActions');
  if (actions && activeContainer) {
    const title = activeContainer.querySelector('#fileListTitle');
    if (title && title.parentNode === activeContainer) {
      title.insertAdjacentElement('afterend', actions);
    } else {
      activeContainer.insertBefore(actions, activeContainer.firstChild);
    }

    const strip = document.getElementById('folderStripContainer');
    if (strip) {
      actions.parentNode.insertBefore(strip, actions);
    }
  }

  syncGlobalsFromPaneState(next);
  syncFolderStripWithPaneState(next);
  if (state.currentFolder) {
    updateBreadcrumbTitle(state.currentFolder);
    try { syncFolderTreeSelection(state.currentFolder); } catch (e) { /* ignore */ }
  }
  updateEncryptedFolderBanner(state.currentFolder || window.currentFolder || 'root');
  refreshFileSummaryForPane(next);
  if (state.needsReload && state.currentFolder) {
    savePaneState(next, { needsReload: false });
    state.needsReload = false;
    loadFileList(state.currentFolder);
  } else if (
    window.dualPaneEnabled &&
    !state.hasLoaded &&
    state.currentFolder &&
    !(opts && opts.skipAutoLoad)
  ) {
    // Lazy-load the pane's last folder only when it becomes active.
    loadFileList(state.currentFolder);
  }

  setPaneHasContent(next, !!state.hasLoaded);
  applyEncryptedFolderUiRestrictions();
  updateFileActionButtons();
  updatePaneWidthClasses();
  updateDualPaneTargetHint();
  updateInactivePanePlaceholder(next === 'secondary' ? 'primary' : 'secondary');
  refreshSourceBadges();
  scheduleFolderIconRepair({ force: true, skipStats: true });
  scheduleInactivePaneFolderIconRepair();

  if (!opts || !opts.skipStore) {
    try { localStorage.setItem('activePane', next); } catch (e) { /* ignore */ }
  }
}

function applyDualPaneMode(force) {
  let enabled = false;
  try {
    enabled = (typeof force === 'boolean')
      ? force
      : localStorage.getItem('dualPaneMode') === 'true';
  } catch (e) { /* ignore */ }
  window.dualPaneEnabled = enabled;

  const shell = document.getElementById('fileListShell');
  if (shell) shell.classList.toggle('dual-pane', enabled);

  const storedActive = (function () {
    try { return localStorage.getItem('activePane'); } catch (e) { return null; }
  })();
  if (enabled) {
    setActivePane(storedActive === 'secondary' ? 'secondary' : 'primary', { skipStore: true, skipAutoLoad: true });
  } else {
    setActivePane('primary', { skipStore: true, skipAutoLoad: true });
  }

  const primaryPane = document.querySelector('.file-list-pane.primary-pane');
  const secondaryPane = document.querySelector('.file-list-pane.secondary-pane');
  if (enabled) {
    if (primaryPane) primaryPane.style.display = '';
    if (secondaryPane) secondaryPane.style.display = '';
  } else {
    if (primaryPane) primaryPane.style.display = '';
    if (secondaryPane) secondaryPane.style.display = 'none';
    if (primaryPane) primaryPane.classList.remove('fr-pane-active');
    if (secondaryPane) secondaryPane.classList.remove('fr-pane-active');
  }
  updatePaneWidthClasses();
  updateDualPaneTargetHint();
}

window.applyDualPaneMode = applyDualPaneMode;

try {
  window.addEventListener('filerise:i18n-applied', () => {
    resetInactivePaneBaseText();
    refreshPaneTitlesFromState();
  });
} catch (e) { /* ignore */ }

try {
  window.addEventListener('filerise:source-change', (e) => {
    const id = e?.detail?.id ? String(e.detail.id) : '';
    if (!id) return;
    ensurePaneState();
    const activeKey = normalizePaneKey(window.activePane);
    const state = getPaneState(activeKey);
    if (!state) return;
    const prev = state.sourceId || '';
    if (prev === id) {
      const folder = state.currentFolder || window.currentFolder || 'root';
      refreshTreeForSource(folder, id);
      return;
    }
    savePaneState(activeKey, {
      sourceId: id,
      currentSubfolders: [],
      currentSubfoldersSourceId: '',
      currentSubfoldersFolder: '',
      currentFolderCaps: null,
      selectedFolderCaps: null
    });
    if (activeKey === normalizePaneKey(window.activePane)) {
      const strip = document.getElementById('folderStripContainer');
      if (strip) {
        strip.innerHTML = '';
        strip.style.display = 'none';
      }
    }
    const storedFolder = readStoredPaneFolder(activeKey, id) || getLastOpenedFolder(id) || 'root';
    savePaneState(activeKey, { currentFolder: storedFolder });
    const folder = storedFolder || state.currentFolder || 'root';
    const treeContainer = document.getElementById('folderTreeContainer');
    const treeWillReload = !!treeContainer && __frTreeSourceId !== id;
    refreshTreeForSource(folder, id);
    // Avoid double-loading: refreshTreeForSource triggers loadFolderTree → selectFolder → loadFileList.
    if (!treeContainer || !treeWillReload) {
      loadFileList(folder, { pane: activeKey }).catch(() => {});
    }
    refreshSourceBadges();
  });
} catch (e) { /* ignore */ }

// Activate pane on click
document.addEventListener('click', (e) => {
  if (!window.dualPaneEnabled) return;
  const pane = e.target && e.target.closest
    ? e.target.closest('.file-list-pane')
    : null;
  if (!pane) return;
  setActivePane(pane.classList.contains('secondary-pane') ? 'secondary' : 'primary');
}, true);

function isPaneDropTargetAllowed(target) {
  if (!target || !target.closest) return true;
  return !target.closest('.folder-item, .folder-row, .folder-option, .folder-strip-container, .folder-tree');
}

function paneDragOverHandler(e) {
  if (!window.dualPaneEnabled) return;
  if (!isPaneDropTargetAllowed(e.target)) return;
  e.preventDefault();
  e.currentTarget.classList.add('fr-pane-drop-target');
}

function paneDragLeaveHandler(e) {
  e.currentTarget.classList.remove('fr-pane-drop-target');
}

async function maybeRestoreInactivePane(activePaneKey) {
  if (!window.dualPaneEnabled || __dualPaneRestoreRunning) return;
  ensurePaneState();

  const active = normalizePaneKey(activePaneKey || window.activePane);
  const other = active === 'secondary' ? 'primary' : 'secondary';
  const state = window.__frPaneState?.[other];
  const otherSourceId = (state && state.sourceId) || readStoredPaneSource(other) || '';
  const otherFolder = (state && state.currentFolder) || readStoredPaneFolder(other, otherSourceId);

  if (!otherFolder || (state && state.hasLoaded)) return;

  __dualPaneRestoreRunning = true;
  try {
    if (normalizePaneKey(window.activePane) !== other) {
      setActivePane(other, { skipStore: true });
    }
    await loadFileList(otherFolder, { pane: other, skipFallback: true });
  } catch (e) { /* ignore */ }
  finally {
    if (normalizePaneKey(window.activePane) !== active) {
      setActivePane(active, { skipStore: true });
    }
    __dualPaneRestoreRunning = false;
  }
}

function pruneMovedFolderFromInactivePane(sourceFolder, sourceParent, sourceId = '') {
  if (!window.dualPaneEnabled || !sourceFolder || !window.__frPaneState) return;

  const inactiveKey = normalizePaneKey(window.activePane) === 'secondary'
    ? 'primary'
    : 'secondary';
  const state = window.__frPaneState[inactiveKey];
  if (!state || state.currentFolder !== sourceParent) return;
  if (sourceId && state.sourceId && state.sourceId !== sourceId) return;

  if (Array.isArray(state.currentSubfolders)) {
    state.currentSubfolders = state.currentSubfolders.filter(sf => sf.full !== sourceFolder);
  }

  const list = document.getElementById('fileListSecondary');
  if (!list) return;

  try {
    const row = list.querySelector(`tr.folder-row[data-folder="${CSS.escape(sourceFolder)}"]`);
    if (row) row.remove();
  } catch (e) { /* ignore */ }
}

function getPaneListForKey(paneKey) {
  const pane = document.querySelector(
    paneKey === 'secondary' ? '.file-list-pane.secondary-pane' : '.file-list-pane.primary-pane'
  );
  if (!pane) return null;
  return pane.querySelector('#fileList, #fileListSecondary') || pane;
}

function pruneMovedFilesFromInactivePane(sourceFolder, movedFiles, sourceId = '') {
  if (!window.dualPaneEnabled || !sourceFolder || !window.__frPaneState) return;

  const names = Array.isArray(movedFiles)
    ? movedFiles.map(f => {
        if (!f) return '';
        if (typeof f === 'string') return f;
        if (typeof f === 'object') {
          return String(f.name || f.file || f.filename || f.fileName || '');
        }
        return String(f);
      }).map(n => String(n || '').trim()).filter(Boolean)
    : [];
  if (!names.length) return;

  const nameSet = new Set();
  names.forEach(name => {
    const raw = String(name || '').trim();
    if (!raw) return;
    nameSet.add(raw);
    const decoded = decodeHtmlEntities(raw);
    if (decoded && decoded !== raw) {
      nameSet.add(decoded);
    }
  });
  ['primary', 'secondary'].forEach(paneKey => {
    const state = window.__frPaneState[paneKey];
    if (!state || state.currentFolder !== sourceFolder) return;
    if (sourceId && state.sourceId && state.sourceId !== sourceId) return;

    if (Array.isArray(state.fileData)) {
      state.fileData = state.fileData.filter(f => f && !nameSet.has(f.name));
    }

    const list = getPaneListForKey(paneKey);
    if (!list) return;

    list.querySelectorAll('tr[data-file-name], .gallery-card[data-file-name]').forEach(row => {
      const raw = row.getAttribute('data-file-name') || '';
      const decoded = decodeHtmlEntities(raw);
      if (nameSet.has(raw) || nameSet.has(decoded)) {
        row.remove();
      }
    });
  });
}

async function paneDropHandler(e) {
  if (!window.dualPaneEnabled) return;
  if (!isPaneDropTargetAllowed(e.target)) return;
  e.preventDefault();
  e.currentTarget.classList.remove('fr-pane-drop-target');

  const paneKey = e.currentTarget.classList.contains('secondary-pane') ? 'secondary' : 'primary';
  const paneState = getPaneState(paneKey);
  const targetFolder = paneState?.currentFolder || '';
  if (!targetFolder) return;

  let dragData = null;
  try {
    const raw = e.dataTransfer.getData('application/json') || '{}';
    dragData = JSON.parse(raw);
  } catch (err) {
    dragData = null;
  }

  if (!dragData) return;

  const sourceId = String(dragData.sourceId || dragData.sourceSourceId || '').trim();
  const destSourceId = getPaneSourceId(paneKey) || getGlobalActiveSourceId();
  const crossSource = sourceId && destSourceId && sourceId !== destSourceId;

  const scheduleRepair = () => {
    try {
      const kick = () => { try { repairBlankFolderIcons({ force: true }); } catch (e) {} };
      if (typeof queueMicrotask === 'function') queueMicrotask(kick);
      setTimeout(kick, 80);
      setTimeout(kick, 250);
    } catch (e) { /* ignore */ }
  };

  if (dragData.dragType === 'folder' && dragData.folder) {
    const sourceFolder = String(dragData.folder || '').trim();
    if (!sourceFolder || sourceFolder === 'root') return;
    if (!crossSource && (targetFolder === sourceFolder || targetFolder.startsWith(sourceFolder + '/'))) {
      showToast(t('invalid_destination'), 'warning');
      return;
    }

    if (normalizePaneKey(window.activePane) !== paneKey) {
      setActivePane(paneKey);
    }

    const sourceParent = dragData.sourceFolder || parentFolderOf(sourceFolder);
    try {
      await runTransferJob({
        kind: 'folder_move',
        payload: {
          source: sourceFolder,
          destination: targetFolder,
          sourceId,
          destSourceId
        },
        progress: {
          action: 'Moving',
          itemCount: 1,
          itemLabel: 'folder',
          bytesKnown: false,
          indeterminate: true,
          source: sourceFolder,
          destination: targetFolder
        }
      });
      showToast(buildMoveFolderSuccessToast(sourceFolder, targetFolder), 'success');
      if (crossSource) {
        if (sourceParent) invalidateFolderStats([sourceParent], sourceId);
        invalidateFolderStats([targetFolder], destSourceId);
        loadFileList(targetFolder, { pane: paneKey }).finally(scheduleRepair);
      } else {
        const statSourceId = sourceId || destSourceId;
        invalidateFolderStats([sourceParent, targetFolder], statSourceId);
        await syncTreeAfterFolderMove(sourceFolder, targetFolder);
        scheduleRepair();
      }
      markPaneNeedsReloadForFolder(sourceParent, sourceId);
      pruneMovedFolderFromInactivePane(sourceFolder, sourceParent, sourceId);
    } catch (err) {
      if (err && err.cancelled) {
        showToast(t('transfer_cancelled') || 'Transfer cancelled.', 'info');
        return;
      }
      const errMsg = err && err.message ? err.message : t('move_folder_error_default');
      console.error('Error moving folder:', err);
      showToast(t('move_folder_error_detail', { error: errMsg }), 'error');
    }
    return;
  }

  const files = Array.isArray(dragData.files) ? dragData.files : [];
  if (!files.length) return;

  const sourceFolder = dragData.sourceFolder || window.currentFolder || 'root';
  if (!crossSource && targetFolder === sourceFolder) {
    showToast(t('already_in_target_folder'), 'info');
    return;
  }

  if (normalizePaneKey(window.activePane) !== paneKey) {
    setActivePane(paneKey);
  }

  const totals = {
    totalBytes: Number.isFinite(dragData.totalBytes) ? dragData.totalBytes : 0,
    bytesKnown: dragData.bytesKnown === true,
    itemCount: files.length
  };
  try {
    await runTransferJob({
      kind: 'file_move',
      payload: {
        source: sourceFolder,
        files,
        destination: targetFolder,
        sourceId,
        destSourceId,
        totalBytes: totals.totalBytes
      },
      progress: {
        action: 'Moving',
        itemCount: totals.itemCount,
        itemLabel: totals.itemCount === 1 ? 'file' : 'files',
        totalBytes: totals.totalBytes,
        bytesKnown: totals.bytesKnown,
        source: sourceFolder,
        destination: targetFolder
      }
    });
    const destLabel = targetFolder || t('root_folder');
    const movedNames = files.map(f => {
      if (typeof f === 'string') return String(f || '').trim();
      if (f && typeof f === 'object') {
        return String(f.name || f.file || f.filename || f.fileName || '').trim();
      }
      return '';
    }).filter(Boolean);
    if (movedNames.length === 1) {
      showToast(t('move_file_success_to', { name: movedNames[0], folder: destLabel }), 'success');
    } else {
      showToast(t('move_files_success_to', { count: movedNames.length || files.length, folder: destLabel }), 'success');
    }
    if (crossSource) {
      invalidateFolderStats([sourceFolder], sourceId);
      invalidateFolderStats([targetFolder], destSourceId);
    } else {
      const statSourceId = sourceId || destSourceId;
      invalidateFolderStats([sourceFolder, targetFolder], statSourceId);
    }
    loadFileList(targetFolder, { pane: paneKey }).finally(scheduleRepair);
    const activeSourceId = getGlobalActiveSourceId();
    if (!destSourceId || destSourceId === activeSourceId) refreshFolderIcon(targetFolder);
    if (!sourceId || sourceId === activeSourceId) refreshFolderIcon(sourceFolder);
    markPaneNeedsReloadForFolder(sourceFolder, sourceId);
    pruneMovedFilesFromInactivePane(sourceFolder, files, sourceId);
  } catch (err) {
    if (err && err.cancelled) {
      showToast(t('transfer_cancelled') || 'Transfer cancelled.', 'info');
      return;
    }
    const errMsg = err && err.message ? err.message : t('unknown_error');
    console.error('Error moving files:', err);
    showToast(t('move_files_error', { error: errMsg }), 'error');
  }
}

function bindPaneDropTargets() {
  const panes = document.querySelectorAll('.file-list-pane');
  panes.forEach(pane => {
    if (pane.dataset.dropBound === '1') return;
    pane.dataset.dropBound = '1';
    pane.addEventListener('dragover', paneDragOverHandler);
    pane.addEventListener('dragleave', paneDragLeaveHandler);
    pane.addEventListener('drop', paneDropHandler);
  });
}

// Apply initial dual-pane state once DOM is ready
try { applyDualPaneMode(); } catch (e) { /* ignore */ }
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', () => applyDualPaneMode(), { once: true });
}
if (typeof window.__frDualPanePending !== 'undefined') {
  try { applyDualPaneMode(!!window.__frDualPanePending); } catch (e) { /* ignore */ }
  delete window.__frDualPanePending;
}

if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', () => bindPaneDropTargets(), { once: true });
} else {
  bindPaneDropTargets();
}

try {
  window.addEventListener('resize', debounce(updatePaneWidthClasses, 150));
} catch (e) { /* ignore */ }

// Global flag for advanced search mode.
window.advancedSearchEnabled = false;

// ---- Inline folder selection helpers --------------------------
function getFolderCheckboxes() {
  return Array.from(document.querySelectorAll('#fileList .folder-checkbox'));
}

function getFileCheckboxes() {
  return Array.from(document.querySelectorAll('#fileList .file-checkbox'));
}

function getActiveSelectedFileNames() {
  return Array.from(document.querySelectorAll('#fileList .file-checkbox:checked'))
    .map(cb => cb.value);
}

function getActiveSelectedFileObjects() {
  const selected = getActiveSelectedFileNames();
  if (!selected.length) return [];
  const selectedSet = new Set(selected);
  return (Array.isArray(fileData) ? fileData : [])
    .filter(f => selectedSet.has(escapeHTML(f.name)));
}

function clearFolderSelections() {
  getFolderCheckboxes().forEach(cb => {
    if (cb.checked) {
      cb.checked = false;
      updateRowHighlight(cb);
    }
  });
  window.selectedInlineFolder = null;
  window.selectedFolderCaps = null;
  savePaneState(normalizePaneKey(window.activePane), { selectedFolderCaps: null });
}

function clearFileSelections() {
  getFileCheckboxes().forEach(cb => {
    if (cb.checked) {
      cb.checked = false;
      updateRowHighlight(cb);
    }
  });
}

function getSelectedFolderPath() {
  const cb = document.querySelector('#fileList .folder-checkbox:checked');
  return cb ? cb.dataset.folder || null : null;
}

function getSelectedTreeFolderPath() {
  const tree = document.getElementById('folderTreeContainer');
  const opt = tree ? tree.querySelector('.folder-option.selected') : document.querySelector('.folder-option.selected');
  if (!opt) return null;
  const folder = opt.getAttribute('data-folder') || '';
  return folder || null;
}

async function refreshSelectedFolderCaps(folder) {
  if (!folder) {
    window.selectedFolderCaps = null;
    savePaneState(normalizePaneKey(window.activePane), { selectedFolderCaps: null });
    updateFileActionButtons();
    return;
  }
  const pane = normalizePaneKey(window.activePane);
  const sourceId = getPaneSourceId(pane);
  window.selectedFolderCaps = null;
  try {
    const caps = await fetchFolderCaps(folder, sourceId);
    window.selectedFolderCaps = caps || null;
    savePaneState(pane, { selectedFolderCaps: window.selectedFolderCaps });
  } catch (e) {
    window.selectedFolderCaps = null;
    savePaneState(pane, { selectedFolderCaps: null });
  }
  updateFileActionButtons();
}

function isTextEntryTarget(target) {
  const tag = target?.tagName ? target.tagName.toLowerCase() : '';
  return tag === 'input' || tag === 'textarea' || tag === 'select' || target?.isContentEditable;
}

function markPaneNeedsReloadForFolder(folder, sourceId = '') {
  if (!window.dualPaneEnabled || !window.__frPaneState || !folder) return;
  const active = normalizePaneKey(window.activePane);
  ['primary', 'secondary'].forEach(pane => {
    if (pane === active) return;
    const state = window.__frPaneState[pane];
    if (state && state.currentFolder === folder) {
      if (sourceId && state.sourceId && state.sourceId !== sourceId) return;
      state.needsReload = true;
    }
  });
}

function handleFolderCheckboxChange(cb) {
  if (!cb) return;

  // Enforce single-select folders and mutually exclusive with files
  if (cb.checked) {
    clearFileSelections();
    getFolderCheckboxes().forEach(other => {
      if (other !== cb && other.checked) {
        other.checked = false;
        updateRowHighlight(other);
      }
    });
    window.selectedInlineFolder = cb.dataset.folder || null;
  } else {
    window.selectedInlineFolder = null;
  }

  updateRowHighlight(cb);
  updateFileActionButtons();
  refreshSelectedFolderCaps(window.selectedInlineFolder);
}

function resetPagingForFolderNavigation(pane) {
  const paneKey = normalizePaneKey(pane || window.activePane);
  savePaneState(paneKey, { currentPage: 1 });
  setPaneFileListPaging(paneKey, null);
  if (paneKey === normalizePaneKey(window.activePane)) {
    window.currentPage = 1;
  }
}

function setCurrentFolderContext(folder, opts = {}) {
  if (!folder) return;
  const pane = normalizePaneKey(opts.pane || window.activePane);
  if (opts.resetPage === true) {
    resetPagingForFolderNavigation(pane);
  }
  window.currentFolder = folder;
  setLastOpenedFolder(folder, getPaneSourceId(pane));
  updateBreadcrumbTitle(folder);
  savePaneState(pane, { currentFolder: folder });
}

function clampRowHeight(v) {
  const n = parseInt(v, 10);
  if (!Number.isFinite(n)) return 44;
  return Math.max(20, Math.min(60, n));
}

function applyRowHeight(v) {
  const h = clampRowHeight(v);
  document.documentElement.style.setProperty('--file-row-height', `${h}px`);
  localStorage.setItem('rowHeight', h);

  // compact marker for tiny rows
  if (h <= 32) {
    document.documentElement.setAttribute('data-row-compact', '1');
  } else {
    document.documentElement.removeAttribute('data-row-compact');
  }

  try { syncFolderIconSizeToRowHeight(); } catch (e) {}
  return h;
}

function getGalleryMaxColumns() {
  const w = window.innerWidth;
  if (w < 600) return 1;
  if (w < 1024) return 3;
  if (w < 1440) return 4;
  return 6;
}

function clampGalleryColumns(v) {
  const max = getGalleryMaxColumns();
  const n = parseInt(v, 10);
  const base = Number.isFinite(n) ? n : 3;
  return Math.max(1, Math.min(max, base));
}

// Apply stored row height immediately so the table uses it on first render.
applyRowHeight(localStorage.getItem('rowHeight') || '44');

// Base gallery column value (clamped per screen width)
window.galleryColumns = clampGalleryColumns(localStorage.getItem('galleryColumns') || window.galleryColumns || 3);

// --- Folder stats cache (for isEmpty.php) ---
const _folderStatsCache = window.__FR_FOLDER_STATS_CACHE || new Map();
const _folderStatsInflight = window.__FR_FOLDER_STATS_INFLIGHT || new Map();
const _folderSummaryCache = window.__FR_FOLDER_SUMMARY_CACHE || new Map();
window.__FR_FOLDER_STATS_CACHE = _folderStatsCache;
window.__FR_FOLDER_STATS_INFLIGHT = _folderStatsInflight;
window.__FR_FOLDER_SUMMARY_CACHE = _folderSummaryCache;
const MAX_CONCURRENT_FOLDER_STATS_REQS = 6;
let _activeFolderStatsReqs = 0;
const _folderStatsQueue = [];

function folderCacheKey(folder, sourceId) {
  const sid = sourceId ? String(sourceId) : '';
  return sid ? `${sid}::${folder}` : folder;
}

function getFolderStatsTimeout(sourceId = '') {
  const type = String(getSourceTypeById(sourceId || getGlobalActiveSourceId()) || '').toLowerCase();
  if (type && type !== 'local') return 6000;
  return 2500;
}

function _runFolderStats(url, timeoutMs) {
  return new Promise(resolve => {
    const start = () => {
      _activeFolderStatsReqs++;
      _fetchJSONWithTimeout(url, timeoutMs || 2500)
        .then(resolve)
        .finally(() => {
          _activeFolderStatsReqs--;
          const next = _folderStatsQueue.shift();
          if (next) next();
        });
    };

    if (_activeFolderStatsReqs < MAX_CONCURRENT_FOLDER_STATS_REQS) start();
    else _folderStatsQueue.push(start);
  });
}

function fetchFolderStats(folder, sourceId = '') {
  if (!folder) return Promise.resolve(null);
  if (isSlowRemoteSource(sourceId)) return Promise.resolve(null);

  const key = folderCacheKey(folder, sourceId);
  if (_folderStatsCache.has(key)) {
    return Promise.resolve(_folderStatsCache.get(key));
  }
  if (_folderStatsInflight.has(key)) {
    return _folderStatsInflight.get(key);
  }

  const sourceParam = sourceId ? `&sourceId=${encodeURIComponent(sourceId)}` : '';
  const url = withBase(`/api/folder/isEmpty.php?folder=${encodeURIComponent(folder)}${sourceParam}&t=${Date.now()}`);
  const timeoutMs = getFolderStatsTimeout(sourceId);
  const p = _runFolderStats(url, timeoutMs).then(data => {
    const payload = (data && !data.__fr_err) ? data : { folders: 0, files: 0 };
    const stillInflight = _folderStatsInflight.get(key) === p;
    if (stillInflight) {
      _folderStatsInflight.delete(key);
    }
    // If this was a transient network/server failure, don't poison the cache.
    if (data && data.__fr_err) {
      return payload;
    }
    if (stillInflight) {
      _folderStatsCache.set(key, payload);
    }
    return payload;
  }).catch(() => {
    if (_folderStatsInflight.get(key) === p) {
      _folderStatsInflight.delete(key);
    }
    return { folders: 0, files: 0 };
  });

  _folderStatsInflight.set(key, p);
  return p;
}

function fetchFolderSummaryStats(folder, depthOverride, sourceId = '') {
  if (!folder) return Promise.resolve(null);
  if (isSlowRemoteSource(sourceId)) {
    return Promise.resolve(null);
  }

  const depthSetting = Number.isFinite(depthOverride) ? depthOverride : getFileListSummaryDepth();
  const depth = Math.max(MIN_FILE_LIST_SUMMARY_DEPTH, Math.min(MAX_FILE_LIST_SUMMARY_DEPTH, depthSetting));
  const depthKey = depth > 0 ? String(depth) : 'u';
  const baseKey = folderCacheKey(folder, sourceId);
  const cacheKey = `${baseKey}::${depthKey}`;

  if (_folderSummaryCache.has(cacheKey)) {
    return _folderSummaryCache.get(cacheKey);
  }

  const depthParam = depth > 0 ? `&depth=${depth}` : '';
  const sourceParam = sourceId ? `&sourceId=${encodeURIComponent(sourceId)}` : '';
  const url = withBase(`/api/folder/isEmpty.php?folder=${encodeURIComponent(folder)}&deep=1${depthParam}${sourceParam}&t=${Date.now()}`);
  const p = _fetchJSONWithTimeout(url, 6000).then(data => {
    if (data && data.__fr_err) {
      _folderSummaryCache.delete(cacheKey);
      return null;
    }
    return data || null;
  });

  _folderSummaryCache.set(cacheKey, p);
  return p;
}

function clearFolderSummaryCache(folder, sourceId = '') {
  if (!folder) return;
  const prefix = `${folderCacheKey(folder, sourceId)}::`;
  for (const key of _folderSummaryCache.keys()) {
    if (key === folder || key.startsWith(prefix)) {
      _folderSummaryCache.delete(key);
    }
  }
}

// --- Folder "peek" cache (first few child folders/files) ---
const FOLDER_PEEK_MAX_ITEMS = 6;
const _folderPeekCache = new Map();

// Listen for invalidation events from drag/drop, etc.
window.addEventListener('folderStatsInvalidated', (e) => {
  const detail = e.detail || {};
  let folders = detail.folders || detail.folder || null;
  if (!folders) return;
  if (!Array.isArray(folders)) folders = [folders];
  const sourceId = detail.sourceId ? String(detail.sourceId) : '';
  const activeSourceId = getGlobalActiveSourceId();

  folders.forEach(f => {
    if (!f) return;
    const key = folderCacheKey(f, sourceId);
    _folderStatsCache.delete(key);
    _folderStatsInflight.delete(key);
    _folderPeekCache.delete(key);
    clearFolderSummaryCache(f, sourceId);

    if (!sourceId || sourceId === activeSourceId) {
      try { refreshFolderIcon(f); } catch (e2) { /* ignore */ }
    }

    try {
      const safe = CSS.escape(f);
      const stripItem = document.querySelector(`#folderStripContainer .folder-item[data-folder="${safe}"]`);
      if (stripItem) attachStripIconAsync(stripItem, f, 48, { sourceId });
      document
        .querySelectorAll(
          `#fileList tr.folder-row[data-folder="${safe}"], ` +
          `#fileListSecondary tr.folder-row[data-folder="${safe}"]`
        )
        .forEach(row => attachStripIconAsync(row, f, 28, { sourceId }));
    } catch (e3) { /* ignore */ }
  });
});

/**
 * 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, sourceId = '') {
  if (!folder) return null;
  if (isSlowRemoteSource(sourceId)) return null;

  const key = folderCacheKey(folder, sourceId);
  if (_folderPeekCache.has(key)) {
    return _folderPeekCache.get(key);
  }

  const sourceParam = sourceId ? `&sourceId=${encodeURIComponent(sourceId)}` : '';
  const p = (async () => {
    try {
      // 1) Files in this folder
      let files = [];
      try {
        const res = await fetch(
          withBase(`/api/file/getFileList.php?folder=${encodeURIComponent(folder)}&recursive=0${sourceParam}&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 (e) {
        // ignore file errors; we can still show folders
      }

      // 2) Direct subfolders
      let subfolderNames = [];
      try {
        const res2 = await fetch(
          withBase(`/api/folder/getFolderList.php?folder=${encodeURIComponent(folder)}&counts=0${sourceParam}`),
          { 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 (e) {
        // 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 (e) {
      return null;
    }
  })();

  _folderPeekCache.set(key, p);
  return p;
}

/* ===========================================================
   SECURITY: build file URLs only via the API (no /uploads)
   =========================================================== */
   function apiFileUrl(folder, name, inline = false, sourceId = "") {
    const fParam = folder && folder !== "root" ? folder : "root";
    const q = new URLSearchParams({
      folder: fParam,
      file: name,
      inline: inline ? "1" : "0"
    });
    let meta = null;
    let sid = String(sourceId || "").trim();
  
    try {
      if (Array.isArray(fileData)) {
        meta = fileData.find(
          f => f.name === name && (f.folder || "root") === fParam
        );
        if (!sid && meta && meta.sourceId) {
          sid = String(meta.sourceId || "").trim();
        }
      }
    } catch (e) { /* best-effort only */ }

    if (!sid) {
      try {
        sid = String(getActivePaneSourceId() || "").trim();
      } catch (e) { /* ignore */ }
    }
    if (sid) q.set("sourceId", sid);

    // Try to find this file in fileData to get a stable cache key
    if (meta) {
      const v = meta.cacheKey || meta.modified || meta.uploaded || meta.sizeBytes;
      if (v != null && v !== "") {
        q.set("t", String(v));  // stable per-file token
      }
    }
  
    return withBase(`/api/file/download.php?${q.toString()}`);
	  }

  function apiMediaThumbUrl(folder, name, sourceId = "") {
    const fParam = folder && folder !== "root" ? folder : "root";
    const q = new URLSearchParams({
      folder: fParam,
      file: name
    });
    if (sourceId) q.set("sourceId", sourceId);

    // Try to find this file in fileData to get a stable cache key
    try {
      if (Array.isArray(fileData)) {
        const meta = fileData.find(
          f => f.name === name && (f.folder || "root") === fParam
        );
        if (meta) {
          const v = meta.cacheKey || meta.modified || meta.uploaded || meta.sizeBytes;
          if (v != null && v !== "") {
            q.set("t", String(v));  // stable per-file token
          }
        }
      }
    } catch (e) { /* best-effort only */ }

    return withBase(`/api/file/thumbnail.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 getSelectAll = () => 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]'
  );
  const selectAll = getSelectAll();
  if (!selectAll) return;
  if (selectAll.__wiredSelectAll) return;
  selectAll.__wiredSelectAll = true;

  const getRowCbs = () =>
    Array.from(fileListContent.querySelectorAll('tbody .file-checkbox'))
      .filter(cb => !cb.disabled && !cb.closest('tr.folder-row'));

  const clearFolderSelections = () => {
    fileListContent.querySelectorAll('tbody .folder-checkbox:checked').forEach(cb => {
      cb.checked = false;
      updateRowHighlight(cb);
    });
  };

  const applySelectAll = () => {
    // Clear any folder selection when toggling all files
    clearFolderSelections();
    const checked = selectAll.checked;
    getRowCbs().forEach(cb => {
      cb.checked = checked;
      updateRowHighlight(cb);
    });
    updateFileActionButtons();
    // No indeterminate state when explicitly toggled
    selectAll.indeterminate = false;
    selectAll.__lastSelectAllChecked = selectAll.checked;
  };

  const handleSelectAllChange = () => {
    applySelectAll();
  };

  // Toggle all rows when the header checkbox changes
  selectAll.addEventListener('change', handleSelectAllChange);

  // Some UI layers can swallow left-click toggles. Track state and force toggle if needed.
  selectAll.addEventListener('mousedown', (e) => {
    if (e.button !== 0) return;
    selectAll.__lastSelectAllChecked = selectAll.checked;
  });

  selectAll.addEventListener('click', (e) => {
    if (e.button !== 0) return;
    if (selectAll.disabled) return;
    const last = selectAll.__lastSelectAllChecked;
    if (last === selectAll.checked) {
      selectAll.checked = !selectAll.checked;
      handleSelectAllChange();
    }
    selectAll.__lastSelectAllChecked = selectAll.checked;
  });

  const headerCell = selectAll.closest('th');
  if (headerCell && !headerCell.__wiredSelectAllCell) {
    headerCell.__wiredSelectAllCell = true;
    headerCell.addEventListener('click', (e) => {
      if (e.target === selectAll) return;
      if (selectAll.disabled) return;
      selectAll.checked = !selectAll.checked;
      handleSelectAllChange();
    });
  }

  // Keep header checkbox state in sync with row selections
  const syncHeader = () => {
    const master = getSelectAll();
    if (!master) return;
    const cbs = getRowCbs();
    const total = cbs.length;
    const checked = cbs.filter(cb => cb.checked).length;
    if (!total) {
      master.checked = false;
      master.indeterminate = false;
      return;
    }
    master.checked = checked === total;
    master.indeterminate = checked > 0 && checked < total;
  };

  // Listen for any row checkbox changes to refresh header state
  if (!fileListContent.__wiredSelectAllContainer) {
    fileListContent.__wiredSelectAllContainer = true;
    fileListContent.addEventListener('change', (e) => {
      if (e.target && e.target.classList.contains('file-checkbox')) {
        syncHeader();
      }
    });
  }

  // Initial sync on mount
  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 snippet style defaults (for file previews)
snippetEl.style.whiteSpace   = "pre-wrap";
snippetEl.style.overflowX    = "auto";
snippetEl.style.textOverflow = "clip";
snippetEl.style.wordBreak    = "break-word";

// Reset per-row sizing...
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 (start props array so we can append later)
    const props = [];

    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;">folder</span>
        <strong>${t("folder") || "Folder"}</strong>
      </div>
    `);

    props.push(`
      <div class="hover-prop-line">
        <strong>${t("path") || "Path"}:</strong> ${escapeHTML(folderPath || "root")}
      </div>
    `);

    propsEl.innerHTML = props.join("");

    // --- Owner + "Your access" (from capabilities) --------------------
    fetchFolderCaps(folderPath, getActivePaneSourceId()).then(caps => {
      if (!caps || !document.body.contains(el)) return;
      if (!hoverPreviewContext || hoverPreviewContext.folder !== folderPath) return;

      // Update the type label to include "(encrypted)" when applicable
      const isEnc = !!(caps && caps.encryption && caps.encryption.encrypted);
      props[0] = `
        <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"}${isEnc ? " (encrypted)" : ""}</strong>
        </div>
      `;

      const owner = caps.owner || caps.user || "";
      if (owner) {
        props.push(`
          <div class="hover-prop-line">
            <strong>${t("owner") || "Owner"}:</strong> ${escapeHTML(owner)}
          </div>
        `);
      }

      // Summarize what the current user can do in this folder
      const perms = [];
      if (caps.canUpload || caps.canCreate)    perms.push(t("perm_upload") || "Upload");
      if (caps.canMoveFolder)                  perms.push(t("perm_move")   || "Move");
      if (caps.canRename)                      perms.push(t("perm_rename") || "Rename");
      if (caps.canShareFolder)                 perms.push(t("perm_share")  || "Share");
      if (caps.canDeleteFolder || caps.canDelete)
                                              perms.push(t("perm_delete") || "Delete");

      if (perms.length) {
        const label = t("your_access") || "Your access";
        props.push(`
          <div class="hover-prop-line">
            <strong>${escapeHTML(label)}:</strong> ${escapeHTML(perms.join(", "))}
          </div>
        `);
      }

      propsEl.innerHTML = props.join("");
    }).catch(() => {});
    // ------------------------------------------------------------------

    // --- Meta: counts + size + created/modified -----------------------
    fetchFolderStats(folderPath, getActivePaneSourceId()).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(", ");

      // Optional: created / modified range under the path/owner/access
      const created  = typeof stats.earliest_uploaded === "string" ? stats.earliest_uploaded : "";
      const modified = typeof stats.latest_mtime       === "string" ? stats.latest_mtime      : "";

      if (modified) {
        props.push(`
          <div class="hover-prop-line">
            <strong>${t("modified") || "Modified"}:</strong> ${escapeHTML(modified)}
          </div>
        `);
      }

      if (created) {
        props.push(`
          <div class="hover-prop-line">
            <strong>${t("created") || "Created"}:</strong> ${escapeHTML(created)}
          </div>
        `);
      }

      propsEl.innerHTML = props.join("");
    }).catch(() => {});
    // ------------------------------------------------------------------

    // Left side: peek inside folder (first few children)
fetchFolderPeek(folderPath, getActivePaneSourceId()).then(result => {
  if (!document.body.contains(el)) return;
  if (!hoverPreviewContext || hoverPreviewContext.folder !== folderPath) return;

  // Folder mode: force single-line-ish behavior and avoid wrapping
  snippetEl.style.whiteSpace   = "pre";
  snippetEl.style.wordBreak    = "normal";
  snippetEl.style.overflowX    = "hidden";
  snippetEl.style.textOverflow = "ellipsis";

  if (!result) {
    const msg =
      t("no_files_or_folders") ||
      t("no_files_found") ||
      "No files or folders";

    snippetEl.textContent = msg;
    snippetEl.style.display = "block";
    return;
  }

  const { items, truncated } = result;

  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 MAX_LABEL_CHARS = 42; // tweak to taste

  const lines = items.map(it => {
    const prefix = it.type === "folder" ? "📁 " : "📄 ";
    const trimmed = _trimLabel(it.name, MAX_LABEL_CHARS);
    return prefix + trimmed;
  });

  // If we had to cut the list to FOLDER_PEEK_MAX_ITEMS, show a clean final "…"
  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");

    // If this row isn't a real file row (e.g. "No files found"), don't show hover preview.
    if (!name) {
      hoverPreviewContext = null;
      hideHoverPreview();
      return;
    }

    const paneEl = row.closest('.file-list-pane');
    const paneKey = paneEl && paneEl.classList.contains('secondary-pane') ? 'secondary' : 'primary';
    const paneFiles = (window.__frPaneState &&
      window.__frPaneState[paneKey] &&
      Array.isArray(window.__frPaneState[paneKey].fileData))
      ? window.__frPaneState[paneKey].fileData
      : (Array.isArray(fileData) ? fileData : []);
    const rawName = decodeHtmlEntities(name);
    const file = paneFiles.find(f => f.name === rawName || escapeHTML(f.name) === name);

    // If we can't resolve a real file from fileData, also skip the preview
    if (!file) {
      hoverPreviewContext = null;
      hideHoverPreview();
      return;
    }

    hoverPreviewContext = {
      type: "file",
      file
    };

    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 sourceId = String(
      file.sourceId ||
      (window.__frPaneState && window.__frPaneState[paneKey] && window.__frPaneState[paneKey].sourceId) ||
      getActivePaneSourceId() ||
      ""
    ).trim();
    const url    = apiFileUrl(folder, file.name, true, sourceId);
    const canTextPreview = canEditFile(file.name);
    const isOfficeSnippet = OFFICE_SNIPPET_EXTS.has(ext);

    // Right column: file details (always render, even if preview errors)
    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 (Number.isFinite(file.sizeBytes) && file.sizeBytes >= 0) {
      const prettySize = formatSize(file.sizeBytes);
      props.push(`
        <div class="hover-prop-line hover-prop-size">
          <strong>${t("size") || "Size"}:</strong>
          <span class="hover-prop-value"
                style="margin-left:4px; font-variant-numeric:tabular-nums;">
            ${escapeHTML(prettySize)}
          </span>
        </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>`);
    }

    // --- NEW: Tags / Metadata line ------------------------------------
    (function addMetaLine() {
      // Tags from backend: file.tags = [{ name, color }, ...]
      const tagNames = Array.isArray(file.tags)
        ? file.tags
            .map(t => t && t.name ? String(t.name).trim() : "")
            .filter(Boolean)
        : [];

      // Optional extra metadata if you ever add it to fileData
      const mime =
        file.mime ||
        file.mimetype ||
        file.contentType ||
        "";

      const extraPieces = [];
      if (mime) extraPieces.push(mime);

      // Example future fields; safe even if undefined
      if (Number.isFinite(file.durationSeconds)) {
        extraPieces.push(`${file.durationSeconds}s`);
      }
      if (file.width && file.height) {
        extraPieces.push(`${file.width}×${file.height}`);
      }

      const parts = [];

      if (tagNames.length) {
        parts.push(tagNames.join(", "));
      }
      if (extraPieces.length) {
        parts.push(extraPieces.join(" • "));
      }

      if (!parts.length) return; // nothing to show

      const useMetadataLabel = parts.length > 1 || extraPieces.length > 0;
      const labelKey = useMetadataLabel ? "metadata" : "tags";
      const label = t(labelKey) || (useMetadataLabel ? "MetaData" : "Tags");

      props.push(
        `<div class="hover-prop-line"><strong>${escapeHTML(label)}:</strong> ${escapeHTML(parts.join(" • "))}</div>`
      );
    })();
    // ------------------------------------------------------------------

    propsEl.innerHTML = props.join("");

    // Left: image / video preview OR text snippet OR "No preview"
    if (isImage) {
      const bytes = Number.isFinite(file.sizeBytes) ? file.sizeBytes : null;
      const maxImagePreviewBytes = getMaxImagePreviewBytes();
      if (bytes != null && bytes > maxImagePreviewBytes) {
        thumbEl.innerHTML = `
          <div style="
            padding:6px 8px;
            border-radius:6px;
            font-size:0.82rem;
            text-align:center;
            background-color:rgba(15,23,42,0.92);
            color:#e5e7eb;
            max-width:100%;
          ">
            ${escapeHTML(t("preview_too_large") || "Preview disabled for large image")}
          </div>
        `;
      } else {
      // --- image thumbnail
      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);
      }

    } else if (isVideo) {
      // --- video thumbnail (ffmpeg-backed) ---
      const bytes = Number.isFinite(file.sizeBytes) ? file.sizeBytes : null;
      const maxVideoPreviewBytes = getMaxVideoPreviewBytes();
      const fallbackMsg = t("no_preview_available") || "No preview available";
      const isStillActive = () =>
        hoverPreviewContext &&
        hoverPreviewContext.type === "file" &&
        hoverPreviewContext.file === file;
      const renderVideoFallbackMessage = () => {
        if (!isStillActive()) return;
        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(fallbackMsg)}
          </div>
        `;
      };

      const sourceType = String(getSourceTypeById(sourceId || getActivePaneSourceId()) || '').toLowerCase();
      const isRemoteSource = sourceId !== '' && !isLocalSourceLike(sourceId);
      const canStreamRemotePreview = isRemoteSource
        && !isFtpSource(sourceId)
        && bytes != null
        && bytes <= maxVideoPreviewBytes;

      const renderRemoteVideoPreview = () => {
        if (!isStillActive()) return;

        const previewUrl = apiFileUrl(folder, file.name, true, sourceId);
        thumbEl.innerHTML = "";
        thumbEl.style.minHeight = "140px";
        thumbEl.style.position = "relative";

        const video = document.createElement("video");
        video.muted = true;
        video.playsInline = true;
        video.preload = "metadata";
        video.src = previewUrl;
        video.style.maxWidth = "200px";
        video.style.maxHeight = "120px";
        video.style.display = "block";
        video.style.borderRadius = "6px";
        video.style.background = "rgba(0,0,0,0.15)";
        video.setAttribute("aria-label", file.name);

        video.addEventListener("loadedmetadata", () => {
          try {
            video.currentTime = 0.1;
          } catch (e) { /* ignore */ }
        }, { once: true });

        video.addEventListener("error", () => {
          renderVideoFallbackMessage();
        }, { once: true });

        thumbEl.appendChild(video);

        const overlay = document.createElement("span");
        overlay.className = "material-icons";
        overlay.textContent = "play_circle";
        overlay.style.position = "absolute";
        overlay.style.left = "50%";
        overlay.style.top = "50%";
        overlay.style.transform = "translate(-50%, -50%)";
        overlay.style.fontSize = "1.6rem";
        overlay.style.opacity = "0.85";
        overlay.style.color = "rgba(255,255,255,0.9)";
        overlay.style.textShadow = "0 2px 6px rgba(0,0,0,0.5)";
        overlay.style.pointerEvents = "none";
        thumbEl.appendChild(overlay);
      };

      if (isRemoteSource) {
        if (canStreamRemotePreview) {
          renderRemoteVideoPreview();
        } else {
          renderVideoFallbackMessage();
        }
      } else {
        const thumbUrl = apiMediaThumbUrl(folder, file.name, sourceId);

        if (bytes == null || bytes <= maxVideoPreviewBytes) {
          thumbEl.style.minHeight = "140px";
          thumbEl.style.position = "relative";

          const img = document.createElement("img");
          img.src = thumbUrl;
          img.alt = file.name;
          img.style.maxWidth = "200px";
          img.style.maxHeight = "120px";
          img.style.display = "block";
          img.style.borderRadius = "6px";

          img.addEventListener("error", () => {
            renderVideoFallbackMessage();
          });

          thumbEl.appendChild(img);

          const overlay = document.createElement("span");
          overlay.className = "material-icons";
          overlay.textContent = "play_circle";
          overlay.style.position = "absolute";
          overlay.style.left = "50%";
          overlay.style.top = "50%";
          overlay.style.transform = "translate(-50%, -50%)";
          overlay.style.fontSize = "1.6rem";
          overlay.style.opacity = "0.85";
          overlay.style.color = "rgba(255,255,255,0.9)";
          overlay.style.textShadow = "0 2px 6px rgba(0,0,0,0.5)";
          overlay.style.pointerEvents = "none";
          thumbEl.appendChild(overlay);
        } else {
          renderVideoFallbackMessage();
        }
      }
    } else if (isPdf) {
      if (!arePdfThumbnailsEnabled()) {
        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(t("no_preview_available") || "No preview available")}
          </div>
        `;
      } else {
      const sourceType = String(getSourceTypeById(sourceId || getActivePaneSourceId()) || '').toLowerCase();
      const isRemoteSource = sourceId !== '' && !isLocalSourceLike(sourceId);
      const renderPdfFallbackMessage = () => {
        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(t("no_preview_available") || "No preview available")}
          </div>
        `;
      };

      if (isRemoteSource) {
        renderPdfFallbackMessage();
      } else {
        const thumbUrl = apiMediaThumbUrl(folder, file.name, sourceId);
        thumbEl.style.minHeight = "140px";
        thumbEl.style.position = "relative";

        const img = document.createElement("img");
        img.src = thumbUrl;
        img.alt = file.name;
        img.style.maxWidth = "200px";
        img.style.maxHeight = "120px";
        img.style.display = "block";
        img.style.borderRadius = "6px";
        img.addEventListener("error", renderPdfFallbackMessage, { once: true });
        thumbEl.appendChild(img);

        const overlay = document.createElement("span");
        overlay.className = "material-icons";
        overlay.textContent = "picture_as_pdf";
        overlay.style.position = "absolute";
        overlay.style.left = "50%";
        overlay.style.top = "50%";
        overlay.style.transform = "translate(-50%, -50%)";
        overlay.style.fontSize = "1.6rem";
        overlay.style.opacity = "0.9";
        overlay.style.color = "rgba(255,255,255,0.95)";
        overlay.style.textShadow = "0 2px 6px rgba(0,0,0,0.55)";
        overlay.style.pointerEvents = "none";
        thumbEl.appendChild(overlay);
      }
      }
    }

        // Text snippet (left) for smaller text/code files
        if (canTextPreview || isOfficeSnippet) {
          fillFileSnippet(file, snippetEl);
        } else if (!isImage && !isVideo && !isPdf) {
          // Non-image, non-video, 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('#', '');
  if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
  const r = parseInt(hex.slice(0, 2), 16) / 255;
  const g = parseInt(hex.slice(2, 4), 16) / 255;
  const b = parseInt(hex.slice(4, 6), 16) / 255;
  const max = Math.max(r, g, b), min = Math.min(r, g, b);
  let h, s, l = (max + min) / 2;
  if (max === min) { h = s = 0; }
  else {
    const d = max - min;
    s = l > .5 ? d / (2 - max - min) : d / (max + min);
    switch (max) {
      case r: h = (g - b) / d + (g < b ? 6 : 0); break;
      case g: h = (b - r) / d + 2; break;
      default: h = (r - g) / d + 4;
    }
    h /= 6;
  }
  return { h: h * 360, s: s * 100, l: l * 100 };
}
function _hslToHex(h, s, l) {
  h /= 360; s /= 100; l /= 100;
  const f = n => {
    const k = (n + h * 12) % 12, a = s * Math.min(l, 1 - l);
    const c = l - a * Math.max(-1, Math.min(k - 3, Math.min(9 - k, 1)));
    return Math.round(255 * c).toString(16).padStart(2, '0');
  };
  return '#' + f(0) + f(8) + f(4);
}
function _lighten(hex, amt = 14) { const { h, s, l } = _hexToHsl(hex); return _hslToHex(h, s, Math.min(100, l + amt)); }
function _darken(hex, amt = 22) { const { h, s, l } = _hexToHsl(hex); return _hslToHex(h, s, Math.max(0, l - amt)); }

let _folderIconRenderSeq = 0;

function _isHexColor(value) {
  return /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(String(value || '').trim());
}

function sanitizeTagColor(value) {
  const raw = String(value || '').trim();
  if (!raw) return '#777777';
  if (_isHexColor(raw)) return raw;
  if (/^[a-zA-Z]{1,32}$/.test(raw)) return raw;
  return '#777777';
}

function _resolveFolderColors(hostEl, fullPath) {
  const frontVar = String(hostEl?.style?.getPropertyValue('--filr-folder-front') || '').trim();
  const backVar = String(hostEl?.style?.getPropertyValue('--filr-folder-back') || '').trim();
  const strokeVar = String(hostEl?.style?.getPropertyValue('--filr-folder-stroke') || '').trim();
  const baseHex = _isHexColor(frontVar)
    ? frontVar
    : ((window.folderColorMap && window.folderColorMap[fullPath]) || '#f6b84e');

  return {
    front: _isHexColor(frontVar) ? frontVar : baseHex,
    back: _isHexColor(backVar) ? backVar : _lighten(baseHex, 14),
    stroke: _isHexColor(strokeVar) ? strokeVar : _darken(baseHex, 22)
  };
}

function _applyFolderColors(hostEl, fullPath) {
  if (!hostEl) return;
  const { front, back, stroke } = _resolveFolderColors(hostEl, fullPath);
  hostEl.style.setProperty('--filr-folder-front', front);
  hostEl.style.setProperty('--filr-folder-back', back);
  hostEl.style.setProperty('--filr-folder-stroke', stroke);
}

function _stampFolderIcon(iconSpan) {
  if (!iconSpan) return '';
  const seq = String(++_folderIconRenderSeq);
  iconSpan.dataset.renderSeq = seq;
  return seq;
}

// tiny fetch helper with timeout for folder counts
function _fetchJSONWithTimeout(url, ms = 2500) {
  const ctrl = new AbortController();
  const tid = setTimeout(() => ctrl.abort(), ms);
  return fetch(url, { credentials: 'include', signal: ctrl.signal })
    .then(r => r.ok ? r.json() : { folders: 0, files: 0, __fr_err: 1 })
    .catch(() => ({ folders: 0, files: 0, __fr_err: 1 }))
    .finally(() => clearTimeout(tid));
}

// Defensive: some browsers occasionally blank out inline SVGs elsewhere when a context menu opens.
// This only repaints *blank* strip/inline-folder icons (no re-render churn when nothing is wrong).
export function repairBlankFolderIcons({ force = false, skipStats = false } = {}) {
  const isBroken = (el) => {
    if (!el) return true;
    const html = String(el.innerHTML || '').trim();
    if (!html) return true;
    const svg = el.querySelector('svg');
    if (!svg) return true;
    // If the SVG exists but is missing expected parts, treat it as broken.
    // (Some Safari/WebKit glitches leave an empty <svg> shell.)
    if (!svg.querySelector('.folder-front') && !svg.querySelector('.folder-back')) return true;
    const rect = svg.getBoundingClientRect();
    if (!rect.width || !rect.height) return true;
    return false;
  };

  // Folder strip icons
  try {
    const strip = document.getElementById('folderStripContainer');
    if (strip) {
      const sourceId = getActivePaneSourceId();
      strip.querySelectorAll('.folder-item[data-folder]').forEach(item => {
        const folder = item.getAttribute('data-folder') || '';
        if (!folder) return;
        const iconSpan = item.querySelector('.folder-svg');
        if (!force && !isBroken(iconSpan)) return;
        attachStripIconAsync(item, folder, 48, { preserveKind: true, sourceId, skipStats });
      });
    }
  } catch (e) { /* best effort */ }

  // Inline folder rows
  try {
    document.querySelectorAll('#fileList tr.folder-row[data-folder], #fileListSecondary tr.folder-row[data-folder]').forEach(row => {
      const folder = row.getAttribute('data-folder') || '';
      if (!folder) return;
      const iconSpan = row.querySelector('.folder-svg');
      if (!force && !isBroken(iconSpan)) return;
      const sourceId = getPaneSourceIdForElement(row);
      attachStripIconAsync(row, folder, 28, { preserveKind: true, sourceId, skipStats });
    });
  } catch (e) { /* best effort */ }
}

function forceRepaintInlineFolderIcons(listEl) {
  if (!listEl) return;
  try {
    listEl.querySelectorAll('tr.folder-row[data-folder]').forEach(row => {
      const folder = row.getAttribute('data-folder') || '';
      if (!folder) return;
      const iconSpan = row.querySelector('.folder-svg');
      if (!iconSpan) return;
      _stampFolderIcon(iconSpan);
      _applyFolderColors(row, folder);
      const kind = iconSpan.dataset.kind || 'empty';
      iconSpan.dataset.kind = kind;
      iconSpan.innerHTML = folderSVG(kind, { encrypted: isEncryptedForFolderIcon(folder) });
    });
    syncFolderIconSizeToRowHeight();
  } catch (e) { /* best effort */ }
}

function scheduleFolderIconRepair(opts = {}) {
  try {
    const { force = false, skipStats = false } = opts || {};
    const kick = () => { try { repairBlankFolderIcons({ force, skipStats }); } catch (e) {} };
    if (typeof queueMicrotask === 'function') queueMicrotask(kick);
    setTimeout(kick, 80);
    setTimeout(kick, 250);
    if (force) {
      setTimeout(kick, 800);
      const gentle = () => { try { repairBlankFolderIcons({ force: false, skipStats: true }); } catch (e) {} };
      setTimeout(gentle, 600);
      setTimeout(gentle, 1200);
      setTimeout(gentle, 2000);
    }
  } catch (e) { /* ignore */ }
}

function scheduleInactivePaneFolderIconRepair() {
  if (!window.dualPaneEnabled) return;
  try {
    const kick = () => {
      try {
        const list = document.getElementById('fileListSecondary');
        forceRepaintInlineFolderIcons(list);
      } catch (e) { /* ignore */ }
    };
    if (typeof queueMicrotask === 'function') queueMicrotask(kick);
    setTimeout(kick, 80);
    setTimeout(kick, 250);
  } catch (e) { /* ignore */ }
}

// Paint initial icon, then flip to "paper" if non-empty
function attachStripIconAsync(hostEl, fullPath, size = 28, { preserveKind = false, sourceId = '', skipStats = false } = {}) {
  _applyFolderColors(hostEl, fullPath);

  const iconSpan = hostEl.querySelector('.folder-svg');
  if (!iconSpan) return;
  const renderSeq = _stampFolderIcon(iconSpan);

  const encrypted = isEncryptedForFolderIcon(fullPath);

  // 1) initial icon
  const currentKind = iconSpan.dataset.kind || 'empty';
  const firstKind = preserveKind ? currentKind : 'empty';
  iconSpan.dataset.kind = firstKind;
  iconSpan.innerHTML = folderSVG(firstKind, { encrypted });

  // make sure this brand-new SVG is sized correctly
  try { syncFolderIconSizeToRowHeight(); } catch (e) {}

  if (skipStats) return;

  const resolvedSource = sourceId || getActivePaneSourceId();
  fetchFolderStats(fullPath, resolvedSource)
  .then(stats => {
    if (!iconSpan.isConnected) return;
    if (iconSpan.dataset.renderSeq && iconSpan.dataset.renderSeq !== renderSeq) return;
    if (!stats) return;
    const folders = Number.isFinite(stats.folders) ? stats.folders : 0;
    const files   = Number.isFinite(stats.files)   ? stats.files   : 0;

    if ((folders + files) > 0 && iconSpan.dataset.kind !== 'paper') {
      iconSpan.dataset.kind = 'paper';
      iconSpan.innerHTML = folderSVG('paper', { encrypted });
      try { syncFolderIconSizeToRowHeight(); } catch (e) {}
    }
  })
  .catch(() => {});
}

/* -----------------------------
   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 (e) { /* 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 ?? {};
}

function normalizeFolderPath(folder) {
  if (!folder) return "";
  const trimmed = String(folder).trim().replace(/^\/+|\/+$/g, "");
  return trimmed === "" ? "root" : trimmed;
}

function folderDepthScore(folder) {
  if (!folder || folder === "root") return 0;
  return folder.split("/").filter(Boolean).length;
}

async function findBestAccessibleFolder({ lastOpenedFolder, sourceId = '' } = {}) {
  try {
    const sourceParam = sourceId ? `&sourceId=${encodeURIComponent(sourceId)}` : '';
    const res = await fetch(withBase(`/api/folder/getFolderList.php?counts=0${sourceParam}`), { credentials: 'include' });
    const data = await safeJson(res);
    const names = Array.isArray(data)
      ? data.map(row => normalizeFolderPath(row?.folder || row)).filter(Boolean)
      : [];
    if (!names.length) return null;

    const unique = Array.from(new Set(names));
    const preferred = normalizeFolderPath(lastOpenedFolder);
    if (preferred && unique.includes(preferred)) return preferred;

    unique.sort((a, b) => {
      const depthDiff = folderDepthScore(a) - folderDepthScore(b);
      return depthDiff !== 0 ? depthDiff : a.localeCompare(b);
    });
    return unique[0] || null;
  } catch (e) {
    return null;
  }
}

// --- Folder capabilities + owner cache ----------------------
const _folderCapsCache  = window.__FR_CAPS_CACHE || new Map();
const _folderCapsInflight = window.__FR_CAPS_INFLIGHT || new Map();
window.__FR_CAPS_CACHE = _folderCapsCache;
window.__FR_CAPS_INFLIGHT = _folderCapsInflight;

async function fetchFolderCaps(folder, sourceId = '') {
  if (!folder) return null;
  const key = folderCacheKey(folder, sourceId);
  if (_folderCapsCache.has(key)) {
    return _folderCapsCache.get(key);
  }
  if (_folderCapsInflight.has(key)) {
    return _folderCapsInflight.get(key);
  }

      const p = (async () => {
    try {
      const sourceParam = sourceId ? `&sourceId=${encodeURIComponent(sourceId)}` : '';
      const res  = await fetch(
        withBase(`/api/folder/capabilities.php?folder=${encodeURIComponent(folder)}${sourceParam}`),
        { credentials: 'include' }
      );
      const data = await safeJson(res);
      _folderCapsCache.set(key, data || null);

      if (data && (data.owner || data.user)) {
        _folderOwnerCache.set(key, data.owner || data.user || "");
      }
      return data || null;
    } catch (e) {
      _folderCapsCache.set(key, null);
      return null;
    } finally {
      _folderCapsInflight.delete(key);
    }
  })();

  _folderCapsInflight.set(key, p);
  return p;
}

async function refreshCurrentFolderCaps(folder, sourceId = '', paneKey = '') {
  const pane = paneKey ? normalizePaneKey(paneKey) : normalizePaneKey(window.activePane);
  const resolvedSource = sourceId || getPaneSourceId(pane);
  window.currentFolderCaps = null;
  savePaneState(pane, { currentFolderCaps: null });
  try {
    const caps = await fetchFolderCaps(folder, resolvedSource);
    window.currentFolderCaps = caps || null;
    savePaneState(pane, { currentFolderCaps: window.currentFolderCaps });
  } catch (e) {
    window.currentFolderCaps = null;
    savePaneState(pane, { currentFolderCaps: null });
  }
  updateEncryptedFolderBanner(folder);
  refreshEncryptedFolderIconsInList();
  applyEncryptedFolderUiRestrictions();
  updateFileActionButtons();
}

// --- Folder owner cache + helper ----------------------
const _folderOwnerCache = new Map();

async function fetchFolderOwner(folder, sourceId = '') {
  if (!folder) return "";
  const key = folderCacheKey(folder, sourceId);
  if (_folderOwnerCache.has(key)) {
    return _folderOwnerCache.get(key);
  }

  try {
    const data = await fetchFolderCaps(folder, sourceId);
    const owner = data && (data.owner || data.user || "");
    _folderOwnerCache.set(key, owner || "");
    return owner || "";
  } catch (e) {
    _folderOwnerCache.set(key, "");
    return "";
  }
}
// ---- 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(withBase(`/api/media/getViewedMap.php?folder=${encodeURIComponent(folder)}&t=${Date.now()}`), { credentials: 'include' });
    const j = await res.json();
    map = j?.map || null;
  } catch (e) { /* 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 (!Number.isFinite(totalBytes) || totalBytes < 0) return "";

  if (totalBytes < 1024) {
    return totalBytes + " B";
  } else if (totalBytes < 1024 * 1024) {
    return (totalBytes / 1024).toFixed(1) + " KB";
  } else if (totalBytes < 1024 * 1024 * 1024) {
    return (totalBytes / (1024 * 1024)).toFixed(1) + " MB";
  } else {
    return (totalBytes / (1024 * 1024 * 1024)).toFixed(1) + " GB";
  }
}


function ensureNonZipDownloadPanel() {
  if (window.__nonZipDownloadPanel) return window.__nonZipDownloadPanel;

  const panel = document.createElement('div');
  panel.id = 'nonZipDownloadPanel';
  panel.setAttribute('role', 'status');

  // Simple bottom-right card using Bootstrap-ish styles + inline layout tweaks
  panel.style.position = 'fixed';
  panel.style.top = '50%';
  panel.style.left = '50%';
  panel.style.transform = 'translate(-50%, -50%)';
  panel.style.zIndex = '9999';
  panel.style.width    = 'min(440px, 95vw)';
  panel.style.minWidth = '280px';
  panel.style.maxWidth = '440px';
  panel.style.padding = '14px 16px';
  panel.style.borderRadius = '12px';
  panel.style.boxShadow = '0 18px 40px rgba(0,0,0,0.35)';
  panel.style.backgroundColor = 'var(--filr-menu-bg, #222)';
  panel.style.color = 'var(--filr-menu-fg, #f9fafb)';
  panel.style.fontSize = '0.9rem';
  panel.style.display = 'none';

  panel.innerHTML = `
    <div class="nonzip-title" style="margin-bottom:6px; font-weight:600;"></div>
    <div class="nonzip-sub" style="margin-bottom:8px; opacity:0.85;"></div>
    <div class="nonzip-actions" style="display:flex; justify-content:flex-end; gap:6px;">
      <button type="button"
              class="btn btn-sm btn-secondary nonzip-cancel-btn">
        ${t('cancel') || 'Cancel'}
      </button>
      <button type="button"
              class="btn btn-sm btn-primary nonzip-next-btn">
        ${t('download_next') || 'Download next'}
      </button>
    </div>
  `;

  document.body.appendChild(panel);

  const nextBtn   = panel.querySelector('.nonzip-next-btn');
  const cancelBtn = panel.querySelector('.nonzip-cancel-btn');

  if (nextBtn) {
    nextBtn.addEventListener('click', () => {
      triggerNextNonZipDownload();
    });
  }
  if (cancelBtn) {
    cancelBtn.addEventListener('click', () => {
      clearNonZipQueue(true);
    });
  }

  window.__nonZipDownloadPanel = panel;
  return panel;
}

function updateNonZipPanelText() {
  const panel = ensureNonZipDownloadPanel();
  const q = window.__nonZipDownloadQueue || [];
  const count = q.length;

  const titleEl = panel.querySelector('.nonzip-title');
  const subEl   = panel.querySelector('.nonzip-sub');

  if (!titleEl || !subEl) return;

  if (!count) {
    titleEl.textContent = t('no_files_queued') || 'No files queued.';
    subEl.textContent   = '';
    return;
  }

  const title =
    t('nonzip_queue_title') ||
    'Files queued for download';

  const raw = t('nonzip_queue_subtitle') ||
    '{count} files queued. Click "Download next" for each file.';

  const msg = raw.replace('{count}', String(count));

  titleEl.textContent = title;
  subEl.textContent   = msg;
}

function showNonZipPanel() {
  const panel = ensureNonZipDownloadPanel();
  updateNonZipPanelText();
  panel.style.display = 'block';
}

function hideNonZipPanel() {
  const panel = ensureNonZipDownloadPanel();
  panel.style.display = 'none';
}

function clearNonZipQueue(showToastCancel = false) {
  window.__nonZipDownloadQueue = [];
  hideNonZipPanel();
  if (showToastCancel) {
    showToast(
      t('nonzip_queue_cleared') || 'Download queue cleared.',
      'info'
    );
  }
}

function triggerNextNonZipDownload() {
  const q = window.__nonZipDownloadQueue || [];
  if (!q.length) {
    hideNonZipPanel();
    showToast(
      t('downloads_started') || 'All downloads started.',
      'success'
    );
    return;
  }

  const { folder, name, sourceId } = q.shift();
  const url = apiFileUrl(folder || 'root', name, /* inline */ false, sourceId);

  const a = document.createElement('a');
  a.href = url;
  a.download = name;
  a.style.display = 'none';
  document.body.appendChild(a);

  try {
    a.click();
  } finally {
    setTimeout(() => {
      if (a && a.parentNode) {
        a.parentNode.removeChild(a);
      }
    }, 500);
  }

  // Update queue + UI
  window.__nonZipDownloadQueue = q;
  if (q.length) {
    updateNonZipPanelText();
  } else {
    hideNonZipPanel();
    showToast(
      t('downloads_started') || 'All downloads started.',
      'success'
    );
  }
}

// Optional debug helpers if you want them globally:
window.triggerNextNonZipDownload = triggerNextNonZipDownload;
window.clearNonZipQueue          = clearNonZipQueue;


function getLocalSummaryStats() {
  const files = Array.isArray(fileData) ? fileData.length : 0;
  const bytes = Array.isArray(fileData)
    ? fileData.reduce((sum, file) => {
      const b = Number.isFinite(file.sizeBytes)
        ? file.sizeBytes
        : parseSizeToBytes(file.size || "");
      return sum + (Number.isFinite(b) && b > 0 ? b : 0);
    }, 0)
    : 0;
  const folders = Array.isArray(window.currentSubfolders)
    ? window.currentSubfolders.length
    : 0;
  return { folders, files, bytes };
}

function buildFolderSummaryHTML(stats, fallback) {
  const safeTitle = (key, fallbackText) => {
    const val = t(key);
    return val && val !== key ? val : fallbackText;
  };
  const fmtCount = (val) => Number.isFinite(val) ? val.toLocaleString() : "-";

  const folders = Number.isFinite(stats?.folders) ? stats.folders : fallback?.folders;
  const files = Number.isFinite(stats?.files) ? stats.files : fallback?.files;
  const bytes = Number.isFinite(stats?.bytes) ? stats.bytes : fallback?.bytes;

  const folderTitle = safeTitle("total_folders", "Total folders");
  const fileTitle = safeTitle("total_files", "Total files");
  const sizeTitle = safeTitle("total_size", "Total size");

  const sizeStr = Number.isFinite(bytes) ? formatSize(bytes) : "-";

  return `
    <span class="fr-summary-item" title="${escapeHTML(folderTitle)}">
      <span class="material-icons" aria-hidden="true">folder</span>
      <span class="fr-summary-count">${fmtCount(folders)}</span>
    </span>
    <span class="fr-summary-sep" aria-hidden="true">&middot;</span>
    <span class="fr-summary-item" title="${escapeHTML(fileTitle)}">
      <span class="material-icons" aria-hidden="true">insert_drive_file</span>
      <span class="fr-summary-count">${fmtCount(files)}</span>
    </span>
    <span class="fr-summary-sep" aria-hidden="true">&middot;</span>
    <span class="fr-summary-item" title="${escapeHTML(sizeTitle)}">
      <span class="material-icons" aria-hidden="true">storage</span>
      <span class="fr-summary-count">${escapeHTML(sizeStr)}</span>
    </span>
  `.trim();
}

function refreshFileSummaryForPane(paneKey) {
  const actionsContainer = document.getElementById("fileListActions");
  if (!actionsContainer) return;

  let summaryElem = document.getElementById("fileSummary");
  if (!summaryElem) {
    summaryElem = document.createElement("div");
    summaryElem.id = "fileSummary";
    summaryElem.className = "fr-summary-pill";
    summaryElem.setAttribute("role", "status");
    summaryElem.setAttribute("aria-live", "polite");
    actionsContainer.appendChild(summaryElem);
  }

  const state = paneKey ? getPaneState(paneKey) : null;
  if (!state || !state.hasLoaded) {
    summaryElem.style.display = "none";
    summaryElem.innerHTML = "";
    summaryElem.dataset.folder = "";
    summaryElem.dataset.truncated = "0";
    summaryElem.removeAttribute("title");
    return;
  }

  const folder = state.currentFolder || window.currentFolder || "root";
  const sourceId = state.sourceId || getActivePaneSourceId();
  summaryElem.classList.add("fr-summary-pill");
  summaryElem.style.display = "flex";
  const fallbackStats = getLocalSummaryStats();
  summaryElem.innerHTML = buildFolderSummaryHTML(null, fallbackStats);
  summaryElem.dataset.folder = folder;
  summaryElem.dataset.truncated = "0";

  fetchFolderSummaryStats(folder, undefined, sourceId).then(stats => {
    if (!stats || summaryElem.dataset.folder !== folder) return;
    summaryElem.innerHTML = buildFolderSummaryHTML(stats, fallbackStats);
    summaryElem.dataset.truncated = stats.truncated ? "1" : "0";
    if (stats.truncated) {
      summaryElem.title = "Totals capped for very large folders.";
    } else {
      summaryElem.removeAttribute("title");
    }
  });
}

/**
 * Advanced Search toggle
 */
function toggleAdvancedSearch() {
  window.advancedSearchEnabled = !window.advancedSearchEnabled;
  const advancedBtn = document.getElementById("advancedSearchToggle");
  if (advancedBtn) {
    advancedBtn.textContent = window.advancedSearchEnabled ? "Basic Search" : "Advanced Search";
  }
  const pane = normalizePaneKey(window.activePane);
  const folder = window.currentFolder || "root";
  if (window.advancedSearchEnabled) {
    loadFileList(folder, { pane, forceLegacy: true, includeContent: true });
    return;
  }
  const trimmed = String(window.currentSearchTerm || '').trim();
  if (trimmed === '' && shouldUseServerFilePagingForRequest({})) {
    loadFileList(folder, { pane });
    return;
  }
  if (window.viewMode === "gallery") {
    renderGalleryView(folder);
  } else {
    renderFileTable(folder);
  }
}

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 = withBase('/vendor/fuse/7.1.0/fuse.min.js?v={{APP_QVER}}');
let _fuseLoadingPromise = null;

function loadScriptOnce(src) {
  // cache by src so we don't append multiple <script> tags
  if (loadScriptOnce._cache?.has(src)) return loadScriptOnce._cache.get(src);
  loadScriptOnce._cache = loadScriptOnce._cache || new Map();
  const p = new Promise((resolve, reject) => {
    const s = document.createElement('script');
    s.src = src;
    s.async = true;
    s.onload = resolve;
    s.onerror = () => reject(new Error(`Failed to load ${src}`));
    document.head.appendChild(s);
  });
  loadScriptOnce._cache.set(src, p);
  return p;
}

function lazyLoadFuse() {
  if (window.Fuse) return Promise.resolve(window.Fuse);
  if (!_fuseLoadingPromise) {
    _fuseLoadingPromise = loadScriptOnce(FUSE_SRC).then(() => window.Fuse);
  }
  return _fuseLoadingPromise;
}

// (Optional) warm-up call you can trigger from main.js after first render:
//   import { warmUpSearch } from './fileListView.js?v={{APP_QVER}}';
//   warmUpSearch();
// This just starts fetching Fuse in the background.
export function warmUpSearch() {
  lazyLoadFuse().catch(() => {/* ignore; we’ll fall back */ });
}

// Lazy + backward-compatible search
function searchFiles(searchTerm) {
  if (!searchTerm) return fileData;

  // kick off Fuse load in the background, but don't await
  lazyLoadFuse().catch(() => { /* ignore */ });

  // keys config (matches your original)
  const fuseKeys = [
    { name: 'name', weight: 0.1 },
    { name: 'uploader', weight: 0.1 },
    { name: 'tags.name', weight: 0.1 }
  ];
  if (window.advancedSearchEnabled) {
    fuseKeys.push({ name: 'content', weight: 0.7 });
  }

  // If Fuse is present, use it right away (synchronous API)
  if (window.Fuse) {
    const options = {
      keys: fuseKeys,
      threshold: 0.4,
      minMatchCharLength: 2,
      ignoreLocation: true
    };
    const fuse = new window.Fuse(fileData, options);
    const results = fuse.search(searchTerm);
    return results.map(r => r.item);
  }

  // Fallback (first keystrokes before Fuse finishes loading):
  // simple case-insensitive substring match on the same fields
  const q = String(searchTerm).toLowerCase();
  const hay = (v) => (v == null ? '' : String(v)).toLowerCase();
  return fileData.filter(item => {
    if (hay(item.name).includes(q)) return true;
    if (hay(item.uploader).includes(q)) return true;
    if (Array.isArray(item.tags) && item.tags.some(t => hay(t?.name).includes(q))) return true;
    if (window.advancedSearchEnabled && hay(item.content).includes(q)) return true;
    return false;
  });
}

/**
 * View mode toggle
 */
export function createViewToggleButton() {
  let toggleBtn = document.getElementById("toggleViewBtn");
  if (!toggleBtn) {
    toggleBtn = document.createElement("button");
    toggleBtn.id = "toggleViewBtn";
    toggleBtn.classList.add("btn", "action-btn", "icon-only", "btn-light");

    if (window.viewMode === "gallery") {
      toggleBtn.innerHTML = '<i class="material-icons">view_list</i>';
      toggleBtn.title = t("switch_to_table_view");
    } else {
      toggleBtn.innerHTML = '<i class="material-icons">view_module</i>';
      toggleBtn.title = t("switch_to_gallery_view");
    }

    const actionsBar = document.getElementById("fileActionsBar");
    const headerButtons = document.querySelector(".header-buttons");

    if (actionsBar) {
      actionsBar.appendChild(toggleBtn);
    } else if (headerButtons && headerButtons.lastElementChild) {
      headerButtons.insertBefore(toggleBtn, headerButtons.lastElementChild);
    } else if (headerButtons) {
      headerButtons.appendChild(toggleBtn);
    }
  } else {
    const actionsBar = document.getElementById("fileActionsBar");
    if (actionsBar && toggleBtn.parentElement !== actionsBar) {
      actionsBar.appendChild(toggleBtn);
    }
  }

  toggleBtn.onclick = () => {
    hideViewOptionsPopover();
    window.viewMode = window.viewMode === "gallery" ? "table" : "gallery";
    localStorage.setItem("viewMode", window.viewMode);
    loadFileList(window.currentFolder);
    if (window.viewMode === "gallery") {
      toggleBtn.innerHTML = '<i class="material-icons">view_list</i>';
      toggleBtn.title = t("switch_to_table_view");
    } else {
      toggleBtn.innerHTML = '<i class="material-icons">view_module</i>';
      toggleBtn.title = t("switch_to_gallery_view");
    }
  };

  return toggleBtn;
}

function ensureSearchEverywhereButton() {
  const cfg = window.__FR_PRO_SEARCH_CFG__ || {};
  const isPro = window.__FR_IS_PRO === true;
  const ver = window.__FR_PRO_VERSION || '';
  const hasMinVersion = isPro && compareSemverLite(ver, '1.3.0') >= 0;
  const enabled = isPro && hasMinVersion && !!cfg.enabled;
  const actionsBar = document.getElementById("fileActionsBar");
  if (!enabled || !actionsBar) {
    if (searchEverywhereBtn && searchEverywhereBtn.parentElement) {
      searchEverywhereBtn.parentElement.removeChild(searchEverywhereBtn);
    }
    return null;
  }

  if (!searchEverywhereBtn) {
    searchEverywhereBtn = document.createElement("button");
    searchEverywhereBtn.id = "searchEverywhereBtn";
    searchEverywhereBtn.className = "btn btn-light action-btn";
    searchEverywhereBtn.innerHTML = `
      <i class="material-icons" aria-hidden="true">travel_explore</i>
      <span style="margin-left:4px;">${t("search_everywhere_label") || "Search everywhere"}</span>
    `;
    searchEverywhereBtn.addEventListener("click", openSearchEverywhereModal);
  }

  if (searchEverywhereBtn.parentElement !== actionsBar) {
    actionsBar.appendChild(searchEverywhereBtn);
  }
  return searchEverywhereBtn;
}

function getSearchEverywhereSources() {
  const sources = [];
  const select = document.getElementById('sourceSelector');
  if (select && select.options && select.options.length) {
    Array.from(select.options).forEach(opt => {
      const id = String(opt.value || '').trim();
      if (!id) return;
      const name = String(opt.dataset?.sourceName || opt.textContent || id);
      const type = String(opt.dataset?.sourceType || '');
      const readOnly = opt.dataset?.sourceReadOnly === '1' || opt.disabled;
      sources.push({ id, name, type, readOnly });
    });
    return sources;
  }

  try {
    const metaMap = window.__FR_SOURCE_META_MAP;
    if (metaMap && typeof metaMap === 'object') {
      Object.keys(metaMap).forEach(id => {
        const meta = metaMap[id] || {};
        sources.push({ id, name: meta.name || id, type: meta.type || '', readOnly: !!meta.readOnly });
      });
    }
  } catch (e) { /* ignore */ }

  return sources;
}

function populateSearchEverywhereSources() {
  if (!searchEverywhereSourceEl) return;
  const sources = getSearchEverywhereSources();
  const wrap = searchEverywhereSourceEl.closest('.search-everywhere-source-wrap');
  searchEverywhereSourceEl.innerHTML = '';

  const multiple = sources.length > 1;
  if (multiple) {
    const opt = document.createElement('option');
    opt.value = 'all';
    opt.textContent = t('search_everywhere_source_all') || 'All sources';
    searchEverywhereSourceEl.appendChild(opt);
  }

  sources.forEach(src => {
    const id = String(src.id || '').trim();
    if (!id) return;
    const opt = document.createElement('option');
    opt.value = id;
    const label = formatSourceBadgeText(src) || id;
    const lockMark = src.readOnly ? ` \uD83D\uDD12 ${t('read_only') || 'Read-only'}` : '';
    opt.textContent = `${label}${lockMark}`;
    searchEverywhereSourceEl.appendChild(opt);
  });

  if (wrap) {
    wrap.style.display = (multiple || sources.length > 0) ? '' : 'none';
  }
  searchEverywhereSourceEl.disabled = sources.length <= 1;

  const prev = searchEverywhereSourceEl.getAttribute('data-prev') || '';
  const hasPrev = Array.from(searchEverywhereSourceEl.options).some(opt => opt.value === prev);
  if (hasPrev) {
    searchEverywhereSourceEl.value = prev;
  } else if (multiple) {
    searchEverywhereSourceEl.value = 'all';
  } else if (searchEverywhereSourceEl.options.length) {
    searchEverywhereSourceEl.value = searchEverywhereSourceEl.options[0].value;
  }
  searchEverywhereSourceEl.setAttribute('data-prev', searchEverywhereSourceEl.value || '');

  if (!searchEverywhereSourceEl.__wired) {
    searchEverywhereSourceEl.__wired = true;
    searchEverywhereSourceEl.addEventListener('change', () => {
      searchEverywhereSourceEl.setAttribute('data-prev', searchEverywhereSourceEl.value || '');
    });
  }
}

function ensureSearchEverywhereModal() {
  if (searchEverywhereModal) return searchEverywhereModal;

  const overlay = document.createElement("div");
  overlay.id = "searchEverywhereModal";
  overlay.style.cssText = `
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,0.55);
    z-index: 4000;
    display: none;
    align-items: center;
    justify-content: center;
    padding: 16px;
  `;

  const card = document.createElement("div");
  card.className = "search-everywhere-card";
  card.style.cssText = `
    max-width: 720px;
    width: 100%;
    border-radius: 12px;
    padding: 16px;
    position: relative;
    box-shadow: 0 12px 35px rgba(0,0,0,0.35);
  `;
  card.innerHTML = `
    <button type="button"
            class="editor-close-btn"
            aria-label="${t("close") || "Close"}"
            id="searchEverywhereClose"
            style="top:8px; right:8px;">&times;</button>
    <div class="d-flex align-items-center" style="gap:10px; margin-bottom:10px;">
      <i class="material-icons" aria-hidden="true">travel_explore</i>
      <div>
        <div style="font-weight:600;">${t("search_everywhere_title") || "Search Everywhere"}</div>
        <div class="text-muted" style="font-size:12px;">${t("search_everywhere_desc") || "ACL-aware search across all folders you can access."}</div>
      </div>
    </div>
    <div class="form-group" style="margin-bottom:8px;">
      <input type="text" id="searchEverywhereInput" class="form-control" placeholder="${t("search_everywhere_placeholder") || "Type to search across all folders"}" autocomplete="off" />
    </div>
    <div class="d-flex align-items-center" style="gap:8px; margin-bottom:10px; flex-wrap: wrap;">
      <div class="search-everywhere-source-wrap" style="display:flex; align-items:center; gap:6px;">
        <label for="searchEverywhereSource" class="mb-0" style="font-size:12px;">${t("search_everywhere_source_label") || "Source"}</label>
        <select id="searchEverywhereSource" class="form-control" style="max-width:220px;"></select>
      </div>
      <label for="searchEverywhereLimit" class="mb-0" style="font-size:12px;">${t("search_everywhere_limit") || "Result limit"}</label>
      <input type="number" id="searchEverywhereLimit" class="form-control" style="max-width:100px;" min="1" max="200" />
      <button type="button" class="btn btn-primary btn-sm" id="searchEverywhereRun">${t("search_everywhere_run") || "Search"}</button>
    </div>
    <div id="searchEverywhereResults" style="max-height:320px; overflow:auto; border:1px solid #e0e0e0; border-radius:10px; padding:8px; background:rgba(0,0,0,0.02);">
      <div class="text-muted" style="font-size:12px;">${t("search_everywhere_hint") || "Results will appear here."}</div>
    </div>
  `;

  overlay.appendChild(card);
  document.body.appendChild(overlay);

  searchEverywhereCard = card;
  const closeBtn = card.querySelector("#searchEverywhereClose");
  closeBtn?.addEventListener("click", closeSearchEverywhereModal);
  overlay.addEventListener("click", (e) => {
    if (e.target === overlay) closeSearchEverywhereModal();
  });

  searchEverywhereModal = overlay;
  searchEverywhereResultsEl = card.querySelector("#searchEverywhereResults");
  searchEverywhereInputEl = card.querySelector("#searchEverywhereInput");
  searchEverywhereLimitEl = card.querySelector("#searchEverywhereLimit");
  searchEverywhereSourceEl = card.querySelector("#searchEverywhereSource");
  populateSearchEverywhereSources();

  const runBtn = card.querySelector("#searchEverywhereRun");
  runBtn?.addEventListener("click", () => {
    if (searchEverywhereInputEl) {
      runSearchEverywhere(searchEverywhereInputEl.value || '');
    }
  });
  searchEverywhereInputEl?.addEventListener("keydown", (e) => {
    if (e.key === 'Enter') {
      e.preventDefault();
      runSearchEverywhere(searchEverywhereInputEl.value || '');
    }
  });

  const dmToggle = document.getElementById('darkModeToggle');
  if (dmToggle && !dmToggle.__searchEverywhereTheme) {
    dmToggle.__searchEverywhereTheme = true;
    dmToggle.addEventListener('click', () => applySearchEverywhereTheme());
  }
  applySearchEverywhereTheme();

  return searchEverywhereModal;
}

function applySearchEverywhereTheme() {
  if (!searchEverywhereModal) return;
  const overlay = searchEverywhereModal;
  const card = searchEverywhereCard || overlay.querySelector(".search-everywhere-card");
  const isDark = document.body.classList.contains("dark-mode");

  overlay.style.background = isDark ? "rgba(0,0,0,0.72)" : "rgba(0,0,0,0.55)";
  if (card) {
    card.style.background = isDark ? "#141414" : "#ffffff";
    card.style.color = isDark ? "#f4f4f4" : "#000000";
    card.style.boxShadow = isDark ? "0 18px 45px rgba(0,0,0,0.65)" : "0 12px 35px rgba(0,0,0,0.2)";
    card.style.border = isDark ? "1px solid rgba(255,255,255,0.08)" : "1px solid #e0e0e0";
  }

  if (searchEverywhereResultsEl) {
    searchEverywhereResultsEl.style.border = isDark ? "1px solid rgba(255,255,255,0.12)" : "1px solid #e0e0e0";
    searchEverywhereResultsEl.style.background = isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)";
  }

  const inputs = searchEverywhereModal.querySelectorAll("#searchEverywhereInput, #searchEverywhereLimit, #searchEverywhereSource");
  inputs.forEach((inp) => {
    if (isDark) {
      inp.style.background = "#1f1f1f";
      inp.style.color = "#f4f4f4";
      inp.style.borderColor = "#444";
    } else {
      inp.style.background = "";
      inp.style.color = "";
      inp.style.borderColor = "";
    }
  });

  searchEverywhereModal.querySelectorAll(".text-muted").forEach((el) => {
    el.style.color = isDark ? "rgba(255,255,255,0.72)" : "";
  });
}

function openSearchEverywhereModal() {
  ensureSearchEverywhereModal();
  if (!searchEverywhereModal) return;
  const cfg = window.__FR_PRO_SEARCH_CFG__ || {};
  const limit = Math.max(1, Math.min(200, Number(cfg.defaultLimit || 50)));
  if (searchEverywhereLimitEl) {
    searchEverywhereLimitEl.value = String(limit);
  }
  populateSearchEverywhereSources();
  applySearchEverywhereTheme();
  searchEverywhereModal.style.display = "flex";
  requestAnimationFrame(() => {
    searchEverywhereInputEl?.focus();
  });
}

function closeSearchEverywhereModal() {
  if (searchEverywhereModal) {
    searchEverywhereModal.style.display = "none";
  }
}

function ensureSearchEverywhereResultBlocks() {
  if (!searchEverywhereResultsEl) {
    searchEverywhereStatusEl = null;
    searchEverywhereListEl = null;
    return { statusEl: null, listEl: null };
  }

  let statusEl = searchEverywhereResultsEl.querySelector("#searchEverywhereStatus");
  let listEl = searchEverywhereResultsEl.querySelector("#searchEverywhereList");
  if (!statusEl || !listEl) {
    searchEverywhereResultsEl.innerHTML = `
      <div id="searchEverywhereStatus" class="text-muted" style="font-size:12px; margin-bottom:6px;"></div>
      <div id="searchEverywhereList"></div>
    `;
    statusEl = searchEverywhereResultsEl.querySelector("#searchEverywhereStatus");
    listEl = searchEverywhereResultsEl.querySelector("#searchEverywhereList");
  }

  searchEverywhereStatusEl = statusEl;
  searchEverywhereListEl = listEl;
  return { statusEl, listEl };
}

function sortSearchEverywhereItems(items) {
  const list = Array.isArray(items) ? items.slice() : [];
  list.sort((a, b) => {
    const saRaw = Number(a?.score);
    const sbRaw = Number(b?.score);
    const sa = Number.isFinite(saRaw) ? saRaw : 999;
    const sb = Number.isFinite(sbRaw) ? sbRaw : 999;
    if (sa === sb) {
      const pa = `${String(a?.sourceId || '')}::${String(a?.path || '')}`;
      const pb = `${String(b?.sourceId || '')}::${String(b?.path || '')}`;
      return pa.localeCompare(pb, undefined, { sensitivity: "base", numeric: true });
    }
    return sa - sb;
  });
  return list;
}

function renderSearchEverywhereResults(items) {
  if (!searchEverywhereResultsEl) return;
  const listEl = searchEverywhereResultsEl.querySelector("#searchEverywhereList") || searchEverywhereResultsEl;
  if (!Array.isArray(items) || !items.length) {
    listEl.innerHTML = `<div class="text-muted" style="font-size:12px;">${t("search_everywhere_no_results") || "No results"}</div>`;
    return;
  }

  const rows = items.map((item) => {
    const icon = item.type === 'folder' ? 'folder' : 'insert_drive_file';
    const name = escapeHTML(item.name || '');
    const path = escapeHTML(item.path || '');
    const sourceId = String(item.sourceId || '').trim();
    const sourceMeta = {
      name: String(item.sourceName || ''),
      type: String(item.sourceType || '')
    };
    let sourceLabel = formatSourceBadgeText(sourceMeta);
    if (!sourceLabel && sourceId) {
      sourceLabel = formatSourceBadgeText(getSourceMetaById(sourceId));
    }
    const sourceText = sourceLabel ? escapeHTML(sourceLabel) : '';
    const uploaderText = item.uploader ? escapeHTML(`Uploaded by ${item.uploader}`) : '';
    const metaLine = [path, sourceText, uploaderText].filter(Boolean).join(' • ');
    return `
      <div class="d-flex align-items-center search-everywhere-row" data-path="${path}" data-folder="${escapeHTML(item.folder || '')}" data-name="${escapeHTML(item.name || '')}" data-type="${item.type}" data-source-id="${escapeHTML(sourceId)}" style="padding:6px 4px; border-bottom:1px solid rgba(0,0,0,0.05); cursor:pointer;">
        <i class="material-icons" aria-hidden="true" style="margin-right:8px;">${icon}</i>
        <div style="flex:1; min-width:0;">
          <div style="font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${name}</div>
          ${metaLine ? `<div class="text-muted" style="font-size:12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${metaLine}</div>` : ''}
        </div>
      </div>
    `;
  }).join("");

  listEl.innerHTML = rows;
  listEl.querySelectorAll('.search-everywhere-row').forEach((row) => {
    row.addEventListener('click', () => {
      const folder = row.getAttribute('data-folder') || 'root';
      const name = row.getAttribute('data-name') || '';
      const sourceId = row.getAttribute('data-source-id') || '';
      navigateToSearchResult(folder, name, sourceId);
    });
  });
}

async function runSearchEverywhereAcrossSources(term, limit, sources, runId) {
  const uniqSources = [];
  const seen = new Set();
  sources.forEach((src) => {
    const id = String(src?.id || '').trim();
    if (!id || seen.has(id)) return;
    seen.add(id);
    uniqSources.push({
      id,
      name: String(src?.name || id),
      type: String(src?.type || ''),
    });
  });

  if (!uniqSources.length) {
    renderSearchEverywhereResults([]);
    if (searchEverywhereStatusEl) searchEverywhereStatusEl.textContent = '';
    return;
  }

  const perSourceLimit = Math.min(limit, 50);
  const statusById = {};
  const labelById = {};
  uniqSources.forEach((src) => {
    statusById[src.id] = 'pending';
    labelById[src.id] = formatSourceBadgeText(src) || src.name || src.id;
  });

  const resultsMap = new Map();
  const reindexedSources = new Set();

  const updateStatus = () => {
    if (!searchEverywhereStatusEl) return;
    const ids = Object.keys(statusById);
    if (!ids.length) {
      searchEverywhereStatusEl.textContent = '';
      return;
    }
    const doneCount = ids.filter(id => statusById[id] !== 'pending').length;
    const pendingLabels = ids.filter(id => statusById[id] === 'pending').map(id => labelById[id]).filter(Boolean);
    const failedLabels = ids.filter(id => statusById[id] === 'error').map(id => labelById[id]).filter(Boolean);

    if (doneCount >= ids.length && pendingLabels.length === 0) {
      if (failedLabels.length) {
        searchEverywhereStatusEl.textContent = `Failed: ${failedLabels.join(', ')}`;
      } else {
        searchEverywhereStatusEl.textContent = '';
      }
      return;
    }

    let text = `${t("loading") || "Loading..."} (${doneCount}/${ids.length})`;
    if (pendingLabels.length) {
      text += ` - Waiting on: ${pendingLabels.join(', ')}`;
    }
    if (failedLabels.length) {
      text += ` - Failed: ${failedLabels.join(', ')}`;
    }
    searchEverywhereStatusEl.textContent = text;
  };

  updateStatus();

  const fetchOne = async (src) => {
    const sourceParam = `&sourceId=${encodeURIComponent(src.id)}`;
    try {
      const res = await fetch(withBase(`/api/pro/search/query.php?q=${encodeURIComponent(term)}&limit=${perSourceLimit}${sourceParam}`), {
        credentials: 'include',
      });
      const data = await res.json();
      if (!res.ok || !data.ok) {
        throw new Error(data.error || `HTTP ${res.status}`);
      }
      if (runId !== searchEverywhereRunId) return;
      statusById[src.id] = 'done';
      if (data.reindexed) reindexedSources.add(src.id);
      const items = Array.isArray(data.items) ? data.items : [];
      items.forEach((item) => {
        const sourceKey = String(item?.sourceId || src.id || '');
        const pathKey = String(item?.path || item?.folder || item?.name || '');
        const key = `${sourceKey}::${pathKey}`;
        if (!item?.sourceId) item.sourceId = sourceKey;
        resultsMap.set(key, item);
      });
      const sorted = sortSearchEverywhereItems(Array.from(resultsMap.values()));
      renderSearchEverywhereResults(sorted.slice(0, limit));
    } catch (e) {
      if (runId !== searchEverywhereRunId) return;
      statusById[src.id] = 'error';
    } finally {
      if (runId !== searchEverywhereRunId) return;
      updateStatus();
    }
  };

  await Promise.allSettled(uniqSources.map(src => fetchOne(src)));

  if (runId !== searchEverywhereRunId) return;
  if (!resultsMap.size) {
    renderSearchEverywhereResults([]);
  }
  updateStatus();
  if (reindexedSources.size) {
    showToast(t("search_everywhere_reindexed") || "Index refreshed.", 'success');
  }
}

async function runSearchEverywhere(term) {
  if (!term || !term.trim()) {
    showToast(t("enter_search_term") || "Enter a search term.", 'warning');
    return;
  }
  ensureSearchEverywhereModal();
  const runId = ++searchEverywhereRunId;
  const { statusEl, listEl } = ensureSearchEverywhereResultBlocks();
  if (statusEl) {
    statusEl.textContent = `${t("loading") || "Loading..."}`;
  }
  if (listEl) {
    listEl.innerHTML = '';
  }

  const limitVal = searchEverywhereLimitEl ? Number(searchEverywhereLimitEl.value || 50) : 50;
  const limit = Math.max(1, Math.min(200, limitVal || 50));
  let sourceId = searchEverywhereSourceEl ? String(searchEverywhereSourceEl.value || '').trim() : '';
  if (sourceId === 'all') {
    const sources = getSearchEverywhereSources();
    if (sources.length > 1) {
      await runSearchEverywhereAcrossSources(term, limit, sources, runId);
      return;
    }
    if (sources.length === 1) {
      sourceId = String(sources[0]?.id || '').trim();
    }
  }
  const sourceParam = sourceId ? `&sourceId=${encodeURIComponent(sourceId)}` : '';

  try {
    const res = await fetch(withBase(`/api/pro/search/query.php?q=${encodeURIComponent(term)}&limit=${limit}${sourceParam}`), {
      credentials: 'include',
    });
    const data = await res.json();
    if (runId !== searchEverywhereRunId) return;
    if (!res.ok || !data.ok) {
      throw new Error(data.error || `HTTP ${res.status}`);
    }
    renderSearchEverywhereResults(data.items || []);
    if (statusEl) statusEl.textContent = '';
    if (data.reindexed) {
      showToast(t("search_everywhere_reindexed") || "Index refreshed.", 'success');
    }
  } catch (e) {
    if (runId !== searchEverywhereRunId) return;
    console.error('Search everywhere error', e);
    if (searchEverywhereResultsEl) {
      searchEverywhereResultsEl.innerHTML = `<div class="text-danger" style="font-size:12px;">${t("search_everywhere_error") || "Search failed."}</div>`;
    }
    if (statusEl) statusEl.textContent = '';
    showToast(t("search_everywhere_error") || "Search failed.", 'error');
  }
}

async function navigateToSearchResult(folder, name, sourceId) {
  if (!folder) return;
  closeSearchEverywhereModal();
  const dest = decodeHtmlEntities(folder || 'root') || 'root';
  const targetName = decodeHtmlEntities(name || '');
  const targetSourceId = String(sourceId || '').trim();
  const activeSourceId = getGlobalActiveSourceId();

  if (targetSourceId && targetSourceId !== activeSourceId) {
    let applied = false;
    if (typeof window.__frApplyActiveSource === 'function') {
      try {
        applied = await window.__frApplyActiveSource(targetSourceId, { skipEvent: true, origin: 'search' });
      } catch (e) { /* ignore */ }
    }
    if (!applied) {
      const sel = document.getElementById('sourceSelector');
      if (sel) sel.value = targetSourceId;
      try { localStorage.setItem('fr_active_source', targetSourceId); } catch (e) { /* ignore */ }
    }

    const activePane = normalizePaneKey(window.activePane);
    resetPagingForFolderNavigation(activePane);
    savePaneState(activePane, {
      sourceId: targetSourceId,
      currentFolderCaps: null,
      selectedFolderCaps: null
    });
    refreshSourceBadges();
    try { resetFolderTreeCaches(); } catch (e) { /* ignore */ }
    __frTreeSourceId = targetSourceId;

    pendingSearchSelection = targetName ? { folder: dest, name: targetName } : null;
    try { await loadFolderTree(dest); } catch (e) { /* ignore */ }

    setTimeout(() => maybeHighlightSearchedFile(dest), 80);
    setTimeout(() => maybeHighlightSearchedFile(dest), 220);
    setTimeout(() => maybeHighlightSearchedFile(dest), 500);
    return;
  }

  pendingSearchSelection = targetName ? { folder: dest, name: targetName } : null;
  setCurrentFolderContext(dest, { resetPage: true });

  // Refresh tree + strip selections to match the jumped folder
  try {
    await expandTreePathAsync(dest, { force: true, includeLeaf: true, persist: false });
    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");
  } catch (e) { /* best effort */ }

  try {
    const strip = document.getElementById("folderStripContainer");
    if (strip) {
      strip.querySelectorAll(".folder-item.selected").forEach(i => i.classList.remove("selected"));
      const stripItem = strip.querySelector(`.folder-item[data-folder="${CSS.escape(dest)}"]`);
      if (stripItem) stripItem.classList.add("selected");
    }
  } catch (e) { /* best effort */ }

  await loadFileList(dest);
  // Extra safety: attempt highlight again after render settles
  setTimeout(() => maybeHighlightSearchedFile(dest), 80);
  setTimeout(() => maybeHighlightSearchedFile(dest), 220);
  setTimeout(() => maybeHighlightSearchedFile(dest), 500);
}

export async function navigateToLinkedFile(folder, name, sourceId) {
  await navigateToSearchResult(folder, name, sourceId);
}

  function bindFolderToolbarActions() {
    const map = [
      { id: "folderMoveInlineBtn",   handler: (folder) => openMoveFolderUI(folder) },
      { id: "folderRenameInlineBtn", handler: () => openRenameFolderModal() },
      { id: "folderColorInlineBtn",  handler: (folder) => openColorFolderModal(folder) },
      { id: "folderEncryptInlineBtn", handler: (folder) => startFolderCryptoJobFlow(folder, 'encrypt') },
      { id: "folderDecryptInlineBtn", handler: (folder) => startFolderCryptoJobFlow(folder, 'decrypt') },
      { id: "folderShareInlineBtn",  handler: (folder) => openFolderShareModal(folder) },
      { id: "folderDeleteInlineBtn", handler: () => openDeleteFolderModal() }
    ];

  const selectedOrToast = () => {
    const folder = getSelectedFolderPath();
    if (!folder) {
      showToast(t("select_folder") || "Select a folder first.", 'warning');
      return null;
    }
    return folder;
  };

  map.forEach(({ id, handler }) => {
    const btn = document.getElementById(id);
    if (!btn || btn.dataset.bound === "1") return;
    btn.dataset.bound = "1";
    btn.addEventListener("click", () => {
      const folder = selectedOrToast();
      if (!folder) return;
      setCurrentFolderContext(folder);
      handler(folder);
    });
  });
}

function applyGalleryColumns(cols) {
  const clamped = clampGalleryColumns(cols);
  window.galleryColumns = clamped;
  localStorage.setItem("galleryColumns", clamped);

  const grid = document.querySelector(".gallery-container");
  if (grid) {
    grid.style.gridTemplateColumns = `repeat(${clamped},minmax(0,1fr))`;
  }
  document.querySelectorAll(".gallery-thumbnail")
    .forEach(img => img.style.maxHeight = getMaxImageHeight() + "px");

  const val = document.getElementById("galleryColumnsValue");
  if (val) val.textContent = clamped;

  return clamped;
}

let viewOptionsPopover = null;
let viewOptionsAnchor = null;
let viewOptionsBound = false;

function hideViewOptionsPopover() {
  if (viewOptionsPopover) {
    viewOptionsPopover.style.display = "none";
  }
  viewOptionsAnchor = null;
  document.removeEventListener("click", viewOptionsOutside, true);
  document.removeEventListener("keydown", viewOptionsEscape, true);
  viewOptionsBound = false;
}

function viewOptionsOutside(ev) {
  if (!viewOptionsPopover || viewOptionsPopover.style.display === "none") return;
  const anchor = viewOptionsAnchor;
  if (viewOptionsPopover.contains(ev.target)) return;
  if (anchor && (ev.target === anchor || anchor.contains(ev.target))) return;
  hideViewOptionsPopover();
}

function viewOptionsEscape(ev) {
  if (ev.key === "Escape") hideViewOptionsPopover();
}

function ensureViewOptionsPopover() {
  if (viewOptionsPopover) return viewOptionsPopover;
  const el = document.createElement("div");
  el.id = "viewOptionsPopover";
  el.className = "filr-popover";
  el.style.display = "none";
  document.body.appendChild(el);
  viewOptionsPopover = el;
  return el;
}

function buildRowHeightControls(host) {
  const wrap = document.createElement("div");
  wrap.className = "vo-control";

  const label = document.createElement("div");
  label.className = "vo-label";
  label.textContent = t("row_height") || "Row height";

  const sliderRow = document.createElement("div");
  sliderRow.className = "vo-slider-row";

  const slider = document.createElement("input");
  slider.type = "range";
  slider.min = "20";
  slider.max = "60";
  slider.value = clampRowHeight(localStorage.getItem("rowHeight") || 44);
  slider.id = "rowHeightSlider";

  const value = document.createElement("span");
  value.id = "rowHeightValue";
  value.className = "vo-value";
  value.textContent = `${slider.value}px`;

  slider.addEventListener("input", () => {
    const h = applyRowHeight(slider.value);
    value.textContent = `${h}px`;
  });

  sliderRow.appendChild(slider);
  sliderRow.appendChild(value);

  wrap.appendChild(label);
  wrap.appendChild(sliderRow);
  host.appendChild(wrap);
}

function buildGalleryControls(host) {
  const wrap = document.createElement("div");
  wrap.className = "vo-control";

  const label = document.createElement("div");
  label.className = "vo-label";
  label.textContent = t("columns") || "Columns";

  const sliderRow = document.createElement("div");
  sliderRow.className = "vo-slider-row";

  const slider = document.createElement("input");
  slider.type = "range";
  slider.min = "1";
  slider.max = String(getGalleryMaxColumns());
  slider.value = clampGalleryColumns(localStorage.getItem("galleryColumns") || window.galleryColumns || 3);
  slider.id = "galleryColumnsSlider";

  const value = document.createElement("span");
  value.id = "galleryColumnsValue";
  value.className = "vo-value";
  value.textContent = slider.value;

  slider.addEventListener("input", () => {
    const cols = applyGalleryColumns(slider.value);
    value.textContent = cols;
  });

  sliderRow.appendChild(slider);
  sliderRow.appendChild(value);

  wrap.appendChild(label);
  wrap.appendChild(sliderRow);
  host.appendChild(wrap);
}

function currentZoomPercent() {
  try {
    if (window.fileriseZoom && typeof window.fileriseZoom.currentPercent === "function") {
      return window.fileriseZoom.currentPercent();
    }
  } catch (e) {}
  const css = getComputedStyle(document.documentElement).getPropertyValue('--app-zoom') || "1";
  const n = parseFloat(css);
  return Number.isFinite(n) && n > 0 ? Math.round(n * 100) : 100;
}

function buildZoomControls(host) {
  const wrap = document.createElement("div");
  wrap.className = "vo-control";

  const label = document.createElement("div");
  label.className = "vo-label";
  label.textContent = t("zoom") || "Zoom";

  const row = document.createElement("div");
  row.className = "vo-slider-row";

  const slider = document.createElement("input");
  slider.type = "range";
  slider.min = "60";
  slider.max = "140";
  slider.step = "5";
  slider.value = currentZoomPercent();
  slider.id = "zoomPercentSlider";

  const value = document.createElement("span");
  value.id = "zoomPercentValue";
  value.className = "vo-value";
  value.textContent = `${slider.value}%`;

  const applyZoom = (pct) => {
    const api = window.fileriseZoom;
    if (api && typeof api.setPercent === "function") {
      api.setPercent(pct);
    } else {
      document.documentElement.style.setProperty('--app-zoom', String(pct / 100));
    }
    value.textContent = `${pct}%`;
    slider.value = String(pct);
  };

  slider.addEventListener("input", () => {
    applyZoom(parseInt(slider.value, 10));
  });

  const btnRow = document.createElement("div");
  btnRow.className = "vo-zoom-btns";

  const mkBtn = (icon, title, handler) => {
    const b = document.createElement("button");
    b.type = "button";
    b.className = "btn btn-sm btn-light vo-zoom-btn";
    b.title = title;
    b.innerHTML = `<span class="material-icons">${icon}</span>`;
    b.addEventListener("click", handler);
    return b;
  };

  btnRow.appendChild(mkBtn("remove", t("zoom_out") || "Zoom out", () => {
    const api = window.fileriseZoom;
    const next = api && api.out ? api.out() : currentZoomPercent() - 5;
    applyZoom(next);
  }));
  btnRow.appendChild(mkBtn("add", t("zoom_in") || "Zoom in", () => {
    const api = window.fileriseZoom;
    const next = api && api.in ? api.in() : currentZoomPercent() + 5;
    applyZoom(next);
  }));
  btnRow.appendChild(mkBtn("refresh", t("reset_zoom") || "Reset", () => {
    const api = window.fileriseZoom;
    const next = api && api.reset ? api.reset() : 100;
    applyZoom(next);
  }));

  row.appendChild(slider);
  row.appendChild(value);

  wrap.appendChild(label);
  wrap.appendChild(row);
  wrap.appendChild(btnRow);
  host.appendChild(wrap);
}

function buildDualPaneToggle(host) {
  const wrap = document.createElement("div");
  wrap.className = "vo-control";

  const row = document.createElement("label");
  row.className = "vo-toggle-row";

  const text = document.createElement("span");
  text.className = "vo-toggle-text";
  text.textContent = t("dual_pane_mode") || "Dual-pane mode";

  const toggle = document.createElement("input");
  toggle.type = "checkbox";
  toggle.className = "vo-toggle-input";
  toggle.id = "voDualPaneToggle";
  toggle.checked = localStorage.getItem("dualPaneMode") === "true";
  toggle.setAttribute("aria-label", text.textContent);

  const applyDualPane = (enabled) => {
    localStorage.setItem("dualPaneMode", enabled ? "true" : "false");
    window.dualPaneEnabled = enabled;
    if (typeof window.applyDualPaneMode === "function") {
      window.applyDualPaneMode(enabled);
    } else {
      window.__frDualPanePending = enabled;
    }
    const panelToggle = document.getElementById("dualPaneMode");
    if (panelToggle) {
      panelToggle.checked = enabled;
    }
  };

  toggle.addEventListener("change", () => {
    applyDualPane(toggle.checked);
  });

  row.appendChild(text);
  row.appendChild(toggle);
  wrap.appendChild(row);
  host.appendChild(wrap);
}

function positionPopover(pop, anchor) {
  if (!anchor) return;
  const rect = anchor.getBoundingClientRect();
  pop.style.display = "block";
  pop.style.visibility = "hidden";

  const { width: pw, height: ph } = pop.getBoundingClientRect();
  let left = rect.left + rect.width - pw;
  let top = rect.bottom + 8;
  const padding = 8;

  left = Math.min(Math.max(padding, left), window.innerWidth - pw - padding);
  top = Math.min(Math.max(padding, top), window.innerHeight - ph - padding);

  pop.style.left = `${left}px`;
  pop.style.top = `${top}px`;
  pop.style.visibility = "visible";
}

function showViewOptionsPopover(triggerBtn) {
  const pop = ensureViewOptionsPopover();
  if (pop.style.display === "block" && viewOptionsAnchor === triggerBtn) {
    hideViewOptionsPopover();
    return;
  }

  pop.innerHTML = "";
  viewOptionsAnchor = triggerBtn || null;

  const title = document.createElement("div");
  title.className = "vo-title";
  title.textContent = t("view_options") || "View options";
  pop.appendChild(title);

  if (window.viewMode === "gallery") {
    buildGalleryControls(pop);
  } else {
    buildRowHeightControls(pop);
  }

  // separator then zoom controls
  const sep = document.createElement("div");
  sep.className = "vo-separator";
  pop.appendChild(sep);
  buildZoomControls(pop);

  const sep2 = document.createElement("div");
  sep2.className = "vo-separator";
  pop.appendChild(sep2);
  buildDualPaneToggle(pop);

  positionPopover(pop, triggerBtn);
  pop.style.display = "block";

  if (!viewOptionsBound) {
    document.addEventListener("click", viewOptionsOutside, true);
    document.addEventListener("keydown", viewOptionsEscape, true);
    viewOptionsBound = true;
  }
}

function bindViewOptionsButton() {
  const btn = document.getElementById("viewOptionsBtn");
  if (!btn || btn.dataset.bound === "1") return;
  btn.dataset.bound = "1";
  btn.addEventListener("click", (ev) => {
    ev.preventDefault();
    ev.stopPropagation();
    showViewOptionsPopover(btn);
  });
}

export function formatFolderName(folder, sourceId = '') {
  if (folder === "root") return getRootLabel(sourceId);
  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, options = {}) {
  clearInlineRenameState({ restore: false });
  await initOnlyOfficeCaps();
  hideHoverPreview();
  const pane = normalizePaneKey(options.pane || window.activePane);
  const reqId = ++__fileListReqSeq[pane]; // latest call wins
  const folderOnly =
    window.userFolderOnly === true ||
    localStorage.getItem("folderOnly") === "true";
  const username = (localStorage.getItem("username") || "").trim();
  let paneSourceId = getPaneSourceId(pane);
  const paneState = getPaneState(pane);
  const paneFolder = (paneState && paneState.sourceId === paneSourceId && typeof paneState.currentFolder === 'string')
    ? paneState.currentFolder.trim()
    : readStoredPaneFolder(pane, paneSourceId);
  const lastOpenedFolder = getLastOpenedFolder(paneSourceId);
  const { skipFallback } = options || {};
  const sourceParam = paneSourceId ? `&sourceId=${encodeURIComponent(paneSourceId)}` : '';
  const isFtp = isFtpSource(paneSourceId);
  const shouldFetchFolders = window.showFoldersInList !== false || window.showInlineFolders !== false;
  const includeContent = window.advancedSearchEnabled === true || options.includeContent === true;
  const useServerPaging = shouldUseServerFilePagingForRequest(options);
  let requestedCursor = (options && Object.prototype.hasOwnProperty.call(options, 'cursor'))
    ? String(options.cursor || '')
    : null;
  const hadExplicitFolderParam = folderParam !== undefined && folderParam !== null && String(folderParam).trim() !== '';
  const prevPaneFolder = (paneState && typeof paneState.currentFolder === 'string')
    ? paneState.currentFolder.trim()
    : '';

  let folder = folderParam || paneFolder || lastOpenedFolder || (paneSourceId ? "root" : (window.currentFolder || "root"));
  const isActivePane = pane === normalizePaneKey(window.activePane);
  if (isActivePane && folder) {
    window.currentFolder = folder;
    updateBreadcrumbTitle(folder);
  }
  if (folderOnly && (!folder || folder === "root") && username) {
    folder = username;
    window.currentFolder = folder;
    setLastOpenedFolder(folder, paneSourceId);
    updateBreadcrumbTitle(folder);
  }
  const prevFolderKey = normalizeFolderPath(prevPaneFolder || paneFolder || '');
  const nextFolderKey = normalizeFolderPath(folder || '');
  const folderChangedByExplicitLoad = hadExplicitFolderParam && prevFolderKey !== nextFolderKey;
  if (folderChangedByExplicitLoad) {
    resetPagingForFolderNavigation(pane);
    // Ignore any stale cursor when changing folders through a direct load call.
    requestedCursor = null;
  }
  savePaneState(pane, { currentFolder: folder });
  const subfoldersMatch =
    String(window.currentSubfoldersSourceId || '') === String(paneSourceId || '') &&
    String(window.currentSubfoldersFolder || '') === String(folder || '');
  if (!subfoldersMatch) {
    window.currentSubfolders = [];
    window.currentSubfoldersSourceId = String(paneSourceId || '');
    window.currentSubfoldersFolder = String(folder || '');
    savePaneState(pane, {
      currentSubfolders: [],
      currentSubfoldersSourceId: window.currentSubfoldersSourceId,
      currentSubfoldersFolder: window.currentSubfoldersFolder
    });
    const strip = document.getElementById('folderStripContainer');
    if (strip) {
      strip.innerHTML = '';
      strip.style.display = 'none';
    }
  }

  const fileListContainer = document.getElementById("fileList");
  if (fileListContainer) {
    fileListContainer.classList.remove("file-list-secondary-empty");
  }
  const actionsContainer = document.getElementById("fileListActions");
  window.selectedFolderCaps = null;
  savePaneState(pane, { selectedFolderCaps: null });
  refreshCurrentFolderCaps(folder, paneSourceId, pane);
  if (!useServerPaging) {
    setPaneFileListPaging(pane, null);
  }

  // 1) show loader (only this request is allowed to render)
  fileListContainer.style.visibility = "visible";
  fileListContainer.setAttribute('aria-busy', 'true');
  fileListContainer.innerHTML = "<div class='loader'>Loading files...</div>";
  const slowToastTimer = setTimeout(() => {
    if (reqId === __fileListReqSeq[pane]) {
      const now = Date.now();
      if ((now - _lastSlowLoadToastAt) >= SLOW_LOAD_TOAST_COOLDOWN_MS) {
        _lastSlowLoadToastAt = now;
        showToast(t('loading_slow') || "Still loading... remote sources can take a while.", 'info');
      }
    }
  }, SLOW_LOAD_TOAST_DELAY_MS);

  try {
    // Kick off both in parallel, but render as soon as FILES are ready
    const recursiveParam = folderOnly ? 0 : 1;
    const buildFileListUrl = (targetFolder, recursiveValue, cursorValue = null, forceNoPaging = false) => {
      const params = new URLSearchParams();
      params.set('folder', targetFolder);
      params.set('recursive', String(recursiveValue));
      if (paneSourceId) {
        params.set('sourceId', paneSourceId);
      }
      if (includeContent) {
        params.set('includeContent', '1');
      }
      if (!forceNoPaging && useServerPaging) {
        const pageSize = getFileListCursorPageSize();
        const sortColumn = String(sortOrder?.column || 'modified');
        const sortDir = (sortOrder && sortOrder.ascending === true) ? 'asc' : 'desc';
        const allowedSort = new Set(['name', 'modified', 'uploaded', 'size', 'uploader']);
        params.set('pageSize', String(pageSize));
        params.set('sortBy', allowedSort.has(sortColumn) ? sortColumn : 'modified');
        params.set('sortDir', sortDir);
        const cursorText = (cursorValue == null) ? '' : String(cursorValue).trim();
        if (cursorText !== '') {
          params.set('cursor', cursorText);
        }
      }
      params.set('t', String(Date.now()));
      return withBase(`/api/file/getFileList.php?${params.toString()}`);
    };

    const startCursor = useServerPaging
      ? resolveServerPagingCursorForLoad({
          pane,
          folder,
          sourceId: paneSourceId,
          requestedCursor
        })
      : null;
    const filesPromise = fetch(buildFileListUrl(folder, recursiveParam, startCursor), { credentials: 'include' });
    let foldersPromise = null;
    if (!isFtp && shouldFetchFolders) {
      foldersPromise = fetch(
        withBase(`/api/folder/getFolderList.php?folder=${encodeURIComponent(folder)}&counts=0${sourceParam}`),
        { credentials: 'include' }
      );
    }

    // ----- FILES FIRST -----
    let filesRes = await filesPromise;

    // If the first attempt is forbidden (common for folder-only users or wrong folder),
    // retry smartly:
    //  - first, drop recursion
    //  - if still forbidden and we have a username, try that folder once
    if (filesRes.status === 403 && folder !== "root") {
      try {
        filesRes = await fetch(
          buildFileListUrl(folder, 0, startCursor),
          { credentials: "include" }
        );
      } catch (e) { /* ignore and fall through */ }
    }
    if (filesRes.status === 403 && username && folder !== username) {
      try {
        const alt = await fetch(
          buildFileListUrl(username, 0, useServerPaging ? '' : null),
          { credentials: "include" }
        );
        if (alt.ok) {
          // switch context to the user folder
          filesRes = alt;
          folder = username;
          window.currentFolder = folder;
          setLastOpenedFolder(folder, paneSourceId);
          updateBreadcrumbTitle(folder);
          // remember that this is a folder-only session
          window.userFolderOnly = true;
          try { localStorage.setItem("folderOnly", "true"); } catch (e) { }
          refreshCurrentFolderCaps(folder, paneSourceId, pane);
          // refresh folders promise for the new folder context
          if (!isFtp && shouldFetchFolders) {
            foldersPromise = fetch(
              withBase(`/api/folder/getFolderList.php?folder=${encodeURIComponent(folder)}&counts=0${sourceParam}`),
              { credentials: 'include' }
            );
          }
        }
      } catch (e) { /* ignore; will fall back to the normal 403 handling */ }
    }

    if (filesRes.status === 401) {
      // session expired — bounce to logout
      window.location.href = withBase("/api/auth/logout.php");
      throw new Error("Unauthorized");
    }
    if (filesRes.status === 403) {
      if (!skipFallback) {
        const fallback = await findBestAccessibleFolder({ lastOpenedFolder, sourceId: paneSourceId });
        if (fallback && fallback !== folder) {
          setCurrentFolderContext(fallback, { pane, resetPage: true });
          return await loadFileList(fallback, { pane, skipFallback: true, forceLegacy: options.forceLegacy === true });
        }
      }
      // For folder-only users, treat 403 as "empty list" instead of hard error.
      if (folderOnly) {
        fileListContainer.innerHTML = `
            <div class="empty-state">
              ${t("no_files_found") || "No files found."}
            </div>`;
        const summaryElem = document.getElementById("fileSummary");
        if (summaryElem) summaryElem.style.display = "none";
        const strip = document.getElementById("folderStripContainer");
        if (strip) strip.style.display = "none";
        updateFileActionButtons();
        fileListContainer.style.visibility = "visible";
        return [];
      }

      // forbidden — friendly message, keep UI responsive
      fileListContainer.innerHTML = `
          <div class="empty-state">
            ${t("no_access_to_resource") || "You don't have access to this folder."}
          </div>`;
      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.');
    }
    const responseSourceId = String(data.sourceId || '').trim();
    if (!paneSourceId && responseSourceId) {
      paneSourceId = responseSourceId;
      savePaneState(pane, { sourceId: responseSourceId });
    }
    const pagingRaw = (data && typeof data.paging === 'object' && data.paging) ? data.paging : null;
    if (useServerPaging && pagingRaw) {
      const limit = Number.isFinite(Number(pagingRaw.limit))
        ? Math.max(FILE_LIST_CURSOR_PAGE_SIZE_MIN, Math.min(FILE_LIST_CURSOR_PAGE_SIZE_MAX, Number(pagingRaw.limit)))
        : getFileListCursorPageSize();
      const total = Number.isFinite(Number(pagingRaw.total)) ? Math.max(0, Number(pagingRaw.total)) : 0;
      const totalPagesRaw = Number.isFinite(Number(pagingRaw.totalPages)) ? Number(pagingRaw.totalPages) : 0;
      const totalPages = Math.max(1, totalPagesRaw || Math.ceil(total / limit) || 1);
      const pageRaw = Number.isFinite(Number(pagingRaw.page)) ? Number(pagingRaw.page) : 1;
      const page = Math.max(1, Math.min(totalPages, pageRaw || 1));
      const nextCursor = (pagingRaw.nextCursor == null) ? null : String(pagingRaw.nextCursor);
      const prevCursor = (pagingRaw.prevCursor == null) ? null : String(pagingRaw.prevCursor);
      const cursor = String(pagingRaw.cursor == null ? '' : pagingRaw.cursor);
      setPaneFileListPaging(pane, {
        enabled: true,
        mode: 'cursor',
        cursor,
        nextCursor,
        prevCursor,
        hasMore: !!pagingRaw.hasMore,
        limit,
        total,
        totalPages,
        page,
        sortBy: String(pagingRaw.sortBy || sortOrder?.column || 'modified'),
        sortDir: String(pagingRaw.sortDir || ((sortOrder && sortOrder.ascending === true) ? 'asc' : 'desc'))
      });
    } else {
      setPaneFileListPaging(pane, null);
    }

    // If another loadFileList ran after this one, bail before touching the DOM
    if (reqId !== __fileListReqSeq[pane]) return [];

    // 3) clear loader
    fileListContainer.innerHTML = "";

    // 4) handle “no files” case
    if (!data.files || Object.keys(data.files).length === 0) {
      if (reqId !== __fileListReqSeq[pane]) return [];
      fileListContainer.innerHTML = `
          <div class="empty-state">
            ${t("no_files_found")}
            <div style="margin-top:6px;font-size:.9em;color:#777">
              ${t("no_folder_access_yet") || "No folder access has been assigned to your account yet."}
            </div>
          </div>`;

      const summaryElem = document.getElementById("fileSummary");
      if (summaryElem) summaryElem.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();
    
      let bytes = Number.isFinite(f.sizeBytes)
        ? f.sizeBytes
        : parseSizeToBytes(String(f.size || ""));
    
      if (!Number.isFinite(bytes) || bytes < 0) {
        bytes = null;
      }
    
      f.sizeBytes = bytes;
    
      // New: normalize display size and create a stable cache key
      if (bytes != null) {
        f.size = formatSize(bytes);
      }
    
      const cacheKey =
        (f.modified && String(f.modified)) ||
        (f.uploaded && String(f.uploaded)) ||
        (bytes != null ? String(bytes) : "") ||
        f.name;
    
      f.cacheKey = cacheKey;
      f.folder   = folder;
      const sid = String(f.sourceId || paneSourceId || '').trim();
      if (sid) {
        f.sourceId = sid;
      }
    
      // 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;
    
      return f;
    });
    fileData = data.files;
    savePaneState(pane, { fileData: fileData, currentFolder: folder, hasLoaded: true });

    if (reqId !== __fileListReqSeq[pane]) return [];

    // 6) inject summary + slider
    if (actionsContainer) {
      // a) summary
      let summaryElem = document.getElementById("fileSummary");
      if (!summaryElem) {
        summaryElem = document.createElement("div");
        summaryElem.id = "fileSummary";
        summaryElem.className = "fr-summary-pill";
        summaryElem.setAttribute("role", "status");
        summaryElem.setAttribute("aria-live", "polite");
        actionsContainer.appendChild(summaryElem);
      }
      summaryElem.classList.add("fr-summary-pill");
      summaryElem.style.display = "flex";
      const fallbackStats = getLocalSummaryStats();
      summaryElem.innerHTML = buildFolderSummaryHTML(null, fallbackStats);
      summaryElem.dataset.folder = folder;
      summaryElem.dataset.truncated = "0";

      fetchFolderSummaryStats(folder, undefined, paneSourceId).then(stats => {
        if (!stats || summaryElem.dataset.folder !== folder) return;
        summaryElem.innerHTML = buildFolderSummaryHTML(stats, fallbackStats);
        summaryElem.dataset.truncated = stats.truncated ? "1" : "0";
        if (stats.truncated) {
          summaryElem.title = "Totals capped for very large folders.";
        } else {
          summaryElem.removeAttribute("title");
        }
      });
    }

    // 7) render files
    if (reqId !== __fileListReqSeq[pane]) return [];

    if (window.viewMode === "gallery") {
      renderGalleryView(folder);
    } else {
      renderFileTable(folder);
    }
    updateFileActionButtons();
    fileListContainer.style.visibility = "visible";

    // Highlight a search-hit file if requested
    maybeHighlightSearchedFile(folder);

    // ----- FOLDERS NEXT (populate strip when ready; doesn't block rows) -----
    try {
      if (!foldersPromise && isFtp && shouldFetchFolders) {
        foldersPromise = new Promise(resolve => {
          setTimeout(() => {
            resolve(fetch(
              withBase(`/api/folder/getFolderList.php?folder=${encodeURIComponent(folder)}&counts=0${sourceParam}`),
              { credentials: 'include' }
            ));
          }, 600);
        });
      }

      if (foldersPromise) {
        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[pane]) return data.files;

        // --- build ONLY the *direct* children of current folder ---
        let subfolders = [];
        const hidden = new Set(["profile_pics", "trash"]);
        if (folder === "root") {
          hidden.add("root");
        }
        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 => {
          const lower = (sf.name || "").toLowerCase();
          return !hidden.has(lower) && !lower.startsWith("resumable_");
        });

        // Expose for inline folder rows in table view
        window.currentSubfolders = subfolders;
        window.currentSubfoldersSourceId = String(paneSourceId || '');
        window.currentSubfoldersFolder = String(folder || '');
        savePaneState(pane, {
          currentSubfolders: subfolders,
          currentSubfoldersSourceId: window.currentSubfoldersSourceId,
          currentSubfoldersFolder: window.currentSubfoldersFolder
        });

        let strip = document.getElementById("folderStripContainer");
        if (!strip) {
          strip = document.createElement("div");
          strip.id = "folderStripContainer";
          strip.className = "folder-strip-container";
          actionsContainer.parentNode.insertBefore(strip, actionsContainer);
        }

        // NEW: paged + responsive strip
        renderFolderStripPaged(strip, subfolders, paneSourceId);

        // Re-render table view once folders are known so they appear inline above files
        if (window.viewMode === "table" && window.showInlineFolders !== false && reqId === __fileListReqSeq[pane]) {
          renderFileTable(folder, fileListContainer, undefined, { preserveSelection: true });
        }
      }
    } catch (e) {
      // ignore folder errors; rows already rendered
    }

    setPaneHasContent(pane, true);
    // Intentionally skip auto-restoring the inactive pane for lazy-load perf.
    if (window.dualPaneEnabled) {
      scheduleFolderIconRepair();
      scheduleInactivePaneFolderIconRepair();
    }
    updateDualPaneTargetHint();
    return data.files;

  } catch (err) {
    if (reqId !== __fileListReqSeq[pane]) return [];
    const errMsg = String(err?.message || '').toLowerCase();
    const missingFolder = /directory not found|folder does not exist|does not exist|not found/.test(errMsg);
    if (missingFolder && !skipFallback) {
      const fallback = getParentFolder(folder || 'root') || 'root';
      if (fallback && fallback !== folder) {
        setCurrentFolderContext(fallback, { pane, resetPage: true });
        if (isActivePane) {
          try { syncFolderTreeSelection(fallback); } catch (e) { /* ignore */ }
        }
        return await loadFileList(fallback, { pane, skipFallback: true, forceLegacy: options.forceLegacy === true });
      }
    }
    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[pane]) {
      clearTimeout(slowToastTimer);
      fileListContainer.style.visibility = "visible";
      fileListContainer.removeAttribute('aria-busy');
    }
  }
}

function makeInlineFolderDragImage(labelText) {
  const isDark = document.body.classList.contains('dark-mode');

  const textColor = isDark
    ? '#f1f3f4'
    : 'var(--filr-text, #111827)';

  const bgColor = isDark
    ? 'rgba(32,33,36,0.96)'
    : 'var(--filr-bg-elevated, #ffffff)';

  const borderColor = isDark
    ? 'rgba(255,255,255,0.14)'
    : 'rgba(15,23,42,0.12)';

  const wrap = document.createElement('div');
  Object.assign(wrap.style, {
    position: 'fixed',
    top: '-9999px',
    left: '-9999px',
    zIndex: '99999',

    display: 'inline-flex',
    alignItems: 'center',
    gap: '8px',

    padding: '7px 16px',
    minHeight: '32px',
    maxWidth: '420px',
    whiteSpace: 'nowrap',

    borderRadius: '999px',
    overflow: 'hidden',
    backgroundClip: 'padding-box',

    background: bgColor,
    color: textColor,
    border: `1px solid ${borderColor}`,
    boxShadow: '0 4px 18px rgba(0,0,0,0.18)',

    fontSize: '14px',
    lineHeight: '1.4',
    fontWeight: '500',

    pointerEvents: 'none'
  });

  const icon = document.createElement('span');
  icon.className = 'material-icons';
  icon.textContent = 'folder';
  Object.assign(icon.style, {
    fontSize: '20px',
    lineHeight: '1',
    flexShrink: '0',
    color: textColor
  });

  const label = document.createElement('span');
  const txt = String(labelText || '');
  label.textContent = txt.length > 60 ? (txt.slice(0, 57) + '…') : txt;
  Object.assign(label.style, {
    overflow: 'hidden',
    textOverflow: 'ellipsis'
  });

  wrap.appendChild(icon);
  wrap.appendChild(label);
  document.body.appendChild(wrap);

  return wrap;
}

function folderRowDragStartHandler(event, fullPath) {
  try { cancelHoverPreview(); } catch (e) {}

  if (!fullPath) return;

  const srcParent = parentFolderOf(fullPath);
  const paneSourceId = getPaneSourceIdForElement(event.currentTarget) || getActivePaneSourceId();

  const payload = {
    dragType: 'folder',
    folder: fullPath,
    sourceFolder: srcParent,
    sourceId: paneSourceId
  };

  event.dataTransfer.effectAllowed = 'move';
  event.dataTransfer.setData('application/json', JSON.stringify(payload));
  event.dataTransfer.setData('text/plain', fullPath);

  const label = fullPath.split('/').pop() || fullPath;
  const ghost = makeInlineFolderDragImage(label);
  event.dataTransfer.setDragImage(ghost, 10, 10);
  setTimeout(() => {
    try { document.body.removeChild(ghost); } catch (e) {}
  }, 0);
}

function injectInlineFolderRows(fileListContent, folder, pageSubfolders) {
  const table = fileListContent.querySelector('table.filr-table');

  // Use the paged subfolders if provided, otherwise fall back to all
  const subfolders = Array.isArray(pageSubfolders) && pageSubfolders.length
    ? pageSubfolders
    : (Array.isArray(window.currentSubfolders) ? window.currentSubfolders : []);

  if (!table || !subfolders.length) return;

  const thead = table.tHead;
  const tbody = table.tBodies && table.tBodies[0];
  if (!thead || !tbody) return;

  const headerRow = thead.rows[0];
  if (!headerRow) return;

  const headerCells = Array.from(headerRow.cells);
  const colCount = headerCells.length;

  // --- Column indices -------------------------------------------------------
  let checkboxIdx = headerCells.findIndex(th =>
    th.classList.contains("checkbox-col") ||
    th.querySelector('input[type="checkbox"]')
  );

  let nameIdx = headerCells.findIndex(th =>
    (th.dataset && th.dataset.column === "name") ||
    /\bname\b/i.test((th.textContent || "").trim())
  );
  if (nameIdx < 0) {
    nameIdx = Math.min(1, colCount - 1); // fallback to 2nd col
  }

  let sizeIdx = headerCells.findIndex(th =>
    (th.dataset && (th.dataset.column === "size" || th.dataset.column === "filesize")) ||
    /\bsize\b/i.test((th.textContent || "").trim())
  );
  if (sizeIdx < 0) sizeIdx = -1;

  let uploaderIdx = headerCells.findIndex(th =>
    (th.dataset && th.dataset.column === "uploader") ||
    /\buploader\b/i.test((th.textContent || "").trim())
  );
  if (uploaderIdx < 0) uploaderIdx = -1;

  let actionsIdx = headerCells.findIndex(th =>
    (th.dataset && th.dataset.column === "actions") ||
    /\bactions?\b/i.test((th.textContent || "").trim()) ||
    /\bactions?-col\b/i.test(th.className || "")
  );
  if (actionsIdx < 0) actionsIdx = -1;

    // NEW: created / modified column indices (uploaded = created in your header)
    let createdIdx = headerCells.findIndex(th =>
      (th.dataset && (th.dataset.column === "uploaded" || th.dataset.column === "created")) ||
      /\b(uploaded|created)\b/i.test((th.textContent || "").trim())
    );
    if (createdIdx < 0) createdIdx = -1;
  
    let modifiedIdx = headerCells.findIndex(th =>
      (th.dataset && th.dataset.column === "modified") ||
      /\bmodified\b/i.test((th.textContent || "").trim())
    );
    if (modifiedIdx < 0) modifiedIdx = -1;

  // Remove any previous folder rows
  tbody.querySelectorAll("tr.folder-row").forEach(tr => tr.remove());


  
  const firstDataRow = tbody.firstElementChild;

  subfolders.forEach(sf => {
    const tr = document.createElement("tr");
    tr.classList.add("folder-row");
    tr.dataset.folder = sf.full;

      // Allow dragging this folder row itself (for folder → folder moves)
  tr.setAttribute('draggable', 'true');
  tr.addEventListener('dragstart', e => folderRowDragStartHandler(e, sf.full));
  
    for (let i = 0; i < colCount; i++) {
      const td = document.createElement("td");

// *** copy header classes so responsive breakpoints match file rows ***
// but strip Bootstrap margin helpers (ml-2 / mx-2) so we don't get a big gap
const headerClass = headerCells[i] && headerCells[i].className;
if (headerClass) {
  td.className = headerClass;
  td.classList.remove("ml-2", "mx-2");
}
  
      // 1) checkbox column (real checkbox; icon moves to name cell)
      if (i === checkboxIdx) {
        td.classList.add("folder-icon-cell", "checkbox-col");
        td.style.textAlign = "center";
        td.style.verticalAlign = "middle";
  
        const cb = document.createElement("input");
        cb.type = "checkbox";
        cb.className = "folder-checkbox";
        cb.dataset.folder = sf.full;
        cb.addEventListener("click", e => e.stopPropagation());
        cb.addEventListener("change", () => handleFolderCheckboxChange(cb));
        td.appendChild(cb);
  
      // 2) name column
      } else if (i === nameIdx) {
        td.classList.add("name-cell", "file-name-cell", "folder-name-cell");
  
        const wrap = document.createElement("div");
        wrap.className = "folder-row-inner";
  
        const iconSpan = document.createElement("span");
        iconSpan.className = "folder-svg folder-row-icon";
  
        const nameSpan = document.createElement("span");
        nameSpan.className = "folder-row-name";
        nameSpan.textContent = sf.name || sf.full;
  
        const metaSpan = document.createElement("span");
        metaSpan.className = "folder-row-meta";
        metaSpan.textContent = ""; // "(15 folders, 19 files)" later
  
        wrap.appendChild(iconSpan);
        wrap.appendChild(nameSpan);
        wrap.appendChild(metaSpan);
        td.appendChild(wrap);
  
      // 3) size column
      } else if (i === sizeIdx) {
        td.classList.add("folder-size-cell");
        td.textContent = "…"; // placeholder until we load stats
        // NEW: match file-row numeric alignment
        td.style.textAlign = "right";
        td.style.fontVariantNumeric = "tabular-nums";
  
      // 4) uploader / owner column
      } else if (i === uploaderIdx) {
        td.classList.add("uploader-cell", "folder-uploader-cell");
        td.textContent = ""; // filled asynchronously with owner
  
      // 5) actions column
    } else if (i === actionsIdx) {
      td.classList.add("folder-actions-cell");
    
      const btn = document.createElement("button");
      btn.type = "button";
      btn.className = "btn btn-link btn-actions-ellipsis";
      btn.title = t("more_actions");
    
      const icon = document.createElement("span");
      icon.className = "material-icons";
      icon.textContent = "more_vert";
    
      btn.appendChild(icon);
      td.appendChild(btn);
    }
  
      // IMPORTANT: always append the cell, no matter which column we're in
      tr.appendChild(td);
    }
  
    // click → navigate, same as before
    tr.addEventListener("click", e => {
      hideHoverPreview();
      if (tr.classList.contains('inline-rename-active') || e.target.closest(".inline-rename-input")) {
        return;
      }
      if (e.target.closest(".folder-checkbox")) {
        return;
      }
      // 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;
      const paneForRow = getPaneKeyForElement(tr);
      setCurrentFolderContext(dest, { resetPage: true, pane: paneForRow });
    
      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")
          .forEach(i => i.classList.remove("selected"));
        const stripItem = strip.querySelector(
          `.folder-item[data-folder="${CSS.escape(dest)}"]`
        );
        if (stripItem) stripItem.classList.add("selected");
      }
    
      loadFileList(dest, { pane: paneForRow });
    });
  
    
        // DnD + context menu – keep existing logic, but also add a visual highlight
        tr.addEventListener("dragover", e => {
          folderDragOverHandler(e);
          tr.classList.add("folder-row-droptarget");
        });
    
        tr.addEventListener("dragleave", e => {
          folderDragLeaveHandler(e);
          tr.classList.remove("folder-row-droptarget");
        });
    
        tr.addEventListener("drop", e => {
          folderDropHandler(e);
          tr.classList.remove("folder-row-droptarget");
        });
  
    const rowSourceId = getActivePaneSourceId();

    tr.addEventListener("contextmenu", async e => {
      e.preventDefault();
      e.stopPropagation();
  
      const dest = sf.full;
      if (!dest) return;

      let caps = null;
      try {
        caps = await fetchFolderCaps(dest, rowSourceId);
      } catch (e2) { /* ignore */ }
      const enc = (caps && caps.encryption) ? caps.encryption : {};
      const canEncrypt = !!enc.canEncrypt;
      const canDecrypt = !!enc.canDecrypt;
  
      const menuItems = [
        {
          label: t("create_folder"),
          action: () => {
            window.currentFolder = dest;
            const modal = document.getElementById("createFolderModal");
            if (modal) modal.style.display = "block";
            const input = document.getElementById("newFolderName");
            if (input) input.focus();
          }
        },
        { label: t("move_folder"),   action: () => openMoveFolderUI(dest) },
        { label: t("rename_folder"), action: () => startInlineRenameForFolderRow(tr, dest) },
        { label: t("color_folder"),  action: () => openColorFolderModal(dest) },
        ...(canEncrypt ? [{
          label: 'Encrypt folder',
          icon: 'lock',
          action: () => startFolderCryptoJobFlow(dest, 'encrypt')
        }] : []),
        ...(canDecrypt ? [{
          label: 'Decrypt folder',
          icon: 'lock_open',
          action: () => startFolderCryptoJobFlow(dest, 'decrypt')
        }] : []),
        { label: t("folder_share"),  action: () => openFolderShareModal(dest) },
        { label: t("delete_folder"), action: () => { window.currentFolder = dest; openDeleteFolderModal(); } }
      ];
      showFolderManagerContextMenu(e.clientX, e.clientY, menuItems);

      // Defensive: repaint icon if it was blanked by the contextmenu interaction.
      try {
        queueMicrotask(() => {
          const iconSpan = tr.querySelector('.folder-svg');
          if (iconSpan && String(iconSpan.innerHTML || '').trim() === '') {
            attachStripIconAsync(tr, dest, 28, { sourceId: rowSourceId });
          }
        });
      } catch (e2) { /* ignore */ }
    });
  
    // insert row above first file row
    tbody.insertBefore(tr, firstDataRow || null);
  
   // ----- ICON: color + alignment (size is driven by row height) -----
attachStripIconAsync(tr, sf.full, 28, { sourceId: rowSourceId });
const iconSpan = tr.querySelector(".folder-row-icon");
if (iconSpan) {
  iconSpan.style.display = "inline-flex";
  iconSpan.style.alignItems = "center";
  iconSpan.style.justifyContent = "flex-start";
  iconSpan.style.marginLeft = "0px";   // small left nudge
  iconSpan.style.marginTop  = "0px";   // small down nudge
}
  
    // ----- FOLDER STATS + OWNER + CAPS -----
    const sizeCellIndex    = (sizeIdx    >= 0 && sizeIdx    < tr.cells.length) ? sizeIdx    : -1;
    const nameCellIndex    = (nameIdx    >= 0 && nameIdx    < tr.cells.length) ? nameIdx    : -1;
    const createdCellIndex = (createdIdx >= 0 && createdIdx < tr.cells.length) ? createdIdx : -1;
    const modifiedCellIndex = (modifiedIdx >= 0 && modifiedIdx < tr.cells.length) ? modifiedIdx : -1;
  
    fetchFolderStats(sf.full, rowSourceId).then(stats => {
      if (!stats) return;
  
      const foldersCount = Number.isFinite(stats.folders) ? stats.folders : 0;
      const filesCount   = Number.isFinite(stats.files)   ? stats.files   : 0;
            // Try multiple possible size keys so backend + JS can drift a bit
            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;
              }
            }
  
      let 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 countLabel = pieces.join(", ");
  
      if (nameCellIndex >= 0) {
        const nameCell = tr.cells[nameCellIndex];
        if (nameCell) {
          const metaSpan = nameCell.querySelector(".folder-row-meta");
          if (metaSpan) metaSpan.textContent = ` (${countLabel})`;
        }
      }
  
      if (sizeCellIndex >= 0) {
        const sizeCell = tr.cells[sizeCellIndex];
        if (sizeCell) {
          let sizeLabel = "—";
          if (bytes != null && bytes >= 0) {
            sizeLabel = formatSize(bytes);
          }
          sizeCell.textContent = sizeLabel;
          sizeCell.title = `${countLabel}${bytes != null && bytes >= 0 ? " • " + sizeLabel : ""}`;
        }
      }

      if (createdCellIndex >= 0) {
        const createdCell = tr.cells[createdCellIndex];
        if (createdCell) {
          const txt = (stats && typeof stats.earliest_uploaded === 'string')
            ? stats.earliest_uploaded
            : '';
          createdCell.textContent = txt;
        }
      }
      
      if (modifiedCellIndex >= 0) {
        const modCell = tr.cells[modifiedCellIndex];
        if (modCell) {
          const txt = (stats && typeof stats.latest_mtime === 'string')
            ? stats.latest_mtime
            : '';
          modCell.textContent = txt;
        }
      }
    }).catch(() => {
      if (sizeCellIndex >= 0) {
        const sizeCell = tr.cells[sizeCellIndex];
        if (sizeCell && !sizeCell.textContent) sizeCell.textContent = "—";
      }
    });
  
    // OWNER + action permissions
    if (uploaderIdx >= 0 || actionsIdx >= 0) {
      fetchFolderCaps(sf.full, rowSourceId).then(caps => {
        if (!caps || !document.body.contains(tr)) return;
  
        if (uploaderIdx >= 0 && uploaderIdx < tr.cells.length) {
          const uploaderCell = tr.cells[uploaderIdx];
          if (uploaderCell) {
            const owner = caps.owner || caps.user || "";
            uploaderCell.textContent = owner || "";
          }
        }
  
        if (actionsIdx >= 0 && actionsIdx < tr.cells.length) {
          const actCell = tr.cells[actionsIdx];
          if (!actCell) return;
  
          actCell.querySelectorAll('button[data-folder-action]').forEach(btn => {
            const action = btn.getAttribute('data-folder-action');
            let enabled = false;
            switch (action) {
              case "move":
                enabled = !!caps.canMoveFolder;
                break;
              case "color":
                enabled = !!caps.canRename;          // same gate as tree “color” button
                break;
              case "rename":
                enabled = !!caps.canRename;
                break;
              case "share":
                enabled = !!caps.canShareFolder;
                break;
            }
            if (enabled === undefined) {
              enabled = true; // fallback so admin still gets buttons even if a flag is missing
            }
            if (enabled) {
              btn.disabled = false;
              btn.style.pointerEvents = "";
              btn.style.opacity = "";
            } else {
              btn.disabled = true;
              btn.style.pointerEvents = "none";
              btn.style.opacity = "0.5";
            }
          });
        }
      }).catch(() => { /* ignore */ });
    }
  });
  syncFolderIconSizeToRowHeight();
}
function syncFolderIconSizeToRowHeight() {
  const cs   = getComputedStyle(document.documentElement);
  const raw  = cs.getPropertyValue('--file-row-height') || '44px';
  const rowH = parseInt(raw, 10) || 60;

  const FUDGE          = 1;
  const MAX_GROWTH_ROW = 44;   // after this, stop growing the icon

  const BASE_ROW_FOR_OFFSET = 20; // where icon looks centered
  const OFFSET_FACTOR       = -0.10;
  const effectiveRow = Math.min(rowH, MAX_GROWTH_ROW);

  const boxSize = Math.max(20, Math.min(35, effectiveRow - 20 + FUDGE));
  const scale   = 1.20;

  // use existing offset curve
  const clampedForOffset = Math.max(30, Math.min(60, rowH));
  let offsetY = (clampedForOffset - BASE_ROW_FOR_OFFSET) * OFFSET_FACTOR;
  if (rowH > 53) {
    offsetY -= -2; 
  }

  document.querySelectorAll(':is(#fileList, #fileListSecondary) .folder-row-icon').forEach(iconSpan => {
    iconSpan.style.width    = boxSize + 'px';
    iconSpan.style.height   = boxSize + 'px';
    iconSpan.style.overflow = 'visible';

    const svg = iconSpan.querySelector('svg');
    if (!svg) return;

    svg.setAttribute('width',  String(boxSize));
    svg.setAttribute('height', String(boxSize));
    svg.style.transformOrigin = 'left center';
    svg.style.transform       = `translateY(${offsetY}px) scale(${scale})`;
  });
}

async function sortSubfoldersForCurrentOrder(subfolders) {
  const base = Array.isArray(subfolders) ? [...subfolders] : [];
  if (!base.length) return base;

  const sourceId = getActivePaneSourceId();
  const col = sortOrder?.column || "uploaded";
  const ascending = sortOrder?.ascending !== false;
  const dir = ascending ? 1 : -1;

  // Name sort (A–Z / Z–A)
  if (col === "name") {
    base.sort((a, b) => {
      const n1 = (a.name || "").toLowerCase();
      const n2 = (b.name || "").toLowerCase();
      if (n1 < n2) return -1 * dir;
      if (n1 > n2) return  1 * dir;
      return 0;
    });
    return base;
  }

  // Size sort – use folder stats (bytes)
  if (col === "size" || col === "filesize") {
    const statsList = await Promise.all(
      base.map(sf => fetchFolderStats(sf.full, sourceId).catch(() => null))
    );

    const decorated = base.map((sf, idx) => {
      const stats = statsList[idx];
      let bytes = 0;

      if (stats) {
        const candidates = [
          stats.bytes,
          stats.sizeBytes,
          stats.size,
          stats.totalBytes
        ];
        for (const v of candidates) {
          const n = Number(v);
          if (Number.isFinite(n) && n >= 0) {
            bytes = n;
            break;
          }
        }
      }

      return { sf, bytes };
    });

    decorated.sort((a, b) => {
      if (a.bytes < b.bytes) return -1 * dir;
      if (a.bytes > b.bytes) return  1 * dir;

      // tie-break by name
      const n1 = (a.sf.name || "").toLowerCase();
      const n2 = (b.sf.name || "").toLowerCase();
      if (n1 < n2) return -1 * dir;
      if (n1 > n2) return  1 * dir;
      return 0;
    });

    return decorated.map(d => d.sf);
  }

  // NEW: Created / Uploaded sort – use earliest_uploaded from stats
  if (col === "uploaded" || col === "created") {
    const statsList = await Promise.all(
      base.map(sf => fetchFolderStats(sf.full, sourceId).catch(() => null))
    );

    const decorated = base.map((sf, idx) => {
      const stats = statsList[idx];
      let ts = 0;

      if (stats && typeof stats.earliest_uploaded === "string") {
        ts = parseCustomDate(String(stats.earliest_uploaded));
        if (!Number.isFinite(ts)) ts = 0;
      }

      return { sf, ts };
    });

    decorated.sort((a, b) => {
      if (a.ts < b.ts) return -1 * dir;
      if (a.ts > b.ts) return  1 * dir;

      // tie-break by name
      const n1 = (a.sf.name || "").toLowerCase();
      const n2 = (b.sf.name || "").toLowerCase();
      if (n1 < n2) return -1 * dir;
      if (n1 > n2) return  1 * dir;
      return 0;
    });

    return decorated.map(d => d.sf);
  }

  // NEW: Modified sort – use latest_mtime from stats
  if (col === "modified") {
    const statsList = await Promise.all(
      base.map(sf => fetchFolderStats(sf.full, sourceId).catch(() => null))
    );

    const decorated = base.map((sf, idx) => {
      const stats = statsList[idx];
      let ts = 0;

      if (stats && typeof stats.latest_mtime === "string") {
        ts = parseCustomDate(String(stats.latest_mtime));
        if (!Number.isFinite(ts)) ts = 0;
      }

      return { sf, ts };
    });

    decorated.sort((a, b) => {
      if (a.ts < b.ts) return -1 * dir;
      if (a.ts > b.ts) return  1 * dir;

      // tie-break by name
      const n1 = (a.sf.name || "").toLowerCase();
      const n2 = (b.sf.name || "").toLowerCase();
      if (n1 < n2) return -1 * dir;
      if (n1 > n2) return  1 * dir;
      return 0;
    });

    return decorated.map(d => d.sf);
  }

  // Default: keep folders A–Z by name regardless of other sorts
  base.sort((a, b) =>
    (a.name || "").localeCompare(b.name || "", undefined, { sensitivity: "base" })
  );
  return base;
}

async function openDefaultFileFromHover(file) {
  if (!file) return;
  const folder = file.folder || window.currentFolder || "root";
  const sourceId = String(file.sourceId || '').trim() || getActivePaneSourceId();

  try {
    if (canEditFile(file.name) && file.editable) {
      const m = await import('./fileEditor.js?v={{APP_QVER}}');
      m.editFile(file.name, folder, sourceId, file.sizeBytes);
    } else {
      const url = apiFileUrl(folder, file.name, true, sourceId);
      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
 */


function getSelectedFileValuesForList(listEl) {
  if (!listEl) return [];
  return Array.from(listEl.querySelectorAll('tbody .file-checkbox:checked'))
    .map(cb => cb.value);
}

function restoreSelectedFileValues(listEl, selectedValues) {
  if (!listEl || !Array.isArray(selectedValues) || !selectedValues.length) return;
  const selectedSet = new Set(selectedValues.map(v => String(v)));
  listEl.querySelectorAll('tbody .file-checkbox').forEach(cb => {
    const raw = cb.value;
    const decoded = decodeHtmlEntities(raw);
    if (selectedSet.has(raw) || (decoded && selectedSet.has(decoded))) {
      cb.checked = true;
      updateRowHighlight(cb);
    }
  });
}

export async function renderFileTable(folder, container, subfolders, options = {}) {
  clearInlineRenameState({ restore: false });
  const fileListContent = container || document.getElementById("fileList");
  const preserveSelection = options && options.preserveSelection === true;
  let preservedSelection = [];
  const searchTerm = (window.currentSearchTerm || "").toLowerCase();
  const itemsPerPageSetting = parseInt(localStorage.getItem("itemsPerPage") || "50", 10);
  const tablePane = getPaneKeyForElement(fileListContent);
  const panePaging = getPaneFileListPaging(tablePane);
  const serverPagingActive = !!(panePaging && searchTerm === "" && !window.advancedSearchEnabled);
  let currentPage = serverPagingActive
    ? (Number(panePaging.page) || 1)
    : (window.currentPage || 1);
  const targetSelection = (pendingSearchSelection && pendingSearchSelection.folder === folder)
    ? pendingSearchSelection
    : null;

  // Files (filtered by search)
  let filteredFiles = searchFiles(searchTerm);

  // Apply current sort (Modified desc by default for you)
  if (!serverPagingActive && Array.isArray(filteredFiles) && filteredFiles.length) {
    filteredFiles = [...filteredFiles].sort(compareFilesForSort);
  }

  // Inline folders: sort once (Explorer-style A→Z)
  const subfoldersSourceId = String(window.currentSubfoldersSourceId || '');
  const subfoldersFolder = String(window.currentSubfoldersFolder || '');
  const paneSourceId = String(getPaneSourceIdForElement(fileListContent) || getActivePaneSourceId() || '');
  const subfoldersMatch = subfoldersSourceId === paneSourceId && subfoldersFolder === String(folder || '');
  const allSubfolders = (subfoldersMatch && Array.isArray(window.currentSubfolders))
    ? window.currentSubfolders
    : [];

// NEW: sort folders according to current sort order (name / size)
const subfoldersSorted = await sortSubfoldersForCurrentOrder(allSubfolders);

  if (preserveSelection) {
    // Capture after async work so user selections during the wait aren't lost.
    preservedSelection = getSelectedFileValuesForList(fileListContent);
  }

  const totalFiles   = filteredFiles.length;
  const totalFolders = subfoldersSorted.length;
  const totalRows    = totalFiles + totalFolders;
  const hasFolders   = totalFolders > 0;
  const folderPagingInServerMode =
    serverPagingActive &&
    window.showInlineFolders !== false &&
    hasFolders;
  let totalPages = 1;
  let pageFolders = [];
  let pageFiles = [];

  if (serverPagingActive) {
    const totalFilesKnown = Number.isFinite(Number(panePaging.total))
      ? Math.max(0, Number(panePaging.total))
      : Math.max(0, totalFiles);

    if (folderPagingInServerMode) {
      const currentCursor = String((panePaging && panePaging.cursor != null) ? panePaging.cursor : '');
      const cursorOffset = parseCursorOffset(currentCursor);
      const inferredPage = Math.floor((totalFolders + cursorOffset) / itemsPerPageSetting) + 1;
      const preferredPageRaw = Number(window.currentPage || inferredPage);
      const preferredPage = Number.isFinite(preferredPageRaw) ? preferredPageRaw : inferredPage;
      const layout = getCombinedPageLayout({
        totalFolders,
        totalFiles: totalFilesKnown,
        itemsPerPage: itemsPerPageSetting,
        targetPage: preferredPage
      });

      totalPages = layout.totalPages;
      currentPage = layout.page;
      pageFolders = subfoldersSorted.slice(layout.folderStart, layout.folderStart + layout.folderCount);

      if (layout.fileCount > 0) {
        const loadedCount = Math.max(0, Array.isArray(filteredFiles) ? filteredFiles.length : 0);
        const rangeStart = layout.fileStart;
        const rangeEnd = layout.fileStart + layout.fileCount;
        const loadedEnd = cursorOffset + loadedCount;
        const hasRange = cursorOffset <= rangeStart && loadedEnd >= rangeEnd;

        if (!hasRange) {
          const targetCursor = String(rangeStart);
          if (targetCursor !== currentCursor) {
            window.currentPage = currentPage;
            savePaneState(tablePane, {
              currentPage,
              currentSearchTerm: window.currentSearchTerm || ''
            });
            loadFileList(folder, { pane: tablePane, cursor: targetCursor });
            return;
          }
        }

        const localStart = Math.max(0, rangeStart - cursorOffset);
        pageFiles = filteredFiles.slice(localStart, localStart + layout.fileCount);
      } else {
        pageFiles = [];
      }
    } else {
      const pagingTotalPages = Number.isFinite(Number(panePaging.totalPages)) ? Number(panePaging.totalPages) : 1;
      const pagingPage = Number.isFinite(Number(panePaging.page)) ? Number(panePaging.page) : currentPage;
      const fileTotalPages = Math.max(1, pagingTotalPages || (panePaging.hasMore ? pagingPage + 1 : pagingPage) || 1);
      const filePage = Math.max(1, Math.min(fileTotalPages, pagingPage || 1));
      totalPages = fileTotalPages;
      currentPage = Math.max(1, Math.min(totalPages, filePage || 1));
      // Legacy server-paging behavior: keep folders visible above each file page.
      pageFolders = subfoldersSorted;
      pageFiles = filteredFiles;
    }

    window.currentPage = currentPage;
  } else {
    // If we have a pending search target, jump to the page that contains it
    if (targetSelection && totalFiles > 0) {
      const idx = filteredFiles.findIndex(f => f.name === targetSelection.name);
      if (idx >= 0) {
        const targetPage = Math.floor(idx / itemsPerPageSetting) + 1;
        if (targetPage !== currentPage) {
          currentPage = targetPage;
          window.currentPage = currentPage;
        }
      }
    }

    // Pagination is now over (folders + files)
    totalPages = totalRows > 0
      ? Math.ceil(totalRows / itemsPerPageSetting)
      : 1;

    if (currentPage > totalPages) {
      currentPage = totalPages;
      window.currentPage = currentPage;
    }

    const startRow = (currentPage - 1) * itemsPerPageSetting;
    const endRow = Math.min(startRow + itemsPerPageSetting, totalRows);

    // Figure out which folders + files belong to THIS page
    for (let rowIndex = startRow; rowIndex < endRow; rowIndex++) {
      if (rowIndex < totalFolders) {
        pageFolders.push(subfoldersSorted[rowIndex]);
      } else {
        const fileIdx = rowIndex - totalFolders;
        const file = filteredFiles[fileIdx];
        if (file) pageFiles.push(file);
      }
    }
  }

  // Keep pane-local pagination state in sync so click-capture pane activation
  // does not restore a stale page before pagination handlers run.
  savePaneState(tablePane, {
    currentPage,
    currentSearchTerm: window.currentSearchTerm || ''
  });

  // Stable id per file row on this page
  const rowIdFor = (file, idx) =>
    `${encodeURIComponent(file.name)}-p${currentPage}-${idx}`;

  // 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);

  headerHTML = headerHTML.replace(/<table([^>]*)>/i, (full, attrs) => {
    // If table already has class="", append filr-table. Otherwise add class attribute.
    if (/class\s*=\s*"/i.test(attrs)) {
      return full.replace(/class="([^"]*)"/i, (m, cls) => `class="${cls} filr-table"`);
    }
    return `<table class="filr-table"${attrs}>`;
  });

    let rowsHTML = "<tbody>";

  if (pageFiles.length > 0) {
    pageFiles.forEach((file, idx) => {
      const rowKey = rowIdFor(file, idx);
      let rowHTML = buildFileTableRow(file, fakeBase);

      // add row id + data-file-name, and ensure the name cell also has "name-cell"
      rowHTML = rowHTML
        .replace("<tr", `<tr id="file-row-${rowKey}" data-file-name="${escapeHTML(file.name)}"`)
        .replace('class="file-name-cell"', 'class="file-name-cell name-cell"');

      let tagBadgesHTML = "";
      if (file.tags && file.tags.length > 0) {
        tagBadgesHTML = '<div class="tag-badges" style="display:inline-block; margin-left:5px;">';
        file.tags.forEach(tag => {
          const safeColor = sanitizeTagColor(tag.color);
          tagBadgesHTML += `<span style="background-color: ${safeColor}; color: #fff; padding: 2px 4px; border-radius: 3px; margin-right: 2px; font-size: 0.8em;">${escapeHTML(tag.name)}</span>`;
        });
        tagBadgesHTML += "</div>";
      }

      rowsHTML += rowHTML.replace(
        /(<td\s+class="[^"]*\bfile-name-cell\b[^"]*">)([\s\S]*?)(<\/td>)/,
        (m, open, inner, close) => {
          return `${open}<span class="filename-text">${inner}</span>${tagBadgesHTML}${close}`;
        }
      );
    });
  } else if (!hasFolders && totalFiles === 0) {
    // Only show "No files found" if there are no folders either
    rowsHTML += `<tr><td colspan="8">${t("no_files_found") || "No files found."}</td></tr>`;
  }

  rowsHTML += "</tbody></table>";

  const bottomControlsHTML = buildBottomControls(itemsPerPageSetting);

  fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML;
  updatePaneWidthClasses();
  restoreSelectedFileValues(fileListContent, preservedSelection);

  (function rightAlignSizeColumn() {
    const table = fileListContent.querySelector("table.filr-table");
    if (!table || !table.tHead || !table.tBodies.length) return;
  
    const headerCells = Array.from(table.tHead.querySelectorAll("th"));
    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;
  
    // Header
    headerCells[sizeIdx].style.textAlign = "right";
  
    // Body cells
    Array.from(table.tBodies[0].rows).forEach(row => {
      if (sizeIdx >= row.cells.length) return;
      row.cells[sizeIdx].style.textAlign = "right";
      row.cells[sizeIdx].style.fontVariantNumeric = "tabular-nums";
    });
  })();

  // ---- MOBILE FIX: show "Size" column for files (Name | Size | Actions) ----
  (function fixMobileFileSizeColumn() {
    const mode = getPaneLayoutMode(fileListContent);
    if (mode !== "narrow") 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);
}

  // Right-align meta columns: created / modified / owner
  (function rightAlignMetaColumns() {
    const table = fileListContent.querySelector("table.filr-table");
    if (!table || !table.tHead || !table.tBodies.length) return;

    const headerCells = Array.from(table.tHead.querySelectorAll("th"));
    const bodyRows = Array.from(table.tBodies[0].rows);

    function alignCol(matchFn, numeric = true) {
      const idx = headerCells.findIndex(matchFn);
      if (idx < 0) return;

      const th = headerCells[idx];
      th.style.textAlign = "right";

      bodyRows.forEach(row => {
        if (idx >= row.cells.length) return;
        const td = row.cells[idx];
        if (!td) return;
        td.style.textAlign = "right";
        if (numeric) {
          td.style.fontVariantNumeric = "tabular-nums";
        }
      });
    }

    // Uploaded / Created
    alignCol(th =>
      (th.dataset && (th.dataset.column === "uploaded" || th.dataset.column === "created")) ||
      /\b(uploaded|created)\b/i.test((th.textContent || "").trim())
    );

    // Modified
    alignCol(th =>
      (th.dataset && th.dataset.column === "modified") ||
      /\bmodified\b/i.test((th.textContent || "").trim())
    );

    // Owner / Uploader
    alignCol(th =>
      (th.dataset && th.dataset.column === "uploader") ||
      /\b(owner|uploader)\b/i.test((th.textContent || "").trim()),
      /* numeric = */ false   // names aren't numbers, but right-align anyway
    );
  })();

// Now wire 3-dot ellipsis so it also picks up folder rows
wireEllipsisContextMenu(fileListContent);

// Hover preview (hover-capable pointer devices only, and only if user didn’t disable it)
if (!isHoverPreviewDisabled()) {
  fileListContent.querySelectorAll("tbody tr").forEach(row => {
    if (row.classList.contains("folder-strip-row")) return;

    row.addEventListener("mouseenter", (e) => {
      hoverPreviewActiveRow = row;
      clearTimeout(hoverPreviewTimer);
      const delayMs = getHoverPreviewDelayMs();
      hoverPreviewTimer = setTimeout(() => {
        if (hoverPreviewActiveRow === row && !isHoverPreviewDisabled()) {
          fillHoverPreviewForRow(row);
          const el = ensureHoverPreviewEl();
          el.style.display = "block";
          positionHoverPreview(e.clientX, e.clientY);
        }
      }, delayMs);
    });

    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
    if (pageFiles.length > 0) {
      pageFiles.forEach((file, idx) => {
        const rowKey = rowIdFor(file, idx);
        const rowEl = document.getElementById(`file-row-${rowKey}`);
        if (!rowEl) return;
  
        const rowSourceId = String(file.sourceId || paneSourceId || '').trim();
        const previewUrl = apiFileUrl(file.folder || folder, file.name, true, rowSourceId);
  
        // 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', () => {
      const dest = String(el.dataset.folder || '').trim();
      if (!dest) return;
      setCurrentFolderContext(dest, { pane: tablePane, resetPage: true });
      loadFileList(dest, { pane: tablePane });
    });
  });

  const goToCombinedServerPage = (targetPage) => {
    const totalFilesKnown = Number.isFinite(Number(panePaging?.total))
      ? Math.max(0, Number(panePaging.total))
      : Math.max(0, totalFiles);
    const layout = getCombinedPageLayout({
      totalFolders,
      totalFiles: totalFilesKnown,
      itemsPerPage: itemsPerPageSetting,
      targetPage
    });

    window.currentPage = layout.page;
    savePaneState(tablePane, {
      currentPage: window.currentPage,
      currentSearchTerm: window.currentSearchTerm || ''
    });

    if (layout.fileCount <= 0) {
      renderFileTable(folder, fileListContent);
      return;
    }

    const targetCursor = String(layout.fileStart);
    const currentCursor = String((panePaging && panePaging.cursor != null) ? panePaging.cursor : '');
    const cursorOffset = parseCursorOffset(currentCursor);
    const loadedCount = Math.max(0, Array.isArray(filteredFiles) ? filteredFiles.length : 0);
    const loadedEnd = cursorOffset + loadedCount;
    const requiredEnd = layout.fileStart + layout.fileCount;
    const hasRange = cursorOffset <= layout.fileStart && loadedEnd >= requiredEnd;

    if (currentCursor === targetCursor && hasRange) {
      renderFileTable(folder, fileListContent);
      return;
    }

    loadFileList(folder, { pane: tablePane, cursor: targetCursor });
  };

  // pagination clicks
  const prevBtn = document.getElementById("prevPageBtn");
  if (prevBtn) prevBtn.addEventListener("click", () => {
    if (serverPagingActive) {
      if (folderPagingInServerMode) {
        const virtualPage = Math.max(1, Number(window.currentPage || currentPage || 1));
        if (virtualPage > 1) {
          goToCombinedServerPage(virtualPage - 1);
        }
        return;
      }

      const prevCursor = panePaging && panePaging.prevCursor != null
        ? String(panePaging.prevCursor)
        : '';
      if (prevCursor !== '') {
        loadFileList(folder, { pane: tablePane, cursor: prevCursor });
      }
      return;
    }
    if (window.currentPage > 1) {
      window.currentPage--;
      renderFileTable(folder, container);
    }
  });
  const nextBtn = document.getElementById("nextPageBtn");
  if (nextBtn) nextBtn.addEventListener("click", () => {
    if (serverPagingActive) {
      if (folderPagingInServerMode) {
        const virtualPage = Math.max(1, Number(window.currentPage || currentPage || 1));
        if (virtualPage < totalPages) {
          goToCombinedServerPage(virtualPage + 1);
        }
        return;
      }

      const nextCursor = panePaging && panePaging.nextCursor != null
        ? String(panePaging.nextCursor)
        : '';
      if (nextCursor !== '') {
        loadFileList(folder, { pane: tablePane, cursor: nextCursor });
      }
      return;
    }
    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;
    if (serverPagingActive) {
      loadFileList(folder, { pane: tablePane, cursor: "" });
      return;
    }
    renderFileTable(folder, container);
  });

  // Row-select (only file rows have checkboxes; folder rows are ignored here)
  fileListContent.querySelectorAll("tbody tr").forEach(row => {
    row.addEventListener("click", e => {
      clearFolderSelections();
      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?v={{APP_QVER}}');
      const rawSize = btn.dataset.editSizeBytes;
      const sizeNum = (rawSize === undefined || rawSize === '') ? NaN : Number(rawSize);
      const sizeArg = Number.isFinite(sizeNum) ? sizeNum : null;
      m.editFile(btn.dataset.editName, btn.dataset.editFolder, paneSourceId, sizeArg);
    });
  });

  // Rename buttons
  fileListContent.querySelectorAll(".rename-btn").forEach(btn => {
    btn.addEventListener("click", async e => {
      e.stopPropagation();
      const m = await import('./fileActions.js?v={{APP_QVER}}');
      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?v={{APP_QVER}}');
      m.previewFile(btn.dataset.previewUrl, btn.dataset.previewName);
    });
  });

  createViewToggleButton();
  ensureSearchEverywhereButton();
  bindViewOptionsButton();

  // search input
  const newSearchInput = document.getElementById("searchInput");
  if (newSearchInput) {
    newSearchInput.addEventListener("input", debounce(function () {
      window.currentSearchTerm = newSearchInput.value;
      window.currentPage = 1;
      savePaneState(tablePane, {
        currentSearchTerm: window.currentSearchTerm || '',
        currentPage: window.currentPage || 1
      });
      const livePaging = getPaneFileListPaging(tablePane);
      const trimmed = String(window.currentSearchTerm || '').trim();
      if (livePaging && trimmed !== '') {
        loadFileList(folder, { pane: tablePane, forceLegacy: true, includeContent: window.advancedSearchEnabled === true });
        return;
      }
      if (!livePaging && trimmed === '' && shouldUseServerFilePagingForRequest({})) {
        loadFileList(folder, { pane: tablePane });
        return;
      }
      renderFileTable(folder, container);
      setTimeout(() => {
        const freshInput = document.getElementById("searchInput");
        if (freshInput) {
          freshInput.focus();
          const len = freshInput.value.length;
          freshInput.setSelectionRange(len, len);
        }
      }, 0);
    }, 300));
  }

  document.querySelectorAll("#fileList table.filr-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) {
      clearFolderSelections();
      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?v={{APP_QVER}}').then(module => {
          module.openShareModal(file, folder);
        });
      }
    });
  });
  bindFolderToolbarActions();
  updateFileActionButtons();
  maybeHighlightSearchedFile(folder);

  // Dragstart only for file rows (skip folder rows)
  document.querySelectorAll("#fileList tbody tr").forEach(row => {
    if (row.classList.contains("folder-row")) return;
    row.setAttribute("draggable", "true");
    import('./fileDragDrop.js?v={{APP_QVER}}').then(module => {
      row.addEventListener("dragstart", module.fileDragStartHandler);
    });
  });

  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(() => { });
}

// 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) {
  clearInlineRenameState({ restore: false });
  const fileListContent = container || document.getElementById("fileList");
  const galleryPane = getPaneKeyForElement(fileListContent);
  const panePaging = getPaneFileListPaging(galleryPane);
  const paneSourceId = String(getPaneSourceIdForElement(fileListContent) || getActivePaneSourceId() || '');
  const searchTerm = (window.currentSearchTerm || "").toLowerCase();
  const serverPagingActive = !!(panePaging && searchTerm === "" && !window.advancedSearchEnabled);
  let filteredFiles = searchFiles(searchTerm);

  if (!serverPagingActive && Array.isArray(filteredFiles) && filteredFiles.length) {
    filteredFiles = [...filteredFiles].sort(compareFilesForSort);
  }

  // pagination settings
  const itemsPerPage = window.itemsPerPage;
  let currentPage = serverPagingActive
    ? (Number(panePaging.page) || 1)
    : (window.currentPage || 1);
  const totalFiles = filteredFiles.length;
  let totalPages = Math.ceil(totalFiles / itemsPerPage);
  if (serverPagingActive) {
    const pagingTotalPages = Number.isFinite(Number(panePaging.totalPages)) ? Number(panePaging.totalPages) : 1;
    totalPages = Math.max(1, pagingTotalPages || totalPages || 1);
    currentPage = Math.max(1, Math.min(totalPages, Number(panePaging.page) || currentPage));
    window.currentPage = currentPage;
  } else if (currentPage > totalPages) {
    currentPage = totalPages || 1;
    window.currentPage = currentPage;
  }
  // Keep pane-local pagination state in sync for gallery mode as well.
  savePaneState(galleryPane, {
    currentPage,
    currentSearchTerm: window.currentSearchTerm || ''
  });

  // --- 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;
        savePaneState(galleryPane, {
          currentSearchTerm: window.currentSearchTerm || '',
          currentPage: window.currentPage || 1
        });
        const livePaging = getPaneFileListPaging(galleryPane);
        const trimmed = String(window.currentSearchTerm || '').trim();
        if (livePaging && trimmed !== '') {
          loadFileList(folder, { pane: galleryPane, forceLegacy: true, includeContent: window.advancedSearchEnabled === true });
          return;
        }
        if (!livePaging && trimmed === '' && shouldUseServerFilePagingForRequest({})) {
          loadFileList(folder, { pane: galleryPane });
          return;
        }
        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 maxCols = getGalleryMaxColumns();
  const startCols = Math.min(window.galleryColumns || 3, maxCols);
  window.galleryColumns = startCols;

  // --- Start gallery grid ---
  galleryHTML += `
      <div class="gallery-container"
           style="display:grid;
                  grid-template-columns:repeat(${startCols},minmax(0,1fr));
                  gap:10px;
                  padding:10px;">
    `;

  // slice current page
  const startIdx = serverPagingActive ? 0 : (currentPage - 1) * itemsPerPage;
  const pageFiles = serverPagingActive
    ? filteredFiles
    : filteredFiles.slice(startIdx, startIdx + itemsPerPage);
  const maxImagePreviewBytes = getMaxImagePreviewBytes();
  const maxVideoPreviewBytes = getMaxVideoPreviewBytes();

  pageFiles.forEach((file, idx) => {
    const idSafe = encodeURIComponent(file.name) + "-" + (startIdx + idx);

    const sourceId = String(file.sourceId || paneSourceId || '').trim();
    const previewURL = apiFileUrl(folder, file.name, true, sourceId);

    // thumbnail
    let thumbnail;
    if (/\.(jpe?g|png|gif|bmp|webp|ico)$/i.test(file.name)) {
      const bytes = Number.isFinite(file.sizeBytes) ? file.sizeBytes : null;
      const tooBig = bytes != null && bytes > maxImagePreviewBytes;
      if (tooBig) {
        const sizeLabel = bytes != null ? formatSize(bytes) : '';
        thumbnail = `
          <div class="gallery-placeholder" style="
            display:flex;
            align-items:center;
            justify-content:center;
            min-height:${getMaxImageHeight()}px;
            padding:10px;
            border-radius:8px;
            background:rgba(0,0,0,0.05);
            color:#444;
            font-size:0.9rem;
            text-align:center;
          ">
            ${escapeHTML(t("preview_too_large") || "Preview disabled for large image")}
            ${sizeLabel ? `<div style="font-size:0.8rem; opacity:0.75; margin-top:4px;">${escapeHTML(sizeLabel)}</div>` : ""}
          </div>
        `;
      } else {
        const cacheKey = previewURL; // include folder & file
        const src = (window.imageCache && window.imageCache[cacheKey]) || previewURL;
        thumbnail = `<img
            src="${src}"
            class="gallery-thumbnail"
            data-cache-key="${cacheKey}"
            alt="${escapeHTML(file.name)}"
            loading="lazy"
            decoding="async"
            fetchpriority="low"
            style="max-width:100%; max-height:${getMaxImageHeight()}px; display:block; margin:0 auto;">`;
      }
    } else if (/\.(mp4|mkv|webm|mov|ogv)$/i.test(file.name)) {
      const bytes = Number.isFinite(file.sizeBytes) ? file.sizeBytes : null;
      if (bytes != null && bytes > maxVideoPreviewBytes) {
        thumbnail = `<span class="material-icons gallery-icon">movie</span>`;
      } else {
        const maxHeight = getMaxImageHeight();
        const sourceId = String(
          file.sourceId ||
          getPaneSourceIdForElement(container) ||
          getActivePaneSourceId() ||
          ""
        ).trim();
        const thumbUrl = apiMediaThumbUrl(folder, file.name, sourceId);
        const cacheKey = thumbUrl;
        const src = (window.imageCache && window.imageCache[cacheKey]) || thumbUrl;
        thumbnail = `
          <div class="gallery-video-thumb" style="
            position:relative;
            display:flex;
            align-items:center;
            justify-content:center;
            min-height:${maxHeight}px;
            background:rgba(0,0,0,0.04);
            border-radius:8px;
          ">
            <img
              src="${src}"
              class="gallery-thumbnail gallery-video-thumb-img"
              data-cache-key="${cacheKey}"
              alt="${escapeHTML(file.name)}"
              loading="lazy"
              decoding="async"
              fetchpriority="low"
              style="max-width:100%; max-height:${maxHeight}px; display:block; border-radius:8px; pointer-events:none;"
            >
            <span class="material-icons" style="
              position:absolute;
              font-size:32px;
              color:rgba(255,255,255,0.9);
              text-shadow:0 2px 6px rgba(0,0,0,0.5);
              pointer-events:none;
            ">play_circle</span>
          </div>
        `;
      }
    } else if (/\.pdf$/i.test(file.name)) {
      if (!arePdfThumbnailsEnabled()) {
        thumbnail = `<span class="material-icons gallery-icon">picture_as_pdf</span>`;
      } else {
      const sourceId = String(
        file.sourceId ||
        getPaneSourceIdForElement(container) ||
        getActivePaneSourceId() ||
        ""
      ).trim();
      const sourceType = String(getSourceTypeById(sourceId || getActivePaneSourceId()) || '').toLowerCase();
      const isRemoteSource = sourceId !== '' && !isLocalSourceLike(sourceId);
      if (isRemoteSource) {
        thumbnail = `<span class="material-icons gallery-icon">picture_as_pdf</span>`;
      } else {
        const maxHeight = getMaxImageHeight();
        const thumbUrl = apiMediaThumbUrl(folder, file.name, sourceId);
        const cacheKey = thumbUrl;
        const src = (window.imageCache && window.imageCache[cacheKey]) || thumbUrl;
        thumbnail = `
          <div class="gallery-video-thumb" style="
            position:relative;
            display:flex;
            align-items:center;
            justify-content:center;
            min-height:${maxHeight}px;
            background:rgba(0,0,0,0.04);
            border-radius:8px;
          ">
            <img
              src="${src}"
              class="gallery-thumbnail gallery-pdf-thumb-img"
              data-cache-key="${cacheKey}"
              alt="${escapeHTML(file.name)}"
              loading="lazy"
              decoding="async"
              fetchpriority="low"
              style="max-width:100%; max-height:${maxHeight}px; display:block; border-radius:8px; pointer-events:none;"
            >
            <span class="material-icons" style="
              position:absolute;
              font-size:32px;
              color:rgba(255,255,255,0.92);
              text-shadow:0 2px 6px rgba(0,0,0,0.55);
              pointer-events:none;
            ">picture_as_pdf</span>
          </div>
        `;
      }
      }
    } else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
      thumbnail = `<span class="material-icons gallery-icon">audiotrack</span>`;
    } else {
      thumbnail = `<span class="material-icons gallery-icon">insert_drive_file</span>`;
    }

    // tag badges
    let tagBadgesHTML = "";
    if (file.tags && file.tags.length) {
      tagBadgesHTML = `<div class="tag-badges" style="margin-top:4px;">`;
      file.tags.forEach(tag => {
        const safeColor = sanitizeTagColor(tag.color);
        tagBadgesHTML += `<span style="background-color:${safeColor};
                                     color:#fff;
                                     padding:2px 4px;
                                     border-radius:3px;
                                     margin-right:2px;
                                     font-size:0.8em;">
              ${escapeHTML(tag.name)}
            </span>`;
      });
      tagBadgesHTML += `</div>`;
    }

    const bytes = Number.isFinite(file.sizeBytes) ? file.sizeBytes : null;
    const sizeLabel = bytes != null ? formatSize(bytes) : "";
    const sizeHTML = sizeLabel
      ? `<span class="gallery-file-size" style="display:block; font-size:0.8rem; opacity:0.75; font-variant-numeric:tabular-nums;">
          ${escapeHTML(sizeLabel)}
        </span>`
      : "";

    // card with checkbox, preview, info, buttons
    galleryHTML += `
        <div class="gallery-card"
             data-file-name="${escapeHTML(file.name)}"
             style="position:relative; border-radius:12px; border:1px solid #ccc; padding:5px; text-align:center; width:100%; min-width:0; box-sizing:border-box;">
          <input type="checkbox"
                 class="file-checkbox"
                 id="cb-${idSafe}"
                 value="${escapeHTML(file.name)}"
                 style="position:absolute; top:5px; left:5px; z-index:10;">
          <label for="cb-${idSafe}"
                 style="position:absolute; top:5px; left:5px; width:16px; height:16px;"></label>
  
          <div class="gallery-preview" style="cursor:pointer;"
               data-preview-url="${previewURL}"
               data-preview-name="${escapeHTML(file.name)}">
            ${thumbnail}
          </div>
  
          <div class="gallery-info" style="margin-top:5px; min-width:0;">
            <span class="gallery-file-name"
                  style="display:block; max-width:100%; white-space:normal; overflow-wrap:anywhere; word-break:break-word;">
              ${escapeHTML(file.name)}
            </span>
            ${sizeHTML}
            ${tagBadgesHTML}
  
            <div class="btn-group btn-group-sm btn-group-hover" role="group" aria-label="File actions" style="margin-top:5px;">
              <button
                type="button"
                class="btn btn-success py-1 download-btn"
                data-download-name="${escapeHTML(file.name)}"
                data-download-folder="${file.folder || "root"}"
                title="${t('download')}"
              >
                <i class="material-icons">file_download</i>
              </button>
  
              ${file.editable ? `
              <button
                type="button"
                class="btn btn-secondary py-1 edit-btn"
                data-edit-name="${escapeHTML(file.name)}"
                data-edit-folder="${file.folder || "root"}"
                data-edit-size-bytes="${bytes != null ? String(bytes) : ''}"
                title="${t('edit')}"
              >
                <i class="material-icons">edit</i>
              </button>` : ""}
  
              <button
                type="button"
                class="btn btn-warning py-1 rename-btn"
                data-rename-name="${escapeHTML(file.name)}"
                data-rename-folder="${file.folder || "root"}"
                title="${t('rename')}"
              >
                <i class="material-icons">drive_file_rename_outline</i>
              </button>
  
              <button
                type="button"
                class="btn btn-secondary py-1 share-btn"
                data-file="${escapeHTML(file.name)}"
                title="${t('share')}"
              >
                <i class="material-icons">share</i>
              </button>
            </div>
          </div>
        </div>
      `;
  });

  galleryHTML += `</div>`; // 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 (serverPagingActive) {
      const prevCursor = panePaging && panePaging.prevCursor != null
        ? String(panePaging.prevCursor)
        : '';
      if (prevCursor !== '') {
        loadFileList(folder, { pane: galleryPane, cursor: prevCursor });
      }
      return;
    }
    if (window.currentPage > 1) {
      window.currentPage--;
      renderGalleryView(folder, container);
    }
  });
  const nextBtn = document.getElementById("nextPageBtn");
  if (nextBtn) nextBtn.addEventListener("click", () => {
    if (serverPagingActive) {
      const nextCursor = panePaging && panePaging.nextCursor != null
        ? String(panePaging.nextCursor)
        : '';
      if (nextCursor !== '') {
        loadFileList(folder, { pane: galleryPane, cursor: nextCursor });
      }
      return;
    }
    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;
    if (serverPagingActive) {
      loadFileList(folder, { pane: galleryPane, cursor: "" });
      return;
    }
    renderGalleryView(folder, container);
  });

  // cache images on load
  fileListContent.querySelectorAll('.gallery-thumbnail').forEach(img => {
    const key = img.dataset.cacheKey;
    img.addEventListener('load', () => cacheImage(img, key));
  });

  // video thumbnail fallback (ffmpeg may be unavailable)
  fileListContent.querySelectorAll('.gallery-video-thumb-img').forEach(img => {
    img.addEventListener('error', () => {
      const wrapper = img.closest('.gallery-video-thumb');
      if (wrapper) wrapper.innerHTML = `<span class="material-icons gallery-icon">movie</span>`;
    }, { once: true });
  });
  fileListContent.querySelectorAll('.gallery-pdf-thumb-img').forEach(img => {
    img.addEventListener('error', () => {
      const wrapper = img.closest('.gallery-video-thumb');
      if (wrapper) wrapper.innerHTML = `<span class="material-icons gallery-icon">picture_as_pdf</span>`;
    }, { once: true });
  });

  // preview clicks (dynamic import to avoid global dependency)
  fileListContent.querySelectorAll(".gallery-preview").forEach(el => {
    el.addEventListener("click", async () => {
      const m = await import('./filePreview.js?v={{APP_QVER}}');
      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?v={{APP_QVER}}');
      const rawSize = btn.dataset.editSizeBytes;
      const sizeNum = (rawSize === undefined || rawSize === '') ? NaN : Number(rawSize);
      const sizeArg = Number.isFinite(sizeNum) ? sizeNum : null;
      m.editFile(btn.dataset.editName, btn.dataset.editFolder, paneSourceId, sizeArg);
    });
  });

  // rename clicks
  fileListContent.querySelectorAll(".rename-btn").forEach(btn => {
    btn.addEventListener("click", async e => {
      e.stopPropagation();
      const m = await import('./fileActions.js?v={{APP_QVER}}');
      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?v={{APP_QVER}}').then(m => m.openShareModal(fileObj, folder));
      }
    });
  });

  // checkboxes
  fileListContent.querySelectorAll(".file-checkbox").forEach(cb => {
    cb.addEventListener("change", () => updateFileActionButtons());
  });

  // pagination helpers
  window.changePage = newPage => {
    if (serverPagingActive) {
      const target = parseInt(newPage, 10);
      if (target === currentPage - 1 && panePaging && panePaging.prevCursor != null) {
        const prevCursor = String(panePaging.prevCursor);
        if (prevCursor !== '') {
          loadFileList(folder, { pane: galleryPane, cursor: prevCursor });
        }
      } else if (target === currentPage + 1 && panePaging && panePaging.nextCursor != null) {
        const nextCursor = String(panePaging.nextCursor);
        if (nextCursor !== '') {
          loadFileList(folder, { pane: galleryPane, cursor: nextCursor });
        }
      }
      return;
    }
    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 (serverPagingActive) {
      loadFileList(folder, { pane: galleryPane, cursor: "" });
      return;
    }
    if (window.viewMode === "gallery") renderGalleryView(folder);
    else renderFileTable(folder);
  };
  refreshViewedBadges(folder).catch(() => { });
  bindFolderToolbarActions();
  updateFileActionButtons();
  createViewToggleButton();
  ensureSearchEverywhereButton();
  bindViewOptionsButton();
  maybeHighlightSearchedFile(folder);
}

/**
 * Fallback: derive selected files from DOM checkboxes if no explicit list
 * of file objects is provided.
 */
function getSelectedFilesForDownload() {
  const checks = Array.from(document.querySelectorAll('#fileList .file-checkbox'));
  if (!checks.length) return [];

  // checkbox values are ESCAPED names
  const selectedEsc = checks.filter(cb => cb.checked).map(cb => cb.value);
  if (!selectedEsc.length) return [];

  const escSet = new Set(selectedEsc);

  const files = Array.isArray(fileData)
    ? fileData.filter(f => escSet.has(escapeHTML(f.name)))
    : [];

  return files.map(f => ({
    folder: f.folder || window.currentFolder || 'root',
    name: f.name
  }));
}

/**
 * Push selected files into a stepper queue and show the
 * bottom-right panel with "Download next / Cancel".
 *
 * Expects `fileObjs` to be an array of file objects from `fileData`
 * (e.g. currentSelection().files in fileMenu.js).
 */
export function downloadSelectedFilesIndividually(fileObjs) {
  const src = Array.isArray(fileObjs) ? fileObjs : [];

  if (!src.length) {
    showToast(t('no_files_selected') || 'No files selected.', 'warning');
    return;
  }

  const mapped = src.map(f => ({
    folder: f.folder || window.currentFolder || 'root',
    name: f.name,
    sourceId: String(f.sourceId || getActivePaneSourceId() || '').trim()
  }));

  const limit = window.maxNonZipDownloads || MAX_NONZIP_MULTI_DOWNLOAD;
  if (mapped.length > limit) {
    const inEncrypted = !!(window.currentFolderCaps && window.currentFolderCaps.encryption && window.currentFolderCaps.encryption.encrypted);
    const msg = inEncrypted
      ? `You selected ${mapped.length} files. In encrypted folders, downloads are limited to ${limit} files at a time.`
      : (t('too_many_plain_downloads', { count: mapped.length, limit }) || `You selected ${mapped.length} files. For more than ${limit} files, please use "Download as Archive".`);
    showToast(msg, 'warning');
    return;
  }

  // Replace any existing queue with the new one.
  window.__nonZipDownloadQueue = mapped.slice();

  // Show the panel; user will click "Download next" for each file.
  showNonZipPanel();

  // auto-fire the first file here:
  triggerNextNonZipDownload();
}

function compareFilesForSort(a, b) {
  const column = sortOrder?.column || "uploaded";
  const ascending = sortOrder?.ascending !== false;

  let valA = a[column] ?? "";
  let valB = b[column] ?? "";

  if (column === "size" || column === "filesize") {
    // numeric size
    valA = Number.isFinite(a.sizeBytes) ? a.sizeBytes : 0;
    valB = Number.isFinite(b.sizeBytes) ? b.sizeBytes : 0;
  } else if (column === "modified" || column === "uploaded") {
    // date sort (newest/oldest)
    const parsedA = parseCustomDate(String(valA || ""));
    const parsedB = parseCustomDate(String(valB || ""));
    valA = parsedA;
    valB = parsedB;
  } else {
    if (typeof valA === "string") valA = valA.toLowerCase();
    if (typeof valB === "string") valB = valB.toLowerCase();
  }

  if (valA < valB) return ascending ? -1 : 1;
  if (valA > valB) return ascending ?  1 : -1;
  return 0;
}

function maybeHighlightSearchedFile(folder) {
  if (!pendingSearchSelection) return;
  const target = pendingSearchSelection;
  if (!target || target.folder !== folder) return;
  const name = target.name;
  if (!name) return;

  const findRow = () => {
    const nodes = document.querySelectorAll('#fileList tr[data-file-name], .gallery-card[data-file-name]');
    for (const row of nodes) {
      const raw = row.getAttribute('data-file-name') || '';
      const decoded = decodeHtmlEntities(raw);
      if (raw === name || decoded === name) return row;
    }
    return null;
  };

  try {
    const row = findRow();
    if (!row) {
      if (typeof target.retries !== 'number') target.retries = 10;
      if (target.retries > 0) {
        target.retries -= 1;
        // Try again shortly in case the DOM finishes rendering
        setTimeout(() => maybeHighlightSearchedFile(folder), 120);
      }
      pendingSearchSelection = target;
      return;
    }

    const alreadySelected = row.classList.contains('row-selected') || row.classList.contains('selected');

    // Select checkbox if present
    const cb = row.querySelector('.file-checkbox');
    if (cb && !alreadySelected) {
      // clear other selections
      document.querySelectorAll('#fileList .file-checkbox').forEach(box => {
        if (box !== cb) {
          box.checked = false;
          updateRowHighlight(box);
        }
      });
      cb.checked = true;
      updateRowHighlight(cb);
      updateFileActionButtons();
    } else if (!cb && !alreadySelected) {
      row.classList.add('row-selected', 'selected');
    }

    if (!alreadySelected) {
      row.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }

    if (!target.clearTimer) {
      const ref = target;
      target.clearTimer = setTimeout(() => {
        if (pendingSearchSelection === ref) {
          pendingSearchSelection = null;
        }
      }, 900);
    }
  } catch (e) {
    // best-effort highlight only; leave selection pending for a future render
  }
}


export function sortFiles(column, folder) {
  if (sortOrder.column === column) {
    sortOrder.ascending = !sortOrder.ascending;
  } else {
    sortOrder.column = column;
    sortOrder.ascending = true;
  }

  const pane = normalizePaneKey(window.activePane);
  const paging = getPaneFileListPaging(pane);
  const hasSearch = String(window.currentSearchTerm || '').trim() !== '';
  if (paging && !hasSearch && !window.advancedSearchEnabled) {
    window.currentPage = 1;
    savePaneState(pane, { currentPage: 1 });
    loadFileList(folder || window.currentFolder || 'root', { pane, cursor: '' });
    return;
  }

  // Re-sort master fileData
  fileData.sort(compareFilesForSort);

  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();

  // Your CodeMirror text-based types
  const textEditExts = new Set([
    "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"
  ]);

  if (textEditExts.has(ext)) return true;            // CodeMirror
  if (OO_ENABLED && OO_EXTS.has(ext)) return true;   // ONLYOFFICE types if enabled
  return false;
}

// Expose global functions for pagination and preview.
window.changePage = function (newPage) {
  const pane = normalizePaneKey(window.activePane);
  const paging = getPaneFileListPaging(pane);
  const target = parseInt(newPage, 10);
  if (paging && Number.isFinite(target)) {
    const folderKey = String(window.currentFolder || 'root');
    if (window.viewMode === 'table' && window.showInlineFolders !== false) {
      const sourceId = String(getPaneSourceId(pane) || getGlobalActiveSourceId() || '');
      const subfolders = getSubfoldersForPagingContext(pane, folderKey, sourceId);
      const totalFolders = Array.isArray(subfolders) ? subfolders.length : 0;
      const totalFiles = Number.isFinite(Number(paging.total)) ? Math.max(0, Number(paging.total)) : 0;
      const layout = getCombinedPageLayout({
        totalFolders,
        totalFiles,
        itemsPerPage: getFileListCursorPageSize(),
        targetPage: target
      });

      window.currentPage = layout.page;
      savePaneState(pane, { currentPage: layout.page });

      if (layout.fileCount <= 0) {
        renderFileTable(folderKey);
        return;
      }

      const targetCursor = String(layout.fileStart);
      const currentCursor = String((paging.cursor == null) ? '' : paging.cursor);
      const cursorOffset = parseCursorOffset(currentCursor);
      const loadedCount = Array.isArray(fileData) ? fileData.length : 0;
      const loadedEnd = cursorOffset + Math.max(0, loadedCount);
      const requiredEnd = layout.fileStart + layout.fileCount;
      const hasRange = cursorOffset <= layout.fileStart && loadedEnd >= requiredEnd;

      if (currentCursor === targetCursor && hasRange) {
        renderFileTable(folderKey);
        return;
      }

      loadFileList(folderKey, { pane, cursor: targetCursor });
      return;
    }

    const pageSize = Number.isFinite(Number(paging.limit))
      ? Math.max(1, Number(paging.limit))
      : getFileListCursorPageSize();
    const totalPages = Number.isFinite(Number(paging.totalPages))
      ? Math.max(1, Number(paging.totalPages))
      : Math.max(1, Math.ceil((Number(paging.total) || 0) / pageSize));
    const clampedTarget = Math.max(1, Math.min(totalPages, target));
    const targetCursor = String(Math.max(0, (clampedTarget - 1) * pageSize));
    const currentCursor = String((paging.cursor == null) ? '' : paging.cursor);
    if (targetCursor === currentCursor) {
      if (window.viewMode === 'gallery') {
        renderGalleryView(folderKey);
      } else {
        renderFileTable(folderKey);
      }
      return;
    }
    loadFileList(folderKey, { pane, cursor: targetCursor });
    return;
  }
  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;
  const pane = normalizePaneKey(window.activePane);
  savePaneState(pane, { currentPage: 1 });
  const paging = getPaneFileListPaging(pane);
  if (paging) {
    loadFileList(window.currentFolder || 'root', { pane, cursor: '' });
    return;
  }
  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;
window.downloadSelectedFilesIndividually = downloadSelectedFilesIndividually;
