Files
cbc2027/templates/index.html
2025-12-16 22:32:27 +00:00

1586 lines
51 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard de Gestión de Audio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<style>
:root {
/* Dark Mode Colors */
--bg-primary: #0f1115;
--bg-secondary: #1a1d24;
--bg-tertiary: #222734;
--bg-hover: #2a3042;
--text-primary: #e6e9ef;
--text-secondary: #a4a9b6;
--text-tertiary: #7c8293;
--border-color: #2a3042;
--accent-color: #646cff;
--accent-hover: #7c83ff;
--success-color: #3ddc84;
--warning-color: #f9a826;
--error-color: #ff6b6b;
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5);
--shadow-xl: 0 12px 48px rgba(0, 0, 0, 0.6);
--glass-bg: rgba(255, 255, 255, 0.03);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
overflow-x: hidden;
}
/* Animated background particles */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 20% 30%, rgba(100, 108, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, rgba(61, 220, 132, 0.08) 0%, transparent 50%),
radial-gradient(circle at 50% 50%, rgba(249, 168, 38, 0.05) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
animation: backgroundShift 20s ease-in-out infinite;
}
@keyframes backgroundShift {
0%, 100% { opacity: 1; }
50% { opacity: 0.8; }
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 60px 20px;
position: relative;
z-index: 1;
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.header {
background: var(--bg-secondary);
border-radius: 24px;
padding: 50px;
margin-bottom: 40px;
box-shadow: var(--shadow-lg);
border: 1px solid var(--border-color);
position: relative;
overflow: hidden;
animation: slideInDown 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--accent-color), var(--success-color), var(--warning-color));
background-size: 200% 100%;
animation: gradientMove 3s linear infinite;
}
@keyframes gradientMove {
0% { background-position: 0% 0%; }
100% { background-position: 200% 0%; }
}
.header:hover {
box-shadow: var(--shadow-xl);
border-color: var(--accent-color);
transform: translateY(-4px);
}
.header h1 {
color: var(--text-primary);
font-size: 3.2rem;
font-weight: 800;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 20px;
letter-spacing: -0.02em;
animation: fadeInUp 0.8s ease 0.2s backwards;
}
.header .icon {
font-size: 4rem;
animation: float 3s ease-in-out infinite, fadeIn 1s ease;
filter: drop-shadow(0 0 20px rgba(100, 108, 255, 0.5));
}
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
33% { transform: translateY(-8px) rotate(1deg); }
66% { transform: translateY(-4px) rotate(-1deg); }
}
.header p {
color: var(--text-secondary);
font-size: 1.3rem;
font-weight: 400;
margin-top: 10px;
animation: fadeInUp 0.8s ease 0.4s backwards;
}
@keyframes slideInDown {
from {
opacity: 0;
transform: translateY(-60px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 25px;
margin-bottom: 40px;
}
.stat-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 20px;
padding: 35px;
text-align: center;
box-shadow: var(--shadow-md);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
animation: fadeInUp 0.6s ease backwards;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--accent-color), var(--accent-hover));
transform: translateX(-100%);
transition: transform 0.4s ease;
}
.stat-card:nth-child(2)::before {
background: linear-gradient(90deg, var(--success-color), #5ff3a0);
}
.stat-card:nth-child(3)::before {
background: linear-gradient(90deg, var(--warning-color), #ffb84d);
}
.stat-card:hover::before {
transform: translateX(0);
}
.stat-card:nth-child(1) { animation-delay: 0.1s; }
.stat-card:nth-child(2) { animation-delay: 0.2s; }
.stat-card:nth-child(3) { animation-delay: 0.3s; }
.stat-card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: var(--shadow-xl);
border-color: var(--accent-color);
}
.stat-number {
font-size: 3.5rem;
font-weight: 900;
margin-bottom: 10px;
background: linear-gradient(135deg, var(--text-primary) 0%, var(--accent-color) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: countUp 1s ease 0.5s backwards;
text-shadow: 0 0 30px rgba(100, 108, 255, 0.3);
}
@keyframes countUp {
from {
transform: scale(0.5);
opacity: 0;
filter: blur(10px);
}
to {
transform: scale(1);
opacity: 1;
filter: blur(0);
}
}
.stat-label {
color: var(--text-secondary);
font-size: 1rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.15em;
}
.controls {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 20px;
padding: 35px;
margin-bottom: 40px;
box-shadow: var(--shadow-md);
display: flex;
gap: 15px;
flex-wrap: wrap;
animation: fadeIn 0.8s ease 0.5s backwards;
transition: all 0.3s ease;
}
.controls:hover {
border-color: var(--accent-color);
box-shadow: var(--shadow-lg);
}
.controls button {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 14px 30px;
border-radius: 14px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
font-family: 'Inter', sans-serif;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.controls button::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
transition: left 0.5s;
}
.controls button:hover::before {
left: 100%;
}
.controls button:hover {
background: var(--accent-color);
border-color: var(--accent-color);
color: white;
transform: translateY(-3px);
box-shadow: 0 8px 24px rgba(100, 108, 255, 0.4);
}
.controls button:active {
transform: translateY(-1px);
}
.controls button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.refresh-btn {
background: linear-gradient(135deg, var(--success-color) 0%, #5ff3a0 100%);
border: none;
color: #000;
font-weight: 700;
}
.refresh-btn:hover {
background: linear-gradient(135deg, #5ff3a0 0%, var(--success-color) 100%);
box-shadow: 0 8px 24px rgba(61, 220, 132, 0.5);
color: #000;
}
#reprocessAllBtn {
background: linear-gradient(135deg, var(--accent-color) 0%, var(--accent-hover) 100%);
border: none;
color: white;
font-weight: 700;
}
#reprocessAllBtn:hover {
box-shadow: 0 8px 24px rgba(100, 108, 255, 0.5);
color: white;
}
.files-container {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 24px;
padding: 45px;
box-shadow: var(--shadow-lg);
position: relative;
animation: fadeIn 1s ease 0.6s backwards;
transition: all 0.3s ease;
}
.files-container:hover {
border-color: var(--accent-color);
box-shadow: var(--shadow-xl);
}
.files-header {
margin-bottom: 30px;
padding-bottom: 25px;
border-bottom: 1px solid var(--border-color);
}
.header-title h2 {
color: var(--text-primary);
font-size: 2.2rem;
font-weight: 800;
margin: 0;
letter-spacing: -0.02em;
animation: fadeInLeft 0.6s ease 0.7s backwards;
}
.header-controls {
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
margin-top: 25px;
flex-wrap: wrap;
}
.source-filters {
display: flex;
gap: 15px;
align-items: center;
animation: fadeInLeft 0.6s ease 0.8s backwards;
}
.filter-checkbox {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
font-size: 0.95rem;
color: var(--text-secondary);
font-weight: 600;
transition: all 0.3s ease;
padding: 10px 20px;
border-radius: 12px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
position: relative;
overflow: hidden;
}
.filter-checkbox::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--accent-color);
opacity: 0;
transition: opacity 0.3s ease;
z-index: 0;
}
.filter-checkbox:hover::before {
opacity: 0.1;
}
.filter-checkbox:hover {
color: var(--text-primary);
border-color: var(--accent-color);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.filter-checkbox span {
position: relative;
z-index: 1;
}
.filter-checkbox input[type="checkbox"] {
display: none;
}
.filter-checkbox .checkmark {
width: 22px;
height: 22px;
border: 2px solid var(--border-color);
border-radius: 8px;
margin-right: 10px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
background: var(--bg-secondary);
position: relative;
z-index: 1;
}
.filter-checkbox:hover .checkmark {
border-color: var(--accent-color);
transform: scale(1.1);
box-shadow: 0 0 15px rgba(100, 108, 255, 0.3);
}
.filter-checkbox input[type="checkbox"]:checked + .checkmark {
background: var(--accent-color);
border-color: var(--accent-color);
animation: checkMarkPop 0.3s ease;
}
@keyframes checkMarkPop {
0% { transform: scale(0); }
50% { transform: scale(1.3); }
100% { transform: scale(1); }
}
.filter-checkbox input[type="checkbox"]:checked + .checkmark::after {
content: '✓';
color: white;
font-size: 14px;
font-weight: bold;
animation: checkMarkBounce 0.3s ease;
}
@keyframes checkMarkBounce {
0% { transform: scale(0) rotate(-180deg); }
50% { transform: scale(1.2) rotate(-90deg); }
100% { transform: scale(1) rotate(0deg); }
}
.sort-checkbox {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
font-size: 0.95rem;
color: var(--text-secondary);
font-weight: 600;
transition: all 0.3s ease;
padding: 10px 20px;
border-radius: 12px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
position: relative;
overflow: hidden;
}
.sort-checkbox::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--success-color);
opacity: 0;
transition: opacity 0.3s ease;
z-index: 0;
}
.sort-checkbox:hover::before {
opacity: 0.1;
}
.sort-checkbox:hover {
color: var(--text-primary);
border-color: var(--success-color);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.sort-checkbox span {
position: relative;
z-index: 1;
}
.sort-checkbox input[type="radio"] {
display: none;
}
.sort-checkbox .checkmark {
width: 22px;
height: 22px;
border: 2px solid var(--border-color);
border-radius: 50%;
margin-right: 10px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
background: var(--bg-secondary);
position: relative;
z-index: 1;
}
.sort-checkbox:hover .checkmark {
border-color: var(--success-color);
transform: scale(1.1);
box-shadow: 0 0 15px rgba(61, 220, 132, 0.3);
}
.sort-checkbox input[type="radio"]:checked + .checkmark {
background: var(--success-color);
border-color: var(--success-color);
animation: checkMarkPop 0.3s ease;
}
.sort-checkbox input[type="radio"]:checked + .checkmark::after {
content: '';
width: 10px;
height: 10px;
background: white;
border-radius: 50%;
animation: checkMarkBounce 0.3s ease;
}
@keyframes checkMarkBounce {
0% { transform: scale(0); opacity: 0; }
50% { transform: scale(1.5); opacity: 1; }
100% { transform: scale(1); opacity: 1; }
}
.search-box {
background: var(--bg-tertiary);
border: 2px solid var(--border-color);
border-radius: 14px;
padding: 14px 22px;
font-size: 1rem;
width: 350px;
color: var(--text-primary);
font-family: 'Inter', sans-serif;
transition: all 0.3s ease;
animation: fadeInRight 0.6s ease 0.9s backwards;
}
.search-box::placeholder {
color: var(--text-tertiary);
}
.search-box:focus {
outline: none;
border-color: var(--accent-color);
background: var(--bg-secondary);
box-shadow: 0 0 0 4px rgba(100, 108, 255, 0.15), var(--shadow-md);
transform: translateY(-2px);
}
.files-grid {
display: grid;
gap: 18px;
}
.file-card {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 25px;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
animation: fadeInLeft 0.6s ease backwards;
cursor: pointer;
}
.file-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background: var(--accent-color);
transform: scaleY(0);
transition: transform 0.3s ease;
transform-origin: bottom;
}
.file-card:hover::before {
transform: scaleY(1);
}
.file-card:nth-child(odd) { animation-delay: 0.1s; }
.file-card:nth-child(even) { animation-delay: 0.2s; }
@keyframes fadeInLeft {
from {
opacity: 0;
transform: translateX(-50px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.file-card:hover {
border-color: var(--accent-color);
box-shadow: var(--shadow-xl);
transform: translateY(-5px) scale(1.01);
background: var(--bg-hover);
}
.file-info {
flex: 1;
}
.file-name {
font-size: 1.2rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 8px;
letter-spacing: -0.01em;
}
.file-meta {
display: flex;
gap: 18px;
flex-wrap: wrap;
font-size: 0.9rem;
color: var(--text-tertiary);
}
.file-formats {
margin-top: 12px;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.file-actions {
display: flex;
gap: 12px;
align-items: center;
}
.status-badge {
padding: 8px 16px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
animation: pulse 2s ease infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.05); opacity: 0.9; }
}
.status-processed {
background: rgba(61, 220, 132, 0.15);
color: var(--success-color);
border: 1px solid rgba(61, 220, 132, 0.3);
}
.status-pending {
background: rgba(249, 168, 38, 0.15);
color: var(--warning-color);
border: 1px solid rgba(249, 168, 38, 0.3);
}
.action-btn {
background: linear-gradient(135deg, var(--accent-color) 0%, var(--accent-hover) 100%);
color: white;
border: none;
padding: 10px 20px;
border-radius: 12px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
font-family: 'Inter', sans-serif;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.action-btn::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.action-btn:active::after {
width: 300px;
height: 300px;
}
.action-btn:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(100, 108, 255, 0.5);
}
.action-btn:disabled {
background: var(--bg-hover);
border: 1px solid var(--border-color);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.action-btn.reset {
background: linear-gradient(135deg, var(--warning-color) 0%, #ffb84d 100%);
color: #000;
}
.action-btn.reset:hover {
box-shadow: 0 8px 25px rgba(249, 168, 38, 0.5);
}
.source-badge {
padding: 5px 12px;
border-radius: 14px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.source-webdav {
background: rgba(100, 108, 255, 0.15);
color: var(--accent-color);
border: 1px solid rgba(100, 108, 255, 0.3);
}
.source-local {
background: rgba(61, 220, 132, 0.15);
color: var(--success-color);
border: 1px solid rgba(61, 220, 132, 0.3);
}
.loading {
display: none;
text-align: center;
padding: 60px;
color: var(--text-secondary);
animation: fadeIn 0.3s ease;
}
.loading.active {
display: block;
}
.spinner {
display: inline-block;
width: 60px;
height: 60px;
border: 4px solid var(--border-color);
border-top: 4px solid var(--accent-color);
border-radius: 50%;
animation: spin 1s linear infinite, glow 2s ease-in-out infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes glow {
0%, 100% { box-shadow: 0 0 20px rgba(100, 108, 255, 0.3); }
50% { box-shadow: 0 0 40px rgba(100, 108, 255, 0.6); }
}
.loading p {
margin-top: 20px;
font-size: 1.2rem;
font-weight: 600;
}
.error, .success {
padding: 20px 24px;
border-radius: 14px;
margin: 20px 0;
font-weight: 600;
border: 1px solid;
animation: slideInRight 0.5s ease;
backdrop-filter: blur(10px);
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(100px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.error {
background: rgba(255, 107, 107, 0.1);
border-color: var(--error-color);
color: var(--error-color);
}
.success {
background: rgba(61, 220, 132, 0.1);
border-color: var(--success-color);
color: var(--success-color);
}
.format-link {
display: inline-block;
padding: 6px 12px;
margin: 3px 6px 3px 0;
background: var(--bg-secondary);
color: var(--text-secondary);
text-decoration: none;
border-radius: 10px;
font-size: 0.8rem;
font-weight: 600;
transition: all 0.3s ease;
border: 1px solid var(--border-color);
position: relative;
overflow: hidden;
}
.format-link::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: var(--accent-color);
transition: left 0.3s ease;
z-index: 0;
}
.format-link:hover::before {
left: 0;
}
.format-link:hover {
color: white;
border-color: var(--accent-color);
text-decoration: none;
transform: translateY(-3px);
box-shadow: var(--shadow-md);
}
.format-link span {
position: relative;
z-index: 1;
}
@keyframes fadeInRight {
from {
opacity: 0;
transform: translateX(50px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@media (max-width: 768px) {
.container {
padding: 30px 15px;
}
.header {
padding: 30px;
}
.header h1 {
font-size: 2.2rem;
}
.stats {
grid-template-columns: 1fr;
}
.stat-card {
padding: 25px;
}
.stat-number {
font-size: 2.5rem;
}
.header-controls {
flex-direction: column;
align-items: stretch;
gap: 15px;
}
.source-filters {
justify-content: center;
flex-wrap: wrap;
}
.search-box {
width: 100%;
}
.file-card {
flex-direction: column;
align-items: stretch;
gap: 15px;
}
.file-actions {
justify-content: center;
}
.controls {
flex-direction: column;
}
.controls button {
width: 100%;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<h1>
<span class="icon">🎵</span>
Dashboard de Gestión de Audio
</h1>
<p>Monitorea y reprocesa archivos de audio con un solo click</p>
</div>
<!-- Estadísticas -->
<div class="stats">
<div class="stat-card">
<div class="stat-number total" id="totalFiles">-</div>
<div class="stat-label">Total Archivos</div>
</div>
<div class="stat-card">
<div class="stat-number processed" id="processedFiles">-</div>
<div class="stat-label">Procesados</div>
</div>
<div class="stat-card">
<div class="stat-number pending" id="pendingFiles">-</div>
<div class="stat-label">Pendientes</div>
</div>
</div>
<!-- Controles -->
<div class="controls">
<button onclick="refreshFiles()" class="refresh-btn">
🔄 Refrescar Lista
</button>
<button onclick="reprocessAllPending()" id="reprocessAllBtn">
⚡ Reprocesar Todos los Pendientes
</button>
</div>
<!-- Mensajes -->
<div id="messages"></div>
<!-- Cargando -->
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Cargando archivos...</p>
</div>
<!-- Lista de Archivos -->
<div class="files-container">
<div class="files-header">
<div class="header-title">
<h2>📁 Archivos de Audio</h2>
</div>
<div class="header-controls">
<div class="source-filters">
<label class="filter-checkbox">
<input type="checkbox" id="filterLocal" checked onchange="filterFiles()">
<span class="checkmark"></span>
<span>Local</span>
</label>
<label class="filter-checkbox">
<input type="checkbox" id="filterWebDAV" onchange="filterFiles()">
<span class="checkmark"></span>
<span>WebDAV</span>
</label>
<div style="width: 2px; height: 30px; background: var(--border-color); margin: 0 10px;"></div>
<label class="sort-checkbox">
<input type="radio" id="sortDate" name="sortBy" value="date" checked onchange="filterFiles()">
<span class="checkmark"></span>
<span>📅 Fecha</span>
</label>
<label class="sort-checkbox">
<input type="radio" id="sortName" name="sortBy" value="name" onchange="filterFiles()">
<span class="checkmark"></span>
<span>🔤 Nombre</span>
</label>
</div>
<input type="text" class="search-box" id="searchBox" placeholder="🔍 Buscar archivos..." onkeyup="filterFiles()">
</div>
</div>
<div class="files-grid" id="filesGrid">
<!-- Los archivos se cargarán aquí dinámicamente -->
</div>
</div>
</div>
<script>
let files = [];
let loading = false;
// Inicialización
document.addEventListener('DOMContentLoaded', function() {
loadFiles();
});
// Cargar archivos desde la API
async function loadFiles() {
setLoading(true);
try {
const response = await fetch('/api/files');
const data = await response.json();
if (data.success) {
files = data.files;
updateStats(data);
renderFiles();
hideMessages();
} else {
showMessage('Error cargando archivos: ' + data.message, 'error');
}
} catch (error) {
showMessage('Error de conexión con el servidor', 'error');
console.error('Error:', error);
} finally {
setLoading(false);
}
}
// Obtener estadísticas actuales de los archivos locales
function getCurrentStats() {
const total = files.length;
const processed = files.filter(f => f.processed).length;
const pending = total - processed;
return { total, processed, pending };
}
// Actualizar estadísticas
function updateStats(data) {
document.getElementById('totalFiles').textContent = data.total;
document.getElementById('processedFiles').textContent = data.processed;
document.getElementById('pendingFiles').textContent = data.pending;
}
// Renderizar archivos
function renderFiles() {
const grid = document.getElementById('filesGrid');
const filteredFiles = filterFilesArray();
if (filteredFiles.length === 0) {
grid.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--text-tertiary); font-size: 1.1rem; animation: fadeIn 0.5s ease;">No se encontraron archivos</div>';
return;
}
grid.innerHTML = filteredFiles.map(file => {
const formats = file.available_formats || {};
const availableFormatsList = Object.keys(formats).filter(ext => formats[ext]);
const formatLinks = availableFormatsList.length > 0
? availableFormatsList.map(ext => {
const baseName = file.filename.substring(0, file.filename.lastIndexOf('.'));
const possibleNames = [
`${baseName}.${ext}`,
`${baseName}_unificado.${ext}`,
`${baseName.replace(/ /g, '_')}.${ext}`,
`${baseName.replace(/ /g, '_')}_unificado.${ext}`
];
const icon = ext === 'txt' ? '📝' : ext === 'docx' ? '📄' : ext === 'md' ? '📋' : '📑';
return `<a href="/downloads/find-file?filename=${encodeURIComponent(baseName)}&ext=${ext}" target="_blank" class="format-link"><span>${icon} ${ext}</span></a>`;
}).join(' ')
: '<span style="color: var(--text-tertiary); font-size: 0.9rem;">no procesado aún</span>';
return `
<div class="file-card" data-filename="${file.filename.toLowerCase()}">
<div class="file-info">
<div class="file-name">${file.filename}</div>
<div class="file-meta">
<span class="source-badge source-${file.source}">${file.source}</span>
<span>📦 ${file.size}</span>
<span>📅 ${file.last_modified}</span>
</div>
<div class="file-formats">
${formatLinks}
</div>
</div>
<div class="file-actions">
<span class="status-badge ${file.processed ? 'status-processed' : 'status-pending'}">
${file.processed ? 'Procesado' : 'Pendiente'}
</span>
${!file.processed ? `
<button class="action-btn" onclick="reprocessFile('${file.path}', '${file.source}')" id="btn-${file.filename.replace(/[^a-zA-Z0-9]/g, '')}">
🚀 Procesar
</button>
` : `
<button class="action-btn reset" onclick="markUnprocessed('${file.path}')" id="btn-${file.filename.replace(/[^a-zA-Z0-9]/g, '')}">
🔄 Resetear
</button>
`}
</div>
</div>
`;
}).join('');
}
// Filtrar archivos
function filterFiles() {
renderFiles();
}
function filterFilesArray() {
const searchTerm = document.getElementById('searchBox').value.toLowerCase();
const filterLocal = document.getElementById('filterLocal').checked;
const filterWebDAV = document.getElementById('filterWebDAV').checked;
const sortBy = document.querySelector('input[name="sortBy"]:checked')?.value || 'date';
let filteredFiles = files.filter(file => {
const matchesSearch = file.filename.toLowerCase().includes(searchTerm);
const matchesSource =
(filterLocal && file.source === 'local') ||
(filterWebDAV && file.source === 'webdav');
return matchesSearch && matchesSource;
});
// Ordenar archivos
if (sortBy === 'date') {
// Para archivos locales, convertir last_modified a fecha
// Para archivos webdav, usar last_modified como string
filteredFiles.sort((a, b) => {
// Si ambos tienen last_modified
if (a.last_modified !== 'Unknown' && b.last_modified !== 'Unknown') {
// Si uno es local y otro webdav, comparar como fechas
if (a.source === 'local' || b.source === 'local') {
try {
// Intentar convertir a timestamp
const dateA = new Date(a.last_modified).getTime();
const dateB = new Date(b.last_modified).getTime();
return dateB - dateA; // Más reciente primero
} catch (e) {
// Si falla, ordenar alfabéticamente por fecha como string
return b.last_modified.localeCompare(a.last_modified);
}
}
}
// Ordenar alfabéticamente como fallback
return a.filename.localeCompare(b.filename);
});
} else if (sortBy === 'name') {
filteredFiles.sort((a, b) => a.filename.localeCompare(b.filename));
}
return filteredFiles;
}
// Actualizar estado de un archivo específico
function updateFileStatus(filePath, isProcessed) {
const file = files.find(f => f.path === filePath);
if (file) {
file.processed = isProcessed;
renderFiles();
}
}
// Reprocesar un archivo
async function reprocessFile(filePath, source) {
const file = files.find(f => f.path === filePath);
if (!file) return;
const formats = file.available_formats || {};
const existingFormats = Object.keys(formats).filter(ext => formats[ext]);
if (existingFormats.length > 0) {
const confirmed = await showConfirmDialog(
`⚠️ Archivos existentes detectados`,
`El archivo "${file.filename}" ya tiene los siguientes formatos: ${existingFormats.join(', ')}.\n\n` +
`¿Estás seguro de que quieres reprocesarlo? Esto sobrescribirá los archivos existentes.`
);
if (!confirmed) {
return;
}
}
await performReprocess(filePath, source);
}
// Función que ejecuta el reprocesamiento real
async function performReprocess(filePath, source) {
const btnId = 'btn-' + filePath.replace(/[^a-zA-Z0-9]/g, '');
const btn = document.getElementById(btnId);
if (!btn) return;
const originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '⏳ Procesando...';
try {
const response = await fetch('/api/reprocess', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
path: filePath,
source: source
})
});
const data = await response.json();
if (data.success) {
showMessage('✅ ' + data.message, 'success');
setTimeout(async () => {
await loadFiles();
showMessage('🔄 Actualizando estado de archivos...', 'success');
}, 2000);
} else {
showMessage('❌ ' + data.message, 'error');
btn.disabled = false;
btn.innerHTML = originalText;
}
} catch (error) {
showMessage('❌ Error de conexión', 'error');
btn.disabled = false;
btn.innerHTML = originalText;
console.error('Error:', error);
}
}
// Marcar como no procesado
async function markUnprocessed(filePath) {
if (!confirm('¿Estás seguro de que quieres marcar este archivo como no procesado?')) {
return;
}
try {
const response = await fetch('/api/mark-unprocessed', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
path: filePath
})
});
const data = await response.json();
if (data.success) {
showMessage('✅ ' + data.message, 'success');
updateFileStatus(filePath, false);
updateStats(getCurrentStats());
} else {
showMessage('❌ ' + data.message, 'error');
}
} catch (error) {
showMessage('❌ Error de conexión', 'error');
console.error('Error:', error);
}
}
// Refrescar archivos
async function refreshFiles() {
showMessage('🔄 Actualizando lista de archivos...', 'success');
await loadFiles();
}
// Reprocesar todos los pendientes
async function reprocessAllPending() {
const pendingFiles = files.filter(f => !f.processed);
if (pendingFiles.length === 0) {
showMessage(' No hay archivos pendientes para procesar', 'success');
return;
}
if (!confirm(`¿Estás seguro de que quieres procesar ${pendingFiles.length} archivos pendientes?`)) {
return;
}
const btn = document.getElementById('reprocessAllBtn');
const originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '⏳ Procesando...';
showMessage(`🚀 Iniciando procesamiento de ${pendingFiles.length} archivos...`, 'success');
for (let i = 0; i < pendingFiles.length; i++) {
const file = pendingFiles[i];
try {
await reprocessFile(file.path, file.source);
if (i < pendingFiles.length - 1) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
} catch (error) {
console.error(`Error procesando ${file.filename}:`, error);
}
}
btn.disabled = false;
btn.innerHTML = originalText;
showMessage('✅ Procesamiento en lote completado', 'success');
}
// Control de estado de carga
function setLoading(isLoading) {
loading = isLoading;
const loadingEl = document.getElementById('loading');
const gridEl = document.getElementById('filesGrid');
if (isLoading) {
loadingEl.classList.add('active');
gridEl.style.display = 'none';
} else {
loadingEl.classList.remove('active');
gridEl.style.display = 'block';
}
}
// Mostrar mensajes
function showMessage(message, type) {
const messagesEl = document.getElementById('messages');
const messageEl = document.createElement('div');
messageEl.className = type;
messageEl.textContent = message;
messagesEl.innerHTML = '';
messagesEl.appendChild(messageEl);
setTimeout(() => {
if (messagesEl.contains(messageEl)) {
messagesEl.removeChild(messageEl);
}
}, 5000);
}
function hideMessages() {
document.getElementById('messages').innerHTML = '';
}
// Función para mostrar diálogo de confirmación
async function showConfirmDialog(title, message) {
return new Promise((resolve) => {
const modalOverlay = document.createElement('div');
modalOverlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
backdrop-filter: blur(5px);
animation: fadeIn 0.3s ease;
`;
const modalContent = document.createElement('div');
modalContent.style.cssText = `
background: var(--bg-secondary);
border: 2px solid var(--border-color);
border-radius: 20px;
padding: 40px;
max-width: 500px;
width: 90%;
box-shadow: var(--shadow-xl);
text-align: center;
animation: scaleIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
`;
modalContent.innerHTML = `
<h2 style="color: var(--text-primary); margin-bottom: 20px; font-size: 1.8rem; font-weight: 700;">${title}</h2>
<p style="color: var(--text-secondary); margin-bottom: 30px; line-height: 1.6; white-space: pre-line; font-size: 1.05rem;">${message}</p>
<div style="display: flex; gap: 15px; justify-content: center;">
<button id="confirm-cancel" style="
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 12px 28px;
border-radius: 12px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
font-family: 'Inter', sans-serif;
transition: all 0.3s ease;
">Cancelar</button>
<button id="confirm-ok" style="
background: linear-gradient(135deg, var(--error-color) 0%, #ff5252 100%);
color: white;
border: none;
padding: 12px 28px;
border-radius: 12px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
font-family: 'Inter', sans-serif;
transition: all 0.3s ease;
">Sí, reprocesar</button>
</div>
`;
modalOverlay.appendChild(modalContent);
document.body.appendChild(modalOverlay);
const cancelBtn = document.getElementById('confirm-cancel');
const okBtn = document.getElementById('confirm-ok');
const cleanup = () => {
modalOverlay.style.animation = 'fadeOut 0.3s ease';
setTimeout(() => {
if (document.body.contains(modalOverlay)) {
document.body.removeChild(modalOverlay);
}
}, 300);
};
cancelBtn.addEventListener('click', () => {
cleanup();
resolve(false);
});
okBtn.addEventListener('click', () => {
cleanup();
resolve(true);
});
cancelBtn.addEventListener('mouseenter', () => {
cancelBtn.style.transform = 'translateY(-3px)';
cancelBtn.style.boxShadow = 'var(--shadow-md)';
cancelBtn.style.borderColor = 'var(--accent-color)';
});
cancelBtn.addEventListener('mouseleave', () => {
cancelBtn.style.transform = 'translateY(0)';
cancelBtn.style.boxShadow = 'none';
cancelBtn.style.borderColor = 'var(--border-color)';
});
okBtn.addEventListener('mouseenter', () => {
okBtn.style.transform = 'translateY(-3px)';
okBtn.style.boxShadow = '0 8px 25px rgba(255, 107, 107, 0.5)';
});
okBtn.addEventListener('mouseleave', () => {
okBtn.style.transform = 'translateY(0)';
okBtn.style.boxShadow = 'none';
});
modalOverlay.addEventListener('click', (e) => {
if (e.target === modalOverlay) {
cleanup();
resolve(false);
}
});
});
}
// Añadir estilos de animación dinámicos
const style = document.createElement('style');
style.textContent = `
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.8) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
`;
document.head.appendChild(style);
</script>
</body>
</html>