// Common utilities and functions class DecypharrUtils { constructor() { this.urlBase = window.urlBase || ''; this.toastContainer = null; this.init(); } init() { this.setupToastSystem(); this.setupThemeToggle(); this.setupPasswordToggles(); this.setupVersionInfo(); this.setupGlobalEventListeners(); this.createToastContainer(); } // Create toast container if it doesn't exist createToastContainer() { let container = document.querySelector('.toast-container'); if (!container) { container = document.createElement('div'); container.className = 'toast-container fixed bottom-4 right-4 z-50 space-y-2'; document.body.appendChild(container); } this.toastContainer = container; } // Setup toast system setupToastSystem() { // Add toast CSS styles this.addToastStyles(); // Global toast handler window.addEventListener('error', (e) => { console.error('Global error:', e.error); this.createToast(`Unexpected error: ${e.error?.message || 'Unknown error'}`, 'error'); }); // Handle unhandled promise rejections window.addEventListener('unhandledrejection', (e) => { console.error('Unhandled promise rejection:', e.reason); this.createToast(`Promise rejected: ${e.reason?.message || 'Unknown error'}`, 'error'); }); } // Add toast styles to document addToastStyles() { if (document.getElementById('toast-styles')) return; const style = document.createElement('style'); style.id = 'toast-styles'; style.textContent = ` @keyframes toastSlideIn { from { opacity: 0; transform: translateX(100%); } to { opacity: 1; transform: translateX(0); } } @keyframes toastSlideOut { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(100%); } } .toast-container .alert { animation: toastSlideIn 0.3s ease-out; max-width: 400px; word-wrap: break-word; } .toast-container .alert.toast-closing { animation: toastSlideOut 0.3s ease-in forwards; } @media (max-width: 640px) { .toast-container { left: 1rem; right: 1rem; bottom: 1rem; } .toast-container .alert { max-width: none; } } `; document.head.appendChild(style); } // URL joining utility joinURL(base, path) { if (!base.endsWith('/')) base += '/'; if (path.startsWith('/')) path = path.substring(1); return base + path; } // Enhanced fetch wrapper async fetcher(endpoint, options = {}) { const url = this.joinURL(this.urlBase, endpoint); // Handle FormData - don't set Content-Type for FormData const defaultOptions = { headers: {}, ...options }; // Only set Content-Type if not FormData if (!(options.body instanceof FormData)) { defaultOptions.headers['Content-Type'] = 'application/json'; } // Merge headers defaultOptions.headers = { ...defaultOptions.headers, ...options.headers }; try { const response = await fetch(url, defaultOptions); // Add loading state management if (options.loadingButton) { this.setButtonLoading(options.loadingButton, false); } return response; } catch (error) { if (options.loadingButton) { this.setButtonLoading(options.loadingButton, false); } throw error; } } // Enhanced toast system createToast(message, type = 'success', duration = null) { const toastTimeouts = { success: 5000, warning: 10000, error: 15000, info: 7000 }; type = ['success', 'warning', 'error', 'info'].includes(type) ? type : 'success'; duration = duration || toastTimeouts[type]; // Ensure toast container exists if (!this.toastContainer) { this.createToastContainer(); } const toastId = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const alertTypeClass = { success: 'alert-success', warning: 'alert-warning', error: 'alert-error', info: 'alert-info' }; const icons = { success: '', error: '', warning: '', info: '' }; const toastHtml = `
${icons[type]}
${message.replace(/\n/g, '
')}
`; this.toastContainer.insertAdjacentHTML('beforeend', toastHtml); // Auto-close toast const timeoutId = setTimeout(() => this.closeToast(toastId), duration); // Store timeout ID for manual closing const toastElement = document.getElementById(toastId); if (toastElement) { toastElement.dataset.timeoutId = timeoutId; } return toastId; } closeToast(toastId) { const toastElement = document.getElementById(toastId); if (toastElement) { // Clear auto-close timeout if (toastElement.dataset.timeoutId) { clearTimeout(parseInt(toastElement.dataset.timeoutId)); } toastElement.classList.add('toast-closing'); setTimeout(() => { if (toastElement.parentNode) { toastElement.remove(); } }, 300); } } // Close all toasts closeAllToasts() { const toasts = this.toastContainer?.querySelectorAll('.alert'); if (toasts) { toasts.forEach(toast => { if (toast.id) { this.closeToast(toast.id); } }); } } // Button loading state management setButtonLoading(buttonElement, loading = true, originalText = null) { if (typeof buttonElement === 'string') { buttonElement = document.getElementById(buttonElement) || document.querySelector(buttonElement); } if (!buttonElement) return; if (loading) { buttonElement.disabled = true; if (!buttonElement.dataset.originalText) { buttonElement.dataset.originalText = originalText || buttonElement.innerHTML; } buttonElement.innerHTML = 'Processing...'; buttonElement.classList.add('loading-state'); } else { buttonElement.disabled = false; buttonElement.innerHTML = buttonElement.dataset.originalText || 'Submit'; buttonElement.classList.remove('loading-state'); delete buttonElement.dataset.originalText; } } // Password field utilities setupPasswordToggles() { document.addEventListener('click', (e) => { const toggleBtn = e.target.closest('.password-toggle-btn'); if (toggleBtn) { e.preventDefault(); e.stopPropagation(); // Find the associated input field const container = toggleBtn.closest('.password-toggle-container'); if (container) { const input = container.querySelector('input, textarea'); const icon = toggleBtn.querySelector('i'); if (input && icon) { this.togglePasswordField(input, icon); } } } }); } togglePasswordField(field, icon) { if (!icon) return; if (field.tagName.toLowerCase() === 'textarea') { this.togglePasswordTextarea(field, icon); } else { this.togglePasswordInput(field, icon); } } togglePasswordInput(field, icon) { if (field.type === 'password') { field.type = 'text'; icon.className = 'bi bi-eye-slash'; } else { field.type = 'password'; icon.className = 'bi bi-eye'; } } togglePasswordTextarea(field, icon) { const isHidden = field.style.webkitTextSecurity === 'disc' || field.style.webkitTextSecurity === '' || field.getAttribute('data-password-visible') !== 'true'; if (isHidden) { field.style.webkitTextSecurity = 'none'; field.style.textSecurity = 'none'; field.setAttribute('data-password-visible', 'true'); icon.className = 'bi bi-eye-slash'; } else { field.style.webkitTextSecurity = 'disc'; field.style.textSecurity = 'disc'; field.setAttribute('data-password-visible', 'false'); icon.className = 'bi bi-eye'; } } // Legacy methods for backward compatibility togglePassword(fieldId) { const field = document.getElementById(fieldId); const button = field?.closest('.password-toggle-container')?.querySelector('.password-toggle-btn'); let icon = button.querySelector("i"); if (field && icon) { this.togglePasswordField(field, icon); } } // Theme management setupThemeToggle() { const themeToggle = document.getElementById('themeToggle'); const htmlElement = document.documentElement; if (!themeToggle) return; const setTheme = (theme) => { htmlElement.setAttribute('data-theme', theme); localStorage.setItem('theme', theme); themeToggle.checked = theme === 'dark'; // Smooth theme transition document.body.style.transition = 'background-color 0.3s ease, color 0.3s ease'; setTimeout(() => { document.body.style.transition = ''; }, 300); // Emit theme change event window.dispatchEvent(new CustomEvent('themechange', { detail: { theme } })); }; // Load saved theme const savedTheme = localStorage.getItem('theme'); if (savedTheme) { setTheme(savedTheme); } else if (window.matchMedia?.('(prefers-color-scheme: dark)').matches) { setTheme('dark'); } else { setTheme('light'); } // Theme toggle event themeToggle.addEventListener('change', () => { const currentTheme = htmlElement.getAttribute('data-theme'); setTheme(currentTheme === 'dark' ? 'light' : 'dark'); }); // Listen for system theme changes if (window.matchMedia) { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { if (!localStorage.getItem('theme')) { setTheme(e.matches ? 'dark' : 'light'); } }); } } // Version info async setupVersionInfo() { try { const response = await this.fetcher('/version'); if (!response.ok) throw new Error('Failed to fetch version'); const data = await response.json(); const versionBadge = document.getElementById('version-badge'); if (versionBadge) { versionBadge.innerHTML = ` ${data.channel}-${data.version} `; // Remove existing badge classes versionBadge.classList.remove('badge-warning', 'badge-error', 'badge-ghost'); if (data.channel === 'beta') { versionBadge.classList.add('badge-warning'); } else if (data.channel === 'nightly') { versionBadge.classList.add('badge-error'); } } } catch (error) { console.error('Error fetching version:', error); const versionBadge = document.getElementById('version-badge'); if (versionBadge) { versionBadge.textContent = 'Unknown'; versionBadge.classList.add('badge-ghost'); } } } // Global event listeners setupGlobalEventListeners() { // Smooth scroll for anchor links document.addEventListener('click', (e) => { const link = e.target.closest('a[href^="#"]'); if (link && link.getAttribute('href') !== '#') { e.preventDefault(); const target = document.querySelector(link.getAttribute('href')); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } } }); // Enhanced form validation document.addEventListener('invalid', (e) => { e.target.classList.add('input-error'); setTimeout(() => e.target.classList.remove('input-error'), 3000); }, true); // Keyboard shortcuts document.addEventListener('keydown', (e) => { // Escape key closes modals and dropdowns if (e.key === 'Escape') { // Close modals document.querySelectorAll('.modal[open]').forEach(modal => modal.close()); // Close dropdowns document.querySelectorAll('.dropdown-open').forEach(dropdown => { dropdown.classList.remove('dropdown-open'); }); // Close context menus document.querySelectorAll('.context-menu:not(.hidden)').forEach(menu => { menu.classList.add('hidden'); }); } // Ctrl/Cmd + / for help (if help system exists) if ((e.ctrlKey || e.metaKey) && e.key === '/') { e.preventDefault(); this.showKeyboardShortcuts(); } }); // Handle page visibility changes document.addEventListener('visibilitychange', () => { if (document.hidden) { // Page is hidden - pause auto-refresh timers if any window.dispatchEvent(new CustomEvent('pageHidden')); } else { // Page is visible - resume auto-refresh timers if any window.dispatchEvent(new CustomEvent('pageVisible')); } }); // Handle online/offline status window.addEventListener('online', () => { this.createToast('Connection restored', 'success'); }); window.addEventListener('offline', () => { this.createToast('Connection lost - working offline', 'warning'); }); } // Show keyboard shortcuts modal showKeyboardShortcuts() { const shortcuts = [ { key: 'Esc', description: 'Close modals and dropdowns' }, { key: 'Ctrl + /', description: 'Show this help' }, { key: 'Ctrl + R', description: 'Refresh page' } ]; const modal = document.createElement('dialog'); modal.className = 'modal'; modal.innerHTML = ` `; document.body.appendChild(modal); modal.showModal(); modal.addEventListener('close', () => { document.body.removeChild(modal); }); } // Utility methods formatBytes(bytes) { if (!bytes || bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; } formatSpeed(speed) { return `${this.formatBytes(speed)}/s`; } formatDuration(seconds) { if (!seconds || seconds === 0) return '0s'; const units = [ { label: 'd', seconds: 86400 }, { label: 'h', seconds: 3600 }, { label: 'm', seconds: 60 }, { label: 's', seconds: 1 } ]; const parts = []; let remaining = seconds; for (const unit of units) { const count = Math.floor(remaining / unit.seconds); if (count > 0) { parts.push(`${count}${unit.label}`); remaining %= unit.seconds; } } return parts.slice(0, 2).join(' ') || '0s'; } // Debounce function debounce(func, wait, immediate = false) { let timeout; return function executedFunction(...args) { const later = () => { timeout = null; if (!immediate) func(...args); }; const callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func(...args); }; } // Throttle function throttle(func, limit) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } // Copy to clipboard utility async copyToClipboard(text) { try { await navigator.clipboard.writeText(text); this.createToast('Copied to clipboard', 'success'); return true; } catch (error) { console.error('Failed to copy to clipboard:', error); this.createToast('Failed to copy to clipboard', 'error'); return false; } } // Validate URL isValidUrl(string) { try { new URL(string); return true; } catch (_) { return false; } } // Escape HTML escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text ? text.replace(/[&<>"']/g, (m) => map[m]) : ''; } // Get current theme getCurrentTheme() { return document.documentElement.getAttribute('data-theme') || 'light'; } // Network status isOnline() { return navigator.onLine; } } // Initialize utilities window.decypharrUtils = new DecypharrUtils(); // Global functions for backward compatibility window.fetcher = (endpoint, options = {}) => window.decypharrUtils.fetcher(endpoint, options); window.createToast = (message, type, duration) => window.decypharrUtils.createToast(message, type, duration); // Export for ES6 modules if needed if (typeof module !== 'undefined' && module.exports) { module.exports = DecypharrUtils; }