add drag and drop uploading, finish file uploading, optimize diretories with tons of files, add more options for file viewing (descending, etc). Add more sorting options.
This commit is contained in:
@@ -14,6 +14,16 @@ let lastSuccessfulResponse = 0;
|
|||||||
let pendingToast = null;
|
let pendingToast = null;
|
||||||
const recentToasts = new Map();
|
const recentToasts = new Map();
|
||||||
const pendingCommandResults = new Map();
|
const pendingCommandResults = new Map();
|
||||||
|
const VIRTUALIZATION_THRESHOLD = 400;
|
||||||
|
const VIRTUAL_ROW_HEIGHT = 58;
|
||||||
|
const VIRTUAL_OVERSCAN = 8;
|
||||||
|
|
||||||
|
let directoryEntries = [];
|
||||||
|
let filteredDirectoryEntries = [];
|
||||||
|
let virtualScrollHandler = null;
|
||||||
|
let virtualResizeHandler = null;
|
||||||
|
let virtualRenderRaf = null;
|
||||||
|
let isVirtualizedList = false;
|
||||||
|
|
||||||
const statusEl = document.getElementById("status-indicator");
|
const statusEl = document.getElementById("status-indicator");
|
||||||
const breadcrumbEl = document.getElementById("breadcrumb");
|
const breadcrumbEl = document.getElementById("breadcrumb");
|
||||||
@@ -31,6 +41,11 @@ const pathInput = document.getElementById("path-input");
|
|||||||
const pathGoBtn = document.getElementById("path-go-btn");
|
const pathGoBtn = document.getElementById("path-go-btn");
|
||||||
const transferPanel = document.getElementById("transfer-panel");
|
const transferPanel = document.getElementById("transfer-panel");
|
||||||
const transferList = document.getElementById("transfer-list");
|
const transferList = document.getElementById("transfer-list");
|
||||||
|
const fileListPanel = document.getElementById("file-list-panel");
|
||||||
|
const sortFieldEl = document.getElementById("sort-field");
|
||||||
|
const sortOrderBtn = document.getElementById("sort-order-btn");
|
||||||
|
const filterTypeEl = document.getElementById("filter-type");
|
||||||
|
const fileCountSummaryEl = document.getElementById("file-count-summary");
|
||||||
|
|
||||||
const searchBar = document.getElementById("search-bar");
|
const searchBar = document.getElementById("search-bar");
|
||||||
const searchInput = document.getElementById("search-input");
|
const searchInput = document.getElementById("search-input");
|
||||||
@@ -58,6 +73,11 @@ if (clientIdHeader) {
|
|||||||
clientIdHeader.textContent = `${clientId} - File Browser`;
|
clientIdHeader.textContent = `${clientId} - File Browser`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let sortField = localStorage.getItem("filebrowser.sortField") || "name";
|
||||||
|
let sortOrder = localStorage.getItem("filebrowser.sortOrder") || "asc";
|
||||||
|
let filterType = localStorage.getItem("filebrowser.filterType") || "all";
|
||||||
|
let dragDepth = 0;
|
||||||
|
|
||||||
function connect() {
|
function connect() {
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
const wsUrl = `${protocol}//${window.location.host}/api/clients/${clientId}/files/ws`;
|
const wsUrl = `${protocol}//${window.location.host}/api/clients/${clientId}/files/ws`;
|
||||||
@@ -297,33 +317,117 @@ function updateBreadcrumb(path) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileList(msg) {
|
function getFileExt(name = "") {
|
||||||
if (msg.error) {
|
const idx = name.lastIndexOf(".");
|
||||||
fileListEl.innerHTML = `<div class="px-4 py-6 text-center text-red-400"><i class="fa-solid fa-exclamation-triangle mr-2"></i>${escapeHtml(msg.error)}</div>`;
|
if (idx < 0 || idx === name.length - 1) return "";
|
||||||
return;
|
return name.slice(idx + 1).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function entryMatchesFilter(entry, mode) {
|
||||||
|
if (mode === "all") return true;
|
||||||
|
if (mode === "dirs") return !!entry.isDir;
|
||||||
|
if (mode === "files") return !entry.isDir;
|
||||||
|
if (entry.isDir) return false;
|
||||||
|
|
||||||
|
const ext = getFileExt(entry.name);
|
||||||
|
const imageExt = new Set(["jpg", "jpeg", "png", "gif", "webp", "bmp", "svg", "ico"]);
|
||||||
|
const docExt = new Set(["txt", "md", "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "csv", "json", "xml", "yaml", "yml"]);
|
||||||
|
const archiveExt = new Set(["zip", "rar", "7z", "tar", "gz", "bz2", "xz"]);
|
||||||
|
const execExt = new Set(["exe", "msi", "bat", "cmd", "ps1", "sh", "appimage", "bin", "com"]);
|
||||||
|
|
||||||
|
if (mode === "images") return imageExt.has(ext);
|
||||||
|
if (mode === "docs") return docExt.has(ext);
|
||||||
|
if (mode === "archives") return archiveExt.has(ext);
|
||||||
|
if (mode === "executables") return execExt.has(ext);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortEntries(entries, field, order) {
|
||||||
|
const dirRank = (entry) => (entry.isDir ? 0 : 1);
|
||||||
|
const factor = order === "desc" ? -1 : 1;
|
||||||
|
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
|
||||||
|
|
||||||
|
return [...entries].sort((a, b) => {
|
||||||
|
const dirDiff = dirRank(a) - dirRank(b);
|
||||||
|
if (dirDiff !== 0) return dirDiff;
|
||||||
|
|
||||||
|
let valueA = a.name || "";
|
||||||
|
let valueB = b.name || "";
|
||||||
|
|
||||||
|
if (field === "size") {
|
||||||
|
valueA = Number(a.size || 0);
|
||||||
|
valueB = Number(b.size || 0);
|
||||||
|
} else if (field === "modified") {
|
||||||
|
valueA = Number(a.modTime || 0);
|
||||||
|
valueB = Number(b.modTime || 0);
|
||||||
|
} else if (field === "type") {
|
||||||
|
valueA = a.isDir ? "" : getFileExt(a.name);
|
||||||
|
valueB = b.isDir ? "" : getFileExt(b.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof valueA === "number" && typeof valueB === "number") {
|
||||||
|
if (valueA === valueB) {
|
||||||
|
return factor * collator.compare(a.name || "", b.name || "");
|
||||||
|
}
|
||||||
|
return factor * (valueA - valueB);
|
||||||
|
}
|
||||||
|
|
||||||
|
const diff = collator.compare(String(valueA), String(valueB));
|
||||||
|
if (diff !== 0) return factor * diff;
|
||||||
|
return factor * collator.compare(a.name || "", b.name || "");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySortAndFilterEntries(entries) {
|
||||||
|
const filtered = entries.filter((entry) => entryMatchesFilter(entry, filterType));
|
||||||
|
return sortEntries(filtered, sortField, sortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSortOrderButton() {
|
||||||
|
if (!sortOrderBtn) return;
|
||||||
|
sortOrderBtn.textContent = sortOrder === "asc" ? "Asc" : "Desc";
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDirectorySummaryAndPaging(totalCount, shownCount) {
|
||||||
|
if (fileCountSummaryEl) {
|
||||||
|
fileCountSummaryEl.textContent = `${totalCount} items`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearVirtualizedListMode() {
|
||||||
|
if (virtualRenderRaf) {
|
||||||
|
cancelAnimationFrame(virtualRenderRaf);
|
||||||
|
virtualRenderRaf = null;
|
||||||
|
}
|
||||||
|
if (virtualScrollHandler) {
|
||||||
|
window.removeEventListener("scroll", virtualScrollHandler);
|
||||||
|
virtualScrollHandler = null;
|
||||||
|
}
|
||||||
|
if (virtualResizeHandler) {
|
||||||
|
window.removeEventListener("resize", virtualResizeHandler);
|
||||||
|
virtualResizeHandler = null;
|
||||||
|
}
|
||||||
|
isVirtualizedList = false;
|
||||||
|
fileListEl.classList.add("divide-y", "divide-slate-800");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDirectoryStandard(entries, canGoUp, parentPath, disableAnimations) {
|
||||||
|
clearVirtualizedListMode();
|
||||||
|
|
||||||
|
if (!disableAnimations) {
|
||||||
|
fileListEl.style.opacity = "0";
|
||||||
|
fileListEl.style.transform = "translateX(20px)";
|
||||||
|
} else {
|
||||||
|
fileListEl.style.transition = "none";
|
||||||
|
fileListEl.style.opacity = "1";
|
||||||
|
fileListEl.style.transform = "translateX(0)";
|
||||||
}
|
}
|
||||||
|
|
||||||
lastSuccessfulResponse = Date.now();
|
const renderList = () => {
|
||||||
updateStatus("connected", "Connected");
|
|
||||||
enableControls(true);
|
|
||||||
|
|
||||||
currentPath = msg.path;
|
|
||||||
const entries = msg.entries || [];
|
|
||||||
|
|
||||||
selectedFiles.clear();
|
|
||||||
updateSelectionUI();
|
|
||||||
|
|
||||||
fileListEl.style.opacity = "0";
|
|
||||||
fileListEl.style.transform = "translateX(20px)";
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
fileListEl.innerHTML = "";
|
fileListEl.innerHTML = "";
|
||||||
|
|
||||||
const canGoUp = shouldShowParentDirectory(currentPath);
|
|
||||||
if (canGoUp) {
|
if (canGoUp) {
|
||||||
const parentPath = getParentPath(currentPath);
|
fileListEl.appendChild(createParentRow(parentPath));
|
||||||
const parentRow = createParentRow(parentPath);
|
|
||||||
fileListEl.appendChild(parentRow);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entries.length === 0 && !canGoUp) {
|
if (entries.length === 0 && !canGoUp) {
|
||||||
@@ -334,24 +438,127 @@ function handleFileList(msg) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
entries.sort((a, b) => {
|
|
||||||
if (a.isDir && !b.isDir) return -1;
|
|
||||||
if (!a.isDir && b.isDir) return 1;
|
|
||||||
return a.name.localeCompare(b.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
entries.forEach((entry, index) => {
|
entries.forEach((entry, index) => {
|
||||||
const row = createFileRow(entry);
|
const row = createFileRow(entry);
|
||||||
row.style.animationDelay = `${index * 0.02}s`;
|
if (!disableAnimations) {
|
||||||
row.classList.add("card-animate");
|
row.style.animationDelay = `${index * 0.02}s`;
|
||||||
|
row.classList.add("card-animate");
|
||||||
|
}
|
||||||
fileListEl.appendChild(row);
|
fileListEl.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
fileListEl.style.transition =
|
if (!disableAnimations) {
|
||||||
"opacity 0.3s ease-out, transform 0.3s ease-out";
|
fileListEl.style.transition =
|
||||||
fileListEl.style.opacity = "1";
|
"opacity 0.3s ease-out, transform 0.3s ease-out";
|
||||||
fileListEl.style.transform = "translateX(0)";
|
fileListEl.style.opacity = "1";
|
||||||
}, 150);
|
fileListEl.style.transform = "translateX(0)";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (disableAnimations) {
|
||||||
|
renderList();
|
||||||
|
} else {
|
||||||
|
setTimeout(renderList, 150);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDirectoryVirtualized(entries, canGoUp, parentPath) {
|
||||||
|
clearVirtualizedListMode();
|
||||||
|
isVirtualizedList = true;
|
||||||
|
fileListEl.style.transition = "none";
|
||||||
|
fileListEl.style.opacity = "1";
|
||||||
|
fileListEl.style.transform = "translateX(0)";
|
||||||
|
fileListEl.classList.remove("divide-y", "divide-slate-800");
|
||||||
|
fileListEl.innerHTML = "";
|
||||||
|
|
||||||
|
if (canGoUp) {
|
||||||
|
fileListEl.appendChild(createParentRow(parentPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = document.createElement("div");
|
||||||
|
const topSpacer = document.createElement("div");
|
||||||
|
const rowsContainer = document.createElement("div");
|
||||||
|
const bottomSpacer = document.createElement("div");
|
||||||
|
|
||||||
|
host.className = "virtualized-list-host";
|
||||||
|
host.appendChild(topSpacer);
|
||||||
|
host.appendChild(rowsContainer);
|
||||||
|
host.appendChild(bottomSpacer);
|
||||||
|
fileListEl.appendChild(host);
|
||||||
|
|
||||||
|
const renderWindow = () => {
|
||||||
|
const hostTop = host.getBoundingClientRect().top + window.scrollY;
|
||||||
|
const viewportTop = window.scrollY;
|
||||||
|
const viewportBottom = viewportTop + window.innerHeight;
|
||||||
|
const relativeTop = Math.max(0, viewportTop - hostTop);
|
||||||
|
const relativeBottom = Math.max(0, viewportBottom - hostTop);
|
||||||
|
|
||||||
|
let start = Math.floor(relativeTop / VIRTUAL_ROW_HEIGHT) - VIRTUAL_OVERSCAN;
|
||||||
|
let end = Math.ceil(relativeBottom / VIRTUAL_ROW_HEIGHT) + VIRTUAL_OVERSCAN;
|
||||||
|
|
||||||
|
start = Math.max(0, start);
|
||||||
|
end = Math.min(entries.length, Math.max(start + 1, end));
|
||||||
|
|
||||||
|
topSpacer.style.height = `${start * VIRTUAL_ROW_HEIGHT}px`;
|
||||||
|
bottomSpacer.style.height = `${Math.max(0, (entries.length - end) * VIRTUAL_ROW_HEIGHT)}px`;
|
||||||
|
|
||||||
|
rowsContainer.innerHTML = "";
|
||||||
|
for (let i = start; i < end; i += 1) {
|
||||||
|
rowsContainer.appendChild(createFileRow(entries[i]));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleRender = () => {
|
||||||
|
if (virtualRenderRaf) return;
|
||||||
|
virtualRenderRaf = requestAnimationFrame(() => {
|
||||||
|
virtualRenderRaf = null;
|
||||||
|
renderWindow();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
virtualScrollHandler = () => scheduleRender();
|
||||||
|
virtualResizeHandler = () => scheduleRender();
|
||||||
|
window.addEventListener("scroll", virtualScrollHandler, { passive: true });
|
||||||
|
window.addEventListener("resize", virtualResizeHandler);
|
||||||
|
|
||||||
|
renderWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCurrentDirectory() {
|
||||||
|
filteredDirectoryEntries = applySortAndFilterEntries(directoryEntries);
|
||||||
|
const visibleEntries = filteredDirectoryEntries;
|
||||||
|
updateDirectorySummaryAndPaging(visibleEntries.length, visibleEntries.length);
|
||||||
|
|
||||||
|
const canGoUp = shouldShowParentDirectory(currentPath);
|
||||||
|
const parentPath = canGoUp ? getParentPath(currentPath) : ".";
|
||||||
|
|
||||||
|
if (visibleEntries.length > VIRTUALIZATION_THRESHOLD) {
|
||||||
|
renderDirectoryVirtualized(visibleEntries, canGoUp, parentPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const disableAnimations = visibleEntries.length > 50;
|
||||||
|
renderDirectoryStandard(visibleEntries, canGoUp, parentPath, disableAnimations);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileList(msg) {
|
||||||
|
if (msg.error) {
|
||||||
|
clearVirtualizedListMode();
|
||||||
|
fileListEl.innerHTML = `<div class="px-4 py-6 text-center text-red-400"><i class="fa-solid fa-exclamation-triangle mr-2"></i>${escapeHtml(msg.error)}</div>`;
|
||||||
|
updateDirectorySummaryAndPaging(0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastSuccessfulResponse = Date.now();
|
||||||
|
updateStatus("connected", "Connected");
|
||||||
|
enableControls(true);
|
||||||
|
|
||||||
|
currentPath = msg.path;
|
||||||
|
directoryEntries = Array.isArray(msg.entries) ? msg.entries : [];
|
||||||
|
|
||||||
|
selectedFiles.clear();
|
||||||
|
updateSelectionUI();
|
||||||
|
renderCurrentDirectory();
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldShowParentDirectory(path) {
|
function shouldShowParentDirectory(path) {
|
||||||
@@ -1232,6 +1439,95 @@ fileInput.onchange = async (e) => {
|
|||||||
listFiles(currentPath);
|
listFiles(currentPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function uploadMultipleFiles(files) {
|
||||||
|
for (const file of files) {
|
||||||
|
await uploadFile(file);
|
||||||
|
}
|
||||||
|
listFiles(currentPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasFileDrag(event) {
|
||||||
|
const types = Array.from(event.dataTransfer?.types || []);
|
||||||
|
return types.includes("Files");
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDropTargetActive(active) {
|
||||||
|
if (!fileListPanel) return;
|
||||||
|
fileListPanel.classList.toggle("ring-2", active);
|
||||||
|
fileListPanel.classList.toggle("ring-blue-500", active);
|
||||||
|
fileListPanel.classList.toggle("ring-offset-2", active);
|
||||||
|
fileListPanel.classList.toggle("ring-offset-slate-950", active);
|
||||||
|
fileListPanel.classList.toggle("bg-blue-500/5", active);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupDragAndDropUpload() {
|
||||||
|
if (!fileListPanel) return;
|
||||||
|
|
||||||
|
fileListPanel.addEventListener("dragenter", (e) => {
|
||||||
|
if (!hasFileDrag(e)) return;
|
||||||
|
e.preventDefault();
|
||||||
|
dragDepth += 1;
|
||||||
|
setDropTargetActive(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
fileListPanel.addEventListener("dragover", (e) => {
|
||||||
|
if (!hasFileDrag(e)) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = "copy";
|
||||||
|
setDropTargetActive(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
fileListPanel.addEventListener("dragleave", (e) => {
|
||||||
|
if (!hasFileDrag(e)) return;
|
||||||
|
e.preventDefault();
|
||||||
|
dragDepth = Math.max(0, dragDepth - 1);
|
||||||
|
if (dragDepth === 0) {
|
||||||
|
setDropTargetActive(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fileListPanel.addEventListener("drop", async (e) => {
|
||||||
|
if (!hasFileDrag(e)) return;
|
||||||
|
e.preventDefault();
|
||||||
|
dragDepth = 0;
|
||||||
|
setDropTargetActive(false);
|
||||||
|
|
||||||
|
const files = Array.from(e.dataTransfer?.files || []);
|
||||||
|
if (files.length === 0) return;
|
||||||
|
|
||||||
|
notifyToast(`Uploading ${files.length} file(s)...`, "info", 3000);
|
||||||
|
await uploadMultipleFiles(files);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortFieldEl) {
|
||||||
|
sortFieldEl.value = sortField;
|
||||||
|
sortFieldEl.addEventListener("change", () => {
|
||||||
|
sortField = sortFieldEl.value;
|
||||||
|
localStorage.setItem("filebrowser.sortField", sortField);
|
||||||
|
renderCurrentDirectory();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterTypeEl) {
|
||||||
|
filterTypeEl.value = filterType;
|
||||||
|
filterTypeEl.addEventListener("change", () => {
|
||||||
|
filterType = filterTypeEl.value;
|
||||||
|
localStorage.setItem("filebrowser.filterType", filterType);
|
||||||
|
renderCurrentDirectory();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortOrderBtn) {
|
||||||
|
updateSortOrderButton();
|
||||||
|
sortOrderBtn.addEventListener("click", () => {
|
||||||
|
sortOrder = sortOrder === "asc" ? "desc" : "asc";
|
||||||
|
localStorage.setItem("filebrowser.sortOrder", sortOrder);
|
||||||
|
updateSortOrderButton();
|
||||||
|
renderCurrentDirectory();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function waitForUploadAck(transfer, offset, timeoutMs) {
|
function waitForUploadAck(transfer, offset, timeoutMs) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const existing = transfer.pendingAcks.get(offset);
|
const existing = transfer.pendingAcks.get(offset);
|
||||||
@@ -1450,6 +1746,7 @@ document.addEventListener("click", (e) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setupDragAndDropUpload();
|
||||||
updateStatus("connecting", "Connecting...");
|
updateStatus("connecting", "Connecting...");
|
||||||
updateBackButton();
|
updateBackButton();
|
||||||
connect();
|
connect();
|
||||||
@@ -1602,6 +1899,8 @@ searchInput.addEventListener("keypress", (e) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function performSearch(pattern, searchContent) {
|
function performSearch(pattern, searchContent) {
|
||||||
|
clearVirtualizedListMode();
|
||||||
|
updateDirectorySummaryAndPaging(0, 0);
|
||||||
const searchId = `search-${Date.now()}`;
|
const searchId = `search-${Date.now()}`;
|
||||||
const cmdId = `search-cmd-${Date.now()}`;
|
const cmdId = `search-cmd-${Date.now()}`;
|
||||||
const msg = {
|
const msg = {
|
||||||
@@ -1624,6 +1923,8 @@ function performSearch(pattern, searchContent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleFileSearchResult(msg) {
|
function handleFileSearchResult(msg) {
|
||||||
|
clearVirtualizedListMode();
|
||||||
|
updateDirectorySummaryAndPaging(0, 0);
|
||||||
if (msg.error) {
|
if (msg.error) {
|
||||||
fileListEl.innerHTML = `<div class="px-4 py-6 text-center text-red-400"><i class="fa-solid fa-exclamation-triangle mr-2"></i>${escapeHtml(msg.error)}</div>`;
|
fileListEl.innerHTML = `<div class="px-4 py-6 text-center text-red-400"><i class="fa-solid fa-exclamation-triangle mr-2"></i>${escapeHtml(msg.error)}</div>`;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -461,8 +461,56 @@
|
|||||||
<div id="breadcrumb" class="flex items-center gap-1 text-sm"></div>
|
<div id="breadcrumb" class="flex items-center gap-1 text-sm"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- List Controls -->
|
||||||
|
<div
|
||||||
|
class="px-4 py-3 bg-slate-900/70 border border-slate-800 rounded-lg flex items-center justify-between gap-3 flex-wrap"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
|
<label for="sort-field" class="text-xs uppercase tracking-wide text-slate-400"
|
||||||
|
>Sort</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
id="sort-field"
|
||||||
|
class="px-2 py-1 rounded bg-slate-800 border border-slate-700 text-sm text-slate-100"
|
||||||
|
>
|
||||||
|
<option value="name">Name</option>
|
||||||
|
<option value="size">Size</option>
|
||||||
|
<option value="modified">Modified</option>
|
||||||
|
<option value="type">Type</option>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
id="sort-order-btn"
|
||||||
|
class="px-3 py-1 rounded bg-slate-700 hover:bg-slate-600 text-sm text-slate-100"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Asc
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<label for="filter-type" class="text-xs uppercase tracking-wide text-slate-400"
|
||||||
|
>Filter</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
id="filter-type"
|
||||||
|
class="px-2 py-1 rounded bg-slate-800 border border-slate-700 text-sm text-slate-100"
|
||||||
|
>
|
||||||
|
<option value="all">All</option>
|
||||||
|
<option value="dirs">Folders</option>
|
||||||
|
<option value="files">Files</option>
|
||||||
|
<option value="images">Images</option>
|
||||||
|
<option value="docs">Documents</option>
|
||||||
|
<option value="archives">Archives</option>
|
||||||
|
<option value="executables">Executables</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span id="file-count-summary" class="text-sm text-slate-400">0 items</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- File List -->
|
<!-- File List -->
|
||||||
<div
|
<div
|
||||||
|
id="file-list-panel"
|
||||||
class="bg-slate-900/70 border border-slate-800 rounded-lg overflow-hidden"
|
class="bg-slate-900/70 border border-slate-800 rounded-lg overflow-hidden"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -603,6 +651,6 @@
|
|||||||
<input type="file" id="file-input" style="display: none" multiple />
|
<input type="file" id="file-input" style="display: none" multiple />
|
||||||
|
|
||||||
<script type="module" src="/assets/nav.js"></script>
|
<script type="module" src="/assets/nav.js"></script>
|
||||||
<script type="module" src="/assets/filebrowser.js?v=uploadfix-20260306-2"></script>
|
<script type="module" src="/assets/filebrowser.js?v=uploadfix-20260306-3"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ set OVERLORD_DISABLE_AGENT_AUTH=true
|
|||||||
set LOG_LEVEL=debug
|
set LOG_LEVEL=debug
|
||||||
|
|
||||||
start "Overlord-Server" cmd /k "cd /d %ROOT%Overlord-Server && set OVERLORD_DISABLE_AGENT_AUTH=true && set OVERLORD_AGENT_TOKEN=dev-token-insecure-local-only && set LOG_LEVEL=debug && set NODE_ENV=development && bun install && bun run start"
|
start "Overlord-Server" cmd /k "cd /d %ROOT%Overlord-Server && set OVERLORD_DISABLE_AGENT_AUTH=true && set OVERLORD_AGENT_TOKEN=dev-token-insecure-local-only && set LOG_LEVEL=debug && set NODE_ENV=development && bun install && bun run start"
|
||||||
|
timeout /t 1 /nobreak >nul
|
||||||
start "Overlord-Client" cmd /k "cd /d %ROOT%Overlord-Client && set OVERLORD_SERVER=wss://localhost:5173 && set OVERLORD_AGENT_TOKEN=dev-token-insecure-local-only && set OVERLORD_TLS_INSECURE_SKIP_VERIFY=true && set OVERLORD_MODE=dev && set GOINSECURE=* && set GOSUMDB=off && set GOPROXY=https://proxy.golang.org,direct && go mod tidy && go run ./cmd/agent"
|
start "Overlord-Client" cmd /k "cd /d %ROOT%Overlord-Client && set OVERLORD_SERVER=wss://localhost:5173 && set OVERLORD_AGENT_TOKEN=dev-token-insecure-local-only && set OVERLORD_TLS_INSECURE_SKIP_VERIFY=true && set OVERLORD_MODE=dev && set GOINSECURE=* && set GOSUMDB=off && set GOPROXY=https://proxy.golang.org,direct && go mod tidy && go run ./cmd/agent"
|
||||||
|
|
||||||
echo Done. Terminals stay open (/k) for logs.
|
echo Done. Terminals stay open (/k) for logs.
|
||||||
|
|||||||
Reference in New Issue
Block a user