Gallery view enhancements

This commit is contained in:
Ryan
2025-04-13 20:48:34 -04:00
committed by GitHub
parent a9c4200827
commit a897d1734f
5 changed files with 322 additions and 109 deletions

View File

@@ -10,6 +10,41 @@
- Added translations and data attributes for almost all user-facing text - Added translations and data attributes for almost all user-facing text
- Extend i18n support: Add new translation keys for Download and Share modals - Extend i18n support: Add new translation keys for Download and Share modals
- **Slider Integration:**
- Added a slider UI (range input, label, and value display) directly above the gallery grid.
- The slider allows users to adjust the number of columns in the gallery from 1 to 6.
- **Dynamic Grid Updates:**
- The gallery grids CSS is updated in real time via the sliders value by setting the grid-template-columns property.
- As the slider value changes, the layout instantly reflects the new column count.
- **Dynamic Image Resizing:**
- Introduced a helper function (getMaxImageHeight) that calculates the maximum image height based on the current column count.
- The max height of each image is updated immediately when the slider is adjusted to create a more dynamic display.
- **Image Caching:**
- Implemented an image caching mechanism using a global window.imageCache object.
- Images are cached on load (via an onload event) to prevent unnecessary reloading, improving performance.
- **Event Handling:**
- The sliders event listener is set up to update both the gallery grid layout and the dimensions of the thumbnails dynamically.
- Share button event listeners remain attached for proper functionality across the updated gallery view.
- **Input Validation & Security:**
- Used `filter_input()` to sanitize and validate incoming GET parameters (token, pass, page).
- Validated file system paths using `realpath()` and ensured the shared folder lies within `UPLOAD_DIR`.
- Escaped all dynamic outputs with `htmlspecialchars()` to prevent XSS.
- **Share Link Verification:**
- Loaded and validated share records from the JSON file.
- Handled expiration and password protection (with proper HTTP status codes for errors).
- **Pagination:**
- Implemented pagination by slicing the full file list into a limited number of files per page (default of 10).
- Calculated total pages and current page to create navigation links.
- **View Toggle (List vs. Gallery):**
- Added a toggle button that switches between a traditional list view and a gallery view.
- Maintained two separate view containers (`#listViewContainer` and `#galleryViewContainer`) to support this switching.
- **Gallery View with Image Caching:**
- For the gallery view, implemented a JavaScript function that creates a grid of image thumbnails.
- Each image uses a cache-busting query string on first load and caches its URL in a global `window.imageCache` for subsequent renders.
- **Persistent Pagination Controls:**
- Moved the pagination controls outside the individual view containers so that they remain visible regardless of the selected view.
--- ---
## Changes 4/12/2025 ## Changes 4/12/2025

View File

