// ==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();
})();