adding trash folder

This commit is contained in:
DioCrafts
2025-03-24 17:49:53 +01:00
parent 38b0e9594b
commit bd623ec07e
11 changed files with 644 additions and 68 deletions
+75
View File
@@ -563,6 +563,11 @@ body {
background-color: white;
}
/* Para modo papelera, ajustar columnas */
.trash-item.file-item {
grid-template-columns: minmax(180px, 1.5fr) 0.5fr 1fr 120px 100px;
}
.file-item:hover {
background-color: #f0f8ff;
}
@@ -939,3 +944,73 @@ body {
margin-right: 8px;
color: #ffc107;
}
/* Estilos para la papelera */
.trash-item {
position: relative;
}
.trash-actions {
position: absolute;
top: 10px;
right: 10px;
display: none;
gap: 8px;
}
.file-card.trash-item:hover .trash-actions,
.file-item.trash-item:hover .actions-cell {
display: flex;
}
.trash-actions button,
.actions-cell button {
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
padding: 4px 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
transition: all 0.2s;
}
.trash-actions button:hover,
.actions-cell button:hover {
background: #f0f0f0;
}
.btn-restore {
color: #4CAF50;
}
.btn-delete {
color: #f44336;
}
.actions-cell {
display: flex;
gap: 8px;
justify-content: flex-start;
align-items: center;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300px;
width: 100%;
color: #666;
grid-column: 1 / -1;
}
/* Botón de peligro para vaciar papelera */
.btn-danger {
background-color: #f44336;
color: white;
}
+248
View File
@@ -12,6 +12,8 @@ const app = {
contextMenuTargetFile: null, // Target file for context menu
selectedTargetFolderId: "", // Selected target folder for move operations
moveDialogMode: 'file', // Move dialog mode: 'file' or 'folder'
isTrashView: false, // Whether we're in trash view
currentSection: 'files', // Current section: 'files' or 'trash'
};
// DOM elements
@@ -62,6 +64,10 @@ function cacheElements() {
elements.listViewBtn = document.getElementById('list-view-btn');
elements.breadcrumb = document.querySelector('.breadcrumb');
elements.logoutBtn = document.getElementById('logout-btn');
elements.pageTitle = document.querySelector('.page-title');
elements.actionsBar = document.querySelector('.actions-bar');
elements.navItems = document.querySelectorAll('.nav-item');
elements.trashBtn = document.querySelector('.nav-item:nth-child(5)'); // The trash nav item
}
/**
@@ -98,6 +104,99 @@ function setupEventListeners() {
elements.gridViewBtn.addEventListener('click', ui.switchToGridView);
elements.listViewBtn.addEventListener('click', ui.switchToListView);
// Sidebar navigation
elements.navItems.forEach(item => {
item.addEventListener('click', () => {
// Remove active class from all nav items
elements.navItems.forEach(navItem => navItem.classList.remove('active'));
// Add active class to clicked item
item.classList.add('active');
// Check if this is the trash item
if (item === elements.trashBtn) {
// Show trash view
app.isTrashView = true;
app.currentSection = 'trash';
// Update UI
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.trash') : 'Papelera';
elements.actionsBar.innerHTML = `
<div class="action-buttons">
<button class="btn btn-danger" id="empty-trash-btn">
<i class="fas fa-trash" style="margin-right: 5px;"></i>
<span>${window.i18n ? window.i18n.t('trash.empty_trash') : 'Vaciar papelera'}</span>
</button>
</div>
`;
// Add event listener to empty trash button
document.getElementById('empty-trash-btn').addEventListener('click', async () => {
if (await fileOps.emptyTrash()) {
loadTrashItems();
}
});
// Load trash items
loadTrashItems();
} else {
// Show regular files view
app.isTrashView = false;
app.currentSection = 'files';
// Reset UI
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.files') : 'Archivos';
elements.actionsBar.innerHTML = `
<div class="action-buttons">
<button class="btn btn-primary" id="upload-btn">
<i class="fas fa-upload" style="margin-right: 5px;"></i> <span data-i18n="actions.upload">Subir</span>
</button>
<button class="btn btn-secondary" id="new-folder-btn">
<i class="fas fa-folder-plus" style="margin-right: 5px;"></i> <span data-i18n="actions.new_folder">Nueva carpeta</span>
</button>
</div>
<div class="view-toggle">
<button class="toggle-btn active" id="grid-view-btn" title="Vista de cuadrícula">
<i class="fas fa-th"></i>
</button>
<button class="toggle-btn" id="list-view-btn" title="Vista de lista">
<i class="fas fa-list"></i>
</button>
</div>
`;
// Restore event listeners
document.getElementById('upload-btn').addEventListener('click', () => {
elements.dropzone.style.display = elements.dropzone.style.display === 'none' ? 'block' : 'none';
if (elements.dropzone.style.display === 'block') {
elements.fileInput.click();
}
});
document.getElementById('new-folder-btn').addEventListener('click', () => {
const folderName = prompt(window.i18n ? window.i18n.t('dialogs.new_name') : 'Nombre de la carpeta:');
if (folderName) {
fileOps.createFolder(folderName);
}
});
document.getElementById('grid-view-btn').addEventListener('click', ui.switchToGridView);
document.getElementById('list-view-btn').addEventListener('click', ui.switchToListView);
// Restore cached elements
elements.uploadBtn = document.getElementById('upload-btn');
elements.newFolderBtn = document.getElementById('new-folder-btn');
elements.gridViewBtn = document.getElementById('grid-view-btn');
elements.listViewBtn = document.getElementById('list-view-btn');
// Load regular files
app.currentPath = '';
ui.updateBreadcrumb('');
loadFiles();
}
});
});
// Load saved view preference
const savedView = localStorage.getItem('oxicloud-view');
if (savedView === 'list') {
@@ -218,9 +317,158 @@ function formatFileSize(bytes) {
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* Load trash items
*/
async function loadTrashItems() {
try {
// Clear existing content
elements.filesGrid.innerHTML = '';
elements.filesListView.innerHTML = `
<div class="list-header">
<div data-i18n="files.name">Nombre</div>
<div data-i18n="files.type">Tipo</div>
<div data-i18n="files.original_location">Ubicación original</div>
<div data-i18n="files.deleted_date">Fecha eliminación</div>
<div data-i18n="files.actions">Acciones</div>
</div>
`;
// Update breadcrumb for trash
ui.updateBreadcrumb(window.i18n ? window.i18n.t('nav.trash') : 'Papelera');
// Get trash items
const trashItems = await fileOps.getTrashItems();
if (trashItems.length === 0) {
// Show empty state
const emptyState = document.createElement('div');
emptyState.className = 'empty-state';
emptyState.innerHTML = `
<i class="fas fa-trash" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i>
<p>${window.i18n ? window.i18n.t('trash.empty_state') : 'La papelera está vacía'}</p>
`;
elements.filesGrid.appendChild(emptyState);
return;
}
// Process each trash item
trashItems.forEach(item => {
addTrashItemToView(item);
});
} catch (error) {
console.error('Error loading trash items:', error);
window.ui.showNotification('Error', 'Error al cargar elementos de la papelera');
}
}
/**
* Add a trash item to the view
* @param {Object} item - Trash item object
*/
function addTrashItemToView(item) {
const isFile = item.item_type === 'file';
const iconClass = isFile ? 'fas fa-file' : 'fas fa-folder';
// Format date
const deletedDate = new Date(item.deleted_at * 1000);
const formattedDate = deletedDate.toLocaleDateString() + ' ' +
deletedDate.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
// Item type label
const typeLabel = isFile ?
(window.i18n ? window.i18n.t('files.file_types.file') : 'Archivo') :
(window.i18n ? window.i18n.t('files.file_types.folder') : 'Carpeta');
// Grid view element
const gridElement = document.createElement('div');
gridElement.className = 'file-card trash-item';
gridElement.dataset.trashId = item.id;
gridElement.dataset.originalId = item.original_id;
gridElement.dataset.itemType = item.item_type;
gridElement.innerHTML = `
<div class="file-icon">
<i class="${iconClass}"></i>
</div>
<div class="file-name">${item.name}</div>
<div class="file-info">${typeLabel} - ${formattedDate}</div>
<div class="trash-actions">
<button class="btn-restore" title="Restaurar">
<i class="fas fa-undo"></i>
</button>
<button class="btn-delete" title="Eliminar permanentemente">
<i class="fas fa-trash"></i>
</button>
</div>
`;
// Add action buttons event listeners
gridElement.querySelector('.btn-restore').addEventListener('click', async (e) => {
e.stopPropagation();
if (await fileOps.restoreFromTrash(item.id)) {
loadTrashItems();
}
});
gridElement.querySelector('.btn-delete').addEventListener('click', async (e) => {
e.stopPropagation();
if (await fileOps.deletePermanently(item.id)) {
loadTrashItems();
}
});
elements.filesGrid.appendChild(gridElement);
// List view element
const listElement = document.createElement('div');
listElement.className = 'file-item trash-item';
listElement.dataset.trashId = item.id;
listElement.dataset.originalId = item.original_id;
listElement.dataset.itemType = item.item_type;
listElement.innerHTML = `
<div class="name-cell">
<div class="file-icon">
<i class="${iconClass}"></i>
</div>
<span>${item.name}</span>
</div>
<div class="type-cell">${typeLabel}</div>
<div class="path-cell">${item.original_path || '--'}</div>
<div class="date-cell">${formattedDate}</div>
<div class="actions-cell">
<button class="btn-restore" title="Restaurar">
<i class="fas fa-undo"></i>
</button>
<button class="btn-delete" title="Eliminar permanentemente">
<i class="fas fa-trash"></i>
</button>
</div>
`;
// Add action buttons event listeners for list view
listElement.querySelector('.btn-restore').addEventListener('click', async (e) => {
e.stopPropagation();
if (await fileOps.restoreFromTrash(item.id)) {
loadTrashItems();
}
});
listElement.querySelector('.btn-delete').addEventListener('click', async (e) => {
e.stopPropagation();
if (await fileOps.deletePermanently(item.id)) {
loadTrashItems();
}
});
elements.filesListView.appendChild(listElement);
}
// Expose needed functions to global scope
window.app = app;
window.loadFiles = loadFiles;
window.loadTrashItems = loadTrashItems;
window.formatFileSize = formatFileSize;
// Set up global selectFolder function for navigation
+148 -12
View File
@@ -241,27 +241,40 @@ const fileOps = {
},
/**
* Delete a file
* Move a file to trash
* @param {string} fileId - File ID
* @param {string} fileName - File name
* @returns {Promise<boolean>} - Success status
*/
async deleteFile(fileId, fileName) {
if (!confirm(`¿Estás seguro de que quieres eliminar el archivo "${fileName}"?`)) {
if (!confirm(`¿Estás seguro de que quieres mover a la papelera el archivo "${fileName}"?`)) {
return false;
}
try {
const response = await fetch(`/api/files/${fileId}`, {
// Use the trash API endpoint
const response = await fetch(`/api/trash/files/${fileId}`, {
method: 'DELETE'
});
if (response.ok) {
window.loadFiles();
window.ui.showNotification('Archivo eliminado', `"${fileName}" eliminado correctamente`);
window.ui.showNotification('Archivo movido a papelera', `"${fileName}" movido a la papelera`);
return true;
} else {
window.ui.showNotification('Error', 'Error al eliminar el archivo');
return false;
// Fallback to direct deletion if trash fails
const fallbackResponse = await fetch(`/api/files/${fileId}`, {
method: 'DELETE'
});
if (fallbackResponse.ok) {
window.loadFiles();
window.ui.showNotification('Archivo eliminado', `"${fileName}" eliminado correctamente`);
return true;
} else {
window.ui.showNotification('Error', 'Error al eliminar el archivo');
return false;
}
}
} catch (error) {
console.error('Error deleting file:', error);
@@ -271,18 +284,19 @@ const fileOps = {
},
/**
* Delete a folder
* Move a folder to trash
* @param {string} folderId - Folder ID
* @param {string} folderName - Folder name
* @returns {Promise<boolean>} - Success status
*/
async deleteFolder(folderId, folderName) {
if (!confirm(`¿Estás seguro de que quieres eliminar la carpeta "${folderName}" y todo su contenido?`)) {
if (!confirm(`¿Estás seguro de que quieres mover a la papelera la carpeta "${folderName}" y todo su contenido?`)) {
return false;
}
try {
const response = await fetch(`/api/folders/${folderId}`, {
// Use the trash API endpoint
const response = await fetch(`/api/trash/folders/${folderId}`, {
method: 'DELETE'
});
@@ -293,17 +307,139 @@ const fileOps = {
window.ui.updateBreadcrumb('');
}
window.loadFiles();
window.ui.showNotification('Carpeta eliminada', `"${folderName}" eliminada correctamente`);
window.ui.showNotification('Carpeta movida a papelera', `"${folderName}" movida a la papelera`);
return true;
} else {
window.ui.showNotification('Error', 'Error al eliminar la carpeta');
return false;
// Fallback to direct deletion if trash fails
const fallbackResponse = await fetch(`/api/folders/${folderId}`, {
method: 'DELETE'
});
if (fallbackResponse.ok) {
// If we're inside the folder we just deleted, go back up
if (window.app.currentPath === folderId) {
window.app.currentPath = '';
window.ui.updateBreadcrumb('');
}
window.loadFiles();
window.ui.showNotification('Carpeta eliminada', `"${folderName}" eliminada correctamente`);
return true;
} else {
window.ui.showNotification('Error', 'Error al eliminar la carpeta');
return false;
}
}
} catch (error) {
console.error('Error deleting folder:', error);
window.ui.showNotification('Error', 'Error al eliminar la carpeta');
return false;
}
},
/**
* Obtener elementos de la papelera
* @returns {Promise<Array>} - Lista de elementos en la papelera
*/
async getTrashItems() {
try {
const response = await fetch('/api/trash');
if (response.ok) {
return await response.json();
} else {
console.error('Error fetching trash items:', response.statusText);
return [];
}
} catch (error) {
console.error('Error fetching trash items:', error);
return [];
}
},
/**
* Restaurar un elemento desde la papelera
* @param {string} trashId - ID del elemento en la papelera
* @returns {Promise<boolean>} - Éxito de la operación
*/
async restoreFromTrash(trashId) {
try {
const response = await fetch(`/api/trash/${trashId}/restore`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({})
});
if (response.ok) {
window.ui.showNotification('Elemento restaurado', 'Elemento restaurado correctamente');
return true;
} else {
window.ui.showNotification('Error', 'Error al restaurar el elemento');
return false;
}
} catch (error) {
console.error('Error restoring item from trash:', error);
window.ui.showNotification('Error', 'Error al restaurar el elemento');
return false;
}
},
/**
* Eliminar permanentemente un elemento de la papelera
* @param {string} trashId - ID del elemento en la papelera
* @returns {Promise<boolean>} - Éxito de la operación
*/
async deletePermanently(trashId) {
if (!confirm('¿Estás seguro de que quieres eliminar permanentemente este elemento? Esta acción no se puede deshacer.')) {
return false;
}
try {
const response = await fetch(`/api/trash/${trashId}`, {
method: 'DELETE'
});
if (response.ok) {
window.ui.showNotification('Elemento eliminado', 'Elemento eliminado permanentemente');
return true;
} else {
window.ui.showNotification('Error', 'Error al eliminar el elemento');
return false;
}
} catch (error) {
console.error('Error deleting item permanently:', error);
window.ui.showNotification('Error', 'Error al eliminar el elemento');
return false;
}
},
/**
* Vaciar la papelera
* @returns {Promise<boolean>} - Éxito de la operación
*/
async emptyTrash() {
if (!confirm('¿Estás seguro de que quieres vaciar la papelera? Esta acción eliminará permanentemente todos los elementos y no se puede deshacer.')) {
return false;
}
try {
const response = await fetch('/api/trash/empty', {
method: 'DELETE'
});
if (response.ok) {
window.ui.showNotification('Papelera vaciada', 'La papelera ha sido vaciada correctamente');
return true;
} else {
window.ui.showNotification('Error', 'Error al vaciar la papelera');
return false;
}
} catch (error) {
console.error('Error emptying trash:', error);
window.ui.showNotification('Error', 'Error al vaciar la papelera');
return false;
}
}
};