// ==UserScript==
// @name Universal Video Split
// @namespace http://tampermonkey.net/
// @version 1.1
// @author Gemini
// @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.1). Панель управления, статистика, оверлей, звук, таймер.
// ==/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'; // URL звука алерта
const overlayGifUrl = 'https://i.imgur.com/SS5Nfff.gif'; // URL гифки для оверлея
const localStorageVolumeKey = 'universalSplitAlertVolume'; // Общий ключ для громкости
const localStorageTimerKey = 'universalSplitOverlayTimer'; // Общий ключ для таймера
const defaultOverlayTimerDuration = 360; // Длительность таймера по умолчанию (секунды)
// --- ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ СОСТОЯНИЯ ---
let currentPlatform = 'unknown'; // 'vk', 'rutube', 'youtube'
let platformConfig = null; // Конфигурация для текущей платформы
let video = null; // Ссылка на HTML-видео элемент
let overlay = null; // Ссылка на элемент оверлея
let splitTriggered = false; // Флаг, сработал ли сплит
let audioPlayer = null; // Аудиоплеер для алерта
let splitCheckIntervalId = null; // ID интервала проверки времени видео
let setupIntervalId = null; // ID интервала для первоначальной настройки
let panelAdded = false; // Флаг, добавлена ли панель управления
let panelElement = null; // Ссылка на панель управления
let controlsElement = null; // Ссылка на нативные контролы (только для VK)
let visibilityCheckIntervalId = null; // ID интервала проверки видимости контролов (только для VK)
let navigationObserver = null; // MutationObserver для отслеживания навигации
let lastUrl = location.href; // Последний известный URL для отслеживания навигации
let overlayTimerDuration = parseInt(localStorage.getItem(localStorageTimerKey), 10);
if (isNaN(overlayTimerDuration) || overlayTimerDuration < 0) {
overlayTimerDuration = defaultOverlayTimerDuration;
}
let overlayTimerIntervalId = null; // ID интервала таймера на оверлее
let overlayCountdownRemaining = overlayTimerDuration; // Оставшееся время на таймере оверлея
// --- КОНФИГУРАЦИЯ ПЛАТФОРМ ---
const platformConfigs = {
vk: {
idPrefix: 'vk',
controlPanelSelector: '#split-control-panel-vk',
videoSelector: 'video',
insertionSelector: '.videoplayer_title',
insertionMethod: 'afterend',
controlsElementSelector: '.videoplayer_controls',
needsVisibilityCheck: true,
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',
insertionMethod: 'prepend',
needsVisibilityCheck: false,
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',
insertionMethod: 'prepend',
needsVisibilityCheck: false,
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-wrap: wrap; align-items: center; gap: 10px 15px; 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; }
.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; }
.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-volume-control input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 12px; height: 12px; cursor: pointer; border-radius: 50%; }
.split-volume-control 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 .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) {
// Добавляем проверку на platformConfig
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}] Split check interval started.`);
}
}
function stopSplitCheckInterval() {
if (splitCheckIntervalId) {
clearInterval(splitCheckIntervalId);
splitCheckIntervalId = null;
GM_log(`[${currentPlatform}] Split check interval stopped.`);
}
}
function addMinutesToActiveSplit(minutesToAdd) {
if (splitMinutes === null) return;
splitMinutes += minutesToAdd;
updateSplitDisplay();
const thresholdSeconds = splitMinutes * 60;
if (video && video.currentTime < thresholdSeconds && splitTriggered) {
removeOverlay();
splitTriggered = false;
if (video.paused) {
video.play().catch(e => GM_log(`[${currentPlatform}] Error playing video after adding minutes: ${e.message}`));
}
}
GM_log(`[${currentPlatform}] Added ${minutesToAdd} minutes. New split: ${splitMinutes}`);
}
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, ИНАЧЕ СКИП`;
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; // Не проверять, если не на нужной платформе
if (!video) {
video = document.querySelector(platformConfig.videoSelector);
if (!video) return;
GM_log(`[${currentPlatform}] Video element found.`);
initAudioPlayer();
const volumeSlider = document.getElementById(getElementId('split-volume-slider'));
if (audioPlayer && volumeSlider) { try { audioPlayer.volume = parseFloat(volumeSlider.value); } catch(e){} }
}
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();
} else if ((!isFinite(video.duration) || video.duration <= 0) && totalVideoMinutes !== null) {
// totalVideoMinutes = null;
// if (panelAdded) updateSplitStatsDisplay();
}
if (splitMinutes !== null && splitMinutes > 0) {
const thresholdSeconds = splitMinutes * 60;
if (isFinite(video.currentTime) && video.currentTime >= thresholdSeconds && !splitTriggered) {
GM_log(`[${currentPlatform}] Split triggered at ${video.currentTime.toFixed(1)}s (threshold: ${thresholdSeconds}s).`);
video.pause();
splitTriggered = true;
showOverlay();
if (audioPlayer) {
audioPlayer.pause();
audioPlayer.currentTime = 0;
audioPlayer.play().catch(e => GM_log(`[${currentPlatform}] Error playing split sound: ${e.message}`));
}
}
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). Removing overlay.`);
removeOverlay();
splitTriggered = false;
}
} else if (splitTriggered) {
GM_log(`[${currentPlatform}] Split cancelled (splitMinutes is ${splitMinutes}). Removing overlay.`);
removeOverlay();
splitTriggered = false;
}
}
// --- РЕФАКТОРИНГ showOverlay: Используем createElement ---
function showOverlay() {
if (overlay) return;
GM_log(`[${currentPlatform}] Showing split overlay.`);
overlay = document.createElement("div");
overlay.id = getElementId("split-overlay");
overlay.className = 'split-overlay-universal';
// Создаем элементы по отдельности
const warningMessage = document.createElement("div");
warningMessage.className = "warning-message";
warningMessage.textContent = "⚠️ НУЖНО ДОНАТНОЕ ТОПЛИВО ⚠️";
const mainMessage = document.createElement("div");
mainMessage.className = "main-message";
mainMessage.textContent = "СПЛИТ НЕ ОПЛАЧЕН";
const timerElement = document.createElement("div");
timerElement.id = getElementId('overlay-timer');
timerElement.className = "overlay-timer";
const remainingMinutesElement = document.createElement("div");
remainingMinutesElement.id = getElementId('overlay-remaining-minutes');
remainingMinutesElement.className = "overlay-remaining-minutes";
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 = "Таймер (сек):";
const timerInputField = document.createElement("input");
timerInputField.type = "number";
timerInputField.id = getElementId('overlay-timer-input');
timerInputField.min = "0";
timerInputField.value = overlayTimerDuration; // Устанавливаем значение
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 }
];
overlayTimerControlGroup.appendChild(timerLabel);
overlayTimerControlGroup.appendChild(timerInputField);
timerButtons.forEach(btnInfo => {
const button = document.createElement("button");
button.textContent = btnInfo.text;
button.dataset.seconds = btnInfo.seconds; // Сохраняем секунды в data-атрибуте
overlayTimerControlGroup.appendChild(button);
});
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)); // Listener здесь
extendButtonsContainer.appendChild(button);
});
const gifElement = document.createElement("img");
gifElement.src = overlayGifUrl;
gifElement.alt = "Split GIF";
// Добавляем созданные элементы в оверлей
overlay.appendChild(warningMessage);
overlay.appendChild(mainMessage);
overlay.appendChild(timerElement);
overlay.appendChild(remainingMinutesElement);
overlay.appendChild(overlayTimerControlGroup);
overlay.appendChild(extendButtonsContainer);
overlay.appendChild(gifElement);
document.body.appendChild(overlay); // Добавляем оверлей на страницу
// Навешиваем обработчики на управление таймером (после добавления в 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;
if (overlayTimerIntervalId) clearInterval(overlayTimerIntervalId);
if (overlayTimerDuration > 0) overlayTimerIntervalId = setInterval(updateOverlayTimer, 1000);
else overlayTimerIntervalId = null;
updateOverlayTimer();
} else {
this.valueAsNumber = overlayTimerDuration;
}
});
overlay.querySelectorAll(`#${getElementId('overlay-timer-control')} button`).forEach(button => {
button.addEventListener("click", () => modifyTimerInputOverlay(parseInt(button.dataset.seconds, 10)));
});
// Запускаем таймер и обновляем данные
overlayCountdownRemaining = overlayTimerDuration;
if (overlayCountdownRemaining < 0) overlayCountdownRemaining = 0;
updateOverlayTimer();
updateOverlayRemainingMinutes();
if (overlayTimerDuration > 0 && !overlayTimerIntervalId) {
overlayTimerIntervalId = setInterval(updateOverlayTimer, 1000);
}
}
// --- КОНЕЦ РЕФАКТОРИНГА showOverlay ---
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}] Removing split overlay.`);
overlay.remove();
overlay = null;
if (overlayTimerIntervalId) { clearInterval(overlayTimerIntervalId); overlayTimerIntervalId = null; }
if (audioPlayer) { audioPlayer.pause(); }
}
}
function initAudioPlayer() {
if (!audioPlayer && splitSoundUrl && !splitSoundUrl.includes('ВАША_ПРЯМАЯ_ССЫЛКА')) {
GM_log(`[${currentPlatform}] Initializing audio player with URL: ${splitSoundUrl}`);
try {
audioPlayer = new Audio(splitSoundUrl);
audioPlayer.preload = 'auto';
audioPlayer.onerror = (e) => { GM_log(`[${currentPlatform}] ERROR loading audio: ${e.target.error.message}`); console.error("Universal Split: Ошибка загрузки звука:", e); audioPlayer = null; };
let savedVolume = localStorage.getItem(localStorageVolumeKey) ?? '0.5';
audioPlayer.volume = parseFloat(savedVolume);
} catch (error) { GM_log(`[${currentPlatform}] ERROR creating Audio object: ${error.message}`); console.error("Universal Split: Ошибка создания Audio:", error); audioPlayer = null; }
} else if (audioPlayer) {
let savedVolume = localStorage.getItem(localStorageVolumeKey) ?? '0.5';
try { audioPlayer.volume = parseFloat(savedVolume); } catch(e){}
}
}
// --- ФУНКЦИИ УПРАВЛЕНИЯ ПАНЕЛЬЮ ---
// --- РЕФАКТОРИНГ addControlPanel: Используем createElement ---
function addControlPanel() {
if (panelAdded || !platformConfig) return;
const insertionElement = document.querySelector(platformConfig.insertionSelector);
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';
// Кнопка Начать/Стоп
const setButton = document.createElement("button");
setButton.id = getElementId('set-split-button');
setButton.className = 'set-split-button';
setButton.textContent = "НАЧАТЬ СПЛИТ";
// Лейбл для сплита
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);
// Группа ввода сплита
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;
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);
});
// Группа управления громкостью
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) ?? '0.5';
volumeControlGroup.appendChild(volumeLabel);
volumeControlGroup.appendChild(volumeSlider);
// Статистика
const statsElement = document.createElement("span");
statsElement.id = getElementId('split-stats');
statsElement.className = 'split-stats';
statsElement.textContent = "Выкуплено: 0 / Всего: ? минут"; // Начальное значение
// Добавляем все элементы на панель
panelElement.appendChild(setButton);
panelElement.appendChild(splitLabel);
panelElement.appendChild(splitInputGroup);
panelElement.appendChild(volumeControlGroup);
panelElement.appendChild(statsElement);
// Вставка панели согласно конфигурации (после создания всех элементов)
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);
}
panelAdded = true;
// Навешиваем обработчики (после добавления в DOM, чтобы элементы были доступны)
setButton.addEventListener("click", () => { // Listener для кнопки Начать/Стоп
const inputVal = parseInt(splitInputField.value, 10); // Используем ссылку на элемент
if (!isNaN(inputVal) && inputVal >= 0) {
splitMinutes = inputVal;
if (splitMinutes > 0) {
startSplitCheckInterval();
setButton.textContent = "СПЛИТ НАЧАТ";
setButton.classList.add("active");
checkSplitCondition();
} else {
stopSplitCheckInterval();
splitTriggered = false;
removeOverlay();
setButton.textContent = "НАЧАТЬ СПЛИТ";
setButton.classList.remove("active");
}
updateSplitDisplay();
} else {
alert("Введите корректное число минут.");
}
});
splitInputGroup.querySelectorAll("button").forEach(button => { // Listeners для кнопок +/-
button.addEventListener("click", () => modifySplitInput(parseInt(button.dataset.minutes, 10)));
});
volumeSlider.addEventListener("input", function() { // Listener для громкости
const newVolume = parseFloat(this.value);
if (audioPlayer) audioPlayer.volume = newVolume;
localStorage.setItem(localStorageVolumeKey, newVolume.toString());
});
// Обновляем отображение панели
updateSplitDisplay();
// Если это VK, запускаем проверку видимости контролов
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.`);
}
}
GM_log(`[${currentPlatform}] Control panel added successfully.`);
}
// --- КОНЕЦ РЕФАКТОРИНГА addControlPanel ---
function ensurePanelPosition() {
if (!panelAdded || !panelElement || !platformConfig) return;
const insertionElement = document.querySelector(platformConfig.insertionSelector);
if (!insertionElement) {
GM_log(`[${currentPlatform}] Insertion element (${platformConfig.insertionSelector}) disappeared. Removing panel.`);
removeControlPanel();
return;
}
let currentPositionCorrect = false;
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);
}
if (!currentPositionCorrect) {
GM_log(`[${currentPlatform}] Panel position incorrect. Re-inserting...`);
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);
}
}
}
function removeControlPanel() {
if (panelElement) {
panelElement.remove();
panelElement = null;
}
panelAdded = false;
stopVisibilityCheckInterval();
controlsElement = null;
GM_log(`[${currentPlatform}] Control panel removed.`);
}
// --- СПЕЦИФИЧНЫЕ ФУНКЦИИ ДЛЯ VK (Видимость) ---
function checkControlsVisibility() {
if (!panelElement || !controlsElement || currentPlatform !== 'vk') return;
const isHiddenByStyle = controlsElement.style.display === 'none';
const isHiddenByClass = controlsElement.classList.contains('hidden');
const controlsAreVisible = !isHiddenByStyle && !isHiddenByClass;
if (controlsAreVisible && !panelElement.classList.contains('visible')) {
panelElement.classList.add('visible');
} else if (!controlsAreVisible && panelElement.classList.contains('visible')) {
panelElement.classList.remove('visible');
}
}
function startVisibilityCheckInterval() {
if (!visibilityCheckIntervalId && platformConfig?.needsVisibilityCheck) {
GM_log(`[${currentPlatform}] Starting controls visibility check interval.`);
visibilityCheckIntervalId = setInterval(checkControlsVisibility, 300);
checkControlsVisibility();
}
}
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';
}
// --- СБРОС СОСТОЯНИЯ ---
function resetState() {
const platformToLog = currentPlatform !== 'unknown' ? currentPlatform : 'Previous';
GM_log(`[${platformToLog}] Resetting state...`);
stopSplitCheckInterval();
stopVisibilityCheckInterval();
if (overlayTimerIntervalId) { clearInterval(overlayTimerIntervalId); overlayTimerIntervalId = null; }
removeOverlay();
removeControlPanel();
splitMinutes = null;
totalVideoMinutes = null;
video = null;
splitTriggered = false;
// Сброс platformConfig происходит в setupPlatformSplit при смене платформы
}
// --- ГЛАВНАЯ ФУНКЦИЯ НАСТРОЙКИ ---
function setupPlatformSplit() {
const detectedPlatform = getCurrentPlatform();
// --- Обработка смены платформы или ухода с поддерживаемой ---
if (detectedPlatform !== currentPlatform) {
if (currentPlatform !== 'unknown') { // Если была активная платформа
GM_log(`Platform changed from ${currentPlatform} to ${detectedPlatform} or unknown. Resetting...`);
resetState(); // Сбросить состояние СТАРОЙ платформы
}
currentPlatform = detectedPlatform; // Установить НОВУЮ платформу
if (currentPlatform !== 'unknown') {
platformConfig = platformConfigs[currentPlatform]; // Загрузить конфиг НОВОЙ
GM_log(`[${currentPlatform}] Initializing...`);
injectGlobalStyles(); // Внедрить стили для НОВОЙ
initAudioPlayer(); // Попробовать инициализировать звук для НОВОЙ
} else {
platformConfig = null; // Мы не на поддерживаемой платформе
return; // Выход, если платформа неизвестна
}
}
// --- Логика настройки для ТЕКУЩЕЙ (уже установленной) платформы ---
if (currentPlatform === 'unknown' || !platformConfig) return; // Двойная проверка
// Если панель уже есть, просто проверяем позицию и интервалы
if (panelAdded) {
ensurePanelPosition();
if (platformConfig.needsVisibilityCheck && !visibilityCheckIntervalId && controlsElement) {
startVisibilityCheckInterval();
}
if (splitMinutes !== null && splitMinutes > 0 && !splitCheckIntervalId) {
startSplitCheckInterval();
}
return; // Панель есть, выходим
}
// --- Если панели еще НЕТ, пытаемся ее создать ---
video = document.querySelector(platformConfig.videoSelector);
const insertionElement = document.querySelector(platformConfig.insertionSelector);
if (video && insertionElement) {
addControlPanel(); // Добавляем панель (внутри инициализируется громкость)
// Если панель успешно добавлена и есть сплит, запускаем проверку
if (panelAdded && splitMinutes !== null && splitMinutes > 0 && !splitCheckIntervalId) {
startSplitCheckInterval();
}
} else {
// GM_log(`[${currentPlatform}] Waiting for video (${!!video}) and insertion point (${!!insertionElement})`);
}
}
// --- ИНИЦИАЛИЗАЦИЯ И ОТСЛЕЖИВАНИЕ НАВИГАЦИИ ---
function initialize() {
GM_log("Universal Video Split: Initializing (v1.1)...");
lastUrl = location.href; // Запоминаем начальный URL
if (!setupIntervalId) {
setupIntervalId = setInterval(setupPlatformSplit, 750);
}
navigationObserver = new MutationObserver((mutations) => {
if (location.href !== lastUrl) {
GM_log(`URL changed from ${lastUrl} to ${location.href}`);
lastUrl = location.href;
// Перезапускаем настройку для новой страницы
// resetState() будет вызван внутри setupPlatformSplit при обнаружении смены платформы
setupPlatformSplit();
// Если интервал был остановлен, перезапускаем его
if (!setupIntervalId) {
setupIntervalId = setInterval(setupPlatformSplit, 750);
}
} else {
// URL не изменился, но DOM мог. setupPlatformSplit в интервале проверит.
}
});
navigationObserver.observe(document.body, { childList: true, subtree: true });
// Первая попытка настройки
setupPlatformSplit();
}
// --- Очистка при выходе ---
window.addEventListener('beforeunload', () => {
GM_log("Universal Video Split: Unloading, cleaning up...");
resetState();
if (setupIntervalId) { clearInterval(setupIntervalId); setupIntervalId = null; }
if (navigationObserver) { navigationObserver.disconnect(); navigationObserver = null; }
if (audioPlayer) { try { audioPlayer.pause(); } catch(e){} audioPlayer = null; }
});
// --- Запуск ---
initialize();
})();