您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Content-aware copy button with video change detection, visual feedback, robust homepage support, enhanced download UI with beautiful hover menu, niches support, and search page support
// ==UserScript== // @name RedGifs Copy Video Embed Link & Download // @namespace http://tampermonkey.net/ // @version 5.7 // @description Content-aware copy button with video change detection, visual feedback, robust homepage support, enhanced download UI with beautiful hover menu, niches support, and search page support // @author monk3xx3 // @match https://redgifs.com/* // @match https://www.redgifs.com/* // @grant GM_xmlhttpRequest // @license MIT // ==/UserScript== (function() { 'use strict'; let copyButton = null; let downloadButton = null; let downloadDropdown = null; let isProcessing = false; let isDownloading = false; let currentVideoId = null; let bearerToken = null; let tokenExpiry = null; let activeVideoElement = null; let videoObserver = null; let highlightTimeout = null; let dropdownTimeout = null; let isDropdownVisible = false; // Enhanced video waiting with retry logic function waitForVideosOnHomepage(maxAttempts = 10, delay = 500) { return new Promise((resolve) => { let attempts = 0; function checkForVideos() { attempts++; const videos = document.querySelectorAll('video'); const hasVideos = videos.length > 0; // Check if we're on homepage, niches, search, or other video pages const isValidVideoPage = window.location.pathname === '/' || window.location.pathname === '' || window.location.pathname === '/home' || window.location.pathname.startsWith('/niches/') || window.location.pathname.startsWith('/search/') || (window.location.hostname.includes('redgifs.com') && window.location.pathname.length <= 1); if (hasVideos || attempts >= maxAttempts) { console.log(`Videos found: ${hasVideos}, Attempts: ${attempts}`); resolve(hasVideos); return; } // If no videos yet and we're on a valid page, keep trying if (isValidVideoPage) { console.log(`Waiting for videos... Attempt ${attempts}/${maxAttempts}`); setTimeout(checkForVideos, delay); } else { resolve(false); } } checkForVideos(); }); } // Enhanced page detection including niches and search pages function isValidPageWithContent() { const path = window.location.pathname; const isValidPage = path === '/' || path === '' || path === '/home' || path.startsWith('/niches/') || path.startsWith('/users/') || path.startsWith('/watch/') || path.startsWith('/search/'); const hasVideoContent = document.querySelector('video') || document.querySelector('[class*="video"]') || document.querySelector('[class*="gif"]') || document.querySelector('[data-testid*="video"]') || document.querySelector('[class*="post"]') || document.querySelector('[class*="content"]') || document.querySelector('[class*="search"]'); return isValidPage && hasVideoContent; } // Enhanced video detection focused on content, not URL function detectCurrentVideo() { try { const videos = document.querySelectorAll('video'); let bestVideo = null; let bestScore = 0; for (const video of videos) { const rect = video.getBoundingClientRect(); const viewportHeight = window.innerHeight; const viewportWidth = window.innerWidth; // Calculate visibility score const visibleHeight = Math.min(rect.bottom, viewportHeight) - Math.max(rect.top, 0); const visibleWidth = Math.min(rect.right, viewportWidth) - Math.max(rect.left, 0); const visibleArea = Math.max(0, visibleHeight) * Math.max(0, visibleWidth); const totalArea = rect.width * rect.height; if (totalArea === 0) continue; const visibilityRatio = visibleArea / totalArea; const sizeScore = Math.min(rect.width / 100, 10) + Math.min(rect.height / 100, 10); const centerDistance = Math.abs(rect.top + rect.height/2 - viewportHeight/2); const centerScore = Math.max(0, 10 - centerDistance / 100); // Enhanced scoring for niches, user pages, and search pages const score = visibilityRatio * 20 + sizeScore + centerScore + (video.readyState >= 3 ? 5 : 0) + (!video.paused ? 10 : 0); if (score > bestScore && visibilityRatio > 0.2 && rect.width > 150) { bestScore = score; bestVideo = video; } } return bestVideo; } catch (error) { console.error('Error detecting current video:', error); return null; } } // Detect available video qualities with estimated file sizes function detectAvailableQualities(videoElement) { const qualities = { sd: { available: false, size: '~2-5 MB', label: 'Standard' }, hd: { available: false, size: '~8-15 MB', label: 'High Quality' } }; if (!videoElement) return qualities; try { // Check video dimensions const width = videoElement.videoWidth || videoElement.clientWidth; const height = videoElement.videoHeight || videoElement.clientHeight; // Always assume SD is available qualities.sd.available = true; // Check if HD is available based on dimensions if (width >= 1280 || height >= 720) { qualities.hd.available = true; } // Check video source URL for quality indicators const src = videoElement.src || videoElement.currentSrc; if (src && src.includes('hd')) { qualities.hd.available = true; } // Estimate file sizes based on duration if available if (videoElement.duration && !isNaN(videoElement.duration)) { const duration = videoElement.duration; qualities.sd.size = `~${Math.round(duration * 0.3)} MB`; qualities.hd.size = `~${Math.round(duration * 1.2)} MB`; } } catch (error) { console.error('Error detecting video qualities:', error); qualities.sd.available = true; // fallback } return qualities; } // Extract video ID from the active video element and surrounding context function extractVideoIdFromContent(videoElement) { if (!videoElement) return null; try { // Method 1: Check video source URLs const sources = [videoElement.src, videoElement.currentSrc]; sources.push(...Array.from(videoElement.querySelectorAll('source')).map(s => s.src)); for (const src of sources) { if (src && !src.startsWith('blob:')) { const match = src.match(/\/([a-zA-Z0-9]{15,})(?:-silent)?\.(?:mp4|webm)/i); if (match) return match[1]; } } // Method 2: Check data attributes on video and parent elements let element = videoElement; let depth = 0; while (element && depth < 10) { // Check all data attributes for (const attr of element.attributes || []) { if (attr.name.includes('id') || attr.name.includes('video') || attr.name.includes('gif')) { const match = attr.value.match(/([a-zA-Z0-9]{15,})/); if (match && match[1].length >= 15) return match[1]; } } // Check class names for video IDs if (element.className) { const match = element.className.match(/([a-zA-Z0-9]{15,})/); if (match && match[1].length >= 15) return match[1]; } element = element.parentElement; depth++; } // Method 3: Look for links and video info in the container const container = videoElement.closest('[class*="video"], [class*="post"], [class*="content"], [class*="item"], [class*="card"]') || videoElement.parentElement; if (container) { // Find watch links const links = container.querySelectorAll('a[href*="watch"], a[href*="/watch/"]'); for (const link of links) { const match = link.href.match(/\/watch\/([a-zA-Z0-9]+)/); if (match) return match[1]; } // Find any elements with video ID patterns const allElements = container.querySelectorAll('*'); for (const el of allElements) { for (const attr of el.attributes || []) { if (attr.value.match(/^[a-zA-Z0-9]{15,}$/)) { return attr.value; } } } } // Method 4: Parse nearby script content for video data const nearbyScripts = document.querySelectorAll('script:not([src])'); for (const script of nearbyScripts) { const content = script.textContent; // Look for video IDs in various JSON structures const patterns = [ /"id":\s*"([a-zA-Z0-9]{15,})"/g, /"video":\s*{[^}]*"id":\s*"([^"]{15,})"/g, /"gif":\s*{[^}]*"id":\s*"([^"]{15,})"/g, /([a-zA-Z0-9]{15,})(?:-silent)?\.mp4/g ]; for (const pattern of patterns) { const matches = [...content.matchAll(pattern)]; if (matches.length > 0) { // Return the most recent match (likely the current video) return matches[matches.length - 1][1]; } } } } catch (error) { console.error('Error extracting video ID:', error); } return null; } // Get current video ID with enhanced detection function getCurrentVideoId() { const activeVideo = detectCurrentVideo(); if (activeVideo !== activeVideoElement) { // Video changed, trigger highlight if (activeVideoElement !== null) { highlightButtonChange(); } activeVideoElement = activeVideo; } if (!activeVideo) return null; const videoId = extractVideoIdFromContent(activeVideo); // Fallback: try URL-based detection only if content detection fails if (!videoId) { const urlMatch = window.location.pathname.match(/\/watch\/([^\/\?#]+)/); if (urlMatch) return urlMatch[1]; } return videoId; } // Highlight button when video changes function highlightButtonChange() { if (!copyButton || isProcessing) return; clearTimeout(highlightTimeout); // Add highlight effect copyButton.style.background = 'rgba(59, 130, 246, 0.9)'; copyButton.style.borderColor = 'rgba(59, 130, 246, 0.8)'; copyButton.style.transform = 'translateY(-2px) scale(1.05)'; copyButton.style.boxShadow = '0 8px 25px rgba(59, 130, 246, 0.4)'; // Update text to show video detected const span = copyButton.querySelector('span'); if (span) { const originalText = span.textContent; span.textContent = 'Video Detected'; highlightTimeout = setTimeout(() => { copyButton.style.background = 'rgba(0, 0, 0, 0.85)'; copyButton.style.borderColor = 'rgba(255, 255, 255, 0.2)'; copyButton.style.transform = 'translateY(0) scale(1)'; copyButton.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.3)'; span.textContent = originalText; }, 1500); } } // Extract bearer token from network requests function extractBearerToken() { return new Promise((resolve) => { const originalFetch = window.fetch; let captured = false; window.fetch = function(...args) { const result = originalFetch.apply(this, args); if (args[1] && args[1].headers && args[1].headers.Authorization) { const token = args[1].headers.Authorization.replace('Bearer ', ''); if (token && !captured) { bearerToken = token; captured = true; try { const payload = JSON.parse(atob(token.split('.')[1])); tokenExpiry = payload.exp * 1000; } catch (e) { tokenExpiry = Date.now() + (24 * 60 * 60 * 1000); } console.log('Bearer token captured via fetch'); resolve(token); } } return result; }; // Also monitor XHR requests const originalXHR = XMLHttpRequest.prototype.setRequestHeader; XMLHttpRequest.prototype.setRequestHeader = function(name, value) { if (name.toLowerCase() === 'authorization' && value.startsWith('Bearer ') && !captured) { const token = value.replace('Bearer ', ''); bearerToken = token; captured = true; try { const payload = JSON.parse(atob(token.split('.')[1])); tokenExpiry = payload.exp * 1000; } catch (e) { tokenExpiry = Date.now() + (24 * 60 * 60 * 1000); } console.log('Bearer token captured via XHR'); resolve(token); } return originalXHR.call(this, name, value); }; // Timeout after 5 seconds setTimeout(() => { if (!captured) { console.log('No token captured, attempting page interaction...'); // Try to trigger network requests const videos = document.querySelectorAll('video'); if (videos.length > 0) { videos[0].currentTime = videos[0].currentTime; // Trigger a tiny seek } setTimeout(() => resolve(bearerToken), 2000); } }, 5000); }); } // Get signed URLs from RedGifs API async function getSignedUrls(videoId) { if (!bearerToken || Date.now() > tokenExpiry) { await extractBearerToken(); if (!bearerToken) { throw new Error('Authentication failed - no bearer token'); } } try { const response = await fetch(`https://api.redgifs.com/v2/gifs/${videoId}?views=yes&users=yes`, { headers: { 'Authorization': `Bearer ${bearerToken}`, 'User-Agent': navigator.userAgent, 'Accept': 'application/json' } }); if (!response.ok) { throw new Error(`API Error: ${response.status} ${response.statusText}`); } const data = await response.json(); const urls = data.gif?.urls; if (!urls) { throw new Error('No video URLs in API response'); } return urls; } catch (error) { console.error('API request failed:', error); throw error; } } // Download video to internal storage using GM_xmlhttpRequest function downloadVideoFile(url, filename) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'blob', headers: { 'User-Agent': navigator.userAgent, 'Referer': 'https://redgifs.com/' }, onload: function(response) { try { // Create blob from response const blob = new Blob([response.response], { type: 'video/mp4' }); // Create download link const downloadUrl = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = downloadUrl; link.download = filename; link.style.display = 'none'; // Trigger download document.body.appendChild(link); link.click(); document.body.removeChild(link); // Clean up setTimeout(() => URL.revokeObjectURL(downloadUrl), 1000); resolve(); } catch (error) { reject(error); } }, onerror: function(error) { reject(new Error('Download failed: ' + error.statusText)); }, onprogress: function(progress) { if (progress.lengthComputable) { const percent = Math.round((progress.loaded / progress.total) * 100); console.log(`Download progress: ${percent}%`); } } }); }); } // Create beautiful download dropdown menu with enhanced UI function createDownloadDropdown() { if (downloadDropdown) return downloadDropdown; downloadDropdown = document.createElement('div'); downloadDropdown.id = 'redgifs-download-dropdown'; // Create dropdown content with better structure downloadDropdown.innerHTML = ` <div class="dropdown-header"> <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"> <path d="M8 12.793L5.646 10.44a.5.5 0 01.708-.708L7.5 10.879V1.5a.5.5 0 011 0v9.379l1.146-1.147a.5.5 0 01.708.708L8 12.793z"/> <path d="M2 14.5a.5.5 0 01.5-.5h11a.5.5 0 010 1h-11a.5.5 0 01-.5-.5z"/> </svg> <span>Download Options</span> </div> <div class="dropdown-separator"></div> <div class="dropdown-item recommended" data-quality="sd"> <div class="item-icon"> <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"> <path d="M6.002 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/> <path d="M2.002 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2h-12zm12 1a1 1 0 0 1 1 1v6.5l-3.777-1.947a.5.5 0 0 0-.577.093l-3.71 3.71-2.66-1.772a.5.5 0 0 0-.63.062L1.002 12V3a1 1 0 0 1 1-1h12z"/> </svg> </div> <div class="item-content"> <div class="item-title">Standard Quality</div> <div class="item-subtitle">Discord friendly • Fast download</div> <div class="item-size">~2-5 MB</div> </div> <div class="item-badge">Recommended</div> </div> <div class="dropdown-item" data-quality="hd"> <div class="item-icon"> <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"> <path d="M6.002 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/> <path d="M1.5 2A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2h-13zm13 1a.5.5 0 0 1 .5.5v6l-3.775-1.947a.5.5 0 0 0-.577.093l-3.71 3.71-2.66-1.772a.5.5 0 0 0-.63.062L1.002 12V3.5a.5.5 0 0 1 .5-.5h13z"/> <circle cx="10.5" cy="8.5" r="1.5" fill="currentColor"/> </svg> </div> <div class="item-content"> <div class="item-title">High Quality</div> <div class="item-subtitle">Best resolution • Larger file</div> <div class="item-size">~8-15 MB</div> </div> </div> `; downloadDropdown.style.cssText = ` position: fixed; bottom: 70px; right: 62px; z-index: 10001; background: rgba(15, 15, 15, 0.98); border: 1px solid rgba(255, 255, 255, 0.12); border-radius: 12px; backdrop-filter: blur(16px); box-shadow: 0 12px 32px rgba(0, 0, 0, 0.6), 0 2px 8px rgba(0, 0, 0, 0.4); display: none; flex-direction: column; min-width: 240px; max-width: 280px; overflow: hidden; transform: translateY(10px) scale(0.95); opacity: 0; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; `; // Enhanced CSS styles for dropdown if (!document.getElementById('redgifs-dropdown-styles')) { const style = document.createElement('style'); style.id = 'redgifs-dropdown-styles'; style.textContent = ` /* Dropdown Header */ #redgifs-download-dropdown .dropdown-header { display: flex; align-items: center; gap: 8px; padding: 12px 16px 8px 16px; color: rgba(255, 255, 255, 0.9); font-size: 13px; font-weight: 600; letter-spacing: -0.01em; } /* Separator */ #redgifs-download-dropdown .dropdown-separator { height: 1px; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.08), transparent); margin: 0 12px 8px 12px; } /* Dropdown Items */ #redgifs-download-dropdown .dropdown-item { display: flex; align-items: center; gap: 12px; padding: 12px 16px; color: white; cursor: pointer; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); user-select: none; border: none; background: none; width: 100%; text-align: left; position: relative; margin: 2px 8px; border-radius: 8px; } #redgifs-download-dropdown .dropdown-item:hover { background: rgba(255, 255, 255, 0.08); transform: translateX(2px); } #redgifs-download-dropdown .dropdown-item.recommended { background: linear-gradient(135deg, rgba(34, 197, 94, 0.12), rgba(34, 197, 94, 0.08)); border: 1px solid rgba(34, 197, 94, 0.2); } #redgifs-download-dropdown .dropdown-item.recommended:hover { background: linear-gradient(135deg, rgba(34, 197, 94, 0.18), rgba(34, 197, 94, 0.12)); border-color: rgba(34, 197, 94, 0.3); transform: translateX(2px); } /* Item Icons */ #redgifs-download-dropdown .item-icon { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; background: rgba(255, 255, 255, 0.08); border-radius: 8px; color: rgba(255, 255, 255, 0.8); flex-shrink: 0; transition: all 0.2s ease; } #redgifs-download-dropdown .dropdown-item:hover .item-icon { background: rgba(255, 255, 255, 0.12); color: rgba(255, 255, 255, 0.95); transform: scale(1.05); } #redgifs-download-dropdown .dropdown-item.recommended .item-icon { background: rgba(34, 197, 94, 0.15); color: rgba(34, 197, 94, 0.9); } #redgifs-download-dropdown .dropdown-item.recommended:hover .item-icon { background: rgba(34, 197, 94, 0.25); color: rgb(34, 197, 94); } /* Item Content */ #redgifs-download-dropdown .item-content { flex: 1; display: flex; flex-direction: column; gap: 2px; } #redgifs-download-dropdown .item-title { font-size: 14px; font-weight: 600; color: rgba(255, 255, 255, 0.95); line-height: 1.2; letter-spacing: -0.01em; } #redgifs-download-dropdown .item-subtitle { font-size: 12px; font-weight: 400; color: rgba(255, 255, 255, 0.65); line-height: 1.3; } #redgifs-download-dropdown .item-size { font-size: 11px; font-weight: 500; color: rgba(255, 255, 255, 0.5); margin-top: 2px; font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; } /* Recommended Badge */ #redgifs-download-dropdown .item-badge { font-size: 10px; font-weight: 600; color: rgb(34, 197, 94); background: rgba(34, 197, 94, 0.15); padding: 3px 8px; border-radius: 12px; letter-spacing: 0.5px; text-transform: uppercase; border: 1px solid rgba(34, 197, 94, 0.2); flex-shrink: 0; } /* Disabled State */ #redgifs-download-dropdown .dropdown-item[disabled] { opacity: 0.4; cursor: not-allowed; pointer-events: none; } /* Show Animation */ #redgifs-download-dropdown.show { transform: translateY(0) scale(1); opacity: 1; } /* Responsive Adjustments */ @media (max-width: 768px) { #redgifs-download-dropdown { min-width: 220px; max-width: 260px; border-radius: 10px; } #redgifs-download-dropdown .dropdown-item { padding: 10px 14px; } #redgifs-download-dropdown .item-title { font-size: 13px; } #redgifs-download-dropdown .item-subtitle { font-size: 11px; } } `; document.head.appendChild(style); } // Add click handlers downloadDropdown.addEventListener('click', handleDropdownClick); return downloadDropdown; } // Handle dropdown item clicks async function handleDropdownClick(event) { const item = event.target.closest('.dropdown-item'); if (!item || isDownloading || item.hasAttribute('disabled')) return; const quality = item.dataset.quality; hideDropdown(); await performDownload(quality); } // Show dropdown with enhanced animations and better UX function showDropdown() { if (!downloadDropdown || isDownloading || isDropdownVisible) return; clearTimeout(dropdownTimeout); isDropdownVisible = true; // Update dropdown based on available qualities const activeVideo = detectCurrentVideo(); const qualities = detectAvailableQualities(activeVideo); const sdItem = downloadDropdown.querySelector('[data-quality="sd"]'); const hdItem = downloadDropdown.querySelector('[data-quality="hd"]'); // Update file size information if (sdItem) { const sizeSpan = sdItem.querySelector('.item-size'); if (sizeSpan) sizeSpan.textContent = qualities.sd.size; sdItem.style.display = qualities.sd.available ? 'flex' : 'none'; } if (hdItem) { const sizeSpan = hdItem.querySelector('.item-size'); if (sizeSpan) sizeSpan.textContent = qualities.hd.size; hdItem.style.display = qualities.hd.available ? 'flex' : 'none'; if (!qualities.hd.available) { hdItem.setAttribute('disabled', 'true'); } else { hdItem.removeAttribute('disabled'); } } // Responsive positioning const mediaQuery = window.matchMedia('(max-width: 768px)'); if (mediaQuery.matches) { downloadDropdown.style.bottom = '60px'; downloadDropdown.style.right = '47px'; } else { downloadDropdown.style.bottom = '70px'; downloadDropdown.style.right = '62px'; } // Show with animation downloadDropdown.style.display = 'flex'; requestAnimationFrame(() => { downloadDropdown.classList.add('show'); }); } // Hide dropdown with smooth animation function hideDropdown() { if (!downloadDropdown || !isDropdownVisible) return; dropdownTimeout = setTimeout(() => { downloadDropdown.classList.remove('show'); isDropdownVisible = false; setTimeout(() => { if (!isDropdownVisible) { downloadDropdown.style.display = 'none'; } }, 250); }, 150); } // Create fixed download button without conflicting elements function createDownloadButton() { if (downloadButton) return downloadButton; downloadButton = document.createElement('button'); downloadButton.id = 'redgifs-download-btn'; // Create icon container separately to avoid innerHTML conflicts const iconContainer = document.createElement('div'); iconContainer.id = 'download-icon-container'; iconContainer.innerHTML = ` <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"> <path d="M8 12.793L5.646 10.44a.5.5 0 01.708-.708L7.5 10.879V1.5a.5.5 0 011 0v9.379l1.146-1.147a.5.5 0 01.708.708L8 12.793z"/> <path d="M2 14.5a.5.5 0 01.5-.5h11a.5.5 0 010 1h-11a.5.5 0 01-.5-.5z"/> </svg> `; downloadButton.appendChild(iconContainer); downloadButton.style.cssText = ` position: fixed; bottom: 70px; right: 20px; z-index: 10000; background: linear-gradient(135deg, rgba(0, 0, 0, 0.9), rgba(20, 20, 20, 0.85)); color: white; border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 10px; padding: 10px; font-size: 12px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); backdrop-filter: blur(12px); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.2); user-select: none; outline: none; width: 36px; height: 36px; min-width: 36px; min-height: 36px; `; // Enhanced hover effects and dropdown handling downloadButton.addEventListener('mouseenter', () => { if (!isDownloading) { downloadButton.style.transform = 'translateY(-2px) scale(1.05)'; downloadButton.style.background = 'linear-gradient(135deg, rgba(20, 20, 20, 0.95), rgba(40, 40, 40, 0.9))'; downloadButton.style.borderColor = 'rgba(255, 255, 255, 0.25)'; downloadButton.style.boxShadow = '0 8px 24px rgba(0, 0, 0, 0.4), 0 2px 8px rgba(0, 0, 0, 0.3)'; clearTimeout(dropdownTimeout); setTimeout(showDropdown, 300); } }); downloadButton.addEventListener('mouseleave', () => { if (!isDownloading) { downloadButton.style.transform = 'translateY(0) scale(1)'; downloadButton.style.background = 'linear-gradient(135deg, rgba(0, 0, 0, 0.9), rgba(20, 20, 20, 0.85))'; downloadButton.style.borderColor = 'rgba(255, 255, 255, 0.15)'; downloadButton.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.2)'; hideDropdown(); } }); // Handle click for default SD download downloadButton.addEventListener('click', () => performDownload('sd')); return downloadButton; } // Update icon content during download states function updateDownloadButtonIcon(content) { const iconContainer = downloadButton?.querySelector('#download-icon-container'); if (iconContainer) { iconContainer.innerHTML = content; } } // Enhanced download functionality with better visual feedback async function performDownload(quality = 'sd') { if (isDownloading) return; isDownloading = true; try { // Enhanced loading animation updateDownloadButtonIcon(` <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"> <circle cx="12" cy="12" r="3" fill="currentColor"> <animate attributeName="r" values="3;6;3" dur="1s" repeatCount="indefinite"/> <animate attributeName="opacity" values="1;0.3;1" dur="1s" repeatCount="indefinite"/> </circle> <circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" stroke-width="2" opacity="0.3"> <animate attributeName="stroke-dasharray" values="0 50;25 25;0 50" dur="1.5s" repeatCount="indefinite"/> <animateTransform attributeName="transform" type="rotate" values="0 12 12;360 12 12" dur="2s" repeatCount="indefinite"/> </circle> </svg> `); downloadButton.style.background = 'linear-gradient(135deg, rgba(59, 130, 246, 0.9), rgba(79, 70, 229, 0.8))'; downloadButton.style.borderColor = 'rgba(59, 130, 246, 0.6)'; const videoId = getCurrentVideoId(); if (!videoId) { throw new Error('Could not detect current video'); } console.log(`Downloading video ${videoId} in ${quality.toUpperCase()} quality`); const urls = await getSignedUrls(videoId); if (!urls) { throw new Error('Could not get video URLs from API'); } // Choose URL based on requested quality with SD fallback let downloadUrl; if (quality === 'hd' && urls.hd) { downloadUrl = urls.hd; } else if (urls.sd) { downloadUrl = urls.sd; } else { downloadUrl = urls.hd || urls.thumbnail; } if (!downloadUrl) { throw new Error('No suitable video URL found'); } // Generate filename const filename = `redgifs_${videoId}_${quality}.mp4`; // Download video to internal storage await downloadVideoFile(downloadUrl, filename); // Enhanced success feedback updateDownloadButtonIcon(` <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"> <path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/> </svg> `); downloadButton.style.background = 'linear-gradient(135deg, rgba(34, 197, 94, 0.9), rgba(21, 128, 61, 0.8))'; downloadButton.style.borderColor = 'rgba(34, 197, 94, 0.6)'; downloadButton.style.boxShadow = '0 8px 24px rgba(34, 197, 94, 0.3), 0 2px 8px rgba(34, 197, 94, 0.2)'; setTimeout(() => { updateDownloadButtonIcon(` <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"> <path d="M8 12.793L5.646 10.44a.5.5 0 01.708-.708L7.5 10.879V1.5a.5.5 0 011 0v9.379l1.146-1.147a.5.5 0 01.708.708L8 12.793z"/> <path d="M2 14.5a.5.5 0 01.5-.5h11a.5.5 0 010 1h-11a.5.5 0 01-.5-.5z"/> </svg> `); downloadButton.style.background = 'linear-gradient(135deg, rgba(0, 0, 0, 0.9), rgba(20, 20, 20, 0.85))'; downloadButton.style.borderColor = 'rgba(255, 255, 255, 0.15)'; downloadButton.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.2)'; isDownloading = false; }, 2500); } catch (error) { console.error('Download failed:', error); // Enhanced error feedback updateDownloadButtonIcon(` <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"> <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/> <path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/> </svg> `); downloadButton.style.background = 'linear-gradient(135deg, rgba(239, 68, 68, 0.9), rgba(185, 28, 28, 0.8))'; downloadButton.style.borderColor = 'rgba(239, 68, 68, 0.6)'; downloadButton.style.boxShadow = '0 8px 24px rgba(239, 68, 68, 0.3), 0 2px 8px rgba(239, 68, 68, 0.2)'; setTimeout(() => { updateDownloadButtonIcon(` <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"> <path d="M8 12.793L5.646 10.44a.5.5 0 01.708-.708L7.5 10.879V1.5a.5.5 0 011 0v9.379l1.146-1.147a.5.5 0 01.708.708L8 12.793z"/> <path d="M2 14.5a.5.5 0 01.5-.5h11a.5.5 0 010 1h-11a.5.5 0 01-.5-.5z"/> </svg> `); downloadButton.style.background = 'linear-gradient(135deg, rgba(0, 0, 0, 0.9), rgba(20, 20, 20, 0.85))'; downloadButton.style.borderColor = 'rgba(255, 255, 255, 0.15)'; downloadButton.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.2)'; isDownloading = false; }, 3500); } } // Create enhanced copy button function createCopyButton() { if (copyButton) return copyButton; copyButton = document.createElement('button'); copyButton.id = 'redgifs-content-copy-btn'; copyButton.innerHTML = ` <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"> <path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/> <path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/> </svg> <span>Copy Video</span> `; copyButton.style.cssText = ` position: fixed; bottom: 20px; right: 20px; z-index: 10000; background: rgba(0, 0, 0, 0.85); color: white; border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 8px; padding: 10px 14px; font-size: 13px; font-weight: 500; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; cursor: pointer; display: flex; align-items: center; gap: 6px; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); backdrop-filter: blur(8px); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); user-select: none; outline: none; min-width: 110px; justify-content: center; `; // Responsive design for copy button only const mediaQuery = window.matchMedia('(max-width: 768px)'); function applyResponsiveStyles() { if (mediaQuery.matches) { copyButton.style.bottom = '15px'; copyButton.style.right = '15px'; copyButton.style.padding = '8px 12px'; copyButton.style.fontSize = '12px'; copyButton.style.minWidth = '90px'; // Adjust download button position on mobile if (downloadButton) { downloadButton.style.bottom = '60px'; downloadButton.style.right = '15px'; } } else { copyButton.style.bottom = '20px'; copyButton.style.right = '20px'; copyButton.style.padding = '10px 14px'; copyButton.style.fontSize = '13px'; copyButton.style.minWidth = '110px'; // Adjust download button position on desktop if (downloadButton) { downloadButton.style.bottom = '70px'; downloadButton.style.right = '20px'; } } } applyResponsiveStyles(); mediaQuery.addListener(applyResponsiveStyles); // Enhanced hover effects copyButton.addEventListener('mouseenter', () => { if (!isProcessing) { copyButton.style.background = 'rgba(0, 0, 0, 0.95)'; copyButton.style.borderColor = 'rgba(255, 255, 255, 0.4)'; copyButton.style.transform = 'translateY(-2px)'; copyButton.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.4)'; } }); copyButton.addEventListener('mouseleave', () => { if (!isProcessing) { copyButton.style.background = 'rgba(0, 0, 0, 0.85)'; copyButton.style.borderColor = 'rgba(255, 255, 255, 0.2)'; copyButton.style.transform = 'translateY(0)'; copyButton.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.3)'; } }); copyButton.addEventListener('click', handleCopyClick); return copyButton; } // Enhanced copy handler async function handleCopyClick(event) { event.preventDefault(); event.stopPropagation(); if (isProcessing) return; isProcessing = true; const originalContent = copyButton.innerHTML; try { copyButton.innerHTML = ` <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"> <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"/> <path d="M12 2 A10 10 0 0 1 22 12" stroke="currentColor" stroke-width="2" fill="none"> <animateTransform attributeName="transform" type="rotate" values="0 12 12;360 12 12" dur="1s" repeatCount="indefinite"/> </path> </svg> <span>Getting URL...</span> `; copyButton.style.background = 'rgba(64, 64, 64, 0.9)'; const videoId = getCurrentVideoId(); if (!videoId) { throw new Error('Could not detect current video'); } if (videoId === currentVideoId) { console.log('Same video detected:', videoId); } else { console.log('New video detected:', videoId); currentVideoId = videoId; } const urls = await getSignedUrls(videoId); if (!urls) { throw new Error('Could not get signed URLs from API'); } const signedUrl = urls.hd || urls.sd || urls.thumbnail || null; if (!signedUrl) { throw new Error('No valid signed URL found'); } // Copy to clipboard let copySuccess = false; if (navigator.clipboard && window.isSecureContext) { try { await navigator.clipboard.writeText(signedUrl); copySuccess = true; } catch (clipboardError) { console.warn('Clipboard API failed, using fallback'); } } if (!copySuccess) { const textArea = document.createElement('textarea'); textArea.value = signedUrl; textArea.style.cssText = 'position:fixed;top:-999px;opacity:0;pointer-events:none;'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); copySuccess = document.execCommand('copy'); document.body.removeChild(textArea); } if (copySuccess) { copyButton.innerHTML = ` <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"> <path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/> </svg> <span>Copied! (1h)</span> `; copyButton.style.background = 'rgba(34, 197, 94, 0.9)'; copyButton.style.borderColor = 'rgba(34, 197, 94, 0.6)'; setTimeout(() => { copyButton.innerHTML = originalContent; copyButton.style.background = 'rgba(0, 0, 0, 0.85)'; copyButton.style.borderColor = 'rgba(255, 255, 255, 0.2)'; isProcessing = false; }, 3000); } else { throw new Error('Copy to clipboard failed'); } } catch (error) { console.error('Copy operation failed:', error); copyButton.innerHTML = ` <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"> <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/> <path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/> </svg> <span>Failed</span> `; copyButton.style.background = 'rgba(239, 68, 68, 0.9)'; copyButton.style.borderColor = 'rgba(239, 68, 68, 0.6)'; setTimeout(() => { copyButton.innerHTML = originalContent; copyButton.style.background = 'rgba(0, 0, 0, 0.85)'; copyButton.style.borderColor = 'rgba(255, 255, 255, 0.2)'; isProcessing = false; }, 4000); } } // Setup comprehensive video monitoring function setupVideoMonitoring() { // Intersection observer for video visibility changes if (videoObserver) { videoObserver.disconnect(); } videoObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting && entry.intersectionRatio > 0.3) { // Video became prominently visible setTimeout(() => { const newVideoId = getCurrentVideoId(); if (newVideoId && newVideoId !== currentVideoId) { currentVideoId = newVideoId; console.log('Video changed via intersection:', newVideoId); } }, 100); } }); }, { threshold: [0.3, 0.5, 0.7], rootMargin: '0px' }); // Observe all videos document.querySelectorAll('video').forEach(video => { videoObserver.observe(video); }); // Scroll monitoring with throttling let scrollTimeout; const scrollHandler = () => { clearTimeout(scrollTimeout); scrollTimeout = setTimeout(() => { const newVideoId = getCurrentVideoId(); if (newVideoId && newVideoId !== currentVideoId) { currentVideoId = newVideoId; console.log('Video changed via scroll:', newVideoId); } }, 150); }; window.addEventListener('scroll', scrollHandler, { passive: true }); // Monitor for new videos being added const domObserver = new MutationObserver((mutations) => { let hasNewVideos = false; mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { if (node.tagName === 'VIDEO' || node.querySelector?.('video')) { hasNewVideos = true; if (node.tagName === 'VIDEO') { videoObserver.observe(node); } else { node.querySelectorAll('video').forEach(v => videoObserver.observe(v)); } } } }); }); if (hasNewVideos) { setTimeout(() => { const newVideoId = getCurrentVideoId(); if (newVideoId && newVideoId !== currentVideoId) { currentVideoId = newVideoId; console.log('Video changed via DOM update:', newVideoId); } }, 300); } }); domObserver.observe(document.body, { childList: true, subtree: true }); } // Enhanced button state management with niches and search support async function updateButtonState() { console.log('Updating button state...', window.location.pathname); // Enhanced page detection including niches and search pages const isValidPage = window.location.pathname.startsWith('/users/') || window.location.pathname.startsWith('/watch/') || window.location.pathname.startsWith('/niches/') || window.location.pathname.startsWith('/search/') || window.location.pathname === '/' || window.location.pathname === '' || window.location.pathname === '/home' || isValidPageWithContent(); if (isValidPage) { // Wait for videos to load const hasVideos = await waitForVideosOnHomepage(); if (hasVideos) { // Create and add both buttons and dropdown if (!copyButton || !document.body.contains(copyButton)) { const copyBtn = createCopyButton(); document.body.appendChild(copyBtn); console.log('Copy button created and added to page'); } if (!downloadButton || !document.body.contains(downloadButton)) { const downloadBtn = createDownloadButton(); document.body.appendChild(downloadBtn); console.log('Download button created and added to page'); } if (!downloadDropdown || !document.body.contains(downloadDropdown)) { const dropdown = createDownloadDropdown(); document.body.appendChild(dropdown); // Setup dropdown hover handling dropdown.addEventListener('mouseenter', () => { clearTimeout(dropdownTimeout); isDropdownVisible = true; }); dropdown.addEventListener('mouseleave', hideDropdown); console.log('Download dropdown created and added to page'); } copyButton.style.display = 'flex'; downloadButton.style.display = 'flex'; // Initialize video monitoring setupVideoMonitoring(); // Get initial video ID with delay to ensure content is ready setTimeout(() => { const initialVideoId = getCurrentVideoId(); if (initialVideoId) { currentVideoId = initialVideoId; console.log('Initial video detected:', initialVideoId); } else { console.log('No initial video detected, will monitor for changes'); } }, 1000); } else { console.log('No videos found after waiting period'); if (copyButton) { copyButton.style.display = 'none'; } if (downloadButton) { downloadButton.style.display = 'none'; } if (downloadDropdown) { downloadDropdown.style.display = 'none'; } } } else { if (copyButton) { copyButton.style.display = 'none'; } if (downloadButton) { downloadButton.style.display = 'none'; } if (downloadDropdown) { downloadDropdown.style.display = 'none'; } } } // Enhanced initialization with multiple attempts async function initialize() { console.log('RedGifs Content-Aware Copy Button & Download initializing...'); // Extract token early with more aggressive retry setTimeout(extractBearerToken, 500); // Initial button state update await updateButtonState(); // Monitor URL changes for SPA navigation with faster polling let currentUrl = window.location.href; const urlCheckInterval = setInterval(async () => { if (window.location.href !== currentUrl) { currentUrl = window.location.href; console.log('URL changed, updating button state'); setTimeout(async () => await updateButtonState(), 300); } }, 250); // Faster polling for better responsiveness // Handle page visibility changes document.addEventListener('visibilitychange', async () => { if (!document.hidden) { setTimeout(async () => await updateButtonState(), 200); } }); // Add additional MutationObserver for content changes const contentObserver = new MutationObserver(async (mutations) => { let hasNewContent = false; mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { if (node.tagName === 'VIDEO' || node.querySelector?.('video') || node.className?.includes('video') || node.className?.includes('gif')) { hasNewContent = true; } } }); }); if (hasNewContent && (!copyButton || copyButton.style.display !== 'flex')) { console.log('New video content detected, updating button state'); setTimeout(async () => await updateButtonState(), 500); } }); contentObserver.observe(document.body, { childList: true, subtree: true }); console.log('RedGifs Content-Aware Copy Button & Download initialized'); } // Enhanced startup sequence with multiple initialization attempts function startScript() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize); } else { initialize(); } // Additional safety net - retry initialization after page load window.addEventListener('load', () => { setTimeout(async () => { if (!downloadButton || !document.body.contains(downloadButton)) { console.log('Safety net: Re-initializing download button after page load'); await updateButtonState(); } }, 1000); }); // Final fallback for stubborn pages setTimeout(async () => { if (!downloadButton || !document.body.contains(downloadButton)) { console.log('Final fallback: Force download button creation'); await updateButtonState(); } }, 3000); } // Start the script with enhanced initialization startScript(); })();