768 lines
28 KiB
JavaScript
768 lines
28 KiB
JavaScript
// fileListView.js
|
||
import {
|
||
escapeHTML,
|
||
debounce,
|
||
buildSearchAndPaginationControls,
|
||
buildFileTableHeader,
|
||
buildFileTableRow,
|
||
buildBottomControls,
|
||
updateFileActionButtons,
|
||
showToast,
|
||
updateRowHighlight,
|
||
toggleRowSelection,
|
||
attachEnterKeyListener
|
||
} from './domUtils.js';
|
||
import { t } from './i18n.js';
|
||
import { bindFileListContextMenu } from './fileMenu.js';
|
||
import { openDownloadModal } from './fileActions.js';
|
||
import { openTagModal, openMultiTagModal } from './fileTags.js';
|
||
|
||
export let fileData = [];
|
||
export let sortOrder = { column: "uploaded", ascending: true };
|
||
|
||
window.itemsPerPage = parseInt(
|
||
localStorage.getItem('itemsPerPage') || window.itemsPerPage || '10',
|
||
10
|
||
);
|
||
window.currentPage = window.currentPage || 1;
|
||
window.viewMode = localStorage.getItem("viewMode") || "table";
|
||
|
||
// Global flag for advanced search mode.
|
||
window.advancedSearchEnabled = false;
|
||
|
||
/**
|
||
* --- Helper Functions ---
|
||
*/
|
||
|
||
/**
|
||
* Convert a file size string (e.g. "456.9KB", "1.2 MB", "1024") into bytes.
|
||
*/
|
||
function parseSizeToBytes(sizeStr) {
|
||
if (!sizeStr) return 0;
|
||
let s = sizeStr.trim();
|
||
let value = parseFloat(s);
|
||
let upper = s.toUpperCase();
|
||
if (upper.includes("KB")) {
|
||
value *= 1024;
|
||
} else if (upper.includes("MB")) {
|
||
value *= 1024 * 1024;
|
||
} else if (upper.includes("GB")) {
|
||
value *= 1024 * 1024 * 1024;
|
||
}
|
||
return value;
|
||
}
|
||
|
||
/**
|
||
* Format the total bytes as a human-readable string.
|
||
*/
|
||
function formatSize(totalBytes) {
|
||
if (totalBytes < 1024) {
|
||
return totalBytes + " Bytes";
|
||
} else if (totalBytes < 1024 * 1024) {
|
||
return (totalBytes / 1024).toFixed(2) + " KB";
|
||
} else if (totalBytes < 1024 * 1024 * 1024) {
|
||
return (totalBytes / (1024 * 1024)).toFixed(2) + " MB";
|
||
} else {
|
||
return (totalBytes / (1024 * 1024 * 1024)).toFixed(2) + " GB";
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Build the folder summary HTML using the filtered file list.
|
||
*/
|
||
function buildFolderSummary(filteredFiles) {
|
||
const totalFiles = filteredFiles.length;
|
||
const totalBytes = filteredFiles.reduce((sum, file) => {
|
||
return sum + parseSizeToBytes(file.size);
|
||
}, 0);
|
||
const sizeStr = formatSize(totalBytes);
|
||
return `<strong>${t('total_files')}:</strong> ${totalFiles} | <strong>${t('total_size')}:</strong> ${sizeStr}`;
|
||
}
|
||
|
||
/**
|
||
* --- Advanced Search Toggle ---
|
||
* Toggles advanced search mode. When enabled, the search will include additional keys (e.g. "content").
|
||
*/
|
||
function toggleAdvancedSearch() {
|
||
window.advancedSearchEnabled = !window.advancedSearchEnabled;
|
||
const advancedBtn = document.getElementById("advancedSearchToggle");
|
||
if (advancedBtn) {
|
||
advancedBtn.textContent = window.advancedSearchEnabled ? "Basic Search" : "Advanced Search";
|
||
}
|
||
// Re-run the file table rendering with updated search settings.
|
||
renderFileTable(window.currentFolder);
|
||
}
|
||
|
||
window.imageCache = window.imageCache || {};
|
||
function cacheImage(imgElem, key) {
|
||
// Save the current src for future renders.
|
||
window.imageCache[key] = imgElem.src;
|
||
}
|
||
window.cacheImage = cacheImage;
|
||
|
||
/**
|
||
* --- Fuse.js Search Helper ---
|
||
* Uses Fuse.js to perform a fuzzy search on fileData.
|
||
* By default, searches over file name, uploader, and tag names.
|
||
* When advanced search is enabled, it also includes the 'content' property.
|
||
*/
|
||
function searchFiles(searchTerm) {
|
||
if (!searchTerm) return fileData;
|
||
|
||
// Define search keys.
|
||
let keys = [
|
||
{ name: 'name', weight: 0.1 },
|
||
{ name: 'uploader', weight: 0.1 },
|
||
{ name: 'tags.name', weight: 0.1 }
|
||
];
|
||
if (window.advancedSearchEnabled) {
|
||
keys.push({ name: 'content', weight: 0.7 });
|
||
}
|
||
|
||
const options = {
|
||
keys: keys,
|
||
threshold: 0.4,
|
||
minMatchCharLength: 2,
|
||
ignoreLocation: true
|
||
};
|
||
|
||
const fuse = new Fuse(fileData, options);
|
||
let results = fuse.search(searchTerm);
|
||
return results.map(result => result.item);
|
||
}
|
||
|
||
/**
|
||
* --- VIEW MODE TOGGLE BUTTON & Helpers ---
|
||
*/
|
||
export function createViewToggleButton() {
|
||
let toggleBtn = document.getElementById("toggleViewBtn");
|
||
if (!toggleBtn) {
|
||
toggleBtn = document.createElement("button");
|
||
toggleBtn.id = "toggleViewBtn";
|
||
toggleBtn.classList.add("btn", "btn-toggleview");
|
||
|
||
// Set initial icon and tooltip based on current view mode.
|
||
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");
|
||
}
|
||
|
||
// Insert the button before the last button in the header.
|
||
const headerButtons = document.querySelector(".header-buttons");
|
||
if (headerButtons && headerButtons.lastElementChild) {
|
||
headerButtons.insertBefore(toggleBtn, headerButtons.lastElementChild);
|
||
} else if (headerButtons) {
|
||
headerButtons.appendChild(toggleBtn);
|
||
}
|
||
}
|
||
|
||
toggleBtn.onclick = () => {
|
||
window.viewMode = window.viewMode === "gallery" ? "table" : "gallery";
|
||
localStorage.setItem("viewMode", window.viewMode);
|
||
loadFileList(window.currentFolder);
|
||
if (window.viewMode === "gallery") {
|
||
toggleBtn.innerHTML = '<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;
|
||
}
|
||
|
||
export function formatFolderName(folder) {
|
||
if (folder === "root") return "(Root)";
|
||
return folder
|
||
.replace(/[_-]+/g, " ")
|
||
.replace(/\b\w/g, char => char.toUpperCase());
|
||
}
|
||
|
||
// Expose inline DOM helpers.
|
||
window.toggleRowSelection = toggleRowSelection;
|
||
window.updateRowHighlight = updateRowHighlight;
|
||
|
||
/**
|
||
* --- FILE LIST & VIEW RENDERING ---
|
||
*/
|
||
export function loadFileList(folderParam) {
|
||
const folder = folderParam || "root";
|
||
const fileListContainer = document.getElementById("fileList");
|
||
|
||
fileListContainer.style.visibility = "hidden";
|
||
fileListContainer.innerHTML = "<div class='loader'>Loading files...</div>";
|
||
|
||
return fetch("/api/file/getFileList.php?folder=" + encodeURIComponent(folder) + "&recursive=1&t=" + new Date().getTime())
|
||
.then(response => {
|
||
if (response.status === 401) {
|
||
showToast("Session expired. Please log in again.");
|
||
window.location.href = "/api/auth/logout.php";
|
||
throw new Error("Unauthorized");
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(data => {
|
||
fileListContainer.innerHTML = ""; // Clear loading message.
|
||
if (data.files && Object.keys(data.files).length > 0) {
|
||
// If the returned "files" is an object instead of an array, transform it.
|
||
if (!Array.isArray(data.files)) {
|
||
data.files = Object.entries(data.files).map(([name, meta]) => {
|
||
meta.name = name;
|
||
return meta;
|
||
});
|
||
}
|
||
// Process each file – add computed properties.
|
||
data.files = data.files.map(file => {
|
||
file.fullName = (file.path || file.name).trim().toLowerCase();
|
||
file.editable = canEditFile(file.name);
|
||
file.folder = folder;
|
||
if (!file.type && /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
|
||
file.type = "image";
|
||
}
|
||
// OPTIONAL: For text documents, preload content (if available from backend)
|
||
// Example: if (/\.txt|html|md|js|css|json|xml$/i.test(file.name)) { file.content = file.content || ""; }
|
||
return file;
|
||
});
|
||
fileData = data.files;
|
||
|
||
// Update file summary.
|
||
const actionsContainer = document.getElementById("fileListActions");
|
||
if (actionsContainer) {
|
||
let summaryElem = document.getElementById("fileSummary");
|
||
if (!summaryElem) {
|
||
summaryElem = document.createElement("div");
|
||
summaryElem.id = "fileSummary";
|
||
summaryElem.style.float = "right";
|
||
summaryElem.style.marginLeft = "auto";
|
||
summaryElem.style.marginRight = "60px";
|
||
summaryElem.style.fontSize = "0.9em";
|
||
actionsContainer.appendChild(summaryElem);
|
||
} else {
|
||
summaryElem.style.display = "block";
|
||
}
|
||
summaryElem.innerHTML = buildFolderSummary(fileData);
|
||
}
|
||
|
||
// Render view based on the view mode.
|
||
if (window.viewMode === "gallery") {
|
||
renderGalleryView(folder);
|
||
updateFileActionButtons();
|
||
} else {
|
||
renderFileTable(folder);
|
||
}
|
||
} else {
|
||
fileListContainer.textContent = t("no_files_found");
|
||
const summaryElem = document.getElementById("fileSummary");
|
||
if (summaryElem) {
|
||
summaryElem.style.display = "none";
|
||
}
|
||
updateFileActionButtons();
|
||
}
|
||
return data.files || [];
|
||
})
|
||
.catch(error => {
|
||
console.error("Error loading file list:", error);
|
||
if (error.message !== "Unauthorized") {
|
||
fileListContainer.textContent = "Error loading files.";
|
||
}
|
||
return [];
|
||
})
|
||
.finally(() => {
|
||
fileListContainer.style.visibility = "visible";
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Update renderFileTable so it writes its content into the provided container.
|
||
*/
|
||
export function renderFileTable(folder, container) {
|
||
const fileListContent = container || document.getElementById("fileList");
|
||
const searchTerm = (window.currentSearchTerm || "").toLowerCase();
|
||
const itemsPerPageSetting = parseInt(localStorage.getItem("itemsPerPage") || "10", 10);
|
||
let currentPage = window.currentPage || 1;
|
||
|
||
// Use Fuse.js search via our helper function.
|
||
const filteredFiles = searchFiles(searchTerm);
|
||
|
||
const totalFiles = filteredFiles.length;
|
||
const totalPages = Math.ceil(totalFiles / itemsPerPageSetting);
|
||
if (currentPage > totalPages) {
|
||
currentPage = totalPages > 0 ? totalPages : 1;
|
||
window.currentPage = currentPage;
|
||
}
|
||
const folderPath = folder === "root"
|
||
? "uploads/"
|
||
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
|
||
|
||
// Build the top controls and append the advanced search toggle button.
|
||
const topControlsHTML = buildSearchAndPaginationControls({
|
||
currentPage,
|
||
totalPages,
|
||
searchTerm: window.currentSearchTerm || ""
|
||
});
|
||
|
||
const combinedTopHTML = topControlsHTML;
|
||
|
||
let headerHTML = buildFileTableHeader(sortOrder);
|
||
const startIndex = (currentPage - 1) * itemsPerPageSetting;
|
||
const endIndex = Math.min(startIndex + itemsPerPageSetting, totalFiles);
|
||
let rowsHTML = "<tbody>";
|
||
if (totalFiles > 0) {
|
||
filteredFiles.slice(startIndex, endIndex).forEach((file, idx) => {
|
||
let rowHTML = buildFileTableRow(file, folderPath);
|
||
rowHTML = rowHTML.replace("<tr", `<tr id="file-row-${encodeURIComponent(file.name)}-${startIndex + idx}"`);
|
||
|
||
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 => {
|
||
tagBadgesHTML += `<span style="background-color: ${tag.color}; color: #fff; padding: 2px 4px; border-radius: 3px; margin-right: 2px; font-size: 0.8em;">${escapeHTML(tag.name)}</span>`;
|
||
});
|
||
tagBadgesHTML += "</div>";
|
||
}
|
||
rowHTML = rowHTML.replace(/(<td class="file-name-cell">)(.*?)(<\/td>)/, (match, p1, p2, p3) => {
|
||
return p1 + p2 + tagBadgesHTML + p3;
|
||
});
|
||
rowHTML = rowHTML.replace(/(<\/div>\s*<\/td>\s*<\/tr>)/, `<button class="share-btn btn btn-sm btn-secondary" data-file="${escapeHTML(file.name)}" title="${t('share')}">
|
||
<i class="material-icons">share</i>
|
||
</button>$1`);
|
||
rowsHTML += rowHTML;
|
||
});
|
||
} else {
|
||
rowsHTML += `<tr><td colspan="8">No files found.</td></tr>`;
|
||
}
|
||
rowsHTML += "</tbody></table>";
|
||
const bottomControlsHTML = buildBottomControls(itemsPerPageSetting);
|
||
|
||
fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML;
|
||
|
||
createViewToggleButton();
|
||
|
||
// Setup event listeners.
|
||
const newSearchInput = document.getElementById("searchInput");
|
||
if (newSearchInput) {
|
||
newSearchInput.addEventListener("input", debounce(function () {
|
||
window.currentSearchTerm = newSearchInput.value;
|
||
window.currentPage = 1;
|
||
renderFileTable(folder, container);
|
||
setTimeout(() => {
|
||
const freshInput = document.getElementById("searchInput");
|
||
if (freshInput) {
|
||
freshInput.focus();
|
||
const len = freshInput.value.length;
|
||
freshInput.setSelectionRange(len, len);
|
||
}
|
||
}, 0);
|
||
}, 300));
|
||
}
|
||
document.querySelectorAll("table.table thead th[data-column]").forEach(cell => {
|
||
cell.addEventListener("click", function () {
|
||
const column = this.getAttribute("data-column");
|
||
sortFiles(column, folder);
|
||
});
|
||
});
|
||
document.querySelectorAll("#fileList .file-checkbox").forEach(checkbox => {
|
||
checkbox.addEventListener("change", function (e) {
|
||
updateRowHighlight(e.target);
|
||
updateFileActionButtons();
|
||
});
|
||
});
|
||
document.querySelectorAll(".share-btn").forEach(btn => {
|
||
btn.addEventListener("click", function (e) {
|
||
e.stopPropagation();
|
||
const fileName = this.getAttribute("data-file");
|
||
const file = fileData.find(f => f.name === fileName);
|
||
if (file) {
|
||
import('./filePreview.js').then(module => {
|
||
module.openShareModal(file, folder);
|
||
});
|
||
}
|
||
});
|
||
});
|
||
updateFileActionButtons();
|
||
document.querySelectorAll("#fileList tbody tr").forEach(row => {
|
||
row.setAttribute("draggable", "true");
|
||
import('./fileDragDrop.js').then(module => {
|
||
row.addEventListener("dragstart", module.fileDragStartHandler);
|
||
});
|
||
});
|
||
document.querySelectorAll(".download-btn, .edit-btn, .rename-btn").forEach(btn => {
|
||
btn.addEventListener("click", e => e.stopPropagation());
|
||
});
|
||
bindFileListContextMenu();
|
||
}
|
||
|
||
// A helper to compute the max image height based on the current column count.
|
||
function getMaxImageHeight() {
|
||
const columns = parseInt(window.galleryColumns || 3, 10);
|
||
return 150 * (7 - columns); // adjust the multiplier as needed.
|
||
}
|
||
|
||
export function renderGalleryView(folder, container) {
|
||
const fileListContent = container || document.getElementById("fileList");
|
||
const searchTerm = (window.currentSearchTerm || "").toLowerCase();
|
||
const filteredFiles = searchFiles(searchTerm);
|
||
const folderPath = folder === "root"
|
||
? "uploads/"
|
||
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
|
||
|
||
// pagination settings
|
||
const itemsPerPage = window.itemsPerPage;
|
||
let currentPage = window.currentPage || 1;
|
||
const totalFiles = filteredFiles.length;
|
||
const totalPages = Math.ceil(totalFiles / itemsPerPage);
|
||
if (currentPage > totalPages) {
|
||
currentPage = totalPages || 1;
|
||
window.currentPage = currentPage;
|
||
}
|
||
|
||
// --- Top controls: search + pagination + items-per-page ---
|
||
let galleryHTML = buildSearchAndPaginationControls({
|
||
currentPage,
|
||
totalPages,
|
||
searchTerm: window.currentSearchTerm || ""
|
||
});
|
||
|
||
// wire up search input just like table view
|
||
setTimeout(() => {
|
||
const searchInput = document.getElementById("searchInput");
|
||
if (searchInput) {
|
||
searchInput.addEventListener("input", debounce(() => {
|
||
window.currentSearchTerm = searchInput.value;
|
||
window.currentPage = 1;
|
||
renderGalleryView(folder);
|
||
// keep caret at end
|
||
setTimeout(() => {
|
||
const f = document.getElementById("searchInput");
|
||
if (f) {
|
||
f.focus();
|
||
const len = f.value.length;
|
||
f.setSelectionRange(len, len);
|
||
}
|
||
}, 0);
|
||
}, 300));
|
||
}
|
||
}, 0);
|
||
|
||
// --- Column slider ---
|
||
const numColumns = window.galleryColumns || 3;
|
||
galleryHTML += `
|
||
<div class="gallery-slider" style="margin:10px; text-align:center;">
|
||
<label for="galleryColumnsSlider" style="margin-right:5px;">
|
||
${t('columns')}:
|
||
</label>
|
||
<input type="range" id="galleryColumnsSlider" min="1" max="6"
|
||
value="${numColumns}" style="vertical-align:middle;">
|
||
<span id="galleryColumnsValue">${numColumns}</span>
|
||
</div>
|
||
`;
|
||
|
||
// --- Start gallery grid ---
|
||
galleryHTML += `
|
||
<div class="gallery-container"
|
||
style="display:grid;
|
||
grid-template-columns:repeat(${numColumns},1fr);
|
||
gap:10px;
|
||
padding:10px;">
|
||
`;
|
||
|
||
// slice current page
|
||
const startIdx = (currentPage - 1) * itemsPerPage;
|
||
const pageFiles = filteredFiles.slice(startIdx, startIdx + itemsPerPage);
|
||
|
||
pageFiles.forEach((file, idx) => {
|
||
const idSafe = encodeURIComponent(file.name) + "-" + (startIdx + idx);
|
||
|
||
// thumbnail
|
||
let thumbnail;
|
||
if (/\.(jpe?g|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
|
||
const cacheKey = folderPath + encodeURIComponent(file.name);
|
||
if (window.imageCache && window.imageCache[cacheKey]) {
|
||
thumbnail = `<img src="${window.imageCache[cacheKey]}"
|
||
class="gallery-thumbnail"
|
||
alt="${escapeHTML(file.name)}"
|
||
style="max-width:100%; max-height:${getMaxImageHeight()}px; display:block; margin:0 auto;">`;
|
||
} else {
|
||
const imageUrl = folderPath + encodeURIComponent(file.name) + "?t=" + Date.now();
|
||
thumbnail = `<img src="${imageUrl}"
|
||
onload="cacheImage(this,'${cacheKey}')"
|
||
class="gallery-thumbnail"
|
||
alt="${escapeHTML(file.name)}"
|
||
style="max-width:100%; max-height:${getMaxImageHeight()}px; display:block; margin:0 auto;">`;
|
||
}
|
||
} 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 => {
|
||
tagBadgesHTML += `<span style="background-color:${tag.color};
|
||
color:#fff;
|
||
padding:2px 4px;
|
||
border-radius:3px;
|
||
margin-right:2px;
|
||
font-size:0.8em;">
|
||
${escapeHTML(tag.name)}
|
||
</span>`;
|
||
});
|
||
tagBadgesHTML += `</div>`;
|
||
}
|
||
|
||
// card with checkbox, preview, info, buttons
|
||
galleryHTML += `
|
||
<div class="gallery-card"
|
||
style="position:relative; border:1px solid #ccc; padding:5px; text-align:center;">
|
||
<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;"
|
||
onclick="previewFile('${folderPath + encodeURIComponent(file.name)}?t='+Date.now(), '${file.name}')">
|
||
${thumbnail}
|
||
</div>
|
||
|
||
<div class="gallery-info" style="margin-top:5px;">
|
||
<span class="gallery-file-name"
|
||
style="display:block; white-space:normal; overflow-wrap:break-word;">
|
||
${escapeHTML(file.name)}
|
||
</span>
|
||
${tagBadgesHTML}
|
||
|
||
<div class="button-wrap" style="display:flex; justify-content:center; gap:5px; margin-top:5px;">
|
||
<button type="button" class="btn btn-sm btn-success download-btn"
|
||
onclick="openDownloadModal('${file.name}', '${file.folder || "root"}')"
|
||
title="${t('download')}">
|
||
<i class="material-icons">file_download</i>
|
||
</button>
|
||
${file.editable ? `
|
||
<button class="btn btn-sm edit-btn"
|
||
onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
|
||
title="${t('Edit')}">
|
||
<i class="material-icons">edit</i>
|
||
</button>` : ""}
|
||
<button class="btn btn-sm btn-warning rename-btn"
|
||
onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
|
||
title="${t('rename')}">
|
||
<i class="material-icons">drive_file_rename_outline</i>
|
||
</button>
|
||
<button class="btn btn-sm btn-secondary 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;
|
||
|
||
// ensure toggle button
|
||
createViewToggleButton();
|
||
|
||
// attach listeners
|
||
|
||
// checkboxes
|
||
document.querySelectorAll(".file-checkbox").forEach(cb => {
|
||
cb.addEventListener("change", () => updateFileActionButtons());
|
||
});
|
||
|
||
// slider
|
||
const slider = document.getElementById("galleryColumnsSlider");
|
||
if (slider) {
|
||
slider.addEventListener("input", () => {
|
||
const v = +slider.value;
|
||
document.getElementById("galleryColumnsValue").textContent = v;
|
||
window.galleryColumns = v;
|
||
document.querySelector(".gallery-container")
|
||
.style.gridTemplateColumns = `repeat(${v},1fr)`;
|
||
document.querySelectorAll(".gallery-thumbnail")
|
||
.forEach(img => img.style.maxHeight = getMaxImageHeight() + "px");
|
||
});
|
||
}
|
||
|
||
// pagination
|
||
window.changePage = newPage => {
|
||
window.currentPage = newPage;
|
||
if (window.viewMode === "gallery") renderGalleryView(folder);
|
||
else renderFileTable(folder);
|
||
};
|
||
|
||
// items per page
|
||
window.changeItemsPerPage = cnt => {
|
||
window.itemsPerPage = +cnt;
|
||
localStorage.setItem("itemsPerPage", cnt);
|
||
window.currentPage = 1;
|
||
if (window.viewMode === "gallery") renderGalleryView(folder);
|
||
else renderFileTable(folder);
|
||
};
|
||
|
||
// update toolbar buttons
|
||
updateFileActionButtons();
|
||
}
|
||
|
||
// Responsive slider constraints based on screen size.
|
||
function updateSliderConstraints() {
|
||
const slider = document.getElementById("galleryColumnsSlider");
|
||
if (!slider) return;
|
||
|
||
const width = window.innerWidth;
|
||
let min = 1;
|
||
let max;
|
||
|
||
// Set maximum based on screen size.
|
||
if (width < 600) { // small devices (phones)
|
||
max = 1;
|
||
} else if (width < 1024) { // medium devices
|
||
max = 3;
|
||
} else if (width < 1440) { // between medium and large devices
|
||
max = 4;
|
||
} else { // large devices and above
|
||
max = 6;
|
||
}
|
||
|
||
// Adjust the slider's current value if needed
|
||
let currentVal = parseInt(slider.value, 10);
|
||
if (currentVal > max) {
|
||
currentVal = max;
|
||
slider.value = max;
|
||
}
|
||
|
||
slider.min = min;
|
||
slider.max = max;
|
||
document.getElementById("galleryColumnsValue").textContent = currentVal;
|
||
|
||
// Update the grid layout based on the current slider value.
|
||
const galleryContainer = document.querySelector(".gallery-container");
|
||
if (galleryContainer) {
|
||
galleryContainer.style.gridTemplateColumns = `repeat(${currentVal}, 1fr)`;
|
||
}
|
||
}
|
||
|
||
window.addEventListener('load', updateSliderConstraints);
|
||
window.addEventListener('resize', updateSliderConstraints);
|
||
|
||
export function sortFiles(column, folder) {
|
||
if (sortOrder.column === column) {
|
||
sortOrder.ascending = !sortOrder.ascending;
|
||
} else {
|
||
sortOrder.column = column;
|
||
sortOrder.ascending = true;
|
||
}
|
||
fileData.sort((a, b) => {
|
||
let valA = a[column] || "";
|
||
let valB = b[column] || "";
|
||
if (column === "modified" || column === "uploaded") {
|
||
const parsedA = parseCustomDate(valA);
|
||
const parsedB = parseCustomDate(valB);
|
||
valA = parsedA;
|
||
valB = parsedB;
|
||
} else if (typeof valA === "string") {
|
||
valA = valA.toLowerCase();
|
||
valB = valB.toLowerCase();
|
||
}
|
||
if (valA < valB) return sortOrder.ascending ? -1 : 1;
|
||
if (valA > valB) return sortOrder.ascending ? 1 : -1;
|
||
return 0;
|
||
});
|
||
if (window.viewMode === "gallery") {
|
||
renderGalleryView(folder);
|
||
} else {
|
||
renderFileTable(folder);
|
||
}
|
||
}
|
||
|
||
function parseCustomDate(dateStr) {
|
||
dateStr = dateStr.replace(/\s+/g, " ").trim();
|
||
const parts = dateStr.split(" ");
|
||
if (parts.length !== 2) {
|
||
return new Date(dateStr).getTime();
|
||
}
|
||
const datePart = parts[0];
|
||
const timePart = parts[1];
|
||
const dateComponents = datePart.split("/");
|
||
if (dateComponents.length !== 3) {
|
||
return new Date(dateStr).getTime();
|
||
}
|
||
let month = parseInt(dateComponents[0], 10);
|
||
let day = parseInt(dateComponents[1], 10);
|
||
let year = parseInt(dateComponents[2], 10);
|
||
if (year < 100) {
|
||
year += 2000;
|
||
}
|
||
const timeRegex = /^(\d{1,2}):(\d{2})(AM|PM)$/i;
|
||
const match = timePart.match(timeRegex);
|
||
if (!match) {
|
||
return new Date(dateStr).getTime();
|
||
}
|
||
let hour = parseInt(match[1], 10);
|
||
const minute = parseInt(match[2], 10);
|
||
const period = match[3].toUpperCase();
|
||
if (period === "PM" && hour !== 12) {
|
||
hour += 12;
|
||
}
|
||
if (period === "AM" && hour === 12) {
|
||
hour = 0;
|
||
}
|
||
return new Date(year, month - 1, day, hour, minute).getTime();
|
||
}
|
||
|
||
export function canEditFile(fileName) {
|
||
const allowedExtensions = [
|
||
"txt", "html", "htm", "css", "js", "json", "xml",
|
||
"md", "py", "ini", "csv", "log", "conf", "config", "bat",
|
||
"rtf", "doc", "docx"
|
||
];
|
||
const ext = fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase();
|
||
return allowedExtensions.includes(ext);
|
||
}
|
||
|
||
// Expose global functions for pagination and preview.
|
||
window.changePage = function (newPage) {
|
||
window.currentPage = newPage;
|
||
if (window.viewMode === 'gallery') {
|
||
renderGalleryView(window.currentFolder);
|
||
} else {
|
||
renderFileTable(window.currentFolder);
|
||
}
|
||
};
|
||
|
||
window.changeItemsPerPage = function (newCount) {
|
||
window.itemsPerPage = parseInt(newCount, 10);
|
||
localStorage.setItem('itemsPerPage', newCount);
|
||
window.currentPage = 1;
|
||
if (window.viewMode === 'gallery') {
|
||
renderGalleryView(window.currentFolder);
|
||
} else {
|
||
renderFileTable(window.currentFolder);
|
||
}
|
||
};
|
||
|
||
// fileListView.js (bottom)
|
||
window.loadFileList = loadFileList;
|
||
window.renderFileTable = renderFileTable;
|
||
window.renderGalleryView = renderGalleryView;
|
||
window.sortFiles = sortFiles;
|
||
window.toggleAdvancedSearch = toggleAdvancedSearch; |