Instagram progress bar — click/drag to seek, with sound, loop, and height controls.
// ==UserScript==
// @name Instagram Fix
// @namespace https://greasyfork.org/en/users/1521486-budget2540
// @version 3.2.3
// @license GNU AGPLv3
// @author budget2540
// @description Instagram progress bar — click/drag to seek, with sound, loop, and height controls.
// @match *://www.instagram.com/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
progressbar: {
height: 6,
color: '#fff',
elapsedColor: '#f00',
opacity: 0.66
},
video: {
disableLoop: true,
unmute: true
},
ui: {
updateInterval: 100,
showLength: true
}
};
const State = {
videoElementMap: new Map(),
containerMap: new Map(),
domObserver: null,
videoObserver: null,
seekDispatcherInstalled: false,
soundSyncDispatcherInstalled: false
};
const Utils = {
getProgressbarHeight() {
try {
if (typeof GM_getValue === 'function') {
return Number(GM_getValue('aivp_height', CONFIG.progressbar.height));
}
} catch (ex) {
console.warn('[AIVP] Failed to get height from storage:', ex);
}
return CONFIG.progressbar.height;
},
setProgressbarHeight(height) {
try {
if (typeof GM_setValue === 'function') {
GM_setValue('aivp_height', height);
}
} catch (ex) {
console.warn('[AIVP] Failed to save height to storage:', ex);
}
CONFIG.progressbar.height = height;
this.applyHeightToExisting(height);
},
applyHeightToExisting(height) {
const progressHeight = Number(height);
const hitAreaHeight = Math.max(16, progressHeight * 3);
for (const container of State.containerMap.keys()) {
try {
container.style.height = `${hitAreaHeight}px`;
const progressBar = container.querySelector('.aivp-elapsed');
if (progressBar) progressBar.style.height = `${progressHeight}px`;
const background = container.querySelector('.aivp-bg');
if (background) background.style.height = `${progressHeight}px`;
const leftPreview = container.querySelector('.aivp-left');
if (leftPreview) leftPreview.style.bottom = `${progressHeight + 6}px`;
} catch (ex) {
console.warn('[AIVP] Failed to update container height:', ex);
}
}
},
formatTime(seconds) {
if (!isFinite(seconds) || seconds < 0) return '0:00';
const totalSeconds = Math.floor(seconds);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const secs = totalSeconds % 60;
const minutesStr = (minutes < 10 && hours > 0) ? `0${minutes}` : String(minutes);
const secondsStr = secs < 10 ? `0${secs}` : String(secs);
return hours > 0 ? `${hours}:${minutesStr}:${secondsStr}` : `${minutesStr}:${secondsStr}`;
},
generateId() {
return `aivp${Date.now()}${Math.floor(Math.random()*1000)}`;
},
safeQuerySelector(element, selector) {
try {
return element?.querySelector(selector);
} catch (ex) {
return null;
}
},
safeQuerySelectorAll(element, selector) {
try {
return element?.querySelectorAll(selector) || [];
} catch (ex) {
return [];
}
},
getLoopEnabled() {
try {
if (typeof GM_getValue === 'function') {
const v = GM_getValue('aivp_loop', null);
if (v !== null && v !== undefined) return !!v;
}
} catch (ex) {
console.warn('[AIVP] Failed to get loop from storage:', ex);
}
return false;
},
setLoopEnabled(enabled) {
const val = !!enabled;
try {
if (typeof GM_setValue === 'function') GM_setValue('aivp_loop', val);
} catch (ex) {
console.warn('[AIVP] Failed to save loop to storage:', ex);
}
CONFIG.video.disableLoop = !val;
this.applyLoopToExisting(val);
},
applyLoopToExisting(enabled) {
try {
for (const video of State.videoElementMap.keys()) {
if (!video || !document.contains(video)) { try { State.videoElementMap.delete(video); } catch (e) {} continue; }
try {
if (enabled) {
video.loop = true;
try { video.setAttribute('loop', ''); } catch (e) {}
} else {
video.loop = false;
try { video.removeAttribute('loop'); } catch (e) {}
}
} catch (ex) { /* ignore per-video */ }
}
} catch (ex) {
console.warn('[AIVP] Failed to apply loop to existing:', ex);
}
},
getSoundEnabled() {
try {
if (typeof GM_getValue === 'function') {
const v = GM_getValue('aivp_sound', null);
if (v !== null && v !== undefined) return !!v;
}
} catch (ex) {
console.warn('[AIVP] Failed to get sound from storage:', ex);
}
return true;
},
setSoundEnabled(enabled) {
const val = !!enabled;
try {
if (typeof GM_setValue === 'function') GM_setValue('aivp_sound', val);
} catch (ex) {
console.warn('[AIVP] Failed to save sound to storage:', ex);
}
CONFIG.video.unmute = val;
this.applySoundToExisting(val);
try {
const sw = SettingsPanel.panel && SettingsPanel.panel.querySelector ? SettingsPanel.panel.querySelector('#aivp-sw-sound') : null;
if (sw) sw.checked = val;
} catch (e) {}
},
applySoundToExisting(enabled) {
try {
for (const video of State.videoElementMap.keys()) {
if (!video || !document.contains(video)) { try { State.videoElementMap.delete(video); } catch (e) {} continue; }
try {
try { ProgressbarSetup.setVideoSound(video, enabled); } catch (e) { /* ignore */ }
} catch (ex) { /* ignore per-video */ }
}
} catch (ex) {
console.warn('[AIVP] Failed to apply sound to existing:', ex);
}
},
getShowLength() {
try {
if (typeof GM_getValue === 'function') {
const v = GM_getValue('aivp_showLength', null);
if (v !== null && v !== undefined) return !!v;
}
} catch (ex) {
console.warn('[AIVP] Failed to get showLength from storage:', ex);
}
return true;
},
setShowLength(enabled) {
const val = !!enabled;
try {
if (typeof GM_setValue === 'function') GM_setValue('aivp_showLength', val);
} catch (ex) {
console.warn('[AIVP] Failed to save showLength to storage:', ex);
}
CONFIG.ui.showLength = val;
this.applyShowLengthToExisting(val);
},
applyShowLengthToExisting(enabled) {
try {
for (const container of State.containerMap.keys()) {
try {
const left = container.querySelector('.aivp-left');
if (left) left.style.display = enabled ? '' : 'none';
} catch (e) { /* ignore per-container */ }
}
} catch (ex) {
console.warn('[AIVP] Failed to apply showLength to existing:', ex);
}
}
};
const MenuCommands = {
register() {
try {
if (typeof GM_registerMenuCommand !== 'function') return;
GM_registerMenuCommand('Instagram Fix Settings', () => SettingsPanel.toggle());
} catch (ex) {
console.warn('[AIVP] Failed to register menu commands:', ex);
}
}
};
const ContainerManager = {
register(container, state) {
State.containerMap.set(container, state);
this.ensureDOMObserver();
},
unregister(container) {
try {
State.containerMap.delete(container);
} catch (ex) {
console.warn('[AIVP] Failed to unregister container:', ex);
}
if (State.containerMap.size === 0) this.cleanup();
},
ensureDOMObserver() {
if (State.domObserver) return;
try {
State.domObserver = new MutationObserver(() => this.cleanupDetachedContainers());
const root = document.documentElement || document.body || document;
State.domObserver.observe(root, { childList: true, subtree: true });
} catch (ex) {
console.warn('[AIVP] Failed to create DOM observer:', ex);
}
},
cleanupDetachedContainers() {
try {
for (const [container, state] of State.containerMap.entries()) {
if (!document.contains(container)) {
if (state?.cleanup) {
try { state.cleanup(); } catch (ex) { console.warn('[AIVP] Error in container cleanup:', ex); }
}
State.containerMap.delete(container);
}
}
if (State.containerMap.size === 0) this.cleanup();
} catch (ex) {
console.warn('[AIVP] Error cleaning up detached containers:', ex);
}
},
cleanup() {
try {
if (State.domObserver) {
State.domObserver.disconnect();
State.domObserver = null;
}
} catch (ex) {
console.warn('[AIVP] Error during cleanup:', ex);
}
}
};
const VideoInteraction = {
hitTest(container, clientX, clientY) {
try {
const rect = container.getBoundingClientRect();
if (!rect || !rect.width) return false;
return clientX >= rect.left && clientX <= rect.right &&
clientY >= rect.top && clientY <= rect.bottom;
} catch (ex) {
return false;
}
},
setupSeekableProgressbar(video, container, elapsedBar) {
const state = {};
state.getVideo = () => {
try {
const live = container.parentElement && container.parentElement.querySelector('video');
if (live && document.contains(live)) return live;
} catch (ex) { /* ignore */ }
return document.contains(video) ? video : null;
};
state.seek = (clientX) => {
const v = state.getVideo() || video;
if (!v || !isFinite(v.duration) || v.duration === 0) return;
const rect = container.getBoundingClientRect();
const x = clientX - rect.left;
const w = rect.width || container.offsetWidth || 1;
const percent = Math.max(0, Math.min(1, x / w));
v.currentTime = percent * v.duration;
elapsedBar.style.width = `${Math.ceil(percent * container.offsetWidth)}px`;
ProgressbarSetup.updateLengthLabel(v, elapsedBar);
};
ContainerManager.register(container, state);
},
installSeekDispatcher() {
if (State.seekDispatcherInstalled) return;
State.seekDispatcherInstalled = true;
let drag = null;
const overSettingsPanel = (e) => {
try {
const panel = SettingsPanel.panel;
return !!(panel && e.target instanceof Node && panel.contains(e.target));
} catch (ex) {
return false;
}
};
const findContainer = (e) => {
for (const container of State.containerMap.keys()) {
try {
if (!document.contains(container)) continue;
if (VideoInteraction.hitTest(container, e.clientX, e.clientY)) return container;
} catch (ex) { /* ignore */ }
}
return null;
};
const onPointerDown = (e) => {
if (e.button !== 0) return;
if (overSettingsPanel(e)) return;
const container = findContainer(e);
if (!container) return;
const st = State.containerMap.get(container);
if (!st) return;
e.stopPropagation();
e.preventDefault();
const v = st.getVideo();
drag = { st, wasPlaying: v ? !v.paused : false };
st.seek(e.clientX);
};
const onPointerMove = (e) => {
if (!drag) return;
e.preventDefault();
drag.st.seek(e.clientX);
};
const onPointerUp = (e) => {
if (!drag) return;
e.preventDefault();
drag.st.seek(e.clientX);
const v = drag.st.getVideo();
if (drag.wasPlaying && v) {
try { v.play(); } catch (ex) { /* ignore */ }
}
drag = null;
};
const onPointerCancel = () => {
drag = null;
};
const onClick = (e) => {
if (overSettingsPanel(e)) return;
const container = findContainer(e);
if (!container) return;
e.stopPropagation();
e.preventDefault();
};
try {
document.addEventListener('pointerdown', onPointerDown, { capture: true, passive: false });
document.addEventListener('pointermove', onPointerMove, { capture: true, passive: false });
document.addEventListener('pointerup', onPointerUp, { capture: true, passive: false });
document.addEventListener('pointercancel', onPointerCancel, true);
document.addEventListener('click', onClick, true);
} catch (ex) {
console.warn('[AIVP] Failed to install seek dispatcher:', ex);
}
}
};
const ProgressbarSetup = {
setupVideo(video) {
if (!video || !video.parentNode) return;
this.setLoop(video, Utils.getLoopEnabled());
const { container, elapsedBar } = this.createProgressbarUI(video);
State.videoElementMap.set(video, elapsedBar);
const parent = video.parentNode;
try {
const computed = parent && parent.nodeType === 1 ? getComputedStyle(parent) : null;
if (computed && computed.position === 'static') parent.style.position = 'relative';
} catch (ex) { /* ignore */ }
try { container.style.zIndex = '2147483647'; } catch (ex) { /* ignore */ }
if (parent && parent.appendChild) parent.appendChild(container);
VideoInteraction.setupSeekableProgressbar(video, container, elapsedBar);
this.setupVideoEventHandlers(video, elapsedBar);
this.setVideoSound(video, Utils.getSoundEnabled());
},
setLoop(video, enabled) {
try {
video.loop = !!enabled;
if (enabled) {
try { video.setAttribute('loop', ''); } catch (e) {}
try { video.removeAttribute('noloop'); } catch (e) {}
} else {
try { video.removeAttribute('loop'); } catch (e) {}
try { video.setAttribute('noloop', ''); } catch (e) {}
}
} catch (ex) { /* ignore */ }
},
createProgressbarUI(video) {
const containerId = Utils.generateId();
const elapsedBarId = `${containerId}bar`;
const progressHeight = Utils.getProgressbarHeight();
const hitAreaHeight = Math.max(16, progressHeight * 3);
const container = document.createElement('div');
container.id = containerId;
container.innerHTML = `<style>
#${containerId} {
position: absolute;
opacity: ${CONFIG.progressbar.opacity};
left: 0;
right: 0;
bottom: 0;
height: ${hitAreaHeight}px;
background: transparent;
cursor: pointer;
z-index: 9999;
touch-action: none;
}
#${elapsedBarId} {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: ${progressHeight}px;
width: 0;
transition: width 100ms linear;
background: ${CONFIG.progressbar.elapsedColor};
}
.aivp-bg {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: ${progressHeight}px;
background: ${CONFIG.progressbar.color};
opacity: 0.25;
}
.aivp-left {
position: absolute;
left: 8px;
bottom: ${progressHeight + 6}px;
background: rgba(0,0,0,0.75);
color: #fff;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
pointer-events: none;
white-space: nowrap;
opacity: 1;
transition: opacity 120ms;
z-index: 10000;
}
</style>
<div class="aivp-bg"></div>
<div id="${elapsedBarId}" class="aivp-elapsed"></div>
<div class="aivp-left">0:00 / 0:00</div>`;
if (!CONFIG.ui.showLength) {
const left = container.querySelector('.aivp-left');
if (left) left.style.display = 'none';
}
const elapsedBar = container.querySelector(`#${elapsedBarId}`);
return { container, elapsedBar };
},
updateLengthLabel(video, elapsedBar) {
if (!CONFIG.ui.showLength) return;
try {
const left = elapsedBar.parentNode?.querySelector('.aivp-left');
if (!left) return;
const d = Number(video.duration);
const total = isFinite(d) && d > 0 ? Utils.formatTime(d) : '0:00';
left.textContent = `${Utils.formatTime(video.currentTime)} / ${total}`;
} catch (ex) { /* ignore */ }
},
setupVideoEventHandlers(video, elapsedBar) {
let updateTimer = null;
const updateProgressBar = () => {
this.updateLengthLabel(video, elapsedBar);
if (!isFinite(video.duration) || video.duration === 0) return;
const container = elapsedBar.parentNode;
if (!container) return;
const width = container.offsetWidth || 1;
elapsedBar.style.width = `${Math.ceil((video.currentTime / video.duration) * width)}px`;
};
const startTimer = () => {
if (CONFIG.video.disableLoop) {
try { video.loop = false; } catch (ex) { /* ignore */ }
}
if (!updateTimer) updateTimer = setInterval(updateProgressBar, CONFIG.ui.updateInterval);
};
const stopTimer = (event) => {
if (event.type === 'ended') {
try {
if (CONFIG.video.disableLoop) {
try { video.pause(); } catch (e) { /* ignore */ }
try { video.loop = false; } catch (e) { /* ignore */ }
try { if (video.removeAttribute) video.removeAttribute('loop'); } catch (e) { /* ignore */ }
}
elapsedBar.style.width = '100%';
} catch (ex) {
console.warn('[AIVP] Error handling ended event:', ex);
}
}
try {
const container = elapsedBar.parentNode;
const leftPreview = container?.querySelector('.aivp-left');
if (leftPreview) leftPreview.textContent = `${Utils.formatTime(video.currentTime)} / ${Utils.formatTime(video.duration)}`;
} catch (ex) { /* ignore */ }
if (updateTimer) {
clearInterval(updateTimer);
updateTimer = null;
}
};
video.addEventListener('play', startTimer);
video.addEventListener('playing', startTimer);
video.addEventListener('waiting', stopTimer);
video.addEventListener('pause', stopTimer);
video.addEventListener('ended', stopTimer);
video.addEventListener('timeupdate', () => this.updateLengthLabel(video, elapsedBar));
video.addEventListener('loadedmetadata', () => this.updateLengthLabel(video, elapsedBar));
this.updateLengthLabel(video, elapsedBar);
},
findAudioToggle(video) {
try {
const precise = '[aria-label="Toggle audio"], [aria-label="Mute"], [aria-label="Unmute"]';
let node = video;
for (let i = 0; i < 10 && node; i++) {
const btn = node.querySelector?.(precise);
if (btn) return btn;
node = node.parentElement;
}
const broad = '[aria-label*="audio" i], [aria-label*="mute" i], [aria-label*="sound" i], [aria-label*="volume" i]';
node = video;
for (let i = 0; i < 10 && node; i++) {
const btn = node.querySelector?.(broad);
if (btn) return btn;
node = node.parentElement;
}
try {
const root = video.closest?.('article') || video.closest?.('section') || video.closest?.('div') || document;
const candidates = root.querySelectorAll('div[role="button"], button, [role="button"]');
const vRect = video.getBoundingClientRect();
for (const c of candidates) {
if (!c.querySelector('svg')) continue;
const r = c.getBoundingClientRect();
if (r.width < 16 || r.width > 80 || r.height < 16 || r.height > 80) continue;
if (r.top >= vRect.top - 20 && r.left >= vRect.left - 20 && r.top <= vRect.bottom && r.left <= vRect.right) return c;
}
} catch (e) {}
} catch (ex) { /* ignore */ }
return null;
},
setVideoSound(video, enabled) {
try {
const wantMuted = !enabled;
const toggle = this.findAudioToggle(video);
const needsChange = video.muted !== wantMuted;
let toggleNeedsClick = false;
if (toggle && !needsChange) {
const label = (toggle.getAttribute('aria-label') || '').toLowerCase();
if (enabled && label.includes('unmute')) toggleNeedsClick = true;
if (!enabled && label.includes('mute') && !label.includes('unmute')) toggleNeedsClick = true;
}
if (needsChange || toggleNeedsClick) {
try { video._aivpSyncing = true; } catch (e) {}
if (toggle) {
try { toggle.click(); } catch (e) {}
}
try { video.muted = wantMuted; } catch (e) {}
if (enabled) { try { video.volume = 1; } catch (e) {} }
setTimeout(() => { try { video._aivpSyncing = false; } catch (e) {} }, 0);
setTimeout(() => {
try {
if (!document.contains(video)) return;
if (video.muted !== wantMuted) {
try { video._aivpSyncing = true; } catch (e) {}
const t2 = this.findAudioToggle(video);
if (t2) { try { t2.click(); } catch (e) {} }
try { video.muted = wantMuted; } catch (e) {}
if (enabled) try { video.volume = 1; } catch (e) {}
setTimeout(() => { try { video._aivpSyncing = false; } catch (e) {} }, 0);
}
} catch (e) {}
}, 400);
if (!toggle) {
setTimeout(() => {
try {
if (!document.contains(video)) return;
const t2 = this.findAudioToggle(video);
if (t2 && video.muted !== wantMuted) {
try { video._aivpSyncing = true; } catch (e) {}
try { t2.click(); } catch (e) {}
try { video.muted = wantMuted; } catch (e) {}
setTimeout(() => { try { video._aivpSyncing = false; } catch (e) {} }, 0);
}
} catch (e) {}
}, 1200);
}
} else {
if (enabled) { try { if (video.volume < 0.5) video.volume = 1; } catch (e) {} }
}
// no _aivpSoundEnforced listeners — removed; manual icon clicks sync via delegated dispatcher
} catch (ex) {
console.warn('[AIVP] Failed to set video sound:', ex);
}
},
installSoundSyncDispatcher() {
if (State.soundSyncDispatcherInstalled) return;
State.soundSyncDispatcherInstalled = true;
const isAudioToggle = (el) => {
try {
if (!el || !(el instanceof Element)) return false;
const sel = '[aria-label="Toggle audio"], [aria-label="Mute"], [aria-label="Unmute"]';
if (el.matches && el.matches(sel)) return true;
if (el.closest && el.closest(sel)) return true;
// broad fallback like findAudioToggle second pass
const broad = el.closest ? el.closest('[aria-label*="audio" i], [aria-label*="mute" i], [aria-label*="sound" i], [aria-label*="volume" i]') : null;
if (broad) return true;
} catch (e) {}
return false;
};
const findVideoForToggle = (toggleEl) => {
try {
// walk up to article/section/div then query video
let node = toggleEl;
for (let i = 0; i < 6 && node; i++) {
const v = node.querySelector ? node.querySelector('video') : null;
if (v && document.contains(v)) return v;
// also check parent's videos
const parent = node.parentElement;
if (parent) {
const pv = parent.querySelector ? parent.querySelector('video') : null;
if (pv && document.contains(pv)) return pv;
}
node = node.parentElement;
}
// fallback: nearest video in document closest to toggle rect
const allVideos = document.querySelectorAll ? document.querySelectorAll('video') : [];
for (const v of allVideos) {
if (!document.contains(v)) continue;
// check if toggle is near video rect (like findAudioToggle)
try {
const vr = v.getBoundingClientRect();
const tr = toggleEl.getBoundingClientRect();
if (tr.width >= 16 && tr.width <= 80 && tr.height >= 16 && tr.height <= 80) {
if (tr.top >= vr.top - 20 && tr.left >= vr.left - 20 && tr.top <= vr.bottom && tr.left <= vr.right) return v;
}
} catch (e) {}
}
} catch (e) {}
return null;
};
const onDocClick = (e) => {
try {
if (e.button !== 0) return;
// ignore clicks inside settings panel
try { if (SettingsPanel.panel && e.target instanceof Node && SettingsPanel.panel.contains(e.target)) return; } catch (e2) {}
let toggleEl = null;
const t = e.target;
// handle Element targets and text nodes (parentElement fallback)
const startEl = (t instanceof Element) ? t : (t && t.parentElement instanceof Element ? t.parentElement : null);
if (startEl) {
if (isAudioToggle(startEl)) toggleEl = startEl;
else if (startEl.closest) {
const c = startEl.closest('[aria-label="Toggle audio"], [aria-label="Mute"], [aria-label="Unmute"], [aria-label*="audio" i], [aria-label*="mute" i], [aria-label*="sound" i], [aria-label*="volume" i]');
if (c) toggleEl = c;
if (!toggleEl) {
const btn = startEl.closest('div[role="button"], button, [role="button"]');
if (btn && btn.querySelector && btn.querySelector('svg')) toggleEl = btn;
}
}
// if startEl itself is svg/path inside button, also try walking up manually
if (!toggleEl) {
let p = startEl.parentElement;
for (let i = 0; i < 4 && p; i++) {
if (isAudioToggle(p)) { toggleEl = p; break; }
const c = p.closest ? p.closest('[aria-label="Toggle audio"], [aria-label="Mute"], [aria-label="Unmute"]') : null;
if (c) { toggleEl = c; break; }
p = p.parentElement;
}
}
}
if (!toggleEl) return;
// resolve video
let video = findVideoForToggle(toggleEl);
if (!video) {
// last resort: most recently registered video near click
for (const v of State.videoElementMap.keys()) {
if (document.contains(v)) { video = v; break; }
}
}
if (!video) return;
// guard programmatic clicks
try { if (video._aivpSyncing) return; } catch (e) {}
// IG updates muted async after click; read after microtask + delay
setTimeout(() => {
try {
if (!document.contains(video)) return;
if (video._aivpSyncing) return;
const nowEnabled = !video.muted;
if (nowEnabled === Utils.getSoundEnabled()) return;
Utils.setSoundEnabled(nowEnabled);
} catch (e) {}
}, 160);
} catch (ex) { /* ignore */ }
};
try {
document.addEventListener('click', onDocClick, true);
} catch (e) {
console.warn('[AIVP] Failed to install sound sync dispatcher:', e);
}
}
};
const VideoDiscovery = {
ensureVideoHasProgressbar(video) {
try {
if (!video || String(video.tagName).toLowerCase() !== 'video') return;
if (video.getAttribute('aivp_done')) {
if (!State.videoElementMap.get(video) && video.readyState >= 2) {
try { ProgressbarSetup.setupVideo(video); } catch (ex) { console.warn('[AIVP] Error setting up video:', ex); }
}
return;
}
video.setAttribute('aivp_done', '1');
if (video.readyState >= 2) {
try { ProgressbarSetup.setupVideo(video); } catch (ex) { console.warn('[AIVP] Error setting up video:', ex); }
} else {
video.addEventListener('canplay', function onCanPlay() {
try { ProgressbarSetup.setupVideo(video); } catch (ex) { console.warn('[AIVP] Error setting up video on canplay:', ex); }
video.removeEventListener('canplay', onCanPlay);
}, { once: true });
}
} catch (ex) {
console.warn('[AIVP] Error ensuring video has progressbar:', ex);
}
},
startMonitoring() {
try {
const existingVideos = document.querySelectorAll('video');
existingVideos.forEach(video => this.ensureVideoHasProgressbar(video));
State.videoObserver = new MutationObserver((mutations) => {
try {
for (const mutation of mutations) {
if (mutation.addedNodes && mutation.addedNodes.length) {
for (const node of mutation.addedNodes) {
if (!node) continue;
if (node.tagName && String(node.tagName).toLowerCase() === 'video') {
this.ensureVideoHasProgressbar(node);
} else if (node.querySelectorAll) {
const descendantVideos = node.querySelectorAll('video');
descendantVideos.forEach(video => this.ensureVideoHasProgressbar(video));
}
}
}
}
} catch (ex) {
console.warn('[AIVP] Error in video observer:', ex);
}
});
const root = document.documentElement || document.body || document;
State.videoObserver.observe(root, { childList: true, subtree: true });
} catch (ex) {
console.warn('[AIVP] Failed to start video monitoring:', ex);
}
}
};
const SettingsPanel = {
panel: null,
escHandler: null,
outsideHandler: null,
toggle() {
if (this.panel && document.contains(this.panel)) {
this.close();
return;
}
this.open();
},
open() {
this.close();
const panel = document.createElement('div');
panel.id = 'aivp-settings-panel';
panel.innerHTML = `<style>
#aivp-settings-panel{position:fixed;bottom:16px;right:16px;z-index:2147483647;width:250px;background:#1c1c1e;color:#fff;border-radius:12px;padding:14px;font:13px -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;box-shadow:0 8px 24px rgba(0,0,0,.5)}
#aivp-settings-panel h3{margin:0 0 10px;font-size:14px;display:flex;justify-content:space-between;align-items:center}
#aivp-settings-panel h3 button{background:none;border:none;color:#aaa;font-size:16px;cursor:pointer}
.aivp-row{display:flex;justify-content:space-between;align-items:center;margin:10px 0}
.aivp-switch{position:relative;width:40px;height:22px;-webkit-appearance:none;appearance:none;background:#555;border-radius:11px;cursor:pointer;outline:none;transition:background .15s;flex-shrink:0}
.aivp-switch:checked{background:#3897f0}
.aivp-switch::before{content:'';position:absolute;top:2px;left:2px;width:18px;height:18px;background:#fff;border-radius:50%;transition:transform .15s}
.aivp-switch:checked::before{transform:translateX(18px)}
#aivp-settings-panel input[type=number]{width:56px;background:#333;border:1px solid #555;color:#fff;border-radius:6px;padding:4px 6px}
#aivp-settings-panel .aivp-reset{background:none;border:none;color:#3897f0;cursor:pointer;font-size:12px}
</style>
<h3>Instagram Fix <button title=\"Close\">✕</button></h3>
<div class=\"aivp-row\"><span>Loop video</span><input type=\"checkbox\" class=\"aivp-switch\" id=\"aivp-sw-loop\"></div>
<div class=\"aivp-row\"><span>Sound</span><input type=\"checkbox\" class=\"aivp-switch\" id=\"aivp-sw-sound\"></div>
<div class=\"aivp-row\"><span>Show video length</span><input type=\"checkbox\" class=\"aivp-switch\" id=\"aivp-sw-length\"></div>
<div class=\"aivp-row\"><span>Progressbar height</span><span><input type=\"number\" id=\"aivp-in-height\" min=\"0\" max=\"100\"> <button class=\"aivp-reset\">Reset</button></span></div>`;
document.body.appendChild(panel);
this.panel = panel;
const loopSw = panel.querySelector('#aivp-sw-loop');
const soundSw = panel.querySelector('#aivp-sw-sound');
const heightIn = panel.querySelector('#aivp-in-height');
loopSw.checked = Utils.getLoopEnabled();
soundSw.checked = Utils.getSoundEnabled();
heightIn.value = Utils.getProgressbarHeight();
loopSw.addEventListener('change', () => Utils.setLoopEnabled(loopSw.checked));
soundSw.addEventListener('change', () => Utils.setSoundEnabled(soundSw.checked));
const lengthSw = panel.querySelector('#aivp-sw-length');
lengthSw.checked = Utils.getShowLength();
lengthSw.addEventListener('change', () => Utils.setShowLength(lengthSw.checked));
const onHeightChange = () => {
let v = parseInt(heightIn.value, 10);
if (isNaN(v)) { heightIn.value = Utils.getProgressbarHeight(); return; }
v = Math.max(0, Math.min(100, v));
heightIn.value = String(v);
Utils.setProgressbarHeight(v);
};
heightIn.addEventListener('input', onHeightChange);
heightIn.addEventListener('change', onHeightChange);
panel.querySelector('.aivp-reset').addEventListener('click', () => {
Utils.setProgressbarHeight(6);
heightIn.value = 6;
});
panel.querySelector('h3 button').addEventListener('click', () => this.close());
this.escHandler = (e) => { if (e.key === 'Escape') this.close(); };
this.outsideHandler = (e) => {
if (!this.panel) return;
const path = typeof e.composedPath === 'function' ? e.composedPath() : [];
if (path.length) { if (path.includes(this.panel) || path.some(el => el instanceof Node && this.panel.contains(el))) return; }
if (e.target instanceof Node && this.panel.contains(e.target)) return;
this.close();
};
document.addEventListener('keydown', this.escHandler, true);
// defer so the opening click does not instantly close the panel
setTimeout(() => document.addEventListener('mousedown', this.outsideHandler, true), 0);
},
close() {
if (this.panel) {
this.panel.remove();
this.panel = null;
}
if (this.escHandler) {
document.removeEventListener('keydown', this.escHandler, true);
this.escHandler = null;
}
if (this.outsideHandler) {
document.removeEventListener('mousedown', this.outsideHandler, true);
this.outsideHandler = null;
}
}
};
const init = () => {
try {
try {
CONFIG.video.disableLoop = !Utils.getLoopEnabled();
CONFIG.video.unmute = Utils.getSoundEnabled();
CONFIG.progressbar.height = Utils.getProgressbarHeight();
CONFIG.ui.showLength = Utils.getShowLength();
} catch (ex) { /* ignore */ }
MenuCommands.register();
VideoDiscovery.startMonitoring();
VideoInteraction.installSeekDispatcher();
ProgressbarSetup.installSoundSyncDispatcher();
console.log('[Instagram Fix] initialized — loop:' + (Utils.getLoopEnabled() ? 'ON' : 'OFF') + ' sound:' + (Utils.getSoundEnabled() ? 'ON' : 'OFF'));
} catch (ex) {
console.error('[AIVP] Failed to initialize:', ex);
}
};
init();
if (typeof module !== 'undefined' && module.exports) {
module.exports = { Utils, CONFIG, ProgressbarSetup, VideoInteraction, State, ContainerManager, SettingsPanel };
}
})();