[INSTAGRAM] PROGRESS BAR

Adds a seekable progress bar to Instagram videos and Reels.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         [INSTAGRAM] PROGRESS BAR
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Adds a seekable progress bar to Instagram videos and Reels.
// @author       Emree.el on Instagram
// @match        *://*.instagram.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const CLASS_NAME = 'emree-instagram-progress-bar';
    const FILL_CLASS_NAME = 'emree-instagram-progress-fill';

    // Keeps track of videos we've already processed.
    const progressBars = new WeakMap();

    function getVideoContainer(video) {
        /*
         * Instagram changes its DOM structure constantly.
         * Instead of assuming video.parentElement is always correct,
         * walk upward and find a reasonably sized container.
         */
        let element = video.parentElement;

        for (let i = 0; element && i < 6; i++) {
            const style = window.getComputedStyle(element);
            const rect = element.getBoundingClientRect();

            if (
                rect.width >= video.clientWidth &&
                rect.height >= video.clientHeight &&
                style.display !== 'contents'
            ) {
                return element;
            }

            element = element.parentElement;
        }

        return video.parentElement;
    }

    function isValidDuration(video) {
        return Number.isFinite(video.duration) && video.duration > 0;
    }

    function createProgressBar(video) {
        if (!(video instanceof HTMLVideoElement)) return;

        // Already processed and still connected to the DOM.
        const existing = progressBars.get(video);

        if (existing && existing.bar.isConnected) {
            return;
        }

        const container = getVideoContainer(video);

        if (!container) return;

        // Avoid creating duplicate bars inside the same container.
        const existingBar = container.querySelector(
            `:scope > .${CLASS_NAME}`
        );

        if (existingBar) {
            progressBars.set(video, {
                bar: existingBar,
                fill: existingBar.querySelector(`.${FILL_CLASS_NAME}`)
            });
            return;
        }

        // Make sure the progress bar can be positioned correctly.
        const containerStyle = window.getComputedStyle(container);

        if (containerStyle.position === 'static') {
            container.style.position = 'relative';
        }

        // Create progress bar.
        const progressBar = document.createElement('div');
        progressBar.className = CLASS_NAME;

        Object.assign(progressBar.style, {
            position: 'absolute',
            left: '0',
            bottom: '5px',
            width: '100%',
            height: '6px',
            background: 'rgba(0, 0, 0, 0.5)',
            cursor: 'pointer',
            zIndex: '999999',
            borderRadius: '999px',
            overflow: 'hidden',
            touchAction: 'none',
            userSelect: 'none'
        });

        // Create fill.
        const progressFill = document.createElement('div');
        progressFill.className = FILL_CLASS_NAME;

        Object.assign(progressFill.style, {
            width: '0%',
            height: '100%',
            background: '#ff0000',
            pointerEvents: 'none',
            borderRadius: 'inherit',
            willChange: 'width'
        });

        progressBar.appendChild(progressFill);
        container.appendChild(progressBar);

        progressBars.set(video, {
            bar: progressBar,
            fill: progressFill,
            container
        });

        function updateProgress() {
            if (!progressBar.isConnected) return;

            if (!isValidDuration(video)) {
                progressFill.style.width = '0%';
                return;
            }

            const progress = Math.max(
                0,
                Math.min(100, (video.currentTime / video.duration) * 100)
            );

            progressFill.style.width = `${progress}%`;
        }

        // Update on normal playback events.
        video.addEventListener('timeupdate', updateProgress, {
            passive: true
        });

        video.addEventListener('loadedmetadata', updateProgress, {
            passive: true
        });

        video.addEventListener('durationchange', updateProgress, {
            passive: true
        });

        video.addEventListener('seeked', updateProgress, {
            passive: true
        });

        // Smooth progress updates.
        let animationFrame = null;

        function animationLoop() {
            if (!video.isConnected || !progressBar.isConnected) {
                animationFrame = null;
                return;
            }

            updateProgress();
            animationFrame = requestAnimationFrame(animationLoop);
        }

        animationFrame = requestAnimationFrame(animationLoop);

        function seek(event) {
            if (!isValidDuration(video)) return;

            const rect = progressBar.getBoundingClientRect();

            if (!rect.width) return;

            const position = Math.max(
                0,
                Math.min(1, (event.clientX - rect.left) / rect.width)
            );

            try {
                video.currentTime = position * video.duration;
                updateProgress();
            } catch (error) {
                // Instagram may temporarily replace the video source.
                // Silently ignore invalid seek attempts.
            }
        }

        /*
         * Pointer events work with mouse, trackpads and touch input.
         * stopPropagation is important because Instagram often attaches
         * its own gesture handlers to parent elements.
         */
        progressBar.addEventListener('pointerdown', (event) => {
            event.preventDefault();
            event.stopPropagation();

            try {
                progressBar.setPointerCapture(event.pointerId);
            } catch (error) {}

            seek(event);
        });

        progressBar.addEventListener('pointermove', (event) => {
            if (event.buttons === 1 || event.pressure > 0) {
                event.preventDefault();
                event.stopPropagation();
                seek(event);
            }
        });

        progressBar.addEventListener('pointerup', (event) => {
            event.preventDefault();
            event.stopPropagation();
            seek(event);

            try {
                progressBar.releasePointerCapture(event.pointerId);
            } catch (error) {}
        });

        // Prevent Instagram click handlers from interfering.
        ['click', 'dblclick'].forEach((eventName) => {
            progressBar.addEventListener(eventName, (event) => {
                event.stopPropagation();
            });
        });

        updateProgress();
    }

    function scanForVideos(root = document) {
        if (root instanceof HTMLVideoElement) {
            createProgressBar(root);
        }

        if (root.querySelectorAll) {
            root.querySelectorAll('video').forEach(createProgressBar);
        }
    }

    /*
     * Instagram is a single-page app and dynamically replaces content.
     * Continuously scan added DOM nodes for new videos.
     */
    const observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
            for (const node of mutation.addedNodes) {
                if (!(node instanceof Element)) continue;

                scanForVideos(node);
            }
        }
    });

    function start() {
        scanForVideos();

        observer.observe(document.documentElement, {
            childList: true,
            subtree: true
        });

        // Backup scan for cases where Instagram reuses an existing DOM node.
        setInterval(() => {
            scanForVideos();
        }, 2000);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', start, {
            once: true
        });
    } else {
        start();
    }
})();