Instagram Fix

Instagram progress bar — click/drag to seek, with sound, loop, and height controls.

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

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

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

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.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

// ==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 };
  }
})();