/** * @fileoverview Manages the file grid view functionality. * This file handles: * - File grid and list view rendering * - File sorting and filtering * - File operations (star, restore, delete) * - View preferences * - Search functionality * - File details display */ import { FilePreview } from './components/filePreview.js'; /** * Formats a file size in bytes to a human-readable string. * @param {number} bytes - The file size in bytes * @returns {string} Formatted file size string */ function formatFileSize(bytes) { if (!bytes) return '0 B'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(1)} ${units[unitIndex]}`; } let currentView = 'grid'; let lastSelectedIndex = -1; let sortColumn = 'name'; // Set default sort column to name let sortDirection = 1; // 1 for ascending, -1 for descending let batchDeleteItems = null; let currentFiles = []; let fileToDelete = null; window.isAdmin = document.body.dataset.isAdmin === 'true'; // Check if we're on the trash page const isTrashPage = window.location.pathname.includes('/trash'); // Initialize FilePreview component const filePreview = new FilePreview({ containerId: 'filePreviewModal', onClose: () => { // Clean up any resources if needed } }); /** * Initializes the file view and fetches files. * Sets up the preferred view and initial file sorting. * @async * @function */ async function initializeView() { try { const response = await fetch('/api/user/preferred_view'); const data = await response.json(); currentView = data.preferred_view || 'grid'; } catch (error) { console.error('Error fetching preferred view:', error); currentView = 'grid'; } // First fetch files await fetchFiles(); // Then toggle view after files are loaded toggleView(currentView); // Sort files by name by default sortFiles('name'); } /** * Toggles between grid and list views. * Updates the UI and saves the user's view preference. * @function * @param {string} view - The view to switch to ('grid' or 'list') */ function toggleView(view) { currentView = view; const grid = document.getElementById('fileGrid'); const gridBtn = document.getElementById('gridViewBtn'); const listBtn = document.getElementById('listViewBtn'); if (view === 'grid') { grid.classList.remove('table-mode'); if (gridBtn) gridBtn.classList.add('active'); if (listBtn) listBtn.classList.remove('active'); } else { grid.classList.add('table-mode'); if (gridBtn) gridBtn.classList.remove('active'); if (listBtn) listBtn.classList.add('active'); } renderFiles(currentFiles); // Save the new preference const csrfToken = getCsrfToken(); if (!csrfToken) { console.error('CSRF token not available for saving view preference'); return; } fetch('/api/user/preferred_view', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ preferred_view: view }) }) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => { console.log('Preferred view saved:', data); }) .catch(error => { console.error('Error saving preferred view:', error); // Continue with the view change even if saving fails }); } /** * Sorts files by the specified column. * Handles different data types (string, number, date) appropriately. * @function * @param {string} column - The column to sort by ('name', 'modified', 'type', 'size', 'auto_delete') */ function sortFiles(column) { if (sortColumn === column) { sortDirection *= -1; // Toggle direction } else { sortColumn = column; sortDirection = 1; } currentFiles.sort((a, b) => { let valA = a[column]; let valB = b[column]; // For size, convert to number if (column === 'size') { valA = typeof valA === 'number' ? valA : 0; valB = typeof valB === 'number' ? valB : 0; } // For name/type, compare as strings if (typeof valA === 'string' && typeof valB === 'string') { return valA.localeCompare(valB) * sortDirection; } // For date (modified), compare as numbers return (valA - valB) * sortDirection; }); renderFiles(currentFiles); } /** * Gets the appropriate icon class for a file based on its extension. * @function * @param {string} filename - The name of the file * @returns {string} The Font Awesome icon class for the file type */ function getFileIcon(filename) { const extension = filename.split('.').pop().toLowerCase(); const iconMap = { pdf: 'fa-file-pdf', doc: 'fa-file-word', docx: 'fa-file-word', xls: 'fa-file-excel', xlsx: 'fa-file-excel', ppt: 'fa-file-powerpoint', pptx: 'fa-file-powerpoint', txt: 'fa-file-alt', jpg: 'fa-file-image', jpeg: 'fa-file-image', png: 'fa-file-image', gif: 'fa-file-image', zip: 'fa-file-archive', rar: 'fa-file-archive', mp3: 'fa-file-audio', mp4: 'fa-file-video' }; return iconMap[extension] || 'fa-file'; } /** * Renders the files in either grid or list view. * Handles both trash and normal file views with appropriate actions. * @function * @param {Array} files - Array of file objects to render */ function renderFiles(files) { if (!files) return; currentFiles = files; const grid = document.getElementById('fileGrid'); grid.innerHTML = ''; if (!files.length) { grid.innerHTML = '
No items found.
'; return; } if (currentView === 'list') { let table = ` ${isTrashPage ? `` : ''} `; files.forEach((file, idx) => { let icon = file.type === 'folder' ? `` : ``; let size = file.type === 'folder' ? '-' : formatFileSize(file.size); let dblClickAction = file.type === 'folder' ? `ondblclick='navigateToFolder("${file.name}")'` : ''; let actionsArr = []; // Add preview button for supported file types if (file.type !== 'folder') { const extension = file.name.split('.').pop().toLowerCase(); const supportedTypes = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'mp4', 'webm', 'mp3', 'wav']; if (supportedTypes.includes(extension)) { actionsArr.push(` `); } } if (isTrashPage) { actionsArr.push(``); actionsArr.push(``); } else { actionsArr.push(``); } actionsArr.push(``); const actions = actionsArr.join(''); table += ` ${isTrashPage ? `` : ''} `; }); table += '
Room Name ${(sortColumn==='name') ? (sortDirection===1?'▲':'▼') : ''} Date Modified ${(sortColumn==='modified') ? (sortDirection===1?'▲':'▼') : ''} Type ${(sortColumn==='type') ? (sortDirection===1?'▲':'▼') : ''} Size ${(sortColumn==='size') ? (sortDirection===1?'▲':'▼') : ''}Auto Delete ${(sortColumn==='auto_delete') ? (sortDirection===1?'▲':'▼') : ''}
${icon} ${file.name} ${formatDate(file.modified)} ${file.type} ${size}${file.auto_delete ? formatDate(file.auto_delete) : 'Never'}${actions}
'; grid.innerHTML = table; } else { files.forEach((file, idx) => { let icon = file.type === 'folder' ? `` : ``; let size = file.type === 'folder' ? '-' : formatFileSize(file.size); let dblClickAction = file.type === 'folder' ? `ondblclick='navigateToFolder("${file.name}")'` : ''; let actionsArr = []; // Add preview button for supported file types if (file.type !== 'folder') { const extension = file.name.split('.').pop().toLowerCase(); const supportedTypes = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'mp4', 'webm', 'mp3', 'wav']; if (supportedTypes.includes(extension)) { actionsArr.push(` `); } } if (isTrashPage) { actionsArr.push(``); actionsArr.push(``); } else { actionsArr.push(``); } actionsArr.push(``); const actions = actionsArr.join(''); grid.innerHTML += `
${icon}
${file.name}
${formatDate(file.modified)}
${size}
${isTrashPage ? `
Auto Delete: ${file.auto_delete ? formatDate(file.auto_delete) : 'Never'}
` : ''}
`; }); } } /** * Fetches files from the server. * Handles both trash and starred file endpoints. * @async * @function */ async function fetchFiles() { try { const endpoint = isTrashPage ? '/api/rooms/trash' : '/api/rooms/starred'; const response = await fetch(endpoint); const files = await response.json(); if (files) { window.currentFiles = files; // Sort files by name by default window.currentFiles.sort((a, b) => { if (a.name < b.name) return -1; if (a.name > b.name) return 1; return 0; }); renderFiles(files); } } catch (error) { console.error('Error loading files:', error); document.getElementById('fileGrid').innerHTML = '
Failed to load files. Please try refreshing the page.
'; } } /** * Gets the CSRF token from various possible locations in the DOM. * @function * @returns {string} The CSRF token or empty string if not found */ function getCsrfToken() { // First try to get it from the meta tag const metaTag = document.querySelector('meta[name="csrf-token"]'); if (metaTag) { const token = metaTag.getAttribute('content'); if (token && token.trim() !== '') { return token; } } // If not found in meta tag, try to get it from any form const forms = document.querySelectorAll('form'); for (const form of forms) { const csrfInput = form.querySelector('input[name="csrf_token"]'); if (csrfInput && csrfInput.value && csrfInput.value.trim() !== '') { return csrfInput.value; } } // If still not found, try to get it from any hidden input const hiddenInputs = document.querySelectorAll('input[name="csrf_token"]'); for (const input of hiddenInputs) { if (input.value && input.value.trim() !== '') { return input.value; } } console.error('CSRF token not found in any of the expected locations'); return ''; } /** * Toggles the star status of a file. * @function * @param {string} filename - The name of the file * @param {string} path - The path of the file * @param {number} roomId - The ID of the room containing the file */ function toggleStar(filename, path = '', roomId) { const csrfToken = getCsrfToken(); if (!csrfToken) { console.error('CSRF token not available'); return; } fetch(`/api/rooms/${roomId}/star`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ filename: filename, path: path }) }) .then(r => r.json()) .then(res => { if (res.success) { // Remove the file from the current view since it's no longer starred currentFiles = currentFiles.filter(f => !(f.name === filename && f.path === path && f.room_id === roomId)); renderFiles(currentFiles); } else { console.error('Failed to toggle star:', res.error); } }) .catch(error => { console.error('Error toggling star:', error); }); } /** * Restores a file from the trash. * @function * @param {string} filename - The name of the file * @param {string} path - The path of the file * @param {number} roomId - The ID of the room containing the file */ function restoreFile(filename, path = '', roomId) { const csrfToken = getCsrfToken(); if (!csrfToken) { console.error('CSRF token not available'); return; } fetch(`/api/rooms/${roomId}/restore`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ filename: filename, path: path }) }) .then(r => r.json()) .then(res => { if (res.success) { // Remove the file from the current view since it's been restored currentFiles = currentFiles.filter(f => !(f.name === filename && f.path === path && f.room_id === roomId)); renderFiles(currentFiles); } else { console.error('Failed to restore file:', res.error); } }) .catch(error => { console.error('Error restoring file:', error); }); } /** * Shows the permanent delete confirmation modal. * @function * @param {string} filename - The name of the file * @param {string} path - The path of the file * @param {number} roomId - The ID of the room containing the file */ function showPermanentDeleteModal(filename, path = '', roomId) { fileToDelete = { filename, path, roomId }; document.getElementById('permanentDeleteItemName').textContent = filename; const modal = new bootstrap.Modal(document.getElementById('permanentDeleteModal')); modal.show(); } /** * Permanently deletes a file after confirmation. * @function */ function permanentDeleteFile() { if (!fileToDelete) return; const { filename, path, roomId } = fileToDelete; const csrfToken = getCsrfToken(); if (!csrfToken) { console.error('CSRF token not available'); return; } fetch(`/api/rooms/${roomId}/delete-permanent`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ filename: filename, path: path }) }) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } // Check if the response is empty const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { return response.json(); } return { success: true }; // If no JSON response, assume success }) .then(res => { if (res.success) { // Remove the file from the current view since it's been deleted currentFiles = currentFiles.filter(f => !(f.name === filename && f.path === path && f.room_id === roomId)); renderFiles(currentFiles); // Close the modal const modal = bootstrap.Modal.getInstance(document.getElementById('permanentDeleteModal')); if (modal) { modal.hide(); } } else { console.error('Failed to delete file:', res.error || 'Unknown error'); } }) .catch(error => { console.error('Error deleting file:', error); // Show error to user const modal = bootstrap.Modal.getInstance(document.getElementById('permanentDeleteModal')); if (modal) { modal.hide(); } // You might want to show an error message to the user here }); } /** * Navigates to a file or folder. * @function * @param {number} roomId - The ID of the room * @param {string} filename - The name of the file/folder * @param {string} path - The path of the file/folder * @param {string} type - The type of item ('file' or 'folder') */ function navigateToFile(roomId, filename, path, type) { if (type === 'folder') { window.location.href = `/room/${roomId}?path=${encodeURIComponent(path ? path + '/' + filename : filename)}`; } else { window.location.href = `/api/rooms/${roomId}/files/${encodeURIComponent(filename)}?path=${encodeURIComponent(path)}`; } } /** * Shows the empty trash confirmation modal. * @function */ function showEmptyTrashModal() { const modal = new bootstrap.Modal(document.getElementById('emptyTrashModal')); modal.show(); } /** * Empties the trash by permanently deleting all trashed files. * @function */ function emptyTrash() { const csrfToken = getCsrfToken(); if (!csrfToken) { console.error('CSRF token not available'); return; } // Get all trashed files to get their room IDs fetch('/api/rooms/trash') .then(r => r.json()) .then(files => { // Get unique room IDs const roomIds = [...new Set(files.map(file => file.room_id))]; // Create an array of promises for emptying trash in each room const emptyPromises = roomIds.map(roomId => fetch(`/api/rooms/${roomId}/trash/empty`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken } }) ); // Execute all promises return Promise.all(emptyPromises); }) .then(responses => { // Check if all responses were successful const allSuccessful = responses.every(r => r.ok); if (allSuccessful) { // Clear all files from the current view currentFiles = []; renderFiles(currentFiles); // Close the modal const modal = bootstrap.Modal.getInstance(document.getElementById('emptyTrashModal')); modal.hide(); } else { console.error('Failed to empty trash in some rooms'); } }) .catch(error => { console.error('Error emptying trash:', error); }); } /** * Shows the file details modal. * @function * @param {number} idx - The index of the file in the currentFiles array */ function showDetailsModal(idx) { const item = currentFiles[idx]; const icon = item.type === 'folder' ? `` : ``; const uploaderPic = item.uploader_profile_pic ? `/uploads/profile_pics/${item.uploader_profile_pic}` : '/static/default-avatar.png'; const detailsHtml = `
${icon}
${item.name}
${item.type === 'folder' ? 'Folder' : 'File'}
Profile Picture ${item.uploaded_by || '-'}
${formatDate(item.modified)}

