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
+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;