507 lines
17 KiB
JavaScript
507 lines
17 KiB
JavaScript
// 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);
|
|
}
|
|
}
|
|
|
|
// Mobile responsive behavior
|
|
function handleWindowResize() {
|
|
const isMobile = window.innerWidth <= AppConfig.ui.mobileBreakpoint;
|
|
|
|
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 isMobile = window.innerWidth <= AppConfig.ui.mobileBreakpoint;
|
|
|
|
if (isMobile) {
|
|
// 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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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');
|
|
|
|
})();
|