big refactoring
This commit is contained in:
+100
-9
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user