// UI Controls Module // Handles UI toggle functions, keyboard shortcuts, notifications, and DOM utilities (function() { 'use strict'; AppModules.require('api', 'screen-reader'); AppModules.register('ui-controls'); // Toast notification system function showToast(message, duration = AppConfig.ui.toastDuration) { const toast = document.getElementById('toast'); if (!toast) return; toast.textContent = message; toast.classList.add('show'); if (duration > 0) { setTimeout(() => { toast.classList.remove('show'); }, duration); } } function hideToast() { const toast = document.getElementById('toast'); if (toast) { toast.classList.remove('show'); } } // Notification badge management function updateNotificationBadge() { const badge = document.getElementById('notificationBadge'); if (badge) { if (AppState.unreadCount > 0) { badge.textContent = AppState.unreadCount > 99 ? '99+' : AppState.unreadCount; badge.classList.add('show'); } else { badge.classList.remove('show'); } } } // Dashboard toggle functionality function toggleDashboard() { const theater = document.querySelector('.theater'); const dashboardToggleBtn = document.querySelector('.video-player__toggle-dashboard-btn'); if (!theater || !dashboardToggleBtn) return; const isEnabled = theater.classList.contains('dashboard-enabled'); if (isEnabled) { theater.classList.remove('dashboard-enabled'); dashboardToggleBtn.setAttribute('aria-expanded', 'false'); AppState.dashboardEnabled = false; } else { theater.classList.add('dashboard-enabled'); dashboardToggleBtn.setAttribute('aria-expanded', 'true'); AppState.dashboardEnabled = true; // Initialize dashboard data when first enabled updateDashboardStats(); updateActiveUsers(); } // Store preference in localStorage try { localStorage.setItem('dashboard-enabled', AppState.dashboardEnabled ? 'true' : 'false'); } catch (e) { AppLogger.log('Audio notification failed:', e); } } // Dashboard stats management function updateDashboardStats() { // Update viewer count in dashboard const dashboardViewerCount = document.getElementById('statsViewersCount'); if (dashboardViewerCount) { const viewers = AppState.viewers || 0; dashboardViewerCount.textContent = viewers.toLocaleString(); } // Update stream quality const streamQuality = document.getElementById('statsStreamQuality'); if (streamQuality) { // Get from URL or assume HD for now streamQuality.textContent = 'HD'; } // Update uptime const uptime = document.getElementById('statsUptime'); if (uptime) { const streamStartTime = AppState.streamStartTime || Date.now() - 60000; // Default to 1 minute const elapsed = Date.now() - streamStartTime; const minutes = Math.floor(elapsed / 60000); const seconds = Math.floor((elapsed % 60000) / 1000); uptime.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; } } // Active users widget update function updateActiveUsers() { const activeUsersContainer = document.getElementById('activeUsers'); if (!activeUsersContainer) return; // This would typically poll for active users from server if (window.ChatSystem && typeof window.ChatSystem.getActiveViewers === 'function') { window.ChatSystem.getActiveViewers().then(users => { displayActiveUsers(users); }).catch(err => { AppLogger.error('Failed to get active users:', err); }); } else { // Fallback: show placeholder displayActiveUsers([]); } } function displayActiveUsers(users) { const activeUsersContainer = document.getElementById('activeUsers'); if (!activeUsersContainer) return; let html = ''; if (users.length === 0) { html = `
Welcome to chat!
`; } else { users.forEach(user => { const statusClass = user.is_online ? 'online' : 'offline'; const nickname = user.nickname || `User ${user.user_id.substring(0, 4)}...`; html += `
${escapeHtml(nickname)}
`; }); } activeUsersContainer.innerHTML = html; } // Add new activity to the dashboard function addDashboardActivity(activity) { const recentActivity = document.getElementById('recentActivity'); if (!recentActivity) return; const activityHtml = `
${activity.icon || '💬'}
${escapeHtml(activity.text)}
${activity.time || 'Just now'}
`; // Remove "Welcome" message if it exists and add new activity const existingActivities = recentActivity.querySelectorAll('.activity-item'); if (existingActivities.length >= 5) { existingActivities[existingActivities.length - 1].remove(); } recentActivity.insertAdjacentHTML('afterbegin', activityHtml); } // Focus management for modals function trapFocus(element) { const focusableElements = element.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); const firstFocusable = focusableElements[0]; const lastFocusable = focusableElements[focusableElements.length - 1]; function handleKeyDown(e) { if (e.key === 'Tab') { if (e.shiftKey) { // Shift + Tab if (document.activeElement === firstFocusable) { e.preventDefault(); lastFocusable.focus(); } } else { // Tab if (document.activeElement === lastFocusable) { e.preventDefault(); firstFocusable.focus(); } } } if (e.key === 'Escape') { // Close modal on Escape toggleChat(); } } element.addEventListener('keydown', handleKeyDown); // Return cleanup function return function() { element.removeEventListener('keydown', handleKeyDown); }; } // Store for focus management let previousFocusElement = null; let focusCleanup = null; // Chat toggle functionality function toggleChat() { const chatSection = document.getElementById('chatSection'); const videoSection = document.getElementById('videoSection'); const toggleBtn = document.getElementById('chatToggleText'); if (!chatSection || !videoSection || !toggleBtn) return; const isMobile = window.innerWidth <= AppConfig.ui.mobileBreakpoint; const wasCollapsed = AppState.chatCollapsed; AppState.chatCollapsed = !AppState.chatCollapsed; if (AppState.chatCollapsed) { // Closing chat if (isMobile) { chatSection.classList.remove('mobile-visible'); toggleBtn.textContent = 'Show Chat'; // Clean up modal focus management if (focusCleanup) { focusCleanup(); focusCleanup = null; } // Restore focus to the toggle button if (previousFocusElement) { previousFocusElement.focus(); previousFocusElement = null; } } else { chatSection.classList.add('collapsed'); videoSection.classList.add('expanded'); toggleBtn.textContent = 'Show Chat'; } } else { // Opening chat if (isMobile) { chatSection.classList.add('mobile-visible'); toggleBtn.textContent = 'Hide Chat'; // Store current focus element for restoration previousFocusElement = document.activeElement; // Set up modal focus management focusCleanup = trapFocus(chatSection); // Focus first focusable element in chat (nickname input) const nicknameInput = document.getElementById('nickname'); if (nicknameInput) { setTimeout(() => nicknameInput.focus(), 100); // Delay to ensure element is visible } // Add modal backdrop styling chatSection.setAttribute('role', 'dialog'); chatSection.setAttribute('aria-modal', 'true'); chatSection.setAttribute('aria-labelledby', 'chatSection'); } else { chatSection.classList.remove('collapsed'); videoSection.classList.remove('expanded'); toggleBtn.textContent = 'Hide Chat'; } // Clear unread count when reopening AppState.unreadCount = 0; updateNotificationBadge(); } } // Viewer count update function updateViewerCount(count) { const viewerElement = document.getElementById('viewerCount'); if (viewerElement) { viewerElement.textContent = count + (count === 1 ? ' viewer' : ' viewers'); } // Announce viewer count changes to screen readers ScreenReader.viewerCount(count); } // Connection status dot function updateConnectionStatus(online) { const statusDot = document.getElementById('statusDot'); const statusText = document.getElementById('statusText'); if (statusDot && statusText) { if (online) { statusDot.classList.remove('offline'); statusText.textContent = 'Connected'; } else { statusDot.classList.add('offline'); statusText.textContent = 'Reconnecting...'; } } } // Keyboard shortcuts handler function handleKeyboardShortcuts(event) { // Skip if typing in input field if (event.target.matches('input, textarea')) { return; } switch (event.key) { case 'c': case 'C': // Toggle chat event.preventDefault(); toggleChat(); break; case 'f': case 'F': // Toggle fullscreen event.preventDefault(); toggleFullscreen(); break; case '/': // Focus chat input event.preventDefault(); const messageInput = document.getElementById('messageInput'); if (messageInput) { messageInput.focus(); } break; case 'p': case 'P': // Toggle picture-in-picture event.preventDefault(); togglePictureInPicture(); break; } } // Fullscreen functionality function toggleFullscreen() { if (!AppState.player) return; if (AppState.player.isFullscreen()) { AppState.player.exitFullscreen(); } else { AppState.player.requestFullscreen(); } } // Picture-in-picture functionality function togglePictureInPicture() { const video = document.getElementById('video-player'); if (!video || !video.requestPictureInPicture) return; if (document.pictureInPictureElement) { document.exitPictureInPicture(); } else { video.requestPictureInPicture(); } } // Sound notification for new messages function playNotificationSound() { if (!AppState.soundEnabled || !document.hidden) return; try { const audioContext = new (window.AudioContext || window.webkitAudioContext)(); const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.frequency.value = 800; oscillator.type = 'sine'; gainNode.gain.value = 0.1; oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.1); } catch(e) { AppLogger.log('Audio notification failed:', e); } } // Determine device type for responsive behavior function getDeviceType() { const width = window.innerWidth; if (width <= AppConfig.ui.mobileBreakpoint) return 'mobile'; if (width >= 768 && width < 1024) return 'tablet'; // Tablet: 768px-1023px return 'desktop'; // Desktop: 1024px+ } // Responsive behavior based on device type function handleWindowResize() { const deviceType = getDeviceType(); const isMobile = deviceType === 'mobile'; if (isMobile && !AppState.chatCollapsed) { // Auto-collapse chat on mobile for better viewing toggleChat(); } } // Touch gesture handling for mobile interactions let touchStartX = 0; let touchStartY = 0; let touchStartTime = 0; let isSwipeGesture = false; function handleTouchStart(event) { touchStartX = event.touches[0].clientX; touchStartY = event.touches[0].clientY; touchStartTime = Date.now(); isSwipeGesture = false; } function handleTouchMove(event) { if (!event.touches || event.touches.length === 0) return; const touchCurrentX = event.touches[0].clientX; const touchCurrentY = event.touches[0].clientY; const deltaX = touchCurrentX - touchStartX; const deltaY = touchCurrentY - touchStartY; // Determine if this is a horizontal swipe (more horizontal than vertical movement) if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 50) { isSwipeGesture = true; } } function handleTouchEnd(event) { if (!touchStartTime) return; const touchEndTime = Date.now(); const touchDuration = touchEndTime - touchStartTime; if (touchDuration < 1000 && isSwipeGesture) { // Swipe gesture within 1 second const touchEndX = event.changedTouches[0].clientX; const deltaX = touchEndX - touchStartX; const minSwipeDistance = 75; // Minimum swipe distance in pixels if (Math.abs(deltaX) > minSwipeDistance) { const deviceType = getDeviceType(); if (deviceType === 'mobile') { // On mobile, swipe left to hide chat, right to show chat if (deltaX < 0) { // Swipe left - hide chat if (!AppState.chatCollapsed) { toggleChat(); } } else { // Swipe right - show chat if (AppState.chatCollapsed) { toggleChat(); } } } else if (deviceType === 'tablet') { // On tablet, swipe gestures control video playback if (deltaX < 0) { // Swipe left - seek forward if (AppState.player && typeof AppState.player.currentTime === 'function') { const currentTime = AppState.player.currentTime(); AppState.player.currentTime(currentTime + 10); // Skip forward 10 seconds showToast('Skipped forward 10s', 1000); if (window.ScreenReader) { ScreenReader.statusUpdate('video', 'skipped forward 10 seconds', 'seek'); } } } else { // Swipe right - seek backward if (AppState.player && typeof AppState.player.currentTime === 'function') { const currentTime = AppState.player.currentTime(); AppState.player.currentTime(Math.max(0, currentTime - 10)); // Skip back 10 seconds showToast('Skipped back 10s', 1000); if (window.ScreenReader) { ScreenReader.statusUpdate('video', 'skipped back 10 seconds', 'seek'); } } } } } } // Reset touch tracking touchStartX = 0; touchStartY = 0; touchStartTime = 0; isSwipeGesture = false; } // Pull-to-refresh functionality for mobile let pullStartY = 0; let pullDistance = 0; let isPulling = false; const pullThreshold = 80; // Minimum pull distance to trigger refresh function handlePullToRefreshTouchStart(event) { if (window.innerWidth > AppConfig.ui.mobileBreakpoint) return; // Only on mobile pullStartY = event.touches[0].clientY; pullDistance = 0; isPulling = true; } function handlePullToRefreshTouchMove(event) { if (!isPulling || window.innerWidth > AppConfig.ui.mobileBreakpoint) return; const currentY = event.touches[0].clientY; pullDistance = currentY - pullStartY; // Only allow pull down from top of page if (window.scrollY === 0 && pullDistance > 0) { event.preventDefault(); // Prevent default scrolling behavior const pullIndicator = document.getElementById('pullRefreshIndicator'); if (pullIndicator) { const opacity = Math.min(pullDistance / pullThreshold, 1); pullIndicator.style.opacity = opacity; pullIndicator.style.transform = `translateY(${Math.min(pullDistance * 0.5, 40)}px)`; } } else { pullDistance = 0; } } function handlePullToRefreshTouchEnd() { if (!isPulling) return; isPulling = false; if (pullDistance > pullThreshold && window.scrollY === 0) { // Trigger refresh performPullToRefresh(); } // Reset pull indicator const pullIndicator = document.getElementById('pullRefreshIndicator'); if (pullIndicator) { pullIndicator.style.opacity = '0'; pullIndicator.style.transform = 'translateY(0px)'; } pullStartY = 0; pullDistance = 0; } function performPullToRefresh() { showToast('Refreshing...', 0); // Show loading message // Trigger the refresh by reloading data if (window.ChatSystem && typeof window.ChatSystem.refresh === 'function') { window.ChatSystem.refresh().then(() => { showToast('Content refreshed', 2000); }).catch(() => { showToast('Refresh failed', 2000); }); } else { // Fallback: refresh the page after a short delay setTimeout(() => { window.location.reload(); }, 500); } } // Double-tap handler for video area (fullscreen toggle) let lastTapTime = 0; const doubleTapDelay = 300; // milliseconds function handleVideoDoubleTap(event) { const currentTime = Date.now(); const timeSinceLastTap = currentTime - lastTapTime; if (timeSinceLastTap < doubleTapDelay && timeSinceLastTap > 0) { // Double tap detected - toggle fullscreen event.preventDefault(); toggleFullscreen(); lastTapTime = 0; } else { lastTapTime = currentTime; } } // Page visibility API handler function handleVisibilityChange() { if (!document.hidden && !AppState.chatCollapsed) { // Clear unread count when page becomes visible AppState.unreadCount = 0; updateNotificationBadge(); } } // DOM utility functions const DOMUtils = { // Add event listener with proper cleanup tracking addEvent: function(element, event, handler) { if (!element) return; element.addEventListener(event, handler); // Store handler for potential cleanup if (!element._handlers) { element._handlers = new Map(); } if (!element._handlers.has(event)) { element._handlers.set(event, []); } element._handlers.get(event).push(handler); }, // Remove event listeners (for cleanup) removeEvent: function(element, event, handler) { if (!element || !element._handlers || !element._handlers.has(event)) return; const handlers = element._handlers.get(event); const index = handlers.indexOf(handler); if (index > -1) { handlers.splice(index, 1); element.removeEventListener(event, handler); } }, // Get element with optional error logging getElement: function(id, required = false) { const element = document.getElementById(id); if (required && !element) { AppLogger.error(`Required element with id '${id}' not found`); } return element; }, // Add/remove classes safely addClass: function(element, className) { if (element) element.classList.add(className); }, removeClass: function(element, className) { if (element) element.classList.remove(className); }, toggleClass: function(element, className) { if (element) element.classList.toggle(className); } }; // Mobile navigation action handler function handleMobileNavAction(action) { switch (action) { case 'toggle-chat': toggleChat(); break; case 'refresh-stream': performPullToRefresh(); break; case 'toggle-fullscreen': toggleFullscreen(); break; case 'toggle-picture-in-picture': togglePictureInPicture(); break; case 'toggle-quality': // Toggle quality selector visibility const qualitySelector = document.getElementById('qualitySelector'); if (qualitySelector) { qualitySelector.style.display = qualitySelector.style.display === 'none' ? 'block' : 'none'; } break; default: AppLogger.warn('Unknown mobile nav action:', action); } } // Initialize event listeners function initializeEventListeners() { // Keyboard shortcuts DOMUtils.addEvent(document, 'keydown', handleKeyboardShortcuts); // Window resize for mobile responsiveness DOMUtils.addEvent(window, 'resize', handleWindowResize); // Page visibility for notification clearing DOMUtils.addEvent(document, 'visibilitychange', handleVisibilityChange); // Touch gesture support for mobile const videoSection = document.getElementById('videoSection'); if (videoSection) { // Swipe gestures on video area for chat toggle DOMUtils.addEvent(videoSection, 'touchstart', handleTouchStart, { passive: true }); DOMUtils.addEvent(videoSection, 'touchmove', handleTouchMove, { passive: true }); DOMUtils.addEvent(videoSection, 'touchend', handleTouchEnd, { passive: true }); // Double-tap on video for fullscreen DOMUtils.addEvent(videoSection, 'touchend', handleVideoDoubleTap); } // Pull-to-refresh on the whole document (only on mobile) DOMUtils.addEvent(document, 'touchstart', handlePullToRefreshTouchStart, { passive: true }); DOMUtils.addEvent(document, 'touchmove', handlePullToRefreshTouchMove, { passive: false }); DOMUtils.addEvent(document, 'touchend', handlePullToRefreshTouchEnd, { passive: true }); // Mobile navigation buttons const mobileNav = document.getElementById('mobileNav'); if (mobileNav) { mobileNav.addEventListener('click', function(event) { const button = event.target.closest('.mobile-nav-btn'); if (button && button.dataset.action) { event.preventDefault(); handleMobileNavAction(button.dataset.action); } }); } AppLogger.log('UI controls event listeners initialized'); } // HTML escape utility function function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Initialize dashboard and resize functionality function initializeDashboard() { // Restore dashboard state from localStorage try { const savedDashboardState = localStorage.getItem('dashboard-enabled'); if (savedDashboardState === 'true' && window.innerWidth >= 1536) { // Only restore if screen is large enough setTimeout(() => toggleDashboard(), 100); // Delay to ensure DOM is ready } } catch (e) { // localStorage not available } // Set initial stream start time for uptime tracking if (!AppState.streamStartTime) { AppState.streamStartTime = Date.now(); } // Update dashboard stats every 10 seconds setInterval(() => { if (AppState.dashboardEnabled) { updateDashboardStats(); } }, 10000); } // Resize handling for dashboard panels let isResizing = false; let currentResizeTarget = null; let startX = 0; let startWidth = 0; function handleResizeMouseDown(event, target) { isResizing = true; currentResizeTarget = target; startX = event.clientX; const targetElement = document.querySelector(target === 'dashboard' ? '.theater__dashboard-section' : '.theater__video-section'); startWidth = targetElement ? targetElement.offsetWidth : 0; document.addEventListener('mousemove', handleResizeMouseMove); document.addEventListener('mouseup', handleResizeMouseUp); document.body.style.cursor = 'col-resize'; event.preventDefault(); } function handleResizeMouseMove(event) { if (!isResizing || !currentResizeTarget) return; const deltaX = event.clientX - startX; const targetElement = document.querySelector(currentResizeTarget === 'dashboard' ? '.theater__dashboard-section' : '.theater__video-section'); if (!targetElement) return; const newWidth = Math.max(200, startWidth + deltaX); targetElement.style.width = newWidth + 'px'; } function handleResizeMouseUp() { isResizing = false; currentResizeTarget = null; document.removeEventListener('mousemove', handleResizeMouseMove); document.removeEventListener('mouseup', handleResizeMouseUp); document.body.style.cursor = ''; } // Initialize event listeners function initializeEventListeners() { // Dashboard toggle button const dashboardToggleBtn = document.querySelector('.video-player__toggle-dashboard-btn'); if (dashboardToggleBtn) { DOMUtils.addEvent(dashboardToggleBtn, 'click', toggleDashboard); } // Dashboard toggle in sidebar const dashboardSidebarToggle = document.querySelector('.dashboard__toggle-btn'); if (dashboardSidebarToggle) { DOMUtils.addEvent(dashboardSidebarToggle, 'click', toggleDashboard); } // Resize handles const resizeHandles = document.querySelectorAll('.resize-handle'); resizeHandles.forEach(handle => { DOMUtils.addEvent(handle, 'mousedown', (e) => { const target = handle.dataset.target; handleResizeMouseDown(e, target); }); }); // Keyboard shortcuts DOMUtils.addEvent(document, 'keydown', handleKeyboardShortcuts); // Window resize for mobile responsiveness DOMUtils.addEvent(window, 'resize', handleWindowResize); // Page visibility for notification clearing DOMUtils.addEvent(document, 'visibilitychange', handleVisibilityChange); // Touch gesture support for mobile const videoSection = document.getElementById('videoSection'); if (videoSection) { // Swipe gestures on video area for chat toggle DOMUtils.addEvent(videoSection, 'touchstart', handleTouchStart, { passive: true }); DOMUtils.addEvent(videoSection, 'touchmove', handleTouchMove, { passive: true }); DOMUtils.addEvent(videoSection, 'touchend', handleTouchEnd, { passive: true }); // Double-tap on video for fullscreen DOMUtils.addEvent(videoSection, 'touchend', handleVideoDoubleTap); } // Pull-to-refresh on the whole document (only on mobile) DOMUtils.addEvent(document, 'touchstart', handlePullToRefreshTouchStart, { passive: true }); DOMUtils.addEvent(document, 'touchmove', handlePullToRefreshTouchMove, { passive: false }); DOMUtils.addEvent(document, 'touchend', handlePullToRefreshTouchEnd, { passive: true }); // Mobile navigation buttons const mobileNav = document.getElementById('mobileNav'); if (mobileNav) { mobileNav.addEventListener('click', function(event) { const button = event.target.closest('.mobile-nav-btn'); if (button && button.dataset.action) { event.preventDefault(); handleMobileNavAction(button.dataset.action); } }); } // Initialize dashboard functionality initializeDashboard(); AppLogger.log('UI controls event listeners initialized'); } // Public API window.UIControls = { showToast: showToast, hideToast: hideToast, toggleChat: toggleChat, toggleDashboard: toggleDashboard, toggleFullscreen: toggleFullscreen, togglePictureInPicture: togglePictureInPicture, updateViewerCount: updateViewerCount, updateConnectionStatus: updateConnectionStatus, updateNotificationBadge: updateNotificationBadge, updateDashboardStats: updateDashboardStats, updateActiveUsers: updateActiveUsers, addDashboardActivity: addDashboardActivity, playNotificationSound: playNotificationSound, handleMobileNavAction: handleMobileNavAction, DOMUtils: DOMUtils }; // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', initializeEventListeners); AppLogger.log('UI Controls module loaded'); })();