您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a small progress bar under Instagram videos and lets you click/drag to seek (scrub). Works on dynamically loaded videos.
// ==UserScript== // @name Instagram Video Progressbar + Scrubbing // @namespace https://greasyfork.org/ // @version 1.0.0 // @description Adds a small progress bar under Instagram videos and lets you click/drag to seek (scrub). Works on dynamically loaded videos. // @match *://www.instagram.com/* // @author X0John // @run-at document-end // ==/UserScript== (function () { 'use strict'; // ------------- CONFIG ------------- const CONFIG = { heightPx: 4,// progress bar height in px bgColor: 'rgba(255,255,255,0.16)', // background bar color elapsedColor: '#ff2d55',// elapsed (filled) color disableLoop: false,// if true, force video.loop = false unmute: false,// if true, try to unmute video (may be fragile) debug: false // set true to enable console logs }; // ---------------------------------- function log(...args) { if (CONFIG.debug) console.log('IG-PROG:', ...args); } // Add progress UI & handlers for a single <video> element function attachProgress(video) { if (!video || video.dataset.igprogAttached) return; video.dataset.igprogAttached = '1'; log('attachProgress', video); // Find an appropriate container to append the absolute-positioned bar. // Usually the video's parent element is usable; make it positioned if needed. const container = video.parentElement || video.parentNode || video; try { const cs = getComputedStyle(container); if (cs.position === 'static') { // set relative so absolute bar positions correctly container.style.position = 'relative'; // small marker to remember we changed it container.dataset.igprogMadeRelative = '1'; } } catch (err) { // ignore; fallback below } // Create bar elements const outer = document.createElement('div'); outer.className = 'ig-progress-outer'; const inner = document.createElement('div'); inner.className = 'ig-progress-inner'; outer.appendChild(inner); // Style Object.assign(outer.style, { position: 'absolute', left: '0', right: '0', bottom: '0', height: CONFIG.heightPx + 'px', background: CONFIG.bgColor, cursor: 'pointer', zIndex: '9999', pointerEvents: 'auto', userSelect: 'none', WebkitUserSelect: 'none', MozUserSelect: 'none', transition: 'opacity .2s', opacity: '0.9' }); Object.assign(inner.style, { width: '0px', height: '100%', background: CONFIG.elapsedColor, transition: 'width 100ms linear' }); // Insert: put after video so it overlays the bottom of the video container.appendChild(outer); // Seek logic using pointer events (works for mouse + touch) let dragging = false; let lastPointerId = null; function getRect() { // ensure outer.getBoundingClientRect() is used; it follows parent positioning return outer.getBoundingClientRect(); } function clamp(n, a, b) { return Math.min(Math.max(n, a), b); } function seekByClientX(clientX) { const rect = getRect(); const width = rect.width || outer.offsetWidth || video.offsetWidth; if (!width || !video.duration || !isFinite(video.duration)) return; const ratio = clamp((clientX - rect.left) / width, 0, 1); try { video.currentTime = ratio * video.duration; } catch (e) { /* ignore */ } inner.style.width = Math.ceil(ratio * width) + 'px'; } function onPointerDown(e) { // Only handle primary button if (e.pointerType === 'mouse' && e.button !== 0) return; e.preventDefault(); e.stopPropagation(); dragging = true; lastPointerId = e.pointerId; // temporarily disable transition for smoother drag inner.style.transition = 'none'; try { outer.setPointerCapture(e.pointerId); } catch (err) {} seekByClientX(e.clientX); } function onPointerMove(e) { if (!dragging) return; if (lastPointerId != null && e.pointerId !== lastPointerId) return; e.preventDefault(); e.stopPropagation(); seekByClientX(e.clientX); } function onPointerUp(e) { if (!dragging) return; if (lastPointerId != null && e.pointerId !== lastPointerId) return; e.preventDefault(); e.stopPropagation(); dragging = false; lastPointerId = null; // restore transition inner.style.transition = 'width 100ms linear'; try { outer.releasePointerCapture && outer.releasePointerCapture(e.pointerId); } catch (err) {} } outer.addEventListener('pointerdown', onPointerDown, { passive: false }); window.addEventListener('pointermove', onPointerMove, { passive: false }); window.addEventListener('pointerup', onPointerUp, { passive: false }); window.addEventListener('pointercancel', onPointerUp, { passive: false }); // Update the bar as video plays (but avoid clobbering while dragging) function updateBar() { if (dragging) return; const dur = video.duration; if (!dur || !isFinite(dur)) return; const ratio = clamp(video.currentTime / dur, 0, 1); const w = outer.getBoundingClientRect().width || outer.offsetWidth || video.offsetWidth || 0; inner.style.width = Math.ceil(ratio * w) + 'px'; } // When duration/metadata load, update once video.addEventListener('loadedmetadata', updateBar); // Real-time updates video.addEventListener('timeupdate', updateBar); // Also on play/seeked video.addEventListener('play', updateBar); video.addEventListener('seeked', updateBar); // If video ends, fill bar video.addEventListener('ended', () => { inner.style.width = '100%'; }); // Optional: disable loop forcing if (CONFIG.disableLoop) { try { video.loop = false; } catch (e) {} } // Optional: attempt to unmute (may or may not work due to site controls) if (CONFIG.unmute && video.muted) { try { video.muted = false; } catch (e) {} } // Clean up if video removed from DOM const mo = new MutationObserver(() => { if (!document.contains(video)) { // remove listeners & bar outer.removeEventListener('pointerdown', onPointerDown); window.removeEventListener('pointermove', onPointerMove); window.removeEventListener('pointerup', onPointerUp); window.removeEventListener('pointercancel', onPointerUp); video.removeEventListener('loadedmetadata', updateBar); video.removeEventListener('timeupdate', updateBar); video.removeEventListener('play', updateBar); video.removeEventListener('seeked', updateBar); try { outer.remove(); } catch (ex) {} try { mo.disconnect(); } catch (ex) {} log('cleaned up for video'); } }); mo.observe(document.documentElement || document.body, { childList: true, subtree: true }); // initial update (if metadata already loaded) setTimeout(updateBar, 50); } // Scan existing videos and attach function scanAndAttach(root = document) { const videos = root.querySelectorAll && root.querySelectorAll('video'); if (!videos) return; videos.forEach(v => attachProgress(v)); } // Observe DOM for added videos const observer = new MutationObserver((mutations) => { for (const m of mutations) { if (m.addedNodes && m.addedNodes.length) { m.addedNodes.forEach(node => { if (!node) return; if (node.nodeType !== 1) return; if (node.tagName && node.tagName.toLowerCase() === 'video') { attachProgress(node); } else { // might contain videos deep inside try { const vids = node.querySelectorAll && node.querySelectorAll('video'); if (vids && vids.length) { vids.forEach(v => attachProgress(v)); } } catch (e) { /* ignore cross-origin etc */ } } }); } } }); // Start scanAndAttach(); observer.observe(document.documentElement || document.body, { childList: true, subtree: true }); // Also periodically (safety) rescan a few times in case videos were added in awkward ways let tries = 0; const rescanInterval = setInterval(() => { scanAndAttach(); tries++; if (tries > 20) clearInterval(rescanInterval); }, 1000); log('Instagram progressbar script loaded'); })();