1991 lines
76 KiB
HTML
1991 lines
76 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title data-i18n="app.title">RustCloud</title>
|
|
<style>
|
|
/* Reset y estilos base */
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
}
|
|
|
|
body {
|
|
display: flex;
|
|
height: 100vh;
|
|
background-color: #f5f7fa;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Barra lateral */
|
|
.sidebar {
|
|
width: 250px;
|
|
background-color: #2a3042;
|
|
color: #fff;
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.logo-container {
|
|
padding: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-start;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.logo {
|
|
width: 40px;
|
|
height: 40px;
|
|
background-color: #ff5e3a;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-right: 10px;
|
|
}
|
|
|
|
.logo svg {
|
|
width: 24px;
|
|
height: 24px;
|
|
fill: white;
|
|
}
|
|
|
|
.app-name {
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
color: white;
|
|
}
|
|
|
|
.nav-menu {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex-grow: 1;
|
|
padding: 0 15px;
|
|
}
|
|
|
|
.nav-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 12px 15px;
|
|
margin-bottom: 5px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
color: white;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.nav-item:hover {
|
|
background-color: #3a4157;
|
|
}
|
|
|
|
.nav-item.active {
|
|
background-color: #374e65;
|
|
}
|
|
|
|
.nav-item i {
|
|
margin-right: 15px;
|
|
width: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
/* Indicador de almacenamiento */
|
|
.storage-container {
|
|
margin: 20px 15px;
|
|
background-color: #374e65;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
}
|
|
|
|
.storage-title {
|
|
text-align: center;
|
|
margin-bottom: 10px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.storage-bar {
|
|
height: 10px;
|
|
background-color: #6b7e8f;
|
|
border-radius: 5px;
|
|
overflow: hidden;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.storage-fill {
|
|
height: 100%;
|
|
background-color: #ff5e3a;
|
|
width: 56%;
|
|
}
|
|
|
|
.storage-info {
|
|
text-align: center;
|
|
font-size: 12px;
|
|
color: #f5f5f5;
|
|
}
|
|
|
|
/* Contenido principal */
|
|
.main-content {
|
|
flex-grow: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Barra superior */
|
|
.top-bar {
|
|
height: 70px;
|
|
background-color: white;
|
|
border-bottom: 1px solid #e6e6e6;
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0 20px;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.search-container {
|
|
flex-grow: 1;
|
|
max-width: 500px;
|
|
position: relative;
|
|
}
|
|
|
|
.search-container input {
|
|
width: 100%;
|
|
padding: 10px 15px 10px 40px;
|
|
border-radius: 50px;
|
|
border: none;
|
|
background-color: #f0f3f7;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.search-icon {
|
|
position: absolute;
|
|
left: 15px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: #8895a7;
|
|
}
|
|
|
|
.user-controls {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.language-selector {
|
|
margin-right: 15px;
|
|
padding: 8px 12px;
|
|
background-color: #f0f3f7;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.language-selector::after {
|
|
content: "▼";
|
|
font-size: 10px;
|
|
margin-left: 5px;
|
|
}
|
|
|
|
.user-avatar {
|
|
width: 40px;
|
|
height: 40px;
|
|
background-color: #ff5e3a;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* Área de contenido */
|
|
.content-area {
|
|
flex-grow: 1;
|
|
padding: 20px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.page-title {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
margin-bottom: 20px;
|
|
color: #2d3748;
|
|
}
|
|
|
|
.actions-bar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
|
|
.btn {
|
|
padding: 10px 20px;
|
|
border-radius: 50px;
|
|
border: none;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.btn-primary {
|
|
background-color: #ff5e3a;
|
|
color: white;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background-color: #f0f3f7;
|
|
color: #333;
|
|
}
|
|
|
|
.view-toggle {
|
|
display: flex;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.toggle-btn {
|
|
background-color: #f0f3f7;
|
|
border: none;
|
|
padding: 8px 15px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.toggle-btn.active {
|
|
background-color: #e6e6e6;
|
|
}
|
|
|
|
/* Files grid */
|
|
.files-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
|
|
.file-card {
|
|
background-color: white;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
|
cursor: pointer;
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
}
|
|
|
|
.file-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
|
|
}
|
|
|
|
.file-card.dragging {
|
|
opacity: 0.5;
|
|
transform: scale(0.95);
|
|
box-shadow: none;
|
|
}
|
|
|
|
.file-card.drop-target {
|
|
background-color: rgba(255, 193, 7, 0.1);
|
|
border: 2px dashed #ffc107;
|
|
}
|
|
|
|
.file-icon {
|
|
width: 100px;
|
|
height: 70px;
|
|
margin-bottom: 15px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 32px;
|
|
}
|
|
|
|
.folder-icon {
|
|
color: #ffc107;
|
|
}
|
|
|
|
.file-name {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
text-align: center;
|
|
margin-bottom: 5px;
|
|
color: #2d3748;
|
|
}
|
|
|
|
.file-info {
|
|
font-size: 12px;
|
|
color: #718096;
|
|
text-align: center;
|
|
}
|
|
|
|
/* Lista de archivos */
|
|
.files-list-view {
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
}
|
|
|
|
.list-header {
|
|
display: grid;
|
|
grid-template-columns: minmax(200px, 2fr) 1fr 1fr 120px;
|
|
padding: 10px 15px;
|
|
border-bottom: 1px solid #e0e6ed;
|
|
font-weight: bold;
|
|
color: #2d3748;
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
.file-item {
|
|
display: grid;
|
|
grid-template-columns: minmax(200px, 2fr) 1fr 1fr 120px;
|
|
padding: 12px 15px;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
align-items: center;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.file-item:hover {
|
|
background-color: #f0f8ff;
|
|
}
|
|
|
|
.file-item .name-cell {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.file-item .file-icon {
|
|
font-size: 20px;
|
|
margin-bottom: 0;
|
|
width: auto;
|
|
height: auto;
|
|
}
|
|
|
|
/* Breadcrumb */
|
|
.breadcrumb {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
font-size: 14px;
|
|
color: #666;
|
|
}
|
|
|
|
.breadcrumb-item {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.breadcrumb-item:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.breadcrumb-separator {
|
|
margin: 0 8px;
|
|
}
|
|
|
|
/* Context menu */
|
|
.context-menu {
|
|
position: absolute;
|
|
background-color: white;
|
|
border: 1px solid #ddd;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
padding: 5px 0;
|
|
min-width: 150px;
|
|
z-index: 1000;
|
|
display: none;
|
|
}
|
|
|
|
.context-menu-item {
|
|
padding: 8px 15px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
color: #333;
|
|
}
|
|
|
|
.context-menu-item:hover {
|
|
background-color: #f0f8ff;
|
|
}
|
|
|
|
.context-menu-item i {
|
|
margin-right: 8px;
|
|
width: 16px;
|
|
text-align: center;
|
|
}
|
|
|
|
/* Dialog */
|
|
.rename-dialog {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(0,0,0,0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 2000;
|
|
display: none;
|
|
}
|
|
|
|
.rename-dialog-content {
|
|
background-color: white;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
width: 400px;
|
|
max-width: 90%;
|
|
}
|
|
|
|
.rename-dialog-header {
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.rename-dialog input {
|
|
width: 100%;
|
|
padding: 10px;
|
|
margin-bottom: 15px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.rename-dialog-buttons {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 10px;
|
|
}
|
|
|
|
/* Dropzone */
|
|
.dropzone {
|
|
border: 2px dashed #ddd;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
text-align: center;
|
|
margin: 20px 0;
|
|
color: #666;
|
|
display: none;
|
|
}
|
|
|
|
.dropzone.active {
|
|
border-color: #ff5e3a;
|
|
background-color: rgba(255, 94, 58, 0.05);
|
|
}
|
|
|
|
.upload-progress {
|
|
margin-top: 15px;
|
|
width: 100%;
|
|
display: none;
|
|
}
|
|
|
|
.progress-bar {
|
|
background-color: #f0f0f0;
|
|
height: 6px;
|
|
border-radius: 3px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background-color: #ff5e3a;
|
|
width: 0%;
|
|
transition: width 0.3s;
|
|
}
|
|
|
|
/* Notificación */
|
|
.notification {
|
|
position: absolute;
|
|
top: 70px;
|
|
right: 20px;
|
|
background-color: white;
|
|
width: 250px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
|
padding: 15px;
|
|
border-left: 4px solid #ff5e3a;
|
|
z-index: 1000;
|
|
display: none;
|
|
}
|
|
|
|
.notification-title {
|
|
font-weight: bold;
|
|
font-size: 14px;
|
|
margin-bottom: 5px;
|
|
color: #2d3748;
|
|
}
|
|
|
|
.notification-message {
|
|
font-size: 12px;
|
|
color: #718096;
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {
|
|
.sidebar {
|
|
width: 60px;
|
|
padding: 15px 10px;
|
|
}
|
|
|
|
.app-name, .storage-title, .storage-info {
|
|
display: none;
|
|
}
|
|
|
|
.nav-item {
|
|
justify-content: center;
|
|
padding: 12px 0;
|
|
}
|
|
|
|
.nav-item i {
|
|
margin-right: 0;
|
|
}
|
|
|
|
.nav-item span {
|
|
display: none;
|
|
}
|
|
}
|
|
</style>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
|
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
|
|
<!-- Internationalization scripts -->
|
|
<script src="/js/i18n.js"></script>
|
|
<script src="/js/languageSelector.js"></script>
|
|
<!-- File renderer -->
|
|
<script src="/js/fileRenderer.js"></script>
|
|
|
|
<!-- Service Worker Registration -->
|
|
<script>
|
|
if ('serviceWorker' in navigator) {
|
|
window.addEventListener('load', () => {
|
|
navigator.serviceWorker.register('/sw.js')
|
|
.then(reg => console.log('Service Worker registered successfully'))
|
|
.catch(err => console.log('Service Worker registration failed:', err));
|
|
});
|
|
}
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<!-- Barra lateral -->
|
|
<div class="sidebar">
|
|
<div class="logo-container">
|
|
<div class="logo">
|
|
<svg viewBox="0 0 24 24">
|
|
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="#fff"/>
|
|
</svg>
|
|
</div>
|
|
<div class="app-name">RustCloud</div>
|
|
</div>
|
|
|
|
<div class="nav-menu">
|
|
<div class="nav-item active">
|
|
<i class="fas fa-folder"></i>
|
|
<span data-i18n="nav.files">Files</span>
|
|
</div>
|
|
<div class="nav-item">
|
|
<i class="fas fa-share-alt"></i>
|
|
<span data-i18n="nav.shared">Shared</span>
|
|
</div>
|
|
<div class="nav-item">
|
|
<i class="fas fa-clock"></i>
|
|
<span data-i18n="nav.recent">Recent</span>
|
|
</div>
|
|
<div class="nav-item">
|
|
<i class="fas fa-star"></i>
|
|
<span data-i18n="nav.favorites">Favorites</span>
|
|
</div>
|
|
<div class="nav-item">
|
|
<i class="fas fa-trash"></i>
|
|
<span data-i18n="nav.trash">Trash</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="storage-container">
|
|
<div class="storage-title">Almacenamiento</div>
|
|
<div class="storage-bar">
|
|
<div class="storage-fill"></div>
|
|
</div>
|
|
<div class="storage-info">56% usado (5.6GB / 10GB)</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Contenido principal -->
|
|
<div class="main-content">
|
|
<!-- Barra superior -->
|
|
<div class="top-bar">
|
|
<div class="search-container">
|
|
<i class="fas fa-search search-icon"></i>
|
|
<input type="text" data-i18n-placeholder="actions.search" placeholder="Buscar archivos, carpetas...">
|
|
</div>
|
|
|
|
<div class="user-controls">
|
|
<div id="language-selector" class="language-selector"></div>
|
|
<div class="user-avatar">MR</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="content-area">
|
|
<h1 class="page-title" data-i18n="nav.files">Archivos</h1>
|
|
|
|
<div class="actions-bar">
|
|
<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>
|
|
</div>
|
|
|
|
<div class="dropzone" id="dropzone">
|
|
<i class="fas fa-cloud-upload-alt" style="font-size: 32px; margin-bottom: 10px;"></i>
|
|
<p data-i18n="dropzone.drag_files">Arrastra archivos aquí o haz clic para seleccionar</p>
|
|
<input type="file" id="file-input" style="display: none;" multiple>
|
|
<div class="upload-progress">
|
|
<div class="progress-bar">
|
|
<div class="progress-fill"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="breadcrumb">
|
|
<span class="breadcrumb-item" data-i18n="breadcrumb.home">Home</span>
|
|
</div>
|
|
|
|
<!-- Files Container -->
|
|
<div class="files-container">
|
|
<!-- Grid View -->
|
|
<div class="files-grid" id="files-grid">
|
|
<!-- Files will be populated here -->
|
|
</div>
|
|
|
|
<!-- List View (hidden by default) -->
|
|
<div class="files-list-view" id="files-list-view" style="display: none;">
|
|
<div class="list-header">
|
|
<div data-i18n="files.name">Nombre</div>
|
|
<div data-i18n="files.type">Tipo</div>
|
|
<div data-i18n="files.size">Tamaño</div>
|
|
<div data-i18n="files.modified">Modificado</div>
|
|
</div>
|
|
<!-- Files will be populated here in list view -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Context menus and dialogs will be created dynamically -->
|
|
|
|
<script>
|
|
// Add missing translations
|
|
window.addEventListener('DOMContentLoaded', function() {
|
|
// Wait for i18n to initialize
|
|
setTimeout(function() {
|
|
if (window.i18n) {
|
|
// English
|
|
if (!window.i18n.t('files.view_grid')) {
|
|
// Add view_grid and view_list translations to existing English translations
|
|
const enTranslations = {
|
|
'files.view_grid': 'Grid view',
|
|
'files.view_list': 'List view'
|
|
};
|
|
|
|
// Add them to the i18n system (if possible)
|
|
for (const key in enTranslations) {
|
|
// We're simulating adding translations to the cache
|
|
// This is a workaround and might need adaptation for your actual i18n system
|
|
const baseObj = window.i18n._getBaseObjectForKey ?
|
|
window.i18n._getBaseObjectForKey('en', key) : null;
|
|
|
|
if (baseObj) {
|
|
const lastKey = key.split('.').pop();
|
|
baseObj[lastKey] = enTranslations[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Spanish
|
|
if (!window.i18n.t('files.view_grid')) {
|
|
// Add view_grid and view_list translations to existing Spanish translations
|
|
const esTranslations = {
|
|
'files.view_grid': 'Vista de cuadrícula',
|
|
'files.view_list': 'Vista de lista'
|
|
};
|
|
|
|
// Add them to the i18n system (if possible)
|
|
for (const key in esTranslations) {
|
|
// We're simulating adding translations to the cache
|
|
// This is a workaround and might need adaptation for your actual i18n system
|
|
const baseObj = window.i18n._getBaseObjectForKey ?
|
|
window.i18n._getBaseObjectForKey('es', key) : null;
|
|
|
|
if (baseObj) {
|
|
const lastKey = key.split('.').pop();
|
|
baseObj[lastKey] = esTranslations[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Re-translate the page
|
|
if (window.i18n.translatePage) {
|
|
window.i18n.translatePage();
|
|
}
|
|
}
|
|
}, 500);
|
|
});
|
|
|
|
// Helper function to format file size
|
|
function formatFileSize(bytes) {
|
|
if (bytes === 0) return '0 Bytes';
|
|
|
|
const k = 1024;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Variables y elementos DOM globales
|
|
const uploadBtn = document.getElementById('upload-btn');
|
|
const dropzone = document.getElementById('dropzone');
|
|
const fileInput = document.getElementById('file-input');
|
|
const filesGrid = document.getElementById('files-grid');
|
|
const filesListView = document.getElementById('files-list-view');
|
|
const newFolderBtn = document.getElementById('new-folder-btn');
|
|
const gridViewBtn = document.getElementById('grid-view-btn');
|
|
const listViewBtn = document.getElementById('list-view-btn');
|
|
|
|
// Variables para menús contextuales
|
|
let contextMenuTargetFolder = null;
|
|
let contextMenuTargetFile = null;
|
|
let selectedTargetFolderId = "";
|
|
let moveDialogMode = 'file'; // 'file' o 'folder'
|
|
|
|
// Current view state
|
|
let currentView = 'grid'; // 'grid' or 'list'
|
|
let currentPath = '';
|
|
|
|
// Crear los menús contextuales y diálogos
|
|
initializeContextMenus();
|
|
|
|
// Configurar listeners para arrastrar y soltar archivos
|
|
setupDragAndDrop();
|
|
|
|
// Configurar listeners de eventos para botones
|
|
setupEventListeners();
|
|
|
|
// Cargar la vista inicial
|
|
currentPath = '';
|
|
updateBreadcrumb('');
|
|
loadFiles();
|
|
|
|
/**
|
|
* Inicializa los menús contextuales y diálogos
|
|
*/
|
|
function initializeContextMenus() {
|
|
// Menú contextual para carpetas
|
|
if (!document.getElementById('folder-context-menu')) {
|
|
const folderMenu = document.createElement('div');
|
|
folderMenu.className = 'context-menu';
|
|
folderMenu.id = 'folder-context-menu';
|
|
folderMenu.innerHTML = `
|
|
<div class="context-menu-item" id="rename-folder-option">
|
|
<i class="fas fa-edit"></i> <span data-i18n="actions.rename">Renombrar</span>
|
|
</div>
|
|
<div class="context-menu-item" id="move-folder-option">
|
|
<i class="fas fa-exchange-alt"></i> <span data-i18n="actions.move">Mover a...</span>
|
|
</div>
|
|
<div class="context-menu-item" id="delete-folder-option">
|
|
<i class="fas fa-trash"></i> <span data-i18n="actions.delete">Eliminar</span>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(folderMenu);
|
|
}
|
|
|
|
// Menú contextual para archivos
|
|
if (!document.getElementById('file-context-menu')) {
|
|
const fileMenu = document.createElement('div');
|
|
fileMenu.className = 'context-menu';
|
|
fileMenu.id = 'file-context-menu';
|
|
fileMenu.innerHTML = `
|
|
<div class="context-menu-item" id="move-file-option">
|
|
<i class="fas fa-exchange-alt"></i> <span data-i18n="actions.move">Mover a...</span>
|
|
</div>
|
|
<div class="context-menu-item" id="delete-file-option">
|
|
<i class="fas fa-trash"></i> <span data-i18n="actions.delete">Eliminar</span>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(fileMenu);
|
|
}
|
|
|
|
// Diálogo de renombrar
|
|
if (!document.getElementById('rename-dialog')) {
|
|
const renameDialog = document.createElement('div');
|
|
renameDialog.className = 'rename-dialog';
|
|
renameDialog.id = 'rename-dialog';
|
|
renameDialog.innerHTML = `
|
|
<div class="rename-dialog-content">
|
|
<div class="rename-dialog-header" data-i18n="dialogs.rename_folder">Renombrar carpeta</div>
|
|
<input type="text" id="rename-input" data-i18n-placeholder="dialogs.new_name" placeholder="Nuevo nombre">
|
|
<div class="rename-dialog-buttons">
|
|
<button class="btn" id="rename-cancel-btn" data-i18n="actions.cancel">Cancelar</button>
|
|
<button class="btn btn-primary" id="rename-confirm-btn" data-i18n="actions.rename">Renombrar</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(renameDialog);
|
|
}
|
|
|
|
// Diálogo de mover
|
|
if (!document.getElementById('move-file-dialog')) {
|
|
const moveDialog = document.createElement('div');
|
|
moveDialog.className = 'rename-dialog';
|
|
moveDialog.id = 'move-file-dialog';
|
|
moveDialog.innerHTML = `
|
|
<div class="rename-dialog-content">
|
|
<div class="rename-dialog-header" data-i18n="dialogs.move_file">Mover archivo</div>
|
|
<p data-i18n="dialogs.select_destination">Selecciona la carpeta destino:</p>
|
|
<div id="folder-select-container" style="max-height: 200px; overflow-y: auto; margin: 15px 0; border: 1px solid #ddd; border-radius: 4px; padding: 10px;">
|
|
<!-- Las carpetas se cargarán aquí dinámicamente -->
|
|
<div class="folder-select-item" data-folder-id="">
|
|
<i class="fas fa-folder"></i> <span data-i18n="dialogs.root">Raíz</span>
|
|
</div>
|
|
</div>
|
|
<div class="rename-dialog-buttons">
|
|
<button class="btn" id="move-cancel-btn" data-i18n="actions.cancel">Cancelar</button>
|
|
<button class="btn btn-primary" id="move-confirm-btn" data-i18n="actions.move_to">Mover</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(moveDialog);
|
|
}
|
|
|
|
// Asignar eventos a los menús y diálogos después de crearlos
|
|
assignMenuEvents();
|
|
}
|
|
|
|
/**
|
|
* Asigna eventos a los menús contextuales y diálogos
|
|
*/
|
|
function assignMenuEvents() {
|
|
// Variables para elementos de los menús y diálogos
|
|
const folderContextMenu = document.getElementById('folder-context-menu');
|
|
const fileContextMenu = document.getElementById('file-context-menu');
|
|
const renameDialog = document.getElementById('rename-dialog');
|
|
const renameInput = document.getElementById('rename-input');
|
|
const renameCancelBtn = document.getElementById('rename-cancel-btn');
|
|
const renameConfirmBtn = document.getElementById('rename-confirm-btn');
|
|
const moveFileDialog = document.getElementById('move-file-dialog');
|
|
const folderSelectContainer = document.getElementById('folder-select-container');
|
|
const moveCancelBtn = document.getElementById('move-cancel-btn');
|
|
const moveConfirmBtn = document.getElementById('move-confirm-btn');
|
|
|
|
// Eventos para el menú contextual de carpetas
|
|
document.getElementById('rename-folder-option').addEventListener('click', () => {
|
|
if (contextMenuTargetFolder) {
|
|
showRenameDialog(contextMenuTargetFolder);
|
|
}
|
|
closeContextMenu();
|
|
});
|
|
|
|
document.getElementById('move-folder-option').addEventListener('click', () => {
|
|
if (contextMenuTargetFolder) {
|
|
showMoveDialog(contextMenuTargetFolder, 'folder');
|
|
}
|
|
closeContextMenu();
|
|
});
|
|
|
|
document.getElementById('delete-folder-option').addEventListener('click', async () => {
|
|
if (contextMenuTargetFolder) {
|
|
if (confirm(`¿Estás seguro de que quieres eliminar la carpeta "${contextMenuTargetFolder.name}" y todo su contenido?`)) {
|
|
try {
|
|
const response = await fetch(`/api/folders/${contextMenuTargetFolder.id}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Si estamos dentro de la carpeta que acabamos de eliminar, volver al nivel superior
|
|
if (currentPath === contextMenuTargetFolder.id) {
|
|
currentPath = '';
|
|
updateBreadcrumb('');
|
|
}
|
|
loadFiles();
|
|
} else {
|
|
alert('Error al eliminar la carpeta');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting folder:', error);
|
|
alert('Error al eliminar la carpeta');
|
|
}
|
|
}
|
|
}
|
|
closeContextMenu();
|
|
});
|
|
|
|
// Eventos para el menú contextual de archivos
|
|
document.getElementById('move-file-option').addEventListener('click', () => {
|
|
if (contextMenuTargetFile) {
|
|
showMoveDialog(contextMenuTargetFile, 'file');
|
|
}
|
|
closeFileContextMenu();
|
|
});
|
|
|
|
document.getElementById('delete-file-option').addEventListener('click', async () => {
|
|
if (contextMenuTargetFile) {
|
|
if (confirm(`¿Estás seguro de que quieres eliminar el archivo "${contextMenuTargetFile.name}"?`)) {
|
|
try {
|
|
const response = await fetch(`/api/files/${contextMenuTargetFile.id}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (response.ok) {
|
|
loadFiles();
|
|
} else {
|
|
alert('Error al eliminar el archivo');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting file:', error);
|
|
alert('Error al eliminar el archivo');
|
|
}
|
|
}
|
|
}
|
|
closeFileContextMenu();
|
|
});
|
|
|
|
// Eventos para el diálogo de renombrar
|
|
renameCancelBtn.addEventListener('click', closeRenameDialog);
|
|
renameConfirmBtn.addEventListener('click', renameFolder);
|
|
|
|
// Eventos para el diálogo de mover
|
|
moveCancelBtn.addEventListener('click', closeMoveDialog);
|
|
moveConfirmBtn.addEventListener('click', async () => {
|
|
if (moveDialogMode === 'file' && contextMenuTargetFile) {
|
|
const success = await moveFile(contextMenuTargetFile.id, selectedTargetFolderId);
|
|
if (success) {
|
|
closeMoveDialog();
|
|
}
|
|
} else if (moveDialogMode === 'folder' && contextMenuTargetFolder) {
|
|
const success = await moveFolder(contextMenuTargetFolder.id, selectedTargetFolderId);
|
|
if (success) {
|
|
closeMoveDialog();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Evento para renombrar al presionar Enter en el input
|
|
renameInput.addEventListener('keyup', (e) => {
|
|
if (e.key === 'Enter') {
|
|
renameFolder();
|
|
} else if (e.key === 'Escape') {
|
|
closeRenameDialog();
|
|
}
|
|
});
|
|
|
|
// Cerrar menús al hacer clic en cualquier parte
|
|
document.addEventListener('click', (e) => {
|
|
if (folderContextMenu && folderContextMenu.style.display === 'block' &&
|
|
!folderContextMenu.contains(e.target)) {
|
|
closeContextMenu();
|
|
}
|
|
|
|
if (fileContextMenu && fileContextMenu.style.display === 'block' &&
|
|
!fileContextMenu.contains(e.target)) {
|
|
closeFileContextMenu();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Configura los listeners para arrastrar y soltar archivos
|
|
*/
|
|
function setupDragAndDrop() {
|
|
// Arrastrar y soltar en el elemento dropzone
|
|
dropzone.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
dropzone.classList.add('active');
|
|
});
|
|
|
|
dropzone.addEventListener('dragleave', () => {
|
|
dropzone.classList.remove('active');
|
|
});
|
|
|
|
dropzone.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
dropzone.classList.remove('active');
|
|
if (e.dataTransfer.files.length > 0) {
|
|
uploadFiles(e.dataTransfer.files);
|
|
}
|
|
});
|
|
|
|
// Arrastrar y soltar en toda la página
|
|
document.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
// Mostrar la zona de soltar cuando se arrastra algo sobre la página
|
|
if (e.dataTransfer.types.includes('Files')) {
|
|
dropzone.style.display = 'block';
|
|
dropzone.classList.add('active');
|
|
}
|
|
});
|
|
|
|
document.addEventListener('dragleave', (e) => {
|
|
// Solo ocultar si el ratón sale completamente de la ventana
|
|
if (e.clientX <= 0 || e.clientY <= 0 ||
|
|
e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) {
|
|
dropzone.classList.remove('active');
|
|
// Opcional: ocultar después de un breve retraso
|
|
setTimeout(() => {
|
|
if (!dropzone.classList.contains('active')) {
|
|
dropzone.style.display = 'none';
|
|
}
|
|
}, 100);
|
|
}
|
|
});
|
|
|
|
document.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
dropzone.classList.remove('active');
|
|
|
|
if (e.dataTransfer.files.length > 0) {
|
|
uploadFiles(e.dataTransfer.files);
|
|
}
|
|
|
|
// Ocultar la zona después de soltar
|
|
setTimeout(() => {
|
|
dropzone.style.display = 'none';
|
|
}, 500);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Configura los listeners de eventos para botones
|
|
*/
|
|
function setupEventListeners() {
|
|
// Toggle dropzone visibility
|
|
uploadBtn.addEventListener('click', () => {
|
|
dropzone.style.display = dropzone.style.display === 'none' ? 'block' : 'none';
|
|
if (dropzone.style.display === 'block') {
|
|
fileInput.click();
|
|
}
|
|
});
|
|
|
|
// File input change event
|
|
fileInput.addEventListener('change', (e) => {
|
|
if (e.target.files.length > 0) {
|
|
uploadFiles(e.target.files);
|
|
}
|
|
});
|
|
|
|
// New folder button
|
|
newFolderBtn.addEventListener('click', () => {
|
|
const folderName = prompt('Nombre de la carpeta:');
|
|
if (folderName) {
|
|
createFolder(folderName);
|
|
}
|
|
});
|
|
|
|
// View toggle event listeners
|
|
gridViewBtn.addEventListener('click', switchToGridView);
|
|
listViewBtn.addEventListener('click', switchToListView);
|
|
|
|
// Load saved view preference
|
|
const savedView = localStorage.getItem('oxicloud-view');
|
|
if (savedView === 'list') {
|
|
switchToListView();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cambia a vista de cuadrícula
|
|
*/
|
|
function switchToGridView() {
|
|
filesGrid.style.display = 'grid';
|
|
filesListView.style.display = 'none';
|
|
gridViewBtn.classList.add('active');
|
|
listViewBtn.classList.remove('active');
|
|
currentView = 'grid';
|
|
localStorage.setItem('oxicloud-view', 'grid');
|
|
}
|
|
|
|
/**
|
|
* Cambia a vista de lista
|
|
*/
|
|
function switchToListView() {
|
|
filesGrid.style.display = 'none';
|
|
filesListView.style.display = 'flex';
|
|
gridViewBtn.classList.remove('active');
|
|
listViewBtn.classList.add('active');
|
|
currentView = 'list';
|
|
localStorage.setItem('oxicloud-view', 'list');
|
|
}
|
|
|
|
/**
|
|
* Cierra el menú contextual de carpetas
|
|
*/
|
|
function closeContextMenu() {
|
|
const menu = document.getElementById('folder-context-menu');
|
|
if (menu) {
|
|
menu.style.display = 'none';
|
|
contextMenuTargetFolder = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cierra el menú contextual de archivos
|
|
*/
|
|
function closeFileContextMenu() {
|
|
const menu = document.getElementById('file-context-menu');
|
|
if (menu) {
|
|
menu.style.display = 'none';
|
|
contextMenuTargetFile = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Muestra el diálogo de renombrar para una carpeta
|
|
*/
|
|
function showRenameDialog(folder) {
|
|
const renameInput = document.getElementById('rename-input');
|
|
const renameDialog = document.getElementById('rename-dialog');
|
|
|
|
renameInput.value = folder.name;
|
|
renameDialog.style.display = 'flex';
|
|
renameInput.focus();
|
|
renameInput.select();
|
|
}
|
|
|
|
/**
|
|
* Cierra el diálogo de renombrar
|
|
*/
|
|
function closeRenameDialog() {
|
|
document.getElementById('rename-dialog').style.display = 'none';
|
|
contextMenuTargetFolder = null;
|
|
}
|
|
|
|
/**
|
|
* Muestra el diálogo de mover para un archivo o carpeta
|
|
*/
|
|
async function showMoveDialog(item, mode) {
|
|
// Establecer el modo (archivo o carpeta)
|
|
moveDialogMode = mode;
|
|
|
|
// Restablecer la selección
|
|
selectedTargetFolderId = "";
|
|
|
|
// Actualizar título del diálogo
|
|
const dialogHeader = document.getElementById('move-file-dialog').querySelector('.rename-dialog-header');
|
|
dialogHeader.textContent = mode === 'file' ?
|
|
(window.i18n ? window.i18n.t('dialogs.move_file') : 'Mover archivo') :
|
|
(window.i18n ? window.i18n.t('dialogs.move_folder') : 'Mover carpeta');
|
|
|
|
// Cargar todas las carpetas disponibles
|
|
await loadAllFolders(item.id, mode);
|
|
|
|
// Mostrar el diálogo
|
|
document.getElementById('move-file-dialog').style.display = 'flex';
|
|
}
|
|
|
|
/**
|
|
* Cierra el diálogo de mover
|
|
*/
|
|
function closeMoveDialog() {
|
|
document.getElementById('move-file-dialog').style.display = 'none';
|
|
contextMenuTargetFile = null;
|
|
contextMenuTargetFolder = null;
|
|
}
|
|
|
|
/**
|
|
* Renombra la carpeta seleccionada
|
|
*/
|
|
async function renameFolder() {
|
|
if (!contextMenuTargetFolder) return;
|
|
|
|
const newName = document.getElementById('rename-input').value.trim();
|
|
if (!newName) {
|
|
alert(window.i18n ? window.i18n.t('errors.empty_name') : 'El nombre no puede estar vacío');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log(`Renaming folder ${contextMenuTargetFolder.id} to "${newName}"`);
|
|
|
|
const response = await fetch(`/api/folders/${contextMenuTargetFolder.id}/rename`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ name: newName })
|
|
});
|
|
|
|
console.log('Response status:', response.status);
|
|
|
|
if (response.ok) {
|
|
// Cerramos el diálogo y recargamos los archivos
|
|
closeRenameDialog();
|
|
loadFiles();
|
|
} else {
|
|
const errorText = await response.text();
|
|
console.error('Error response:', errorText);
|
|
|
|
let errorMessage = 'Error desconocido';
|
|
try {
|
|
// Intentar parsear como JSON
|
|
const errorData = JSON.parse(errorText);
|
|
errorMessage = errorData.error || response.statusText;
|
|
} catch (e) {
|
|
// Si no es JSON, usar el texto tal cual
|
|
errorMessage = errorText || response.statusText;
|
|
}
|
|
|
|
alert(`Error al renombrar la carpeta: ${errorMessage}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error renaming folder:', error);
|
|
alert('Error al renombrar la carpeta. Inténtalo de nuevo.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Carga todas las carpetas para el diálogo de mover
|
|
*/
|
|
async function loadAllFolders(itemId, mode) {
|
|
try {
|
|
const response = await fetch('/api/folders');
|
|
if (response.ok) {
|
|
const folders = await response.json();
|
|
const folderSelectContainer = document.getElementById('folder-select-container');
|
|
|
|
// Limpiar el contenedor excepto la opción de carpeta raíz
|
|
folderSelectContainer.innerHTML = `
|
|
<div class="folder-select-item selected" data-folder-id="">
|
|
<i class="fas fa-folder"></i> <span data-i18n="dialogs.root">Raíz</span>
|
|
</div>
|
|
`;
|
|
|
|
// Seleccionar la raíz por defecto
|
|
selectedTargetFolderId = "";
|
|
|
|
// Añadir todas las carpetas disponibles
|
|
if (Array.isArray(folders)) {
|
|
folders.forEach(folder => {
|
|
// Si es una carpeta, no mostrar:
|
|
// 1. La propia carpeta que se mueve
|
|
// 2. Ninguna subcarpeta de la carpeta que se mueve (para evitar ciclos)
|
|
if (mode === 'folder' && folder.id === itemId) {
|
|
return;
|
|
}
|
|
|
|
// No mostrar la carpeta actual del elemento como opción
|
|
if (mode === 'file' && contextMenuTargetFile && folder.id === contextMenuTargetFile.folder_id) {
|
|
return;
|
|
}
|
|
|
|
if (mode === 'folder' && contextMenuTargetFolder && folder.id === contextMenuTargetFolder.folder_id) {
|
|
return;
|
|
}
|
|
|
|
const folderItem = document.createElement('div');
|
|
folderItem.className = 'folder-select-item';
|
|
folderItem.dataset.folderId = folder.id;
|
|
folderItem.innerHTML = `<i class="fas fa-folder"></i> ${folder.name}`;
|
|
|
|
folderItem.addEventListener('click', () => {
|
|
// Deseleccionar todos
|
|
document.querySelectorAll('.folder-select-item').forEach(item => {
|
|
item.classList.remove('selected');
|
|
});
|
|
|
|
// Seleccionar este
|
|
folderItem.classList.add('selected');
|
|
selectedTargetFolderId = folder.id;
|
|
});
|
|
|
|
folderSelectContainer.appendChild(folderItem);
|
|
});
|
|
}
|
|
|
|
// Evento para la opción de carpeta raíz
|
|
const rootOption = folderSelectContainer.querySelector('.folder-select-item');
|
|
rootOption.addEventListener('click', () => {
|
|
document.querySelectorAll('.folder-select-item').forEach(item => {
|
|
item.classList.remove('selected');
|
|
});
|
|
rootOption.classList.add('selected');
|
|
selectedTargetFolderId = "";
|
|
});
|
|
|
|
// Re-traducir la página para aplicar las traducciones a los nuevos elementos
|
|
if (window.i18n && window.i18n.translatePage) {
|
|
window.i18n.translatePage();
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading folders:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mueve un archivo a otra carpeta
|
|
*/
|
|
async function moveFile(fileId, targetFolderId) {
|
|
try {
|
|
const response = await fetch(`/api/files/${fileId}/move`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
folder_id: targetFolderId === "" ? null : targetFolderId
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Recargar archivos después de mover
|
|
await loadFiles();
|
|
return true;
|
|
} else {
|
|
let errorMessage = 'Error desconocido';
|
|
try {
|
|
const errorData = await response.json();
|
|
errorMessage = errorData.error || 'Error desconocido';
|
|
} catch (e) {
|
|
errorMessage = 'Error al procesar la respuesta del servidor';
|
|
}
|
|
alert(`Error al mover el archivo: ${errorMessage}`);
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error moving file:', error);
|
|
alert('Error al mover el archivo. Inténtalo de nuevo.');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mueve una carpeta a otra carpeta
|
|
*/
|
|
async function moveFolder(folderId, targetFolderId) {
|
|
try {
|
|
const response = await fetch(`/api/folders/${folderId}/move`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
parent_id: targetFolderId === "" ? null : targetFolderId
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Recargar archivos después de mover
|
|
await loadFiles();
|
|
return true;
|
|
} else {
|
|
let errorMessage = 'Error desconocido';
|
|
try {
|
|
const errorData = await response.json();
|
|
errorMessage = errorData.error || 'Error desconocido';
|
|
} catch (e) {
|
|
errorMessage = 'Error al procesar la respuesta del servidor';
|
|
}
|
|
alert(`Error al mover la carpeta: ${errorMessage}`);
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error moving folder:', error);
|
|
alert('Error al mover la carpeta. Inténtalo de nuevo.');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Crea una nueva carpeta
|
|
*/
|
|
async function createFolder(name) {
|
|
try {
|
|
const response = await fetch('/api/folders', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
name: name,
|
|
parent_id: currentPath || null
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
loadFiles();
|
|
} else {
|
|
alert('Error al crear la carpeta');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error creating folder:', error);
|
|
alert('Error al crear la carpeta');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Actualiza la información de ruta (breadcrumb)
|
|
*/
|
|
function updateBreadcrumb(folderName) {
|
|
const breadcrumb = document.querySelector('.breadcrumb');
|
|
breadcrumb.innerHTML = '';
|
|
|
|
const homeItem = document.createElement('span');
|
|
homeItem.className = 'breadcrumb-item';
|
|
homeItem.textContent = window.i18n ? window.i18n.t('breadcrumb.home') : 'Home';
|
|
homeItem.addEventListener('click', () => {
|
|
currentPath = '';
|
|
updateBreadcrumb('');
|
|
loadFiles();
|
|
});
|
|
|
|
breadcrumb.appendChild(homeItem);
|
|
|
|
if (folderName) {
|
|
const separator = document.createElement('span');
|
|
separator.className = 'breadcrumb-separator';
|
|
separator.textContent = '>';
|
|
breadcrumb.appendChild(separator);
|
|
|
|
const folderItem = document.createElement('span');
|
|
folderItem.className = 'breadcrumb-item';
|
|
folderItem.textContent = folderName;
|
|
breadcrumb.appendChild(folderItem);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Carga archivos y carpetas
|
|
*/
|
|
async function loadFiles() {
|
|
try {
|
|
let url = '/api/folders';
|
|
if (currentPath) {
|
|
url += `/${currentPath}`;
|
|
}
|
|
|
|
const response = await fetch(url);
|
|
if (!response.ok) {
|
|
throw new Error(`Server responded with status: ${response.status}`);
|
|
}
|
|
const folders = await response.json();
|
|
|
|
// Clear existing files in both views
|
|
filesGrid.innerHTML = '';
|
|
filesListView.innerHTML = `
|
|
<div class="list-header">
|
|
<div data-i18n="files.name">Nombre</div>
|
|
<div data-i18n="files.type">Tipo</div>
|
|
<div data-i18n="files.size">Tamaño</div>
|
|
<div data-i18n="files.modified">Modificado</div>
|
|
</div>
|
|
`;
|
|
|
|
// Re-traducir la página para aplicar las traducciones a los nuevos elementos de la cabecera
|
|
if (window.i18n && window.i18n.translatePage) {
|
|
window.i18n.translatePage();
|
|
}
|
|
|
|
// Add folders (check if it's an array)
|
|
const folderList = Array.isArray(folders) ? folders : [];
|
|
folderList.forEach(folder => {
|
|
addFolderToView(folder);
|
|
});
|
|
|
|
// Also load files in this folder
|
|
let filesUrl = '/api/files';
|
|
if (currentPath) {
|
|
filesUrl += `?folder_id=${currentPath}`;
|
|
}
|
|
|
|
try {
|
|
const filesResponse = await fetch(filesUrl);
|
|
if (filesResponse.ok) {
|
|
const files = await filesResponse.json();
|
|
|
|
// Add files (check if it's an array)
|
|
const fileList = Array.isArray(files) ? files : [];
|
|
fileList.forEach(file => {
|
|
addFileToView(file);
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading files:', error);
|
|
// File API may not be implemented yet, so we silently ignore this error
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading files:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Añade una carpeta a las vistas
|
|
*/
|
|
function addFolderToView(folder) {
|
|
// Create folder element for grid view
|
|
const folderGridElement = document.createElement('div');
|
|
folderGridElement.className = 'file-card';
|
|
folderGridElement.dataset.folderId = folder.id;
|
|
folderGridElement.dataset.folderName = folder.name;
|
|
folderGridElement.dataset.parentId = folder.parent_id || "";
|
|
folderGridElement.innerHTML = `
|
|
<div class="file-icon folder-icon">
|
|
<i class="fas fa-folder"></i>
|
|
</div>
|
|
<div class="file-name">${folder.name}</div>
|
|
<div class="file-info">15 archivos</div>
|
|
`;
|
|
|
|
// Hacer la carpeta arrastrable (excepto si estamos en la misma carpeta que es su padre)
|
|
if (currentPath !== "") {
|
|
folderGridElement.setAttribute('draggable', 'true');
|
|
|
|
// Eventos de arrastrar
|
|
folderGridElement.addEventListener('dragstart', (e) => {
|
|
e.dataTransfer.setData('text/plain', folder.id);
|
|
e.dataTransfer.setData('application/oxicloud-folder', 'true');
|
|
folderGridElement.classList.add('dragging');
|
|
});
|
|
|
|
folderGridElement.addEventListener('dragend', () => {
|
|
folderGridElement.classList.remove('dragging');
|
|
document.querySelectorAll('.drop-target').forEach(el => {
|
|
el.classList.remove('drop-target');
|
|
});
|
|
});
|
|
}
|
|
|
|
// Evento de clic normal para navegar
|
|
folderGridElement.addEventListener('click', () => {
|
|
currentPath = folder.id;
|
|
updateBreadcrumb(folder.name);
|
|
loadFiles();
|
|
});
|
|
|
|
// Evento de clic derecho para el menú contextual
|
|
folderGridElement.addEventListener('contextmenu', (e) => {
|
|
e.preventDefault();
|
|
|
|
// Guardamos la carpeta seleccionada
|
|
contextMenuTargetFolder = {
|
|
id: folder.id,
|
|
name: folder.name,
|
|
parent_id: folder.parent_id || ""
|
|
};
|
|
|
|
// Posicionamos y mostramos el menú contextual
|
|
let folderContextMenu = document.getElementById('folder-context-menu');
|
|
folderContextMenu.style.left = `${e.pageX}px`;
|
|
folderContextMenu.style.top = `${e.pageY}px`;
|
|
folderContextMenu.style.display = 'block';
|
|
});
|
|
|
|
// Eventos para arrastrar y soltar archivos en carpetas
|
|
folderGridElement.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
folderGridElement.classList.add('drop-target');
|
|
});
|
|
|
|
folderGridElement.addEventListener('dragleave', () => {
|
|
folderGridElement.classList.remove('drop-target');
|
|
});
|
|
|
|
folderGridElement.addEventListener('drop', async (e) => {
|
|
e.preventDefault();
|
|
folderGridElement.classList.remove('drop-target');
|
|
|
|
const id = e.dataTransfer.getData('text/plain');
|
|
const isFolder = e.dataTransfer.getData('application/oxicloud-folder') === 'true';
|
|
|
|
if (id) {
|
|
if (isFolder) {
|
|
// Evitar mover una carpeta a sí misma o a su hijo
|
|
if (id === folder.id) {
|
|
alert("No puedes mover una carpeta a sí misma");
|
|
return;
|
|
}
|
|
|
|
await moveFolder(id, folder.id);
|
|
} else {
|
|
await moveFile(id, folder.id);
|
|
}
|
|
}
|
|
});
|
|
|
|
filesGrid.appendChild(folderGridElement);
|
|
|
|
// Create folder element for list view
|
|
const folderListElement = document.createElement('div');
|
|
folderListElement.className = 'file-item';
|
|
folderListElement.dataset.folderId = folder.id;
|
|
folderListElement.dataset.folderName = folder.name;
|
|
folderListElement.dataset.parentId = folder.parent_id || "";
|
|
|
|
// Convert timestamps to human readable format
|
|
const modifiedDate = new Date(folder.modified_at * 1000);
|
|
const formattedDate = modifiedDate.toLocaleDateString() + ' ' +
|
|
modifiedDate.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
|
|
|
// Hacer la carpeta arrastrable (excepto si estamos en la misma carpeta que es su padre)
|
|
if (currentPath !== "") {
|
|
folderListElement.setAttribute('draggable', 'true');
|
|
|
|
// Eventos de arrastrar
|
|
folderListElement.addEventListener('dragstart', (e) => {
|
|
e.dataTransfer.setData('text/plain', folder.id);
|
|
e.dataTransfer.setData('application/oxicloud-folder', 'true');
|
|
folderListElement.classList.add('dragging');
|
|
});
|
|
|
|
folderListElement.addEventListener('dragend', () => {
|
|
folderListElement.classList.remove('dragging');
|
|
document.querySelectorAll('.drop-target').forEach(el => {
|
|
el.classList.remove('drop-target');
|
|
});
|
|
});
|
|
}
|
|
|
|
folderListElement.innerHTML = `
|
|
<div class="name-cell">
|
|
<div class="file-icon folder-icon">
|
|
<i class="fas fa-folder"></i>
|
|
</div>
|
|
<span>${folder.name}</span>
|
|
</div>
|
|
<div>Carpeta</div>
|
|
<div>--</div>
|
|
<div>${formattedDate}</div>
|
|
`;
|
|
|
|
// Evento de clic normal para navegar
|
|
folderListElement.addEventListener('click', () => {
|
|
currentPath = folder.id;
|
|
updateBreadcrumb(folder.name);
|
|
loadFiles();
|
|
});
|
|
|
|
// Evento de clic derecho para el menú contextual
|
|
folderListElement.addEventListener('contextmenu', (e) => {
|
|
e.preventDefault();
|
|
|
|
// Guardamos la carpeta seleccionada
|
|
contextMenuTargetFolder = {
|
|
id: folder.id,
|
|
name: folder.name,
|
|
parent_id: folder.parent_id || ""
|
|
};
|
|
|
|
// Posicionamos y mostramos el menú contextual
|
|
let folderContextMenu = document.getElementById('folder-context-menu');
|
|
folderContextMenu.style.left = `${e.pageX}px`;
|
|
folderContextMenu.style.top = `${e.pageY}px`;
|
|
folderContextMenu.style.display = 'block';
|
|
});
|
|
|
|
// Eventos para arrastrar y soltar archivos en carpetas (vista lista)
|
|
folderListElement.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
folderListElement.classList.add('drop-target');
|
|
});
|
|
|
|
folderListElement.addEventListener('dragleave', () => {
|
|
folderListElement.classList.remove('drop-target');
|
|
});
|
|
|
|
folderListElement.addEventListener('drop', async (e) => {
|
|
e.preventDefault();
|
|
folderListElement.classList.remove('drop-target');
|
|
|
|
const id = e.dataTransfer.getData('text/plain');
|
|
const isFolder = e.dataTransfer.getData('application/oxicloud-folder') === 'true';
|
|
|
|
if (id) {
|
|
if (isFolder) {
|
|
// Evitar mover una carpeta a sí misma o a su hijo
|
|
if (id === folder.id) {
|
|
alert("No puedes mover una carpeta a sí misma");
|
|
return;
|
|
}
|
|
|
|
await moveFolder(id, folder.id);
|
|
} else {
|
|
await moveFile(id, folder.id);
|
|
}
|
|
}
|
|
});
|
|
|
|
filesListView.appendChild(folderListElement);
|
|
}
|
|
|
|
/**
|
|
* Añade un archivo a las vistas
|
|
*/
|
|
function addFileToView(file) {
|
|
// Determine file icon and type label based on MIME type
|
|
let iconClass = 'fas fa-file';
|
|
let typeLabel = 'Documento';
|
|
|
|
if (file.mime_type) {
|
|
if (file.mime_type.startsWith('image/')) {
|
|
iconClass = 'fas fa-file-image';
|
|
typeLabel = 'Imagen';
|
|
} else if (file.mime_type.startsWith('text/')) {
|
|
iconClass = 'fas fa-file-alt';
|
|
typeLabel = 'Texto';
|
|
} else if (file.mime_type.startsWith('video/')) {
|
|
iconClass = 'fas fa-file-video';
|
|
typeLabel = 'Video';
|
|
} else if (file.mime_type.startsWith('audio/')) {
|
|
iconClass = 'fas fa-file-audio';
|
|
typeLabel = 'Audio';
|
|
} else if (file.mime_type === 'application/pdf') {
|
|
iconClass = 'fas fa-file-pdf';
|
|
typeLabel = 'PDF';
|
|
}
|
|
}
|
|
|
|
// Format file size
|
|
const fileSize = formatFileSize(file.size);
|
|
|
|
// Convert timestamp to human readable format
|
|
const modifiedDate = new Date(file.modified_at * 1000);
|
|
const formattedDate = modifiedDate.toLocaleDateString() + ' ' +
|
|
modifiedDate.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
|
|
|
// Create file element for grid view
|
|
const fileGridElement = document.createElement('div');
|
|
fileGridElement.className = 'file-card';
|
|
fileGridElement.innerHTML = `
|
|
<div class="file-icon">
|
|
<i class="${iconClass}"></i>
|
|
</div>
|
|
<div class="file-name">${file.name}</div>
|
|
<div class="file-info">Modificado ${formattedDate}</div>
|
|
`;
|
|
|
|
// Atributos de datos para el archivo
|
|
fileGridElement.dataset.fileId = file.id;
|
|
fileGridElement.dataset.fileName = file.name;
|
|
fileGridElement.dataset.folderId = file.folder_id || "";
|
|
|
|
// Hacer el archivo arrastrable para mover
|
|
fileGridElement.setAttribute('draggable', 'true');
|
|
|
|
// Eventos de arrastrar
|
|
fileGridElement.addEventListener('dragstart', (e) => {
|
|
e.dataTransfer.setData('text/plain', file.id);
|
|
fileGridElement.classList.add('dragging');
|
|
});
|
|
|
|
fileGridElement.addEventListener('dragend', () => {
|
|
fileGridElement.classList.remove('dragging');
|
|
// Eliminar la clase drop-target de todas las carpetas
|
|
document.querySelectorAll('.drop-target').forEach(el => {
|
|
el.classList.remove('drop-target');
|
|
});
|
|
});
|
|
|
|
// Add download functionality
|
|
fileGridElement.addEventListener('click', () => {
|
|
window.location.href = `/api/files/${file.id}`;
|
|
});
|
|
|
|
// Menú contextual para el archivo
|
|
fileGridElement.addEventListener('contextmenu', (e) => {
|
|
e.preventDefault();
|
|
|
|
// Guardamos el archivo seleccionado
|
|
contextMenuTargetFile = {
|
|
id: file.id,
|
|
name: file.name,
|
|
folder_id: file.folder_id || ""
|
|
};
|
|
|
|
// Posicionamos y mostramos el menú contextual
|
|
let fileContextMenu = document.getElementById('file-context-menu');
|
|
fileContextMenu.style.left = `${e.pageX}px`;
|
|
fileContextMenu.style.top = `${e.pageY}px`;
|
|
fileContextMenu.style.display = 'block';
|
|
});
|
|
|
|
filesGrid.appendChild(fileGridElement);
|
|
|
|
// Create file element for list view
|
|
const fileListElement = document.createElement('div');
|
|
fileListElement.className = 'file-item';
|
|
fileListElement.dataset.fileId = file.id;
|
|
fileListElement.dataset.fileName = file.name;
|
|
fileListElement.dataset.folderId = file.folder_id || "";
|
|
|
|
fileListElement.innerHTML = `
|
|
<div class="name-cell">
|
|
<div class="file-icon">
|
|
<i class="${iconClass}"></i>
|
|
</div>
|
|
<span>${file.name}</span>
|
|
</div>
|
|
<div>${typeLabel}</div>
|
|
<div>${fileSize}</div>
|
|
<div>${formattedDate}</div>
|
|
`;
|
|
|
|
// Hacer el archivo arrastrable para mover (vista lista)
|
|
fileListElement.setAttribute('draggable', 'true');
|
|
|
|
// Eventos de arrastrar (vista lista)
|
|
fileListElement.addEventListener('dragstart', (e) => {
|
|
e.dataTransfer.setData('text/plain', file.id);
|
|
fileListElement.classList.add('dragging');
|
|
});
|
|
|
|
fileListElement.addEventListener('dragend', () => {
|
|
fileListElement.classList.remove('dragging');
|
|
document.querySelectorAll('.drop-target').forEach(el => {
|
|
el.classList.remove('drop-target');
|
|
});
|
|
});
|
|
|
|
// Add download functionality
|
|
fileListElement.addEventListener('click', () => {
|
|
window.location.href = `/api/files/${file.id}`;
|
|
});
|
|
|
|
// Menú contextual para el archivo (vista lista)
|
|
fileListElement.addEventListener('contextmenu', (e) => {
|
|
e.preventDefault();
|
|
|
|
// Una vez creado el menú, asignamos el archivo objetivo
|
|
contextMenuTargetFile = {
|
|
id: file.id,
|
|
name: file.name,
|
|
folder_id: file.folder_id || ""
|
|
};
|
|
|
|
let fileContextMenu = document.getElementById('file-context-menu');
|
|
fileContextMenu.style.left = `${e.pageX}px`;
|
|
fileContextMenu.style.top = `${e.pageY}px`;
|
|
fileContextMenu.style.display = 'block';
|
|
});
|
|
|
|
filesListView.appendChild(fileListElement);
|
|
}
|
|
|
|
/**
|
|
* Sube archivos al servidor
|
|
*/
|
|
async function uploadFiles(files) {
|
|
const progressBar = document.querySelector('.progress-fill');
|
|
const uploadProgressDiv = document.querySelector('.upload-progress');
|
|
uploadProgressDiv.style.display = 'block';
|
|
progressBar.style.width = '0%';
|
|
|
|
let uploadedCount = 0;
|
|
const totalFiles = files.length;
|
|
|
|
for (let i = 0; i < totalFiles; i++) {
|
|
const file = files[i];
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
if (currentPath) {
|
|
formData.append('folder_id', currentPath);
|
|
}
|
|
|
|
try {
|
|
// Usar fetch en lugar de XMLHttpRequest para una mejor gestión de errores
|
|
const response = await fetch('/api/files/upload', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
// Actualizar progreso
|
|
uploadedCount++;
|
|
const percentComplete = (uploadedCount / totalFiles) * 100;
|
|
progressBar.style.width = percentComplete + '%';
|
|
|
|
if (response.ok) {
|
|
console.log(`Successfully uploaded ${file.name}`);
|
|
|
|
if (i === totalFiles - 1) {
|
|
// Last file uploaded
|
|
loadFiles();
|
|
setTimeout(() => {
|
|
dropzone.style.display = 'none';
|
|
uploadProgressDiv.style.display = 'none';
|
|
}, 1000);
|
|
|
|
// Mostrar notificación de éxito
|
|
showNotification('Archivo subido', `${file.name} completado`);
|
|
}
|
|
} else {
|
|
const errorData = await response.text();
|
|
console.error('Upload error:', errorData);
|
|
alert(`Error al subir el archivo: ${file.name}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Network error during upload:', error);
|
|
alert(`Error de red al subir el archivo: ${file.name}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Muestra una notificación
|
|
*/
|
|
function showNotification(title, message) {
|
|
// Comprobar si ya existe la notificación, de lo contrario crearla
|
|
let notification = document.querySelector('.notification');
|
|
if (!notification) {
|
|
notification = document.createElement('div');
|
|
notification.className = 'notification';
|
|
notification.innerHTML = `
|
|
<div class="notification-title">${title}</div>
|
|
<div class="notification-message">${message}</div>
|
|
`;
|
|
document.body.appendChild(notification);
|
|
} else {
|
|
notification.querySelector('.notification-title').textContent = title;
|
|
notification.querySelector('.notification-message').textContent = message;
|
|
}
|
|
|
|
// Mostrar la notificación
|
|
notification.style.display = 'block';
|
|
|
|
// Ocultar la notificación después de 5 segundos
|
|
setTimeout(() => {
|
|
notification.style.display = 'none';
|
|
}, 5000);
|
|
}
|
|
|
|
// Exponer la función selectFolder globalmente para usarla en el breadcrumb
|
|
window.selectFolder = (id, name) => {
|
|
currentPath = id;
|
|
updateBreadcrumb(name);
|
|
loadFiles();
|
|
};
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |