880 lines
32 KiB
JavaScript
880 lines
32 KiB
JavaScript
// 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 = `
|
|
<div class="user-item">
|
|
<div class="user-item__status online"></div>
|
|
<div class="user-item__nickname">Welcome to chat!</div>
|
|
</div>
|
|
`;
|
|
} else {
|
|
users.forEach(user => {
|
|
const statusClass = user.is_online ? 'online' : 'offline';
|
|
const nickname = user.nickname || `User ${user.user_id.substring(0, 4)}...`;
|
|
html += `
|
|
<div class="user-item">
|
|
<div class="user-item__status ${statusClass}"></div>
|
|
<div class="user-item__nickname">${escapeHtml(nickname)}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
}
|
|
|
|
activeUsersContainer.innerHTML = html;
|
|
}
|
|
|
|
// Add new activity to the dashboard
|
|
function addDashboardActivity(activity) {
|
|
const recentActivity = document.getElementById('recentActivity');
|
|
if (!recentActivity) return;
|
|
|
|
const activityHtml = `
|
|
<div class="activity-item">
|
|
<div class="activity-item__avatar">${activity.icon || '💬'}</div>
|
|
<div class="activity-item__content">
|
|
<div class="activity-item__text">${escapeHtml(activity.text)}</div>
|
|
<div class="activity-item__time">${activity.time || 'Just now'}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 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);
|
|
}
|
|
} 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');
|
|
}
|
|
|
|
// 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');
|
|
|
|
})();
|