big refactoring

This commit is contained in:
Dionisio
2026-02-03 17:59:04 +01:00
parent 52840e57df
commit 8f2b0a354c
46 changed files with 8505 additions and 1418 deletions
+98
View File
@@ -196,9 +196,107 @@
font-weight: 500;
}
/* Language selector panel styles */
.language-selector-panel {
text-align: center;
}
.language-subtitle {
color: #64748b;
font-size: 16px;
margin-bottom: 30px;
}
.language-options {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 25px;
}
.language-option {
display: flex;
align-items: center;
padding: 15px 20px;
border: 2px solid #e2e8f0;
border-radius: 10px;
cursor: pointer;
transition: all 0.2s ease;
background-color: #f9fafb;
}
.language-option:hover {
border-color: #ff5e3a;
background-color: #fff;
}
.language-option.selected {
border-color: #ff5e3a;
background-color: #fff5f3;
}
.language-option input[type="radio"] {
display: none;
}
.language-radio {
width: 22px;
height: 22px;
border: 2px solid #cbd5e1;
border-radius: 50%;
margin-right: 15px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.language-option.selected .language-radio {
border-color: #ff5e3a;
}
.language-radio::after {
content: '';
width: 12px;
height: 12px;
background-color: #ff5e3a;
border-radius: 50%;
opacity: 0;
transition: opacity 0.2s ease;
}
.language-option.selected .language-radio::after {
opacity: 1;
}
.language-flag {
font-size: 28px;
margin-right: 15px;
}
.language-name {
font-size: 16px;
font-weight: 500;
color: #1e293b;
}
.language-native {
font-size: 14px;
color: #64748b;
margin-left: auto;
}
@media (max-width: 480px) {
.auth-panel {
width: 90%;
padding: 20px;
}
.language-option {
padding: 12px 15px;
}
.language-flag {
font-size: 24px;
}
}
+516 -47
View File
@@ -13,6 +13,59 @@ body {
overflow: hidden;
}
/* Custom scrollbar styling */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(0, 0, 0, 0.3);
}
/* Firefox scrollbar */
* {
scrollbar-width: thin;
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
}
/* Global select styling */
select {
padding: 8px 32px 8px 12px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background-color: white;
font-size: 14px;
color: #2d3748;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23718096' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
font-family: inherit;
}
select:hover {
border-color: #cbd5e0;
}
select:focus {
outline: none;
border-color: #ff5e3a;
box-shadow: 0 0 0 3px rgba(255, 94, 58, 0.1);
}
/* Sidebar */
.sidebar {
width: 250px;
@@ -131,17 +184,20 @@ body {
/* Top bar */
.top-bar {
height: 70px;
min-height: 70px;
max-height: 70px;
background-color: white;
border-bottom: 1px solid #e6e6e6;
display: flex;
align-items: center;
padding: 0 30px;
justify-content: space-between;
flex-shrink: 0;
}
.search-container {
flex-grow: 1;
max-width: 500px;
max-width: 600px;
position: relative;
margin-right: 20px;
display: flex;
@@ -150,40 +206,78 @@ body {
.search-container input {
width: 100%;
padding: 10px 15px 10px 40px;
border-radius: 50px;
border: none;
background-color: #f0f3f7;
padding: 12px 50px 12px 44px;
border-radius: 12px;
border: 2px solid #e2e8f0;
background-color: #f8fafc;
font-size: 14px;
height: 40px;
height: 46px;
color: #1a202c;
transition: all 0.2s ease;
}
.search-container input:hover {
border-color: #cbd5e0;
background-color: #fff;
}
.search-container input:focus {
outline: none;
border-color: #ff5e3a;
background-color: #fff;
box-shadow: 0 0 0 4px rgba(255, 94, 58, 0.1);
}
.search-container input::placeholder {
color: #a0aec0;
}
.search-icon {
position: absolute;
left: 15px;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: #8895a7;
color: #a0aec0;
font-size: 16px;
pointer-events: none;
transition: color 0.2s ease;
}
.search-container input:focus + .search-icon,
.search-container:focus-within .search-icon {
color: #ff5e3a;
}
.search-button {
background-color: #ff5e3a;
position: absolute;
right: 6px;
top: 50%;
transform: translateY(-50%);
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
color: white;
border: none;
border-radius: 50%;
border-radius: 10px;
width: 36px;
height: 36px;
margin-left: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(255, 94, 58, 0.3);
}
.search-button:hover {
background-color: #e64a29;
transform: translateY(-50%) scale(1.05);
box-shadow: 0 4px 12px rgba(255, 94, 58, 0.4);
}
.search-button:active {
transform: translateY(-50%) scale(0.98);
}
.search-button i {
font-size: 14px;
}
/* Styles for search results */
@@ -217,36 +311,144 @@ body {
.user-controls {
display: flex;
align-items: center;
gap: 12px;
}
.logout-btn {
margin-left: 15px;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background-color: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 10px;
color: #64748b;
cursor: pointer;
font-size: 18px;
transition: color 0.2s;
font-size: 16px;
transition: all 0.2s ease;
}
.logout-btn:hover {
background-color: #fef2f2;
border-color: #fecaca;
color: #ef4444;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.15);
}
.logout-btn:active {
transform: translateY(0);
}
/* Language Selector - Custom Dropdown */
.language-selector {
position: relative;
margin-right: 15px;
}
.language-selector-toggle {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
background-color: #f0f3f7;
border: 1px solid #e2e8f0;
border-radius: 50px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
color: #4a5568;
transition: all 0.2s ease;
user-select: none;
}
.language-selector-toggle:hover {
background-color: #e2e8f0;
border-color: #cbd5e0;
}
.language-selector-toggle i {
font-size: 14px;
color: #718096;
}
.language-selector-toggle .lang-code {
font-weight: 600;
color: #2d3748;
}
.language-selector-toggle .dropdown-arrow {
font-size: 10px;
color: #718096;
transition: transform 0.2s ease;
margin-left: 2px;
}
.language-selector.open .dropdown-arrow {
transform: rotate(180deg);
}
.language-selector-dropdown {
position: absolute;
top: calc(100% + 8px);
right: 0;
min-width: 160px;
background-color: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
border: 1px solid #e2e8f0;
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.2s ease;
z-index: 1000;
overflow: hidden;
}
.language-selector.open .language-selector-dropdown {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.language-option {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
cursor: pointer;
font-size: 14px;
color: #4a5568;
transition: background-color 0.15s ease;
}
.language-option:hover {
background-color: #f7fafc;
}
.language-option.active {
background-color: #fff5f3;
color: #ff5e3a;
}
.language-selector {
margin-right: 15px;
padding: 5px 12px;
background-color: #f0f3f7;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
.language-option .lang-flag {
font-size: 18px;
line-height: 1;
}
.language-selector::after {
content: "▼";
font-size: 8px;
margin-left: 5px;
color: #718096;
.language-option .lang-name {
flex: 1;
}
.language-option .lang-check {
color: #ff5e3a;
font-size: 12px;
opacity: 0;
}
.language-option.active .lang-check {
opacity: 1;
}
.user-avatar {
@@ -265,7 +467,8 @@ body {
.content-area {
flex-grow: 1;
padding: 20px;
overflow-y: auto;
overflow-y: scroll;
scrollbar-gutter: stable;
}
.page-title {
@@ -283,68 +486,144 @@ body {
.action-buttons {
display: flex;
gap: 10px;
gap: 12px;
}
.btn {
padding: 10px 20px;
border-radius: 50px;
padding: 12px 24px;
border-radius: 12px;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 500;
gap: 8px;
transition: all 0.2s ease;
}
.btn i {
font-size: 15px;
}
.btn-primary {
background-color: #ff5e3a;
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
color: white;
box-shadow: 0 4px 15px rgba(255, 94, 58, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(255, 94, 58, 0.4);
}
.btn-primary:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(255, 94, 58, 0.3);
}
.btn-secondary {
background-color: #f0f3f7;
color: #333;
background-color: #f8fafc;
color: #4a5568;
border: 2px solid #e2e8f0;
}
.btn-secondary:hover {
background-color: #edf2f7;
border-color: #cbd5e0;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.btn-secondary:active {
transform: translateY(0);
background-color: #e2e8f0;
}
.btn-danger {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
color: white;
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.3);
}
.btn-danger:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
}
.btn-danger:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(239, 68, 68, 0.3);
}
/* View Toggle Buttons */
.view-toggle {
display: flex;
border-radius: 8px;
overflow: hidden;
gap: 6px;
padding: 4px;
background-color: #f0f3f7;
border-radius: 12px;
border: 1px solid #e2e8f0;
}
.toggle-btn {
background-color: #f0f3f7;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 36px;
background-color: transparent;
border: none;
padding: 8px 15px;
border-radius: 8px;
cursor: pointer;
color: #64748b;
font-size: 16px;
transition: all 0.2s ease;
}
.toggle-btn:hover {
background-color: #e2e8f0;
color: #4a5568;
}
.toggle-btn.active {
background-color: #e6e6e6;
background-color: white;
color: #ff5e3a;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.toggle-btn i {
pointer-events: none;
}
/* Files grid */
.files-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(200px, 240px));
gap: 20px;
justify-content: start;
}
.file-card {
background-color: white;
border-radius: 8px;
border: 1px solid #e2e8f0;
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;
transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
width: 100%;
min-height: 180px;
}
.file-card:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
box-shadow: 0 5px 15px rgba(0,0,0,0.08);
border-color: #cbd5e0;
}
.file-card.dragging {
@@ -888,6 +1167,176 @@ body {
color: #718096;
}
/* Modern Modal Overlay */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: none;
justify-content: center;
align-items: center;
z-index: 3000;
opacity: 0;
transition: opacity 0.2s ease;
}
.modal-overlay.active {
display: flex;
opacity: 1;
}
.modal-container {
background-color: white;
border-radius: 16px;
width: 420px;
max-width: 90%;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
transform: scale(0.9) translateY(-20px);
transition: transform 0.2s ease;
overflow: hidden;
}
.modal-overlay.active .modal-container {
transform: scale(1) translateY(0);
}
.modal-header {
display: flex;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #e2e8f0;
position: relative;
}
.modal-icon {
width: 44px;
height: 44px;
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 14px;
flex-shrink: 0;
}
.modal-icon i {
color: white;
font-size: 20px;
}
.modal-header h3 {
font-size: 18px;
font-weight: 600;
color: #1a202c;
margin: 0;
flex: 1;
}
.modal-close-btn {
position: absolute;
top: 16px;
right: 16px;
width: 32px;
height: 32px;
border: none;
background: #f0f3f7;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #64748b;
transition: all 0.15s ease;
}
.modal-close-btn:hover {
background: #e2e8f0;
color: #1a202c;
}
.modal-body {
padding: 24px;
}
.modal-body label {
display: block;
font-size: 14px;
font-weight: 500;
color: #4a5568;
margin-bottom: 8px;
}
.modal-input {
width: 100%;
padding: 12px 16px;
font-size: 15px;
border: 2px solid #e2e8f0;
border-radius: 10px;
background: #f8fafc;
color: #1a202c;
transition: all 0.15s ease;
outline: none;
}
.modal-input:focus {
border-color: #ff5e3a;
background: white;
box-shadow: 0 0 0 3px rgba(255, 94, 58, 0.1);
}
.modal-input::placeholder {
color: #a0aec0;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 24px;
background: #f8fafc;
border-top: 1px solid #e2e8f0;
}
.modal-footer .btn {
padding: 10px 20px;
font-size: 14px;
font-weight: 500;
border-radius: 10px;
cursor: pointer;
transition: all 0.15s ease;
}
.modal-footer .btn-secondary {
background: white;
border: 1px solid #e2e8f0;
color: #4a5568;
}
.modal-footer .btn-secondary:hover {
background: #f0f3f7;
border-color: #cbd5e0;
}
.modal-footer .btn-primary {
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
border: none;
color: white;
box-shadow: 0 2px 8px rgba(255, 94, 58, 0.3);
}
.modal-footer .btn-primary:hover {
box-shadow: 0 4px 12px rgba(255, 94, 58, 0.4);
transform: translateY(-1px);
}
.modal-footer .btn-primary:active {
transform: translateY(0);
}
/* Dialogs (Rename, Move, Share) */
.rename-dialog, .share-dialog {
position: fixed;
@@ -1104,14 +1553,34 @@ header {
.filter-group label {
font-size: 14px;
color: #4a5568;
font-weight: 500;
}
.filter-group select {
padding: 8px 12px;
padding: 8px 32px 8px 12px;
border: 1px solid #e2e8f0;
border-radius: 6px;
border-radius: 8px;
background-color: white;
font-size: 14px;
color: #2d3748;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23718096' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.filter-group select:hover {
border-color: #cbd5e0;
}
.filter-group select:focus {
outline: none;
border-color: #ff5e3a;
box-shadow: 0 0 0 3px rgba(255, 94, 58, 0.1);
}
.search-box {
+37 -9
View File
@@ -17,6 +17,7 @@
<!-- Scripts -->
<script src="/js/i18n.js"></script>
<script src="/js/languageSelector.js"></script>
<script src="/js/modal.js"></script>
<script src="/js/ui.js"></script>
<script src="/js/contextMenus.js"></script>
<script src="/js/fileOperations.js"></script>
@@ -79,11 +80,11 @@
</div>
<div class="storage-container">
<div class="storage-title">Almacenamiento</div>
<div class="storage-title" data-i18n="storage.title">Storage</div>
<div class="storage-bar">
<div class="storage-fill"></div>
</div>
<div class="storage-info">Calculando...</div>
<div class="storage-info" data-i18n="storage.calculating">Calculating...</div>
</div>
</div>
@@ -93,14 +94,16 @@
<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...">
<button id="search-button" class="search-button"><i class="fas fa-search"></i></button>
<input type="text" id="search-input" data-i18n-placeholder="actions.search" placeholder="Buscar archivos, carpetas...">
<button id="search-button" class="search-button" data-i18n-title="actions.search_btn" title="Search">
<i class="fas fa-search"></i>
</button>
</div>
<div class="user-controls">
<div id="language-selector" class="language-selector">ES</div>
<div class="user-avatar">MR</div>
<div id="logout-btn" class="logout-btn" title="Cerrar sesión">
<div id="language-selector"></div>
<div class="user-avatar" id="user-avatar">AD</div>
<div id="logout-btn" class="logout-btn" data-i18n-title="actions.logout" title="Log out">
<i class="fas fa-sign-out-alt"></i>
</div>
</div>
@@ -112,10 +115,12 @@
<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>
<i class="fas fa-cloud-upload-alt"></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>
<i class="fas fa-folder-plus"></i>
<span data-i18n="actions.new_folder">Nueva carpeta</span>
</button>
</div>
@@ -164,5 +169,28 @@
</div>
</div>
</div>
<!-- Input Modal (for New Folder, Rename, etc.) -->
<div id="input-modal" class="modal-overlay">
<div class="modal-container">
<div class="modal-header">
<div class="modal-icon">
<i id="modal-icon" class="fas fa-folder-plus"></i>
</div>
<h3 id="modal-title" data-i18n="dialogs.new_folder_title">Nueva carpeta</h3>
<button class="modal-close-btn" id="modal-close-btn">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<label id="modal-label" for="modal-input" data-i18n="dialogs.folder_name">Nombre de la carpeta</label>
<input type="text" id="modal-input" class="modal-input" placeholder="" autocomplete="off">
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="modal-cancel-btn" data-i18n="actions.cancel">Cancelar</button>
<button class="btn btn-primary" id="modal-confirm-btn" data-i18n="actions.confirm">Crear</button>
</div>
</div>
</div>
</body>
</html>
+100 -9
View File
@@ -181,8 +181,8 @@ function setupEventListeners() {
});
// New folder button
elements.newFolderBtn.addEventListener('click', () => {
const folderName = prompt(window.i18n ? window.i18n.t('dialogs.new_name') : 'Nombre de la carpeta:');
elements.newFolderBtn.addEventListener('click', async () => {
const folderName = await window.Modal.promptNewFolder();
if (folderName) {
fileOps.createFolder(folderName);
}
@@ -256,7 +256,7 @@ function setupEventListeners() {
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>
<i class="fas fa-trash-alt"></i>
<span>${window.i18n ? window.i18n.t('trash.empty_trash') : 'Vaciar papelera'}</span>
</button>
</div>
@@ -330,8 +330,8 @@ function setupEventListeners() {
}
});
document.getElementById('new-folder-btn').addEventListener('click', () => {
const folderName = prompt(window.i18n ? window.i18n.t('dialogs.new_name') : 'Nombre de la carpeta:');
document.getElementById('new-folder-btn').addEventListener('click', async () => {
const folderName = await window.Modal.promptNewFolder();
if (folderName) {
fileOps.createFolder(folderName);
}
@@ -899,8 +899,8 @@ function switchToFilesView() {
}
});
document.getElementById('new-folder-btn').addEventListener('click', () => {
const folderName = prompt(window.i18n ? window.i18n.t('dialogs.new_name') : 'Nombre de la carpeta:');
document.getElementById('new-folder-btn').addEventListener('click', async () => {
const folderName = await window.Modal.promptNewFolder();
if (folderName) {
fileOps.createFolder(folderName);
}
@@ -1136,6 +1136,59 @@ window.switchToSharedView = switchToSharedView;
window.switchToFavoritesView = switchToFavoritesView;
window.switchToRecentFilesView = switchToRecentFilesView;
/**
* Fetch updated user data from the server (including storage usage)
* This calls the /api/auth/me endpoint which also triggers storage recalculation
*/
async function refreshUserData() {
const TOKEN_KEY = 'oxicloud_token';
const USER_DATA_KEY = 'oxicloud_user';
const token = localStorage.getItem(TOKEN_KEY);
console.log('refreshUserData called, token:', token ? token.substring(0, 20) + '...' : 'null');
if (!token || token === 'mock_token_emergency_bypass' || token === 'emergency_token') {
console.log('No valid token, skipping user data refresh');
return null;
}
try {
console.log('Fetching /api/auth/me...');
const response = await fetch('/api/auth/me', {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
console.log('/api/auth/me response status:', response.status);
if (!response.ok) {
console.warn('Failed to fetch user data:', response.status);
return null;
}
const userData = await response.json();
console.log('Refreshed user data from server:', userData);
console.log('Storage from server: used=', userData.storage_used_bytes, 'quota=', userData.storage_quota_bytes);
// Update local storage with fresh data
localStorage.setItem(USER_DATA_KEY, JSON.stringify(userData));
// Update storage display with actual values
updateStorageUsageDisplay(userData);
return userData;
} catch (error) {
console.error('Error refreshing user data:', error);
return null;
}
}
// Expose refreshUserData globally
window.refreshUserData = refreshUserData;
/**
* Check if user is authenticated and load user's home folder
*/
@@ -1192,6 +1245,22 @@ function checkAuthentication() {
if (userAvatar) {
userAvatar.textContent = userInitials;
}
// Show cached storage first, then try to refresh from server
updateStorageUsageDisplay(userData);
// Try to get updated storage from server (if we have a real token)
const token = localStorage.getItem(TOKEN_KEY);
if (token && token !== 'mock_token_emergency_bypass' && token !== 'emergency_token') {
console.log('Bypass mode: Attempting to refresh storage from server...');
refreshUserData().then(freshData => {
if (freshData) {
console.log('Bypass mode: Storage updated from server');
}
}).catch(err => {
console.warn('Bypass mode: Could not refresh user data:', err);
});
}
}
// Reset all counters to prevent loops
@@ -1238,9 +1307,19 @@ function checkAuthentication() {
userAvatar.textContent = userInitials;
}
// Update storage usage information
// Update storage usage information with cached data first (for fast display)
updateStorageUsageDisplay(userData);
// Then refresh user data from server in the background to get updated storage
// This triggers the backend to recalculate storage and returns fresh data
refreshUserData().then(freshData => {
if (freshData) {
console.log('Storage usage updated from server');
}
}).catch(err => {
console.warn('Could not refresh user data:', err);
});
// Find and load the user's home folder
findUserHomeFolder(userData.username);
} else {
@@ -1506,7 +1585,19 @@ function updateStorageUsageDisplay(userData) {
}
if (storageInfo) {
storageInfo.textContent = `${usagePercentage}% usado (${usedFormatted} / ${quotaFormatted})`;
// Remove data-i18n attribute to prevent i18n from overwriting our value
storageInfo.removeAttribute('data-i18n');
// Use i18n if available
if (window.i18n && window.i18n.t) {
storageInfo.textContent = window.i18n.t('storage.used', {
percentage: usagePercentage,
used: usedFormatted,
total: quotaFormatted
});
} else {
storageInfo.textContent = `${usagePercentage}% used (${usedFormatted} / ${quotaFormatted})`;
}
}
console.log(`Updated storage display: ${usagePercentage}% (${usedFormatted} / ${quotaFormatted})`);
+220 -11
View File
@@ -15,9 +15,189 @@ const TOKEN_KEY = 'oxicloud_token';
const REFRESH_TOKEN_KEY = 'oxicloud_refresh_token';
const TOKEN_EXPIRY_KEY = 'oxicloud_token_expiry';
const USER_DATA_KEY = 'oxicloud_user';
const LOCALE_KEY = 'oxicloud-locale';
const FIRST_RUN_KEY = 'oxicloud_first_run_completed';
// Language selector texts (used before i18n is loaded)
const LANGUAGE_TEXTS = {
en: {
title: 'Welcome to OxiCloud',
subtitle: 'Please select your language',
continue: 'Continue'
},
es: {
title: 'Bienvenido a OxiCloud',
subtitle: 'Por favor, selecciona tu idioma',
continue: 'Continuar'
},
zh: {
title: '欢迎使用 OxiCloud',
subtitle: '请选择您的语言',
continue: '继续'
}
};
// Check if this is a first run (no locale saved)
function isFirstRun() {
return !localStorage.getItem(LOCALE_KEY);
}
// Check system status from the server
async function checkSystemStatus() {
try {
const response = await fetch('/api/auth/status');
if (!response.ok) {
console.warn('Could not check system status, assuming initialized');
return { initialized: true, admin_count: 1, registration_allowed: true };
}
return await response.json();
} catch (error) {
console.error('Error checking system status:', error);
return { initialized: true, admin_count: 1, registration_allowed: true };
}
}
// Initialize language selector panel
function initLanguageSelector() {
const languagePanel = document.getElementById('language-panel');
const languageOptions = document.querySelectorAll('.language-option');
const continueBtn = document.getElementById('language-continue');
let selectedLanguage = null;
if (!languagePanel) return;
// Handle language option clicks
languageOptions.forEach(option => {
option.addEventListener('click', () => {
// Remove selected class from all options
languageOptions.forEach(opt => opt.classList.remove('selected'));
// Add selected class to clicked option
option.classList.add('selected');
// Check the radio button
option.querySelector('input[type="radio"]').checked = true;
// Store selected language
selectedLanguage = option.getAttribute('data-lang');
// Enable continue button
continueBtn.disabled = false;
// Update UI texts based on selected language
updateLanguagePanelTexts(selectedLanguage);
});
});
// Handle continue button click
continueBtn.addEventListener('click', async () => {
if (!selectedLanguage) return;
// Save locale preference
localStorage.setItem(LOCALE_KEY, selectedLanguage);
localStorage.setItem(FIRST_RUN_KEY, 'true');
// Update i18n if available
if (window.i18n && window.i18n.setLocale) {
await window.i18n.setLocale(selectedLanguage);
}
// Hide language panel
languagePanel.style.display = 'none';
// Check system status to determine which panel to show
const systemStatus = await checkSystemStatus();
console.log('System status after language selection:', systemStatus);
if (!systemStatus.initialized) {
// No admin exists - show admin setup
console.log('No admin exists, showing admin setup panel');
document.getElementById('login-panel').style.display = 'none';
document.getElementById('register-panel').style.display = 'none';
document.getElementById('admin-setup-panel').style.display = 'block';
// Hide the "Already set up? Sign in" link
const backToLoginLink = document.getElementById('back-to-login');
if (backToLoginLink) {
backToLoginLink.parentElement.style.display = 'none';
}
} else {
// Admin exists - show login panel
document.getElementById('login-panel').style.display = 'block';
}
// Translate the page with new locale
if (window.i18n && window.i18n.translatePage) {
window.i18n.translatePage();
}
});
}
// Update language panel texts based on selected language
function updateLanguagePanelTexts(lang) {
const texts = LANGUAGE_TEXTS[lang] || LANGUAGE_TEXTS.en;
const titleEl = document.getElementById('language-title');
const subtitleEl = document.getElementById('language-subtitle');
const continueBtn = document.getElementById('language-continue');
if (titleEl) titleEl.textContent = texts.title;
if (subtitleEl) subtitleEl.textContent = texts.subtitle;
if (continueBtn) continueBtn.textContent = texts.continue;
}
// Show appropriate panel based on system status and first run
async function showInitialPanel() {
const languagePanel = document.getElementById('language-panel');
const loginPanel = document.getElementById('login-panel');
const adminSetupPanel = document.getElementById('admin-setup-panel');
const registerPanel = document.getElementById('register-panel');
if (!languagePanel || !loginPanel) return;
// ALWAYS check if this is user's first run (language selection) FIRST
// Language selection should happen before anything else
if (isFirstRun()) {
// First run - show language selector first
// After language is selected, the continue button handler will check system status
console.log('First run - showing language selector');
languagePanel.style.display = 'block';
loginPanel.style.display = 'none';
registerPanel.style.display = 'none';
adminSetupPanel.style.display = 'none';
return;
}
// Language already selected - now check system status
const systemStatus = await checkSystemStatus();
console.log('System status:', systemStatus);
if (!systemStatus.initialized) {
// No admin exists - this is a fresh install, show admin setup
console.log('Fresh install detected - showing admin setup');
languagePanel.style.display = 'none';
loginPanel.style.display = 'none';
registerPanel.style.display = 'none';
adminSetupPanel.style.display = 'block';
// Hide the "Already set up? Sign in" link since there's no admin yet
const backToLoginLink = document.getElementById('back-to-login');
if (backToLoginLink) {
backToLoginLink.parentElement.style.display = 'none';
}
return;
}
// System is initialized - show login panel
languagePanel.style.display = 'none';
loginPanel.style.display = 'block';
registerPanel.style.display = 'none';
adminSetupPanel.style.display = 'none';
// Hide the admin setup link if admin already exists
const showAdminSetupLink = document.getElementById('show-admin-setup');
if (showAdminSetupLink && systemStatus.admin_count > 0) {
showAdminSetupLink.parentElement.style.display = 'none';
}
}
// DOM elements
let loginPanel, registerPanel, adminSetupPanel;
let loginPanel, registerPanel, adminSetupPanel, languagePanel;
let loginForm, registerForm, adminSetupForm;
let loginError, registerError, registerSuccess, adminSetupError;
@@ -29,6 +209,7 @@ function initLoginElements() {
return false;
}
languagePanel = document.getElementById('language-panel');
loginPanel = document.getElementById('login-panel');
registerPanel = document.getElementById('register-panel');
adminSetupPanel = document.getElementById('admin-setup-panel');
@@ -41,6 +222,9 @@ function initLoginElements() {
registerError = document.getElementById('register-error');
registerSuccess = document.getElementById('register-success');
adminSetupError = document.getElementById('admin-setup-error');
// Initialize language selector
initLanguageSelector();
// Panel toggles
document.getElementById('show-register').addEventListener('click', () => {
@@ -138,6 +322,14 @@ document.addEventListener('DOMContentLoaded', () => {
}
authInitialized = true;
// Show appropriate panel (language selector on first run, login otherwise, or admin setup if no admin)
// This is async so we call it and let it run
showInitialPanel().then(() => {
console.log('Initial panel shown based on system status');
}).catch(err => {
console.error('Error showing initial panel:', err);
});
// Siempre limpiar los contadores al cargar la página de login
// para asegurar que no quedamos atrapados en un bucle
console.log('Login page loaded, clearing all counters');
@@ -308,7 +500,8 @@ if (isLoginPage && registerForm) {
// Validate passwords match
if (password !== confirmPassword) {
registerError.textContent = 'Las contraseñas no coinciden';
const errorMsg = window.i18n ? window.i18n.t('auth.passwords_mismatch') : 'Las contraseñas no coinciden';
registerError.textContent = errorMsg;
registerError.style.display = 'block';
return;
}
@@ -317,7 +510,8 @@ if (isLoginPage && registerForm) {
const data = await register(username, email, password);
// Show success message
registerSuccess.textContent = '¡Cuenta creada con éxito! Puedes iniciar sesión ahora.';
const successMsg = window.i18n ? window.i18n.t('auth.account_success') : '¡Cuenta creada con éxito! Puedes iniciar sesión ahora.';
registerSuccess.textContent = successMsg;
registerSuccess.style.display = 'block';
// Clear form
@@ -329,7 +523,8 @@ if (isLoginPage && registerForm) {
registerPanel.style.display = 'none';
}, 2000);
} catch (error) {
registerError.textContent = error.message || 'Error al registrar cuenta';
const errorMsg = window.i18n ? window.i18n.t('auth.admin_create_error') : 'Error al registrar cuenta';
registerError.textContent = error.message || errorMsg;
registerError.style.display = 'block';
}
});
@@ -340,8 +535,10 @@ if (isLoginPage && adminSetupForm) {
adminSetupForm.addEventListener('submit', async (e) => {
e.preventDefault();
// Clear previous errors
// Clear previous errors/success messages
adminSetupError.style.display = 'none';
const adminSetupSuccess = document.getElementById('admin-setup-success');
if (adminSetupSuccess) adminSetupSuccess.style.display = 'none';
const email = document.getElementById('admin-email').value;
const password = document.getElementById('admin-password').value;
@@ -349,7 +546,8 @@ if (isLoginPage && adminSetupForm) {
// Validate passwords match
if (password !== confirmPassword) {
adminSetupError.textContent = 'Las contraseñas no coinciden';
const errorMsg = window.i18n ? window.i18n.t('auth.passwords_mismatch') : 'Las contraseñas no coinciden';
adminSetupError.textContent = errorMsg;
adminSetupError.style.display = 'block';
return;
}
@@ -358,13 +556,24 @@ if (isLoginPage && adminSetupForm) {
// Register admin account
const data = await register('admin', email, password, 'admin');
// Show success and switch to login
alert('¡Cuenta de administrador creada con éxito! Ahora puedes iniciar sesión.');
// Show success message in the GUI instead of alert
const successMsg = window.i18n ? window.i18n.t('auth.admin_success') : '¡Cuenta de administrador creada con éxito! Ahora puedes iniciar sesión.';
if (adminSetupSuccess) {
adminSetupSuccess.textContent = successMsg;
adminSetupSuccess.style.display = 'block';
}
// Wait 2 seconds then switch to login panel
setTimeout(() => {
loginPanel.style.display = 'block';
adminSetupPanel.style.display = 'none';
if (adminSetupSuccess) adminSetupSuccess.style.display = 'none';
}, 2000);
loginPanel.style.display = 'block';
adminSetupPanel.style.display = 'none';
} catch (error) {
adminSetupError.textContent = error.message || 'Error al crear cuenta de administrador';
const errorMsg = window.i18n ? window.i18n.t('auth.admin_create_error') : 'Error al crear cuenta de administrador';
adminSetupError.textContent = error.message || errorMsg;
adminSetupError.style.display = 'block';
}
});
+25 -53
View File
@@ -21,20 +21,26 @@ const fileOps = {
for (let i = 0; i < totalFiles; i++) {
const file = files[i];
const formData = new FormData();
// IMPORTANT: folder_id MUST be added BEFORE file for multipart processing
// The backend reads fields in order, and needs folder_id before processing the file
const targetFolderId = window.app.currentPath || window.app.userHomeFolderId;
if (targetFolderId) {
formData.append('folder_id', targetFolderId);
}
// Add the file AFTER folder_id
formData.append('file', file);
if (window.app.currentPath) {
formData.append('folder_id', window.app.currentPath);
}
try {
console.log(`Uploading file to current path: ${window.app.currentPath || 'root'}`);
console.log(`Uploading file to folder: ${targetFolderId || 'root'}`);
// Usamos la URL correcta para la subida de archivos
console.log('Formulario a enviar:', {
file: file.name,
size: file.size,
folder_id: window.app.currentPath || 'root'
folder_id: targetFolderId || 'root'
});
const response = await fetch('/api/files/upload', {
@@ -62,63 +68,29 @@ const fileOps = {
const responseData = await response.json();
console.log(`Successfully uploaded ${file.name}`, responseData);
// Agregar el archivo a la vista inmediatamente para mostrar retroalimentación instantánea
// Esto permite que el usuario vea el archivo aunque el refresco posterior falle
if (window.ui && window.ui.addFileToView) {
console.log('Añadiendo archivo subido directamente a la vista:', responseData);
window.ui.addFileToView(responseData);
window.ui.updateFileIcons();
}
// Show success notification immediately
window.ui.showNotification('Archivo subido', `${file.name} completado`);
if (i === totalFiles - 1) {
// Last file uploaded
console.log('Recargando lista de archivos después de subida');
// Last file uploaded - wait and reload once
console.log('Último archivo subido, esperando antes de recargar...');
// Wait for backend to persist
await new Promise(resolve => setTimeout(resolve, 800));
// Single reload with force refresh
try {
// Esperar 1500ms para asegurar que los mapeos de ID se guarden
// Tiempo aumentado significativamente para permitir al backend completar persistencia
await new Promise(resolve => setTimeout(resolve, 1500));
// Forzar recarga de archivos con parámetro de bypass de caché
const timestamp = new Date().getTime();
const filesUrl = `/api/files?t=${timestamp}&folder_id=${window.app.currentPath || ''}`;
console.log(`Recargando lista de archivos desde: ${filesUrl}`);
const filesResponse = await fetch(filesUrl, {
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache'
}
});
if (filesResponse.ok) {
const files = await filesResponse.json();
console.log(`Recarga de archivos completada, obtenidos ${files.length} archivos`);
}
// Esperar un momento más antes de la recarga final
await new Promise(resolve => setTimeout(resolve, 500));
// Hacer una única recarga final forzando refresco completo
await window.loadFiles({forceRefresh: true});
// Asegurarse de que no haya recargas adicionales o duplicados
} catch (reloadError) {
console.error("Error durante recarga de archivos:", reloadError);
// Intentar recargar lista de nuevo en caso de error
await window.loadFiles({forceRefresh: true});
// Segundo intento con retraso y forzando refresco
setTimeout(() => window.loadFiles({forceRefresh: true}), 500);
console.error("Error recargando archivos:", reloadError);
}
// Hide upload UI
setTimeout(() => {
document.getElementById('dropzone').style.display = 'none';
const dropzone = document.getElementById('dropzone');
if (dropzone) dropzone.style.display = 'none';
uploadProgressDiv.style.display = 'none';
}, 1000);
// Show success notification
window.ui.showNotification('Archivo subido', `${file.name} completado`);
}, 500);
}
} else {
const errorData = await response.text();
+141 -28
View File
@@ -1,16 +1,17 @@
/**
* Language Selector Component for OxiCloud
* Custom styled dropdown with flags
*/
// Language codes and names
// Language codes, names, and flag emojis
const languages = [
{ code: 'en', name: 'English' },
{ code: 'es', name: 'Español' },
{ code: 'zh', name: '中文' }
{ code: 'en', name: 'English', flag: '🇬🇧' },
{ code: 'es', name: 'Español', flag: '🇪🇸' },
{ code: 'zh', name: '中文', flag: '🇨🇳' }
];
/**
* Creates and initializes a language selector component
* Creates and initializes a custom language selector component
* @param {string} containerId - ID of the container element
*/
function createLanguageSelector(containerId = 'language-selector') {
@@ -23,45 +24,157 @@ function createLanguageSelector(containerId = 'language-selector') {
document.body.appendChild(container);
}
// Create dropdown
const select = document.createElement('select');
select.className = 'language-select';
select.setAttribute('aria-label', 'Select language');
// Ensure container has the right class
container.className = 'language-selector';
// Add options
// Get current language
const currentLocale = window.i18n ? window.i18n.getCurrentLocale() : 'en';
const currentLang = languages.find(l => l.code === currentLocale) || languages[0];
// Create toggle button
const toggle = document.createElement('div');
toggle.className = 'language-selector-toggle';
toggle.setAttribute('role', 'button');
toggle.setAttribute('aria-haspopup', 'listbox');
toggle.setAttribute('aria-expanded', 'false');
toggle.setAttribute('tabindex', '0');
toggle.innerHTML = `
<i class="fas fa-globe"></i>
<span class="lang-code">${currentLang.code.toUpperCase()}</span>
<i class="fas fa-chevron-down dropdown-arrow"></i>
`;
// Create dropdown menu
const dropdown = document.createElement('div');
dropdown.className = 'language-selector-dropdown';
dropdown.setAttribute('role', 'listbox');
// Add language options
languages.forEach(lang => {
const option = document.createElement('option');
option.value = lang.code;
option.textContent = lang.name;
select.appendChild(option);
const option = document.createElement('div');
option.className = `language-option${lang.code === currentLocale ? ' active' : ''}`;
option.setAttribute('role', 'option');
option.setAttribute('data-lang', lang.code);
option.setAttribute('aria-selected', lang.code === currentLocale);
option.innerHTML = `
<span class="lang-flag">${lang.flag}</span>
<span class="lang-name">${lang.name}</span>
<i class="fas fa-check lang-check"></i>
`;
option.addEventListener('click', async (e) => {
e.stopPropagation();
await selectLanguage(lang.code, container);
});
dropdown.appendChild(option);
});
// Set current language
const currentLocale = window.i18n ? window.i18n.getCurrentLocale() : 'en';
select.value = currentLocale;
// Clear and build container
container.innerHTML = '';
container.appendChild(toggle);
container.appendChild(dropdown);
// Add change event
select.addEventListener('change', async (e) => {
const locale = e.target.value;
if (window.i18n) {
await window.i18n.setLocale(locale);
// Toggle dropdown on click
toggle.addEventListener('click', (e) => {
e.stopPropagation();
toggleDropdown(container);
});
// Keyboard support
toggle.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleDropdown(container);
} else if (e.key === 'Escape') {
closeDropdown(container);
}
});
// Add to container
container.innerHTML = '';
container.appendChild(select);
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!container.contains(e.target)) {
closeDropdown(container);
}
});
// Add event listener for locale changes
// Listen for locale changes from i18n system
window.addEventListener('localeChanged', (e) => {
select.value = e.detail.locale;
updateSelectedLanguage(e.detail.locale, container);
});
return container;
}
/**
* Toggle dropdown open/closed
*/
function toggleDropdown(container) {
const isOpen = container.classList.contains('open');
if (isOpen) {
closeDropdown(container);
} else {
openDropdown(container);
}
}
/**
* Open dropdown
*/
function openDropdown(container) {
container.classList.add('open');
const toggle = container.querySelector('.language-selector-toggle');
if (toggle) {
toggle.setAttribute('aria-expanded', 'true');
}
}
/**
* Close dropdown
*/
function closeDropdown(container) {
container.classList.remove('open');
const toggle = container.querySelector('.language-selector-toggle');
if (toggle) {
toggle.setAttribute('aria-expanded', 'false');
}
}
/**
* Select a language
*/
async function selectLanguage(langCode, container) {
// Update i18n if available
if (window.i18n) {
await window.i18n.setLocale(langCode);
}
updateSelectedLanguage(langCode, container);
closeDropdown(container);
}
/**
* Update the UI to reflect selected language
*/
function updateSelectedLanguage(langCode, container) {
const lang = languages.find(l => l.code === langCode) || languages[0];
// Update toggle button text
const langCodeSpan = container.querySelector('.lang-code');
if (langCodeSpan) {
langCodeSpan.textContent = lang.code.toUpperCase();
}
// Update active state on options
const options = container.querySelectorAll('.language-option');
options.forEach(option => {
const isActive = option.getAttribute('data-lang') === langCode;
option.classList.toggle('active', isActive);
option.setAttribute('aria-selected', isActive);
});
}
// Create language selector when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
// Create language selector
createLanguageSelector();
});
+214
View File
@@ -0,0 +1,214 @@
/**
* Modal System for OxiCloud
* Provides modern, styled modals to replace browser prompts/alerts
*/
const Modal = {
// Modal element references
overlay: null,
container: null,
icon: null,
title: null,
label: null,
input: null,
cancelBtn: null,
confirmBtn: null,
closeBtn: null,
// Current callback
onConfirm: null,
onCancel: null,
/**
* Initialize modal system
*/
init() {
this.overlay = document.getElementById('input-modal');
if (!this.overlay) {
console.warn('Modal overlay not found');
return;
}
this.icon = document.getElementById('modal-icon');
this.title = document.getElementById('modal-title');
this.label = document.getElementById('modal-label');
this.input = document.getElementById('modal-input');
this.cancelBtn = document.getElementById('modal-cancel-btn');
this.confirmBtn = document.getElementById('modal-confirm-btn');
this.closeBtn = document.getElementById('modal-close-btn');
// Event listeners
this.cancelBtn.addEventListener('click', () => this.close(false));
this.closeBtn.addEventListener('click', () => this.close(false));
this.confirmBtn.addEventListener('click', () => this.confirm());
// Close on overlay click
this.overlay.addEventListener('click', (e) => {
if (e.target === this.overlay) {
this.close(false);
}
});
// Handle Enter and Escape keys
this.input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.confirm();
} else if (e.key === 'Escape') {
this.close(false);
}
});
},
/**
* Show input modal (replacement for prompt())
* @param {Object} options - Modal configuration
* @param {string} options.title - Modal title
* @param {string} options.label - Input label
* @param {string} options.placeholder - Input placeholder
* @param {string} options.value - Initial input value
* @param {string} options.icon - Font Awesome icon class (e.g., 'fa-folder-plus')
* @param {string} options.confirmText - Confirm button text
* @param {string} options.cancelText - Cancel button text
* @returns {Promise<string|null>} - Resolves with input value or null if cancelled
*/
prompt(options = {}) {
return new Promise((resolve) => {
const {
title = 'Input',
label = '',
placeholder = '',
value = '',
icon = 'fa-keyboard',
confirmText = null,
cancelText = null
} = options;
// Set modal content
this.icon.className = `fas ${icon}`;
this.title.textContent = title;
this.label.textContent = label;
this.input.placeholder = placeholder;
this.input.value = value;
// Set button text (use i18n if available)
if (confirmText) {
this.confirmBtn.textContent = confirmText;
} else if (window.i18n) {
this.confirmBtn.textContent = window.i18n.t('actions.confirm');
}
if (cancelText) {
this.cancelBtn.textContent = cancelText;
} else if (window.i18n) {
this.cancelBtn.textContent = window.i18n.t('actions.cancel');
}
// Set callbacks
this.onConfirm = () => {
const inputValue = this.input.value.trim();
resolve(inputValue || null);
};
this.onCancel = () => resolve(null);
// Show modal
this.open();
});
},
/**
* Show modal for creating new folder
* @returns {Promise<string|null>}
*/
promptNewFolder() {
const t = window.i18n ? window.i18n.t.bind(window.i18n) : (k) => k;
return this.prompt({
title: t('dialogs.new_folder_title') || 'Nueva carpeta',
label: t('dialogs.folder_name') || 'Nombre de la carpeta',
placeholder: t('dialogs.folder_placeholder') || 'Mi carpeta',
icon: 'fa-folder-plus',
confirmText: t('actions.create') || 'Crear'
});
},
/**
* Show modal for renaming
* @param {string} currentName - Current name of file/folder
* @param {boolean} isFolder - Whether it's a folder
* @returns {Promise<string|null>}
*/
promptRename(currentName, isFolder = false) {
const t = window.i18n ? window.i18n.t.bind(window.i18n) : (k) => k;
return this.prompt({
title: t('dialogs.rename_title') || 'Renombrar',
label: t('dialogs.new_name') || 'Nuevo nombre',
placeholder: '',
value: currentName,
icon: isFolder ? 'fa-folder' : 'fa-file',
confirmText: t('actions.rename') || 'Renombrar'
});
},
/**
* Open the modal
*/
open() {
if (!this.overlay) return;
// Show overlay
this.overlay.style.display = 'flex';
// Trigger animation
requestAnimationFrame(() => {
this.overlay.classList.add('active');
});
// Focus input after animation
setTimeout(() => {
this.input.focus();
this.input.select();
}, 100);
},
/**
* Close the modal
* @param {boolean} confirmed - Whether the action was confirmed
*/
close(confirmed = false) {
if (!this.overlay) return;
this.overlay.classList.remove('active');
setTimeout(() => {
this.overlay.style.display = 'none';
if (!confirmed && this.onCancel) {
this.onCancel();
}
// Clear callbacks
this.onConfirm = null;
this.onCancel = null;
}, 200);
},
/**
* Confirm the action
*/
confirm() {
if (this.onConfirm) {
this.onConfirm();
}
this.close(true);
}
};
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
Modal.init();
});
// Export for use in other modules
window.Modal = Modal;
+41 -2
View File
@@ -26,7 +26,10 @@
"copy": "Copy",
"notify": "Notify",
"send": "Send",
"clear_recent": "Clear recent"
"clear_recent": "Clear recent",
"logout": "Log out",
"create": "Create",
"search_btn": "Search"
},
"share": {
"dialogTitle": "Share Link",
@@ -168,6 +171,10 @@
"dialogs": {
"rename_folder": "Rename folder",
"new_name": "New name",
"new_folder_title": "New folder",
"folder_name": "Folder name",
"folder_placeholder": "My folder",
"rename_title": "Rename",
"move_file": "Move file",
"select_destination": "Select destination folder",
"root": "Root",
@@ -245,7 +252,16 @@
"admin_email": "Admin email",
"admin_password": "Admin password",
"create_admin": "Create administrator",
"back_to_login": "Already set up?"
"back_to_login": "Already set up?",
"admin_success": "Administrator account created successfully! You can now sign in.",
"account_success": "Account created successfully! You can now sign in.",
"passwords_mismatch": "Passwords do not match",
"admin_create_error": "Error creating administrator account"
},
"storage": {
"title": "Storage",
"calculating": "Calculating...",
"used": "{{percentage}}% used ({{used}} / {{total}})"
},
"viewer": {
"unsupported_file": "This file type cannot be previewed.",
@@ -253,5 +269,28 @@
"zoom_in": "Zoom in",
"zoom_out": "Zoom out",
"zoom_reset": "Reset zoom"
},
"language_selector": {
"title": "Welcome to OxiCloud",
"subtitle": "Please select your language",
"continue": "Continue",
"languages": {
"en": "English",
"es": "Español",
"zh": "中文"
}
},
"favorites": {
"empty_state": "No favorites yet",
"empty_hint": "Star files or folders to add them to your favorites",
"add": "Add to favorites",
"remove": "Remove from favorites"
},
"recent": {
"title": "Recent",
"clear": "Clear recent",
"accessed": "Accessed",
"empty_state": "No recent files",
"empty_hint": "Files you open will appear here"
}
}
+41 -2
View File
@@ -145,7 +145,10 @@
"copy": "Copiar",
"notify": "Notificar",
"send": "Enviar",
"clear_recent": "Limpiar recientes"
"clear_recent": "Limpiar recientes",
"logout": "Cerrar sesión",
"create": "Crear",
"search_btn": "Buscar"
},
"files": {
"name": "Nombre",
@@ -168,6 +171,10 @@
"dialogs": {
"rename_folder": "Renombrar carpeta",
"new_name": "Nuevo nombre",
"new_folder_title": "Nueva carpeta",
"folder_name": "Nombre de la carpeta",
"folder_placeholder": "Mi carpeta",
"rename_title": "Renombrar",
"move_file": "Mover archivo",
"select_destination": "Selecciona la carpeta destino",
"root": "Raíz",
@@ -245,7 +252,16 @@
"admin_email": "Email administrador",
"admin_password": "Contraseña administrador",
"create_admin": "Crear administrador",
"back_to_login": "¿Ya está configurado?"
"back_to_login": "¿Ya está configurado?",
"admin_success": "¡Cuenta de administrador creada con éxito! Ahora puedes iniciar sesión.",
"account_success": "¡Cuenta creada con éxito! Ahora puedes iniciar sesión.",
"passwords_mismatch": "Las contraseñas no coinciden",
"admin_create_error": "Error al crear cuenta de administrador"
},
"storage": {
"title": "Almacenamiento",
"calculating": "Calculando...",
"used": "{{percentage}}% usado ({{used}} / {{total}})"
},
"viewer": {
"unsupported_file": "Este tipo de archivo no se puede previsualizar.",
@@ -253,5 +269,28 @@
"zoom_in": "Acercar",
"zoom_out": "Alejar",
"zoom_reset": "Restablecer zoom"
},
"language_selector": {
"title": "Bienvenido a OxiCloud",
"subtitle": "Por favor, selecciona tu idioma",
"continue": "Continuar",
"languages": {
"en": "English",
"es": "Español",
"zh": "中文"
}
},
"favorites": {
"empty_state": "Aún no hay favoritos",
"empty_hint": "Marca archivos o carpetas con estrella para añadirlos a favoritos",
"add": "Añadir a favoritos",
"remove": "Quitar de favoritos"
},
"recent": {
"title": "Recientes",
"clear": "Limpiar recientes",
"accessed": "Accedido",
"empty_state": "No hay archivos recientes",
"empty_hint": "Los archivos que abras aparecerán aquí"
}
}
+41 -2
View File
@@ -26,7 +26,10 @@
"copy": "复制",
"notify": "通知",
"send": "发送",
"clear_recent": "清除最近"
"clear_recent": "清除最近",
"logout": "退出登录",
"create": "创建",
"search_btn": "搜索"
},
"share": {
"dialogTitle": "共享链接",
@@ -130,6 +133,10 @@
"dialogs": {
"rename_folder": "重命名文件夹",
"new_name": "新名称",
"new_folder_title": "新建文件夹",
"folder_name": "文件夹名称",
"folder_placeholder": "我的文件夹",
"rename_title": "重命名",
"move_file": "移动文件",
"select_destination": "选择目标文件夹",
"root": "根目录",
@@ -207,7 +214,16 @@
"admin_email": "管理员电子邮件",
"admin_password": "管理员密码",
"create_admin": "创建管理员",
"back_to_login": "已设置完成?"
"back_to_login": "已设置完成?",
"admin_success": "管理员账号创建成功!您现在可以登录。",
"account_success": "账号创建成功!您现在可以登录。",
"passwords_mismatch": "密码不匹配",
"admin_create_error": "创建管理员账号时出错"
},
"storage": {
"title": "存储空间",
"calculating": "计算中...",
"used": "{{percentage}}% 已使用 ({{used}} / {{total}})"
},
"viewer": {
"unsupported_file": "无法预览此文件类型。",
@@ -215,5 +231,28 @@
"zoom_in": "放大",
"zoom_out": "缩小",
"zoom_reset": "重置缩放"
},
"language_selector": {
"title": "欢迎使用 OxiCloud",
"subtitle": "请选择您的语言",
"continue": "继续",
"languages": {
"en": "English",
"es": "Español",
"zh": "中文"
}
},
"favorites": {
"empty_state": "还没有收藏",
"empty_hint": "为文件或文件夹添加星标以将其添加到收藏夹",
"add": "添加到收藏夹",
"remove": "从收藏夹移除"
},
"recent": {
"title": "最近",
"clear": "清除最近",
"accessed": "访问于",
"empty_state": "没有最近文件",
"empty_hint": "您打开的文件将显示在这里"
}
}
+42 -1
View File
@@ -17,7 +17,47 @@
</head>
<body>
<div class="auth-container">
<div class="auth-panel" id="login-panel">
<!-- Language Selector Panel - Shown first on fresh install -->
<div class="auth-panel language-selector-panel" id="language-panel" style="display: none;">
<div class="auth-logo">
<div class="auth-logo-icon">
<svg viewBox="0 0 500 500">
<path d="M345 310c32 0 58-26 58-58s-26-58-58-58c-6.2 0-12 0.9-17.5 2.7C318 166 289 143 255 143c-34.3 0-63.1 22.6-73 53.7C176.9 195.7 171 195 165 195c-32 0-58 26-58 58s26 58 58 58h180z" fill="#fff"/>
</svg>
</div>
<div class="auth-logo-text">OxiCloud</div>
</div>
<h2 class="auth-title" id="language-title">Welcome to OxiCloud</h2>
<p class="language-subtitle" id="language-subtitle">Please select your language</p>
<div class="language-options" id="language-options">
<label class="language-option" data-lang="en">
<input type="radio" name="language" value="en">
<span class="language-radio"></span>
<span class="language-flag">🇬🇧</span>
<span class="language-name">English</span>
</label>
<label class="language-option" data-lang="es">
<input type="radio" name="language" value="es">
<span class="language-radio"></span>
<span class="language-flag">🇪🇸</span>
<span class="language-name">Español</span>
</label>
<label class="language-option" data-lang="zh">
<input type="radio" name="language" value="zh">
<span class="language-radio"></span>
<span class="language-flag">🇨🇳</span>
<span class="language-name">中文</span>
</label>
</div>
<button type="button" class="auth-button" id="language-continue" disabled>Continue</button>
</div>
<div class="auth-panel" id="login-panel" style="display: none;">
<div class="auth-logo">
<div class="auth-logo-icon">
<svg viewBox="0 0 500 500">
@@ -174,6 +214,7 @@
</div>
<div class="auth-error" id="admin-setup-error"></div>
<div class="auth-success" id="admin-setup-success"></div>
<form class="auth-form" id="admin-setup-form">
<div class="auth-input-group">
+8 -11
View File
@@ -44,11 +44,11 @@
</div>
<div class="storage-container">
<div class="storage-title">Almacenamiento</div>
<div class="storage-title" data-i18n="storage.title">Storage</div>
<div class="storage-bar">
<div class="storage-fill"></div>
</div>
<div class="storage-info">56% usado (5.6GB / 10GB)</div>
<div class="storage-info" data-i18n="storage.calculating">Calculating...</div>
</div>
</div>
@@ -59,18 +59,15 @@
<div class="search-container">
<i class="fas fa-search search-icon"></i>
<input type="text" id="shared-search" data-i18n-placeholder="actions.search" placeholder="Search shared items...">
<button id="shared-search-btn" class="search-button"><i class="fas fa-search"></i></button>
<button id="shared-search-btn" class="search-button" data-i18n-title="actions.search_btn" title="Search">
<i class="fas fa-search"></i>
</button>
</div>
<div class="user-controls">
<div id="language-selector" class="language-selector">
<select id="language-selector-dropdown">
<option value="en">English</option>
<option value="es">Español</option>
</select>
</div>
<div class="user-avatar">MR</div>
<div id="logout-btn" class="logout-btn" title="Cerrar sesión">
<div id="language-selector"></div>
<div class="user-avatar" id="user-avatar">AD</div>
<div id="logout-btn" class="logout-btn" data-i18n-title="actions.logout" title="Log out">
<i class="fas fa-sign-out-alt"></i>
</div>
</div>