From 4fa5faa2bf1f9475c8f50120adc926adc5c5f1c4 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 10 Apr 2025 00:45:35 -0400 Subject: [PATCH] =?UTF-8?q?Shift=20Key=20Multi=E2=80=91Selection=20&=20Tot?= =?UTF-8?q?al=20Files=20and=20File=20Size?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 34 +++++++++- LICENSE | 2 +- README.md | 2 +- css/styles.css | 25 +++++++- js/domUtils.js | 58 +++++++++++++++-- js/fileListView.js | 152 +++++++++++++++++++++++++++++++-------------- shareFolder.php | 24 ++++++- 7 files changed, 239 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5533370..acf6e40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,38 @@ # Changelog -## Folder Sharing Feature - Changelog 4/9/2025 +## Shift Key Multi‑Selection Changes 4/10/2025 + +- **Implemented Range Selection:** + - Modified the `toggleRowSelection` function so that when the Shift key is held down, all rows between the last clicked (anchor) row (stored as `window.lastSelectedFileRow`) and the currently clicked row are selected. +- **Modifier Handling:** + - Regular clicks (or Ctrl/Cmd clicks) simply toggle the clicked row without clearing other selections. +- **Prevented Default Browser Behavior:** + - Added `event.preventDefault()` in the Shift‑click branch to avoid unwanted text selection. +- **Maintaining the Anchor:** + - The last clicked row is stored for future range selections. + +## Total Files and File Size Summary + +- **Size Calculation:** + - Created `parseSizeToBytes(sizeStr)` to convert file size strings (e.g. `"456.9KB"`, `"1.2 MB"`) into a numerical byte value. + - Created `formatSize(totalBytes)` to format a byte value into a human‑readable string (choosing between Bytes, KB, MB, or GB). + - Created `buildFolderSummary(filteredFiles)` to: + - Sum the sizes of all files (using `parseSizeToBytes`). + - Count the total number of files. +- **Dynamic Display in `loadFileList`:** + - Updated `loadFileList` to update a summary element (with `id="fileSummary"`) inside the `#fileListActions` container when files are present. + - When no files are found, the summary element is hidden (setting its `display` to `"none"` or clearing the container). +- **Responsive Styling:** + - Added CSS media queries to the `#fileSummary` element so that on small screens it is centered and any extra side margins are removed. Dark and light mode supported. + +- **Other changes** + + - `shareFolder.php` updated to display format size. + - Fix to prevent the filename text from overflowing its container in the gallery view. + +--- + +## Folder Sharing Feature - Changelog 4/9/2025 v1.1.0 ### New Endpoints diff --git a/LICENSE b/LICENSE index 036a9ee..7089ac5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 SeNS +Copyright (c) 2025 FileRise Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 8feeaa0..cee6dff 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Upload, organize, and share files through a sleek web interface. **FileRise** is --- -## Features at a Glance or [Full Feature Wiki](https://github.com/error311/FileRise/wiki/Features) +## Features at a Glance or [Full Features Wiki](https://github.com/error311/FileRise/wiki/Features) - 🚀 **Easy File Uploads:** Upload multiple files and folders via drag & drop or file picker. Supports large files with pause/resumable chunked uploads and shows real-time progress for each file. No more failed transfers – FileRise will pick up where it left off if your connection drops. diff --git a/css/styles.css b/css/styles.css index 870baef..c44c765 100644 --- a/css/styles.css +++ b/css/styles.css @@ -2134,4 +2134,27 @@ body.dark-mode .header-drop-zone.drag-active { content: "Drop"; font-size: 10px; color: #aaa; -} \ No newline at end of file +} + +/* Disable text selection on rows to prevent accidental copying when shift-clicking */ +#fileList tbody tr.clickable-row { + -webkit-user-select: none; /* Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+/Edge */ + user-select: none; /* Standard */ +} + +#fileSummary { + color: black; +} +@media only screen and (max-width: 600px) { + #fileSummary { + float: none !important; + margin: 0 auto !important; + text-align: center !important; + } +} + +body.dark-mode #fileSummary { + color: white; +} diff --git a/js/domUtils.js b/js/domUtils.js index 91188ab..0490cb4 100644 --- a/js/domUtils.js +++ b/js/domUtils.js @@ -223,15 +223,63 @@ export function updateRowHighlight(checkbox) { } export function toggleRowSelection(event, fileName) { + // Prevent default text selection when shift is held. + if (event.shiftKey) { + event.preventDefault(); + } + + // Ignore clicks on interactive elements. const targetTag = event.target.tagName.toLowerCase(); - if (targetTag === 'a' || targetTag === 'button' || targetTag === 'input') { + if (["a", "button", "input"].includes(targetTag)) { return; } + + // Get the clicked row and its checkbox. const row = event.currentTarget; - const checkbox = row.querySelector('.file-checkbox'); + const checkbox = row.querySelector(".file-checkbox"); if (!checkbox) return; - checkbox.checked = !checkbox.checked; - updateRowHighlight(checkbox); + + // Get all rows in the current file list view. + const allRows = Array.from(document.querySelectorAll("#fileList tbody tr")); + + // Helper: clear all selections (not used in this updated version). + const clearAllSelections = () => { + allRows.forEach(r => { + const cb = r.querySelector(".file-checkbox"); + if (cb) { + cb.checked = false; + updateRowHighlight(cb); + } + }); + }; + + // If the user is holding the Shift key, perform range selection. + if (event.shiftKey) { + // Use the last clicked row as the anchor. + const lastRow = window.lastSelectedFileRow || row; + const currentIndex = allRows.indexOf(row); + const lastIndex = allRows.indexOf(lastRow); + const start = Math.min(currentIndex, lastIndex); + const end = Math.max(currentIndex, lastIndex); + + // If neither CTRL nor Meta is pressed, you might choose + // to clear existing selections. For this example we leave existing selections intact. + for (let i = start; i <= end; i++) { + const cb = allRows[i].querySelector(".file-checkbox"); + if (cb) { + cb.checked = true; + updateRowHighlight(cb); + } + } + } + // Otherwise, for all non-shift clicks simply toggle the selected state. + else { + checkbox.checked = !checkbox.checked; + updateRowHighlight(checkbox); + } + + // Update the anchor row to the row that was clicked. + window.lastSelectedFileRow = row; updateFileActionButtons(); } @@ -241,7 +289,7 @@ export function attachEnterKeyListener(modalId, buttonId) { // Make the modal focusable modal.setAttribute("tabindex", "-1"); modal.focus(); - modal.addEventListener("keydown", function(e) { + modal.addEventListener("keydown", function (e) { if (e.key === "Enter") { e.preventDefault(); const btn = document.getElementById(buttonId); diff --git a/js/fileListView.js b/js/fileListView.js index 6f3c7a5..6f919f5 100644 --- a/js/fileListView.js +++ b/js/fileListView.js @@ -15,6 +15,7 @@ import { 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 }; @@ -23,9 +24,63 @@ window.itemsPerPage = window.itemsPerPage || 10; window.currentPage = window.currentPage || 1; window.viewMode = localStorage.getItem("viewMode") || "table"; // "table" or "gallery" -// ----------------------------- -// VIEW MODE TOGGLE BUTTON & Helpers -// ----------------------------- +/** + * --- Helper Functions --- + */ + +/** + * Convert a file size string (e.g. "456.9KB", "1.2 MB", "1024") into bytes. + */ +function parseSizeToBytes(sizeStr) { + if (!sizeStr) return 0; + // Remove any whitespace + let s = sizeStr.trim(); + // Extract the numerical part. + let value = parseFloat(s); + // Determine if there is a unit. Convert the unit to uppercase for easier matching. + 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, choosing an appropriate unit. + */ +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. + * This function sums the file sizes in bytes correctly, then formats the total. + */ +function buildFolderSummary(filteredFiles) { + const totalFiles = filteredFiles.length; + const totalBytes = filteredFiles.reduce((sum, file) => { + // file.size might be something like "456.9KB" or just "1024". + return sum + parseSizeToBytes(file.size); + }, 0); + const sizeStr = formatSize(totalBytes); + return `Total Files: ${totalFiles}  |  Total Size: ${sizeStr}`; +} + +/** + * --- VIEW MODE TOGGLE BUTTON & Helpers --- + */ export function createViewToggleButton() { let toggleBtn = document.getElementById("toggleViewBtn"); if (!toggleBtn) { @@ -58,11 +113,9 @@ export function formatFolderName(folder) { window.toggleRowSelection = toggleRowSelection; window.updateRowHighlight = updateRowHighlight; -import { openTagModal, openMultiTagModal } from './fileTags.js'; - -// ----------------------------- -// FILE LIST & VIEW RENDERING -// ----------------------------- +/** + * --- FILE LIST & VIEW RENDERING --- + */ export function loadFileList(folderParam) { const folder = folderParam || "root"; const fileListContainer = document.getElementById("fileList"); @@ -80,7 +133,7 @@ export function loadFileList(folderParam) { return response.json(); }) .then(data => { - fileListContainer.innerHTML = ""; + fileListContainer.innerHTML = ""; // Clear loading message. if (data.files && data.files.length > 0) { data.files = data.files.map(file => { file.fullName = (file.path || file.name).trim().toLowerCase(); @@ -92,6 +145,26 @@ export function loadFileList(folderParam) { return file; }); fileData = data.files; + + // Update the file list actions area without removing existing buttons. + 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 the view normally. if (window.viewMode === "gallery") { renderGalleryView(folder); } else { @@ -99,6 +172,10 @@ export function loadFileList(folderParam) { } } else { fileListContainer.textContent = t("no_files_found"); + const summaryElem = document.getElementById("fileSummary"); + if (summaryElem) { + summaryElem.style.display = "none"; + } updateFileActionButtons(); } return data.files || []; @@ -115,8 +192,12 @@ export function loadFileList(folderParam) { }); } -export function renderFileTable(folder) { - const fileListContainer = document.getElementById("fileList"); +/** + * Update renderFileTable so that it writes its content into the provided container. + * If no container is provided, it defaults to the element with id "fileList". + */ +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; @@ -126,14 +207,12 @@ export function renderFileTable(folder) { const tagMatch = file.tags && file.tags.some(tag => tag.name.toLowerCase().includes(searchTerm)); return nameMatch || tagMatch; }); - 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("/") + "/"; @@ -147,7 +226,6 @@ export function renderFileTable(folder) { const startIndex = (currentPage - 1) * itemsPerPageSetting; const endIndex = Math.min(startIndex + itemsPerPageSetting, totalFiles); let rowsHTML = ""; - if (totalFiles > 0) { filteredFiles.slice(startIndex, endIndex).forEach((file, idx) => { let rowHTML = buildFileTableRow(file, folderPath); @@ -161,15 +239,12 @@ export function renderFileTable(folder) { }); tagBadgesHTML += ""; } - rowHTML = rowHTML.replace(/()(.*?)(<\/td>)/, (match, p1, p2, p3) => { return p1 + p2 + tagBadgesHTML + p3; }); - rowHTML = rowHTML.replace(/(<\/div>\s*<\/td>\s*<\/tr>)/, `$1`); - + share + $1`); rowsHTML += rowHTML; }); } else { @@ -177,16 +252,18 @@ export function renderFileTable(folder) { } rowsHTML += ""; const bottomControlsHTML = buildBottomControls(itemsPerPageSetting); - fileListContainer.innerHTML = topControlsHTML + headerHTML + rowsHTML + bottomControlsHTML; + + fileListContent.innerHTML = topControlsHTML + headerHTML + rowsHTML + bottomControlsHTML; createViewToggleButton(); + // Setup event listeners as before... const newSearchInput = document.getElementById("searchInput"); if (newSearchInput) { newSearchInput.addEventListener("input", debounce(function () { window.currentSearchTerm = newSearchInput.value; window.currentPage = 1; - renderFileTable(folder); + renderFileTable(folder, container); setTimeout(() => { const freshInput = document.getElementById("searchInput"); if (freshInput) { @@ -197,21 +274,18 @@ export function renderFileTable(folder) { }, 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(); @@ -224,40 +298,34 @@ export function renderFileTable(folder) { } }); }); - updateFileActionButtons(); - - // Add drag-and-drop support for each table row. - document.querySelectorAll("#fileList tbody tr").forEach(row => { + document.querySelectorAll("#fileListContent tbody tr").forEach(row => { row.setAttribute("draggable", "true"); import('./fileDragDrop.js').then(module => { row.addEventListener("dragstart", module.fileDragStartHandler); }); }); - - // Prevent clicks on these buttons from selecting the row document.querySelectorAll(".download-btn, .edit-btn, .rename-btn").forEach(btn => { btn.addEventListener("click", e => e.stopPropagation()); }); - - // re‑bind context menu bindFileListContextMenu(); } -export function renderGalleryView(folder) { - const fileListContainer = document.getElementById("fileList"); +/** + * Similarly, update renderGalleryView to accept an optional container. + */ +export function renderGalleryView(folder, container) { + const fileListContent = container || document.getElementById("fileList"); const searchTerm = (window.currentSearchTerm || "").toLowerCase(); const filteredFiles = fileData.filter(file => { return file.name.toLowerCase().includes(searchTerm) || (file.tags && file.tags.some(tag => tag.name.toLowerCase().includes(searchTerm))); }); - const folderPath = folder === "root" ? "uploads/" : "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/"; const gridStyle = "display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; padding: 10px;"; let galleryHTML = `