703 lines
29 KiB
JavaScript
703 lines
29 KiB
JavaScript
/**
|
|
* @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 = '<div class="col"><div class="text-muted">No items found.</div></div>';
|
|
return;
|
|
}
|
|
|
|
if (currentView === 'list') {
|
|
let table = `<table><thead><tr>
|
|
<th></th>
|
|
<th>Room</th>
|
|
<th onclick="sortFiles('name')" style="cursor:pointer;">Name ${(sortColumn==='name') ? (sortDirection===1?'▲':'▼') : ''}</th>
|
|
<th onclick="sortFiles('modified')" style="cursor:pointer;">Date Modified ${(sortColumn==='modified') ? (sortDirection===1?'▲':'▼') : ''}</th>
|
|
<th onclick="sortFiles('type')" style="cursor:pointer;">Type ${(sortColumn==='type') ? (sortDirection===1?'▲':'▼') : ''}</th>
|
|
<th onclick="sortFiles('size')" style="cursor:pointer;">Size ${(sortColumn==='size') ? (sortDirection===1?'▲':'▼') : ''}</th>
|
|
${isTrashPage ? `<th onclick="sortFiles('auto_delete')" style="cursor:pointer;">Auto Delete ${(sortColumn==='auto_delete') ? (sortDirection===1?'▲':'▼') : ''}</th>` : ''}
|
|
<th class='file-actions'></th>
|
|
</tr></thead><tbody>`;
|
|
|
|
files.forEach((file, idx) => {
|
|
let icon = file.type === 'folder' ?
|
|
`<i class="fas fa-folder" style="color:var(--primary-color);"></i>` :
|
|
`<i class="fas ${getFileIcon(file.name)}" style="color:var(--secondary-color);"></i>`;
|
|
|
|
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 = [
|
|
// Images
|
|
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp', 'tiff',
|
|
// Documents
|
|
'pdf', 'txt', 'md', 'csv', 'py', 'js', 'html', 'css', 'json', 'xml', 'sql', 'sh', 'bat',
|
|
'docx', 'doc', 'xlsx', 'xls', 'pptx', 'ppt', 'odt', 'odp', 'ods',
|
|
// Media
|
|
'mp4', 'webm', 'avi', 'mov', 'wmv', 'flv', 'mkv',
|
|
'mp3', 'wav', 'ogg', 'm4a', 'flac'
|
|
];
|
|
|
|
if (supportedTypes.includes(extension)) {
|
|
actionsArr.push(`
|
|
<button class="btn btn-sm file-action-btn" title="Preview" onclick="event.stopPropagation(); previewFile(${idx})"
|
|
style="background-color:var(--primary-opacity-8);color:var(--primary-color);">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
`);
|
|
}
|
|
}
|
|
|
|
if (isTrashPage) {
|
|
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Restore' style='background-color:var(--primary-opacity-8);color:var(--primary-color);' onclick='event.stopPropagation();restoreFile("${file.name}", "${file.path}", ${file.room_id})'><i class='fas fa-undo'></i></button>`);
|
|
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Delete Permanently' style='background-color:var(--danger-opacity-15);color:var(--danger-color);' onclick='event.stopPropagation();showPermanentDeleteModal("${file.name}", "${file.path}", ${file.room_id})'><i class='fas fa-trash'></i></button>`);
|
|
} else {
|
|
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='${file.starred ? 'Unstar' : 'Star'}' style='background-color:${file.starred ? 'var(--warning-opacity-15)' : 'var(--primary-opacity-8)'};color:${file.starred ? 'var(--warning-color)' : 'var(--primary-color)'};' onclick='event.stopPropagation();toggleStar("${file.name}", "${file.path}", ${file.room_id})'><i class='fas fa-star'></i></button>`);
|
|
}
|
|
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Details' style='background-color:var(--primary-opacity-8);color:var(--primary-color);' onclick='event.stopPropagation();showDetailsModal(${idx})'><i class='fas fa-info-circle'></i></button>`);
|
|
const actions = actionsArr.join('');
|
|
table += `<tr ${dblClickAction} ondblclick='navigateToFile(${file.room_id}, "${file.name}", "${file.path}", "${file.type}")'>
|
|
<td class='file-icon'><span style="display:inline-flex;align-items:center;gap:1.2rem;font-size:1.5rem;">${icon}</span></td>
|
|
<td class='room-name'><button class='btn btn-sm' style='background-color:var(--primary-opacity-8);color:var(--primary-color);' onclick='event.stopPropagation();window.location.href="/rooms/${file.room_id}"'><i class='fas fa-door-open me-1'></i>${file.room_name || 'Room ' + file.room_id}</button></td>
|
|
<td class='file-name' title='${file.name}'>${file.name}</td>
|
|
<td class='file-date'>${formatDate(file.modified)}</td>
|
|
<td class='file-type'>${file.type}</td>
|
|
<td class='file-size'>${size}</td>
|
|
${isTrashPage ? `<td class='auto-delete'>${file.auto_delete ? formatDate(file.auto_delete) : 'Never'}</td>` : ''}
|
|
<td class='file-actions'>${actions}</td>
|
|
</tr>`;
|
|
});
|
|
table += '</tbody></table>';
|
|
grid.innerHTML = table;
|
|
} else {
|
|
files.forEach((file, idx) => {
|
|
let icon = file.type === 'folder' ?
|
|
`<i class="fas fa-folder" style="color:var(--primary-color);"></i>` :
|
|
`<i class="fas ${getFileIcon(file.name)}" style="color:var(--secondary-color);"></i>`;
|
|
|
|
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 = [
|
|
// Images
|
|
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp', 'tiff',
|
|
// Documents
|
|
'pdf', 'txt', 'md', 'csv', 'py', 'js', 'html', 'css', 'json', 'xml', 'sql', 'sh', 'bat',
|
|
'docx', 'doc', 'xlsx', 'xls', 'pptx', 'ppt', 'odt', 'odp', 'ods',
|
|
// Media
|
|
'mp4', 'webm', 'avi', 'mov', 'wmv', 'flv', 'mkv',
|
|
'mp3', 'wav', 'ogg', 'm4a', 'flac'
|
|
];
|
|
|
|
if (supportedTypes.includes(extension)) {
|
|
actionsArr.push(`
|
|
<button class="btn btn-sm file-action-btn" title="Preview" onclick="event.stopPropagation(); previewFile(${idx})"
|
|
style="background-color:var(--primary-opacity-8);color:var(--primary-color);">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
`);
|
|
}
|
|
}
|
|
|
|
if (isTrashPage) {
|
|
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Restore' style='background-color:var(--primary-opacity-8);color:var(--primary-color);' onclick='event.stopPropagation();restoreFile("${file.name}", "${file.path}", ${file.room_id})'><i class='fas fa-undo'></i></button>`);
|
|
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Delete Permanently' style='background-color:var(--danger-opacity-15);color:var(--danger-color);' onclick='event.stopPropagation();showPermanentDeleteModal("${file.name}", "${file.path}", ${file.room_id})'><i class='fas fa-trash'></i></button>`);
|
|
} else {
|
|
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='${file.starred ? 'Unstar' : 'Star'}' style='background-color:${file.starred ? 'var(--warning-opacity-15)' : 'var(--primary-opacity-8)'};color:${file.starred ? 'var(--warning-color)' : 'var(--primary-color)'};' onclick='event.stopPropagation();toggleStar("${file.name}", "${file.path}", ${file.room_id})'><i class='fas fa-star'></i></button>`);
|
|
}
|
|
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Details' style='background-color:var(--primary-opacity-8);color:var(--primary-color);' onclick='event.stopPropagation();showDetailsModal(${idx})'><i class='fas fa-info-circle'></i></button>`);
|
|
const actions = actionsArr.join('');
|
|
grid.innerHTML += `
|
|
<div class='col'>
|
|
<div class='card file-card h-100 border-0 shadow-sm position-relative' ${dblClickAction} onclick='navigateToFile(${file.room_id}, "${file.name}", "${file.path}", "${file.type}")'>
|
|
<div class='card-body d-flex flex-column align-items-center justify-content-center text-center'>
|
|
<div class='mb-2'>${icon}</div>
|
|
<div class='fw-semibold file-name-ellipsis' title='${file.name}'>${file.name}</div>
|
|
<div class='text-muted small'>${formatDate(file.modified)}</div>
|
|
<div class='text-muted small'>${size}</div>
|
|
${isTrashPage ? `<div class='text-muted small'>Auto Delete: ${file.auto_delete ? formatDate(file.auto_delete) : 'Never'}</div>` : ''}
|
|
<div class='mt-2'><button class='btn btn-sm' style='background-color:var(--primary-opacity-8);color:var(--primary-color);' onclick='event.stopPropagation();window.location.href="/rooms/${file.room_id}"'><i class='fas fa-door-open me-1'></i>${file.room_name || 'Room ' + file.room_id}</button></div>
|
|
</div>
|
|
<div class='card-footer bg-white border-0 d-flex justify-content-center gap-2'>${actions}</div>
|
|
</div>
|
|
</div>`;
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 = '<div class="col"><div class="text-danger">Failed to load files. Please try refreshing the page.</div></div>';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
|
|
/**
|
|
* 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'
|
|
? `<i class='fas fa-folder' style='font-size:2.2rem;color:var(--primary-color);'></i>`
|
|
: `<i class='fas fa-file-alt' style='font-size:2.2rem;color:var(--secondary-color);'></i>`;
|
|
const uploaderPic = item.uploader_profile_pic
|
|
? `/uploads/profile_pics/${item.uploader_profile_pic}`
|
|
: '/static/default-avatar.png';
|
|
const detailsHtml = `
|
|
<div class='d-flex align-items-center gap-3 mb-3'>
|
|
<div>${icon}</div>
|
|
<div style='min-width:0;'>
|
|
<div class='fw-bold' style='font-size:1.1rem;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;' title='${item.name}'>${item.name}</div>
|
|
<div class='text-muted small'>${item.type === 'folder' ? 'Folder' : 'File'}</div>
|
|
</div>
|
|
</div>
|
|
<div class='mb-2 d-flex align-items-center gap-2'>
|
|
<img src='${uploaderPic}' alt='Profile Picture' class='rounded-circle border' style='width:28px;height:28px;object-fit:cover;'>
|
|
<span class='fw-semibold' style='font-size:0.98rem;'>${item.uploaded_by || '-'}</span>
|
|
</div>
|
|
<div class='mb-2 text-muted' style='font-size:0.92rem;'><i class='far fa-clock me-1'></i>${formatDate(item.modified)}</div>
|
|
<hr style='margin:0.7rem 0 0.5rem 0; border-color:var(--primary-bg-light);'>
|
|
<div style='font-size:0.91rem;color:var(--text-dark);'><strong style='color:var(--primary-color);'>Room:</strong> <button class='btn btn-sm' style='background-color:var(--primary-opacity-8);color:var(--primary-color);' onclick='window.location.href=\"/rooms/${item.room_id}\"'><i class='fas fa-door-open me-1'></i>${item.room_name || 'Room ' + item.room_id}</button></div>
|
|
<div style='font-size:0.91rem;color:var(--text-dark);'><strong style='color:var(--primary-color);'>Path:</strong> <span style='word-break:break-all;'>${(item.path ? item.path + '/' : '') + item.name}</span></div>
|
|
<div style='font-size:0.91rem;color:var(--text-dark);'><strong style='color:var(--primary-color);'>Size:</strong> ${item.size === '-' ? '-' : (item.size > 0 ? (item.size < 1024*1024 ? (item.size/1024).toFixed(1)+' KB' : (item.size/1024/1024).toFixed(2)+' MB') : '0 KB')}</div>
|
|
<div style='font-size:0.91rem;color:var(--text-dark);'><strong style='color:var(--primary-color);'>Uploaded at:</strong> ${item.uploaded_at ? new Date(item.uploaded_at).toLocaleString() : '-'}</div>
|
|
${isTrashPage ? `<div style='font-size:0.91rem;color:var(--text-dark);'><strong style='color:var(--primary-color);'>Auto Delete:</strong> ${item.auto_delete ? formatDate(item.auto_delete) : 'Never'}</div>` : ''}
|
|
`;
|
|
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;
|
|
window.toggleView = toggleView;
|
|
window.fetchFiles = fetchFiles;
|
|
|
|
// 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', window.emptyTrash);
|
|
}
|
|
|
|
const confirmPermanentDeleteBtn = document.getElementById('confirmPermanentDelete');
|
|
if (confirmPermanentDeleteBtn) {
|
|
confirmPermanentDeleteBtn.addEventListener('click', permanentDeleteFile);
|
|
}
|
|
}
|
|
});
|