// UI Controls Module // Handles UI toggle functions, keyboard shortcuts, notifications, and DOM utilities (function() { 'use strict'; AppModules.require('api'); 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'); } } } // 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; AppState.chatCollapsed = !AppState.chatCollapsed; if (AppState.chatCollapsed) { if (isMobile) { chatSection.classList.remove('mobile-visible'); toggleBtn.textContent = 'Show Chat'; } else { chatSection.classList.add('collapsed'); videoSection.classList.add('expanded'); toggleBtn.textContent = 'Show Chat'; } } else { if (isMobile) { chatSection.classList.add('mobile-visible'); toggleBtn.textContent = 'Hide Chat'; } 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'); } } // 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); } } 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); } } } } } // 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'); } // Public API window.UIControls = { showToast: showToast, hideToast: hideToast, toggleChat: toggleChat, toggleFullscreen: toggleFullscreen, togglePictureInPicture: togglePictureInPicture, updateViewerCount: updateViewerCount, updateConnectionStatus: updateConnectionStatus, updateNotificationBadge: updateNotificationBadge, playNotificationSound: playNotificationSound, handleMobileNavAction: handleMobileNavAction, DOMUtils: DOMUtils }; // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', initializeEventListeners); AppLogger.log('UI Controls module loaded'); })();