reformatted starred and trash pages
This commit is contained in:
Binary file not shown.
Binary file not shown.
105
static/css/file-grid.css
Normal file
105
static/css/file-grid.css
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
.file-name-ellipsis {
|
||||||
|
max-width: 140px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.file-name-ellipsis { max-width: 180px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-action-btn {
|
||||||
|
min-width: 32px;
|
||||||
|
min-height: 32px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 5px;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover > .card-footer .file-action-btn {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer.bg-white.border-0.d-flex.justify-content-center.gap-2 {
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.file-card {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.file-card .file-action-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fileGrid.table-mode {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fileGrid.table-mode table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fileGrid.table-mode th, #fileGrid.table-mode td {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fileGrid.table-mode th {
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #6c757d;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fileGrid.table-mode tr:hover td {
|
||||||
|
background-color: rgba(22, 118, 123, 0.08);
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fileGrid.table-mode .file-icon {
|
||||||
|
width: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fileGrid.table-mode .file-actions {
|
||||||
|
min-width: 90px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fileGrid.table-mode .file-action-btn {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
min-width: 28px;
|
||||||
|
min-height: 28px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disable text selection for file grid and table rows/cards */
|
||||||
|
#fileGrid, #fileGrid * {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fileGrid .card.file-card {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fileGrid.table-mode tr {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
510
static/js/file-grid.js
Normal file
510
static/js/file-grid.js
Normal file
@@ -0,0 +1,510 @@
|
|||||||
|
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 the view and fetch files
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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='font-size:1.5rem;color:#16767b;'></i>`
|
||||||
|
: `<i class='fas fa-file-alt' style='font-size:1.5rem;color:#741b5f;'></i>`;
|
||||||
|
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(`<button class='btn btn-sm file-action-btn' title='Restore' style='background-color:rgba(22,118,123,0.08);color:#16767b;' 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:rgba(220,53,69,0.08);color:#dc3545;' 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 ? 'rgba(255,215,0,0.15)' : 'rgba(22,118,123,0.08)'};color:${file.starred ? '#ffd700' : '#16767b'};' 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:rgba(22,118,123,0.08);color:#16767b;' onclick='event.stopPropagation();showDetailsModal(${idx})'><i class='fas fa-info-circle'></i></button>`);
|
||||||
|
const actions = actionsArr.join('');
|
||||||
|
table += `<tr ${dblClickAction} onclick='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;">${icon}</span></td>
|
||||||
|
<td class='room-name'><button class='btn btn-sm' style='background-color:rgba(22,118,123,0.08);color:#16767b;' 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='font-size:2.5rem;color:#16767b;'></i>`
|
||||||
|
: `<i class='fas fa-file-alt' style='font-size:2.5rem;color:#741b5f;'></i>`;
|
||||||
|
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(`<button class='btn btn-sm file-action-btn' title='Restore' style='background-color:rgba(22,118,123,0.08);color:#16767b;' 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:rgba(220,53,69,0.08);color:#dc3545;' 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 ? 'rgba(255,215,0,0.15)' : 'rgba(22,118,123,0.08)'};color:${file.starred ? '#ffd700' : '#16767b'};' 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:rgba(22,118,123,0.08);color:#16767b;' 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:rgba(22,118,123,0.08);color:#16767b;' 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>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 '';
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPermanentDeleteModal(filename, path = '', roomId) {
|
||||||
|
fileToDelete = { filename, path, roomId };
|
||||||
|
document.getElementById('permanentDeleteItemName').textContent = filename;
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('permanentDeleteModal'));
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
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(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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showEmptyTrashModal() {
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('emptyTrashModal'));
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyTrash() {
|
||||||
|
const csrfToken = getCsrfToken();
|
||||||
|
if (!csrfToken) {
|
||||||
|
console.error('CSRF token not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/api/rooms/empty-trash', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': csrfToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(res => {
|
||||||
|
if (res.success) {
|
||||||
|
// 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:', res.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error emptying trash:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDetailsModal(idx) {
|
||||||
|
const item = currentFiles[idx];
|
||||||
|
const icon = item.type === 'folder'
|
||||||
|
? `<i class='fas fa-folder' style='font-size:2.2rem;color:#16767b;'></i>`
|
||||||
|
: `<i class='fas fa-file-alt' style='font-size:2.2rem;color:#741b5f;'></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:#e6f3f4;'>
|
||||||
|
<div style='font-size:0.91rem;color:#555;'><strong style='color:#16767b;'>Room:</strong> <button class='btn btn-sm' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='window.location.href=\"/room/${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:#555;'><strong style='color:#16767b;'>Path:</strong> <span style='word-break:break-all;'>${(item.path ? item.path + '/' : '') + item.name}</span></div>
|
||||||
|
<div style='font-size:0.91rem;color:#555;'><strong style='color:#16767b;'>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:#555;'><strong style='color:#16767b;'>Uploaded at:</strong> ${item.uploaded_at ? new Date(item.uploaded_at).toLocaleString() : '-'}</div>
|
||||||
|
${isTrashPage ? `<div style='font-size:0.91rem;color:#555;'><strong style='color:#16767b;'>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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -3,10 +3,12 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token }}">
|
||||||
<title>{% block title %}DocuPulse{% endblock %}</title>
|
<title>{% block title %}DocuPulse{% endblock %}</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||||
|
<link href="{{ url_for('static', filename='css/file-grid.css') }}" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--primary-color: #16767b;
|
--primary-color: #16767b;
|
||||||
|
|||||||
17
templates/components/details_modal.html
Normal file
17
templates/components/details_modal.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!-- Details Modal -->
|
||||||
|
<div id="detailsModal" class="modal fade" tabindex="-1" aria-labelledby="detailsModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="detailsModalLabel">Item Details</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="detailsModalBody">
|
||||||
|
<!-- Populated by JS -->
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
30
templates/components/empty_trash_modal.html
Normal file
30
templates/components/empty_trash_modal.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<!-- Empty Trash Confirmation Modal -->
|
||||||
|
<div id="emptyTrashModal" class="modal fade" tabindex="-1" aria-labelledby="emptyTrashModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="emptyTrashModalLabel">Empty Trash</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="d-flex align-items-center gap-3 mb-3">
|
||||||
|
<i class="fas fa-exclamation-triangle text-danger" style="font-size: 2rem;"></i>
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1">Are you sure you want to empty the trash?</h6>
|
||||||
|
<p class="text-muted mb-0">This will permanently delete all items in the trash.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>
|
||||||
|
This action cannot be undone. All items will be permanently removed from the system.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-danger" id="confirmEmptyTrash">
|
||||||
|
<i class="fas fa-trash me-1"></i>Empty Trash
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
30
templates/components/permanent_delete_modal.html
Normal file
30
templates/components/permanent_delete_modal.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<!-- Permanent Delete Confirmation Modal -->
|
||||||
|
<div id="permanentDeleteModal" class="modal fade" tabindex="-1" aria-labelledby="permanentDeleteModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="permanentDeleteModalLabel">Permanently Delete Item</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="d-flex align-items-center gap-3 mb-3">
|
||||||
|
<i class="fas fa-exclamation-triangle text-danger" style="font-size: 2rem;"></i>
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1">Are you sure you want to permanently delete this item?</h6>
|
||||||
|
<p class="text-muted mb-0" id="permanentDeleteItemName"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>
|
||||||
|
This action cannot be undone. The item will be permanently removed from the system.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-danger" id="confirmPermanentDelete">
|
||||||
|
<i class="fas fa-trash me-1"></i>Delete Permanently
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
7
templates/components/search_bar.html
Normal file
7
templates/components/search_bar.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<!-- Search Bar Component -->
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-3">
|
||||||
|
<div class="ms-auto" style="max-width: 300px; position: relative;">
|
||||||
|
<input type="text" id="quickSearchInput" class="form-control form-control-sm" placeholder="Quick search files..." autocomplete="off" style="padding-right: 2rem;" />
|
||||||
|
<button id="clearSearchBtn" type="button" style="position: absolute; right: 0.25rem; top: 50%; transform: translateY(-50%); display: none; border: none; background: transparent; font-size: 1.2rem; color: #888; cursor: pointer; z-index: 2;">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -3,466 +3,22 @@
|
|||||||
{% block title %}Starred - DocuPulse{% endblock %}
|
{% block title %}Starred - DocuPulse{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<meta name="csrf-token" content="{{ csrf_token }}">
|
<div class="container-fluid py-4">
|
||||||
<div class="container mt-4">
|
<div class="card shadow-sm">
|
||||||
<div class="row mb-4">
|
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||||
<div class="col">
|
<h5 class="mb-0"><i class="fas fa-star me-2" style="color:#16767b;"></i>Starred</h5>
|
||||||
<h2>Starred Items</h2>
|
|
||||||
<div class="text-muted">Your starred files and folders from all rooms</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card shadow-sm mb-4">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center bg-white">
|
|
||||||
<div class="d-flex align-items-center gap-3">
|
|
||||||
<div class="btn-group btn-group-sm" role="group" style="margin-right: 0.5rem;">
|
|
||||||
<button type="button" id="gridViewBtn" class="btn btn-outline-secondary active" onclick="toggleView('grid')">
|
|
||||||
<i class="fas fa-th-large"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" id="listViewBtn" class="btn btn-outline-secondary" onclick="toggleView('list')">
|
|
||||||
<i class="fas fa-list"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<h5 class="mb-0">Files</h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex align-items-center gap-2 mb-3">
|
{% include 'components/search_bar.html' %}
|
||||||
<div class="ms-auto" style="max-width: 300px; position: relative;">
|
|
||||||
<input type="text" id="quickSearchInput" class="form-control form-control-sm" placeholder="Quick search files..." autocomplete="off" style="padding-right: 2rem;" />
|
|
||||||
<button id="clearSearchBtn" type="button" style="position: absolute; right: 0.25rem; top: 50%; transform: translateY(-50%); display: none; border: none; background: transparent; font-size: 1.2rem; color: #888; cursor: pointer; z-index: 2;">×</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="fileGrid" class="row row-cols-1 row-cols-md-2 row-cols-lg-4 g-4"></div>
|
<div id="fileGrid" class="row row-cols-1 row-cols-md-2 row-cols-lg-4 g-4"></div>
|
||||||
<div id="fileError" class="text-danger mt-2"></div>
|
<div id="fileError" class="text-danger mt-2"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Details Modal -->
|
{% include 'components/details_modal.html' %}
|
||||||
<div id="detailsModal" class="modal fade" tabindex="-1" aria-labelledby="detailsModalLabel" aria-hidden="true">
|
{% endblock %}
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="detailsModalLabel">Item Details</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body" id="detailsModalBody">
|
|
||||||
<!-- Populated by JS -->
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
{% block extra_js %}
|
||||||
let currentView = 'grid';
|
<script src="{{ url_for('static', filename='js/file-grid.js') }}"></script>
|
||||||
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 = [];
|
|
||||||
|
|
||||||
// Initialize the view and fetch files
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(ts) {
|
|
||||||
if (!ts) return '';
|
|
||||||
const d = new Date(ts * 1000);
|
|
||||||
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
|
||||||
}
|
|
||||||
|
|
||||||
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');
|
|
||||||
gridBtn.classList.add('active');
|
|
||||||
listBtn.classList.remove('active');
|
|
||||||
} else {
|
|
||||||
grid.classList.add('table-mode');
|
|
||||||
gridBtn.classList.remove('active');
|
|
||||||
listBtn.classList.add('active');
|
|
||||||
}
|
|
||||||
renderFiles(currentFiles);
|
|
||||||
// Save the new preference
|
|
||||||
fetch('/api/user/preferred_view', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ preferred_view: view })
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Network response was not ok');
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
console.log('Preferred view saved:', data);
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Error saving preferred view:', error));
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 starred items yet.</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>
|
|
||||||
<th class='file-actions'></th>
|
|
||||||
</tr></thead><tbody>`;
|
|
||||||
files.forEach((file, idx) => {
|
|
||||||
let icon = file.type === 'folder'
|
|
||||||
? `<i class='fas fa-folder' style='font-size:1.5rem;color:#16767b;'></i>`
|
|
||||||
: `<i class='fas fa-file-alt' style='font-size:1.5rem;color:#741b5f;'></i>`;
|
|
||||||
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)}'\"`;
|
|
||||||
}
|
|
||||||
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='${file.starred ? 'Unstar' : 'Star'}' style='background-color:${file.starred ? 'rgba(255,215,0,0.15)' : 'rgba(22,118,123,0.08)'};color:${file.starred ? '#ffd700' : '#16767b'};' 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:rgba(22,118,123,0.08);color:#16767b;' onclick='event.stopPropagation();showDetailsModal(${idx})'><i class='fas fa-info-circle'></i></button>`);
|
|
||||||
const actions = actionsArr.join('');
|
|
||||||
table += `<tr ${dblClickAction} onclick='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;">${icon}</span></td>
|
|
||||||
<td class='room-name'><button class='btn btn-sm' style='background-color:rgba(22,118,123,0.08);color:#16767b;' 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>
|
|
||||||
<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='font-size:2.5rem;color:#16767b;'></i>`
|
|
||||||
: `<i class='fas fa-file-alt' style='font-size:2.5rem;color:#741b5f;'></i>`;
|
|
||||||
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)}'\"`;
|
|
||||||
}
|
|
||||||
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='${file.starred ? 'Unstar' : 'Star'}' style='background-color:${file.starred ? 'rgba(255,215,0,0.15)' : 'rgba(22,118,123,0.08)'};color:${file.starred ? '#ffd700' : '#16767b'};' 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:rgba(22,118,123,0.08);color:#16767b;' 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>
|
|
||||||
<div class='mt-2'><button class='btn btn-sm' style='background-color:rgba(22,118,123,0.08);color:#16767b;' 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>`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchFiles() {
|
|
||||||
fetch('/api/rooms/starred')
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(files => {
|
|
||||||
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>';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleStar(filename, path = '', roomId) {
|
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
|
||||||
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) {
|
|
||||||
// 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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigateToFile(roomId, filename, path, type) {
|
|
||||||
if (file.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)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showDetailsModal(idx) {
|
|
||||||
const item = currentFiles[idx];
|
|
||||||
const icon = item.type === 'folder'
|
|
||||||
? `<i class='fas fa-folder' style='font-size:2.2rem;color:#16767b;'></i>`
|
|
||||||
: `<i class='fas fa-file-alt' style='font-size:2.2rem;color:#741b5f;'></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:#e6f3f4;'>
|
|
||||||
<div style='font-size:0.91rem;color:#555;'><strong style='color:#16767b;'>Room:</strong> <button class='btn btn-sm' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='window.location.href=\"/room/${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:#555;'><strong style='color:#16767b;'>Path:</strong> <span style='word-break:break-all;'>${(item.path ? item.path + '/' : '') + item.name}</span></div>
|
|
||||||
<div style='font-size:0.91rem;color:#555;'><strong style='color:#16767b;'>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:#555;'><strong style='color:#16767b;'>Uploaded at:</strong> ${item.uploaded_at ? new Date(item.uploaded_at).toLocaleString() : '-'}</div>
|
|
||||||
`;
|
|
||||||
document.getElementById('detailsModalBody').innerHTML = detailsHtml;
|
|
||||||
var modal = new bootstrap.Modal(document.getElementById('detailsModal'));
|
|
||||||
modal.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Live search
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.file-name-ellipsis {
|
|
||||||
max-width: 140px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
|
||||||
@media (min-width: 992px) {
|
|
||||||
.file-name-ellipsis { max-width: 180px; }
|
|
||||||
}
|
|
||||||
.file-action-btn {
|
|
||||||
min-width: 32px;
|
|
||||||
min-height: 32px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 5px;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
transition: opacity 0.15s;
|
|
||||||
}
|
|
||||||
.card:hover > .card-footer .file-action-btn {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
.card-footer.bg-white.border-0.d-flex.justify-content-center.gap-2 {
|
|
||||||
min-height: 40px;
|
|
||||||
}
|
|
||||||
.card.file-card {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.card.file-card .file-action-btn {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
#fileGrid.table-mode {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
#fileGrid.table-mode table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
#fileGrid.table-mode th, #fileGrid.table-mode td {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-bottom: 1px solid #e9ecef;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
#fileGrid.table-mode th {
|
|
||||||
background: #f8f9fa;
|
|
||||||
color: #6c757d;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
#fileGrid.table-mode tr:hover td {
|
|
||||||
background-color: rgba(22, 118, 123, 0.08);
|
|
||||||
transition: background 0.15s;
|
|
||||||
}
|
|
||||||
#fileGrid.table-mode .file-icon {
|
|
||||||
width: 40px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
#fileGrid.table-mode .file-actions {
|
|
||||||
min-width: 90px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
#fileGrid.table-mode .file-action-btn {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
min-width: 28px;
|
|
||||||
min-height: 28px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
margin-left: 0.25rem;
|
|
||||||
}
|
|
||||||
/* Disable text selection for file grid and table rows/cards */
|
|
||||||
#fileGrid, #fileGrid * {
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
}
|
|
||||||
#fileGrid .card.file-card {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
#fileGrid.table-mode tr {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.btn-group.btn-group-sm .btn {
|
|
||||||
background-color: #fff;
|
|
||||||
border-color: #e9ecef;
|
|
||||||
color: #6c757d;
|
|
||||||
transition: background-color 0.15s, color 0.15s;
|
|
||||||
}
|
|
||||||
.btn-group.btn-group-sm .btn.active, .btn-group.btn-group-sm .btn:active {
|
|
||||||
background-color: #e6f3f4 !important;
|
|
||||||
color: #16767b !important;
|
|
||||||
border-color: #16767b !important;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
.btn-group.btn-group-sm .btn:focus {
|
|
||||||
box-shadow: 0 0 0 0.1rem #16767b33;
|
|
||||||
}
|
|
||||||
.btn-group.btn-group-sm .btn:hover:not(.active) {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
color: #16767b;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -3,641 +3,27 @@
|
|||||||
{% block title %}Trash - DocuPulse{% endblock %}
|
{% block title %}Trash - DocuPulse{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<meta name="csrf-token" content="{{ csrf_token }}">
|
<div class="container-fluid py-4">
|
||||||
<div class="container mt-4">
|
<div class="card shadow-sm">
|
||||||
<div class="row mb-4">
|
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||||
<div class="col">
|
<h5 class="mb-0"><i class="fas fa-trash me-2" style="color:#16767b;"></i>Trash</h5>
|
||||||
<h2>Trash</h2>
|
<button class="btn btn-danger btn-sm" onclick="showEmptyTrashModal()">
|
||||||
<div class="text-muted">Your deleted files and folders from all rooms</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card shadow-sm mb-4">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center bg-white">
|
|
||||||
<div class="d-flex align-items-center gap-3">
|
|
||||||
<div class="btn-group btn-group-sm" role="group" style="margin-right: 0.5rem;">
|
|
||||||
<button type="button" id="gridViewBtn" class="btn btn-outline-secondary active" onclick="toggleView('grid')">
|
|
||||||
<i class="fas fa-th-large"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" id="listViewBtn" class="btn btn-outline-secondary" onclick="toggleView('list')">
|
|
||||||
<i class="fas fa-list"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<h5 class="mb-0">Files</h5>
|
|
||||||
</div>
|
|
||||||
{% if current_user.is_admin %}
|
|
||||||
<button type="button" id="emptyTrashBtn" class="btn btn-danger btn-sm" onclick="showEmptyTrashModal()">
|
|
||||||
<i class="fas fa-trash me-1"></i>Empty Trash
|
<i class="fas fa-trash me-1"></i>Empty Trash
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex align-items-center gap-2 mb-3">
|
{% include 'components/search_bar.html' %}
|
||||||
<div class="ms-auto" style="max-width: 300px; position: relative;">
|
|
||||||
<input type="text" id="quickSearchInput" class="form-control form-control-sm" placeholder="Quick search files..." autocomplete="off" style="padding-right: 2rem;" />
|
|
||||||
<button id="clearSearchBtn" type="button" style="position: absolute; right: 0.25rem; top: 50%; transform: translateY(-50%); display: none; border: none; background: transparent; font-size: 1.2rem; color: #888; cursor: pointer; z-index: 2;">×</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="fileGrid" class="row row-cols-1 row-cols-md-2 row-cols-lg-4 g-4"></div>
|
<div id="fileGrid" class="row row-cols-1 row-cols-md-2 row-cols-lg-4 g-4"></div>
|
||||||
<div id="fileError" class="text-danger mt-2"></div>
|
<div id="fileError" class="text-danger mt-2"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Details Modal -->
|
{% include 'components/details_modal.html' %}
|
||||||
<div id="detailsModal" class="modal fade" tabindex="-1" aria-labelledby="detailsModalLabel" aria-hidden="true">
|
{% include 'components/permanent_delete_modal.html' %}
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
{% include 'components/empty_trash_modal.html' %}
|
||||||
<div class="modal-content">
|
{% endblock %}
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="detailsModalLabel">Item Details</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body" id="detailsModalBody">
|
|
||||||
<!-- Populated by JS -->
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Permanent Delete Confirmation Modal -->
|
{% block extra_js %}
|
||||||
<div id="permanentDeleteModal" class="modal fade" tabindex="-1" aria-labelledby="permanentDeleteModalLabel" aria-hidden="true">
|
<script src="{{ url_for('static', filename='js/file-grid.js') }}"></script>
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="permanentDeleteModalLabel">Permanently Delete Item</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="d-flex align-items-center gap-3 mb-3">
|
|
||||||
<i class="fas fa-exclamation-triangle text-danger" style="font-size: 2rem;"></i>
|
|
||||||
<div>
|
|
||||||
<h6 class="mb-1">Are you sure you want to permanently delete this item?</h6>
|
|
||||||
<p class="text-muted mb-0" id="permanentDeleteItemName"></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
<i class="fas fa-info-circle me-2"></i>
|
|
||||||
This action cannot be undone. The item will be permanently removed from the system.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
||||||
<button type="button" class="btn btn-danger" id="confirmPermanentDelete">
|
|
||||||
<i class="fas fa-trash me-1"></i>Delete Permanently
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Empty Trash Confirmation Modal -->
|
|
||||||
<div id="emptyTrashModal" class="modal fade" tabindex="-1" aria-labelledby="emptyTrashModalLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="emptyTrashModalLabel">Empty Trash</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="d-flex align-items-center gap-3 mb-3">
|
|
||||||
<i class="fas fa-exclamation-triangle text-danger" style="font-size: 2rem;"></i>
|
|
||||||
<div>
|
|
||||||
<h6 class="mb-1">Are you sure you want to empty the trash?</h6>
|
|
||||||
<p class="text-muted mb-0">This will permanently delete all items in the trash.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
<i class="fas fa-info-circle me-2"></i>
|
|
||||||
This action cannot be undone. All items will be permanently removed from the system.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
||||||
<button type="button" class="btn btn-danger" id="confirmEmptyTrash">
|
|
||||||
<i class="fas fa-trash me-1"></i>Empty Trash
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
let currentView = 'grid';
|
|
||||||
let lastSelectedIndex = -1;
|
|
||||||
let sortColumn = 'auto_delete'; // Set default sort column to auto_delete
|
|
||||||
let sortDirection = 1; // 1 for ascending, -1 for descending
|
|
||||||
let batchDeleteItems = null;
|
|
||||||
let currentFiles = [];
|
|
||||||
let fileToDelete = null;
|
|
||||||
window.isAdmin = {{ 'true' if current_user.is_admin else 'false' }};
|
|
||||||
|
|
||||||
// Initialize the view and fetch files
|
|
||||||
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 auto_delete by default
|
|
||||||
sortFiles('auto_delete');
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(ts) {
|
|
||||||
if (!ts) return '';
|
|
||||||
const d = new Date(ts * 1000);
|
|
||||||
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
|
||||||
}
|
|
||||||
|
|
||||||
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');
|
|
||||||
gridBtn.classList.add('active');
|
|
||||||
listBtn.classList.remove('active');
|
|
||||||
} else {
|
|
||||||
grid.classList.add('table-mode');
|
|
||||||
gridBtn.classList.remove('active');
|
|
||||||
listBtn.classList.add('active');
|
|
||||||
}
|
|
||||||
renderFiles(currentFiles);
|
|
||||||
// Save the new preference
|
|
||||||
fetch('/api/user/preferred_view', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ preferred_view: view })
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Network response was not ok');
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
console.log('Preferred view saved:', data);
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Error saving preferred view:', error));
|
|
||||||
}
|
|
||||||
|
|
||||||
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];
|
|
||||||
|
|
||||||
// Special handling for auto_delete column
|
|
||||||
if (column === 'auto_delete') {
|
|
||||||
valA = valA ? new Date(valA).getTime() : Infinity;
|
|
||||||
valB = valB ? new Date(valB).getTime() : Infinity;
|
|
||||||
return (valA - valB) * sortDirection;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 deleted items yet.</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>
|
|
||||||
<th>Deleted By</th>
|
|
||||||
<th>Deleted At</th>
|
|
||||||
<th>Auto-Delete</th>
|
|
||||||
<th class='file-actions'></th>
|
|
||||||
</tr></thead><tbody>`;
|
|
||||||
files.forEach((file, idx) => {
|
|
||||||
let icon = file.type === 'folder'
|
|
||||||
? `<i class='fas fa-folder' style='font-size:1.5rem;color:#16767b;'></i>`
|
|
||||||
: `<i class='fas fa-file-alt' style='font-size:1.5rem;color:#741b5f;'></i>`;
|
|
||||||
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 = [];
|
|
||||||
if (file.can_restore) {
|
|
||||||
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Restore' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='restoreFile("${file.name}", "${file.path}", ${file.room_id})'><i class='fas fa-undo'></i></button>`);
|
|
||||||
}
|
|
||||||
if (window.isAdmin) {
|
|
||||||
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Delete Permanently' style='background-color:rgba(239,68,68,0.1);color:#b91c1c;' onclick='deletePermanently("${file.name}", "${file.path}", ${file.room_id})'><i class='fas fa-trash'></i></button>`);
|
|
||||||
}
|
|
||||||
const actions = actionsArr.join('');
|
|
||||||
|
|
||||||
// Calculate days until auto-deletion
|
|
||||||
const deletedAt = new Date(file.deleted_at);
|
|
||||||
const now = new Date();
|
|
||||||
const daysUntilDeletion = 30 - Math.floor((now - deletedAt) / (1000 * 60 * 60 * 24));
|
|
||||||
let autoDeleteStatus = '';
|
|
||||||
if (daysUntilDeletion <= 0) {
|
|
||||||
autoDeleteStatus = '<span class="badge bg-danger">Deleting soon</span>';
|
|
||||||
} else if (daysUntilDeletion <= 7) {
|
|
||||||
autoDeleteStatus = `<span class="badge bg-warning text-dark">${daysUntilDeletion} days left</span>`;
|
|
||||||
} else {
|
|
||||||
autoDeleteStatus = `<span class="badge bg-info text-dark">${daysUntilDeletion} days left</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
table += `<tr>
|
|
||||||
<td class='file-icon'><span style="display:inline-flex;align-items:center;gap:1.2rem;">${icon}</span></td>
|
|
||||||
<td class='room-name'><button class='btn btn-sm' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='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>
|
|
||||||
<td class='deleted-by'>${file.deleted_by || '-'}</td>
|
|
||||||
<td class='deleted-at'>${file.deleted_at ? new Date(file.deleted_at).toLocaleString() : '-'}</td>
|
|
||||||
<td class='auto-delete'>${autoDeleteStatus}</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='font-size:2.5rem;color:#16767b;'></i>`
|
|
||||||
: `<i class='fas fa-file-alt' style='font-size:2.5rem;color:#741b5f;'></i>`;
|
|
||||||
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 = [];
|
|
||||||
if (file.can_restore) {
|
|
||||||
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Restore' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='restoreFile("${file.name}", "${file.path}", ${file.room_id})'><i class='fas fa-undo'></i></button>`);
|
|
||||||
}
|
|
||||||
if (window.isAdmin) {
|
|
||||||
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Delete Permanently' style='background-color:rgba(239,68,68,0.1);color:#b91c1c;' onclick='deletePermanently("${file.name}", "${file.path}", ${file.room_id})'><i class='fas fa-trash'></i></button>`);
|
|
||||||
}
|
|
||||||
const actions = actionsArr.join('');
|
|
||||||
|
|
||||||
// Calculate days until auto-deletion
|
|
||||||
const deletedAt = new Date(file.deleted_at);
|
|
||||||
const now = new Date();
|
|
||||||
const daysUntilDeletion = 30 - Math.floor((now - deletedAt) / (1000 * 60 * 60 * 24));
|
|
||||||
let autoDeleteStatus = '';
|
|
||||||
if (daysUntilDeletion <= 0) {
|
|
||||||
autoDeleteStatus = '<span class="badge bg-danger">Deleting soon</span>';
|
|
||||||
} else if (daysUntilDeletion <= 7) {
|
|
||||||
autoDeleteStatus = `<span class="badge bg-warning text-dark">${daysUntilDeletion} days left</span>`;
|
|
||||||
} else {
|
|
||||||
autoDeleteStatus = `<span class="badge bg-info text-dark">${daysUntilDeletion} days left</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
grid.innerHTML += `
|
|
||||||
<div class='col'>
|
|
||||||
<div class='card file-card h-100 border-0 shadow-sm position-relative'>
|
|
||||||
<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>
|
|
||||||
<div class='mt-2'><button class='btn btn-sm' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='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 class='text-muted small mt-1'>Deleted by: ${file.deleted_by || '-'}</div>
|
|
||||||
<div class='text-muted small'>${file.deleted_at ? new Date(file.deleted_at).toLocaleString() : '-'}</div>
|
|
||||||
<div class='mt-2'>${autoDeleteStatus}</div>
|
|
||||||
</div>
|
|
||||||
<div class='card-footer bg-white border-0 d-flex justify-content-center gap-2'>${actions}</div>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchFiles() {
|
|
||||||
fetch('/api/rooms/trash')
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(files => {
|
|
||||||
if (files) {
|
|
||||||
window.currentFiles = files;
|
|
||||||
// Sort files by auto_delete by default
|
|
||||||
window.currentFiles.sort((a, b) => {
|
|
||||||
const timeA = a.auto_delete ? new Date(a.auto_delete).getTime() : Infinity;
|
|
||||||
const timeB = b.auto_delete ? new Date(b.auto_delete).getTime() : Infinity;
|
|
||||||
return timeA - timeB;
|
|
||||||
});
|
|
||||||
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>';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function restoreFile(filename, path, roomId) {
|
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
|
||||||
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) {
|
|
||||||
// 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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function deletePermanently(filename, path, roomId) {
|
|
||||||
fileToDelete = { filename, path, roomId };
|
|
||||||
const modal = new bootstrap.Modal(document.getElementById('permanentDeleteModal'));
|
|
||||||
document.getElementById('permanentDeleteItemName').textContent = filename;
|
|
||||||
modal.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('confirmPermanentDelete').addEventListener('click', function() {
|
|
||||||
if (!fileToDelete) return;
|
|
||||||
|
|
||||||
const { filename, path, roomId } = fileToDelete;
|
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
|
||||||
|
|
||||||
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) {
|
|
||||||
// Remove the file from the current view since it's been permanently deleted
|
|
||||||
currentFiles = currentFiles.filter(f => !(f.name === filename && f.path === path && f.room_id === roomId));
|
|
||||||
renderFiles(currentFiles);
|
|
||||||
// Close the modal
|
|
||||||
bootstrap.Modal.getInstance(document.getElementById('permanentDeleteModal')).hide();
|
|
||||||
} else {
|
|
||||||
console.error('Failed to delete file permanently:', res.error);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error deleting file permanently:', error);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
fileToDelete = null;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function showDetailsModal(idx) {
|
|
||||||
const item = currentFiles[idx];
|
|
||||||
const icon = item.type === 'folder'
|
|
||||||
? `<i class='fas fa-folder' style='font-size:2.2rem;color:#16767b;'></i>`
|
|
||||||
: `<i class='fas fa-file-alt' style='font-size:2.2rem;color:#741b5f;'></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:#e6f3f4;'>
|
|
||||||
<div style='font-size:0.91rem;color:#555;'><strong style='color:#16767b;'>Room:</strong> <button class='btn btn-sm' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='window.location.href=\"/room/${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:#555;'><strong style='color:#16767b;'>Path:</strong> <span style='word-break:break-all;'>${(item.path ? item.path + '/' : '') + item.name}</span></div>
|
|
||||||
<div style='font-size:0.91rem;color:#555;'><strong style='color:#16767b;'>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:#555;'><strong style='color:#16767b;'>Uploaded at:</strong> ${item.uploaded_at ? new Date(item.uploaded_at).toLocaleString() : '-'}</div>
|
|
||||||
`;
|
|
||||||
document.getElementById('detailsModalBody').innerHTML = detailsHtml;
|
|
||||||
var modal = new bootstrap.Modal(document.getElementById('detailsModal'));
|
|
||||||
modal.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Live search
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function showEmptyTrashModal() {
|
|
||||||
const modal = new bootstrap.Modal(document.getElementById('emptyTrashModal'));
|
|
||||||
modal.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('confirmEmptyTrash').addEventListener('click', function() {
|
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
|
||||||
|
|
||||||
// Get all unique room IDs from current files
|
|
||||||
const roomIds = [...new Set(currentFiles.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}/delete-permanent`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': csrfToken
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
filename: '*', // Special value to indicate all files
|
|
||||||
path: '' // Empty path to indicate root
|
|
||||||
})
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Execute all promises
|
|
||||||
Promise.all(emptyPromises)
|
|
||||||
.then(responses => {
|
|
||||||
// Check if all responses were successful
|
|
||||||
const allSuccessful = responses.every(response => response.ok);
|
|
||||||
if (allSuccessful) {
|
|
||||||
// Clear the current files array and re-render
|
|
||||||
currentFiles = [];
|
|
||||||
renderFiles(currentFiles);
|
|
||||||
// Close the modal
|
|
||||||
bootstrap.Modal.getInstance(document.getElementById('emptyTrashModal')).hide();
|
|
||||||
// Refresh the files list to ensure we have the latest state
|
|
||||||
fetchFiles();
|
|
||||||
} else {
|
|
||||||
console.error('Failed to empty trash in some rooms');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error emptying trash:', error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.file-name-ellipsis {
|
|
||||||
max-width: 140px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
|
||||||
@media (min-width: 992px) {
|
|
||||||
.file-name-ellipsis { max-width: 180px; }
|
|
||||||
}
|
|
||||||
.file-action-btn {
|
|
||||||
min-width: 32px;
|
|
||||||
min-height: 32px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 5px;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
transition: opacity 0.15s;
|
|
||||||
}
|
|
||||||
.card:hover > .card-footer .file-action-btn {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
.card-footer.bg-white.border-0.d-flex.justify-content-center.gap-2 {
|
|
||||||
min-height: 40px;
|
|
||||||
}
|
|
||||||
.card.file-card {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.card.file-card .file-action-btn {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
#fileGrid.table-mode {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
#fileGrid.table-mode table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
#fileGrid.table-mode th, #fileGrid.table-mode td {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-bottom: 1px solid #e9ecef;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
#fileGrid.table-mode th {
|
|
||||||
background: #f8f9fa;
|
|
||||||
color: #6c757d;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
#fileGrid.table-mode tr:hover td {
|
|
||||||
background-color: rgba(22, 118, 123, 0.08);
|
|
||||||
transition: background 0.15s;
|
|
||||||
}
|
|
||||||
#fileGrid.table-mode .file-icon {
|
|
||||||
width: 40px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
#fileGrid.table-mode .file-actions {
|
|
||||||
min-width: 90px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
#fileGrid.table-mode .file-action-btn {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
min-width: 28px;
|
|
||||||
min-height: 28px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
margin-left: 0.25rem;
|
|
||||||
}
|
|
||||||
/* Disable text selection for file grid and table rows/cards */
|
|
||||||
#fileGrid, #fileGrid * {
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
}
|
|
||||||
#fileGrid .card.file-card {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
#fileGrid.table-mode tr {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user