Room:
Path: ${(item.path ? item.path + '/' : '') + item.name}
Size: ${item.size === '-' ? '-' : (item.size > 0 ? (item.size < 1024*1024 ? (item.size/1024).toFixed(1)+' KB' : (item.size/1024/1024).toFixed(2)+' MB') : '0 KB')}
Uploaded at: ${item.uploaded_at ? new Date(item.uploaded_at).toLocaleString() : '-'}
${isTrashPage ? `
Auto Delete: ${item.auto_delete ? formatDate(item.auto_delete) : 'Never'}
` : ''} `; document.getElementById('detailsModalBody').innerHTML = detailsHtml; var modal = new bootstrap.Modal(document.getElementById('detailsModal')); modal.show(); } /** * Formats a date string to a localized format. * @function * @param {string} dateString - The date string to format * @returns {string} The formatted date string */ function formatDate(dateString) { const date = new Date(dateString); return date.toLocaleString(); } // Add previewFile function async function previewFile(index) { const file = currentFiles[index]; if (!file) return; const fileUrl = `/api/rooms/${file.room_id}/files/${encodeURIComponent(file.name)}?path=${encodeURIComponent(file.path || '')}&preview=true`; await filePreview.previewFile({ name: file.name, url: fileUrl }); } // Make functions globally accessible window.previewFile = previewFile; window.restoreFile = restoreFile; window.showPermanentDeleteModal = showPermanentDeleteModal; window.showDetailsModal = showDetailsModal; window.toggleStar = toggleStar; window.sortFiles = sortFiles; window.navigateToFile = navigateToFile; window.showEmptyTrashModal = showEmptyTrashModal; window.permanentDeleteFile = permanentDeleteFile; // Initialize search functionality document.addEventListener('DOMContentLoaded', function() { initializeView(); const quickSearchInput = document.getElementById('quickSearchInput'); const clearSearchBtn = document.getElementById('clearSearchBtn'); let searchTimeout = null; quickSearchInput.addEventListener('input', function() { const query = quickSearchInput.value.trim().toLowerCase(); clearSearchBtn.style.display = query.length > 0 ? 'block' : 'none'; if (searchTimeout) clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { if (query.length === 0) { fetchFiles(); return; } const filteredFiles = currentFiles.filter(file => file.name.toLowerCase().includes(query) ); renderFiles(filteredFiles); }, 200); }); clearSearchBtn.addEventListener('click', function() { quickSearchInput.value = ''; clearSearchBtn.style.display = 'none'; fetchFiles(); }); // Add event listeners for trash-specific buttons if (isTrashPage) { const confirmEmptyTrashBtn = document.getElementById('confirmEmptyTrash'); if (confirmEmptyTrashBtn) { confirmEmptyTrashBtn.addEventListener('click', emptyTrash); } const confirmPermanentDeleteBtn = document.getElementById('confirmPermanentDelete'); if (confirmPermanentDeleteBtn) { confirmPermanentDeleteBtn.addEventListener('click', permanentDeleteFile); } } });