/**
* @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
*/
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');
/**
* 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-CSRFToken': 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 = '
';
return;
}
if (currentView === 'list') {
let table = `
|
Room |
Name ${(sortColumn==='name') ? (sortDirection===1?'▲':'▼') : ''} |
Date Modified ${(sortColumn==='modified') ? (sortDirection===1?'▲':'▼') : ''} |
Type ${(sortColumn==='type') ? (sortDirection===1?'▲':'▼') : ''} |
Size ${(sortColumn==='size') ? (sortDirection===1?'▲':'▼') : ''} |
${isTrashPage ? `Auto Delete ${(sortColumn==='auto_delete') ? (sortDirection===1?'▲':'▼') : ''} | ` : ''}
|
`;
files.forEach((file, idx) => {
let icon = file.type === 'folder'
? ``
: ``;
let size = file.size !== '-' ? (file.size > 0 ? (file.size < 1024*1024 ? (file.size/1024).toFixed(1)+' KB' : (file.size/1024/1024).toFixed(2)+' MB') : '0 KB') : '-';
let actionsArr = [];
let dblClickAction = '';
if (file.type === 'folder') {
dblClickAction = `ondblclick=\"window.location.href='/room/${file.room_id}?path=${encodeURIComponent(file.path ? file.path + '/' + file.name : file.name)}'\"`;
} else {
dblClickAction = `ondblclick=\"window.location.href='/api/rooms/${file.room_id}/files/${encodeURIComponent(file.name)}?path=${encodeURIComponent(file.path)}'\"`;
}
if (isTrashPage) {
actionsArr.push(``);
actionsArr.push(``);
} else {
actionsArr.push(``);
}
actionsArr.push(``);
const actions = actionsArr.join('');
table += `
| ${icon} |
|
${file.name} |
${formatDate(file.modified)} |
${file.type} |
${size} |
${isTrashPage ? `${file.auto_delete ? formatDate(file.auto_delete) : 'Never'} | ` : ''}
${actions} |
`;
});
table += '
';
grid.innerHTML = table;
} else {
files.forEach((file, idx) => {
let icon = file.type === 'folder'
? ``
: ``;
let size = file.size !== '-' ? (file.size > 0 ? (file.size < 1024*1024 ? (file.size/1024).toFixed(1)+' KB' : (file.size/1024/1024).toFixed(2)+' MB') : '0 KB') : '-';
let actionsArr = [];
let dblClickAction = '';
if (file.type === 'folder') {
dblClickAction = `ondblclick=\"window.location.href='/room/${file.room_id}?path=${encodeURIComponent(file.path ? file.path + '/' + file.name : file.name)}'\"`;
} else {
dblClickAction = `ondblclick=\"window.location.href='/api/rooms/${file.room_id}/files/${encodeURIComponent(file.name)}?path=${encodeURIComponent(file.path)}'\"`;
}
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-CSRFToken': csrfToken
},
body: JSON.stringify({
filename: filename,
path: path
})
})
.then(r => r.json())
.then(res => {
if (res.success) {
// Log the star/unstar event
fetch('/api/events/log', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
event_type: res.starred ? 'file_star' : 'file_unstar',
details: {
filename: filename,
path: path,
room_id: roomId,
timestamp: new Date().toISOString()
}
})
});
// 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-CSRFToken': csrfToken
},
body: JSON.stringify({
filename: filename,
path: path
})
})
.then(r => r.json())
.then(res => {
if (res.success) {
// Log the restore event
fetch('/api/events/log', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
event_type: 'file_restore',
details: {
filename: filename,
path: path,
room_id: roomId,
timestamp: new Date().toISOString()
}
})
});
// 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-CSRFToken': csrfToken
},
body: JSON.stringify({
filename: filename,
path: path
})
})
.then(r => r.json())
.then(res => {
if (res.success) {
// Log the permanent delete event
fetch('/api/events/log', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
event_type: 'file_delete_permanent',
details: {
filename: filename,
path: path,
room_id: roomId,
timestamp: new Date().toISOString()
}
})
});
// Remove the file from the current view
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'));
modal.hide();
} else {
console.error('Failed to delete file permanently:', res.error);
}
})
.catch(error => {
console.error('Error deleting file permanently:', error);
});
}
/**
* 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-CSRFToken': 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'}
${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();
}
// 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);
}
}
});