@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
JavaScript
/**
* 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
};