Instagram Enhanced

я заебался мужики

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Instagram Enhanced
// @namespace    http://tampermonkey.net/
// @version      6
// @description  я заебался мужики
// @author       You
// @match        https://www.instagram.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_openInTab
// @grant        unsafeWindow
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    let savedVolume = GM_getValue('reelsVolume', 0.5);
    let savedSpeed = GM_getValue('reelsSpeed', 1);
    let processedVideos = new WeakSet();
    let processedContainers = new WeakSet();
    const processedComments = new WeakSet();
    const videoControls = new WeakMap();

    const style = document.createElement('style');
    style.textContent = `
        .video-overlay {
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            z-index: 9999;
            pointer-events: none;
        }

        .video-overlay > * {
            pointer-events: auto;
        }

        .volume-control-wrapper {
            position: absolute;
            bottom: 100px;
            right: 12px;
            display: flex;
            flex-direction: column;
            align-items: center;
            opacity: 0;
            transition: opacity 0.2s ease;
            z-index: 10000;
        }

        .speed-control-wrapper {
            position: absolute;
            top: 12px;
            left: 12px;
            opacity: 0;
            transition: opacity 0.2s ease;
            z-index: 10000;
        }

        .speed-button {
            background: rgba(0, 0, 0, 0.6);
            color: white;
            border: none;
            border-radius: 4px;
            padding: 6px 10px;
            font-size: 12px;
            cursor: pointer;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            font-weight: 500;
            backdrop-filter: blur(10px);
            transition: all 0.15s ease;
            letter-spacing: 0.3px;
        }

        .speed-button:hover {
            background: rgba(0, 0, 0, 0.75);
        }

        .speed-button:active {
            transform: scale(0.95);
        }

        .speed-menu {
            position: absolute;
            top: 100%;
            left: 0;
            margin-top: 4px;
            background: rgba(0, 0, 0, 0.85);
            border-radius: 6px;
            padding: 4px;
            display: none;
            flex-direction: column;
            gap: 2px;
            backdrop-filter: blur(20px);
            min-width: 120px;
        }

        .speed-menu.active {
            display: flex;
        }

        .speed-option {
            background: transparent;
            color: white;
            border: none;
            padding: 8px 12px;
            font-size: 13px;
            cursor: pointer;
            border-radius: 4px;
            text-align: left;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            transition: all 0.1s;
            font-weight: 400;
        }

        .speed-option:hover {
            background: rgba(255, 255, 255, 0.1);
        }

        .speed-option.active {
            background: rgba(255, 255, 255, 0.15);
            font-weight: 500;
        }

        /* Seek bar ВНУТРИ рилса, снизу по центру, выше нативных кнопок */
        .seek-bar-wrapper {
            position: absolute;
            bottom: 50px;
            left: 50%;
            transform: translateX(-50%);
            width: 85%;
            opacity: 0;
            transition: opacity 0.2s ease;
            z-index: 10000;
        }

        .seek-bar-container {
            background: rgba(0, 0, 0, 0.6);
            backdrop-filter: blur(10px);
            border-radius: 6px;
            padding: 8px 12px;
        }

        .seek-bar {
            width: 100%;
            height: 3px;
            -webkit-appearance: none;
            appearance: none;
            background: linear-gradient(to right,
                white 0%,
                white var(--progress, 0%),
                rgba(255, 255, 255, 0.3) var(--progress, 0%),
                rgba(255, 255, 255, 0.3) 100%);
            outline: none;
            border-radius: 2px;
            cursor: pointer;
            transition: height 0.15s;
        }

        .seek-bar:hover {
            height: 4px;
        }

        .seek-bar::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: 12px;
            height: 12px;
            background: white;
            border-radius: 50%;
            cursor: pointer;
            transition: transform 0.15s;
        }

        .seek-bar::-webkit-slider-thumb:hover {
            transform: scale(1.15);
        }

        .seek-bar::-moz-range-thumb {
            width: 12px;
            height: 12px;
            background: white;
            border-radius: 50%;
            cursor: pointer;
            border: none;
        }

        .time-display {
            color: white;
            font-size: 11px;
            margin-top: 5px;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            font-weight: 400;
            opacity: 0.9;
            text-align: center;
        }

        .volume-slider-container {
            background: rgba(0, 0, 0, 0.6);
            backdrop-filter: blur(10px);
            border-radius: 20px;
            padding: 10px 8px;
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 10px;
        }

        .volume-slider-vertical {
            -webkit-appearance: slider-vertical;
            writing-mode: bt-lr;
            width: 3px;
            height: 80px;
            background: linear-gradient(to top,
                white 0%,
                white var(--volume-progress, 50%),
                rgba(255, 255, 255, 0.3) var(--volume-progress, 50%),
                rgba(255, 255, 255, 0.3) 100%);
            outline: none;
            border-radius: 2px;
            cursor: pointer;
        }

        .volume-slider-vertical::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: 12px;
            height: 12px;
            background: white;
            border-radius: 50%;
            cursor: pointer;
            transition: transform 0.15s;
        }

        .volume-slider-vertical::-webkit-slider-thumb:hover {
            transform: scale(1.15);
        }

        .volume-slider-vertical::-moz-range-thumb {
            width: 12px;
            height: 12px;
            background: white;
            border-radius: 50%;
            cursor: pointer;
            border: none;
        }

        .volume-icon {
            width: 24px;
            height: 24px;
            cursor: pointer;
            transition: transform 0.15s;
        }

        .volume-icon:hover {
            transform: scale(1.1);
        }

        .volume-icon:active {
            transform: scale(0.95);
        }

        .ig-date-label {
            color: rgb(115, 115, 115);
            font-size: 12px;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            font-weight: 400;
            white-space: nowrap;
            pointer-events: none;
        }

        /* Hide Instagram's native mute button — volume is handled by our slider */
        button[aria-label="Включить звук"],
        button[aria-label="Выключить звук"],
        button[aria-label="Mute"],
        button[aria-label="Unmute"],
        button[aria-label="Ton an"],
        button[aria-label="Ton aus"],
        button[aria-label="Activer le son"],
        button[aria-label="Désactiver le son"],
        button[aria-label="Activar sonido"],
        button[aria-label="Silenciar"],
        button[aria-label="Attiva audio"],
        button[aria-label="Disattiva audio"] {
            display: none !important;
        }

        /* Fallback: hide by data attribute Instagram uses on mute buttons */
        [data-bloks-name="bk.components.Flexbox"] button svg + * ~ button,
        ._9ym9 { display: none !important; }
    `;
    document.head.appendChild(style);

    const processedDates = new WeakSet();

    function formatDate(dateStr) {
        const d = new Date(dateStr);
        if (isNaN(d)) return null;
        return d.toLocaleString('ru-RU', {
            day: '2-digit', month: '2-digit', year: 'numeric',
            hour: '2-digit', minute: '2-digit'
        });
    }

    function injectDateLabel(timeEl) {
        if (processedDates.has(timeEl)) return;
        const datetime = timeEl.getAttribute('datetime');
        if (!datetime) return;
        const formatted = formatDate(datetime);
        if (!formatted) return;
        processedDates.add(timeEl);

        const label = document.createElement('span');
        label.className = 'ig-date-label';
        label.textContent = '· ' + formatted;

        const parent = timeEl.parentElement;
        if (parent) {
            parent.style.display = 'flex';
            parent.style.alignItems = 'center';
            parent.style.flexWrap = 'wrap';
            parent.style.gap = '4px';
        }
        timeEl.insertAdjacentElement('afterend', label);
    }

    function processDates() {
        document.querySelectorAll('time[datetime]').forEach(injectDateLabel);
    }

    function formatTime(seconds) {
        const mins = Math.floor(seconds / 60);
        const secs = Math.floor(seconds % 60);
        return `${mins}:${secs.toString().padStart(2, '0')}`;
    }

    function updateSpeedButton(video, button, menu) {
        const currentSpeed = video.playbackRate;
        const newText = currentSpeed === 1 ? '⚡ Скорость' : `⚡ ${currentSpeed}x`;
        if (button.textContent !== newText) {
            button.textContent = newText;
        }

        if (menu) {
            const options = menu.querySelectorAll('.speed-option');
            options.forEach(opt => {
                const optSpeed = parseFloat(opt.dataset.speed);
                if (Math.abs(optSpeed - currentSpeed) < 0.01) {
                    opt.classList.add('active');
                } else {
                    opt.classList.remove('active');
                }
            });
        }
    }

    function updateVolumeSlider(video, slider, icon) {
        const currentVol = video.volume;
        const sliderVal = Math.round(currentVol * 100);

        if (Math.abs(slider.value - sliderVal) > 1) {
            slider.value = sliderVal;
            slider.style.setProperty('--volume-progress', `${sliderVal}%`);
            updateVolumeIcon(icon, currentVol);
        }
    }

    function createSpeedControl(overlay, video) {
        const speedWrapper = document.createElement('div');
        speedWrapper.className = 'speed-control-wrapper';

        const currentSpeed = video.playbackRate || savedSpeed;
        const speedText = currentSpeed === 1 ? '⚡ Скорость' : `⚡ ${currentSpeed}x`;

        speedWrapper.innerHTML = `
            <button class="speed-button">${speedText}</button>
            <div class="speed-menu">
                <button class="speed-option" data-speed="0.5">0.5x</button>
                <button class="speed-option" data-speed="1">Обычная</button>
                <button class="speed-option" data-speed="1.25">1.25x</button>
                <button class="speed-option" data-speed="1.5">1.5x</button>
                <button class="speed-option" data-speed="1.75">1.75x</button>
                <button class="speed-option" data-speed="2">2x</button>
            </div>
        `;

        const button = speedWrapper.querySelector('.speed-button');
        const menu = speedWrapper.querySelector('.speed-menu');
        const options = speedWrapper.querySelectorAll('.speed-option');

        const controls = videoControls.get(video) || {};
        controls.speedButton = button;
        controls.speedMenu = menu;
        videoControls.set(video, controls);

        updateSpeedButton(video, button, menu);

        button.addEventListener('click', function (e) {
            e.stopPropagation();
            menu.classList.toggle('active');
        });

        options.forEach(option => {
            option.addEventListener('click', function (e) {
                e.stopPropagation();
                const speed = parseFloat(this.dataset.speed);
                savedSpeed = speed;
                GM_setValue('reelsSpeed', savedSpeed);

                if (video) {
                    video.playbackRate = savedSpeed;
                    updateSpeedButton(video, button, menu);
                }

                menu.classList.remove('active');
            });
        });

        overlay.appendChild(speedWrapper);
        return speedWrapper;
    }

    function createSeekBar(overlay, video) {
        const seekWrapper = document.createElement('div');
        seekWrapper.className = 'seek-bar-wrapper';
        seekWrapper.innerHTML = `
            <div class="seek-bar-container">
                <input type="range" class="seek-bar" min="0" max="100" value="0" step="0.1" style="--progress: 0%">
                <div class="time-display">0:00 / 0:00</div>
            </div>
        `;

        const seekBar = seekWrapper.querySelector('.seek-bar');
        const timeDisplay = seekWrapper.querySelector('.time-display');
        let isSeeking = false;

        video.addEventListener('timeupdate', function () {
            if (!isSeeking && video.duration) {
                const percent = (video.currentTime / video.duration) * 100;
                seekBar.value = percent;
                seekBar.style.setProperty('--progress', `${percent}%`);
                timeDisplay.textContent = `${formatTime(video.currentTime)} / ${formatTime(video.duration)}`;
            }
        });

        video.addEventListener('loadedmetadata', function () {
            timeDisplay.textContent = `0:00 / ${formatTime(video.duration)}`;
        });

        seekBar.addEventListener('input', function (e) {
            e.stopPropagation();
            isSeeking = true;
            this.style.setProperty('--progress', `${this.value}%`);
            const time = (this.value / 100) * video.duration;
            timeDisplay.textContent = `${formatTime(time)} / ${formatTime(video.duration)}`;
        });

        seekBar.addEventListener('change', function (e) {
            e.stopPropagation();
            const time = (this.value / 100) * video.duration;
            video.currentTime = time;
            isSeeking = false;
        });

        overlay.appendChild(seekWrapper);
        return seekWrapper;
    }

    function createVolumeControl(overlay, video) {
        const volumeWrapper = document.createElement('div');
        volumeWrapper.className = 'volume-control-wrapper';

        const currentVol = Math.round(savedVolume * 100);
        const volumeIcon = savedVolume === 0 ?
            `<svg class="volume-icon" fill="white" viewBox="0 0 24 24"><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/></svg>` :
            `<svg class="volume-icon" fill="white" viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/></svg>`;

        volumeWrapper.innerHTML = `
            <div class="volume-slider-container">
                <input type="range" class="volume-slider-vertical" min="0" max="100" value="${currentVol}" orient="vertical" style="--volume-progress: ${currentVol}%">
                ${volumeIcon}
            </div>
        `;

        const slider = volumeWrapper.querySelector('.volume-slider-vertical');
        const icon = volumeWrapper.querySelector('.volume-icon');

        const controls = videoControls.get(video) || {};
        controls.volumeSlider = slider;
        controls.volumeIcon = icon;
        videoControls.set(video, controls);

        slider.addEventListener('input', function (e) {
            e.stopPropagation();
            savedVolume = this.value / 100;
            GM_setValue('reelsVolume', savedVolume);
            this.style.setProperty('--volume-progress', `${this.value}%`);
            if (video) video.volume = savedVolume;
            updateVolumeIcon(icon, savedVolume);
        });

        icon.addEventListener('click', function (e) {
            e.stopPropagation();
            if (savedVolume > 0) {
                slider.value = 0;
                savedVolume = 0;
            } else {
                slider.value = 50;
                savedVolume = 0.5;
            }
            slider.style.setProperty('--volume-progress', `${slider.value}%`);
            GM_setValue('reelsVolume', savedVolume);
            if (video) video.volume = savedVolume;
            updateVolumeIcon(icon, savedVolume);
        });

        overlay.appendChild(volumeWrapper);
        return volumeWrapper;
    }

    function updateVolumeIcon(icon, volume) {
        const newPath = volume === 0 ?
            '<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>' :
            '<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>';

        if (icon.innerHTML !== newPath) {
            icon.innerHTML = newPath;
        }
    }

    // Unmute a video and apply saved volume, fighting Instagram's muted=true
    function applyVolume(video) {
        if (savedVolume > 0) {
            video.muted = false;
        }
        video.volume = savedVolume;
        video.playbackRate = savedSpeed;
    }

    function setupVideo(video) {
        if (processedVideos.has(video)) return;
        processedVideos.add(video);

        applyVolume(video);

        // Instagram often re-mutes on loadedmetadata / canplay / play — intercept all of them
        video.addEventListener('loadedmetadata', function () { applyVolume(this); });
        video.addEventListener('canplay',        function () { applyVolume(this); });
        video.addEventListener('play',           function () { applyVolume(this); });

        // Also intercept Instagram setting video.muted = true via the property setter
        try {
            const proto = Object.getPrototypeOf(video);
            const desc = Object.getOwnPropertyDescriptor(proto, 'muted')
                      || Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'muted');
            if (desc && desc.set) {
                Object.defineProperty(video, 'muted', {
                    get: function () { return desc.get ? desc.get.call(this) : false; },
                    set: function (val) {
                        // If IG tries to mute and we have saved volume > 0, ignore it
                        if (val === true && savedVolume > 0) return;
                        desc.set.call(this, val);
                    },
                    configurable: true,
                });
            }
        } catch (e) { /* ignore — not critical */ }
    }

    function findVideoContainer(video) {
        return video.parentElement;
    }

    function findPlayerContainer(video) {
        // Reels: --x-height на предке
        const reels = video.closest('[style*="--x-height"]');
        if (reels) return reels;

        // Feed/пост: ищем data-instancekey с максимальным перекрытием с видео.
        // Его родитель — это нужный нам контейнер (xyzq4qe / аналог).
        const vr = video.getBoundingClientRect();
        if (vr.width > 0 && vr.height > 0) {
            let bestEl = null, bestArea = 0;
            document.querySelectorAll('[data-instancekey]').forEach(inst => {
                const r = inst.getBoundingClientRect();
                const ow = Math.min(vr.right, r.right) - Math.max(vr.left, r.left);
                const oh = Math.min(vr.bottom, r.bottom) - Math.max(vr.top, r.top);
                if (ow > 0 && oh > 0 && ow * oh > bestArea) {
                    bestArea = ow * oh;
                    bestEl = inst.parentElement || inst;
                }
            });
            if (bestEl) return bestEl;
        }

        // Fallback: ищем общего предка, у которого data-instancekey — ПРЯМОЙ ребёнок.
        let el = video.parentElement;
        while (el && el !== document.body) {
            const parent = el.parentElement;
            if (!parent) break;
            if ([...parent.children].some(c => c !== el && c.hasAttribute('data-instancekey'))) return parent;
            el = parent;
        }
        return video.parentElement;
    }

    function processContainer(container, video) {
        if (processedContainers.has(container)) return;
        processedContainers.add(container);

        const videoContainer = findVideoContainer(video);
        videoContainer.classList.add('video-container');
        if (window.getComputedStyle(videoContainer).position === 'static') {
            videoContainer.style.position = 'relative';
        }

        const reelContainer = findPlayerContainer(video);
        if (window.getComputedStyle(reelContainer).position === 'static') {
            reelContainer.style.position = 'relative';
        }

        const overlay = document.createElement('div');
        overlay.className = 'video-overlay';
        reelContainer.appendChild(overlay);  // добавляем последним — выше data-instancekey

        const volumeWrapper = createVolumeControl(overlay, video);
        const seekWrapper = createSeekBar(overlay, video);
        const speedWrapper = createSpeedControl(overlay, video);

        const controlWrappers = [volumeWrapper, seekWrapper, speedWrapper].filter(Boolean);

        const showControls = () => controlWrappers.forEach(w => w.style.opacity = '1');
        const hideControls = () => controlWrappers.forEach(w => w.style.opacity = '0');

        let hideTimer;
        const scheduleHide = () => { hideTimer = setTimeout(hideControls, 300); };
        const cancelHide = () => clearTimeout(hideTimer);

        const hoverTargets = [reelContainer, ...controlWrappers];
        hoverTargets.forEach(el => {
            el.addEventListener('mouseenter', () => { cancelHide(); showControls(); });
            el.addEventListener('mouseleave', scheduleHide);
        });
    }


    function findAndProcessComments() {
        // Instagram использует CSS-переменные для размеров панели комментариев
        // Ищем контейнер со style="--x-maxHeight: XXX; --x-width: YYY;"

        const allDivs = document.querySelectorAll('div[style*="--x-maxHeight"][style*="--x-width"]');

        for (const target of allDivs) {
            // Проверяем, что это действительно панель комментариев
            if (target.offsetHeight < 300 || target.offsetWidth < 200) continue;

            // Пропускаем уже обработанные
            if (processedComments.has(target)) continue;

            processedComments.add(target);

            console.log('🎯 Найдена панель комментариев Instagram!', target);

            // Получаем текущие размеры из CSS-переменных
            const currentStyle = target.getAttribute('style') || '';
            const maxHeightMatch = currentStyle.match(/--x-maxHeight:\s*([\d.]+)px/);
            const widthMatch = currentStyle.match(/--x-width:\s*([\d.]+)px/);

            const defaultHeight = maxHeightMatch ? parseFloat(maxHeightMatch[1]) : 600;
            const defaultWidth = widthMatch ? parseFloat(widthMatch[1]) : 400;

            // Загружаем сохраненные размеры
            const savedWidth = GM_getValue('commentsWidth', defaultWidth);
            const savedHeight = GM_getValue('commentsHeight', defaultHeight);

            console.log('📐 Сохраненные размеры:', savedWidth, 'x', savedHeight);

            // УБИРАЕМ CSS-переменные и делаем контейнер resizable
            target.style.setProperty('--x-maxHeight', 'none', 'important');
            target.style.setProperty('--x-width', 'auto', 'important');

            // Устанавливаем обычные CSS-свойства с сохраненными размерами
            target.style.width = savedWidth + 'px';
            target.style.height = savedHeight + 'px';
            target.style.minWidth = '350px';
            target.style.minHeight = '400px';
            target.style.maxWidth = '95vw';
            target.style.maxHeight = '95vh';
            target.style.resize = 'both';
            target.style.overflow = 'auto';
            target.style.border = '2px solid rgba(255, 255, 255, 0.3)';
            target.style.borderRadius = '12px';
            target.style.boxShadow = '0 4px 30px rgba(0,0,0,0.3)';
            target.style.display = 'flex';
            target.style.flexDirection = 'column';
            target.style.position = 'relative';

            // Добавляем визуальный индикатор resize
            const resizeHint = document.createElement('div');
            resizeHint.className = 'resize-hint-instagram-comments';
            resizeHint.style.cssText = `
                position: absolute;
                bottom: 0;
                right: 0;
                width: 20px;
                height: 20px;
                background: linear-gradient(135deg, transparent 50%, rgba(255,255,255,0.3) 50%);
                cursor: nwse-resize;
                pointer-events: none;
                border-bottom-right-radius: 10px;
            `;
            target.appendChild(resizeHint);

            // Функция для адаптации содержимого
            const fixContent = () => {
                // Находим все прямые дети
                const children = Array.from(target.children);

                children.forEach((child, index) => {
                    if (child.className === 'resize-hint-instagram-comments') return;

                    const childStyle = window.getComputedStyle(child);

                    // Первый ребенок (обычно header) - фиксированный
                    if (index === 0 || child.offsetHeight < 100) {
                        child.style.flexShrink = '0';
                        child.style.flexGrow = '0';
                    } else {
                        // Остальное содержимое - растягивается
                        child.style.flex = '1 1 auto';
                        child.style.minHeight = '0';
                        child.style.overflow = 'auto';
                        child.style.display = 'flex';
                        child.style.flexDirection = 'column';
                    }
                });

                // Все скроллируемые элементы внутри
                const scrollables = target.querySelectorAll('[style*="flex: 1 1 auto"]');
                scrollables.forEach(el => {
                    if (el !== target) {
                        el.style.minHeight = '0';
                        el.style.height = 'auto';
                        el.style.maxHeight = 'none';

                        // Убираем CSS-переменные у вложенных элементов
                        el.style.setProperty('--x-maxHeight', 'none', 'important');
                        el.style.setProperty('--x-minHeight', '0', 'important');
                    }
                });
            };

            // Применяем фикс
            fixContent();

            // Отслеживаем изменения
            const observer = new MutationObserver(() => {
                requestAnimationFrame(fixContent);
            });

            observer.observe(target, {
                childList: true,
                subtree: true,
                attributes: true,
                attributeFilter: ['style', 'class']
            });

            // Отслеживаем resize и СОХРАНЯЕМ размеры
            let saveTimeout;
            const resizeObserver = new ResizeObserver(() => {
                fixContent();

                // Сохраняем размеры с небольшой задержкой (чтобы не спамить при изменении)
                clearTimeout(saveTimeout);
                saveTimeout = setTimeout(() => {
                    const newWidth = parseInt(target.style.width);
                    const newHeight = parseInt(target.style.height);

                    if (newWidth && newHeight) {
                        GM_setValue('commentsWidth', newWidth);
                        GM_setValue('commentsHeight', newHeight);
                        console.log('💾 Размеры сохранены:', newWidth, 'x', newHeight);
                    }
                }, 500); // Сохраняем через 500мс после окончания изменения размера
            });
            resizeObserver.observe(target);

            console.log('✅ Панель комментариев теперь resizable с автосохранением!');
        }
    }


    // Hide Instagram's native mute/unmute button (we have our own volume slider)
    function hideNativeMuteButtons() {
        document.querySelectorAll('button').forEach(btn => {
            const label = (btn.getAttribute('aria-label') || '').toLowerCase();
            if (label.includes('mute') || label.includes('unmute') ||
                label.includes('звук') || label.includes('son') ||
                label.includes('audio') || label.includes('silencia')) {
                btn.style.setProperty('display', 'none', 'important');
            }
        });
    }

    function findAndProcessVideos() {
        const videos = document.querySelectorAll('video');
        videos.forEach(video => {
            if (video.offsetWidth < 50 || video.offsetHeight < 50) return;

            setupVideo(video);

            const container = video.parentElement;

            if (container && !processedContainers.has(container)) {
                processContainer(container, video);
            }
        });

        hideNativeMuteButtons();
        findAndProcessComments();
    }

    setInterval(function () {
        document.querySelectorAll('video').forEach(video => {
            if (Math.abs(video.volume - savedVolume) > 0.05) {
                video.volume = savedVolume;
            }
            if (savedVolume > 0 && video.muted) {
                video.muted = false;
            }
            if (Math.abs(video.playbackRate - savedSpeed) > 0.05) {
                video.playbackRate = savedSpeed;
            }

            const controls = videoControls.get(video);
            if (controls) {
                if (controls.speedButton && controls.speedMenu) {
                    updateSpeedButton(video, controls.speedButton, controls.speedMenu);
                }
                if (controls.volumeSlider && controls.volumeIcon) {
                    updateVolumeSlider(video, controls.volumeSlider, controls.volumeIcon);
                }
            }
        });
    }, 1000);

    let debounceTimer;
    const observer = new MutationObserver(function () {
        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(() => { findAndProcessVideos(); processDates(); rewritePostLinks(); }, 150);
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });
    findAndProcessVideos();
    processDates();
    rewritePostLinks();

    let lastUrl = location.href;
    new MutationObserver(function () {
        const url = location.href;
        if (url !== lastUrl) {
            lastUrl = url;
            processedVideos = new WeakSet();
            processedContainers = new WeakSet();
            setTimeout(() => {
                findAndProcessVideos();
                processDates();
                rewritePostLinks();
                initLikesPage();
            }, 300);
        }
    }).observe(document, { subtree: true, childList: true });

    // ── /p/ → /reel/ REWRITER ───────────────────────────────────────────────

    let fetchPatched = false;
    // ig_cache_key (decoded) → media_code (shortcode). Built from wbloks responses.
    const codeByMediaId = new Map();

    function rewritePostLinks() {
        document.querySelectorAll('a[href*="/p/"]').forEach(a => {
            if (a.dataset.reelPatched) return;
            const m = a.href.match(/\/p\/([A-Za-z0-9_-]+)/);
            if (!m) return;
            a.dataset.reelPatched = '1';
            a.href = `https://www.instagram.com/reels/${m[1]}/`;
        });
    }

    // Parse wbloks response: extract media_id → media_code mappings.
    // Instagram uses Bloks DSL format (not JSON):
    //   (bk.action.array.Make, "media_id", "media_code", ...)   ← column names
    //   (bk.action.array.Make, "3891891610077986313_67659193585", "DYCxsybBX4J", ...)  ← values
    // So we find the column-name arrays, note positions of "media_id" and "media_code",
    // then extract values from the matching value arrays that follow.
    function indexWbloksResponse(text) {
        let count = 0;

        // The response body is a JSON string — so actual quote chars are escaped as \"
        // Real data looks like: \"3891891610077986313_67659193585\", \"DYCxsybBX4J\"
        // We need to match both escaped (\" ... \") and unescaped (" ... ") variants.
        // Use a pattern that handles both: optional backslash before each quote.
        const re = /\\?"(\d{10,25}_(\d{5,20}))\\?"\s*,\s*\\?"([A-Za-z0-9_-]{5,20})\\?"/g;
        let m;
        while ((m = re.exec(text)) !== null) {
            const rawId = m[1];   // e.g. "3891891610077986313_67659193585"
            const part2 = m[2];
            const code  = m[3];   // e.g. "DYCxsybBX4J"

            // Skip known non-shortcode type strings
            if (['clips', 'feed', 'reel', 'igtv', 'story', 'post'].includes(code)) continue;
            // Shortcodes are 8-15 chars, skip if outside that range
            if (code.length < 7 || code.length > 16) continue;

            const part1 = rawId.split('_')[0];
            const full  = part1 + part2;  // concatenated (matches ig_cache_key decode)

            codeByMediaId.set(rawId, code);  // "part1_part2"
            codeByMediaId.set(full,  code);  // "part1part2"
            codeByMediaId.set(part1, code);  // just part1 (fallback)
            count++;
        }

        console.log(`[IG] wbloks indexed: +${count} posts (total: ${codeByMediaId.size}), response length: ${text.length}`);
        return count;
    }

    // Get shortcode by reading ig_cache_key from a thumbnail img URL
    // ig_cache_key decodes to a full 19-digit media_id (part1+part2 concatenated).
    // The wbloks indexer stores part1 (first ~10 digits) as a Map key.
    // So we do a prefix search: find a stored key that is a prefix of idStr.
    function shortcodeForImg(img) {
        if (!img || !img.src) return null;
        const m = img.src.match(/ig_cache_key=([^&]+)/);
        if (!m) return null;
        try {
            const keyPart = decodeURIComponent(m[1]).split('.')[0];
            const idStr = atob(keyPart).trim();
            console.log(`[IG] ig_cache_key decoded: "${idStr}" (mapSize=${codeByMediaId.size})`);

            // 1. Direct hits: full id, or just the first numeric part
            let result = codeByMediaId.get(idStr)
                || codeByMediaId.get(idStr.split('_')[0])
                || null;

            // 2. Prefix / suffix scan for numeric ids:
            //    idStr may be part1+part2 concat; stored keys include full concat AND part1.
            //    Also handles case where stored key is longer (part1_part2) and idStr is prefix.
            if (!result && /^\d{10,}$/.test(idStr)) {
                for (const [key, code] of codeByMediaId) {
                    if (!/^\d{10,}$/.test(key)) continue;
                    if (idStr === key || idStr.startsWith(key) || key.startsWith(idStr)) {
                        result = code;
                        break;
                    }
                }
            }

            console.log(`[IG] shortcode lookup: "${idStr}" → ${result}`);
            return result;
        } catch (e) {
            console.log('[IG] ig_cache_key decode error:', e);
        }
        return null;
    }

    // Patch fetch + XHR to capture wbloks responses (read-only, doesn't block)
    function patchFetchForWbloks() {
        if (fetchPatched) return;
        fetchPatched = true;
        const w = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;

        const handleWbloks = (url, getText) => {
            if (!url.includes('/async/wbloks/')) return;
            console.log('[IG] wbloks fetch caught:', url.slice(0, 150));
            getText().then(t => {
                console.log(`[IG] wbloks body length: ${t.length}, contains "clips": ${t.includes('clips')}, contains "media_code": ${t.includes('media_code')}`);
                const indexed = indexWbloksResponse(t);
                // If nothing indexed, dump a sample around "media_code" to help debug
                if (indexed === 0 && t.includes('media_code')) {
                    const idx = t.indexOf('media_code');
                    console.log('[IG] media_code context sample:', t.slice(Math.max(0, idx - 200), idx + 300));
                }
            }).catch(e => console.log('[IG] wbloks read error:', e));
        };

        const origFetch = w.fetch;
        w.fetch = function (input, init) {
            const url = typeof input === 'string' ? input : (input && input.url) || '';
            const p = origFetch.apply(this, arguments);
            handleWbloks(url, () => p.then(res => res.clone().text()));
            return p;
        };

        const XHR = w.XMLHttpRequest;
        const origOpen = XHR.prototype.open;
        XHR.prototype.open = function (method, url) {
            this._igUrl = url;
            return origOpen.apply(this, arguments);
        };
        const origSend = XHR.prototype.send;
        XHR.prototype.send = function () {
            const url = String(this._igUrl || '');
            if (url.includes('/async/wbloks/')) {
                this.addEventListener('load', () => {
                    handleWbloks(url, () => Promise.resolve(this.responseText));
                });
            }
            return origSend.apply(this, arguments);
        };
    }

    function initLikesPage() {
        if (!location.pathname.includes('/your_activity/interactions/likes')) return;

        patchFetchForWbloks();

        // Known aria-label values for post thumbnails across languages
        const POST_LABELS = [
            'Изображение публикации',   // Russian
            'Post Image',               // English
            'Bild des Beitrags',        // German
            'Image de la publication',  // French
            'Imagen de publicación',    // Spanish
            'Immagine del post',        // Italian
            'Imagem da publicação',     // Portuguese
        ];

        document.addEventListener('mousedown', e => {
            if (e.button !== 1) return;

            // Accept clicks on [role="button"] with a known post aria-label,
            // OR on any img inside the likes grid (fallback for unknown locales).
            const item = e.target.closest('[role="button"]');
            const isKnownLabel = item && POST_LABELS.includes(item.getAttribute('aria-label'));
            const hasImg = item && item.querySelector('img');

            // If it's a [role="button"] with an img but unknown label, still allow it
            // as long as it's NOT a sort/filter button (those never contain an img).
            if (!item || (!isKnownLabel && !hasImg)) return;

            // ALWAYS prevent default — otherwise browser does autoscroll on middle-click
            e.preventDefault();
            e.stopPropagation();

            const img = item.querySelector('img') || (e.target.tagName === 'IMG' ? e.target : null);

            // Try shortcode from ig_cache_key first
            let code = shortcodeForImg(img);

            // Fallback 1: look for a /p/ or /reel/ href on a nearby <a> ancestor
            if (!code) {
                const anchor = e.target.closest('a[href]') || item.closest('a[href]') || item.querySelector('a[href]');
                if (anchor) {
                    const mReel = anchor.href.match(/\/reel\/([A-Za-z0-9_-]+)/);
                    const mPost = anchor.href.match(/\/p\/([A-Za-z0-9_-]+)/);
                    code = (mReel && mReel[1]) || (mPost && mPost[1]) || null;
                }
            }

            // Fallback 2: scan all <a> inside the item
            if (!code) {
                for (const a of item.querySelectorAll('a[href]')) {
                    const mReel = a.href.match(/\/reel\/([A-Za-z0-9_-]+)/);
                    const mPost = a.href.match(/\/p\/([A-Za-z0-9_-]+)/);
                    code = (mReel && mReel[1]) || (mPost && mPost[1]) || null;
                    if (code) break;
                }
            }

            if (!code) {
                console.log(`[IG] post not indexed yet (mapSize=${codeByMediaId.size}). Scroll down to load more.`);
                return;
            }
            GM_openInTab(`https://www.instagram.com/reels/${code}/`, { active: false, insert: true });
        }, true);

        // Block auxclick so browser doesn't fire any fallback navigation
        document.addEventListener('auxclick', e => {
            if (e.button === 1) { e.preventDefault(); e.stopPropagation(); }
        }, true);
    }

    initLikesPage();

})();