я заебался мужики
// ==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();
})();