@@ -90,6 +90,13 @@ function toggleAdvancedSearch() {
renderFileTable(window.currentFolder); 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 --- * --- Fuse.js Search Helper ---
* Uses Fuse.js to perform a fuzzy search on fileData. * Uses Fuse.js to perform a fuzzy search on fileData.
@@ -388,76 +395,142 @@ export function renderFileTable(folder, container) {
/** /**
* Similarly, update renderGalleryView to accept an optional container. * Similarly, update renderGalleryView to accept an optional container.
*/ */
export function renderGalleryView(folder, container) { // A helper to compute the max image height based on the current column count.
function getMaxImageHeight() {
// Use the slider value (default to 3 if undefined).
const columns = parseInt(window.galleryColumns || 3, 10);
// Simple scaling: fewer columns yield bigger images.
// For instance, if columns === 6, max-height is 150px,
// and if columns === 1, max-height could be 150 * 6 = 900px.
return 150 * (7 - columns); // adjust the multiplier as needed.
}
export function renderGalleryView(folder, container) {
const fileListContent = container || document.getElementById("fileList"); const fileListContent = container || document.getElementById("fileList");
const searchTerm = (window.currentSearchTerm || "").toLowerCase(); const searchTerm = (window.currentSearchTerm || "").toLowerCase();
// Use Fuse.js search for gallery view as well.
const filteredFiles = searchFiles(searchTerm); const filteredFiles = searchFiles(searchTerm);
const folderPath = folder === "root" const folderPath = folder === "root"
? "uploads/" ? "uploads/"
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/"; : "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 = `<div class="gallery-container" style="${gridStyle}">`; // Use the current global column value (default to 3).
const numColumns = window.galleryColumns || 3;
// --- Insert slider controls ---
// Build the slider HTML.
const sliderHTML = `
<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>
`;
// Set up the grid container using the slider's value.
const gridStyle = `display: grid; grid-template-columns: repeat(${numColumns}, 1fr); gap: 10px; padding: 10px;`;
// Build the gallery container HTML and include the slider above it.
let galleryHTML = sliderHTML;
galleryHTML += `<div class="gallery-container" style="${gridStyle}">`;
filteredFiles.forEach((file) => { filteredFiles.forEach((file) => {
let thumbnail; let thumbnail;
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) { if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
thumbnail = `<img src="${folderPath + encodeURIComponent(file.name)}?t=${new Date().getTime()}" class="gallery-thumbnail" alt="${escapeHTML(file.name)}" style="max-width: 100%; max-height: 150px; display: block; margin: 0 auto;">`; const cacheKey = folderPath + encodeURIComponent(file.name);
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) { if (window.imageCache && window.imageCache[cacheKey]) {
thumbnail = `<span class="material-icons gallery-icon">audiotrack</span>`; 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 { } else {
thumbnail = `<span class="material-icons gallery-icon">insert_drive_file</span>`; const imageUrl = folderPath + encodeURIComponent(file.name) + "?t=" + new Date().getTime();
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;">`;
} }
let tagBadgesHTML = ""; } else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
if (file.tags && file.tags.length > 0) { thumbnail = `<span class="material-icons gallery-icon">audiotrack</span>`;
tagBadgesHTML = `<div class="tag-badges" style="margin-top:4px;">`; } else {
file.tags.forEach(tag => { thumbnail = `<span class="material-icons gallery-icon">insert_drive_file</span>`;
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>`; let tagBadgesHTML = "";
} if (file.tags && file.tags.length > 0) {
galleryHTML += `<div class="gallery-card" style="border: 1px solid #ccc; padding: 5px; text-align: center;"> tagBadgesHTML = `<div class="tag-badges" style="margin-top:4px;">`;
<div class="gallery-preview" style="cursor: pointer;" onclick="previewFile('${folderPath + encodeURIComponent(file.name)}?t=' + new Date().getTime(), '${file.name}')"> file.tags.forEach(tag => {
${thumbnail} 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>`;
</div> });
<div class="gallery-info" style="margin-top: 5px;"> tagBadgesHTML += `</div>`;
<span class="gallery-file-name" style="display: block; white-space: normal; overflow-wrap: break-word; word-wrap: break-word;">${escapeHTML(file.name)}</span> }
${tagBadgesHTML}
<div class="button-wrap" style="display: flex; justify-content: center; gap: 5px;"> galleryHTML += `
<button type="button" class="btn btn-sm btn-success download-btn" <div class="gallery-card" style="border: 1px solid #ccc; padding: 5px; text-align: center;">
onclick="openDownloadModal('${file.name}', '${file.folder || 'root'}')" <div class="gallery-preview" style="cursor: pointer;" onclick="previewFile('${folderPath + encodeURIComponent(file.name)}?t=' + new Date().getTime(), '${file.name}')">
title="${t('download')}"> ${thumbnail}
<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> <div class="gallery-info" style="margin-top: 5px;">
</div>`; <span class="gallery-file-name" style="display: block; white-space: normal; overflow-wrap: break-word; word-wrap: break-word;">
${escapeHTML(file.name)}
</span>
${tagBadgesHTML}
<div class="button-wrap" style="display: flex; justify-content: center; gap: 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>"; galleryHTML += "</div>"; // End gallery container.
fileListContent.innerHTML = galleryHTML; fileListContent.innerHTML = galleryHTML;
createViewToggleButton(); createViewToggleButton();
updateFileActionButtons(); updateFileActionButtons();
// Attach share button event listeners.
document.querySelectorAll(".share-btn").forEach(btn => { document.querySelectorAll(".share-btn").forEach(btn => {
btn.addEventListener("click", e => { btn.addEventListener("click", e => {
e.stopPropagation(); e.stopPropagation();
const fileName = btn.getAttribute("data-file"); const fileName = btn.getAttribute("data-file");
const file = fileData.find(f => f.name === fileName); const file = fileData.find(f => f.name === fileName);
import('./filePreview.js').then(module => { if (file) {
module.openShareModal(file, folder); import('./filePreview.js').then(module => {
}); module.openShareModal(file, folder);
}); });
}
});
}); });
}
// --- Slider Event Listener ---
const slider = document.getElementById("galleryColumnsSlider");
if (slider) {
slider.addEventListener("input", function() {
const value = this.value;
// Update the slider display.
document.getElementById("galleryColumnsValue").textContent = value;
// Update global value so new renders use the correct value.
window.galleryColumns = value;
// Update the grid columns.
const galleryContainer = document.querySelector(".gallery-container");
if (galleryContainer) {
galleryContainer.style.gridTemplateColumns = `repeat(${value}, 1fr)`;
}
// Compute the new max image height.
const newMaxHeight = getMaxImageHeight();
document.querySelectorAll(".gallery-thumbnail").forEach(img => {
img.style.maxHeight = newMaxHeight + "px";
});
});
}
}
export function sortFiles(column, folder) { export function sortFiles(column, folder) {
if (sortOrder.column === column) { if (sortOrder.column === column) {

View File

@@ -236,7 +236,8 @@ const translations = {
"please_save_recovery_code": "Please save this code securely. It will not be shown again and can only be used once.", "please_save_recovery_code": "Please save this code securely. It will not be shown again and can only be used once.",
"ok": "OK", "ok": "OK",
"show": "Show", "show": "Show",
"items_per_page": "items per page" "items_per_page": "items per page",
"columns":"Columns"
}, },
es: { es: {
"please_log_in_to_continue": "Por favor, inicie sesión para continuar.", "please_log_in_to_continue": "Por favor, inicie sesión para continuar.",
@@ -463,7 +464,8 @@ const translations = {
"save_permissions": "Guardar permisos", "save_permissions": "Guardar permisos",
"your_recovery_code": "Su código de recuperación", "your_recovery_code": "Su código de recuperación",
"please_save_recovery_code": "Por favor, guarde este código de forma segura. No se mostrará de nuevo y solo podrá usarse una vez.", "please_save_recovery_code": "Por favor, guarde este código de forma segura. No se mostrará de nuevo y solo podrá usarse una vez.",
"ok": "OK" "ok": "OK",
"columns": "Columnas"
}, },
fr: { fr: {
"please_log_in_to_continue": "Veuillez vous connecter pour continuer.", "please_log_in_to_continue": "Veuillez vous connecter pour continuer.",
@@ -690,7 +692,8 @@ const translations = {
"save_permissions": "Enregistrer les permissions", "save_permissions": "Enregistrer les permissions",
"your_recovery_code": "Votre code de récupération", "your_recovery_code": "Votre code de récupération",
"please_save_recovery_code": "Veuillez sauvegarder ce code en toute sécurité. Il ne sera plus affiché et ne pourra être utilisé qu'une seule fois.", "please_save_recovery_code": "Veuillez sauvegarder ce code en toute sécurité. Il ne sera plus affiché et ne pourra être utilisé qu'une seule fois.",
"ok": "OK" "ok": "OK",
"columns": "Colonnes"
}, },
de: { de: {
"please_log_in_to_continue": "Bitte melden Sie sich an, um fortzufahren.", "please_log_in_to_continue": "Bitte melden Sie sich an, um fortzufahren.",
@@ -928,7 +931,8 @@ const translations = {
"please_save_recovery_code": "Bitte speichern Sie diesen Code sicher. Er wird nicht erneut angezeigt und kann nur einmal verwendet werden.", "please_save_recovery_code": "Bitte speichern Sie diesen Code sicher. Er wird nicht erneut angezeigt und kann nur einmal verwendet werden.",
"ok": "OK", "ok": "OK",
"show": "Zeige", "show": "Zeige",
"items_per_page": "elemente pro seite" "items_per_page": "elemente pro seite",
"columns": "Spalten"
} }
}; };

View File

@@ -17,7 +17,7 @@ if (empty($token)) {
exit; exit;
} }
// Load share folder records. // Load share folder records securely.
$shareFile = META_DIR . "share_folder_links.json"; $shareFile = META_DIR . "share_folder_links.json";
if (!file_exists($shareFile)) { if (!file_exists($shareFile)) {
http_response_code(404); http_response_code(404);
@@ -139,7 +139,7 @@ $allFiles = array_values(array_filter(scandir($realFolderPath), function($item)
})); }));
sort($allFiles); sort($allFiles);
// Pagination variables. // Pagination variables — limits the number of files (and thus images) per page.
$itemsPerPage = 10; $itemsPerPage = 10;
$totalFiles = count($allFiles); $totalFiles = count($allFiles);
$totalPages = max(1, ceil($totalFiles / $itemsPerPage)); $totalPages = max(1, ceil($totalFiles / $itemsPerPage));
@@ -195,6 +195,20 @@ function formatBytes($bytes) {
padding: 20px; padding: 20px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1); box-shadow: 0 2px 12px rgba(0,0,0,0.1);
} }
/* Toggle button */
.toggle-btn {
margin-bottom: 20px;
padding: 8px 16px;
background: #007BFF;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
.toggle-btn:hover {
background: #0056b3;
}
/* List view table styles */
table { table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
@@ -220,13 +234,12 @@ function formatBytes($bytes) {
a:hover { a:hover {
text-decoration: underline; text-decoration: underline;
} }
/* Simple download icon style */
.download-icon { .download-icon {
margin-left: 8px; margin-left: 8px;
font-weight: bold; font-weight: bold;
color: #007BFF; color: #007BFF;
} }
/* Pagination styles */ /* Pagination styles - placed outside the view containers. */
.pagination { .pagination {
text-align: center; text-align: center;
margin-top: 20px; margin-top: 20px;
@@ -242,6 +255,24 @@ function formatBytes($bytes) {
.pagination span.current { .pagination span.current {
background: #0056b3; background: #0056b3;
} }
/* Gallery view styles */
.shared-gallery-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px;
padding: 10px 0;
}
.shared-gallery-card {
border: 1px solid #ccc;
padding: 5px;
text-align: center;
}
.shared-gallery-card img {
max-width: 100%;
display: block;
margin: 0 auto;
}
/* Upload container */
.upload-container { .upload-container {
margin-top: 30px; margin-top: 30px;
text-align: center; text-align: center;
@@ -279,22 +310,27 @@ function formatBytes($bytes) {
<h1>Shared Folder: <?php echo htmlspecialchars($folder, ENT_QUOTES, 'UTF-8'); ?></h1> <h1>Shared Folder: <?php echo htmlspecialchars($folder, ENT_QUOTES, 'UTF-8'); ?></h1>
</div> </div>
<div class="container"> <div class="container">
<?php if (empty($filesOnPage)): ?> <!-- Toggle Button -->
<p style="text-align:center;">This folder is empty.</p> <button id="toggleBtn" class="toggle-btn" onclick="toggleViewMode()">Switch to Gallery View</button>
<?php else: ?>
<table> <!-- View Containers -->
<thead> <div id="listViewContainer">
<tr> <?php if (empty($filesOnPage)): ?>
<th>Filename</th> <p style="text-align:center;">This folder is empty.</p>
<th>Size</th> <?php else: ?>
</tr> <table>
</thead> <thead>
<tbody> <tr>
<?php foreach ($filesOnPage as $file): <th>Filename</th>
$filePath = $realFolderPath . DIRECTORY_SEPARATOR . $file; <th>Size</th>
$fileSize = formatBytes(filesize($filePath)); </tr>
// Build download link using share token and file name. </thead>
$downloadLink = "downloadSharedFile.php?token=" . urlencode($token) . "&file=" . urlencode($file); <tbody>
<?php foreach ($filesOnPage as $file):
$filePath = $realFolderPath . DIRECTORY_SEPARATOR . $file;
$fileSize = formatBytes(filesize($filePath));
// Build download link using share token and file name.
$downloadLink = "downloadSharedFile.php?token=" . urlencode($token) . "&file=" . urlencode($file);
?> ?>
<tr> <tr>
<td> <td>
@@ -305,37 +341,41 @@ function formatBytes($bytes) {
</td> </td>
<td><?php echo $fileSize; ?></td> <td><?php echo $fileSize; ?></td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
</table> </table>
<!-- Pagination Controls --> <?php endif; ?>
<div class="pagination"> </div>
<?php if ($currentPage > 1): ?>
<a href="shareFolder.php?token=<?php echo urlencode($token); ?>&page=<?php echo $currentPage - 1; ?><?php echo !empty($providedPass) ? "&pass=" . urlencode($providedPass) : ""; ?>">Prev</a> <!-- Gallery View Container (hidden by default) -->
<div id="galleryViewContainer" style="display:none;"></div>
<!-- Pagination Controls (always visible) -->
<div class="pagination">
<?php if ($currentPage > 1): ?>
<a href="shareFolder.php?token=<?php echo urlencode($token); ?>&page=<?php echo $currentPage - 1; ?><?php echo !empty($providedPass) ? "&pass=" . urlencode($providedPass) : ""; ?>">Prev</a>
<?php else: ?>
<span>Prev</span>
<?php endif; ?>
<?php
$startPage = max(1, $currentPage - 2);
$endPage = min($totalPages, $currentPage + 2);
for ($i = $startPage; $i <= $endPage; $i++): ?>
<?php if ($i == $currentPage): ?>
<span class="current"><?php echo $i; ?></span>
<?php else: ?> <?php else: ?>
<span>Prev</span> <a href="shareFolder.php?token=<?php echo urlencode($token); ?>&page=<?php echo $i; ?><?php echo !empty($providedPass) ? "&pass=" . urlencode($providedPass) : ""; ?>"><?php echo $i; ?></a>
<?php endif; ?> <?php endif; ?>
<?php endfor; ?>
<?php <?php if ($currentPage < $totalPages): ?>
// Display up to 5 page links centered around the current page. <a href="shareFolder.php?token=<?php echo urlencode($token); ?>&page=<?php echo $currentPage + 1; ?><?php echo !empty($providedPass) ? "&pass=" . urlencode($providedPass) : ""; ?>">Next</a>
$startPage = max(1, $currentPage - 2); <?php else: ?>
$endPage = min($totalPages, $currentPage + 2); <span>Next</span>
for ($i = $startPage; $i <= $endPage; $i++): ?> <?php endif; ?>
<?php if ($i == $currentPage): ?> </div>
<span class="current"><?php echo $i; ?></span>
<?php else: ?>
<a href="shareFolder.php?token=<?php echo urlencode($token); ?>&page=<?php echo $i; ?><?php echo !empty($providedPass) ? "&pass=" . urlencode($providedPass) : ""; ?>"><?php echo $i; ?></a>
<?php endif; ?>
<?php endfor; ?>
<?php if ($currentPage < $totalPages): ?>
<a href="shareFolder.php?token=<?php echo urlencode($token); ?>&page=<?php echo $currentPage + 1; ?><?php echo !empty($providedPass) ? "&pass=" . urlencode($providedPass) : ""; ?>">Next</a>
<?php else: ?>
<span>Next</span>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($record['allowUpload']) : ?> <?php if ($record['allowUpload']) : ?>
<div class="upload-container"> <div class="upload-container">
<h3>Upload File (50mb max size)</h3> <h3>Upload File (50mb max size)</h3>
@@ -352,5 +392,66 @@ function formatBytes($bytes) {
<div class="footer"> <div class="footer">
&copy; <?php echo date("Y"); ?> FileRise. All rights reserved. &copy; <?php echo date("Y"); ?> FileRise. All rights reserved.
</div> </div>
<script>
// Global variables
var viewMode = 'list';
window.imageCache = window.imageCache || {};
// File data for the gallery view (current page file names)
// Since the server-side pagination limits the files per page, gallery view shows the same files.
var filesData = <?php echo json_encode($filesOnPage); ?>;
// Define the relative URL for this shared folder's files.
var filesUrlBase = "uploads/<?php echo htmlspecialchars($folder, ENT_QUOTES, 'UTF-8'); ?>";
// Helper function to cache image URLs.
function cacheImage(imgElem, key) {
window.imageCache[key] = imgElem.src;
}
// Render gallery view using filesData.
function renderGalleryView() {
var galleryContainer = document.getElementById("galleryViewContainer");
var html = '<div class="shared-gallery-container">';
filesData.forEach(function(file) {
var fileUrl = filesUrlBase + "/" + encodeURIComponent(file);
var ext = file.split('.').pop().toLowerCase();
var thumbnail = "";
if (['jpg','jpeg','png','gif','bmp','webp','svg','ico'].indexOf(ext) >= 0) {
var cacheKey = fileUrl;
if (window.imageCache[cacheKey]) {
thumbnail = '<img src="'+window.imageCache[cacheKey]+'" alt="'+file+'">';
} else {
var imageUrl = fileUrl + '?t=' + new Date().getTime();
thumbnail = '<img src="'+imageUrl+'" onload="cacheImage(this, \''+cacheKey+'\')" alt="'+file+'">';
}
} else {
thumbnail = '<span class="material-icons">insert_drive_file</span>';
}
html += '<div class="shared-gallery-card">';
html += '<div class="gallery-preview" onclick="window.location.href=\''+fileUrl+'\'" style="cursor:pointer;">'+ thumbnail +'</div>';
html += '<div class="gallery-info"><span class="gallery-file-name">'+file+'</span></div>';
html += '</div>';
});
html += '</div>';
galleryContainer.innerHTML = html;
}
// Toggle between list and gallery views.
function toggleViewMode() {
if (viewMode === 'list') {
viewMode = 'gallery';
document.getElementById("listViewContainer").style.display = "none";
renderGalleryView();
document.getElementById("galleryViewContainer").style.display = "block";
document.getElementById("toggleBtn").textContent = "Switch to List View";
} else {
viewMode = 'list';
document.getElementById("galleryViewContainer").style.display = "none";
document.getElementById("listViewContainer").style.display = "block";
document.getElementById("toggleBtn").textContent = "Switch to Gallery View";
}
}
</script>
</body> </body>
</html> </html>

View File

@@ -75,7 +75,7 @@ if ($fileUpload['size'] > $maxSize) {
} }
// Define allowed file extensions. // Define allowed file extensions.
$allowedExtensions = ['jpg','jpeg','png','gif','pdf','doc','docx','txt','xls','xlsx','ppt','pptx','mp4','webm','mp3']; $allowedExtensions = ['jpg','jpeg','png','gif','pdf','doc','docx','txt','xls','xlsx','ppt','pptx','mp4','webm','mp3','mkv'];
$uploadedName = basename($fileUpload['name']); $uploadedName = basename($fileUpload['name']);
$ext = strtolower(pathinfo($uploadedName, PATHINFO_EXTENSION)); $ext = strtolower(pathinfo($uploadedName, PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExtensions)) { if (!in_array($ext, $allowedExtensions)) {