// ==UserScript== // @name YouTube Split // @namespace http://tampermonkey.net/ // @version 2.0 // @author sosal // @match *://www.youtube.com/watch* // @grant none // @description Устанавливает сплит по времени // @downloadURL none // ==/UserScript== (function() { 'use strict'; // --- Конфигурация --- let splitMinutes = null; // Значение сплита в минутах, по умолчанию null (сплит не активен) let totalVideoMinutes = null; // Общая длительность видео в минутах (округленная вверх), null пока не определена const extendCost = 300; // Стоимость продления +1 минуты в условных рублях // ВСТАВЬТЕ СЮДА ПРЯМУЮ ССЫЛКУ НА ВАШ ЗВУК! const splitSoundUrl = 'https://github.com/lardan099/donat/raw/refs/heads/main/alert_orig.mp3'; const localStorageVolumeKey = 'ytSplitAlertVolume'; // Ключ для сохранения громкости в localStorage // --- Глобальные переменные состояния --- let video = null; // Элемент видео let overlay = null; // Элемент оверлея let splitTriggered = false; // Флаг, что сплит активирован (видео остановлено) let audioPlayer = null; // Для проигрывания звука let splitCheckIntervalId = null; // ID интервала проверки сплита (для логики сплита) let setupIntervalId = null; // ID интервала для поиска элементов и добавления панели let panelAdded = false; // Флаг, чтобы добавлять панель только один раз // --- CSS Стили для улучшения внешнего вида --- 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); } /* Стили для кнопок модификации сплита (+1, +5 и т.д.) */ #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; } #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); } `; // --- Вспомогательная функция для внедрения CSS --- 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); console.log("YouTube Split: Стили внедрены."); } // Обновление отображения значения сплита в спинере // Эта функция теперь вызывается только при добавлении панели и при изменении global splitMinutes 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; // Не обновляем global splitMinutes здесь и не вызываем updateSplitDisplay() } // Функция для запуска интервала проверки сплита function startSplitCheckInterval() { if (!splitCheckIntervalId) { splitCheckIntervalId = setInterval(checkSplitCondition, 500); console.log("YouTube Split: Запущена проверка сплита (interval)."); } } // Функция для остановки интервала проверки сплита function stopSplitCheckInterval() { if (splitCheckIntervalId) { clearInterval(splitCheckIntervalId); splitCheckIntervalId = null; console.log("YouTube Split: Проверка сплита остановлена."); } } // Создание панели управления сплитом и вставка ее под видео // Эта функция вызывается, когда нужный контейнер уже найден и panelAdded === false function addControlPanel(primaryContainer) { if (panelAdded) { ensurePanelPosition(); updateSplitDisplay(); // Обновим поле ввода и статистику на существующей панели return; } if (!primaryContainer) { console.error("YouTube Split: addControlPanel вызвана без 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(); // Запускаем проверку если сплит > 0 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("Введите корректное число минут."); } }); // --- Метка "Сплит (мин):" (Исправление TrustedHTML) --- 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"; // HTMLMediaElement volume is typically 0 to 1 volumeSlider.step = "0.05"; // Adjust step for finer control // Установка начального значения громкости из localStorage (или дефолт) let savedVolume = localStorage.getItem(localStorageVolumeKey); if (savedVolume === null) { savedVolume = '0.5'; // Громкость по умолчанию 50% } volumeSlider.value = savedVolume; // Обработчик изменения громкости volumeSlider.addEventListener("input", function() { // Применяем громкость к аудиоплееру, если он есть if (audioPlayer) { audioPlayer.volume = parseFloat(this.value); // Убедимся, что это число } // Сохраняем значение в localStorage localStorage.setItem(localStorageVolumeKey, this.value); }); volumeControlGroup.appendChild(volumeLabel); volumeControlGroup.appendChild(volumeSlider); // --- Статистика выкупленных/всего минут --- const statsElement = document.createElement("span"); statsElement.id = "split-stats"; // Начальный текст будет установлен функцией updateSplitStatsDisplay // --- Собираем панель --- panel.appendChild(setButton); panel.appendChild(label); panel.appendChild(inputGroup); panel.appendChild(volumeControlGroup); // Добавляем регулятор громкости panel.appendChild(statsElement); // Добавляем статистику // Вставляем панель в найденный контейнер (#primary) как первый дочерний элемент primaryContainer.insertBefore(panel, primaryContainer.firstChild); panelAdded = true; // Устанавливаем флаг, что панель добавлена console.log("YouTube Split: Панель управления добавлена под видео."); // Убедимся, что поле ввода и статистика показывают актуальное значение после добавления updateSplitDisplay(); // Обновляет и поле ввода, и статистику } // Функция для проверки и корректировки позиции панели // Вызывается из setupElementsAndPanel 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) { console.log("YouTube Split: Панель сместилась, перемещаем обратно."); primaryContainer.insertBefore(panel, primaryContainer.firstChild); } } // Если панель есть, а контейнера нет, setupElementsAndPanel ее удалит. } function addMinutesToActiveSplit(minutesToAdd) { // Эта функция вызывается только когда сплит уже активен (splitMinutes не null) if (splitMinutes === null) return; splitMinutes += minutesToAdd; updateSplitDisplay(); // Обновляем поле ввода на панели и статистику const thresholdSeconds = splitMinutes * 60; if (video && video.currentTime < thresholdSeconds) { removeOverlay(); splitTriggered = false; video.play(); } // Оверлей остается, если продления недостаточно } // Проверка времени видео (вызывается только когда splitCheckIntervalId активен) function checkSplitCondition() { // Ищем видео каждый раз, на случай его пересоздания YouTube if (!video) { video = document.querySelector("video"); if (!video) { console.log("YouTube Split: Видеоэлемент не найден в checkSplitCondition, останавливаем проверку сплита."); stopSplitCheckInterval(); splitTriggered = false; removeOverlay(); return; } // Убеждаемся, что аудио плеер есть и громкость применена initAudioPlayer(); const volumeSlider = document.getElementById('split-volume-slider'); if(audioPlayer && volumeSlider) audioPlayer.volume = parseFloat(volumeSlider.value); // Применяем актуальную громкость } // Обновляем totalVideoMinutes, если он еще не определен и duration доступна if (totalVideoMinutes === null && video && isFinite(video.duration) && video.duration > 0) { totalVideoMinutes = Math.ceil(video.duration / 60); // Округляем вверх console.log(`YouTube Split: Общая длительность видео определена: ${totalVideoMinutes} минут.`); updateSplitStatsDisplay(); // Обновляем статистику с новым значением total } // Проверяем условие сплита только если splitMinutes активно (не null и > 0) 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 { // Если splitMinutes стал null или 0 во время работы проверки 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); }); overlay.appendChild(warningMessage); overlay.appendChild(splitMessage); overlay.appendChild(extendButtonsContainer); document.body.appendChild(overlay); console.log("YouTube Split: Оверлей показан."); } 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; } console.log("YouTube Split: Оверлей убран."); } } // Инициализация аудио плеера и установка громкости 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); console.log("YouTube Split: Аудио плеер инициализирован."); // Применяем сохраненную громкость после создания плеера let savedVolume = localStorage.getItem(localStorageVolumeKey); if (savedVolume !== null) { audioPlayer.volume = parseFloat(savedVolume); } else { audioPlayer.volume = 0.5; // Громкость по умолчанию } } } else { console.warn("YouTube Split: URL звука для сплита не указан или является плейсхолдером."); 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) { console.log("YouTube Split: Видео и контейнер #primary найдены. Добавляем панель."); addControlPanel(primaryContainer); // Передаем найденный контейнер } else { ensurePanelPosition(); // updateSplitDisplay() НЕ вызывается здесь // updateSplitStatsDisplay() НЕ вызывается здесь // updateSplitDisplay() вызывается при изменении splitMinutes // updateSplitStatsDisplay() вызывается при изменении splitMinutes И totalVideoMinutes } // Проверяем длительность видео, если она еще не определена if (totalVideoMinutes === null && isFinite(video.duration) && video.duration > 0) { totalVideoMinutes = Math.ceil(video.duration / 60); console.log(`YouTube Split: Общая длительность видео определена: ${totalVideoMinutes} минут.`); if (panelAdded) { // Если панель уже есть, обновляем статистику updateSplitStatsDisplay(); } } } else { // Элементы еще не найдены if (panelAdded) { console.log("YouTube Split: Необходимые элементы отсутствуют, удаляем панель и сбрасываем состояние."); const existingPanel = document.getElementById("split-control-panel"); if(existingPanel) existingPanel.remove(); panelAdded = false; // Сбрасываем флаг // Не сбрасываем splitMinutes на null здесь } video = null; // Сбрасываем видео, если оно пропало totalVideoMinutes = null; // Сбрасываем длительность, если видео пропало } } // --- Запуск скрипта --- if (!setupIntervalId) { setupIntervalId = setInterval(setupElementsAndPanel, 500); console.log("YouTube Split: Запущен основной интервал поиска элементов."); } // Очистка при уходе со страницы или навигации по SPA let lastUrl = location.href; const urlObserver = new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; console.log("YouTube Split: URL changed, cleaning up and re-initializing."); 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); console.log("YouTube Split: Перезапущен основной интервал поиска элементов после смены URL."); } } }); urlObserver.observe(document.body, { childList: true, subtree: true }); window.addEventListener('beforeunload', function() { console.log("YouTube Split: Очистка при выгрузке страницы."); stopSplitCheckInterval(); if (setupIntervalId) { clearInterval(setupIntervalId); setupIntervalId = null; } if (audioPlayer) { audioPlayer.pause(); audioPlayer = null; } if (urlObserver) { urlObserver.disconnect(); } }); })();