MissAV Precision Clipper (Material Edition)

HLS media clipper specialized for MissAV. Features a responsive Material UI, overlay protection, and scrollable jump intervals for seamless video slicing.

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 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.

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

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

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

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

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

// ==UserScript==
// @name         MissAV Precision Clipper (Material Edition)
// @namespace    https://github.com/gentlemanan/missav-downloader
// @version      9.6
// @description  HLS media clipper specialized for MissAV. Features a responsive Material UI, overlay protection, and scrollable jump intervals for seamless video slicing.
// @author       gentlemanan
// @match        *://missav.com/*
// @match        *://*.missav.com/*
// @match        *://missav.ws/*
// @match        *://*.missav.ws/*
// @match        *://missav.ai/*
// @match        *://*.missav.ai/*
// @match        *://missav.live/*
// @match        *://*.missav.live/*
// @match        *://missav123.com/*
// @match        *://*.missav123.com/*
// @grant        unsafeWindow
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/mux.min.js
// @run-at       document-start
// @icon         https://www.google.com/s2/favicons?sz=64&domain=missav.com
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // ==========================================
    // UTILITIES
    // ==========================================
    class Utils {
        static formatTime(sec) {
            if (sec === null || isNaN(sec)) return '--:--';
            const m = Math.floor(sec / 60).toString().padStart(2, '0');
            const s = Math.floor(sec % 60).toString().padStart(2, '0');
            return `${m}:${s}`;
        }

        static saveFile(blob, startTime, endTime, ext) {
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            const s = Utils.formatTime(startTime).replace(':', 'm') + 's';
            const e = Utils.formatTime(endTime).replace(':', 'm') + 's';
            a.download = `MissAV_Clip_${s}_to_${e}.${ext}`;
            a.click();
            setTimeout(() => URL.revokeObjectURL(url), 5000);
        }
    }

    // ==========================================
    // NETWORK INTERCEPTOR
    // ==========================================
    class NetworkInterceptor {
        constructor(windowContext) {
            this.context = windowContext || window;
            this.m3u8Url = null;
        }

        init() {
            this._interceptFetch();
            this._interceptXHR();
        }

        getInterceptedUrl() {
            return this.m3u8Url;
        }

        _interceptFetch() {
            const originFetch = this.context.fetch;
            const self = this;
            this.context.fetch = async function (input, init) {
                const url = input instanceof Request ? input.url : input.toString();
                if (url.includes('.m3u8')) self.m3u8Url = url;
                return originFetch.apply(this, arguments);
            };
        }

        _interceptXHR() {
            const originXHR = this.context.XMLHttpRequest;
            const self = this;
            class XHRInterceptor extends originXHR {
                open(method, url) {
                    if (url && url.toString().includes('.m3u8')) {
                        self.m3u8Url = url.toString();
                    }
                    super.open(...arguments);
                }
            }
            this.context.XMLHttpRequest = XHRInterceptor;
        }
    }

    // ==========================================
    // HLS PROCESSOR
    // ==========================================
    class HLSProcessor {
        constructor() {
            if (typeof muxjs === 'undefined') {
                console.error('[Clipper] mux.js library not loaded.');
            }
        }

        async processClip(m3u8Url, startT, endT, uiManager) {
            if (!m3u8Url) throw new Error('No HLS stream (.m3u8) intercepted.');
            if (startT === null || endT === null || startT >= endT) throw new Error('Invalid Start/End times.');

            uiManager.showToast('Fetching & Muxing HLS...');

            let playlistText = await (await fetch(m3u8Url)).text();
            let baseUrl = m3u8Url.substring(0, m3u8Url.lastIndexOf('/') + 1);

            if (playlistText.includes('#EXT-X-STREAM-INF')) {
                const mediaUrl = playlistText.split('\n').find((l, i, arr) => arr[i - 1]?.includes('#EXT-X-STREAM-INF')).trim();
                const finalUrl = mediaUrl.startsWith('http') ? mediaUrl : baseUrl + mediaUrl;
                baseUrl = finalUrl.substring(0, finalUrl.lastIndexOf('/') + 1);
                playlistText = await (await fetch(finalUrl)).text();
            }

            if (playlistText.includes('#EXT-X-KEY:METHOD=AES-128')) {
                throw new Error('Encrypted stream detected. Cannot download raw chunks.');
            }

            let time = 0;
            const segments = playlistText.split('\n').reduce((acc, line) => {
                if (line.startsWith('#EXTINF:')) {
                    acc.duration = parseFloat(line.split(':')[1]);
                } else if (line && !line.startsWith('#')) {
                    time += acc.duration;
                    if (time > startT && (time - acc.duration) < endT) {
                        acc.urls.push(line.startsWith('http') ? line : baseUrl + line);
                    }
                }
                return acc;
            }, { duration: 0, urls: [] }).urls;

            if (!segments.length) throw new Error('No segments found in this timeframe.');

            const buffers = [];
            for (const url of segments) {
                buffers.push(new Uint8Array(await (await fetch(url)).arrayBuffer()));
            }

            const transmuxer = new muxjs.mp4.Transmuxer();
            let initSegment = null;
            const dataSegments = [];

            transmuxer.on('data', (s) => {
                if (!initSegment) initSegment = s.initSegment;
                dataSegments.push(s.data);
            });

            const combined = new Uint8Array(buffers.reduce((a, b) => a + b.length, 0));
            let offset = 0;
            for (const b of buffers) {
                combined.set(b, offset);
                offset += b.length;
            }

            transmuxer.push(combined);
            transmuxer.flush();

            if (!initSegment) throw new Error('Transmuxing failed.');

            Utils.saveFile(new Blob([initSegment, ...dataSegments], { type: 'video/mp4' }), startT, endT, 'mp4');
            uiManager.showToast('✅ Download Complete!');
        }
    }

    // ==========================================
    // UI MANAGER (Material Design)
    // ==========================================
    class UIManager {
        constructor(appInstance) {
            this.app = appInstance;
            this.fab = null;
            this.overlay = null;
            this._onFullscreenChange = this._handleFullscreenChange.bind(this);
            this._injectMaterialCSS();
        }

        _injectMaterialCSS() {
            const css = `
                :root {
                    --google-blue: #1a73e8;
                    --google-surface: #ffffff;
                    --google-text: #202124;
                    --md-shadow-2: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
                    --md-shadow-4: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22);
                }

                .mat-fab {
                    position: fixed; right: 24px; bottom: 24px; width: 56px; height: 56px;
                    border-radius: 50%; background: var(--google-surface); color: var(--google-blue);
                    z-index: 2147483640; display: flex; align-items: center; justify-content: center;
                    box-shadow: var(--md-shadow-2); cursor: pointer; user-select: none;
                    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); border: none;
                }
                .mat-fab:hover { box-shadow: var(--md-shadow-4); transform: translateY(-2px); }
                .mat-fab svg { width: 24px; height: 24px; fill: currentColor; }

                .mat-theater-overlay {
                    position: fixed; inset: 0; width: 100vw; height: 100vh;
                    background: #000; z-index: 2147483645; overflow: hidden;
                    font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                    display: flex; flex-direction: column;
                }

                .mat-video-wrapper {
                    flex: 1; position: relative;
                    display: flex; align-items: center; justify-content: center;
                    min-height: 0; cursor: pointer; overflow: hidden;
                }
                .mat-video-wrapper video {
                    width: 100% !important; height: 100% !important;
                    object-fit: contain !important; background: #000 !important;
                    pointer-events: none;
                }

                .mat-top-bar {
                    position: absolute; top: 16px; right: 24px; z-index: 10;
                    pointer-events: auto;
                }
                .mat-icon-btn {
                    width: 48px; height: 48px; background: rgba(20,20,20,0.6); color: #fff;
                    border: 1px solid rgba(255,255,255,0.1); border-radius: 50%; cursor: pointer;
                    display: flex; align-items: center; justify-content: center;
                    transition: background 0.2s; backdrop-filter: blur(8px);
                }
                .mat-icon-btn:hover { background: rgba(220, 40, 40, 0.8); }
                .mat-icon-btn svg { width: 28px; height: 28px; fill: currentColor; }

                .mat-controls-bar {
                    flex: 0 0 auto; background: #101010;
                    border-top: 1px solid #222; padding: 20px 24px;
                    display: flex; flex-direction: column; align-items: center;
                    z-index: 2; pointer-events: auto;
                }

                .mat-controls-inner {
                    width: 100%; max-width: 900px;
                    display: flex; flex-direction: column; gap: 16px;
                }

                .mat-progress-container { display: flex; align-items: center; gap: 16px; color: #fff; font-size: 13px; font-weight: 500; font-variant-numeric: tabular-nums; }
                .mat-slider {
                    flex: 1; -webkit-appearance: none; height: 4px; border-radius: 2px;
                    background: rgba(255,255,255,0.3); outline: none; cursor: pointer;
                }
                .mat-slider::-webkit-slider-thumb {
                    -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%;
                    background: var(--google-blue); cursor: pointer; transition: transform 0.1s;
                }
                .mat-slider::-webkit-slider-thumb:hover { transform: scale(1.3); }

                .mat-toolbar { display: flex; align-items: center; gap: 12px; justify-content: center; }

                /* Tooltip & Jump Controls */
                .mat-jump-container { display: flex; align-items: center; gap: 4px; }
                .mat-jump-nav-btn {
                    background: transparent; border: 1px solid rgba(255,255,255,0.2);
                    color: #fff; width: 34px; height: 34px; border-radius: 50%; cursor: pointer;
                    display: flex; align-items: center; justify-content: center;
                    font-size: 14px; font-weight: bold; transition: all 0.2s;
                }
                .mat-jump-nav-btn:hover { background: rgba(255,255,255,0.15); border-color: rgba(255,255,255,0.4); }

                .mat-tooltip-wrap { position: relative; display: flex; align-items: center; }
                .mat-tooltip-wrap::before {
                    content: attr(data-tooltip);
                    position: absolute; bottom: 100%; left: 50%; transform: translate(-50%, -8px);
                    background: rgba(30, 30, 30, 0.95); color: #fff; padding: 6px 12px;
                    border-radius: 6px; font-size: 12px; white-space: nowrap; font-weight: 500;
                    opacity: 0; pointer-events: none; transition: opacity 0.2s ease, transform 0.2s ease;
                    border: 1px solid rgba(255,255,255,0.15); box-shadow: 0 8px 16px rgba(0,0,0,0.4);
                    z-index: 100;
                }
                .mat-tooltip-wrap:hover::before { opacity: 1; transform: translate(-50%, -4px); }

                .mat-jump-input {
                    background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.1);
                    color: #fff; width: 56px; height: 34px; border-radius: 16px;
                    text-align: center; font-weight: 600; font-size: 14px;
                    font-family: inherit; font-variant-numeric: tabular-nums;
                    transition: border-color 0.2s, background 0.2s; outline: none; box-sizing: border-box;
                }
                .mat-jump-input:focus { border-color: var(--google-blue); background: rgba(255,255,255,0.15); }
                .mat-jump-input:hover { background: rgba(255,255,255,0.15); }
                .mat-jump-input::-webkit-inner-spin-button,
                .mat-jump-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }

                .mat-btn {
                    background: rgba(255,255,255,0.1); color: #fff; border: 1px solid rgba(255,255,255,0.1);
                    border-radius: 16px; padding: 6px 16px; cursor: pointer; height: 34px; box-sizing: border-box;
                    font-size: 14px; font-weight: 500; transition: all 0.2s;
                    display: flex; align-items: center; gap: 8px; font-family: inherit;
                }
                .mat-btn:hover { background: rgba(255,255,255,0.2); }
                .mat-btn.primary { background: var(--google-surface); color: var(--google-blue); border: none; font-weight: 600; }
                .mat-btn.primary:hover { background: #e8f0fe; }
                .mat-btn.primary:disabled { opacity: 0.5; cursor: not-allowed; }

                .mat-toast {
                    position: fixed; bottom: 120px; left: 50%; transform: translateX(-50%);
                    background: #323232; color: #fff; padding: 14px 24px;
                    border-radius: 8px; z-index: 2147483647; font-size: 14px; font-weight: 500;
                    box-shadow: var(--md-shadow-4); font-family: inherit;
                }
            `;
            const style = document.createElement('style');
            style.textContent = css;
            document.documentElement.appendChild(style);
        }

        showToast(message) {
            const el = document.createElement('div');
            el.className = 'mat-toast';
            el.textContent = message;
            document.documentElement.appendChild(el);
            setTimeout(() => el.remove(), 4000);
        }

        renderFAB() {
            if (document.getElementById('mat-clipper-fab')) return;
            this.fab = document.createElement('button');
            this.fab.id = 'mat-clipper-fab';
            this.fab.className = 'mat-fab';
            this.fab.title = 'Open HLS Clipper';
            this.fab.innerHTML = `<svg viewBox="0 0 24 24"><path d="M9.64 7.64c.23-.5.36-1.05.36-1.64 0-2.21-1.79-4-4-4S2 3.79 2 6s1.79 4 4 4c.59 0 1.14-.13 1.64-.36L10 12l-2.36 2.36C7.14 14.13 6.59 14 6 14c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4c0-.59-.13-1.14-.36-1.64L12 14l7 7h3v-1L9.64 7.64zM6 8c-1.1 0-2-.89-2-2s.9-2 2-2 2 .89 2 2-.9 2-2 2zm0 12c-1.1 0-2-.89-2-2s.9-2 2-2 2 .89 2 2-.9 2-2 2zm6-7.5c-.28 0-.5-.22-.5-.5s.22-.5.5-.5.5.22.5.5-.22.5-.5.5zM19 3l-6 6 2 2 7-7V3z"/></svg>`;

            this.fab.addEventListener('click', () => this.app.openTheaterMode());
            document.documentElement.appendChild(this.fab);
        }

        toggleFAB(visible) {
            if (this.fab) this.fab.style.display = visible ? 'flex' : 'none';
        }

        renderTheater(videoElement) {
            this.overlay = document.createElement('div');
            this.overlay.className = 'mat-theater-overlay';

            this.overlay.innerHTML = `
                <div class="mat-top-bar">
                    <button class="mat-icon-btn" id="mat-btn-close" title="Close Theater">
                        <svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
                    </button>
                </div>
                <div class="mat-video-wrapper" id="mat-video-wrap"></div>

                <div class="mat-controls-bar">
                    <div class="mat-controls-inner">
                        <div class="mat-progress-container">
                            <span id="mat-lbl-cur">00:00</span>
                            <input type="range" class="mat-slider" id="mat-seek" min="0" max="1000" value="0">
                            <span id="mat-lbl-dur">00:00</span>
                        </div>
                        <div class="mat-toolbar">
                            <button class="mat-btn" id="mat-btn-play">
                                <svg style="width:18px;height:18px;fill:currentColor;" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg> Play/Pause
                            </button>

                            <div class="mat-jump-container">
                                <button class="mat-jump-nav-btn" id="mat-btn-jump-back" title="Jump Backward">&laquo;</button>
                                <div class="mat-tooltip-wrap" data-tooltip="Scroll to adjust interval (sec)">
                                    <input type="number" class="mat-jump-input" id="mat-input-jump" value="10" min="1" max="120" aria-label="Jump interval">
                                </div>
                                <button class="mat-jump-nav-btn" id="mat-btn-jump-fwd" title="Jump Forward">&raquo;</button>
                            </div>

                            <button class="mat-btn" id="mat-btn-start">Set Start</button>
                            <button class="mat-btn" id="mat-btn-end">Set End</button>
                            <button class="mat-btn" id="mat-btn-fullscreen">
                                <svg id="mat-icon-fullscreen" style="width:18px;height:18px;fill:currentColor;" viewBox="0 0 24 24"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg> Fullscreen
                            </button>
                            <div style="flex:1"></div>
                            <button class="mat-btn primary" id="mat-btn-render">Export MP4 Clip</button>
                        </div>
                    </div>
                </div>
            `;
            document.documentElement.appendChild(this.overlay);
            document.getElementById('mat-video-wrap').appendChild(videoElement);

            this._bindTheaterEvents(videoElement);
            document.addEventListener('fullscreenchange', this._onFullscreenChange);
        }

        destroyTheater() {
            document.removeEventListener('fullscreenchange', this._onFullscreenChange);

            if (document.fullscreenElement) document.exitFullscreen().catch(()=>{});

            if (this.overlay) {
                this.overlay.remove();
                this.overlay = null;
            }
        }

        _handleFullscreenChange() {
            const btnFullscreen = document.getElementById('mat-btn-fullscreen');
            if (!btnFullscreen) return;

            if (document.fullscreenElement) {
                btnFullscreen.innerHTML = '<svg style="width:18px;height:18px;fill:currentColor;" viewBox="0 0 24 24"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/></svg> Exit Fullscreen';
            } else {
                btnFullscreen.innerHTML = '<svg style="width:18px;height:18px;fill:currentColor;" viewBox="0 0 24 24"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg> Fullscreen';
            }
        }

        _bindTheaterEvents(video) {
            const btnPlay = document.getElementById('mat-btn-play');
            const btnStart = document.getElementById('mat-btn-start');
            const btnEnd = document.getElementById('mat-btn-end');
            const btnRender = document.getElementById('mat-btn-render');
            const btnClose = document.getElementById('mat-btn-close');
            const btnFullscreen = document.getElementById('mat-btn-fullscreen');
            const videoWrap = document.getElementById('mat-video-wrap');
            const seekSlider = document.getElementById('mat-seek');
            const lblCur = document.getElementById('mat-lbl-cur');
            const lblDur = document.getElementById('mat-lbl-dur');

            // Jump Elements
            const jumpInput = document.getElementById('mat-input-jump');
            const btnJumpBack = document.getElementById('mat-btn-jump-back');
            const btnJumpFwd = document.getElementById('mat-btn-jump-fwd');

            const togglePlay = () => video.paused ? video.play() : video.pause();
            btnPlay.onclick = togglePlay;
            videoWrap.onclick = togglePlay;
            btnClose.onclick = () => this.app.closeTheaterMode();

            // Handle << and >> Buttons
            const getJumpValue = () => parseFloat(jumpInput.value) || 10;

            btnJumpBack.onclick = () => {
                video.currentTime = Math.max(0, video.currentTime - getJumpValue());
            };

            btnJumpFwd.onclick = () => {
                video.currentTime = Math.min(video.duration, video.currentTime + getJumpValue());
            };

            // Scroll to adjust the number input
            jumpInput.addEventListener('wheel', (e) => {
                e.preventDefault();
                const step = e.deltaY > 0 ? -1 : 1;
                let newVal = (parseInt(jumpInput.value) || 10) + step;
                newVal = Math.max(1, Math.min(120, newVal));
                jumpInput.value = newVal;
            });

            btnFullscreen.onclick = () => {
                if (!document.fullscreenElement) {
                    this.overlay.requestFullscreen().catch(err => {
                        this.showToast(`Error enabling fullscreen: ${err.message}`);
                    });
                } else {
                    document.exitFullscreen();
                }
            };

            btnStart.onclick = () => {
                this.app.setClipStart(video.currentTime);
                btnStart.innerText = `Start: ${Utils.formatTime(video.currentTime)}`;
            };
            btnEnd.onclick = () => {
                this.app.setClipEnd(video.currentTime);
                btnEnd.innerText = `End: ${Utils.formatTime(video.currentTime)}`;
            };

            btnRender.onclick = () => {
                btnRender.disabled = true;
                const origText = btnRender.innerText;
                btnRender.innerText = 'Processing...';

                this.app.executeRender().catch(err => {
                    this.showToast(`Error: ${err.message}`);
                    console.error('[Clipper]', err);
                }).finally(() => {
                    btnRender.disabled = false;
                    btnRender.innerText = origText;
                });
            };

            video.addEventListener('timeupdate', () => {
                if (isNaN(video.duration)) return;
                lblCur.innerText = Utils.formatTime(video.currentTime);
                lblDur.innerText = Utils.formatTime(video.duration);
                if (document.activeElement !== seekSlider) {
                    seekSlider.value = (video.currentTime / video.duration) * 1000;
                }
            });

            seekSlider.addEventListener('input', () => {
                if (isNaN(video.duration)) return;
                video.currentTime = (seekSlider.value / 1000) * video.duration;
            });
        }
    }

    // ==========================================
    // MAIN APP CONTROLLER
    // ==========================================
    class VideoClipperApp {
        constructor() {
            this.interceptor = new NetworkInterceptor(unsafeWindow);
            this.ui = new UIManager(this);
            this.processor = new HLSProcessor();

            this.videoData = {
                element: null,
                originalParent: null,
                originalSibling: null,
                originalStyles: '',
                originalControls: true
            };

            this.clipState = {
                start: null,
                end: null
            };

            this.theaterHiddenElements = [];
        }

        init() {
            this.interceptor.init();

            const boot = () => {
                this.ui.renderFAB();
                setInterval(() => this.ui.renderFAB(), 2000);
            };

            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', boot);
            } else {
                boot();
            }
        }

        _findTargetVideo() {
            const missAvPlayer = document.querySelector('video.plyr, video#player, .plyr__video-wrapper video, .video-js video');
            if (missAvPlayer) return missAvPlayer;

            const videos = Array.from(document.querySelectorAll('video'));
            return videos.sort((a, b) => (b.clientWidth * b.clientHeight) - (a.clientWidth * a.clientHeight))[0];
        }

        _hidePageForTheater() {
            this.theaterHiddenElements = [];
            const keepNodes = new Set([this.ui.overlay, this.ui.fab]);

            Array.from(document.body.children).forEach(el => {
                if (keepNodes.has(el) || el.classList.contains('mat-theater-overlay')) return;

                this.theaterHiddenElements.push({
                    el,
                    visibility: el.style.visibility,
                    pointerEvents: el.style.pointerEvents,
                    opacity: el.style.opacity
                });

                el.style.visibility = 'hidden';
                el.style.pointerEvents = 'none';
                el.style.opacity = '0';
            });

            Array.from(document.querySelectorAll('body *')).forEach(el => {
                if (keepNodes.has(el) || el.closest('.mat-theater-overlay')) return;

                const style = window.getComputedStyle(el);
                const zIndex = parseInt(style.zIndex, 10);

                if ((style.position === 'fixed' || style.position === 'sticky') && (!isNaN(zIndex) && zIndex >= 100)) {
                    this.theaterHiddenElements.push({
                        el,
                        visibility: el.style.visibility,
                        pointerEvents: el.style.pointerEvents,
                        opacity: el.style.opacity
                    });

                    el.style.visibility = 'hidden';
                    el.style.pointerEvents = 'none';
                    el.style.opacity = '0';
                }
            });
        }

        _restorePageAfterTheater() {
            this.theaterHiddenElements.forEach(item => {
                item.el.style.visibility = item.visibility;
                item.el.style.pointerEvents = item.pointerEvents;
                item.el.style.opacity = item.opacity;
            });
            this.theaterHiddenElements = [];
        }

        openTheaterMode() {
            const vid = this._findTargetVideo();
            if (!vid) return this.ui.showToast('No video found on page!');

            this.videoData = {
                element: vid,
                originalParent: vid.parentElement,
                originalSibling: vid.nextSibling,
                originalStyles: vid.getAttribute('style') || '',
                originalControls: vid.controls
            };

            this.clipState = { start: null, end: null };

            vid.removeAttribute('style');
            vid.controls = false;

            this.ui.toggleFAB(false);
            document.body.style.overflow = 'hidden';

            this._hidePageForTheater();
            this.ui.renderTheater(vid);
        }

        closeTheaterMode() {
            const { element, originalParent, originalSibling, originalStyles, originalControls } = this.videoData;

            if (element) {
                element.setAttribute('style', originalStyles);
                element.controls = originalControls;
                if (originalParent) {
                    originalParent.insertBefore(element, originalSibling);
                }
            }

            this.ui.destroyTheater();
            this._restorePageAfterTheater();

            this.ui.toggleFAB(true);
            document.body.style.overflow = '';
            this.videoData.element = null;
        }

        setClipStart(time) { this.clipState.start = time; }
        setClipEnd(time) { this.clipState.end = time; }

        async executeRender() {
            const m3u8Url = this.interceptor.getInterceptedUrl();
            await this.processor.processClip(m3u8Url, this.clipState.start, this.clipState.end, this.ui);
        }
    }

    // ==========================================
    // BOOTSTRAP
    // ==========================================
    new VideoClipperApp().init();

})();