Instagram Video Controls with Persistent UI and Volume

Adds native controls to Instagram videos + draggable UI (remembers volume/mute/position)

// ==UserScript==
// @name         Instagram Video Controls with Persistent UI and Volume
// @description  Adds native controls to Instagram videos + draggable UI (remembers volume/mute/position)
// @description  based on the script https://greasyfork.org/en/scripts/536598-instagram-video-controls
// @namespace    http://tampermonkey.net/
// @version      0.0.8
// @author       manaka
// @match        *://*.instagram.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    const POS_KEY = 'igvc-ui-position';
    const SETTINGS_KEY = 'igvc-settings';

    const settings = loadSettings();

    const debounce = (func, wait) => {
        let timeout;
        return function (...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    };

    function loadSettings() {
        const saved = localStorage.getItem(SETTINGS_KEY);
        if (saved) {
            try {
                const parsed = JSON.parse(saved);
                return {
                    volume: typeof parsed.volume === 'number' ? parsed.volume : 0.5,
                    muted: typeof parsed.muted === 'boolean' ? parsed.muted : false
                };
            } catch {
                return { volume: 0.5, muted: false };
            }
        }
        return { volume: 0.5, muted: false };
    }

    function saveSettings() {
        localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
    }

    const createControlUI = () => {
        const container = document.createElement('div');
        container.id = 'igvc-floating-ui';
        container.style.position = 'fixed';
        container.style.top = '10px';
        container.style.right = '10px';
        container.style.zIndex = '9999';
        container.style.background = 'rgba(0,0,0,0.7)';
        container.style.color = '#fff';
        container.style.padding = '6px 10px';
        container.style.borderRadius = '8px';
        container.style.fontSize = '12px';
        container.style.fontFamily = 'sans-serif';
        container.style.cursor = 'move';
        container.style.userSelect = 'none';

        const savedPos = localStorage.getItem(POS_KEY);
        if (savedPos) {
            const { top, left } = JSON.parse(savedPos);
            container.style.top = top;
            container.style.left = left;
            container.style.right = 'auto';
        }

        const volumeLabel = document.createElement('label');
        volumeLabel.textContent = 'Volume: ';
        volumeLabel.style.marginRight = '4px';

        const volumeSlider = document.createElement('input');
        volumeSlider.type = 'range';
        volumeSlider.min = '0';
        volumeSlider.max = '1';
        volumeSlider.step = '0.01';
        volumeSlider.value = settings.volume;
        volumeSlider.style.verticalAlign = 'middle';

        const muteLabel = document.createElement('label');
        muteLabel.style.marginLeft = '10px';
        muteLabel.textContent = 'Mute';

        const muteCheckbox = document.createElement('input');
        muteCheckbox.type = 'checkbox';
        muteCheckbox.checked = settings.muted;
        muteCheckbox.style.marginLeft = '4px';

        volumeSlider.addEventListener('input', () => {
            settings.volume = parseFloat(volumeSlider.value);
            settings.muted = false;
            muteCheckbox.checked = false;
            saveSettings();
            applySettingsToAll();
        });

        muteCheckbox.addEventListener('change', () => {
            settings.muted = muteCheckbox.checked;
            saveSettings();
            applySettingsToAll();
        });

        container.appendChild(volumeLabel);
        container.appendChild(volumeSlider);
        container.appendChild(muteLabel);
        container.appendChild(muteCheckbox);
        document.body.appendChild(container);

        makeDraggable(container, volumeSlider);
    };

    const makeDraggable = (element, ignoreElement) => {
        let offsetX, offsetY, isDragging = false;

        element.addEventListener('mousedown', (e) => {
            if (ignoreElement.contains(e.target)) return; // prevent drag on slider
            isDragging = true;
            offsetX = e.clientX - element.getBoundingClientRect().left;
            offsetY = e.clientY - element.getBoundingClientRect().top;
            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);
        });

        const onMouseMove = (e) => {
            if (!isDragging) return;
            const left = `${e.clientX - offsetX}px`;
            const top = `${e.clientY - offsetY}px`;
            element.style.left = left;
            element.style.top = top;
            element.style.right = 'auto';
        };

        const onMouseUp = () => {
            if (!isDragging) return;
            isDragging = false;
            document.removeEventListener('mousemove', onMouseMove);
            document.removeEventListener('mouseup', onMouseUp);
            localStorage.setItem(POS_KEY, JSON.stringify({
                top: element.style.top,
                left: element.style.left
            }));
        };
    };

    const applySettings = (video) => {
        video.muted = settings.muted;
        video.volume = settings.volume;
    };

    const monitorVideo = (video) => {
        if (video.dataset.igvcProcessed) return;
        video.dataset.igvcProcessed = 'true';

        video.setAttribute('controls', 'true');
        video.style.position = 'relative';
        video.style.zIndex = '1';

        if (location.pathname.startsWith('/stories/')) {
            video.style.height = 'calc(100% - 62px)';
        }

        applySettings(video);

        video.addEventListener('volumechange', () => {
            applySettings(video);
        });

        const originalPlay = video.play;
        video.play = function () {
            const result = originalPlay.apply(this, arguments);
            result.then(() => applySettings(this)).catch(() => {});
            return result;
        };
    };

    const applySettingsToAll = () => {
        document.querySelectorAll('video').forEach(applySettings);
    };

    const applyToAllVideos = () => {
        document.querySelectorAll('video').forEach(monitorVideo);
    };

    const init = () => {
        createControlUI();
        applyToAllVideos();
        const observer = new MutationObserver(debounce(applyToAllVideos, 300));
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    };

    init();
})();