Adds a floating share button that appears when videos are detected. Hold down to copy instead of share. Auto-detects and downloads M3U8 playlists.
目前為
// ==UserScript== // @name Universal Video Share Button with M3U8 Support // @namespace http://tampermonkey.net/ // @version 4.5 // @description Adds a floating share button that appears when videos are detected. Hold down to copy instead of share. Auto-detects and downloads M3U8 playlists. // @author Minoa // @license MIT // @match *://*/* // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/m3u8-parser.min.js // @grant none // ==/UserScript== (function() { 'use strict'; var floatingButton = null; var pressTimer = null; var isLongPress = false; var checkInterval = null; var detectedM3U8s = []; var detectedM3U8Urls = []; var allDetectedVideos = new Map(); var processedVideos = new Map(); // Track what was done with each video var downloadedBlobs = new Map(); // Store downloaded M3U8 blobs // Color scheme const COLORS = { button: '#55423d', buttonHover: '#6b5651', icon: '#ffc0ad', text: '#fff3ec' }; // Common video selectors across different sites var VIDEO_SELECTORS = [ 'video', '.video-player video', '.player video', '#player video', '.video-container video', '[class*="video"] video', '[class*="player"] video', 'iframe[src*="youtube.com"]', 'iframe[src*="vimeo.com"]', 'iframe[src*="dailymotion.com"]', 'iframe[src*="twitch.tv"]' ]; // Video extensions to detect var VIDEO_EXTENSIONS = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.wmv', '.flv', '.mkv', '.m4v', '.3gp']; // M3U8 Detection - Hook into network requests (function setupNetworkDetection() { // Hook fetch const originalFetch = window.fetch; window.fetch = function(...args) { const promise = originalFetch.apply(this, args); const url = typeof args[0] === 'string' ? args[0] : args[0]?.url; promise.then(response => { if (url) { // Don't track .ts segments if (!url.match(/seg-\d+-.*\.ts/i) && !url.endsWith('.ts')) { checkUrlForVideo(url); } if (url.includes('.m3u8') || url.includes('.m3u')) { detectM3U8(url); } } return response; }).catch(e => { throw e; }); return promise; }; // Hook XMLHttpRequest const originalOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(...args) { this.addEventListener("load", function() { try { const url = args[1]; if (url) { // Don't track .ts segments if (!url.match(/seg-\d+-.*\.ts/i) && !url.endsWith('.ts')) { checkUrlForVideo(url); } if (url.includes('.m3u8') || url.includes('.m3u')) { detectM3U8(url); } if (this.responseText && this.responseText.trim().startsWith("#EXTM3U")) { detectM3U8(url); } } } catch(e) {} }); return originalOpen.apply(this, args); }; // Hook Response.text() to catch m3u8 content const originalText = Response.prototype.text; Response.prototype.text = function() { return originalText.call(this).then(text => { if (text.trim().startsWith("#EXTM3U")) { detectM3U8(this.url); } return text; }); }; // Hook video element src changes const originalSetAttribute = Element.prototype.setAttribute; Element.prototype.setAttribute = function(name, value) { if (this.tagName === 'VIDEO' || this.tagName === 'SOURCE') { if (name === 'src' && value) { checkUrlForVideo(value); } } return originalSetAttribute.call(this, name, value); }; })(); function checkUrlForVideo(url) { try { // Skip blob URLs and .ts segments if (url.startsWith('blob:')) return; if (url.match(/seg-\d+-.*\.ts/i)) return; if (url.endsWith('.ts')) return; // Skip if it's already an M3U8 (will be handled separately) if (url.includes('.m3u8') || url.includes('.m3u')) return; // Check if it's a video URL const lowerUrl = url.toLowerCase(); const isVideo = VIDEO_EXTENSIONS.some(ext => lowerUrl.includes(ext)); if (isVideo) { const fullUrl = new URL(url, location.href).href; if (!allDetectedVideos.has(fullUrl)) { allDetectedVideos.set(fullUrl, { url: fullUrl, type: 'video', timestamp: Date.now(), title: 'Video - ' + getFilenameFromUrl(fullUrl) }); checkForVideos(); } } } catch(e) {} } function getFilenameFromUrl(url) { try { const pathname = new URL(url).pathname; const filename = pathname.split('/').pop(); return filename || 'Unknown'; } catch(e) { return 'Unknown'; } } function getTimeAgo(timestamp) { const seconds = Math.floor((Date.now() - timestamp) / 1000); if (seconds < 60) return 'just now'; const minutes = Math.floor(seconds / 60); if (minutes < 60) return minutes + 'm ago'; const hours = Math.floor(minutes / 60); if (hours < 24) return hours + 'h ago'; const days = Math.floor(hours / 24); return days + 'd ago'; } function getBaseUrlFromSegment(segmentUrl) { try { const url = new URL(segmentUrl); const path = url.pathname.substring(0, url.pathname.lastIndexOf('/') + 1); return url.origin + path; } catch(e) { return null; } } function isSegmentOfPlaylist(videoUrl) { // Check if this video URL is a segment of a detected playlist if (!videoUrl.endsWith('.ts')) return false; if (!videoUrl.match(/seg-\d+-/i)) return false; const baseUrl = getBaseUrlFromSegment(videoUrl); if (!baseUrl) return false; // Check if any M3U8 shares the same base path for (const [m3u8Url, data] of allDetectedVideos.entries()) { if (data.type === 'm3u8') { const m3u8Base = getBaseUrlFromSegment(m3u8Url); if (m3u8Base && baseUrl.startsWith(m3u8Base)) { return true; } } } return false; } function hasM3U8Playlist() { return Array.from(allDetectedVideos.values()).some(v => v.type === 'm3u8'); } function isMasterPlaylist(url, manifest) { // Master playlists have playlists, not segments if (url.includes('master.m3u8')) return true; if (manifest.playlists && manifest.playlists.length > 0 && (!manifest.segments || manifest.segments.length === 0)) { return true; } return false; } function shouldFilterM3U8(url, manifest) { // If this is a master playlist and we have index playlists from the same base, filter it out if (!isMasterPlaylist(url, manifest)) return false; const baseUrl = getBaseUrlFromSegment(url); if (!baseUrl) return false; // Check if we have any index-*.m3u8 from the same base for (const [otherUrl, data] of allDetectedVideos.entries()) { if (data.type === 'm3u8' && otherUrl !== url) { const otherBase = getBaseUrlFromSegment(otherUrl); if (otherBase === baseUrl && otherUrl.includes('index-')) { return true; // Filter out this master playlist } } } return false; } async function detectM3U8(url) { try { if (url.startsWith('blob:')) return; url = new URL(url, location.href).href; if (detectedM3U8Urls.includes(url)) return; detectedM3U8Urls.push(url); const response = await fetch(url); const content = await response.text(); const parser = new m3u8Parser.Parser(); parser.push(content); parser.end(); const manifest = parser.manifest; let duration = 0; if (manifest.segments) { for (var s = 0; s < manifest.segments.length; s++) { duration += manifest.segments[s].duration; } } const m3u8Data = { url: url, manifest: manifest, content: content, duration: duration, title: 'M3U8 - ' + (duration ? Math.ceil(duration / 60) + 'min' : 'Unknown'), timestamp: Date.now() }; detectedM3U8s.push(m3u8Data); allDetectedVideos.set(url, { url: url, type: 'm3u8', timestamp: Date.now(), title: m3u8Data.title, m3u8Data: m3u8Data }); checkForVideos(); } catch(e) { console.error("M3U8 parse error:", e); } } function getVideoUrl(videoElement) { if (videoElement.tagName === 'VIDEO') { if (videoElement.currentSrc && !videoElement.currentSrc.startsWith('blob:')) return videoElement.currentSrc; if (videoElement.src && !videoElement.src.startsWith('blob:')) return videoElement.src; var sources = videoElement.querySelectorAll('source'); for (var i = 0; i < sources.length; i++) { if (sources[i].src && !sources[i].src.startsWith('blob:')) return sources[i].src; } } if (videoElement.tagName === 'IFRAME') { return videoElement.src; } return null; } function getUniqueVideos() { var videos = []; var seenUrls = new Set(); // Add all detected videos from network allDetectedVideos.forEach(function(videoData) { // Skip .ts segments if we have playlists if (hasM3U8Playlist() && isSegmentOfPlaylist(videoData.url)) { return; } // Skip master playlists if we have index playlists if (videoData.type === 'm3u8' && shouldFilterM3U8(videoData.url, videoData.m3u8Data.manifest)) { return; } if (!seenUrls.has(videoData.url)) { seenUrls.add(videoData.url); videos.push(videoData); } }); // Add regular videos from page for (var s = 0; s < VIDEO_SELECTORS.length; s++) { var elements = document.querySelectorAll(VIDEO_SELECTORS[s]); for (var i = 0; i < elements.length; i++) { var element = elements[i]; var rect = element.getBoundingClientRect(); if (rect.width > 100 && rect.height > 100) { var url = getVideoUrl(element); if (url && !seenUrls.has(url)) { seenUrls.add(url); const videoData = { type: 'video', element: element, url: url, title: element.title || element.alt || ('Video ' + (videos.length + 1)), timestamp: Date.now() }; videos.push(videoData); allDetectedVideos.set(url, videoData); } } } } // Check iframes for videos var iframes = document.querySelectorAll('iframe'); for (var i = 0; i < iframes.length; i++) { try { var iframeDoc = iframes[i].contentDocument || iframes[i].contentWindow.document; if (iframeDoc) { var iframeVideos = iframeDoc.querySelectorAll('video'); for (var v = 0; v < iframeVideos.length; v++) { var url = getVideoUrl(iframeVideos[v]); if (url && !seenUrls.has(url)) { seenUrls.add(url); const videoData = { type: 'video', element: iframeVideos[v], url: url, title: 'Iframe Video ' + (videos.length + 1), timestamp: Date.now() }; videos.push(videoData); allDetectedVideos.set(url, videoData); } } } } catch(e) { // Cross-origin iframe, skip } } return videos; } function getButtonIcon(videos) { if (videos.length > 1) return '⇓'; if (videos.length === 1) { return videos[0].type === 'm3u8' ? '⇣' : '↯'; } return '▶'; } function createFloatingButton() { if (floatingButton) return floatingButton; var container = document.createElement('div'); container.id = 'universal-video-share-container'; container.style.cssText = 'position: fixed; top: 15px; left: 15px; width: 40px; height: 40px; z-index: 999999;'; var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('width', '40'); svg.setAttribute('height', '40'); svg.style.cssText = 'position: absolute; top: 0; left: 0; transform: rotate(-90deg);'; var circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); circle.setAttribute('cx', '20'); circle.setAttribute('cy', '20'); circle.setAttribute('r', '18'); circle.setAttribute('fill', 'none'); circle.setAttribute('stroke', '#4ade80'); circle.setAttribute('stroke-width', '3'); circle.setAttribute('stroke-dasharray', '113'); circle.setAttribute('stroke-dashoffset', '113'); circle.setAttribute('stroke-linecap', 'round'); circle.style.cssText = 'transition: stroke-dashoffset 0.3s ease;'; circle.id = 'progress-circle'; svg.appendChild(circle); container.appendChild(svg); floatingButton = document.createElement('div'); floatingButton.innerHTML = '▶'; floatingButton.id = 'universal-video-share-float'; floatingButton.style.cssText = `position: absolute; top: 2px; left: 2px; width: 36px; height: 36px; background: ${COLORS.button}; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); color: ${COLORS.icon}; border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 50%; font-size: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; user-select: none; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);`; container.appendChild(floatingButton); floatingButton.progressCircle = circle; floatingButton.addEventListener('mouseenter', function() { this.style.background = COLORS.buttonHover; this.style.transform = 'scale(1.05)'; }); floatingButton.addEventListener('mouseleave', function() { this.style.background = COLORS.button; this.style.transform = 'scale(1)'; }); floatingButton.addEventListener('mousedown', function(e) { e.preventDefault(); isLongPress = false; pressTimer = setTimeout(function() { isLongPress = true; floatingButton.style.background = 'rgba(74, 222, 128, 0.8)'; floatingButton.innerHTML = '⎘'; }, 500); }); floatingButton.addEventListener('mouseup', function(e) { e.preventDefault(); clearTimeout(pressTimer); var videos = getUniqueVideos(); floatingButton.style.background = COLORS.button; floatingButton.innerHTML = getButtonIcon(videos); if (isLongPress) { handleCopy(); } else { handleShare(); } }); floatingButton.addEventListener('mouseleave', function() { clearTimeout(pressTimer); var videos = getUniqueVideos(); floatingButton.style.background = COLORS.button; floatingButton.innerHTML = getButtonIcon(videos); }); floatingButton.addEventListener('touchstart', function(e) { e.preventDefault(); isLongPress = false; pressTimer = setTimeout(function() { isLongPress = true; floatingButton.style.background = 'rgba(74, 222, 128, 0.8)'; floatingButton.innerHTML = '⎘'; }, 500); }); floatingButton.addEventListener('touchend', function(e) { e.preventDefault(); clearTimeout(pressTimer); var videos = getUniqueVideos(); floatingButton.style.background = COLORS.button; floatingButton.innerHTML = getButtonIcon(videos); if (isLongPress) { handleCopy(); } else { handleShare(); } }); document.body.appendChild(container); return floatingButton; } function updateProgress(percent) { if (!floatingButton || !floatingButton.progressCircle) return; var offset = 113 - (113 * percent / 100); floatingButton.progressCircle.setAttribute('stroke-dashoffset', offset); } function resetProgress() { if (!floatingButton || !floatingButton.progressCircle) return; floatingButton.progressCircle.setAttribute('stroke-dashoffset', '113'); } function handleShare() { var videos = getUniqueVideos(); if (videos.length === 0) { showNotification('No videos found', 'error'); return; } if (videos.length === 1) { shareVideo(videos[0]); } else { showVideoSelector(videos, 'share'); } } function handleCopy() { var videos = getUniqueVideos(); if (videos.length === 0) { showNotification('No videos found', 'error'); return; } if (videos.length === 1) { // For M3U8, still download but skip share if (videos[0].type === 'm3u8') { downloadM3U8(videos[0], true); // Force download } else { copyVideoUrl(videos[0]); } } else { showVideoSelector(videos, 'copy'); } } function showVideoSelector(videos, action) { var existingSelector = document.querySelector('#video-selector-popup'); if (existingSelector) { existingSelector.remove(); } var popup = document.createElement('div'); popup.id = 'video-selector-popup'; popup.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); z-index: 9999999; display: flex; align-items: center; justify-content: center; padding: 20px; box-sizing: border-box;'; var container = document.createElement('div'); container.style.cssText = 'background: rgba(20, 20, 20, 0.95); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px; padding: 24px; max-width: 600px; max-height: 70%; overflow-y: auto; position: relative; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);'; var closeButton = document.createElement('button'); closeButton.innerHTML = '✕'; closeButton.style.cssText = `position: absolute; top: 12px; right: 12px; background: rgba(255, 255, 255, 0.1); border: none; font-size: 16px; cursor: pointer; color: ${COLORS.text}; width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: background 0.2s;`; closeButton.addEventListener('click', function() { popup.remove(); }); closeButton.addEventListener('mouseenter', function() { this.style.background = 'rgba(255, 255, 255, 0.2)'; }); closeButton.addEventListener('mouseleave', function() { this.style.background = 'rgba(255, 255, 255, 0.1)'; }); var title = document.createElement('h3'); title.textContent = 'Select Video to ' + (action.charAt(0).toUpperCase() + action.slice(1)); title.style.cssText = `margin: 0 0 16px 0; color: ${COLORS.text}; font-size: 16px; font-weight: 600; text-align: center;`; container.appendChild(closeButton); container.appendChild(title); // Sort videos by timestamp (newest first) videos.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); for (var i = 0; i < videos.length; i++) { var videoData = videos[i]; var videoItem = document.createElement('div'); videoItem.style.cssText = 'margin-bottom: 12px; padding: 12px; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; cursor: pointer; transition: all 0.2s ease; background: rgba(255, 255, 255, 0.05);'; (function(currentVideoData) { videoItem.addEventListener('mouseenter', function() { this.style.borderColor = 'rgba(255, 255, 255, 0.3)'; this.style.background = 'rgba(255, 255, 255, 0.1)'; }); videoItem.addEventListener('mouseleave', function() { this.style.borderColor = 'rgba(255, 255, 255, 0.1)'; this.style.background = 'rgba(255, 255, 255, 0.05)'; }); var headerDiv = document.createElement('div'); headerDiv.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;'; var typeBadge = document.createElement('span'); typeBadge.textContent = currentVideoData.type === 'm3u8' ? 'M3U8' : 'VIDEO'; typeBadge.style.cssText = 'display: inline-block; background: ' + (currentVideoData.type === 'm3u8' ? 'rgba(239, 68, 68, 0.8)' : 'rgba(59, 130, 246, 0.8)') + `; color: ${COLORS.text}; padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600;`; var timeAgo = document.createElement('span'); timeAgo.textContent = getTimeAgo(currentVideoData.timestamp || Date.now()); timeAgo.style.cssText = `color: ${COLORS.text}; opacity: 0.6; font-size: 10px;`; headerDiv.appendChild(typeBadge); headerDiv.appendChild(timeAgo); videoItem.appendChild(headerDiv); var videoInfo = document.createElement('div'); videoInfo.innerHTML = `<div style="color: ${COLORS.text}; font-size: 13px; font-weight: 500; margin-bottom: 4px;">` + currentVideoData.title + `</div><div style="color: ${COLORS.text}; opacity: 0.5; font-size: 11px; word-break: break-all; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">` + currentVideoData.url + '</div>'; videoItem.appendChild(videoInfo); videoItem.addEventListener('click', function() { popup.remove(); if (action === 'share') { shareVideo(currentVideoData); } else { if (currentVideoData.type === 'm3u8') { downloadM3U8(currentVideoData, true); // Force download for copy } else { copyVideoUrl(currentVideoData); } } }); })(videoData); container.appendChild(videoItem); } popup.appendChild(container); popup.addEventListener('click', function(e) { if (e.target === popup) { popup.remove(); } }); document.body.appendChild(popup); } async function downloadM3U8(videoData, forceDownload = false) { // Check if we already downloaded this const cachedBlob = downloadedBlobs.get(videoData.url); if (cachedBlob && !forceDownload) { showNotification('Using cached video...', 'info'); const filename = cachedBlob.filename; await shareOrDownloadBlob(cachedBlob.blob, filename, videoData, forceDownload); return; } showNotification('Downloading M3U8...', 'info'); resetProgress(); try { const manifest = videoData.m3u8Data.manifest; const baseUrl = videoData.url.substring(0, videoData.url.lastIndexOf('/') + 1); const segments = manifest.segments; if (!segments || segments.length === 0) { throw new Error("No segments found in this playlist"); } var segmentData = []; for (var i = 0; i < segments.length; i++) { var segUrl = segments[i].uri.startsWith('http') ? segments[i].uri : baseUrl + segments[i].uri; var response = await fetch(segUrl); var data = await response.arrayBuffer(); segmentData.push(data); var percent = Math.floor((i + 1) / segments.length * 100); updateProgress(percent); if (percent % 20 === 0 || i === segments.length - 1) { showNotification('Downloading: ' + percent + '%', 'info'); } } showNotification('Merging...', 'info'); var merged = new Blob(segmentData, { type: 'video/mp2t' }); var urlPath = new URL(videoData.url).pathname; var baseName = urlPath.split('/').pop().replace(/\.(m3u8?|ts)$/i, '') || 'video'; var filename = baseName + '-noenc.ts'; // Cache the blob downloadedBlobs.set(videoData.url, { blob: merged, filename: filename }); await shareOrDownloadBlob(merged, filename, videoData, forceDownload); } catch(e) { console.error("M3U8 download failed:", e); showNotification('Download failed: ' + e.message, 'error'); resetProgress(); } } async function shareOrDownloadBlob(blob, filename, videoData, forceDownload = false) { const processed = processedVideos.get(videoData.url); // If already processed and user clicks again, force download if (processed && !forceDownload) { forceDownload = true; showNotification('Downloading...', 'info'); } if (!forceDownload && navigator.share && navigator.canShare) { try { var file = new File([blob], filename, { type: 'video/mp2t' }); if (navigator.canShare({ files: [file] })) { var shareStartTime = Date.now(); var shareCompleted = false; // Set a timeout to detect if share dialog stayed open var shareTimeout = setTimeout(function() { if (!shareCompleted) { console.log("Share dialog appears to be active (2s elapsed)"); } }, 2000); try { await navigator.share({ files: [file], title: document.title, text: 'M3U8 Video' }); shareCompleted = true; clearTimeout(shareTimeout); processedVideos.set(videoData.url, { action: 'shared', timestamp: Date.now() }); showNotification('Shared!', 'success'); resetProgress(); return; } catch(e) { shareCompleted = true; clearTimeout(shareTimeout); var timeElapsed = Date.now() - shareStartTime; // If share was open for >2 seconds, assume it worked if (timeElapsed > 2000) { processedVideos.set(videoData.url, { action: 'shared', timestamp: Date.now() }); showNotification('Share completed', 'success'); resetProgress(); return; } console.log("Share failed/cancelled after " + timeElapsed + "ms, downloading instead:", e); } } } catch(e) { console.log("Share API error:", e); } } // Download the file var blobUrl = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = blobUrl; a.download = filename; a.style.display = 'none'; document.body.appendChild(a); a.click(); setTimeout(function() { document.body.removeChild(a); URL.revokeObjectURL(blobUrl); }, 100); processedVideos.set(videoData.url, { action: 'downloaded', timestamp: Date.now() }); showNotification('Downloaded: ' + filename, 'success'); resetProgress(); } function shareVideo(videoData) { if (videoData.type === 'm3u8') { const processed = processedVideos.get(videoData.url); const forceDownload = processed && (processed.action === 'shared' || processed.action === 'downloaded'); downloadM3U8(videoData, forceDownload); return; } const processed = processedVideos.get(videoData.url); if (processed && (processed.action === 'shared' || processed.action === 'copied')) { downloadVideo(videoData); return; } if (navigator.share) { var shareStartTime = Date.now(); navigator.share({ title: document.title, url: videoData.url }).then(function() { processedVideos.set(videoData.url, { action: 'shared', timestamp: Date.now() }); showNotification('Shared!', 'success'); }).catch(function(error) { if (Date.now() - shareStartTime > 2000) { processedVideos.set(videoData.url, { action: 'shared', timestamp: Date.now() }); showNotification('Share completed', 'success'); } else { console.log('Share cancelled:', error); copyVideoUrl(videoData); } }); } else { copyVideoUrl(videoData); } } function downloadVideo(videoData) { showNotification('Opening video...', 'info'); window.open(videoData.url, '_blank'); processedVideos.set(videoData.url, { action: 'downloaded', timestamp: Date.now() }); } function copyVideoUrl(videoData) { const processed = processedVideos.get(videoData.url); if (processed && processed.action === 'copied') { downloadVideo(videoData); return; } if (navigator.clipboard && navigator.clipboard.writeText) { var copyStartTime = Date.now(); navigator.clipboard.writeText(videoData.url).then(function() { processedVideos.set(videoData.url, { action: 'copied', timestamp: Date.now() }); showNotification('URL copied', 'success'); }).catch(function(error) { if (Date.now() - copyStartTime > 2000) { processedVideos.set(videoData.url, { action: 'copied', timestamp: Date.now() }); showNotification('URL copied', 'success'); } else { fallbackCopy(videoData.url, videoData); } }); } else { fallbackCopy(videoData.url, videoData); } } function fallbackCopy(url, videoData) { try { var textArea = document.createElement('textarea'); textArea.value = url; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; document.body.appendChild(textArea); textArea.select(); if (document.execCommand('copy')) { processedVideos.set(videoData.url, { action: 'copied', timestamp: Date.now() }); showNotification('URL copied', 'success'); } else { showNotification('Copy failed', 'error'); } document.body.removeChild(textArea); } catch (err) { showNotification('Copy failed', 'error'); } } function showNotification(message, type) { if (type === undefined) type = 'success'; var existingNotifications = document.querySelectorAll('.universal-video-notification'); for (var i = 0; i < existingNotifications.length; i++) { existingNotifications[i].remove(); } var notification = document.createElement('div'); notification.textContent = message; notification.className = 'universal-video-notification'; var bgColor = type === 'success' ? 'rgba(74, 222, 128, 0.9)' : type === 'error' ? 'rgba(239, 68, 68, 0.9)' : 'rgba(59, 130, 246, 0.9)'; notification.style.cssText = `position: fixed; top: 65px; left: 15px; background: ${bgColor}; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); color: ${COLORS.text}; padding: 8px 12px; border-radius: 6px; z-index: 999998; font-weight: 500; font-size: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); max-width: 250px; word-wrap: break-word; transform: translateX(-300px); transition: transform 0.3s ease;`; document.body.appendChild(notification); setTimeout(function() { notification.style.transform = 'translateX(0)'; }, 100); setTimeout(function() { if (notification.parentNode) { notification.style.transform = 'translateX(-300px)'; setTimeout(function() { notification.remove(); }, 300); } }, 3000); } function checkForVideos() { var videos = getUniqueVideos(); if (videos.length > 0) { if (!floatingButton) { createFloatingButton(); } if (floatingButton) { floatingButton.innerHTML = getButtonIcon(videos); } } else { if (floatingButton && floatingButton.parentNode) { floatingButton.parentNode.remove(); floatingButton = null; } } } function init() { console.log('Universal Video Share with M3U8 initialized'); setTimeout(checkForVideos, 1000); checkInterval = setInterval(checkForVideos, 5000); } init(); })();