Add Instagram Video Progressbar (Improved)

Improved fork of "Add Instagram Video Progressbar" (original: jcunews). Adds keyboard seek & focus fixes, throttled pointer handling for better perf, robust DOM cleanup, persistent progressbar-height setting with menu commands, and improved unmute/story handling.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Add Instagram Video Progressbar (Improved)
// @namespace    https://greasyfork.org/en/users/1521486-budget2540
// @version      2.0.2
// @license      GNU AGPLv3
// @author       budget2540
// @description  Improved fork of "Add Instagram Video Progressbar" (original: jcunews). Adds keyboard seek & focus fixes, throttled pointer handling for better perf, robust DOM cleanup, persistent progressbar-height setting with menu commands, and improved unmute/story handling.
// @match        *://www.instagram.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @run-at       document-start
// ==/UserScript==

(function() {
  'use strict';

  //===== CONFIGURATION BEGIN =====
  const CONFIG = {
    progressbar: {
      height: 6,             // in pixels. set to zero to hide
      color: '#fff',         // background color
      elapsedColor: '#f00',  // elapsed time color
      opacity: 0.66,         // progressbar opacity
      hudOpacity: 0.95       // HUD tooltip opacity
    },
    video: {
      disableLoop: true,     // disable video looping
      unmute: true           // automatically unmute videos
    },
    keyboard: {
      seekStep: 3            // seconds to seek per arrow key press (default)
    },
    ui: {
      updateInterval: 100,   // milliseconds between progress updates
      hudFadeDelay: 250,     // milliseconds before HUD fades out
    }
  };
  //===== CONFIGURATION END =====

  // ===== CORE STATE MANAGEMENT =====
  const State = {
    videoElementMap: new WeakMap(),        // maps video elements to their elapsed bar elements
    containerMap: new Map(),                // tracks all progressbar containers
    activeVideo: null,                      // currently focused/hovered video for keyboard controls
    keyHandlerInstalled: false,             // global keydown handler flag
    globalPointerHandlerInstalled: false,   // global pointermove handler flag
    lastHoveredContainer: null,             // last hovered container for optimization
    pointerMoveScheduled: false,            // rAF throttling flag
    lastPointerEvent: null,                 // cached pointer event
    domObserver: null,                      // MutationObserver for cleanup
    videoObserver: null,                    // MutationObserver for new videos
    originalAddEventListener: null,         // original HTMLVideoElement.addEventListener
    scrollHandlerInstalled: false,          // scroll handler flag
    scrollUpdateScheduled: false,           // throttling flag for scroll updates
    nextButton: null,
    loopObservers: new WeakMap()            // per-video mutation observers to guard against loop attr re-adding
  };

  // ===== UTILITY FUNCTIONS =====
  const Utils = {
    // Persistent settings with GM_getValue/GM_setValue fallback
    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 = progressHeight;
      
      document.querySelectorAll('div[id^="aivp"]').forEach(container => {
        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 hud = container.querySelector('.aivp-hud');
          if (hud) {
            hud.style.bottom = `${progressHeight + 6}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);
        }
      });
    },

    // Format seconds to HH:MM:SS or MM:SS
    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}`;
    },

    // Generate unique ID for containers
    generateId() {
      return `aivp${Date.now()}`;
    },

    // Safe element query
    safeQuerySelector(element, selector) {
      try {
        return element?.querySelector(selector);
      } catch (ex) {
        return null;
      }
    },

    // Safe element query all
    safeQuerySelectorAll(element, selector) {
      try {
        return element?.querySelectorAll(selector) || [];
      } catch (ex) {
        return [];
      }
    },

    // Persistent seek-step (seconds) with GM_getValue/GM_setValue fallback
    getSeekStep() {
      try {
        if (typeof GM_getValue === 'function') {
          const v = Number(GM_getValue('aivp_seek_step', CONFIG.keyboard.seekStep));
          if (isFinite(v) && v > 0) return v;
        }
      } catch (ex) {
        console.warn('[AIVP] Failed to get seek step from storage:', ex);
      }
      return CONFIG.keyboard.seekStep;
    },

    setSeekStep(seconds) {
      try {
        const s = Number(seconds);
        if (!isFinite(s) || s <= 0) return;
        if (typeof GM_setValue === 'function') {
          GM_setValue('aivp_seek_step', s);
        }
        CONFIG.keyboard.seekStep = s;
      } catch (ex) {
        console.warn('[AIVP] Failed to save seek step to storage:', ex);
      }
    },

    // Debugging aid: log structured data
    logData(label, data) {
      console.log(`[AIVP] ${label}:`, JSON.stringify(data, null, 2));
    }
  };

  // ===== MENU COMMANDS =====
  const MenuCommands = {
    register() {
      try {
        if (typeof GM_registerMenuCommand !== 'function') return;

        GM_registerMenuCommand('Set AIVP progressbar height (px)', () => {
          const current = Utils.getProgressbarHeight();
          const input = prompt('Set progress bar height in pixels (0 to hide):', String(current));
          
          if (input === null) return;
          
          const value = parseInt(input, 10);
          if (isNaN(value) || value < 0) {
            alert('Invalid height value. Please enter a number >= 0.');
            return;
          }
          
          Utils.setProgressbarHeight(value);
          alert(`Progress bar height set to ${value}px`);
        });

        GM_registerMenuCommand('Reset AIVP progressbar height to default', () => {
          const defaultHeight = 6;
          Utils.setProgressbarHeight(defaultHeight);
          alert(`Progress bar height reset to default (${defaultHeight}px`);
        });

        // New commands for seek step configuration
        GM_registerMenuCommand('Set AIVP seek step (seconds)', () => {
          const current = Utils.getSeekStep();
          const input = prompt('Set seek step in seconds (e.g. 0.5, 1.5, 3):', String(current));
          if (input === null) return;
          const value = parseFloat(input);
          if (!isFinite(value) || value <= 0) {
            alert('Invalid seek step value. Please enter a number > 0.');
            return;
          }
          Utils.setSeekStep(value);
          alert(`Seek step set to ${value} second(s)`);
        });

        GM_registerMenuCommand('Reset AIVP seek step to default', () => {
          const defaultSeek = 3;
          Utils.setSeekStep(defaultSeek);
          alert(`Seek step reset to default (${defaultSeek} seconds)`);
        });
       } catch (ex) {
         console.warn('[AIVP] Failed to register menu commands:', ex);
       }
     }
   };

  // ===== KEYBOARD CONTROLS =====
  const KeyboardControls = {
    handleKeydown(event) {
      // Only react to left/right arrows and when not typing in an input
      if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
      
      const activeElement = document.activeElement;
      if (activeElement && (
        activeElement.tagName === 'INPUT' || 
        activeElement.tagName === 'TEXTAREA' || 
        activeElement.isContentEditable
      )) {
        return;
      }
      
      const video = State.activeVideo;
      if (!video || !isFinite(video.duration) || video.duration === 0) return;
      
      event.preventDefault();
      
      // Use persisted/configured seek step
      const step = Utils.getSeekStep();
      const delta = (event.key === 'ArrowLeft') ? -step : step;
      const newTime = Math.max(0, Math.min(video.duration, video.currentTime + delta));
      
      try {
        video.currentTime = newTime;
        
        // Update visual bar immediately
        const elapsedBar = State.videoElementMap.get(video);
        if (elapsedBar && elapsedBar.parentNode) {
          const container = elapsedBar.parentNode;
          const width = container.offsetWidth || 1;
          elapsedBar.style.width = `${Math.ceil((newTime / video.duration) * width)}px`;
        }
      } catch (ex) {
        console.warn('[AIVP] Failed to seek video:', ex);
      }
    },

    install() {
      if (State.keyHandlerInstalled) return;
      
      document.addEventListener('keydown', this.handleKeydown.bind(this), false);
      State.keyHandlerInstalled = true;
    }
  };

  // ===== POINTER/HOVER MANAGEMENT =====
  const PointerManager = {
    handleGlobalPointerMove(event) {
      // Only capture and schedule; actual processing happens in rAF to avoid running at full pointer frequency
      State.lastPointerEvent = event;
      
      if (!State.pointerMoveScheduled) {
        State.pointerMoveScheduled = true;
        requestAnimationFrame(() => this.processPointerMove());
      }
    },

    processPointerMove() {
      State.pointerMoveScheduled = false;
      const event = State.lastPointerEvent;
      State.lastPointerEvent = null;
      
      if (!event) return;
      
      try {
        const targetContainer = event.target?.closest?.('[id^="aivp"]');
        
        // Nothing changed, skip
        if (targetContainer === State.lastHoveredContainer) return;

        // Hide previous hovered container (if not dragging)
        if (State.lastHoveredContainer) {
          const prevState = State.containerMap.get(State.lastHoveredContainer);
          if (prevState && !prevState.dragging && prevState.hideHudDelayed) {
            prevState.hideHudDelayed(150);
          }
        }

        // Show new container if any
        if (targetContainer) {
          const newState = State.containerMap.get(targetContainer);
          if (newState?.showHud) {
            newState.showHud();
          }
          State.lastHoveredContainer = targetContainer;
        } else {
          State.lastHoveredContainer = null;
        }
      } catch (ex) {
        console.warn('[AIVP] Error processing pointer move:', ex);
      }
    },

    install() {
      if (State.globalPointerHandlerInstalled) return;
      
      try {
        const handler = (e) => this.handleGlobalPointerMove(e);
        document.addEventListener('pointermove', handler, { passive: true });
        State.globalPointerHandlerInstalled = true;
        State.pointerHandler = handler;
      } catch (ex) {
        console.warn('[AIVP] Failed to install pointer handler:', ex);
      }
    },

    uninstall() {
      if (!State.globalPointerHandlerInstalled) return;
      
      try {
        if (State.pointerHandler) {
          document.removeEventListener('pointermove', State.pointerHandler, { passive: true });
        }
        State.globalPointerHandlerInstalled = false;
        State.lastHoveredContainer = null;
      } catch (ex) {
        console.warn('[AIVP] Failed to uninstall pointer handler:', ex);
      }
    }
  };

  // ===== CONTAINER LIFECYCLE MANAGEMENT =====
  const ContainerManager = {
    register(container, state) {
      State.containerMap.set(container, state);
      
      // Install pointer handler once
      PointerManager.install();
      
      // Ensure DOM observer is running to clean up detached containers
      this.ensureDOMObserver();
    },

    unregister(container) {
      try {
        State.containerMap.delete(container);
      } catch (ex) {
        console.warn('[AIVP] Failed to unregister container:', ex);
      }
      
      // If no containers remain, cleanup
      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)) {
            // Call optional cleanup helper on state if provided
            if (state?.cleanup) {
              try {
                state.cleanup();
              } catch (ex) {
                console.warn('[AIVP] Error in container cleanup:', ex);
              }
            }
            State.containerMap.delete(container);
          }
        }
        
        // If no containers remain, cleanup observers and handlers
        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;
        }
        
        PointerManager.uninstall();
        ScrollManager.uninstall();
      } catch (ex) {
        console.warn('[AIVP] Error during cleanup:', ex);
      }
    }
  };

  // ===== VIDEO INTERACTION SETUP =====
  const VideoInteraction = {
    setupVideoFocus(video, container) {
      // Make the video/container the active target when hovered or focused
      try {
        container.tabIndex = 0;
        video.tabIndex = 0;
      } catch (ex) {
        console.warn('[AIVP] Failed to set tabIndex:', ex);
      }
      
      const setActive = () => { State.activeVideo = video; };
      const clearActive = () => { if (State.activeVideo === video) State.activeVideo = null; };
      
      container.addEventListener('mouseenter', setActive);
      container.addEventListener('mouseleave', clearActive);
      container.addEventListener('focus', setActive, true);
      container.addEventListener('blur', clearActive, true);
      video.addEventListener('mouseenter', setActive);
      video.addEventListener('mouseleave', clearActive);
    },

    setupSeekableProgressbar(video, container, elapsedBar) {
      const state = {
        dragging: false,
        hudFadeTimeout: 0
      };
      
      let wasPlaying = false;
      
      const hud = container.querySelector('.aivp-hud');
      const backgroundBar = container.querySelector('.aivp-bg');
      const leftPreview = container.querySelector('.aivp-left');
      
      // HUD management functions
      const showHud = () => {
        if (!hud) return;
        if (state.hudFadeTimeout) {
          clearTimeout(state.hudFadeTimeout);
          state.hudFadeTimeout = 0;
        }
        hud.style.opacity = CONFIG.progressbar.hudOpacity;
        if (leftPreview) leftPreview.style.opacity = 1;
      };
      
      const hideHudDelayed = (delay) => {
        if (!hud) return;
        if (state.hudFadeTimeout) clearTimeout(state.hudFadeTimeout);
        state.hudFadeTimeout = setTimeout(() => {
          if (hud) hud.style.opacity = 0;
          if (leftPreview) leftPreview.style.opacity = 0;
          state.hudFadeTimeout = 0;
        }, delay || CONFIG.ui.hudFadeDelay);
      };
      
      const updateHudPosition = (percent) => {
        if (!hud) return;
        const x = Math.ceil(percent * container.offsetWidth);
        hud.style.left = `${x}px`;
        if (leftPreview) leftPreview.style.left = `${x}px`;
        showHud();
      };
      
      const seekAtClientX = (clientX) => {
        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));
        
        if (!isFinite(video.duration) || video.duration === 0) return;
        
        // Apply seek and update UI immediately
        video.currentTime = percent * video.duration;
        elapsedBar.style.width = `${Math.ceil(percent * container.offsetWidth)}px`;
        
        // Update HUD with formatted time and position
        if (hud) {
          hud.textContent = Utils.formatTime(video.currentTime);
          updateHudPosition(percent);
        }
        
        if (leftPreview && isFinite(video.duration) && video.duration > 0) {
          leftPreview.textContent = `${Utils.formatTime(video.currentTime)} / ${Utils.formatTime(video.duration)}`;
          leftPreview.style.opacity = 1;
        }
      };
      
      // Event handlers
      const onClick = (e) => {
        e.stopPropagation();
        e.preventDefault();
        seekAtClientX(e.clientX);
      };
      
      const onMouseDown = (e) => {
        e.stopPropagation();
        e.preventDefault();
        state.dragging = true;
        wasPlaying = !video.paused;
        seekAtClientX(e.clientX);
        document.addEventListener('mousemove', onMouseMove, { passive: false });
        document.addEventListener('mouseup', onMouseUp, { passive: false });
        showHud();
      };
      
      const onMouseMove = (e) => {
        if (!state.dragging) return;
        e.preventDefault();
        seekAtClientX(e.clientX);
      };
      
      const onMouseUp = (e) => {
        if (!state.dragging) return;
        e.preventDefault();
        seekAtClientX(e.clientX);
        state.dragging = false;
        document.removeEventListener('mousemove', onMouseMove, { passive: false });
        document.removeEventListener('mouseup', onMouseUp, { passive: false });
        if (wasPlaying) {
          try { video.play(); } catch (ex) { /* ignore */ }
        }
        hideHudDelayed(300);
      };
      
      // Show HUD on hover and update position
      container.addEventListener('mousemove', (e) => {
        if (state.dragging) return;
        const rect = container.getBoundingClientRect();
        const percent = Math.max(0, Math.min(1, (e.clientX - rect.left) / (rect.width || 1)));
        
        if (hud && video && isFinite(video.duration) && video.duration > 0) {
          hud.textContent = Utils.formatTime(percent * video.duration);
          updateHudPosition(percent);
        }
        
        if (leftPreview && isFinite(video.duration) && video.duration > 0) {
          leftPreview.textContent = `${Utils.formatTime(percent * video.duration)} / ${Utils.formatTime(video.duration)}`;
          leftPreview.style.opacity = 1;
        }
        showHud();
      });
      
      container.addEventListener('mouseleave', () => {
        if (!state.dragging) hideHudDelayed(200);
      });
      
      // Wire up events on the progressbar container
      container.addEventListener('click', onClick);
      container.addEventListener('mousedown', onMouseDown, { passive: false });
      
      // Register this container's state
      state.showHud = showHud;
      state.hideHudDelayed = hideHudDelayed;
      state.hud = hud;
      state.leftPreview = leftPreview;
      state.cleanup = () => {
        try {
          if (state.hud && state.hud.parentNode) state.hud.style.opacity = 0;
        } catch (ex) { /* ignore */ }

        // Disconnect any per-video loop observer attached earlier
        try {
          const obs = State.loopObservers.get(video);
          if (obs) {
            obs.disconnect();
            State.loopObservers.delete(video);
          }
        } catch (ex) { /* ignore */ }

        // Remove any temporary markers on the video
        try {
          if (video && video.removeEventListener) {
            // We do not track individual handlers here beyond the MutationObserver
          }
        } catch (ex) { /* ignore */ }
      };
      
      ContainerManager.register(container, state);
    }
  };

  // ===== PROGRESSBAR CREATION AND SETUP =====
  const ProgressbarSetup = {
    // Main setup function called for each video
    setupVideo(video) {
      if (!video || !video.parentNode) return;
      
      // Handle video loop disabling
      if (CONFIG.video.disableLoop && !video.attributes.noloop) {
        this.disableLoopForVideo(video);
      }
      
      // Create progressbar UI
      const { container, elapsedBar } = this.createProgressbarUI(video);
      
      // Map video to its elapsed bar element
      State.videoElementMap.set(video, elapsedBar);
      
      // Attach the progressbar to the video's parent to avoid layout issues in some feeds
      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 */ }

      // Ensure the container sits above most IG overlays
      try { container.style.zIndex = '2147483647'; } catch (ex) { /* ignore */ }

      if (parent && parent.appendChild) {
        parent.appendChild(container);
      }
      
      // Setup interactions and controls
      VideoInteraction.setupVideoFocus(video, container);
      VideoInteraction.setupSeekableProgressbar(video, container, elapsedBar);
      this.setupVideoEventHandlers(video, elapsedBar);
      
      // Unmute if configured
      if (CONFIG.video.unmute && video.muted) {
        this.unmuteVideo(video);
      }
    },

    disableLoopForVideo(video) {
      try {
        // Find next button for stories
        State.nextButton = video.parentNode.parentNode?.parentNode?.parentNode?.lastElementChild;
        video.setAttribute('noloop', '');
        // Immediately ensure the loop attribute/property is removed
        try {
          video.loop = false;
          video.removeAttribute('loop');
        } catch (ex) {
          // ignore setting loop if browser prevents it
        }

        // Disconnect previously registered observer if existing
        try {
          const prevObs = State.loopObservers.get(video);
          if (prevObs) {
            prevObs.disconnect();
            State.loopObservers.delete(video);
          }
        } catch (ex) {
          /* ignore */
        }

        // Observe the video element for any attempts to re-add the loop attribute
        try {
          const attrObserver = new MutationObserver((mutations) => {
            for (const m of mutations) {
              if (m.type === 'attributes' && m.attributeName === 'loop') {
                try {
                  // Remove the attribute and ensure property is false
                  if (video.hasAttribute('loop')) video.removeAttribute('loop');
                  if (video.loop) video.loop = false;
                } catch (ex) {
                  /* ignore */
                }
              }
            }
          });

          attrObserver.observe(video, { attributes: true, attributeFilter: ['loop'] });
          State.loopObservers.set(video, attrObserver);
        } catch (ex) {
          console.warn('[AIVP] Failed to observe video loop attribute:', ex);
        }

        // As a final mitigation, ensure that when the video plays we re-assert loop=false
        const ensureNoLoopOnPlay = () => {
          try {
            if (video.loop) video.loop = false;
            if (video.hasAttribute('loop')) video.removeAttribute('loop');
          } catch (ex) { /* ignore */ }
        };
        video.addEventListener('play', ensureNoLoopOnPlay);

        // Attach a short timeout to enforce it immediately in case the attribute is set shortly after
        setTimeout(() => {
          try {
            if (video.loop) video.loop = false;
            if (video.hasAttribute('loop')) video.removeAttribute('loop');
          } catch (ex) { /* ignore */ }
        }, 50);

        // Find and fix play/pause buttons
        const roleElements = Utils.safeQuerySelectorAll(video.parentNode.parentNode, 'div[role]');
        roleElements.forEach(element => {
          Object.keys(element).some(key => {
            if (key.startsWith('__reactProps$')) {
              const props = element[key];
              if (props?.onClick && String(props.onClick).includes('pause')) {
                element.addEventListener('click', () => {
                  if (video.paused) video.play();
                });
                return true;
              }
            }
            return false;
          });
        });
      } catch (ex) {
        console.warn('[AIVP] Error disabling video loop:', ex);
      }
    },

    createProgressbarUI(video) {
      const containerId = Utils.generateId();
      const elapsedBarId = `${containerId}bar`;
      const progressHeight = Utils.getProgressbarHeight();
      const hitAreaHeight = progressHeight;
      
      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; 
}
#${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-hud { 
  position: absolute; 
  left: 0; 
  transform: translateX(-50%); 
  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: 0; 
  transition: opacity 120ms; 
  z-index: 10000; 
}
.aivp-left { 
  position: absolute; 
  left: 0; 
  transform: translateX(-50%); 
  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: 0; 
  transition: opacity 120ms; 
  z-index: 10000; 
}
</style>
<div class="aivp-bg"></div>
<div id="${elapsedBarId}" class="aivp-elapsed"></div>
<div class="aivp-hud">0:00</div>
<div class="aivp-left">0:00 / 0:00</div>`;
      
      const elapsedBar = container.querySelector(`#${elapsedBarId}`);
      
      return { container, elapsedBar };
    },

    setupVideoEventHandlers(video, elapsedBar) {
      let updateTimer = null;
      
      const updateProgressBar = () => {
        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 = () => {
        // Set this video as the active video for keyboard controls
        State.activeVideo = video;
        
        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 {
            // Ensure video does not restart due to loop being set
            if (CONFIG.video.disableLoop) {
              try { video.pause(); } catch (e) { /* ignore */ }
              try { video.loop = false; } catch (e) { /* ignore */ }
              try { video.removeAttribute && video.removeAttribute('loop'); } catch (e) { /* ignore */ }
            }

            elapsedBar.style.width = '100%';

            // Advance story/next if available but allow UI to settle briefly
            if (CONFIG.video.disableLoop && State.nextButton) {
              setTimeout(() => {
                try { State.nextButton.click(); } catch (ex) { /* ignore */ }
              }, 80);
            }
          } catch (ex) {
            console.warn('[AIVP] Error handling ended event:', ex);
          }
        }
        
        // Update left preview when timer stopped
        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);
    },

    unmuteVideo(video) {
      try {
        if (location.pathname.startsWith('/stories/')) {
          const storyContainer = video.closest('div[style*="width"]')
            ?.parentNode?.closest('div[style*="width"]')
            ?.parentNode?.closest('div[style*="width"]');
          const muteButton = storyContainer?.querySelector('div[aria-label="Toggle audio"]');
          if (muteButton) muteButton.click();
        } else {
          const buttons = Utils.safeQuerySelectorAll(video.parentNode.parentNode, 'button');
          buttons.forEach(button => {
            Object.keys(button).some(key => {
              if (key.startsWith('__reactProps$')) {
                const props = button[key];
                if (props?.onClick && String(props.onClick).includes('AUDIO_STATES')) {
                  button.click();
                  return true;
                }
              }
              return false;
            });
          });
        }
      } catch (ex) {
        console.warn('[AIVP] Failed to unmute video:', ex);
      }
    }
  };

  // ===== VIDEO DISCOVERY AND MONITORING =====
  const VideoDiscovery = {
    ensureVideoHasProgressbar(video) {
      try {
        if (!video || String(video.tagName).toLowerCase() !== 'video') return;
        
        // Check if already initialized
        if (video.getAttribute('aivp_done')) {
          // If visual elements aren't created yet, and the video is ready, call setup now
          if (!State.videoElementMap.get(video) && video.readyState >= 2) {
            try {
              ProgressbarSetup.setupVideo(video);
            } catch (ex) {
              console.warn('[AIVP] Error setting up video:', ex);
            }
          }
          return;
        }
        
        // Mark as initialized
        video.setAttribute('aivp_done', '1');
        
        // If the video is already in a "can play" state, call setup immediately
        if (video.readyState >= 2) {
          try {
            ProgressbarSetup.setupVideo(video);
          } catch (ex) {
            console.warn('[AIVP] Error setting up video:', ex);
          }
        } else {
          // Wait for canplay event
          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 {
        // Initial scan of existing videos
        const existingVideos = document.querySelectorAll('video');
        existingVideos.forEach(video => this.ensureVideoHasProgressbar(video));
        
        // Observe DOM for newly inserted video elements
        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 the added node is a video
                  if (node.tagName && String(node.tagName).toLowerCase() === 'video') {
                    this.ensureVideoHasProgressbar(node);
                  } else if (node.querySelectorAll) {
                    // Find any descendant videos
                    const descendantVideos = node.querySelectorAll('video');
                    descendantVideos.forEach(video => this.ensureVideoHasProgressbar(video));
                  }
                }
              }
            }
          } catch (ex) {
            console.warn('[AIVP] Error in video observer:', ex);
          }
        });
        
        // Observe large subtree
        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);
      }
    }
  };

  // ===== SCROLL MANAGEMENT =====
  const ScrollManager = {
    handleScroll() {
      // Throttle scroll updates to avoid excessive processing
      if (!State.scrollUpdateScheduled) {
        State.scrollUpdateScheduled = true;
        requestAnimationFrame(() => {
          this.updateActiveVideoFromScroll();
          State.scrollUpdateScheduled = false;
        });
      }
    },

    updateActiveVideoFromScroll() {
      try {
        // Find all videos with progress bars that are in the document
        const videos = Array.from(State.videoElementMap.keys()).filter(video => {
          return video && document.contains(video) && video.readyState >= 2;
        });

        if (videos.length === 0) return;

        // Find the video closest to the center of the viewport
        const viewportCenter = window.innerHeight / 2;
        let closestVideo = null;
        let minDistance = Infinity;

        for (const video of videos) {
          const rect = video.getBoundingClientRect();
          // Skip videos that are not visible
          if (rect.bottom < 0 || rect.top > window.innerHeight) continue;
          
          const videoCenter = rect.top + rect.height / 2;
          const distance = Math.abs(videoCenter - viewportCenter);
          
          if (distance < minDistance) {
            minDistance = distance;
            closestVideo = video;
          }
        }

        // Set the closest visible video as active
        if (closestVideo && closestVideo !== State.activeVideo) {
          State.activeVideo = closestVideo;
        }
      } catch (ex) {
        console.warn('[AIVP] Error updating active video from scroll:', ex);
      }
    },

    install() {
      if (State.scrollHandlerInstalled) return;
      
      try {
        const handler = () => this.handleScroll();
        window.addEventListener('scroll', handler, { passive: true });
        State.scrollHandlerInstalled = true;
        State.scrollHandler = handler;
      } catch (ex) {
        console.warn('[AIVP] Failed to install scroll handler:', ex);
      }
    },

    uninstall() {
      if (!State.scrollHandlerInstalled) return;
      
      try {
        if (State.scrollHandler) {
          window.removeEventListener('scroll', State.scrollHandler, { passive: true });
        }
        State.scrollHandlerInstalled = false;
      } catch (ex) {
        console.warn('[AIVP] Failed to uninstall scroll handler:', ex);
      }
    }
  };

  // ===== INITIALIZATION =====
  const init = () => {
    try {
      // Override HTMLVideoElement.prototype.addEventListener to hook into video lifecycle
      State.originalAddEventListener = HTMLVideoElement.prototype.addEventListener;
      HTMLVideoElement.prototype.addEventListener = function(type, ...args) {
        const result = State.originalAddEventListener.apply(this, [type, ...args]);
        
        // Ensure progressbar is added when video starts interacting with events
        if (!this.getAttribute('aivp_done')) {
          VideoDiscovery.ensureVideoHasProgressbar(this);
        }
        
        return result;
      };
      
      // Register menu commands
      MenuCommands.register();

      // Initialize keyboard seek step from storage
      try {
        CONFIG.keyboard.seekStep = Utils.getSeekStep();
      } catch (ex) { /* ignore */ }
       
       // Install keyboard controls
       KeyboardControls.install();

       // Install scroll handler for active video management
       ScrollManager.install();
       
       // Start monitoring for videos
       VideoDiscovery.startMonitoring();
       
       // Initial update of active video
       ScrollManager.updateActiveVideoFromScroll();
       
       console.log('[AIVP] Instagram Video Progressbar initialized successfully');
    } catch (ex) {
      console.error('[AIVP] Failed to initialize:', ex);
    }
  };

  // Start initialization
  init();
 })();