Remove User
diff --git a/public/js/appCore.js b/public/js/appCore.js
index a064e8a..3f08084 100644
--- a/public/js/appCore.js
+++ b/public/js/appCore.js
@@ -83,7 +83,7 @@ export async function loadCsrfToken() {
APP INIT (shared)
========================= */
export function initializeApp() {
- const saved = parseInt(localStorage.getItem('rowHeight') || '48', 10);
+ const saved = parseInt(localStorage.getItem('rowHeight') || '44', 10);
document.documentElement.style.setProperty('--file-row-height', saved + 'px');
const last = localStorage.getItem('lastOpenedFolder');
diff --git a/public/js/fileListView.js b/public/js/fileListView.js
index 57cb428..8b15d19 100644
--- a/public/js/fileListView.js
+++ b/public/js/fileListView.js
@@ -37,7 +37,7 @@ import {
} from './fileDragDrop.js?v={{APP_QVER}}';
export let fileData = [];
-export let sortOrder = { column: "uploaded", ascending: true };
+export let sortOrder = { column: "modified", ascending: false };
const FOLDER_STRIP_PAGE_SIZE = 50;
@@ -196,6 +196,13 @@ function renderFolderStripPaged(strip, subfolders) {
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)}"]`);
@@ -265,16 +272,29 @@ async function fillFileSnippet(file, snippetEl) {
if (!res.ok) throw 0;
const text = await res.text();
- const MAX_LINES = 6;
- const MAX_CHARS = 600;
+ const MAX_LINES = 6;
+ const MAX_CHARS_TOTAL = 600;
+ const MAX_LINE_CHARS = 20; // β per-line cap (tweak to taste)
const allLines = text.split(/\r?\n/);
- let visibleLines = allLines.slice(0, MAX_LINES);
- let snippet = visibleLines.join("\n");
- let truncated = allLines.length > MAX_LINES;
- if (snippet.length > MAX_CHARS) {
- snippet = snippet.slice(0, MAX_CHARS);
+ // Take the first few lines and trim each so they don't wrap forever
+ 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");
+
+ // Also enforce an overall character ceiling just in case
+ if (snippet.length > MAX_CHARS_TOTAL) {
+ snippet = snippet.slice(0, MAX_CHARS_TOTAL);
truncated = true;
}
@@ -286,6 +306,7 @@ async function fillFileSnippet(file, snippetEl) {
_fileSnippetCache.set(key, finalSnippet);
snippetEl.textContent = finalSnippet;
+
} catch {
snippetEl.textContent = "";
snippetEl.style.display = "none";
@@ -571,6 +592,13 @@ window.addEventListener('folderColorChanged', (e) => {
// Hide "Edit" for files >10 MiB
const MAX_EDIT_BYTES = 10 * 1024 * 1024;
+// Max number of files allowed for non-ZIP multi-download
+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 = 0;
@@ -812,18 +840,26 @@ function fillHoverPreviewForRow(row) {
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 content
+thumbEl.innerHTML = "";
+propsEl.innerHTML = "";
+snippetEl.textContent = "";
+snippetEl.style.display = "none";
+metaEl.textContent = "";
+titleEl.textContent = "";
- // Reset per-row sizing (we only make this tall for images)
- thumbEl.style.minHeight = "0";
+// 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");
@@ -841,23 +877,61 @@ function fillHoverPreviewForRow(row) {
folder: folderPath
};
- // Right column: icon + path
- const iconHtml = `
+ // Right column: icon + path (start props array so we can append later)
+ const props = [];
+
+ props.push(`
folder
${t("folder") || "Folder"}
- `;
+ `);
- let propsHtml = iconHtml;
- propsHtml += `
+ props.push(`
${t("path") || "Path"}: ${escapeHTML(folderPath || "root")}
- `;
- propsEl.innerHTML = propsHtml;
+ `);
- // Meta: counts + size
+ propsEl.innerHTML = props.join("");
+
+ // --- Owner + "Your access" (from capabilities) --------------------
+ fetchFolderCaps(folderPath).then(caps => {
+ if (!caps || !document.body.contains(el)) return;
+ if (!hoverPreviewContext || hoverPreviewContext.folder !== folderPath) return;
+
+ const owner = caps.owner || caps.user || "";
+ if (owner) {
+ props.push(`
+
+ ${t("owner") || "Owner"}: ${escapeHTML(owner)}
+
+ `);
+ }
+
+ // 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(`
+
+ ${escapeHTML(label)}: ${escapeHTML(perms.join(", "))}
+
+ `);
+ }
+
+ propsEl.innerHTML = props.join("");
+ }).catch(() => {});
+ // ------------------------------------------------------------------
+
+ // --- Meta: counts + size + created/modified -----------------------
fetchFolderStats(folderPath).then(stats => {
if (!stats || !document.body.contains(el)) return;
if (!hoverPreviewContext || hoverPreviewContext.folder !== folderPath) return;
@@ -884,22 +958,55 @@ function fillHoverPreviewForRow(row) {
metaEl.textContent = sizeLabel
? `${pieces.join(", ")} β’ ${sizeLabel}`
: pieces.join(", ");
- }).catch(() => {});
- // Left side: peek inside folder (first few children)
+ // 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(`
+
+ ${t("modified") || "Modified"}: ${escapeHTML(modified)}
+
+ `);
+ }
+
+ if (created) {
+ props.push(`
+
+ ${t("created") || "Created"}: ${escapeHTML(created)}
+
+ `);
+ }
+
+ propsEl.innerHTML = props.join("");
+ }).catch(() => {});
+ // ------------------------------------------------------------------
+
// Left side: peek inside folder (first few children)
fetchFolderPeek(folderPath).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) {
- snippetEl.style.display = "none";
+ 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 nothing inside, show a friendly message like files do
if (!items || !items.length) {
const msg =
t("no_files_or_folders") ||
@@ -911,12 +1018,15 @@ fetchFolderPeek(folderPath).then(result => {
return;
}
+ const MAX_LABEL_CHARS = 42; // tweak to taste
+
const lines = items.map(it => {
const prefix = it.type === "folder" ? "π " : "π ";
- return prefix + it.name;
+ const trimmed = _trimLabel(it.name, MAX_LABEL_CHARS);
+ return prefix + trimmed;
});
- // If we had to cut the list to FOLDER_PEEK_MAX_ITEMS, turn the LAST line into "β¦"
+ // If we had to cut the list to FOLDER_PEEK_MAX_ITEMS, show a clean final "β¦"
if (truncated && lines.length) {
lines[lines.length - 1] = "β¦";
}
@@ -1024,6 +1134,56 @@ fetchFolderPeek(folderPath).then(result => {
props.push(`
${t("owner") || "Owner"}: ${escapeHTML(file.uploader)}
`);
}
+ // --- 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(
+ `
${escapeHTML(label)}: ${escapeHTML(parts.join(" β’ "))}
`
+ );
+ })();
+ // ------------------------------------------------------------------
+
+ propsEl.innerHTML = props.join("");
+
propsEl.innerHTML = props.join("");
// Text snippet (left) for smaller text/code files
@@ -1372,6 +1532,165 @@ function formatSize(totalBytes) {
}
}
+
+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 = `
+
+
+
+
+
+
+ `;
+
+ 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 } = q.shift();
+ const url = apiFileUrl(folder || 'root', name, /* inline */ false);
+
+ 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;
+
+
/**
* Build the folder summary HTML using the filtered file list.
*/
@@ -1719,7 +2038,7 @@ export async function loadFileList(folderParam) {
?.style.setProperty("grid-template-columns", `repeat(${v},1fr)`);
};
} else {
- const currentHeight = parseInt(localStorage.getItem("rowHeight") || "48", 10);
+ const currentHeight = parseInt(localStorage.getItem("rowHeight") || "44", 10);
sliderContainer.innerHTML = `