Greasy Fork

Universal Video Split

Универсальный сплит по времени для VK, Rutube, YouTube (v1.4). Панель управления, статистика, оверлей, звук, таймер. Отображение названия видео на панели и оверлее.

目前为 2025-04-28 提交的版本。查看 最新版本

// ==UserScript==
// @name         Universal Video Split
// @namespace    http://tampermonkey.net/
// @version      1.4 // Incrementing version
// @author       Gemini (Combined by AI)
// @match        *://vk.com/video_ext.php*
// @match        *://vkvideo.ru/video_ext.php*
// @match        *://rutube.ru/video/*
// @match        *://www.youtube.com/watch*
// @grant        GM_addStyle
// @grant        GM_log
// @description  Универсальный сплит по времени для VK, Rutube, YouTube (v1.4). Панель управления, статистика, оверлей, звук, таймер. Отображение названия видео на панели и оверлее.
// ==/UserScript==

(function() {
    'use strict';

    // --- КОНФИГУРАЦИЯ (Общая) ---
    let splitMinutes = null;
    let totalVideoMinutes = null;
    const extendCost = 300;
    const splitSoundUrl = 'https://github.com/lardan099/donat/raw/refs/heads/main/mix_06s.mp3';
    const overlayGifUrl = 'https://i.imgur.com/SS5Nfff.gif';
    const localStorageVolumeKey = 'universalSplitAlertVolume';
    const localStorageTimerKey = 'universalSplitOverlayTimer';
    const defaultOverlayTimerDuration = 360;
    const defaultAlertVolume = '1.0';

    // --- ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ СОСТОЯНИЯ ---
    let currentPlatform = 'unknown';
    let platformConfig = null;
    let video = null;
    let overlay = null;
    let splitTriggered = false; // Flag to ensure split actions only happen once per trigger event
    let audioPlayer = null;
    let splitCheckIntervalId = null;
    let setupIntervalId = null; // Interval to poll for video/insertion elements on complex pages
    let panelAdded = false;
    let panelElement = null;
    let controlsElement = null; // Specific for VK visibility check
    let visibilityCheckIntervalId = null; // Specific for VK visibility check
    let navigationObserver = null; // Observer to detect URL changes for SPAs
    let lastUrl = location.href; // Track last URL for navigation observer

    let overlayTimerDuration = parseInt(localStorage.getItem(localStorageTimerKey), 10);
    if (isNaN(overlayTimerDuration) || overlayTimerDuration < 0) {
        overlayTimerDuration = defaultOverlayTimerDuration;
    }
    let overlayTimerIntervalId = null;
    let overlayCountdownRemaining = overlayTimerDuration;

    // --- КОНФИГУРАЦИЯ ПЛАТФОРМ ---
    const platformConfigs = {
        vk: {
            idPrefix: 'vk',
            controlPanelSelector: '#split-control-panel-vk',
            videoSelector: 'video',
            insertionSelector: '.videoplayer_title', // Insert panel after this
            insertionMethod: 'afterend',
            controlsElementSelector: '.videoplayer_controls', // Element to watch for visibility (VK only)
            needsVisibilityCheck: true,
            videoTitleSelector: '.videoplayer_title h2.videoplayer_title_text', // Selector for video title
            styles: `
                #split-control-panel-vk { background: #f0f2f5; border: 1px solid #dce1e6; color: #333; display: none; opacity: 0; transition: opacity 0.2s ease-in-out; position: relative; z-index: 10; }
                #split-control-panel-vk.visible { display: flex; opacity: 1; }
                #split-control-panel-vk label { color: #656565; } #split-control-panel-vk label i { color: #828282; }
                #split-control-panel-vk input[type="number"] { background: #fff; color: #000; border: 1px solid #c5d0db; }
                #split-control-panel-vk input[type="number"]:focus { border-color: #aebdcb; }
                #split-control-panel-vk button { background: #e5ebf1; color: #333; border: 1px solid #dce1e6; }
                #split-control-panel-vk button:hover { background: #dae2ea; } #split-control-panel-vk button:active { background: #ccd5e0; border-color: #c5d0db; }
                #split-control-panel-vk .split-input-group button { background: #f0f2f5; border: 1px solid #dce1e6; } #split-control-panel-vk .split-input-group button:hover { background: #e5ebf1; }
                #split-control-panel-vk .set-split-button { background: #5181b8; color: #fff; border: none; } #split-control-panel-vk .set-split-button:hover { background: #4a76a8; }
                #split-control-panel-vk .set-split-button.active { background: #6a9e42; } #split-control-panel-vk .set-split-button.active:hover { background: #5c8c38; }
                #split-control-panel-vk .split-volume-control label { color: #656565; }
                #split-control-panel-vk .split-volume-control input[type="range"] { background: #dae2ea; }
                #split-control-panel-vk .split-volume-control input[type="range"]::-webkit-slider-thumb { background: #5181b8; } #split-control-panel-vk .split-volume-control input[type="range"]::-moz-range-thumb { background: #5181b8; }
                #split-control-panel-vk .split-stats { color: #333; }
            `
        },
        rutube: {
            idPrefix: 'rutube',
            controlPanelSelector: '#split-control-panel-rutube',
            videoSelector: 'video',
            insertionSelector: '.video-pageinfo-container-module__pageInfoContainer', // Insert panel into this
            insertionMethod: 'prepend',
            needsVisibilityCheck: false,
            videoTitleSelector: 'h1.video-pageinfo-container-module__title', // Selector for video title
            styles: `
                #split-control-panel-rutube { background: #222; border: 1px solid #444; color: #eee; }
                #split-control-panel-rutube label { color: #aaa; } #split-control-panel-rutube label i { color: #888; }
                #split-control-panel-rutube input[type="number"] { background: #333; color: #eee; border: 1px solid #555; }
                #split-control-panel-rutube button { background: #444; color: #eee; border: none; } #split-control-panel-rutube button:hover { background: #555; }
                #split-control-panel-rutube .split-input-group button { background: #333; border: 1px solid #555; } #split-control-panel-rutube .split-input-group button:hover { background: #444; }
                #split-control-panel-rutube .set-split-button { background: #007bff; color: white; } #split-control-panel-rutube .set-split-button:hover { background: #0056b3; }
                #split-control-panel-rutube .set-split-button.active { background: #28a745; } #split-control-panel-rutube .set-split-button.active:hover { background: #1e7e34; }
                #split-control-panel-rutube .split-volume-control label { color: #aaa; }
                #split-control-panel-rutube .split-volume-control input[type="range"] { background: #444; }
                #split-control-panel-rutube .split-volume-control input[type="range"]::-webkit-slider-thumb { background: #007bff; } #split-control-panel-rutube .split-volume-control input[type="range"]::-moz-range-thumb { background: #007bff; }
                #split-control-panel-rutube .split-stats { color: #eee; }
            `
        },
        youtube: {
            idPrefix: 'youtube',
            controlPanelSelector: '#split-control-panel-youtube',
            videoSelector: 'video',
            insertionSelector: 'ytd-watch-flexy #primary', // Insert panel into this
            insertionMethod: 'prepend',
            needsVisibilityCheck: false,
            videoTitleSelector: '#title h1', // Selector for video title
            styles: `
                #split-control-panel-youtube { background: var(--yt-spec-badge-chip-background); border: 1px solid var(--yt-spec-border-div); color: var(--yt-spec-text-primary); max-width: var(--ytd-watch-flexy-width); }
                ytd-watch-flexy:not([theater]) #primary #split-control-panel-youtube { margin-left: auto; margin-right: auto; }
                ytd-watch-flexy[theater] #primary #split-control-panel-youtube { max-width: 100%; }
                #split-control-panel-youtube label { color: var(--yt-spec-text-secondary); } #split-control-panel-youtube label i { color: var(--yt-spec-text-disabled); }
                #split-control-panel-youtube input[type="number"] { background: var(--yt-spec-filled-button-background); color: var(--yt-spec-text-primary); border: 1px solid var(--yt-spec-action-simulate-border); }
                #split-control-panel-youtube button { background: var(--yt-spec-grey-1); color: var(--yt-spec-text-primary); border: none; } #split-control-panel-youtube button:hover { background: var(--yt-spec-grey-2); }
                #split-control-panel-youtube .split-input-group button { background: var(--yt-spec-filled-button-background); border: 1px solid var(--yt-spec-action-simulate-border); } #split-control-panel-youtube .split-input-group button:hover { background: var(--yt-spec-grey-2); }
                #split-control-panel-youtube .set-split-button { background: var(--yt-spec-brand-suggested-action); color: var(--yt-spec-text-reverse); } #split-control-panel-youtube .set-split-button:hover { background: var(--yt-spec-brand-suggested-action-hover); }
                #split-control-panel-youtube .set-split-button.active { background: var(--yt-spec-call-to-action); } #split-control-panel-youtube .set-split-button.active:hover { background: var(--yt-spec-call-to-action-hover); }
                #split-control-panel-youtube .split-volume-control label { color: var(--yt-spec-text-secondary); }
                #split-control-panel-youtube .split-volume-control input[type="range"] { background: var(--yt-spec-grey-1); }
                #split-control-panel-youtube .split-volume-control input[type="range"]::-webkit-slider-thumb { background: var(--yt-spec-brand-button-background); } #split-control-panel-youtube .split-volume-control input[type="range"]::-moz-range-thumb { background: var(--yt-spec-brand-button-background); }
                #split-control-panel-youtube .split-stats { color: var(--yt-spec-text-primary); }
            `
        }
    };

    // --- ОБЩИЕ СТИЛИ (Панель + Оверлей) ---
    function injectGlobalStyles() {
        let platformStyles = platformConfig ? platformConfig.styles : '';

        GM_addStyle(`
            /* --- Общие стили панели управления --- */
            .split-control-panel-universal { margin-top: 10px; margin-bottom: 15px; padding: 10px 15px; border-radius: 8px; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; font-family: -apple-system, BlinkMacSystemFont, "Roboto", "Helvetica Neue", Geneva, "Noto Sans Armenian", "Noto Sans Bengali", "Noto Sans Cherokee", "Noto Sans Devanagari", "Noto Sans Ethiopic", "Noto Sans Georgian", "Noto Sans Hebrew", "Noto Sans Kannada", "Noto Sans Khmer", "Noto Sans Lao", "Noto Sans Osmanya", "Noto Sans Tamil", "Noto Sans Telugu", "Noto Sans Thai", sans-serif,arial,Tahoma,verdana; font-size: 13px; width: 100%; box-sizing: border-box; line-height: 1.4; }
            /* Reduced margin-bottom on the title */
            .split-control-panel-universal .panel-video-title { font-size: 14px; font-weight: bold; margin-bottom: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* Style for panel title */
            .split-control-panel-universal .panel-controls-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 15px; width: 100%; }
            .split-control-panel-universal label { font-weight: 500; flex-shrink: 0; }
            .split-control-panel-universal label i { font-style: normal; font-size: 11px; display: block; }
            .split-input-group { display: flex; align-items: center; gap: 4px; }
            .split-control-panel-universal input[type="number"] { width: 55px; padding: 6px 8px; border-radius: 4px; text-align: center; font-size: 14px; -moz-appearance: textfield; }
            .split-control-panel-universal input[type="number"]::-webkit-outer-spin-button, .split-control-panel-universal input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
            .split-control-panel-universal button { padding: 6px 12px; font-size: 13px; cursor: pointer; border-radius: 4px; transition: background 0.15s ease-in-out; font-weight: 500; flex-shrink: 0; line-height: normal; }
            .split-input-group button { padding: 6px 8px; }
            .set-split-button { order: -1; margin-right: auto; } /* Keep set button to the left */
            .split-volume-control { display: flex; align-items: center; gap: 5px; margin-left: auto; }
            .split-volume-control label { flex-shrink: 0; }
            .split-volume-control input[type="range"] { flex-grow: 1; min-width: 70px; -webkit-appearance: none; appearance: none; height: 6px; outline: none; opacity: 0.8; transition: opacity .2s; border-radius: 3px; cursor: pointer; }
            .split-volume-control input[type="range"]:hover { opacity: 1; }
            .split-control-panel-universal input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 12px; height: 12px; cursor: pointer; border-radius: 50%; }
            .split-control-panel-universal input[type="range"]::-moz-range-thumb { width: 12px; height: 12px; cursor: pointer; border-radius: 50%; border: none; }
            .split-stats { font-size: 14px; font-weight: 500; white-space: nowrap; }

             /* --- Общие стили оверлея --- */
            .split-overlay-universal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.95); color: white; display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 999999 !important; font-family: -apple-system, BlinkMacSystemFont, "Roboto", "Helvetica Neue", Geneva, "Noto Sans Armenian", "Noto Sans Bengali", "Noto Sans Cherokee", "Noto Sans Devanagari", "Noto Sans Ethiopic", "Noto Sans Georgian", "Noto Sans Hebrew", "Noto Sans Kannada", "Noto Sans Khmer", "Noto Sans Lao", "Noto Sans Osmanya", "Noto Sans Tamil", "Noto Sans Telugu", "Noto Sans Thai", sans-serif,arial,Tahoma,verdana; text-align: center; padding: 20px; box-sizing: border-box; }
            .split-overlay-universal .overlay-video-title { font-size: clamp(18px, 3vw, 28px); margin-bottom: 15px; color: #ccc; font-weight: normal; max-width: 90%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
            .split-overlay-universal .warning-message { font-size: clamp(24px, 4vw, 36px); margin-bottom: 15px; color: yellow; font-weight: bold; text-shadow: 0 0 8px rgba(255, 255, 0, 0.5); }
            .split-overlay-universal .main-message { font-size: clamp(40px, 8vw, 72px); font-weight: bold; margin-bottom: 20px; color: red; text-shadow: 0 0 15px rgba(255, 0, 0, 0.7); }
            .split-overlay-universal .overlay-timer, .split-overlay-universal .overlay-remaining-minutes { font-size: clamp(28px, 5vw, 48px); font-weight: bold; margin-bottom: 20px; }
            .split-overlay-universal .overlay-timer { color: orange; } .split-overlay-universal .overlay-remaining-minutes { color: cyan; }
            .split-overlay-universal .overlay-timer-control { margin-bottom: 20px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; justify-content: center; color: white; font-size: 18px; }
            .split-overlay-universal .overlay-timer-control label { font-weight: 500; }
            .split-overlay-universal .overlay-timer-control input[type="number"] { width: 70px; padding: 8px 10px; background: rgba(255, 255, 255, 0.1); color: white; border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 4px; text-align: center; font-size: 18px; -moz-appearance: textfield; }
            .split-overlay-universal .overlay-timer-control input[type="number"]::-webkit-outer-spin-button, .split-overlay-universal .overlay-timer-control input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
            .split-overlay-universal .overlay-timer-control button { padding: 8px 12px; font-size: 16px; cursor: pointer; background: rgba(255, 255, 255, 0.1); color: white; border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 4px; transition: background 0.2s ease-in-out; font-weight: 500; }
            .split-overlay-universal .overlay-timer-control button:hover { background: rgba(255, 255, 255, 0.2); }
            .split-overlay-universal .extend-buttons { display: flex; gap: 15px; flex-wrap: wrap; justify-content: center; margin-bottom: 40px; }
            .split-overlay-universal .extend-buttons button { padding: 12px 25px; font-size: clamp(18px, 3vw, 24px); cursor: pointer; background: #dc3545; border: none; color: white; border-radius: 4px; font-weight: bold; transition: background 0.2s ease-in-out; }
            .split-overlay-universal .extend-buttons button:hover { background: #c82333; }
            .split-overlay-universal img { max-width: 90%; max-height: 45vh; height: auto; border-radius: 8px; margin-top: 20px; }

            /* --- Стили конкретных платформ --- */
            ${platformStyles}
        `);
    }

    // --- ФУНКЦИИ ЯДРА ---

    function getElementId(baseId) {
        return platformConfig ? `${baseId}-${platformConfig.idPrefix}` : baseId;
    }

    function updateSplitDisplay() {
        const inputField = document.getElementById(getElementId("split-input"));
        if (inputField) inputField.valueAsNumber = splitMinutes === null ? 0 : splitMinutes;
        updateSplitStatsDisplay();
    }

    function updateSplitStatsDisplay() {
        const statsElement = document.getElementById(getElementId("split-stats"));
        if (statsElement) {
            const boughtMinutes = splitMinutes === null ? 0 : splitMinutes;
            statsElement.textContent = `Выкуплено: ${boughtMinutes} / Всего: ${totalVideoMinutes !== null ? totalVideoMinutes : '?'} минут`;
        }
        if (overlay) updateOverlayRemainingMinutes();
    }

    function modifySplitInput(minutesToModify) {
        const inputField = document.getElementById(getElementId("split-input"));
        if (!inputField) return;
        let currentVal = inputField.valueAsNumber;
        if (isNaN(currentVal)) currentVal = 0;
        let newVal = currentVal + minutesToModify;
        if (newVal < 0) newVal = 0;
        inputField.valueAsNumber = newVal;
    }

    function modifyTimerInputOverlay(secondsToModify) {
        const inputField = document.getElementById(getElementId("overlay-timer-input"));
        if (!inputField) return;
        let currentVal = inputField.valueAsNumber;
        if (isNaN(currentVal)) currentVal = 0;
        let newVal = currentVal + secondsToModify;
        if (newVal < 0) newVal = 0;
        inputField.valueAsNumber = newVal;

        overlayTimerDuration = newVal;
        localStorage.setItem(localStorageTimerKey, overlayTimerDuration.toString());
        overlayCountdownRemaining = overlayTimerDuration;
        if (overlayCountdownRemaining < 0) overlayCountdownRemaining = 0;

        if (overlayTimerIntervalId) { clearInterval(overlayTimerIntervalId); overlayTimerIntervalId = null; }
        updateOverlayTimer();
        if (overlayTimerDuration > 0) {
            overlayTimerIntervalId = setInterval(updateOverlayTimer, 1000);
        }
    }

    function startSplitCheckInterval() {
        if (!splitCheckIntervalId) {
            splitCheckIntervalId = setInterval(checkSplitCondition, 500);
            GM_log(`[${currentPlatform || 'unknown'}] Split check interval started.`);
        }
    }

    function stopSplitCheckInterval() {
        if (splitCheckIntervalId) {
            clearInterval(splitCheckIntervalId);
            splitCheckIntervalId = null;
            GM_log(`[${currentPlatform || 'unknown'}] Split check interval stopped.`);
        }
    }

    function addMinutesToActiveSplit(minutesToAdd) {
        if (splitMinutes === null) return;
        splitMinutes += minutesToAdd;
        updateSplitDisplay();

        const thresholdSeconds = splitMinutes * 60;
        // If video is before the new threshold AND overlay was active, remove overlay
        if (video && isFinite(video.currentTime) && video.currentTime < thresholdSeconds && splitTriggered) {
            GM_log(`[${currentPlatform}] Added minutes, new split threshold ${thresholdSeconds}s. Current time ${video.currentTime.toFixed(1)}s is before threshold. Removing overlay.`);
            removeOverlay();
            splitTriggered = false; // Reset the flag

            // Resume video if it was paused by the split, UNLESS the user paused it manually
            // This is tricky - simply attempting play is the most straightforward approach
            if (video.paused) {
                 GM_log(`[${currentPlatform}] Attempting to play video after adding minutes.`);
                 video.play().catch(e => GM_log(`[${currentPlatform}] Error playing video after adding minutes: ${e.message}`));
            }
        }
        GM_log(`[${currentPlatform}] Added ${minutesToAdd} minutes. New split: ${splitMinutes} minutes (${splitMinutes*60}s).`);
    }


    function updateOverlayTimer() {
        const timerElement = document.getElementById(getElementId('overlay-timer'));
        if (!timerElement) {
             if (overlayTimerIntervalId) { clearInterval(overlayTimerIntervalId); overlayTimerIntervalId = null; }
             return;
        }
        if (overlayCountdownRemaining > 0) {
            const minutes = Math.floor(overlayCountdownRemaining / 60);
            const seconds = overlayCountdownRemaining % 60;
            timerElement.textContent = `ЖДЕМ ${minutes}:${seconds < 10 ? '0' : ''}${seconds}, ИНАЧЕ СКИП`;
            overlayCountdownRemaining--;
        } else {
            timerElement.textContent = `ЖДЕМ 0:00, ИНАЧЕ СКИП`;
             // Timer reached 0. Decide what to do? Maybe jump ahead or just stay paused?
             // Current logic: stay paused and wait for user interaction (add minutes) or manual resume.
            if (overlayTimerIntervalId) { clearInterval(overlayTimerIntervalId); overlayTimerIntervalId = null; }
        }
    }

    function updateOverlayRemainingMinutes() {
        const remainingElement = document.getElementById(getElementId('overlay-remaining-minutes'));
        if (remainingElement) {
            const remainingMinutes = totalVideoMinutes !== null && splitMinutes !== null ? Math.max(0, totalVideoMinutes - splitMinutes) : '?';
            remainingElement.textContent = `ОСТАЛОСЬ ${remainingMinutes} минут выкупить`;
        }
    }

    function checkSplitCondition() {
        if (!platformConfig) return;

        // Ensure video element is found and audio player is initialized
        if (!video) {
            video = document.querySelector(platformConfig.videoSelector);
            if (!video) return; // Exit if video element isn't available yet
            GM_log(`[${currentPlatform}] Video element found.`);
            initAudioPlayer();
            const volumeSlider = document.getElementById(getElementId('split-volume-slider'));
             // Sync slider value to audio player volume on video element discovery
            if (audioPlayer && volumeSlider) { try { audioPlayer.volume = parseFloat(volumeSlider.value); } catch(e){} }
        }

        // Get total video duration if available and not already set
        if (totalVideoMinutes === null && isFinite(video.duration) && video.duration > 0) {
            totalVideoMinutes = Math.ceil(video.duration / 60);
            GM_log(`[${currentPlatform}] Total video duration found: ${totalVideoMinutes} minutes.`);
            if (panelAdded) updateSplitStatsDisplay();
        }
        // Note: Handling live streams or cases where duration changes might require more logic.
        // For now, totalVideoMinutes is set once if a valid duration is found.

        // Check the split condition only if splitMinutes is set and greater than 0
        if (splitMinutes !== null && splitMinutes > 0) {
            const thresholdSeconds = splitMinutes * 60;

            // Trigger split if current time is at or past the threshold AND split hasn't been triggered yet for this segment
            if (isFinite(video.currentTime) && video.currentTime >= thresholdSeconds && !splitTriggered) {
                GM_log(`[${currentPlatform}] Split condition met at ${video.currentTime.toFixed(1)}s (threshold: ${thresholdSeconds}s). Triggering split...`);
                video.pause(); // Pause the video
                splitTriggered = true; // Set the flag to prevent re-triggering
                showOverlay(); // Show the overlay

                // Play the alert sound
                if (audioPlayer) {
                    try {
                         audioPlayer.pause(); // Pause any ongoing playback of the sound
                         audioPlayer.currentTime = 0; // Reset sound to beginning
                         GM_log(`[${currentPlatform}] Playing split sound.`); // Log before playing
                         audioPlayer.play().catch(e => GM_log(`[${currentPlatform}] Error playing split sound: ${e.message}`)); // Play the sound
                    } catch(e) {
                         GM_log(`[${currentPlatform}] Error during audio playback attempt: ${e.message}`);
                    }
                }
            }

            // Handle the case where the user *manually seeks back* before the split point while the overlay is active
            if (splitTriggered && isFinite(video.currentTime) && video.currentTime < thresholdSeconds) {
                GM_log(`[${currentPlatform}] Video time (${video.currentTime.toFixed(1)}s) is now before split threshold (${thresholdSeconds}s) after seeking back. Removing overlay.`);
                removeOverlay(); // Remove the overlay
                splitTriggered = false; // Reset the flag so it can trigger again if the user seeks forward

                // Resume video if it was paused by the split
                 // This assumes if video.paused is true AND splitTriggered was true, the split paused it.
                 // It's not perfect but works in most cases.
                if (video.paused) {
                     GM_log(`[${currentPlatform}] Attempting to play video after seeking back past split point.`);
                     video.play().catch(e => GM_log(`[${currentPlatform}] Attempted to play video after seeking back past split point failed: ${e.message}`));
                }
            }
        } else if (splitTriggered) {
             // If splitMinutes is 0 or null, and the split was previously triggered, clean up
            GM_log(`[${currentPlatform}] Split cancelled (splitMinutes is ${splitMinutes}). Removing overlay.`);
            removeOverlay(); // Remove the overlay
            splitTriggered = false; // Reset the flag
             // No need to resume video here, as it would only be paused if splitMinutes > 0 initially.
        }
    }

    function showOverlay() {
        if (overlay) return; // Only show if not already visible
        if (!platformConfig) return; // Need config to proceed

        GM_log(`[${currentPlatform}] Showing split overlay.`);

        // --- GET VIDEO TITLE FOR OVERLAY ---
        let videoTitle = "Неизвестное видео";
        if (platformConfig.videoTitleSelector) {
            const titleElement = document.querySelector(platformConfig.videoTitleSelector);
            if (titleElement) {
                // Use textContent and trim for clean text, fallback to default if empty
                videoTitle = titleElement.textContent ? titleElement.textContent.trim() : "Неизвестное видео";
                 if (!videoTitle) videoTitle = "Неизвестное видео"; // Fallback if trim results in empty string
            } else {
                 GM_log(`[${currentPlatform}] Video title element not found with selector: ${platformConfig.videoTitleSelector}`);
            }
        } else {
             GM_log(`[${currentPlatform}] No video title selector defined for platform.`);
        }
        // --- END GET VIDEO TITLE FOR OVERLAY ---

        overlay = document.createElement("div");
        overlay.id = getElementId("split-overlay");
        overlay.className = 'split-overlay-universal';

        // --- ADD VIDEO TITLE TO OVERLAY ---
        const titleElement = document.createElement("div");
        titleElement.className = "overlay-video-title";
        titleElement.textContent = videoTitle;
        overlay.appendChild(titleElement); // Add title at the top of the overlay
        // --- END ADD VIDEO TITLE TO OVERLAY ---


        const warningMessage = document.createElement("div");
        warningMessage.className = "warning-message";
        warningMessage.textContent = "⚠️ НУЖНО ДОНАТНОЕ ТОПЛИВО ⚠️";
        overlay.appendChild(warningMessage);


        const mainMessage = document.createElement("div");
        mainMessage.className = "main-message";
        mainMessage.textContent = "СПЛИТ НЕ ОПЛАЧЕН";
        overlay.appendChild(mainMessage);


        const timerElement = document.createElement("div");
        timerElement.id = getElementId('overlay-timer');
        timerElement.className = "overlay-timer";
        overlay.appendChild(timerElement);


        const remainingMinutesElement = document.createElement("div");
        remainingMinutesElement.id = getElementId('overlay-remaining-minutes');
        remainingMinutesElement.className = "overlay-remaining-minutes";
        overlay.appendChild(remainingMinutesElement);


        const overlayTimerControlGroup = document.createElement("div");
        overlayTimerControlGroup.id = getElementId('overlay-timer-control');
        overlayTimerControlGroup.className = "overlay-timer-control";

        const timerLabel = document.createElement("label");
        timerLabel.setAttribute("for", getElementId('overlay-timer-input'));
        timerLabel.textContent = "Таймер (сек):";
        overlayTimerControlGroup.appendChild(timerLabel);

        const timerInputField = document.createElement("input");
        timerInputField.type = "number";
        timerInputField.id = getElementId('overlay-timer-input');
        timerInputField.min = "0";
        timerInputField.value = overlayTimerDuration; // Initialize with saved duration
        overlayTimerControlGroup.appendChild(timerInputField);


        const timerButtons = [
            { text: '-60', seconds: -60 }, { text: '-10', seconds: -10 }, { text: '-5', seconds: -5 },
            { text: '+5', seconds: 5 }, { text: '+10', seconds: 10 }, { text: '+60', seconds: 60 }
        ];
        timerButtons.forEach(btnInfo => {
            const button = document.createElement("button");
            button.textContent = btnInfo.text;
            button.dataset.seconds = btnInfo.seconds;
            overlayTimerControlGroup.appendChild(button);
        });
        overlay.appendChild(overlayTimerControlGroup);


        const extendButtonsContainer = document.createElement("div");
        extendButtonsContainer.id = getElementId('split-extend-buttons');
        extendButtonsContainer.className = "extend-buttons";
        const extendButtonConfigs = [
            { minutes: 1, cost: extendCost }, { minutes: 5, cost: extendCost * 5 },
            { minutes: 10, cost: extendCost * 10 }, { minutes: 20, cost: extendCost * 20 }
        ];
        extendButtonConfigs.forEach(config => {
            const button = document.createElement("button");
            button.textContent = `+ ${config.minutes} минут${getMinuteEnding(config.minutes)} - ${config.cost} рублей`;
            button.addEventListener("click", () => addMinutesToActiveSplit(config.minutes));
            extendButtonsContainer.appendChild(button);
        });
        overlay.appendChild(extendButtonsContainer);


        const gifElement = document.createElement("img");
        gifElement.src = overlayGifUrl;
        gifElement.alt = "Split GIF";
        overlay.appendChild(gifElement);


        document.body.appendChild(overlay); // Add overlay to the DOM

        // Add event listeners for timer controls *after* adding to DOM
        overlay.querySelector(`#${getElementId('overlay-timer-input')}`).addEventListener('input', function() {
            const val = this.valueAsNumber;
            if (!isNaN(val) && val >= 0) {
                 overlayTimerDuration = val;
                 localStorage.setItem(localStorageTimerKey, overlayTimerDuration.toString());
                 overlayCountdownRemaining = overlayTimerDuration; // Reset countdown when duration changes
                 if (overlayTimerIntervalId) { clearInterval(overlayTimerIntervalId); overlayTimerIntervalId = null; }
                 if (overlayTimerDuration > 0) overlayTimerIntervalId = setInterval(updateOverlayTimer, 1000);
                 updateOverlayTimer(); // Update immediately
            } else { /* Optional: revert to last valid value on invalid input */ }
        });
        overlay.querySelectorAll(`#${getElementId('overlay-timer-control')} button`).forEach(button => {
            button.addEventListener("click", () => modifyTimerInputOverlay(parseInt(button.dataset.seconds, 10)));
        });

        // Initialize and start the countdown timer
        overlayCountdownRemaining = overlayTimerDuration; // Start countdown from the set duration
        if (overlayCountdownRemaining < 0) overlayCountdownRemaining = 0; // Ensure non-negative
        updateOverlayTimer(); // Initial display update
        updateOverlayRemainingMinutes(); // Update remaining minutes display

        // Start timer interval if duration is positive, otherwise ensure it's stopped
        if (overlayTimerDuration > 0 && !overlayTimerIntervalId) {
            overlayTimerIntervalId = setInterval(updateOverlayTimer, 1000);
        } else if (overlayTimerDuration <= 0 && overlayTimerIntervalId) {
             clearInterval(overlayTimerIntervalId); overlayTimerIntervalId = null;
        }
    }

    function getMinuteEnding(count) {
        count = Math.abs(count); const d1 = count % 10; const d2 = count % 100;
        if (d2 >= 11 && d2 <= 19) return ''; if (d1 === 1) return 'а'; if (d1 >= 2 && d1 <= 4) return 'ы'; return '';
    }

    function removeOverlay() {
        if (overlay) {
             GM_log(`[${currentPlatform || 'unknown'}] Removing split overlay.`);
            overlay.remove(); // Remove element from DOM
            overlay = null; // Clear the variable

            // Stop the timer interval if it's running
            if (overlayTimerIntervalId) { clearInterval(overlayTimerIntervalId); overlayTimerIntervalId = null; }

            // Pause the audio player if it's playing the alert sound
            if (audioPlayer) {
                try { audioPlayer.pause(); } catch(e){}
            }
        }
    }

    function initAudioPlayer() {
        // Only create a new Audio player if one doesn't exist and a valid URL is provided
        if (!audioPlayer && splitSoundUrl && !splitSoundUrl.includes('YOUR_DIRECT_URL_HERE')) {
            GM_log(`[${currentPlatform || 'unknown'}] Initializing audio player with URL: ${splitSoundUrl}`);
            try {
                audioPlayer = new Audio(splitSoundUrl);
                audioPlayer.preload = 'auto'; // Start loading the audio early
                audioPlayer.loop = true; // IMPORTANT: Ensure the sound does NOT loop
                audioPlayer.onerror = (e) => { GM_log(`[${currentPlatform || 'unknown'}] ERROR loading audio: ${e.message || e.target.error?.message || 'Unknown error'}`); console.error("Universal Split: Ошибка загрузки звука:", e); audioPlayer = null; };

                // Set volume from local storage or use the default
                let savedVolume = localStorage.getItem(localStorageVolumeKey) ?? defaultAlertVolume;
                audioPlayer.volume = parseFloat(savedVolume);
            } catch (error) { GM_log(`[${currentPlatform || 'unknown'}] ERROR creating Audio object: ${error.message}`); console.error("Universal Split: Ошибка создания Audio:", error); audioPlayer = null; }
        } else if (audioPlayer) {
             // If player already exists, just ensure settings are correct (e.g., after navigation)
             let savedVolume = localStorage.getItem(localStorageVolumeKey) ?? defaultAlertVolume;
             try { audioPlayer.volume = parseFloat(savedVolume); } catch(e){}
             audioPlayer.loop = true; // Double-check loop is false
        }
    }

    // --- ФУНКЦИИ УПРАВЛЕНИЯ ПАНЕЛЬЮ ---

    function addControlPanel() {
        // Prevent adding the panel if it's already added or if no config is available
        if (panelAdded || !platformConfig) return;
        const insertionElement = document.querySelector(platformConfig.insertionSelector);
        // We need the insertion element to place the panel
        if (!insertionElement) { GM_log(`[${currentPlatform}] Insertion element (${platformConfig.insertionSelector}) not found.`); return; }

        GM_log(`[${currentPlatform}] Adding control panel...`);
        panelElement = document.createElement("div");
        panelElement.id = getElementId("split-control-panel");
        panelElement.className = 'split-control-panel-universal'; // Apply universal styles + platform-specific via id

        // --- GET VIDEO TITLE FOR PANEL ---
        let videoTitle = "Неизвестное видео";
        if (platformConfig.videoTitleSelector) {
             const titleElement = document.querySelector(platformConfig.videoTitleSelector);
             if (titleElement) {
                 // Use textContent and trim for clean text
                 videoTitle = titleElement.textContent ? titleElement.textContent.trim() : "Неизвестное видео";
                  if (!videoTitle) videoTitle = "Неизвестное видео"; // Fallback if trim results in empty string
             } else {
                  GM_log(`[${currentPlatform}] Video title element not found with selector: ${platformConfig.videoTitleSelector}`);
             }
        } else {
             GM_log(`[${currentPlatform}] No video title selector defined for platform.`);
        }
        // --- ADD VIDEO TITLE TO PANEL ---
        const panelTitleElement = document.createElement("div");
        panelTitleElement.className = "panel-video-title";
        panelTitleElement.textContent = videoTitle;
        panelElement.appendChild(panelTitleElement); // Add title at the top


        // --- Create a row for controls and stats to manage layout ---
        const controlsRow = document.createElement("div");
        controlsRow.className = "panel-controls-row";

        const setButton = document.createElement("button");
        setButton.id = getElementId('set-split-button');
        setButton.className = 'set-split-button';
        setButton.textContent = "НАЧАТЬ СПЛИТ";
         // Set button text and class based on current split state
        if (splitMinutes !== null && splitMinutes > 0) {
             setButton.textContent = "СПЛИТ НАЧАТ";
             setButton.classList.add("active");
        }
        controlsRow.appendChild(setButton); // Add set button to the row


        const splitLabel = document.createElement("label");
        splitLabel.setAttribute("for", getElementId('split-input'));
        splitLabel.appendChild(document.createTextNode("Сплит (мин):"));
        const splitLabelInstruction = document.createElement("i");
        splitLabelInstruction.textContent = "(уст. перед \"Начать\")";
        splitLabel.appendChild(splitLabelInstruction);
        controlsRow.appendChild(splitLabel); // Add label to the row

        const splitInputGroup = document.createElement("div");
        splitInputGroup.id = getElementId('split-input-group');
        splitInputGroup.className = 'split-input-group';

        const splitInputField = document.createElement("input");
        splitInputField.type = "number";
        splitInputField.id = getElementId('split-input');
        splitInputField.min = "0";
        splitInputField.value = splitMinutes === null ? 0 : splitMinutes; // Initialize input with current split state
        splitInputGroup.appendChild(splitInputField);

        const splitModifyButtons = [ { text: '+1', minutes: 1 }, { text: '+5', minutes: 5 }, { text: '+10', minutes: 10 }, { text: '+20', minutes: 20 } ];
        splitModifyButtons.forEach(btnInfo => {
            const button = document.createElement("button");
            button.textContent = btnInfo.text; button.dataset.minutes = btnInfo.minutes;
            splitInputGroup.appendChild(button);
        });
        controlsRow.appendChild(splitInputGroup); // Add input group to the row


        const volumeControlGroup = document.createElement("div");
        volumeControlGroup.id = getElementId('split-volume-control');
        volumeControlGroup.className = 'split-volume-control';
        const volumeLabel = document.createElement("label");
        volumeLabel.setAttribute("for", getElementId('split-volume-slider'));
        volumeLabel.textContent = "Громк. алерта:";
        const volumeSlider = document.createElement("input");
        volumeSlider.type = "range"; volumeSlider.id = getElementId('split-volume-slider');
        volumeSlider.min = "0"; volumeSlider.max = "1"; volumeSlider.step = "0.05";
        volumeSlider.value = localStorage.getItem(localStorageVolumeKey) ?? defaultAlertVolume; // Initialize slider with saved volume or default
        volumeControlGroup.appendChild(volumeLabel); volumeControlGroup.appendChild(volumeSlider);
        controlsRow.appendChild(volumeControlGroup); // Add volume control to the row


        const statsElement = document.createElement("span");
        statsElement.id = getElementId('split-stats'); statsElement.className = 'split-stats';
        statsElement.textContent = "Выкуплено: 0 / Всего: ? минут"; // Initial text, updated by updateSplitDisplay/Stats

        panelElement.appendChild(controlsRow); // Add the row containing controls (button, input, volume)
        panelElement.appendChild(statsElement); // Add stats element below the controls row


        // Insert the panel into the DOM according to platform config
        switch (platformConfig.insertionMethod) {
            case 'prepend': insertionElement.insertBefore(panelElement, insertionElement.firstChild); break;
            case 'appendChild': insertionElement.appendChild(panelElement); break;
            case 'before': insertionElement.parentNode.insertBefore(panelElement, insertionElement); break;
            case 'afterend': insertionElement.parentNode.insertBefore(panelElement, insertionElement.nextSibling); break;
            default: insertionElement.insertBefore(panelElement, insertionElement.firstChild); // Default to prepend
        }
        panelAdded = true; // Mark panel as added

        // --- Event Listeners ---
        setButton.addEventListener("click", () => {
            const inputVal = parseInt(splitInputField.value, 10);
            if (!isNaN(inputVal) && inputVal >= 0) {
                splitMinutes = inputVal; // Set the global splitMinutes state
                if (splitMinutes > 0) {
                    startSplitCheckInterval(); // Start the interval if split is active
                    setButton.textContent = "СПЛИТ НАЧАТ";
                    setButton.classList.add("active");
                    // Attempt autoplay if video is paused when split starts
                    if (video && video.paused) {
                         GM_log(`[${currentPlatform}] Attempting to play video on split start...`);
                         video.play().catch(e => GM_log(`[${currentPlatform}] Autoplay on split start failed (likely autoplay policy): ${e.message}`));
                    }
                } else { // splitMinutes is 0 or set to 0
                    stopSplitCheckInterval(); // Stop the interval if split is disabled
                    splitTriggered = false; // Ensure the triggered flag is false
                    removeOverlay(); // Remove overlay if it was visible
                    setButton.textContent = "НАЧАТЬ СПЛИТ";
                    setButton.classList.remove("active");
                }
                updateSplitDisplay(); // Update stats/input field display
            } else {
                alert("Введите корректное число минут.");
                splitInputField.valueAsNumber = splitMinutes === null ? 0 : splitMinutes; // Revert input to last valid value
            }
        });

        splitInputGroup.querySelectorAll("button").forEach(button => {
             button.addEventListener("click", () => modifySplitInput(parseInt(button.dataset.minutes, 10)));
        });

        volumeSlider.addEventListener("input", function() {
            const newVolume = parseFloat(this.value);
            if (audioPlayer) audioPlayer.volume = newVolume; // Update audio player volume
            localStorage.setItem(localStorageVolumeKey, newVolume.toString()); // Save volume preference
        });

        updateSplitDisplay(); // Initial update for stats and input field (in case splitMinutes was restored)

        // Initialize visibility check for VK if required by the platform
        if (platformConfig.needsVisibilityCheck) {
            controlsElement = document.querySelector(platformConfig.controlsElementSelector);
            if (controlsElement) startVisibilityCheckInterval();
            else GM_log(`[${currentPlatform}] Native controls element (${platformConfig.controlsElementSelector}) not found for visibility check.`);
        }

         // Start the split check interval if split was already active (e.g., after page reload/navigation)
        if (splitMinutes !== null && splitMinutes > 0) {
             startSplitCheckInterval();
        } else {
             stopSplitCheckInterval(); // Ensure it's stopped if split is off
        }

        GM_log(`[${currentPlatform}] Control panel added successfully.`);
    }

    // Ensures the panel stays in the correct place on sites with dynamic layouts
    function ensurePanelPosition() {
        if (!panelAdded || !panelElement || !platformConfig) return;
        const insertionElement = document.querySelector(platformConfig.insertionSelector);
        // If the element we attach to is gone, remove the panel
        if (!insertionElement) { GM_log(`[${currentPlatform}] Insertion element (${platformConfig.insertionSelector}) disappeared. Removing panel.`); removeControlPanel(); return; }

        let currentPositionCorrect = false;
        // Check if the panel element is currently positioned correctly relative to the insertion element
        switch(platformConfig.insertionMethod) {
            case 'prepend': currentPositionCorrect = (insertionElement.firstChild === panelElement); break;
            case 'appendChild': currentPositionCorrect = (insertionElement.lastChild === panelElement); break;
            case 'before': currentPositionCorrect = (insertionElement.previousSibling === panelElement); break;
            case 'afterend': currentPositionCorrect = (insertionElement.nextSibling === panelElement); break;
            default: currentPositionCorrect = (insertionElement.firstChild === panelElement); // Assume prepend if method is undefined
        }
        if (!currentPositionCorrect) {
            GM_log(`[${currentPlatform}] Panel position incorrect. Attempting re-insertion...`);
             try {
                 // Remove the element from its current parent before re-inserting
                 if (panelElement.parentNode) {
                     panelElement.parentNode.removeChild(panelElement);
                 }
                 // Re-insert according to the configured method
                 switch(platformConfig.insertionMethod) {
                     case 'prepend': insertionElement.insertBefore(panelElement, insertionElement.firstChild); break;
                     case 'appendChild': insertionElement.appendChild(panelElement); break;
                     case 'before': insertionElement.parentNode.insertBefore(panelElement, insertionElement); break;
                     case 'afterend': insertionElement.parentNode.insertBefore(panelElement, insertionElement.nextSibling); break;
                     default: insertionElement.insertBefore(panelElement, insertionElement.firstChild);
                 }
                 GM_log(`[${currentPlatform}] Panel re-inserted successfully.`);
             } catch (e) {
                 GM_log(`[${currentPlatform}] Failed to re-insert panel: ${e.message}`);
                 removeControlPanel(); // If re-insertion fails, remove the panel to avoid further errors
             }
        }
    }

    function removeControlPanel() {
        if (panelElement) {
             try {
                 panelElement.remove(); // Remove element from DOM
                 GM_log(`[${currentPlatform || 'unknown'}] Control panel element removed.`);
             } catch (e) {
                 GM_log(`[${currentPlatform || 'unknown'}] Error removing panel element: ${e.message}`);
             }
            panelElement = null; // Clear the variable
        }
        panelAdded = false; // Mark panel as removed
        stopVisibilityCheckInterval(); // Stop the VK visibility check
        controlsElement = null; // Clear the controls element reference
        // Note: splitMinutes and totalVideoMinutes state are *not* reset here, allowing them to persist across navigation.
    }

    // --- СПЕЦИФИЧНЫЕ ФУНКЦИИ ДЛЯ VK (Видимость панели) ---
    function checkControlsVisibility() {
        // Only run if VK, and if both the panel and controls elements exist
        if (!panelElement || !controlsElement || currentPlatform !== 'vk') {
             // If VK, but elements are missing, stop check and clean up
             if (currentPlatform === 'vk' && (!controlsElement || !panelElement)) {
                  GM_log(`[${currentPlatform}] Controls or panel element missing during visibility check. Stopping check and potentially removing panel.`);
                  stopVisibilityCheckInterval();
                  if (!panelElement) removeControlPanel(); // Only remove if panel itself is gone
             }
            return;
        }
        // Check VK's common methods for hiding controls
        const isHiddenByStyle = controlsElement.style.display === 'none';
        const isHiddenByClass = controlsElement.classList.contains('hidden') || controlsElement.classList.contains('videoplayer_controls_hide');
        const controlsAreVisible = !isHiddenByStyle && !isHiddenByClass;

        // Toggle the 'visible' class on the panel based on native controls visibility
        if (controlsAreVisible && !panelElement.classList.contains('visible')) {
            panelElement.classList.add('visible');
        } else if (!controlsAreVisible && panelElement.classList.contains('visible')) {
            panelElement.classList.remove('visible');
        }
    }

    function startVisibilityCheckInterval() {
        // Only start if VK, required, interval not running, and controls element found
        if (!visibilityCheckIntervalId && platformConfig?.needsVisibilityCheck && currentPlatform === 'vk' && controlsElement) {
            GM_log(`[${currentPlatform}] Starting controls visibility check interval.`);
            visibilityCheckIntervalId = setInterval(checkControlsVisibility, 300);
            checkControlsVisibility(); // Run immediately on start
        }
    }

    function stopVisibilityCheckInterval() {
        if (visibilityCheckIntervalId) {
            GM_log(`[${currentPlatform}] Stopping controls visibility check interval.`);
            clearInterval(visibilityCheckIntervalId);
            visibilityCheckIntervalId = null;
        }
    }

    // --- ОПРЕДЕЛЕНИЕ ТЕКУЩЕЙ ПЛАТФОРМЫ ---
    function getCurrentPlatform() {
        const hostname = location.hostname; const pathname = location.pathname;
        if (hostname.includes('vk.com') || hostname.includes('vkvideo.ru')) { if (pathname.includes('/video_ext.php')) return 'vk'; }
        else if (hostname.includes('rutube.ru')) { if (pathname.startsWith('/video/')) return 'rutube'; }
        else if (hostname.includes('youtube.com')) { if (pathname.startsWith('/watch')) return 'youtube'; }
        return 'unknown'; // Return 'unknown' if none match
    }

    // --- СБРОС СОСТОЯНИЯ ПРИ СМЕНЕ ВИДЕО ИЛИ ПЛАТФОРМЫ ---
    function resetState() {
        const platformToLog = currentPlatform !== 'unknown' ? currentPlatform : 'Previous';
        GM_log(`[${platformToLog}] Resetting state...`);

        // Stop all active intervals and observers managed by the script
        stopSplitCheckInterval();
        stopVisibilityCheckInterval();
        if (overlayTimerIntervalId) { clearInterval(overlayTimerIntervalId); overlayTimerIntervalId = null; }
        // Note: setupIntervalId is *not* cleared here, it's needed to re-setup on the new page

        // Remove UI elements from the DOM
        removeOverlay();
        removeControlPanel(); // This also stops the VK visibility check

        // Reset state variables specific to the current video/split
        splitMinutes = null; // Clear the configured split time
        totalVideoMinutes = null; // Clear the total duration
        video = null; // Clear the video element reference
        splitTriggered = false; // Reset the split triggered flag

        // Pause and reset audio player, but keep the player object for reuse
        if (audioPlayer) {
            try { audioPlayer.pause(); audioPlayer.currentTime = 0; } catch(e){ GM_log(`[${platformToLog}] Error resetting audio player: ${e.message}`); }
        }

        // Reset overlay timer countdown, but keep the duration setting from local storage
        overlayCountdownRemaining = overlayTimerDuration; // Reset countdown using the saved duration
    }

    // --- ГЛАВНАЯ ФУНКЦИЯ НАСТРОЙКИ СКРИПТА НА СТРАНИЦЕ ---
    function setupPlatformSplit() {
        const detectedPlatform = getCurrentPlatform();

        // If the platform has changed or become unknown, reset and start fresh
        if (detectedPlatform !== currentPlatform) {
             if (currentPlatform !== 'unknown') { GM_log(`Platform or video likely changed from ${currentPlatform} to ${detectedPlatform}. Resetting...`); resetState(); }
             currentPlatform = detectedPlatform; // Update current platform state

             if (currentPlatform !== 'unknown') {
                 platformConfig = platformConfigs[currentPlatform]; // Get configuration for the new platform
                 GM_log(`[${currentPlatform}] Initializing for new platform...`);
                 injectGlobalStyles(); // Re-inject styles (needed in case platform styles change or are removed)
                 initAudioPlayer(); // Ensure audio player is ready/re-initialized with settings
             } else {
                 platformConfig = null; // No config for unknown platform
                 GM_log(`[Universal] Unknown platform detected (${location.href}), no setup.`);
                 // If platform becomes unknown, stop the setup interval as we can't proceed
                 if (setupIntervalId) { clearInterval(setupIntervalId); setupIntervalId = null; GM_log(`[Universal] Stopping setup interval for unknown platform.`); }
                 return; // Exit if platform is unknown
             }
        }

        // If we are on a recognized platform but the panel is already added, just ensure its state and position
        if (currentPlatform !== 'unknown' && panelAdded) {
            ensurePanelPosition(); // Check and fix panel position if needed
             // Re-check controls visibility for VK if necessary
            if (platformConfig.needsVisibilityCheck && currentPlatform === 'vk' && !visibilityCheckIntervalId) {
                 controlsElement = document.querySelector(platformConfig.controlsElementSelector); // Re-find element if needed
                 if (controlsElement) startVisibilityCheckInterval();
            }
             // Re-start split check interval if split was active across navigation
            if (splitMinutes !== null && splitMinutes > 0 && !splitCheckIntervalId) {
                 startSplitCheckInterval();
                 checkSplitCondition(); // Check condition immediately after restarting the interval
            } else if (splitCheckIntervalId && (splitMinutes === null || splitMinutes <= 0)) {
                 stopSplitCheckInterval(); // Ensure interval is stopped if split is inactive
            }

            // If the panel is present and setup appears stable, stop the polling interval
             if (setupIntervalId) { clearInterval(setupIntervalId); setupIntervalId = null; GM_log(`[${currentPlatform}] Setup interval stopped, panel found and state checked.`); }
            return; // Setup is complete for this page state
        }

        // If the panel is not yet added for a recognized platform, try to add it
        // Find necessary elements: video and insertion point
        video = document.querySelector(platformConfig.videoSelector);
        const insertionElement = document.querySelector(platformConfig.insertionSelector);
        // Find VK specific controls element if needed
        controlsElement = platformConfig?.needsVisibilityCheck && currentPlatform === 'vk' ? document.querySelector(platformConfig.controlsElementSelector) : null;

        // If both video and insertion element are found, add the control panel
        if (video && insertionElement) {
            addControlPanel(); // This function handles adding the panel, listeners, and starting related intervals

            // If panel was successfully added, stop the polling interval
             if (panelAdded && setupIntervalId) { clearInterval(setupIntervalId); setupIntervalId = null; GM_log(`[${currentPlatform}] Setup interval stopped, panel successfully added.`); }
        } else {
            // Elements not found yet. The setupIntervalId remains active and will call setupPlatformSplit again.
            // This is normal for SPAs or pages where elements load dynamically.
            // GM_log(`[${currentPlatform}] Video (${platformConfig.videoSelector}) or insertion element (${platformConfig.insertionSelector}) not found yet. Retrying...`);
        }
    }

    // --- ИНИЦИАЛИЗАЦИЯ СКРИПТА И ОТСЛЕЖИВАНИЕ НАВИГАЦИИ ---
    function initialize() {
        GM_log("Universal Video Split: Initializing (v1.4)...");
        lastUrl = location.href; // Store the initial URL

        // Start a polling interval to repeatedly try setting up the script.
        // This handles pages where elements load asynchronously or are delayed.
        // The interval will stop itself once setup is successful.
        if (!setupIntervalId) { setupIntervalId = setInterval(setupPlatformSplit, 750); GM_log("Setup interval started."); }

        // Use a MutationObserver to detect significant changes in the DOM, often indicative of SPA navigation.
        // This is more efficient than polling for URL changes constantly.
        if (!navigationObserver) {
            navigationObserver = new MutationObserver((mutations) => {
                 // Check if the URL has changed. This is the primary trigger for a "new page" event in an SPA.
                 if (location.href !== lastUrl) {
                     GM_log(`URL changed detected by observer from ${lastUrl} to ${location.href}`);
                     lastUrl = location.href; // Update the last known URL
                     // When URL changes, reset the state related to the *previous* video/platform
                     // and rely on the active setupInterval (or a new one if it stopped)
                     // to configure the script for the *new* page.
                     resetState(); // This stops specific intervals/observers but leaves setupIntervalId.
                     // setupPlatformSplit will be called by the existing interval and handle detection/setup.
                     // If the setupIntervalId happened to be cleared (e.g., on an unknown platform),
                     // we should ensure it restarts here to catch the new page.
                      if (!setupIntervalId) { setupIntervalId = setInterval(setupPlatformSplit, 750); GM_log("Setup interval restarted after URL change."); }
                 }
            });
            // Observe changes in the body and its descendants. childList and subtree are needed to catch element additions/removals during navigation.
            navigationObserver.observe(document.body, { childList: true, subtree: true });
            GM_log("MutationObserver for navigation started.");
        }

        // Run setup once immediately on script load
        setupPlatformSplit();
    }

    // --- Очистка при выходе со страницы ---
    window.addEventListener('beforeunload', () => {
        GM_log("Universal Video Split: Unloading, cleaning up...");
        resetState(); // Perform a full reset of intervals, observers, and UI elements

        // Explicitly clear the setup interval on page unload
        if (setupIntervalId) { clearInterval(setupIntervalId); setupIntervalId = null; GM_log("Setup interval cleared on unload."); }

        // Disconnect the navigation observer on page unload
        if (navigationObserver) { navigationObserver.disconnect(); navigationObserver = null; GM_log("MutationObserver disconnected on unload."); }

        // Explicitly pause and nullify audioPlayer (though browser might handle this)
        if (audioPlayer) { try { audioPlayer.pause(); } catch(e){} audioPlayer = null; GM_log("Audio player cleared on unload."); }
    });

    // --- Запуск скрипта ---
    initialize();

})();