// ==UserScript== // @name X Reels ++ // @namespace http://tampermonkey.net/ // @version 28.8.18 // @description Transforms the X/Twitter feed into a full-screen viewer with keyboard & mouse wheel navigation, smart auto-scroll, playback speed control, and a robust follow-and-return action. // @author Kristijan1001 // @match https://twitter.com/* // @match https://x.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=x.com // @grant none // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/539679/X%20Reels%20%2B%2B.user.js // @updateURL https://update.greasyfork.icu/scripts/539679/X%20Reels%20%2B%2B.meta.js // ==/UserScript== (function() { 'use strict'; class TikTokFeed { constructor() { this.activePost = { id: null, element: null, mediaItems: [], currentMediaIndex: 0 }; this.activePlaceholder = null; this.activeDisplayedMediaElement = null; this.isActive = false; this.isNavigating = false; this.lastScrollTime = 0; this.container = null; this.likeButton = null; this.followButton = null; this.exitButton = null; this.scrollUpButton = null; this.scrollDownButton = null; this.savedScrollPosition = 0; this.mutationObserver = null; this.isReturningFromFollow = false; // Updated: Parse potentially existing smart value (-1) this.autoScrollDelay = parseInt(localStorage.getItem('xreels_autoScrollDelay') || '0', 10); this.autoScrollTimeoutId = null; this.videoEndedListener = null; // New listener for Smart mode this.hideControlsTimeoutId = null; this.hideInfoTimeoutId = null; this.galleryIndicatorTimeout = null; this.savedVolume = parseFloat(localStorage.getItem('xreels_volume') || '0.7'); this.savedMuted = localStorage.getItem('xreels_muted') === 'true'; this.savedPlaybackRate = parseFloat(localStorage.getItem('xreels_playbackRate')) || 1.0; this.boundHandleWheel = null; this.boundHandleKeydown = null; this.boundHandlePopState = null; } init() { console.log('🔥 X Reels Initializing...'); if ('scrollRestoration' in history) { history.scrollRestoration = 'manual'; console.log('History scroll restoration set to manual.'); } this.setupEventListeners(); setTimeout(() => this.addManualTrigger(), 1000); } addManualTrigger() { if (window.self !== window.top) return; if (window.location !== window.parent.location) return; let trigger = document.getElementById('tiktok-trigger'); if (trigger) trigger.remove(); trigger = document.createElement('div'); trigger.id = 'tiktok-trigger'; trigger.innerHTML = `
X Reels
Press X
`; trigger.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 1000000; display: flex; align-items: center; gap: 12px; padding: 14px 20px; background: linear-gradient(135deg, rgba(15, 15, 15, 0.95) 0%, rgba(30, 30, 30, 0.95) 100%); border: 1px solid rgba(255, 0, 80, 0.3); border-radius: 16px; cursor: pointer; user-select: none; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.05) inset, 0 0 20px rgba(255, 0, 80, 0.15); backdrop-filter: blur(20px) saturate(180%); transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); transform: translateY(0) scale(1); opacity: 1; `; const style = document.createElement('style'); style.textContent = ` #tiktok-trigger { animation: slideInDown 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); } #tiktok-trigger .trigger-icon { width: 42px; height: 42px; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #ff0050 0%, #ff4081 100%); border-radius: 10px; flex-shrink: 0; box-shadow: 0 4px 16px rgba(255, 0, 80, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.1) inset; transition: all 0.3s ease; } #tiktok-trigger .trigger-icon svg { color: white; width: 22px; height: 22px; animation: iconPulse 2.5s ease-in-out infinite; } #tiktok-trigger .trigger-content { display: flex; flex-direction: column; gap: 1px; } #tiktok-trigger .trigger-title { color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 15px; font-weight: 700; letter-spacing: 0.5px; line-height: 1.2; } #tiktok-trigger .trigger-subtitle { color: rgba(255, 255, 255, 0.55); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 11px; font-weight: 500; letter-spacing: 0.3px; } #tiktok-trigger:hover { transform: translateY(-3px) scale(1.02); border-color: rgba(255, 0, 80, 0.5); box-shadow: 0 10px 32px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.1) inset, 0 0 25px rgba(255, 0, 80, 0.25); } #tiktok-trigger:hover .trigger-icon { transform: scale(1.08) rotate(5deg); box-shadow: 0 5px 16px rgba(255, 0, 80, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.2) inset; } #tiktok-trigger:hover .trigger-icon svg { animation: iconSpin 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); } #tiktok-trigger:active { transform: translateY(-1px) scale(0.98); transition: all 0.1s ease; } #tiktok-trigger::before { content: ''; position: absolute; inset: -2px; background: linear-gradient(135deg, #ff0050, #ff4081, #ff0050); border-radius: 14px; opacity: 0; transition: opacity 0.3s ease; z-index: -1; filter: blur(8px); } #tiktok-trigger:hover::before { opacity: 0.25; animation: gradientRotate 2s linear infinite; } @keyframes slideInDown { from { opacity: 0; transform: translateY(-30px) scale(0.9); } to { opacity: 1; transform: translateY(0) scale(1); } } @keyframes iconPulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.05); opacity: 0.9; } } @keyframes iconSpin { from { transform: rotate(0deg) scale(1); } to { transform: rotate(360deg) scale(1.08); } } @keyframes gradientRotate { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } @media (max-width: 768px) { #tiktok-trigger { top: 12px; right: 12px; padding: 8px 12px; gap: 8px; } #tiktok-trigger .trigger-icon { width: 32px; height: 32px; } #tiktok-trigger .trigger-icon svg { width: 16px; height: 16px; } #tiktok-trigger .trigger-title { font-size: 12px; } #tiktok-trigger .trigger-subtitle { font-size: 9px; } } `; if (!document.getElementById('xreels-pulse-style')) { style.id = 'xreels-pulse-style'; document.head.appendChild(style); } trigger.addEventListener('click', () => this.startFeed()); document.body.appendChild(trigger); } startFeed() { if (this.isActive) return; document.querySelectorAll('video').forEach(vid => { if (!vid.paused) vid.pause(); vid.muted = true; }); const firstPostWithMedia = this.findCentralMediaArticle(); if (!firstPostWithMedia) { console.error('❌ No media found on screen. Scroll down and try again.'); return; } this.isActive = true; this.createContainer(); this.navigateToPost(firstPostWithMedia); const trigger = document.getElementById('tiktok-trigger'); if (trigger) trigger.style.display = 'none'; } exit() { console.log('👋 Exiting feed...'); this.isActive = false; this.stopAutoScrollTimer(); clearTimeout(this.hideControlsTimeoutId); clearTimeout(this.hideInfoTimeoutId); if (this.activeDisplayedMediaElement && this.activeDisplayedMediaElement.tagName === 'VIDEO') { this.activeDisplayedMediaElement.controls = false; } this.hideLoadingIndicator(); this.restoreOriginalMediaPosition(); if (this.activePost && this.activePost.mediaItems) { this.activePost.mediaItems.forEach(item => { if (item.type === 'video' && item.originalElement && item.placeholder && item.placeholder.parentElement) { item.originalElement.style.cssText = ''; if (item.originalElement.parentNode !== item.placeholder.parentElement) { item.placeholder.parentElement.replaceChild(item.originalElement, item.placeholder); } if (!item.originalElement.paused) { item.originalElement.pause(); } } }); } if (this.container) this.container.remove(); this.container = null; document.body.style.scrollBehavior = 'auto'; const trigger = document.getElementById('tiktok-trigger'); if (trigger) trigger.style.display = 'inline-flex'; this.activePost = { id: null, element: null, mediaItems: [], currentMediaIndex: 0 }; this.activeDisplayedMediaElement = null; this.activePlaceholder = null; this.likeButton = null; this.followButton = null; this.exitButton = null; this.scrollUpButton = null; this.scrollDownButton = null; this.savedScrollPosition = 0; this.disconnectObserver(); this.isReturningFromFollow = false; } createContainer() { if (this.container) this.container.remove(); this.container = document.createElement('div'); this.container.id = 'tiktok-feed-container'; this.container.style.cssText = ` position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.90); z-index: 2147483647; pointer-events: none; overflow: hidden; `; // Insert styles for Fancy Buttons if(!document.getElementById('xreels-fancy-buttons')) { const fancyButtonsStyle = document.createElement('style'); fancyButtonsStyle.id = 'xreels-fancy-buttons'; fancyButtonsStyle.textContent = ` @keyframes pulse-ring { 0% { transform: scale(0.95); opacity: 1; } 50% { transform: scale(1.05); opacity: 0.7; } 100% { transform: scale(1.15); opacity: 0; } } @keyframes pulse-rect { 0% { transform: scale(0.98); opacity: 1; } 50% { transform: scale(1.02); opacity: 0.7; } 100% { transform: scale(1.06); opacity: 0; } } @keyframes heart-beat { 0%, 100% { transform: scale(1); } 25% { transform: scale(1.2); } 50% { transform: scale(1.1); } 75% { transform: scale(1.25); } } @keyframes float-up { 0% { transform: translateY(0) scale(1); opacity: 1; } 100% { transform: translateY(-8px) scale(1.1); opacity: 0; } } @keyframes float-down { 0% { transform: translateY(0) scale(1); opacity: 1; } 100% { transform: translateY(8px) scale(1.1); opacity: 0; } } @keyframes exit-shake { 0%, 100% { transform: translateX(0) rotate(0deg); } 25% { transform: translateX(-3px) rotate(-5deg); } 75% { transform: translateX(3px) rotate(5deg); } } .fancy-exit-btn { position: relative; } .fancy-exit-btn::before { content: ''; position: absolute; inset: 0; border-radius: 16px; background: radial-gradient(circle at center, rgba(255,255,255,0.3) 0%, transparent 70%); opacity: 0; transition: opacity 0.4s ease; pointer-events: none; z-index: 1; } .fancy-exit-btn:hover::before { opacity: 1; } .fancy-exit-btn:hover { transform: translateY(-3px) scale(1.05); background: linear-gradient(135deg, rgba(239, 68, 68, 0.5) 0%, rgba(220, 38, 38, 0.5) 100%) !important; border-color: rgba(239, 68, 68, 0.9) !important; box-shadow: 0 12px 40px rgba(239, 68, 68, 0.5), 0 0 30px rgba(239, 68, 68, 0.3) inset !important; } .fancy-exit-btn:hover svg { animation: exit-shake 0.5s ease; } .fancy-exit-btn:hover .exit-pulse { animation: pulse-rect 1s cubic-bezier(0.4, 0, 0.6, 1) infinite !important; } .fancy-exit-btn:active { transform: translateY(-1px) scale(1.02); transition: all 0.1s ease; } .fancy-action-btn { position: relative; } .fancy-action-btn::before { content: ''; position: absolute; inset: 0; border-radius: 50%; background: radial-gradient(circle at center, rgba(255,255,255,0.3) 0%, transparent 70%); opacity: 0; transition: opacity 0.4s ease; pointer-events: none; z-index: 1; } .fancy-action-btn:hover::before { opacity: 1; } .fancy-action-btn:hover { transform: translateY(-4px) scale(1.08); box-shadow: 0 12px 40px rgba(255, 255, 255, 0.2), 0 0 30px currentColor inset; } .fancy-action-btn:hover .like-icon, .fancy-action-btn:hover .follow-icon, .fancy-action-btn:hover .arrow-icon { transform: scale(1.15); } .fancy-action-btn:active { transform: translateY(-2px) scale(1.02); transition: all 0.1s ease; } #tiktok-like-btn:hover .pulse-ring { animation: pulse-ring 1s cubic-bezier(0.4, 0, 0.6, 1) infinite !important; } #tiktok-scroll-up-btn:hover .arrow-icon { animation: float-up 0.6s ease infinite; } #tiktok-scroll-down-btn:hover .arrow-icon { animation: float-down 0.6s ease infinite; } #tiktok-like-btn.liked .heart-path { fill: #ff1447; stroke: #ff1447; animation: heart-beat 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); transform-origin: center; } #tiktok-like-btn.liked { background: linear-gradient(135deg, rgba(255, 20, 71, 0.5) 0%, rgba(220, 0, 50, 0.5) 100%); border-color: rgba(255, 20, 71, 0.8); box-shadow: 0 12px 40px rgba(255, 20, 71, 0.5), 0 0 30px rgba(255, 20, 71, 0.3) inset; } #tiktok-follow-btn.following .follow-plus-v, #tiktok-follow-btn.following .follow-plus-h { opacity: 0; transform: scale(0); } #tiktok-follow-btn.following { background: linear-gradient(135deg, rgba(29, 161, 242, 0.5) 0%, rgba(26, 140, 216, 0.5) 100%); border-color: rgba(29, 161, 242, 0.8); box-shadow: 0 12px 40px rgba(29, 161, 242, 0.5), 0 0 30px rgba(29, 161, 242, 0.3) inset; } #tiktok-follow-btn.following::after { content: '✓'; position: absolute; top: 50%; right: -4px; transform: translate(0, -50%); font-size: 14px; color: white; font-weight: bold; z-index: 3; text-shadow: 0 2px 8px rgba(0,0,0,0.4); } `; document.head.appendChild(fancyButtonsStyle); } this.container.innerHTML = `
Keyboard Shortcuts
• X: Enter/Exit Feed
• Scroll: ⬆️/⬇️
• Cycle Media: ⬅️/➡️
• Space: Play/Pause
• < / > : Scrub
• L: Like
• F: Follow
• Q: Exit
Auto: Off
`; document.body.appendChild(this.container); this.likeButton = this.container.querySelector('#tiktok-like-btn'); this.followButton = this.container.querySelector('#tiktok-follow-btn'); this.exitButton = this.container.querySelector('#tiktok-exit-btn'); this.scrollUpButton = this.container.querySelector('#tiktok-scroll-up-btn'); this.scrollDownButton = this.container.querySelector('#tiktok-scroll-down-btn'); this.likeButton.addEventListener('click', () => this.toggleLike()); this.followButton.addEventListener('click', () => this.toggleFollow()); this.exitButton.addEventListener('click', () => this.exit()); const infoControlsWrapper = this.container.querySelector('.info-controls-wrapper'); const controlsExpanded = this.container.querySelector('.controls-expanded'); const infoHeader = this.container.querySelector('.info-header'); const infoArrow = infoHeader.querySelector('span'); let isHoveringInfo = false; let isInfoExpanded = false; infoControlsWrapper.addEventListener('mouseenter', () => { isHoveringInfo = true; clearTimeout(this.hideInfoTimeoutId); if (!isInfoExpanded) { controlsExpanded.style.maxHeight = '200px'; controlsExpanded.style.opacity = '1'; controlsExpanded.style.paddingTop = '8px'; infoArrow.style.transform = 'rotate(180deg)'; isInfoExpanded = true; } }); infoControlsWrapper.addEventListener('mouseleave', () => { isHoveringInfo = false; this.hideInfoTimeoutId = setTimeout(() => { if (!isHoveringInfo) { controlsExpanded.style.maxHeight = '0'; controlsExpanded.style.opacity = '0'; controlsExpanded.style.paddingTop = '0'; infoArrow.style.transform = 'rotate(0deg)'; isInfoExpanded = false; } }, 500); }); this.scrollUpButton.addEventListener('click', () => { const mediaNavigated = this.handleMediaNavigation('prev'); if (!mediaNavigated) { this.handlePostNavigation('prev'); } }); this.scrollDownButton.addEventListener('click', () => { const mediaNavigated = this.handleMediaNavigation('next'); if (!mediaNavigated) { this.handlePostNavigation('next'); } }); this.setupAutoScrollControls(); } setupAutoScrollControls() { const container = document.getElementById('xreels-autoscroll-container'); const menu = document.getElementById('xreels-autoscroll-menu'); const display = document.getElementById('xreels-autoscroll-display'); const timeLabels = { 0: 'Off', '-1': 'Auto (Smart)', 1000: '1s', 2000: '2s', 3000: '3s', 5000: '5s', 8000: '8s', 30000: '30s', 60000: '60s' }; display.textContent = timeLabels[this.autoScrollDelay] || 'Off'; container.addEventListener('click', (e) => { e.stopPropagation(); menu.style.display = menu.style.display === 'none' ? 'block' : 'none'; }); // Handle options styling via JS to match functionality without external CSS file document.querySelectorAll('.autoscroll-option').forEach(option => { option.addEventListener('mouseenter', () => { option.style.background = 'rgba(255,255,255,0.1)'; }); option.addEventListener('mouseleave', () => { option.style.background = 'transparent'; }); option.addEventListener('click', (e) => { e.stopPropagation(); const value = parseInt(option.dataset.value, 10); this.autoScrollDelay = value; localStorage.setItem('xreels_autoScrollDelay', value.toString()); display.textContent = timeLabels[value]; menu.style.display = 'none'; if (this.isActive && (value > 0 || value === -1)) { this.startAutoScrollTimer(); } else if (value === 0) { this.stopAutoScrollTimer(); } }); }); document.addEventListener('click', () => { if (menu) menu.style.display = 'none'; }); } startAutoScrollTimer() { this.stopAutoScrollTimer(); if (!this.isActive) return; // SMART MODE (-1) if (this.autoScrollDelay === -1) { if (this.activeDisplayedMediaElement && this.activeDisplayedMediaElement.tagName === 'VIDEO') { // Video: Wait for end const video = this.activeDisplayedMediaElement; this.videoEndedListener = () => { if (!this.isActive) return; const mediaNavigated = this.handleMediaNavigation('next'); if (!mediaNavigated) { this.handlePostNavigation('next'); } }; video.addEventListener('ended', this.videoEndedListener); } else { // Image: Wait 3 seconds this.autoScrollTimeoutId = setTimeout(() => { const mediaNavigated = this.handleMediaNavigation('next'); if (!mediaNavigated) { this.handlePostNavigation('next'); } }, 3000); } } // STANDARD TIMER MODE (> 0) else if (this.autoScrollDelay > 0) { this.autoScrollTimeoutId = setTimeout(() => { const mediaNavigated = this.handleMediaNavigation('next'); if (!mediaNavigated) { this.handlePostNavigation('next'); } }, this.autoScrollDelay); } } stopAutoScrollTimer() { if (this.autoScrollTimeoutId) { clearTimeout(this.autoScrollTimeoutId); this.autoScrollTimeoutId = null; } // Cleanup Video Ended Listener if (this.videoEndedListener && this.activeDisplayedMediaElement) { this.activeDisplayedMediaElement.removeEventListener('ended', this.videoEndedListener); this.videoEndedListener = null; } } async toggleLike() { if (!this.activePost.element) return; this.likeButton.style.transform = 'scale(1.3)'; this.likeButton.style.filter = 'brightness(1.5)'; this.likeButton.style.transition = 'transform 0.1s ease-out, filter 0.1s ease-out'; const likeButton = this.activePost.element.querySelector('[data-testid="like"], [data-testid="unlike"]'); if (likeButton) { likeButton.click(); await new Promise(resolve => setTimeout(resolve, 250)); this.updateLikeButtonState(); } setTimeout(() => { this.likeButton.style.transform = 'scale(1)'; this.likeButton.style.filter = 'brightness(1)'; this.likeButton.style.transition = 'all 0.3s ease'; }, 200); } async toggleFollow() { if (!this.activePost.element) return; const currentMedia = this.activePost.mediaItems[this.activePost.currentMediaIndex]; if (!currentMedia) return; const contextElement = currentMedia.contextElement || this.activePost.element; const originalTweetId = this.activePost.id; this.savedScrollPosition = window.scrollY; this.isReturningFromFollow = true; console.log(`Saved scroll position: ${this.savedScrollPosition} for tweet ${originalTweetId}`); let userLinkToClick = contextElement.querySelector('[data-testid="User-Name"] a'); let clickedQuoteCard = false; const isMainContext = contextElement === this.activePost.element; if (!userLinkToClick && !isMainContext) { console.log("No user link found in context (likely Quote Tweet). Clicking context to navigate to tweet."); userLinkToClick = contextElement.closest('[role="link"]') || contextElement; clickedQuoteCard = true; } if (!userLinkToClick) { if (isMainContext) { console.warn("Could not find user link in main context."); userLinkToClick = this.activePost.element.querySelector('[data-testid="User-Name"] a'); } else { console.warn("Could not find actionable link in Quote Context."); } } if (!userLinkToClick) { console.warn("Could not find user link for follow action."); this.isReturningFromFollow = false; return; } console.log(`Navigating... (QuoteCard: ${clickedQuoteCard})`); userLinkToClick.click(); await new Promise(resolve => setTimeout(resolve, 1500)); let extraDepth = 0; if (clickedQuoteCard) { let tweetUserLink = null; let loadAttempts = 0; while (!tweetUserLink && loadAttempts < 20) { tweetUserLink = document.querySelector('article[data-testid="tweet"] [data-testid="User-Name"] a'); if (!tweetUserLink) await new Promise(r => setTimeout(r, 200)); loadAttempts++; } if (tweetUserLink) { console.log("On Tweet page, clicking profile link..."); tweetUserLink.click(); await new Promise(resolve => setTimeout(resolve, 1500)); extraDepth = 1; } else { console.warn("Could not find profile link on Tweet page."); } } let followBtn = null; let attempts = 0; const maxAttempts = 15; while (!followBtn && attempts < maxAttempts) { followBtn = document.querySelector('[data-testid*="follow"], [aria-label*="Follow"], [aria-label*="Unfollow"]'); if (!followBtn) { await new Promise(resolve => setTimeout(resolve, 200)); } attempts++; } if (followBtn) { followBtn.click(); await new Promise(resolve => setTimeout(resolve, 500)); } else { console.warn("Follow/Unfollow button not found on profile page."); } if (clickedQuoteCard && extraDepth > 0) { window.history.go(-2); } else { window.history.back(); } await new Promise(resolve => setTimeout(resolve, 1500)); this.showLoadingIndicator('Restoring feed position...'); let targetPostElement = null; attempts = 0; const checkInterval = 200; const maxChecks = 25; while (!targetPostElement && attempts < maxChecks) { targetPostElement = document.querySelector(`article[role="article"] a[href*="/status/${originalTweetId}"]`)?.closest('article[role="article"]'); if (!targetPostElement) { await new Promise(resolve => setTimeout(resolve, checkInterval)); } attempts++; } if (targetPostElement) { console.log(`Found original tweet element (ID: ${originalTweetId}), re-navigating...`); this.navigateToPost(targetPostElement); } else { console.warn(`Original tweet element (ID: ${originalTweetId}) not found after returning. Falling back to central post.`); this.navigateToPost(this.findCentralMediaArticle()); } window.scrollTo(0, this.savedScrollPosition); this.hideLoadingIndicator(); this.isReturningFromFollow = false; } updateLikeButtonState() { if (!this.likeButton || !this.activePost.element) return; const likedButton = this.activePost.element.querySelector('[data-testid="unlike"]'); if (likedButton) { this.likeButton.classList.add('liked'); const heartPath = this.likeButton.querySelector('.heart-path'); if (heartPath) { heartPath.setAttribute('fill', '#ff1447'); heartPath.setAttribute('stroke', '#ff1447'); } } else { this.likeButton.classList.remove('liked'); const heartPath = this.likeButton.querySelector('.heart-path'); if (heartPath) { heartPath.setAttribute('fill', 'none'); heartPath.setAttribute('stroke', 'white'); } } } updateFollowButtonState() { if (!this.followButton || !this.activePost.element) return; const currentMedia = this.activePost.mediaItems[this.activePost.currentMediaIndex]; if (!currentMedia) return; const contextElement = currentMedia.contextElement || this.activePost.element; let isFollowingTargetAuthor = false; const mainAuthorUnfollowBtn = contextElement.querySelector('[data-testid$="-unfollow"]'); if (mainAuthorUnfollowBtn) { isFollowingTargetAuthor = true; } else { const mainAuthorFollowingText = Array.from(contextElement.querySelectorAll('[data-testid="User-Name"] ~ div button')).some(btn => { const text = btn.textContent.trim(); return text === 'Following' || text === 'Unfollow'; }); if (mainAuthorFollowingText) { isFollowingTargetAuthor = true; } } if (isFollowingTargetAuthor) { this.followButton.classList.add('following'); } else { this.followButton.classList.remove('following'); } } updateUI() { if (!this.container || !this.activePost || !this.activePost.element) return; const currentMedia = this.activePost.mediaItems[this.activePost.currentMediaIndex]; if (!currentMedia) return; const authorName = currentMedia.author || 'Unknown User'; const authorHandle = currentMedia.handle; const tweetText = currentMedia.text || ''; this.container.querySelector('.info').innerHTML = `
${authorName}
${tweetText}
`; const authorElement = this.container.querySelector('.author-name'); if (authorElement && authorHandle) { authorElement.addEventListener('click', (e) => { e.stopPropagation(); window.open(`https://x.com/${authorHandle}`, '_blank'); }); } const galleryIndicator = this.container.querySelector('.gallery-indicator'); if (this.activePost.mediaItems.length > 1) { galleryIndicator.textContent = `${this.activePost.currentMediaIndex + 1} / ${this.activePost.mediaItems.length}`; galleryIndicator.style.display = 'block'; galleryIndicator.style.opacity = '1'; if (this.galleryIndicatorTimeout) { clearTimeout(this.galleryIndicatorTimeout); } this.galleryIndicatorTimeout = setTimeout(() => { galleryIndicator.style.opacity = '0'; }, 1500); } else { galleryIndicator.style.display = 'none'; } this.updateLikeButtonState(); this.updateFollowButtonState(); } showLoadingIndicator(message = 'Loading...') { if (this.container) { const indicator = this.container.querySelector('.loading-indicator'); if(indicator) { indicator.textContent = message; indicator.style.display = 'block'; } } } hideLoadingIndicator() { if (this.container) { const indicator = this.container.querySelector('.loading-indicator'); if(indicator) indicator.style.display = 'none'; } } navigateToPost(targetArticleElement) { if (!targetArticleElement || this.isNavigating) return; this.isNavigating = true; this.stopAutoScrollTimer(); this.restoreOriginalMediaPosition(); this.showLoadingIndicator('Loading...'); const postTweetId = this.generateTweetId(targetArticleElement); const foundMediaItems = []; const addedElements = new Set(); const extractMetadata = (contextEl, isMain) => { let author = 'Unknown User'; let handle = ''; let text = ''; const authorEl = contextEl.querySelector('[data-testid="User-Name"] [dir="ltr"]'); if (authorEl) { author = authorEl.textContent; } else if (isMain) { const mainAuthor = targetArticleElement.querySelector('[data-testid="User-Name"] [dir="ltr"]'); if (mainAuthor) author = mainAuthor.textContent; } const linkEl = contextEl.querySelector('[data-testid="User-Name"] a'); if (linkEl) { handle = linkEl.getAttribute('href'); } else if (isMain) { const mainLink = targetArticleElement.querySelector('[data-testid="User-Name"] a'); if (mainLink) handle = mainLink.getAttribute('href'); } else if (!isMain && contextEl.getAttribute('role') === 'link') { const quoteHref = contextEl.getAttribute('href'); if (quoteHref) { const match = quoteHref.match(/^\/([^/]+)\/status\//); if (match && match[1]) { handle = '/' + match[1]; } } } if (!handle) { const userInfo = contextEl.querySelector('[data-testid="User-Name"]'); if (userInfo) { const content = userInfo.textContent; const match = content.match(/@([a-zA-Z0-9_]+)/); if (match) { handle = '/' + match[1]; } } } if (handle && handle.startsWith('/')) handle = handle.substring(1); const textEl = contextEl.querySelector('[data-testid="tweetText"]'); if (textEl) { if (isMain) { const quoteWrapper = textEl.closest('div[role="link"]'); if (quoteWrapper && quoteWrapper !== contextEl && targetArticleElement.contains(quoteWrapper)) { } } text = textEl.textContent; } return { author, handle, text }; }; const resolveContext = (mediaEl) => { let current = mediaEl.parentElement; while (current && current !== targetArticleElement) { if (current.getAttribute('role') === 'link' && current.querySelector('[data-testid="User-Name"]')) { return current; } current = current.parentElement; } return targetArticleElement; }; targetArticleElement.querySelectorAll('div[data-testid="videoPlayer"] video, div[data-testid="tweetPhoto"] img').forEach(mediaEl => { if (addedElements.has(mediaEl)) return; const contextElement = resolveContext(mediaEl); const isMain = contextElement === targetArticleElement; const { author, handle, text } = extractMetadata(contextElement, isMain); if (mediaEl.tagName === 'VIDEO' && this.isValidMedia(mediaEl)) { foundMediaItems.push({ type: 'video', originalElement: mediaEl, src: mediaEl.src, placeholder: null, contextElement: contextElement, author: author, handle: handle, text: text }); addedElements.add(mediaEl); } else if (mediaEl.tagName === 'IMG' && !mediaEl.src.includes('video_thumb') && this.isValidMedia(mediaEl)) { foundMediaItems.push({ type: 'image', originalElement: mediaEl, src: this.getFullQualityImageUrl(mediaEl), placeholder: null, contextElement: contextElement, author: author, handle: handle, text: text }); addedElements.add(mediaEl); } }); if (foundMediaItems.length === 0) { console.warn('No valid media found in the target article, skipping to next.'); this.isNavigating = false; this.hideLoadingIndicator(); this.handlePostNavigation('next'); return; } this.activePost = { id: postTweetId, element: targetArticleElement, mediaItems: foundMediaItems, currentMediaIndex: 0 }; document.body.style.scrollBehavior = 'auto'; this.attemptScrollToPost(targetArticleElement); setTimeout(() => { this.displayCurrentMediaItem(); this.hideLoadingIndicator(); this.isNavigating = false; }, 650); } async attemptScrollToPost(element, attempts = 0, maxAttempts = 10, interval = 100) { if (!element || attempts >= maxAttempts) { console.warn('Failed to scroll post into view after multiple attempts.'); return; } element.scrollIntoView({ behavior: 'auto', block: 'center' }); await new Promise(resolve => setTimeout(resolve, interval)); if (!this.isElementInViewport(element)) { this.attemptScrollToPost(element, attempts + 1, maxAttempts, interval); } } isElementInViewport(el) { if (!el) return false; const rect = el.getBoundingClientRect(); const margin = 50; return ( rect.top >= -margin && rect.left >= -margin && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) + margin && rect.right <= (window.innerWidth || document.documentElement.clientWidth) + margin ); } displayCurrentMediaItem() { if (!this.activePost || !this.activePost.mediaItems || this.activePost.mediaItems.length === 0) return; this.restoreOriginalMediaPosition(); const mediaItemToDisplay = this.activePost.mediaItems[this.activePost.currentMediaIndex]; if (!mediaItemToDisplay) return; this.updateUI(); const mediaWrapper = this.container.querySelector('.media-wrapper'); mediaWrapper.innerHTML = ''; if (mediaItemToDisplay.type === 'video') { this.displayVideo(mediaItemToDisplay); } else { this.displayImage(mediaItemToDisplay); } this.startAutoScrollTimer(); } displayVideo(mediaItem) { const mediaWrapper = this.container.querySelector('.media-wrapper'); const videoElement = mediaItem.originalElement; if (!mediaItem.placeholder) { const placeholder = document.createElement('div'); const rect = videoElement.getBoundingClientRect(); placeholder.style.width = `${rect.width}px`; placeholder.style.height = `${rect.height}px`; mediaItem.placeholder = placeholder; if (videoElement.parentElement) { videoElement.parentElement.replaceChild(placeholder, videoElement); } } this.activeDisplayedMediaElement = videoElement; const videoContainer = document.createElement('div'); videoContainer.className = 'custom-video-container'; videoContainer.style.cssText = ` position: relative; width: 100%; height: 100%; background: black; display: flex; align-items: center; justify-content: center; cursor: pointer; `; videoElement.style.cssText = ` width: 100%; height: 100%; object-fit: contain; border-radius: 0; display: block; background: black; `; videoElement.loop = false; // Important: Disable loop for Smart Autoscroll to work videoElement.muted = true; videoElement.controls = false; videoElement.volume = this.savedVolume; videoElement.playbackRate = this.savedPlaybackRate; const controlsOverlay = document.createElement('div'); controlsOverlay.className = 'video-controls-overlay'; controlsOverlay.style.cssText = ` position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(to top, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.4) 50%, transparent 100%); padding: 12px 30px 12px 12px; opacity: 0; transition: opacity 0.3s ease; pointer-events: all; z-index: 1000010; `; const progressContainer = document.createElement('div'); progressContainer.style.cssText = ` width: 100%; height: 14px; background: transparent; border-radius: 10px; margin-bottom: 10px; cursor: pointer; pointer-events: all; position: relative; display: flex; align-items: center; padding: 5px 0; `; const progressBackground = document.createElement('div'); progressBackground.style.cssText = ` width: 100%; height: 4px; background: rgba(255,255,255,0.3); border-radius: 2px; position: relative; `; const progressBar = document.createElement('div'); progressBar.className = 'progress-fill'; progressBar.style.cssText = ` height: 100%; background: linear-gradient(90deg, #FF6F61, #E63946); border-radius: 3px; width: 0%; transition: width 0.1s ease; position: relative; `; const progressThumb = document.createElement('div'); progressThumb.style.cssText = ` position: absolute; right: -5px; top: 50%; transform: translateY(-50%); width: 10px; height: 10px; background: #FF6F61; border-radius: 50%; opacity: 0; transition: opacity 0.3s ease; box-shadow: 0 2px 8px rgba(0,0,0,0.3); `; progressBar.appendChild(progressThumb); progressBackground.appendChild(progressBar); progressContainer.appendChild(progressBackground); const controlsContainer = document.createElement('div'); controlsContainer.style.cssText = ` display: flex; align-items: center; gap: 15px; pointer-events: all; `; const playButton = document.createElement('button'); playButton.innerHTML = ` `; playButton.style.cssText = ` background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.2); color: white; font-size: 16px; cursor: pointer; padding: 0; border-radius: 50%; transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; position: relative; overflow: hidden; backdrop-filter: blur(10px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); flex-shrink: 0; `; const playButtonStyle = document.createElement('style'); playButtonStyle.textContent = ` .custom-video-container button:hover { background: rgba(255,255,255,0.25) !important; border-color: rgba(255,255,255,0.4) !important; transform: scale(1.1); box-shadow: 0 6px 20px rgba(0,0,0,0.4) !important; } .custom-video-container button:active { transform: scale(0.95); } .custom-video-container button::before { content: ''; position: absolute; inset: 0; border-radius: 50%; background: radial-gradient(circle, rgba(255,255,255,0.3) 0%, transparent 70%); opacity: 0; transition: opacity 0.3s ease; pointer-events: none; } .custom-video-container button:hover::before { opacity: 1; } .play-icon, .pause-icon { transition: all 0.3s ease; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3)); } .video-controls-overlay:hover .progress-fill div { opacity: 1; } .custom-video-container:hover .video-controls-overlay { opacity: 1; } `; document.head.appendChild(playButtonStyle); const timeDisplay = document.createElement('div'); timeDisplay.style.cssText = ` color: white; font-size: 11px; font-weight: 600; min-width: 70px; text-shadow: 0 1px 2px rgba(0,0,0,0.8); `; timeDisplay.textContent = '0:00 / 0:00'; const speedButton = document.createElement('button'); speedButton.innerHTML = '1.0x'; speedButton.style.cssText = ` background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.2); color: white; font-size: 11px; font-weight: 600; cursor: pointer; padding: 4px 8px; border-radius: 12px; transition: all 0.3s ease; min-width: 42px; text-align: center; flex-shrink: 0; backdrop-filter: blur(10px); `; speedButton.title = 'Playback Speed'; const volumeContainer = document.createElement('div'); volumeContainer.style.cssText = ` display: flex; align-items: center; gap: 8px; margin-left: auto; margin-right: 25px; flex-shrink: 0; `; const muteButton = document.createElement('button'); muteButton.innerHTML = '🔇'; muteButton.style.cssText = ` background: none; border: none; color: white; font-size: 14px; cursor: pointer; padding: 4px; border-radius: 4px; transition: background 0.3s ease; flex-shrink: 0; `; const volumeSlider = document.createElement('input'); volumeSlider.type = 'range'; volumeSlider.min = '0'; volumeSlider.max = '100'; volumeSlider.step = '1'; volumeSlider.value = (this.savedVolume * 100).toString(); volumeSlider.style.cssText = ` width: 70px; height: 4px; background: rgba(255,255,255,0.3); border-radius: 2px; outline: none; cursor: pointer; -webkit-appearance: none; appearance: none; opacity: 0.5; flex-shrink: 0; `; const volumeSliderStyle = document.createElement('style'); volumeSliderStyle.textContent = ` .custom-video-container input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 12px; height: 12px; border-radius: 50%; background: #FF6F61; cursor: pointer; box-shadow: 0 2px 4px rgba(0,0,0,0.3); } .custom-video-container input[type="range"]::-moz-range-thumb { width: 12px; height: 12px; border-radius: 50%; background: #FF6F61; cursor: pointer; border: none; box-shadow: 0 2px 4px rgba(0,0,0,0.3); } `; document.head.appendChild(volumeSliderStyle); volumeContainer.appendChild(muteButton); volumeContainer.appendChild(volumeSlider); controlsContainer.appendChild(playButton); controlsContainer.appendChild(timeDisplay); controlsContainer.appendChild(speedButton); controlsContainer.appendChild(volumeContainer); controlsOverlay.appendChild(progressContainer); controlsOverlay.appendChild(controlsContainer); videoContainer.appendChild(videoElement); videoContainer.appendChild(controlsOverlay); mediaWrapper.appendChild(videoContainer); let isDragging = false; const updatePlayPauseIcon = () => { const playIcon = playButton.querySelector('.play-icon'); const pauseIcon = playButton.querySelector('.pause-icon'); if (playIcon && pauseIcon) { if (videoElement.paused) { playIcon.style.display = 'block'; pauseIcon.style.display = 'none'; } else { playIcon.style.display = 'none'; pauseIcon.style.display = 'block'; } } }; const updateControlsUI = () => { muteButton.innerHTML = videoElement.muted ? '🔇' : '🔊'; volumeSlider.style.opacity = videoElement.muted ? '0.5' : '1'; volumeSlider.value = (videoElement.volume * 100).toString(); updatePlayPauseIcon(); }; const togglePlayPause = () => { if (videoElement.paused) { videoElement.play().catch(err => { console.warn('Play failed:', err); this.attemptVideoPlay(videoElement); }); } else { videoElement.pause(); } }; playButton.addEventListener('click', (e) => { e.stopPropagation(); togglePlayPause(); }); videoElement.addEventListener('click', togglePlayPause); const toggleMute = () => { videoElement.muted = !videoElement.muted; this.savedMuted = videoElement.muted; localStorage.setItem('xreels_muted', this.savedMuted.toString()); }; videoElement.addEventListener('dblclick', (e) => { e.stopPropagation(); toggleMute(); }); muteButton.addEventListener('click', (e) => { e.stopPropagation(); toggleMute(); }); volumeSlider.addEventListener('input', (e) => { e.stopPropagation(); const newVolume = parseFloat(e.target.value) / 100; videoElement.volume = newVolume; videoElement.muted = false; this.savedVolume = newVolume; localStorage.setItem('xreels_volume', this.savedVolume.toString()); this.savedMuted = false; localStorage.setItem('xreels_muted', 'false'); if (newVolume === 0 && !videoElement.muted) { videoElement.muted = true; this.savedMuted = true; localStorage.setItem('xreels_muted', 'true'); } }); // UPDATED: Extended speed options const speedOptions = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.5, 3, 3.5, 4]; speedButton.addEventListener('click', (e) => { e.stopPropagation(); const currentIndex = speedOptions.indexOf(this.savedPlaybackRate); const nextIndex = (currentIndex + 1) % speedOptions.length; this.savedPlaybackRate = speedOptions[nextIndex]; videoElement.playbackRate = this.savedPlaybackRate; speedButton.innerHTML = this.savedPlaybackRate.toFixed(2) + 'x'; localStorage.setItem('xreels_playbackRate', this.savedPlaybackRate.toString()); }); const updateProgress = () => { if (!isDragging && videoElement.duration) { const progress = (videoElement.currentTime / videoElement.duration) * 100; progressBar.style.width = progress + '%'; } }; const updateTime = () => { const formatTime = (seconds) => { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; }; const current = formatTime(videoElement.currentTime || 0); const total = formatTime(videoElement.duration || 0); timeDisplay.textContent = `${current} / ${total}`; }; videoElement.addEventListener('timeupdate', () => { updateProgress(); updateTime(); }); videoElement.addEventListener('loadedmetadata', () => { updateTime(); videoElement.playbackRate = this.savedPlaybackRate; }); videoElement.addEventListener('loadeddata', () => { updateControlsUI(); videoElement.playbackRate = this.savedPlaybackRate; }); // UPDATED: Aggressive Speed Enforcement // Fixes issue where speed resets to 1.0x when videos loop or reload const enforceSpeed = () => { if (Math.abs(videoElement.playbackRate - this.savedPlaybackRate) > 0.01) { videoElement.playbackRate = this.savedPlaybackRate; } }; videoElement.addEventListener('ratechange', enforceSpeed); videoElement.addEventListener('playing', enforceSpeed); videoElement.addEventListener('play', enforceSpeed); const handleProgressClick = (e) => { const rect = progressBackground.getBoundingClientRect(); const pos = (e.clientX - rect.left) / rect.width; if (videoElement.duration) { videoElement.currentTime = pos * videoElement.duration; } }; progressContainer.addEventListener('mousedown', (e) => { e.stopPropagation(); isDragging = true; handleProgressClick(e); const handleMouseMove = (e) => { if (isDragging) { handleProgressClick(e); } }; const handleMouseUp = () => { isDragging = false; document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }); let isHoveringControls = false; const controlsHoverZone = 150; videoContainer.addEventListener('mousemove', (e) => { const rect = videoContainer.getBoundingClientRect(); const distanceFromBottom = rect.bottom - e.clientY; if (distanceFromBottom <= controlsHoverZone) { if (!isHoveringControls) { isHoveringControls = true; controlsOverlay.style.opacity = '1'; } } else { if (isHoveringControls) { isHoveringControls = false; if (!isDragging) { controlsOverlay.style.opacity = '0'; } } } }); videoContainer.addEventListener('mouseleave', () => { isHoveringControls = false; if (!isDragging) { controlsOverlay.style.opacity = '0'; } }); updateControlsUI(); videoElement.addEventListener('volumechange', updateControlsUI); videoElement.addEventListener('play', updatePlayPauseIcon); videoElement.addEventListener('pause', updatePlayPauseIcon); videoElement.playbackRate = this.savedPlaybackRate; speedButton.innerHTML = this.savedPlaybackRate.toFixed(2) + 'x'; this.attemptVideoPlay(videoElement); } async simulateTwitterPlayClick(vidEl) { const videoPlayerContainer = vidEl.closest('div[data-testid="videoPlayer"]'); if (videoPlayerContainer) { let playButton = videoPlayerContainer.querySelector( 'svg[role="img"][aria-label="Play"], ' + 'div[role="button"][tabindex="0"][aria-label*="Play"], ' + 'div[data-testid="playButton"], ' + 'div[data-testid="SensitiveMediaDisclosure"] button, ' + 'div[data-testid="videoPlayer"] > div > div > div[tabindex="0"], ' + 'div[data-testid="tweetPhoto"] div[role="button"], ' + '[data-testid="ShyPinButton"], ' + 'div[data-testid="play-pause-button"]' ); if (!playButton) { playButton = videoPlayerContainer; console.log('No specific play button found, falling back to clicking video container.'); } if (playButton) { console.log('Attempting to simulate click on Twitter\'s video player button/container:', playButton); playButton.click(); await new Promise(resolve => setTimeout(resolve, 100)); return true; } } return false; } async attemptVideoPlay(video) { let playAttempts = 0; const maxPlayAttempts = 15; const attemptInterval = 500; while (playAttempts < maxPlayAttempts) { if (!this.isActive || this.activeDisplayedMediaElement !== video) { console.log('Video no longer active or feed exited, stopping autoplay attempts.'); video.pause(); return; } if (!video.paused && video.muted === this.savedMuted) { console.log('Video is playing with correct mute state. Autoplay successful.'); return; } console.log(`Autoplay attempt ${playAttempts + 1}/${maxPlayAttempts} for video. State: paused=${video.paused}, muted=${video.muted}`); if (video.paused) { try { if (video.currentTime >= video.duration - 0.5) { video.currentTime = 0; } video.muted = true; await video.play(); console.log('Direct muted play attempt successful.'); video.muted = this.savedMuted; video.volume = this.savedVolume; video.playbackRate = this.savedPlaybackRate; video.dispatchEvent(new Event('volumechange')); } catch (error) { console.warn(`Direct play failed (attempt ${playAttempts + 1}):`, error.name, error.message); await this.simulateTwitterPlayClick(video); } } else { video.muted = this.savedMuted; video.playbackRate = this.savedPlaybackRate; video.dispatchEvent(new Event('volumechange')); } playAttempts++; await new Promise(resolve => setTimeout(resolve, attemptInterval)); } console.warn('All aggressive autoplay attempts exhausted. Video might require manual interaction.'); video.controls = true; video.muted = this.savedMuted; video.volume = this.savedVolume; video.playbackRate = this.savedPlaybackRate; video.dispatchEvent(new Event('volumechange')); } displayImage(mediaItem) { const mediaWrapper = this.container.querySelector('.media-wrapper'); const img = document.createElement('img'); img.src = mediaItem.src; img.style.cssText = ` width: 100%; height: 100%; object-fit: contain; border-radius: 0; background: black; `; mediaWrapper.appendChild(img); this.activeDisplayedMediaElement = img; } restoreOriginalMediaPosition() { if (this.activeDisplayedMediaElement) { if (this.activeDisplayedMediaElement.tagName === 'VIDEO' || this.activeDisplayedMediaElement.tagName === 'IMG') { this.activeDisplayedMediaElement.style.cssText = ''; const currentMediaItemObject = this.activePost.mediaItems.find(item => item.originalElement === this.activeDisplayedMediaElement); if (currentMediaItemObject && currentMediaItemObject.placeholder && currentMediaItemObject.placeholder.parentNode) { currentMediaItemObject.placeholder.parentElement.replaceChild(this.activeDisplayedMediaElement, currentMediaItemObject.placeholder); } else if (this.activeDisplayedMediaElement.parentNode) { this.activeDisplayedMediaElement.remove(); } } } this.activeDisplayedMediaElement = null; } handlePostNavigation(direction) { if (this.isNavigating) return; let nextPostElement = null; if (direction === 'prev') { nextPostElement = this.findNextMediaArticle(this.activePost.element, true); } else { nextPostElement = this.findNextMediaArticle(this.activePost.element, false); } if (nextPostElement) { this.navigateToPost(nextPostElement); } else { this.showLoadingIndicator('No more visible posts. Scrolling to load more...'); window.scrollTo({ top: document.body.scrollHeight, behavior: 'auto' }); setTimeout(() => { this.hideLoadingIndicator(); const newlyLoadedPost = this.findNextMediaArticle(this.activePost.element, false); if (newlyLoadedPost && newlyLoadedPost !== this.activePost.element) { this.navigateToPost(newlyLoadedPost); } else { console.log('No new media posts found after scrolling.'); } }, 2000); } } handleMediaNavigation(direction) { if (this.isNavigating || !this.activePost || !this.activePost.mediaItems || this.activePost.mediaItems.length <= 1) { return false; } let newMediaIndex = this.activePost.currentMediaIndex; let mediaNavigated = false; if (direction === 'next') { if (newMediaIndex < this.activePost.mediaItems.length - 1) { newMediaIndex++; this.activePost.currentMediaIndex = newMediaIndex; this.displayCurrentMediaItem(); mediaNavigated = true; } } else if (direction === 'prev') { if (newMediaIndex > 0) { newMediaIndex--; this.activePost.currentMediaIndex = newMediaIndex; this.displayCurrentMediaItem(); mediaNavigated = true; } } return mediaNavigated; } findNextMediaArticle(currentArticle, findPrevious = false) { const allArticles = Array.from(document.querySelectorAll('article[role="article"]')); const currentIndex = currentArticle ? allArticles.indexOf(currentArticle) : -1; let startIndex = findPrevious ? currentIndex - 1 : currentIndex + 1; let endIndex = findPrevious ? -1 : allArticles.length; let step = findPrevious ? -1 : 1; for (let i = startIndex; findPrevious ? i >= endIndex : i < endIndex; i += step) { const article = allArticles[i]; if (article.querySelector('div[data-testid="videoPlayer"] video, div[data-testid="tweetPhoto"] img')) { const mediaEls = article.querySelectorAll('div[data-testid="videoPlayer"] video, div[data-testid="tweetPhoto"] img'); for (const el of mediaEls) { if (this.isValidMedia(el)) { return article; } } } } return null; } findCentralMediaArticle() { const allArticles = Array.from(document.querySelectorAll('article[role="article"]')); const viewportHeight = window.innerHeight || document.documentElement.clientHeight; const viewportCenter = viewportHeight / 2; let closestArticle = null; let minDistance = Infinity; for (const article of allArticles) { const mediaElements = article.querySelectorAll('div[data-testid="videoPlayer"] video, div[data-testid="tweetPhoto"] img'); let hasValidMedia = false; for (const el of mediaElements) { if (this.isValidMedia(el)) { hasValidMedia = true; break; } } if (hasValidMedia) { const rect = article.getBoundingClientRect(); if (rect.bottom > 0 && rect.top < viewportHeight) { const articleCenter = rect.top + rect.height / 2; const distance = Math.abs(viewportCenter - articleCenter); if (distance < minDistance) { minDistance = distance; closestArticle = article; } } } } return closestArticle; } getFullQualityImageUrl(imgElement) { if (!imgElement || !imgElement.src) return null; let src = imgElement.src; if (src.includes('pbs.twimg.com/media/')) { const url = new URL(src); url.searchParams.set('name', 'orig'); src = url.href; } return src; } generateTweetId(tweet) { if (!tweet) return null; const link = tweet.querySelector('a[href*="/status/"][dir="ltr"], a[href*="/status/"]'); if (link) { const match = link.href.match(/\/status\/(\d+)/); if (match && match[1]) return match[1]; } return null; } isValidMedia(element) { if (!element) return false; if (element.tagName === 'VIDEO') { const container = element.closest('div[data-testid="videoPlayer"]'); return container && element.readyState >= 0; } else if (element.tagName === 'IMG') { return element.src && element.src.includes('pbs.twimg.com/media/') && !element.src.includes('video_thumb'); } return false; } setupEventListeners() { if (this.boundHandleWheel) { document.removeEventListener('wheel', this.boundHandleWheel, { passive: false, capture: true }); document.removeEventListener('keydown', this.boundHandleKeydown, { capture: true }); window.removeEventListener('popstate', this.boundHandlePopState); } this.boundHandleWheel = this.handleWheel.bind(this); this.boundHandleKeydown = this.handleKeydown.bind(this); this.boundHandlePopState = this.handlePopState.bind(this); document.addEventListener('wheel', this.boundHandleWheel, { passive: false, capture: true }); document.addEventListener('keydown', this.boundHandleKeydown, { capture: true }); window.addEventListener('popstate', this.boundHandlePopState); } handleWheel(e) { if (!this.isActive) return; e.preventDefault(); e.stopPropagation(); if (Date.now() - this.lastScrollTime < 300) return; this.lastScrollTime = Date.now(); const direction = e.deltaY > 0 ? 'next' : 'prev'; const mediaNavigated = this.handleMediaNavigation(direction); if (!mediaNavigated) { this.handlePostNavigation(direction); } } handleKeydown(e) { if (e.key.toLowerCase() === 'x') { e.preventDefault(); e.stopImmediatePropagation(); if (this.isActive) { this.exit(); } else { this.startFeed(); } return; } if (!this.isActive) { return; } if (document.activeElement && (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA')) { if (e.key.toLowerCase() === 'q') { e.preventDefault(); e.stopImmediatePropagation(); this.exit(); } return; } if (e.key === ' ') { e.preventDefault(); e.stopPropagation(); if (this.activeDisplayedMediaElement && this.activeDisplayedMediaElement.tagName === 'VIDEO') { if (this.activeDisplayedMediaElement.paused) { this.activeDisplayedMediaElement.play(); } else { this.activeDisplayedMediaElement.pause(); } } return; } if (this.activeDisplayedMediaElement && this.activeDisplayedMediaElement.tagName === 'VIDEO') { const video = this.activeDisplayedMediaElement; const scrubAmount = 5; if (e.key === ',' || e.key === '<') { e.preventDefault(); e.stopPropagation(); video.currentTime = Math.max(0, video.currentTime - scrubAmount); return; } else if (e.key === '.' || e.key === '>') { e.preventDefault(); e.stopPropagation(); video.currentTime = Math.min(video.duration, video.currentTime + scrubAmount); return; } } const keyMap = { 'q': () => this.exit(), 'Q': () => this.exit(), 'ArrowUp': () => { e.preventDefault(); e.stopPropagation(); this.handlePostNavigation('prev'); }, 'ArrowDown': () => { e.preventDefault(); e.stopPropagation(); this.handlePostNavigation('next'); }, 'ArrowLeft': () => { e.preventDefault(); e.stopPropagation(); this.handleMediaNavigation('prev'); }, 'ArrowRight': () => { e.preventDefault(); e.stopPropagation(); this.handleMediaNavigation('next'); }, 'l': () => { e.preventDefault(); e.stopPropagation(); this.toggleLike(); }, 'L': () => { e.preventDefault(); e.stopPropagation(); this.toggleLike(); }, 'f': () => { e.preventDefault(); e.stopPropagation(); this.toggleFollow(); }, 'F': () => { e.preventDefault(); e.stopPropagation(); this.toggleFollow(); } }; if (keyMap[e.key]) { e.preventDefault(); e.stopImmediatePropagation(); keyMap[e.key](); } } handlePopState() { if (this.isActive && this.isReturningFromFollow) { console.log('Popstate detected, returning from follow action.'); this.hideLoadingIndicator(); this.isReturningFromFollow = false; } } disconnectObserver() { if (this.mutationObserver) { this.mutationObserver.disconnect(); this.mutationObserver = null; console.log('MutationObserver disconnected.'); } } } function onReady() { if (window.tikTokFeedInstance) { console.log('Existing TikTok Feed instance found, cleaning up...'); if (window.tikTokFeedInstance.boundHandleWheel) { document.removeEventListener('wheel', window.tikTokFeedInstance.boundHandleWheel, { passive: false, capture: true }); document.removeEventListener('keydown', window.tikTokFeedInstance.boundHandleKeydown, { capture: true }); window.removeEventListener('popstate', window.tikTokFeedInstance.boundHandlePopState); } if (window.tikTokFeedInstance.isActive) { window.tikTokFeedInstance.exit(); } if (window.tikTokFeedInstance.activeDisplayedMediaElement && window.tikTokFeedInstance.activeDisplayedMediaElement.tagName === 'VIDEO') { window.tikTokFeedInstance.activeDisplayedMediaElement.pause(); window.tikTokFeedInstance.activeDisplayedMediaElement.muted = true; } window.tikTokFeedInstance.disconnectObserver(); window.tikTokFeedInstance = null; console.log('Previous TikTok Feed instance cleaned up.'); } window.tikTokFeedInstance = new TikTokFeed(); window.tikTokFeedInstance.init(); } if (document.readyState === 'complete' || document.readyState === 'interactive') { onReady(); } else { document.addEventListener('DOMContentLoaded', onReady); } })();