YouTube Split

Устанавливает сплит по времени, панель управления под видео, показывает статистику "Выкуплено/Всего минут", оверлей "СПЛИТ НЕ ОПЛАЧЕН" с гифкой и проигрывает звук при достижении порога.

As of 2025-04-18. See the latest version.

// ==UserScript==
// @name         YouTube Split
// @namespace    http://tampermonkey.net/
// @version      2.2
// @author       Gemini 2.5 Flash
// @match        *://www.youtube.com/watch*
// @grant        none
// @description Устанавливает сплит по времени, панель управления под видео, показывает статистику "Выкуплено/Всего минут", оверлей "СПЛИТ НЕ ОПЛАЧЕН" с гифкой и проигрывает звук при достижении порога.
// ==/UserScript==

(function() {
    'use strict';

    // --- Конфигурация ---
    let splitMinutes = null;
    let totalVideoMinutes = null;
    const extendCost = 300;
    const splitSoundUrl = 'https://github.com/lardan099/donat/raw/refs/heads/main/alert_orig.mp3'; // ВСТАВЬТЕ СЮДА ПРЯМУЮ ССЫЛКУ НА ВАШ ЗВУК!
    const overlayGifUrl = 'https://i.imgur.com/SS5Nfff.gif'; // URL гифки для оверлея
    const localStorageVolumeKey = 'ytSplitAlertVolume';


    // --- Глобальные переменные состояния ---
    let video = null;
    let overlay = null;
    let splitTriggered = false;
    let audioPlayer = null;
    let splitCheckIntervalId = null;
    let setupIntervalId = null;
    let panelAdded = false;


    const styles = `
        #split-control-panel {
            margin-top: 10px;
            margin-bottom: 15px;
            padding: 10px 15px;
            background: var(--yt-spec-badge-chip-background);
            border: 1px solid var(--yt-spec-border-div);
            border-radius: 8px;
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            gap: 10px 20px;
            color: var(--yt-spec-text-primary);
            font-family: "Roboto", Arial, sans-serif;
            font-size: 14px;
            max-width: var(--ytd-watch-flexy-width);
            width: 100%;
            box-sizing: border-box;
        }

        ytd-watch-flexy:not([use- Sarkis]) #primary #split-control-panel {
            margin-left: auto;
            margin-right: auto;
        }

        #split-control-panel label {
            font-weight: 500;
            color: var(--yt-spec-text-secondary);
            flex-shrink: 0;
            line-height: 1.3;
        }

        #split-control-panel label i {
             font-style: normal;
             font-size: 12px;
             color: var(--yt-spec-text-disabled);
        }

        #split-input-group {
            display: flex;
            align-items: center;
            gap: 5px;
        }

        #split-control-panel input[type="number"] {
            width: 60px;
            padding: 8px 10px;
            background: var(--yt-spec-filled-button-background);
            color: var(--yt-spec-text-primary);
            border: 1px solid var(--yt-spec-action-simulate-border);
            border-radius: 4px;
            text-align: center;
            font-size: 15px;
            -moz-appearance: textfield;
        }

        #split-control-panel input[type="number"]::-webkit-outer-spin-button,
        #split-control-panel input[type="number"]::-webkit-inner-spin-button {
            -webkit-appearance: none;
            margin: 0;
        }

        #split-control-panel button {
            padding: 8px 15px;
            font-size: 15px;
            cursor: pointer;
            background: var(--yt-spec-grey-1);
            color: var(--yt-spec-text-primary);
            border: none;
            border-radius: 4px;
            transition: background 0.2s ease-in-out;
            font-weight: 500;
            flex-shrink: 0;
        }

        #split-control-panel button:hover {
             background: var(--yt-spec-grey-2);
        }

        #split-input-group button {
            padding: 8px 10px;
            font-size: 14px;
            background: var(--yt-spec-filled-button-background);
            border: 1px solid var(--yt-spec-action-simulate-border);
        }
         #split-input-group button:hover {
            background: var(--yt-spec-grey-2);
         }

        #split-control-panel button#set-split-button {
             background: var(--yt-spec-brand-suggested-action);
             color: var(--yt-spec-text-reverse);
             order: -1;
             margin-right: auto;
        }
         #split-control-panel button#set-split-button:hover {
             background: var(--yt-spec-brand-suggested-action-hover);
        }

        #split-volume-control {
             display: flex;
             align-items: center;
             gap: 5px;
        }
         #split-volume-control label {
             font-weight: 500;
             color: var(--yt-spec-text-secondary);
             flex-shrink: 0;
             line-height: normal;
         }
         #split-volume-control input[type="range"] {
             flex-grow: 1;
             min-width: 80px;
             -webkit-appearance: none;
             appearance: none;
             height: 8px;
             background: var(--yt-spec-grey-1);
             outline: none;
             opacity: 0.7;
             transition: opacity .2s;
             border-radius: 4px;
         }
         #split-volume-control input[type="range"]:hover {
             opacity: 1;
         }

        #split-volume-control input[type="range"]::-webkit-slider-thumb {
             -webkit-appearance: none;
             appearance: none;
             width: 15px;
             height: 15px;
             background: var(--yt-spec-brand-button-background);
             cursor: pointer;
             border-radius: 50%;
        }

        #split-volume-control input[type="range"]::-moz-range-thumb {
             width: 15px;
             height: 15px;
             background: var(--yt-spec-brand-button-background);
             cursor: pointer;
             border-radius: 50%;
        }

        #split-stats {
            font-size: 15px;
            color: var(--yt-spec-text-primary);
            font-weight: 500;
            margin-left: 10px;
        }


        #split-overlay {
            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: 99999;
            font-family: "Roboto", Arial, sans-serif;
            text-align: center;
            padding: 20px;
            box-sizing: border-box;
        }

        #split-overlay #split-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 #split-main-message {
            font-size: clamp(40px, 8vw, 72px);
            font-weight: bold;
            margin-bottom: 40px;
            color: red;
            text-shadow: 0 0 15px rgba(255, 0, 0, 0.7);
        }

        #split-extend-buttons {
            display: flex;
            gap: 15px;
            flex-wrap: wrap;
            justify-content: center;
            margin-bottom: 40px; /* Отступ перед гифкой */
        }

        #split-extend-buttons button {
            padding: 12px 25px;
            font-size: clamp(18px, 3vw, 24px);
            cursor: pointer;
            background: var(--yt-spec-red-500);
            border: none;
            color: white;
            border-radius: 4px;
            font-weight: bold;
            transition: background 0.2s ease-in-out;
        }

        #split-extend-buttons button:hover {
            background: var(--yt-spec-red-600);
        }

        /* Стили для гифки на оверлее */
        #split-overlay img {
            max-width: 115%; /* Увеличим максимальную ширину */
            max-height: 50vh; /* Увеличим максимальную высоту (например, до 50% высоты вьюпорта) */
            height: auto;
            border-radius: 8px;
            margin-top: 20px; /* Добавим отступ сверху, чтобы отделить от кнопок */
        }
    `;

    function injectStyles() {
        if (document.getElementById('yt-split-styles')) {
            return;
        }
        const styleElement = document.createElement("style");
        styleElement.id = 'yt-split-styles';
        styleElement.textContent = styles;
        document.head.appendChild(styleElement);
    }

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

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

     function modifySplitInput(minutesToModify) {
         const inputField = document.getElementById("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 startSplitCheckInterval() {
        if (!splitCheckIntervalId) {
            splitCheckIntervalId = setInterval(checkSplitCondition, 500);
        }
    }

    function stopSplitCheckInterval() {
        if (splitCheckIntervalId) {
            clearInterval(splitCheckIntervalId);
            splitCheckIntervalId = null;
        }
    }

    function addControlPanel(primaryContainer) {
        if (panelAdded) {
            ensurePanelPosition();
            updateSplitDisplay();
            return;
        }

        if (!primaryContainer) {
             return;
        }

        const panel = document.createElement("div");
        panel.id = "split-control-panel";

        const setButton = document.createElement("button");
        setButton.id = "set-split-button";
        setButton.textContent = "НАЧАТЬ СПЛИТ";
        setButton.addEventListener("click", function() {
            const inputField = document.getElementById("split-input");
            const inputVal = inputField.valueAsNumber;

            if (!isNaN(inputVal) && inputVal >= 0) {
                const oldSplitMinutes = splitMinutes;
                splitMinutes = inputVal;

                 if (splitMinutes > 0) {
                     startSplitCheckInterval();
                     setButton.textContent = "СПЛИТ НАЧАТ";
                     setButton.style.background = 'var(--yt-spec-call-to-action)';

                     if (video) {
                         const thresholdSeconds = splitMinutes * 60;
                         if (video.currentTime >= thresholdSeconds) {
                             video.pause();
                             splitTriggered = true;
                             showOverlay();
                             if(splitSoundUrl && audioPlayer && audioPlayer.src !== 'ВАША_ПРЯМАЯ_ССЫЛКА_НА_ЗВУКОВОЙ_ФАЙЛ_ТУТ'){
                                 audioPlayer.pause();
                                 audioPlayer.currentTime = 0;
                                 audioPlayer.play().catch(e => console.error("YouTube Split: Ошибка при воспроизведении звука:", e));
                             }
                         } else {
                             splitTriggered = false;
                             removeOverlay();
                             if (video.paused && oldSplitMinutes === null) {
                                 video.play();
                             }
                         }
                     }

                 } else { // splitMinutes === 0
                     stopSplitCheckInterval();
                     splitTriggered = false;
                     removeOverlay();
                     setButton.textContent = "НАЧАТЬ СПЛИТ";
                     setButton.style.background = 'var(--yt-spec-brand-suggested-action)';

                     if (video && video.paused && oldSplitMinutes !== null && oldSplitMinutes > 0) {
                         video.play();
                     }
                 }
                 updateSplitDisplay();
                 updateSplitStatsDisplay();
            } else {
                alert("Введите корректное число минут.");
            }
        });

        const label = document.createElement("label");
        label.setAttribute("for", "split-input");
        const labelTextMain = document.createTextNode("Сплит (мин):");
        const breakElement = document.createElement("br");
        const italicElement = document.createElement("i");
        const labelTextInstruction = document.createTextNode("(уст. перед \"Начать\")");

        label.appendChild(labelTextMain);
        label.appendChild(breakElement);
        italicElement.appendChild(labelTextInstruction);
        label.appendChild(italicElement);

        const inputGroup = document.createElement("div");
        inputGroup.id = "split-input-group";

        const inputField = document.createElement("input");
        inputField.type = "number";
        inputField.id = "split-input";
        inputField.min = "0";
        inputField.valueAsNumber = splitMinutes === null ? 0 : splitMinutes;

         const modifyButtons = [
             { text: '-10', minutes: -10 },
             { text: '-5', minutes: -5 },
             { text: '-1', minutes: -1 },
             { text: '+1', minutes: 1 },
             { text: '+5', minutes: 5 },
             { text: '+10', minutes: 10 },
             { text: '+20', minutes: 20 }
         ];

         modifyButtons.forEach(btnInfo => {
             const button = document.createElement("button");
             button.textContent = btnInfo.text;
             button.addEventListener("click", () => modifySplitInput(btnInfo.minutes));
             inputGroup.appendChild(button);
         });

        inputGroup.insertBefore(inputField, inputGroup.children[0]);


        const volumeControlGroup = document.createElement("div");
        volumeControlGroup.id = "split-volume-control";

        const volumeLabel = document.createElement("label");
        volumeLabel.setAttribute("for", "split-volume-slider");
        volumeLabel.textContent = "Громкость алерта:";

        const volumeSlider = document.createElement("input");
        volumeSlider.type = "range";
        volumeSlider.id = "split-volume-slider";
        volumeSlider.min = "0";
        volumeSlider.max = "1";
        volumeSlider.step = "0.05";

        let savedVolume = localStorage.getItem(localStorageVolumeKey);
        if (savedVolume === null) {
             savedVolume = '0.5';
        }
        volumeSlider.value = savedVolume;

        volumeSlider.addEventListener("input", function() {
             if (audioPlayer) {
                 audioPlayer.volume = parseFloat(this.value);
             }
             localStorage.setItem(localStorageVolumeKey, this.value);
        });

        volumeControlGroup.appendChild(volumeLabel);
        volumeControlGroup.appendChild(volumeSlider);

        if (audioPlayer) {
             audioPlayer.volume = parseFloat(savedVolume);
        }

        const statsElement = document.createElement("span");
        statsElement.id = "split-stats";


        panel.appendChild(setButton);
        panel.appendChild(label);
        panel.appendChild(inputGroup);
        panel.appendChild(volumeControlGroup);
        panel.appendChild(statsElement);

        primaryContainer.insertBefore(panel, primaryContainer.firstChild);
        panelAdded = true;
        updateSplitDisplay();
        updateSplitStatsDisplay();
    }

     function ensurePanelPosition() {
         if (!panelAdded) return;

         const panel = document.getElementById("split-control-panel");
         const primaryContainer = document.querySelector("ytd-watch-flexy #primary");

         if (panel && primaryContainer) {
             if (primaryContainer.firstChild !== panel) {
                  primaryContainer.insertBefore(panel, primaryContainer.firstChild);
             }
         }
     }


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

        splitMinutes += minutesToAdd;
        updateSplitDisplay();

        const thresholdSeconds = splitMinutes * 60;

        if (video && video.currentTime < thresholdSeconds) {
            removeOverlay();
            splitTriggered = false;
            video.play();
        }
    }

    function checkSplitCondition() {
        if (!video) {
            video = document.querySelector("video");
            if (!video) {
                 stopSplitCheckInterval();
                 splitTriggered = false;
                 removeOverlay();
                 return;
            }
             initAudioPlayer();
             const volumeSlider = document.getElementById('split-volume-slider');
             if(audioPlayer && volumeSlider) audioPlayer.volume = parseFloat(volumeSlider.value);
        }

        if (totalVideoMinutes === null && isFinite(video.duration) && video.duration > 0) {
             totalVideoMinutes = Math.ceil(video.duration / 60);
             if (panelAdded) {
                 updateSplitStatsDisplay();
             }
        }


        if (splitMinutes !== null && splitMinutes > 0) {
            const thresholdSeconds = splitMinutes * 60;

            if (video.currentTime >= thresholdSeconds && !splitTriggered) {
                video.pause();
                splitTriggered = true;
                showOverlay();
                if(splitSoundUrl && audioPlayer && audioPlayer.src !== 'ВАША_ПРЯМАЯ_ССЫЛКА_НА_ЗВУКОВОЙ_ФАЙЛ_ТУТ'){
                     audioPlayer.pause();
                     audioPlayer.currentTime = 0;
                     audioPlayer.play().catch(e => console.error("YouTube Split: Ошибка при воспроизведении звука:", e));
                }
            }
            if (splitTriggered && video.currentTime < thresholdSeconds) {
                removeOverlay();
                splitTriggered = false;
                video.play();
            }
        } else {
             stopSplitCheckInterval();
             splitTriggered = false;
             removeOverlay();
             if (video && video.paused) video.play();
        }
    }

    function showOverlay() {
        if (overlay) return;

        overlay = document.createElement("div");
        overlay.id = "split-overlay";

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

        const splitMessage = document.createElement("div");
        splitMessage.id = "split-main-message";
        splitMessage.textContent = "СПЛИТ НЕ ОПЛАЧЕН";

        const extendButtonsContainer = document.createElement("div");
        extendButtonsContainer.id = "split-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", function() {
                addMinutesToActiveSplit(config.minutes);
            });
            extendButtonsContainer.appendChild(button);
        });

        // --- Добавление гифки ---
        const gifElement = document.createElement("img");
        gifElement.src = overlayGifUrl;
        // CSS стили для гифки определены в блоке styles


        overlay.appendChild(warningMessage);
        overlay.appendChild(splitMessage);
        overlay.appendChild(extendButtonsContainer);
        overlay.appendChild(gifElement); // Добавляем гифку после кнопок

        document.body.appendChild(overlay);
    }

    function getMinuteEnding(count) {
        const lastDigit = count % 10;
        const lastTwoDigits = count % 100;

        if (lastTwoDigits >= 11 && lastTwoDigits <= 14) {
            return '';
        }
        if (lastDigit === 1) {
            return 'а';
        }
        if (lastDigit >= 2 && lastDigit <= 4) {
            return 'ы';
        }
        return '';
    }

    function removeOverlay() {
        if (overlay) {
            overlay.remove();
            overlay = null;
            if (audioPlayer) {
                 audioPlayer.pause();
                 audioPlayer.currentTime = 0;
            }
        }
    }

     function initAudioPlayer() {
         if (splitSoundUrl && splitSoundUrl !== 'ВАША_ПРЯМАЯ_ССЫЛКА_НА_ЗВУКОВОЙ_ФАЙЛ_ТУТ') {
             if (!audioPlayer || audioPlayer.src !== splitSoundUrl) {
                  if (audioPlayer) {
                     audioPlayer.pause();
                     audioPlayer = null;
                  }
                 audioPlayer = new Audio(splitSoundUrl);
                 audioPlayer.preload = 'auto';
                 audioPlayer.onerror = (e) => console.error("YouTube Split: Не удалось загрузить или воспроизвести звук:", e);

                 let savedVolume = localStorage.getItem(localStorageVolumeKey);
                 if (savedVolume !== null) {
                      audioPlayer.volume = parseFloat(savedVolume);
                 } else {
                      audioPlayer.volume = 0.5;
                 }
             }
         } else {
              if (audioPlayer) {
                 audioPlayer.pause();
                 audioPlayer = null;
             }
         }
     }

    function setupElementsAndPanel() {
        injectStyles();
        initAudioPlayer();

        video = document.querySelector("video");
        const primaryContainer = document.querySelector("ytd-watch-flexy #primary");
        const panel = document.getElementById("split-control-panel");

        if (video && primaryContainer) {
             if (!panelAdded) {
                 addControlPanel(primaryContainer);
             } else {
                  ensurePanelPosition();
             }

            if (totalVideoMinutes === null && isFinite(video.duration) && video.duration > 0) {
                totalVideoMinutes = Math.ceil(video.duration / 60);
                 if (panelAdded) {
                     updateSplitStatsDisplay();
                 }
            }

        } else {
            if (panelAdded) {
                 const existingPanel = document.getElementById("split-control-panel");
                 if(existingPanel) existingPanel.remove();
                 panelAdded = false;
            }
             video = null;
             totalVideoMinutes = null;
        }
    }

    if (!setupIntervalId) {
        setupIntervalId = setInterval(setupElementsAndPanel, 500);
    }

    let lastUrl = location.href;
    const urlObserver = new MutationObserver(() => {
        if (location.href !== lastUrl) {
            lastUrl = location.href;

            stopSplitCheckInterval();
            if (setupIntervalId) {
                 clearInterval(setupIntervalId);
                 setupIntervalId = null;
            }

             if (audioPlayer) {
                 audioPlayer.pause();
             }
             removeOverlay();

            const oldPanel = document.getElementById("split-control-panel");
            if (oldPanel) {
                oldPanel.remove();
            }
            const oldStyles = document.getElementById("yt-split-styles");
             if(oldStyles) oldStyles.remove();

            splitMinutes = null;
            totalVideoMinutes = null;
            video = null;
            splitTriggered = false;
            panelAdded = false;

            if (!setupIntervalId) {
                 setupIntervalId = setInterval(setupElementsAndPanel, 500);
            }
        }
    });

     urlObserver.observe(document.body, {
        childList: true,
        subtree: true
     });


    window.addEventListener('beforeunload', function() {
        stopSplitCheckInterval();
        if (setupIntervalId) {
            clearInterval(setupIntervalId);
            setupIntervalId = null;
        }
         if (audioPlayer) {
            audioPlayer.pause();
            audioPlayer = null;
         }
        if (urlObserver) {
             urlObserver.disconnect();
        }
    });


})();