UNPKG

@zyrecx/tflix

Version:

TFlix transforms Cineby.app into a TV-friendly experience for Samsung TVs running TizenBrew with enhanced remote navigation.

514 lines (442 loc) 15 kB
/** * Cineby.app Content Detector and Enhancer * This module detects and enhances specific elements on Cineby.app */ /** * Detect and enhance content cards/items */ function enhanceContentItems() { const selectors = [ // Common movie/show card selectors - update these after inspecting the actual site '.movie-card', '.content-item', '.film-item', '.show-card', // Typical class names for grid items '.grid-item', '.card', // Image containers '.poster-container', '.thumbnail', // Any anchors with images (likely to be content items) 'a:has(img)' ]; // Find all content items using the selectors const allSelectors = selectors.join(', '); const contentItems = document.querySelectorAll(allSelectors); // Make each item focusable and add navigation attributes contentItems.forEach((item, index) => { // Ensure the item is focusable if (!item.getAttribute('tabindex')) { item.setAttribute('tabindex', '0'); } // Add data attribute for easier selection item.setAttribute('data-tflix-item', index); // Add click event if it's not an anchor if (item.tagName !== 'A' && !item.onclick) { item.addEventListener('click', () => { // If there's an anchor inside, click it const anchor = item.querySelector('a'); if (anchor) { anchor.click(); } }); } // Add focus and blur event listeners item.addEventListener('focus', () => { item.classList.add('tflix-focused'); }); item.addEventListener('blur', () => { item.classList.remove('tflix-focused'); }); }); } /** * Enhance navigation menus for better remote control navigation */ function enhanceNavigationMenus() { const navSelectors = [ 'nav', 'header nav', '.main-nav', '.navigation', '.menu', '.sidebar' ]; // Find all navigation containers const navContainers = document.querySelectorAll(navSelectors.join(', ')); navContainers.forEach(nav => { // Find all navigation items/links const navItems = nav.querySelectorAll('a, button'); navItems.forEach((item, index) => { // Ensure the item is focusable if (!item.getAttribute('tabindex')) { item.setAttribute('tabindex', '0'); } // Add data attribute for easier selection item.setAttribute('data-tflix-nav-item', index); // Add focus and blur event listeners item.addEventListener('focus', () => { item.classList.add('tflix-focused'); }); item.addEventListener('blur', () => { item.classList.remove('tflix-focused'); }); }); }); } /** * Enhance video player controls and interaction */ function enhanceVideoPlayer() { const videoPlayer = document.querySelector('video'); if (!videoPlayer) return; // Add focus capability to native controls if they exist const controls = document.querySelectorAll('.video-controls button, .player-controls button'); controls.forEach((control, index) => { // Ensure the control is focusable if (!control.getAttribute('tabindex')) { control.setAttribute('tabindex', '0'); } // Add data attribute for easier selection control.setAttribute('data-tflix-control', index); // Add focus and blur event listeners control.addEventListener('focus', () => { control.classList.add('tflix-focused'); }); control.addEventListener('blur', () => { control.classList.remove('tflix-focused'); }); }); } /** * Enhance search functionality */ function enhanceSearchFunctionality() { // Look for search icon, button or input const searchSelectors = [ // Common search elements 'input[type="search"]', 'input[placeholder*="search" i]', 'input[placeholder*="find" i]', 'button[aria-label*="search" i]', '.search-button', '.search-icon', 'a[href*="search"]', // Icon based search 'svg[class*="search" i]', 'i[class*="search" i]', // Parent containers '.search-container', 'form[action*="search"]' ]; const searchElements = document.querySelectorAll(searchSelectors.join(', ')); searchElements.forEach(element => { // Make the search element more prominent and focusable element.setAttribute('tabindex', '0'); element.setAttribute('data-tflix-search', 'true'); // Add specific styling to make it stand out element.classList.add('tflix-search-element'); // Make parent element focusable too if (element.parentElement && !element.parentElement.getAttribute('tabindex')) { element.parentElement.setAttribute('tabindex', '0'); element.parentElement.setAttribute('data-tflix-search-parent', 'true'); } // Ensure clicking activates search element.addEventListener('click', () => { activateSearch(element); }); // On focus, show a toast to inform user they can press OK to search element.addEventListener('focus', () => { showSearchToast(); }); }); // Add specific handler for the navigation/header area addSearchNavigationHandler(); } /** * Add specific handler for navigation/header search */ function addSearchNavigationHandler() { // Try to find a header or navigation const headerElements = document.querySelectorAll('header, nav, .header, .navigation, .top-bar'); headerElements.forEach(header => { // Look for potential search elements in the header const searchLink = Array.from(header.querySelectorAll('a')).find(a => a.textContent.toLowerCase().includes('search') || a.href.includes('search') || a.getAttribute('aria-label')?.toLowerCase().includes('search') ); if (searchLink) { searchLink.setAttribute('tabindex', '0'); searchLink.setAttribute('data-tflix-search-nav', 'true'); // Add clear styling searchLink.classList.add('tflix-search-element'); // Ensure Enter key activates search searchLink.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); window.location.href = searchLink.href; } }); } }); // If the site is cineby.app, specifically look for the search link if (window.location.hostname.includes('cineby.app')) { // Make search more accessible without requiring keyboard shortcuts const searchLinks = document.querySelectorAll('a[href*="search"]'); searchLinks.forEach(link => { link.setAttribute('tabindex', '0'); link.classList.add('tflix-search-element'); }); } } /** * Check if element is an input field * @param {Element} element - Element to check * @returns {boolean} - True if it's an input element */ function isInputElement(element) { if (!element) return false; const tagName = element.tagName.toLowerCase(); return tagName === 'input' || tagName === 'textarea' || element.isContentEditable || element.getAttribute('role') === 'textbox'; } /** * Activate search functionality * @param {Element} element - The search element */ function activateSearch(element) { // If it's an input, focus it if (element.tagName.toLowerCase() === 'input') { element.focus(); return; } // If it's a link to search page, navigate to it if (element.tagName.toLowerCase() === 'a' && (element.href.includes('search') || element.getAttribute('href')?.includes('search'))) { window.location.href = element.href; return; } // If it's a button inside a form, submit the form const form = element.closest('form'); if (form) { form.submit(); return; } // For cineby.app specifically, navigate to the search page if (window.location.hostname.includes('cineby.app')) { window.location.href = 'https://www.cineby.app/search'; return; } } /** * Show toast informing about search functionality */ function showSearchToast() { const toast = document.createElement('div'); toast.className = 'tflix-toast'; toast.textContent = 'Press OK to access search'; document.body.appendChild(toast); // Show the toast setTimeout(() => { toast.classList.add('show'); }, 10); // Hide after 2 seconds setTimeout(() => { toast.classList.remove('show'); setTimeout(() => { toast.remove(); }, 300); }, 2000); } /** * Enhance video player with better controls specifically for Cineby.app */ function enhanceCinebyVideoPlayer() { // Only run on movie pages if (!window.location.pathname.includes('/movie/')) return; // Try to find the video player const videoPlayers = document.querySelectorAll('video'); if (!videoPlayers.length) { // If no video player is found immediately, set up an observer to catch it when it appears const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.addedNodes.length) { for (const node of mutation.addedNodes) { if (node.nodeName === 'VIDEO' || (node.querySelector && node.querySelector('video'))) { const video = node.nodeName === 'VIDEO' ? node : node.querySelector('video'); setupVideoPlayerControls(video); observer.disconnect(); return; } } } } }); observer.observe(document.body, { childList: true, subtree: true }); } else { // If video is already present, set up controls immediately videoPlayers.forEach(setupVideoPlayerControls); } } /** * Setup video player controls for Cineby.app * @param {HTMLElement} video - The video element */ function setupVideoPlayerControls(video) { if (!video) return; // Store reference to the video window.tflixVideoElement = video; // Make sure the video is visible and styled properly video.style.display = 'block'; video.style.opacity = '1'; video.style.visibility = 'visible'; // Enable native controls as a fallback video.controls = true; // Add our own key event listeners to control playback document.addEventListener('keydown', handleVideoKeyEvents); // Set initial volume if (video.volume > 0.8) { video.volume = 0.8; // Default to 80% volume } // Add time display addVideoTimeDisplay(video); } /** * Handle key events for video playback * @param {Event} e - The keydown event */ function handleVideoKeyEvents(e) { const video = window.tflixVideoElement; if (!video) return; // Check if we're on a video page if (!window.location.pathname.includes('/movie/')) return; switch (e.key) { case 'Enter': e.preventDefault(); if (video.paused) { video.play(); } else { video.pause(); } break; case 'ArrowUp': e.preventDefault(); video.volume = Math.min(1, video.volume + 0.1); showVideoInfoToast(`Volume: ${Math.round(video.volume * 100)}%`); break; case 'ArrowDown': e.preventDefault(); video.volume = Math.max(0, video.volume - 0.1); showVideoInfoToast(`Volume: ${Math.round(video.volume * 100)}%`); break; case 'ArrowLeft': e.preventDefault(); video.currentTime = Math.max(0, video.currentTime - 10); showVideoInfoToast(`- 10 seconds`); break; case 'ArrowRight': e.preventDefault(); video.currentTime = Math.min(video.duration, video.currentTime + 10); showVideoInfoToast(`+ 10 seconds`); break; } } /** * Add time display to the video * @param {HTMLElement} video - The video element */ function addVideoTimeDisplay(video) { if (!video) return; // Create time display element const timeDisplay = document.createElement('div'); timeDisplay.className = 'tflix-video-time'; // Add to the video container const videoContainer = video.parentElement; if (videoContainer) { videoContainer.appendChild(timeDisplay); } // Update time display function updateTimeDisplay() { if (!video.paused) { const current = formatTime(video.currentTime); const total = formatTime(video.duration); timeDisplay.textContent = `${current} / ${total}`; timeDisplay.style.display = 'block'; // Hide after 3 seconds if video is playing setTimeout(() => { if (!video.paused) { timeDisplay.style.display = 'none'; } }, 3000); } } // Format time in MM:SS function formatTime(seconds) { if (isNaN(seconds)) return '00:00'; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } // Update time on timeupdate event video.addEventListener('timeupdate', updateTimeDisplay); video.addEventListener('play', updateTimeDisplay); video.addEventListener('pause', updateTimeDisplay); video.addEventListener('seeking', updateTimeDisplay); } /** * Show a toast with video information * @param {string} message - The message to display */ function showVideoInfoToast(message) { let toast = document.querySelector('.tflix-video-toast'); if (!toast) { toast = document.createElement('div'); toast.className = 'tflix-video-toast'; document.body.appendChild(toast); } toast.textContent = message; toast.classList.add('show'); // Hide after 1.5 seconds setTimeout(() => { toast.classList.remove('show'); }, 1500); } /** * Initialize content enhancements */ function initializeContentEnhancements() { // First run detectAndEnhanceContent(); // Set up observer to continue detecting as the DOM changes const observer = new MutationObserver(() => { detectAndEnhanceContent(); }); // Start observing document body for DOM changes observer.observe(document.body, { childList: true, subtree: true }); } /** * Detect and enhance content based on current DOM */ function detectAndEnhanceContent() { enhanceContentItems(); enhanceNavigationMenus(); enhanceVideoPlayer(); enhanceSearchFunctionality(); enhanceCinebyVideoPlayer(); } // Initialize when the page is loaded const interval = setInterval(() => { if (document.readyState === 'complete' || document.readyState === 'interactive') { initializeContentEnhancements(); clearInterval(interval); } }, 250); export { detectAndEnhanceContent };