YouTube Split

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

当前为 2025-04-18 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Split
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.9 // Повышаем версию
  5. // @author sosal
  6. // @match *://www.youtube.com/watch*
  7. // @grant none
  8. // @description Устанавливает сплит по времени (только после нажатия "НАЧАТЬ СПЛИТ"), выводит панель управления под видео с кнопками настройки и громкости, показывает оверлей "СПЛИТ НЕ ОПЛАЧЕН" с кнопками продления и проигрывает звук при достижении порога. Исправлены ошибки.
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. // --- Конфигурация ---
  15. let splitMinutes = null; // Значение сплита в минутах, по умолчанию null (сплит не активен)
  16. const extendCost = 300; // Стоимость продления +1 минуты в условных рублях
  17. // ВСТАВЬТЕ СЮДА ПРЯМУЮ ССЫЛКУ НА ВАШ ЗВУК!
  18. const splitSoundUrl = 'https://github.com/lardan099/donat/raw/refs/heads/main/alert_orig.mp3';
  19. const localStorageVolumeKey = 'ytSplitAlertVolume'; // Ключ для сохранения громкости в localStorage
  20.  
  21.  
  22. // --- Глобальные переменные состояния ---
  23. let video = null; // Элемент видео
  24. let overlay = null; // Элемент оверлея
  25. let splitTriggered = false; // Флаг, что сплит активирован (видео остановлено)
  26. let audioPlayer = null; // Для проигрывания звука
  27. let splitCheckIntervalId = null; // ID интервала проверки сплита (для логики сплита)
  28. let setupIntervalId = null; // ID интервала для поиска элементов и добавления панели
  29. let panelAdded = false; // Флаг, чтобы добавлять панель только один раз
  30.  
  31. // --- CSS Стили для улучшения внешнего вида ---
  32. const styles = `
  33. /* Стили для панели управления */
  34. #split-control-panel {
  35. margin-top: 10px;
  36. margin-bottom: 15px;
  37. padding: 10px 15px;
  38. background: var(--yt-spec-badge-chip-background);
  39. border: 1px solid var(--yt-spec-border-div);
  40. border-radius: 8px;
  41. display: flex;
  42. flex-wrap: wrap;
  43. align-items: center;
  44. gap: 10px 20px; /* Увеличим горизонтальный разрыв */
  45. color: var(--yt-spec-text-primary);
  46. font-family: "Roboto", Arial, sans-serif;
  47. font-size: 14px;
  48. max-width: var(--ytd-watch-flexy-width);
  49. width: 100%;
  50. box-sizing: border-box;
  51. }
  52.  
  53. ytd-watch-flexy:not([use- Sarkis]) #primary #split-control-panel {
  54. margin-left: auto;
  55. margin-right: auto;
  56. }
  57.  
  58. #split-control-panel label {
  59. font-weight: 500;
  60. color: var(--yt-spec-text-secondary);
  61. flex-shrink: 0;
  62. line-height: 1.3;
  63. }
  64.  
  65. #split-control-panel label i {
  66. font-style: normal;
  67. font-size: 12px;
  68. color: var(--yt-spec-text-disabled);
  69. }
  70.  
  71. /* Контейнер для поля ввода и кнопок модификации */
  72. #split-input-group {
  73. display: flex;
  74. align-items: center;
  75. gap: 5px;
  76. }
  77.  
  78. #split-control-panel input[type="number"] {
  79. width: 60px;
  80. padding: 8px 10px;
  81. background: var(--yt-spec-filled-button-background);
  82. color: var(--yt-spec-text-primary);
  83. border: 1px solid var(--yt-spec-action-simulate-border);
  84. border-radius: 4px;
  85. text-align: center;
  86. font-size: 15px;
  87. -moz-appearance: textfield;
  88. }
  89.  
  90. #split-control-panel input[type="number"]::-webkit-outer-spin-button,
  91. #split-control-panel input[type="number"]::-webkit-inner-spin-button {
  92. -webkit-appearance: none;
  93. margin: 0;
  94. }
  95.  
  96. /* Стили для всех кнопок внутри панели */
  97. #split-control-panel button {
  98. padding: 8px 15px;
  99. font-size: 15px;
  100. cursor: pointer;
  101. background: var(--yt-spec-grey-1);
  102. color: var(--yt-spec-text-primary);
  103. border: none;
  104. border-radius: 4px;
  105. transition: background 0.2s ease-in-out;
  106. font-weight: 500;
  107. flex-shrink: 0;
  108. }
  109.  
  110. #split-control-panel button:hover {
  111. background: var(--yt-spec-grey-2);
  112. }
  113.  
  114. /* Стили для кнопок модификации сплита (+1, +5 и т.д.) */
  115. #split-input-group button {
  116. padding: 8px 10px;
  117. font-size: 14px;
  118. background: var(--yt-spec-filled-button-background);
  119. border: 1px solid var(--yt-spec-action-simulate-border);
  120. }
  121. #split-input-group button:hover {
  122. background: var(--yt-spec-grey-2);
  123. }
  124.  
  125. #split-control-panel button#set-split-button {
  126. background: var(--yt-spec-brand-suggested-action);
  127. color: var(--yt-spec-text-reverse);
  128. order: -1;
  129. margin-right: auto;
  130. }
  131. #split-control-panel button#set-split-button:hover {
  132. background: var(--yt-spec-brand-suggested-action-hover);
  133. }
  134.  
  135. /* Стили для регулятора громкости */
  136. #split-volume-control {
  137. display: flex;
  138. align-items: center;
  139. gap: 5px;
  140. }
  141. #split-volume-control label {
  142. font-weight: 500;
  143. color: var(--yt-spec-text-secondary);
  144. flex-shrink: 0;
  145. line-height: normal; /* Сброс line-height для этой метки */
  146. }
  147. #split-volume-control input[type="range"] {
  148. flex-grow: 1; /* Занимает доступное место */
  149. min-width: 80px; /* Минимальная ширина слайдера */
  150. -webkit-appearance: none; /* Удаляем стандартный стиль Chrome/Safari */
  151. appearance: none;
  152. height: 8px; /* Высота трека */
  153. background: var(--yt-spec-grey-1);
  154. outline: none;
  155. opacity: 0.7;
  156. transition: opacity .2s;
  157. border-radius: 4px;
  158. }
  159. #split-volume-control input[type="range"]:hover {
  160. opacity: 1;
  161. }
  162.  
  163. /* Стили для ползунка (Chrome/Safari) */
  164. #split-volume-control input[type="range"]::-webkit-slider-thumb {
  165. -webkit-appearance: none;
  166. appearance: none;
  167. width: 15px; /* Ширина ползунка */
  168. height: 15px; /* Высота ползунка */
  169. background: var(--yt-spec-brand-button-background); /* Цвет ползунка */
  170. cursor: pointer;
  171. border-radius: 50%; /* Круглый ползунок */
  172. }
  173.  
  174. /* Стили для ползунка (Firefox) */
  175. #split-volume-control input[type="range"]::-moz-range-thumb {
  176. width: 15px;
  177. height: 15px;
  178. background: var(--yt-spec-brand-button-background);
  179. cursor: pointer;
  180. border-radius: 50%;
  181. }
  182.  
  183.  
  184. /* Стили для оверлея */
  185. #split-overlay {
  186. position: fixed;
  187. top: 0;
  188. left: 0;
  189. width: 100%;
  190. height: 100%;
  191. background: rgba(0, 0, 0, 0.95);
  192. color: white;
  193. display: flex;
  194. flex-direction: column;
  195. justify-content: center;
  196. align-items: center;
  197. z-index: 99999;
  198. font-family: "Roboto", Arial, sans-serif;
  199. text-align: center;
  200. padding: 20px;
  201. box-sizing: border-box;
  202. }
  203.  
  204. #split-overlay #split-warning-message {
  205. font-size: clamp(24px, 4vw, 36px);
  206. margin-bottom: 15px;
  207. color: yellow;
  208. font-weight: bold;
  209. text-shadow: 0 0 8px rgba(255, 255, 0, 0.5);
  210. }
  211.  
  212. #split-overlay #split-main-message {
  213. font-size: clamp(40px, 8vw, 72px);
  214. font-weight: bold;
  215. margin-bottom: 40px;
  216. color: red;
  217. text-shadow: 0 0 15px rgba(255, 0, 0, 0.7);
  218. }
  219.  
  220. #split-extend-buttons {
  221. display: flex;
  222. gap: 15px;
  223. flex-wrap: wrap;
  224. justify-content: center;
  225. }
  226.  
  227. #split-extend-buttons button {
  228. padding: 12px 25px;
  229. font-size: clamp(18px, 3vw, 24px);
  230. cursor: pointer;
  231. background: var(--yt-spec-red-500);
  232. border: none;
  233. color: white;
  234. border-radius: 4px;
  235. font-weight: bold;
  236. transition: background 0.2s ease-in-out;
  237. }
  238.  
  239. #split-extend-buttons button:hover {
  240. background: var(--yt-spec-red-600);
  241. }
  242. `;
  243.  
  244. // --- Вспомогательная функция для внедрения CSS ---
  245. function injectStyles() {
  246. if (document.getElementById('yt-split-styles')) {
  247. return;
  248. }
  249. const styleElement = document.createElement("style");
  250. styleElement.id = 'yt-split-styles';
  251. styleElement.textContent = styles;
  252. document.head.appendChild(styleElement);
  253. console.log("YouTube Split: Стили внедрены.");
  254. }
  255.  
  256. // Обновление отображения значения сплита в спинере
  257. // Эта функция теперь вызывается только при добавлении панели и при изменении global splitMinutes
  258. function updateSplitDisplay() {
  259. const inputField = document.getElementById("split-input");
  260. if (inputField) {
  261. inputField.valueAsNumber = splitMinutes === null ? 0 : splitMinutes;
  262. }
  263. }
  264.  
  265. // Функция для изменения значения в поле ввода сплита
  266. // Эта функция вызывается кнопками +/- и меняет ТОЛЬКО ПОЛЕ ВВОДА
  267. function modifySplitInput(minutesToModify) {
  268. const inputField = document.getElementById("split-input");
  269. if (!inputField) return;
  270.  
  271. let currentVal = inputField.valueAsNumber;
  272.  
  273. if (isNaN(currentVal)) {
  274. currentVal = 0;
  275. }
  276.  
  277. let newVal = currentVal + minutesToModify;
  278.  
  279. if (newVal < 0) {
  280. newVal = 0;
  281. }
  282.  
  283. inputField.valueAsNumber = newVal;
  284. }
  285.  
  286. // Функция для запуска интервала проверки сплита
  287. function startSplitCheckInterval() {
  288. if (!splitCheckIntervalId) {
  289. splitCheckIntervalId = setInterval(checkSplitCondition, 500);
  290. console.log("YouTube Split: Запущена проверка сплита (interval).");
  291. } else {
  292. // console.log("YouTube Split: Проверка сплита уже запущена.");
  293. }
  294. }
  295.  
  296. // Функция для остановки интервала проверки сплита
  297. function stopSplitCheckInterval() {
  298. if (splitCheckIntervalId) {
  299. clearInterval(splitCheckIntervalId);
  300. splitCheckIntervalId = null;
  301. console.log("YouTube Split: Проверка сплита остановлена.");
  302. }
  303. }
  304.  
  305. // Создание панели управления сплитом и вставка ее под видео
  306. // Эта функция вызывается, когда нужный контейнер уже найден и panelAdded === false
  307. function addControlPanel(primaryContainer) {
  308. if (panelAdded) { // Дополнительная проверка, хотя setupElementsAndPanel тоже проверяет
  309. ensurePanelPosition(); // Убедимся в позиции существующей панели
  310. updateSplitDisplay(); // Обновим поле ввода на существующей панели
  311. return;
  312. }
  313.  
  314. if (!primaryContainer) {
  315. console.error("YouTube Split: addControlPanel вызвана без primaryContainer!");
  316. return;
  317. }
  318.  
  319. const panel = document.createElement("div");
  320. panel.id = "split-control-panel";
  321.  
  322. // --- Кнопка "НАЧАТЬ СПЛИТ" ---
  323. const setButton = document.createElement("button");
  324. setButton.id = "set-split-button";
  325. setButton.textContent = "НАЧАТЬ СПЛИТ";
  326. setButton.addEventListener("click", function() {
  327. const inputField = document.getElementById("split-input");
  328. const inputVal = inputField.valueAsNumber;
  329.  
  330. if (!isNaN(inputVal) && inputVal >= 0) {
  331. const oldSplitMinutes = splitMinutes; // Сохраняем для логики продолжения видео
  332. splitMinutes = inputVal; // Устанавливаем активный сплит из поля ввода
  333. // updateSplitDisplay(); // Не вызываем здесь, чтобы не мешать modifySplitInput
  334.  
  335. if (splitMinutes > 0) {
  336. startSplitCheckInterval(); // Запускаем проверку если сплит > 0
  337.  
  338. // Проверяем условие сразу после установки, только если видео найдено
  339. if (video) {
  340. const thresholdSeconds = splitMinutes * 60;
  341. if (video.currentTime >= thresholdSeconds) {
  342. video.pause();
  343. splitTriggered = true;
  344. showOverlay();
  345. if(splitSoundUrl && audioPlayer && audioPlayer.src !== 'ВАША_ПРЯМАЯ_ССЫЛКА_НА_ЗВУКОВОЙ_ФАЙЛ_ТУТ'){
  346. audioPlayer.pause();
  347. audioPlayer.currentTime = 0;
  348. audioPlayer.play().catch(e => console.error("YouTube Split: Ошибка при воспроизведении звука:", e));
  349. }
  350. } else {
  351. splitTriggered = false;
  352. removeOverlay();
  353. // Если видео было на паузе *до* нажатия "НАЧАТЬ СПЛИТ" (т.е. сплит был null), и новое время сплита не достигнуто, запускаем
  354. if (video.paused && oldSplitMinutes === null) {
  355. video.play();
  356. }
  357. }
  358. }
  359.  
  360. } else { // splitMinutes === 0
  361. stopSplitCheckInterval();
  362. splitTriggered = false;
  363. removeOverlay();
  364. if (video && video.paused && oldSplitMinutes !== null && oldSplitMinutes > 0) {
  365. video.play();
  366. }
  367. }
  368. // Убеждаемся, что поле ввода показывает актуальное *активное* значение после нажатия
  369. updateSplitDisplay();
  370.  
  371.  
  372. } else {
  373. alert("Введите корректное число минут.");
  374. }
  375. });
  376.  
  377. // --- Метка "Сплит (мин):" (Исправление TrustedHTML) ---
  378. const label = document.createElement("label");
  379. label.setAttribute("for", "split-input");
  380.  
  381. const labelTextMain = document.createTextNode("Сплит (мин):");
  382. const breakElement = document.createElement("br");
  383. const italicElement = document.createElement("i");
  384. const labelTextInstruction = document.createTextNode("(уст. перед \"Начать\")");
  385.  
  386. label.appendChild(labelTextMain);
  387. label.appendChild(breakElement);
  388. italicElement.appendChild(labelTextInstruction);
  389. label.appendChild(italicElement);
  390. // --- Конец Исправления TrustedHTML ---
  391.  
  392.  
  393. // --- Группа ввода сплита и +/- кнопок ---
  394. const inputGroup = document.createElement("div");
  395. inputGroup.id = "split-input-group";
  396.  
  397. const inputField = document.createElement("input");
  398. inputField.type = "number";
  399. inputField.id = "split-input";
  400. inputField.min = "0";
  401. // Начальное значение будет установлено функцией updateSplitDisplay при добавлении
  402. inputField.valueAsNumber = splitMinutes === null ? 0 : splitMinutes;
  403.  
  404. const modifyButtons = [
  405. { text: '-10', minutes: -10 },
  406. { text: '-5', minutes: -5 },
  407. { text: '-1', minutes: -1 },
  408. { text: '+1', minutes: 1 },
  409. { text: '+5', minutes: 5 },
  410. { text: '+10', minutes: 10 },
  411. { text: '+20', minutes: 20 }
  412. ];
  413.  
  414. modifyButtons.forEach(btnInfo => {
  415. const button = document.createElement("button");
  416. button.textContent = btnInfo.text;
  417. button.addEventListener("click", () => modifySplitInput(btnInfo.minutes));
  418. inputGroup.appendChild(button);
  419. });
  420.  
  421. inputGroup.insertBefore(inputField, inputGroup.children[0]);
  422.  
  423.  
  424. // --- Регулятор громкости алерта ---
  425. const volumeControlGroup = document.createElement("div");
  426. volumeControlGroup.id = "split-volume-control";
  427.  
  428. const volumeLabel = document.createElement("label");
  429. volumeLabel.setAttribute("for", "split-volume-slider");
  430. volumeLabel.textContent = "Громкость алерта:";
  431.  
  432. const volumeSlider = document.createElement("input");
  433. volumeSlider.type = "range";
  434. volumeSlider.id = "split-volume-slider";
  435. volumeSlider.min = "0";
  436. volumeSlider.max = "1"; // HTMLMediaElement volume is typically 0 to 1
  437. volumeSlider.step = "0.05"; // Adjust step for finer control
  438.  
  439. // Установка начального значения громкости из localStorage (или дефолт)
  440. let savedVolume = localStorage.getItem(localStorageVolumeKey);
  441. if (savedVolume === null) {
  442. savedVolume = '0.5'; // Громкость по умолчанию 50%
  443. }
  444. volumeSlider.value = savedVolume;
  445.  
  446. // Обработчик изменения громкости
  447. volumeSlider.addEventListener("input", function() {
  448. // Применяем громкость к аудиоплееру, если он есть
  449. if (audioPlayer) {
  450. audioPlayer.volume = this.value;
  451. }
  452. // Сохраняем значение в localStorage
  453. localStorage.setItem(localStorageVolumeKey, this.value);
  454. });
  455.  
  456. volumeControlGroup.appendChild(volumeLabel);
  457. volumeControlGroup.appendChild(volumeSlider);
  458.  
  459. // Применяем громкость к аудиоплееру сразу после создания панели,
  460. // т.к. audioPlayer мог быть создан до панели.
  461. if (audioPlayer) {
  462. audioPlayer.volume = savedVolume;
  463. }
  464.  
  465.  
  466. // --- Собираем панель ---
  467. panel.appendChild(setButton);
  468. panel.appendChild(label);
  469. panel.appendChild(inputGroup);
  470. panel.appendChild(volumeControlGroup); // Добавляем регулятор громкости
  471.  
  472.  
  473. // Вставляем панель в найденный контейнер (#primary) как первый дочерний элемент
  474. primaryContainer.insertBefore(panel, primaryContainer.firstChild);
  475. panelAdded = true; // Устанавливаем флаг, что панель добавлена
  476. console.log("YouTube Split: Панель управления добавлена под видео.");
  477.  
  478. // Убедимся, что поле ввода показывает актуальное значение после добавления
  479. updateSplitDisplay();
  480. }
  481.  
  482. // Функция для проверки и корректировки позиции панели
  483. // Вызывается из setupElementsAndPanel
  484. function ensurePanelPosition() {
  485. if (!panelAdded) return; // Проверяем позицию только если панель добавлена
  486.  
  487. const panel = document.getElementById("split-control-panel");
  488. const primaryContainer = document.querySelector("ytd-watch-flexy #primary");
  489.  
  490. if (panel && primaryContainer) {
  491. if (primaryContainer.firstChild !== panel) {
  492. console.log("YouTube Split: Панель сместилась, перемещаем обратно.");
  493. primaryContainer.insertBefore(panel, primaryContainer.firstChild);
  494. }
  495. }
  496. // Если панель есть, но контейнера нет, setupElementsAndPanel ее удалит.
  497. }
  498.  
  499.  
  500. function addMinutesToActiveSplit(minutesToAdd) {
  501. if (splitMinutes === null) return;
  502.  
  503. splitMinutes += minutesToAdd;
  504. updateSplitDisplay(); // Обновляем поле ввода на панели
  505.  
  506. const thresholdSeconds = splitMinutes * 60;
  507.  
  508. if (video && video.currentTime < thresholdSeconds) {
  509. removeOverlay();
  510. splitTriggered = false;
  511. video.play();
  512. }
  513. // Оверлей остается, если продления недостаточно
  514. }
  515.  
  516. function checkSplitCondition() {
  517. // Ищем видео каждый раз, на случай его пересоздания YouTube
  518. if (!video) {
  519. video = document.querySelector("video");
  520. if (!video) {
  521. console.log("YouTube Split: Видеоэлемент не найден в checkSplitCondition, останавливаем проверку сплита.");
  522. stopSplitCheckInterval();
  523. splitTriggered = false;
  524. removeOverlay();
  525. // Панель и аудио плеер будут обработаны основным setupIntervalId или urlObserver
  526. return;
  527. }
  528. // Убеждаемся, что аудио плеер есть и громкость применена
  529. initAudioPlayer();
  530. const volumeSlider = document.getElementById('split-volume-slider');
  531. if(audioPlayer && volumeSlider) audioPlayer.volume = volumeSlider.value;
  532. }
  533.  
  534. // Проверяем условие сплита только если splitMinutes активно (не null и > 0)
  535. if (splitMinutes !== null && splitMinutes > 0) {
  536. const thresholdSeconds = splitMinutes * 60;
  537.  
  538. if (video.currentTime >= thresholdSeconds && !splitTriggered) {
  539. video.pause();
  540. splitTriggered = true;
  541. showOverlay();
  542. // Воспроизводим звук при первом триггере
  543. if(splitSoundUrl && audioPlayer && audioPlayer.src !== 'ВАША_ПРЯМАЯ_ССЫЛКА_НА_ЗВУКОВОЙ_ФАЙЛ_ТУТ'){
  544. audioPlayer.pause();
  545. audioPlayer.currentTime = 0;
  546. audioPlayer.play().catch(e => console.error("YouTube Split: Ошибка при воспроизведении звука:", e));
  547. }
  548. }
  549. if (splitTriggered && video.currentTime < thresholdSeconds) {
  550. removeOverlay();
  551. splitTriggered = false;
  552. video.play();
  553. }
  554. } else {
  555. // Если splitMinutes стал null или 0 во время работы проверки
  556. stopSplitCheckInterval();
  557. splitTriggered = false;
  558. removeOverlay();
  559. if (video && video.paused) video.play();
  560. }
  561. // ensurePanelPosition() убрана отсюда
  562. }
  563.  
  564. function showOverlay() {
  565. if (overlay) return;
  566.  
  567. overlay = document.createElement("div");
  568. overlay.id = "split-overlay";
  569.  
  570. const warningMessage = document.createElement("div");
  571. warningMessage.id = "split-warning-message";
  572. warningMessage.textContent = "⚠️ НУЖНО ДОНАТНОЕ ТОПЛИВО ⚠️";
  573.  
  574. const splitMessage = document.createElement("div");
  575. splitMessage.id = "split-main-message";
  576. splitMessage.textContent = "СПЛИТ НЕ ОПЛАЧЕН";
  577.  
  578. const extendButtonsContainer = document.createElement("div");
  579. extendButtonsContainer.id = "split-extend-buttons";
  580.  
  581. const extendButtonConfigs = [
  582. { minutes: 1, cost: extendCost },
  583. { minutes: 5, cost: extendCost * 5 },
  584. { minutes: 10, cost: extendCost * 10 },
  585. { minutes: 20, cost: extendCost * 20 }
  586. ];
  587.  
  588. extendButtonConfigs.forEach(config => {
  589. const button = document.createElement("button");
  590. button.textContent = `+ ${config.minutes} минут${getMinuteEnding(config.minutes)} - ${config.cost} рублей`;
  591. button.addEventListener("click", function() {
  592. addMinutesToActiveSplit(config.minutes);
  593. });
  594. extendButtonsContainer.appendChild(button);
  595. });
  596.  
  597. overlay.appendChild(warningMessage);
  598. overlay.appendChild(splitMessage);
  599. overlay.appendChild(extendButtonsContainer);
  600.  
  601. document.body.appendChild(overlay);
  602. console.log("YouTube Split: Оверлей показан.");
  603. }
  604.  
  605. function getMinuteEnding(count) {
  606. const lastDigit = count % 10;
  607. const lastTwoDigits = count % 100;
  608.  
  609. if (lastTwoDigits >= 11 && lastTwoDigits <= 14) {
  610. return ''; // минут
  611. }
  612. if (lastDigit === 1) {
  613. return 'а'; // минута
  614. }
  615. if (lastDigit >= 2 && lastDigit <= 4) {
  616. return 'ы'; // минуты
  617. }
  618. return ''; // минут
  619. }
  620.  
  621. function removeOverlay() {
  622. if (overlay) {
  623. overlay.remove();
  624. overlay = null;
  625. if (audioPlayer) {
  626. audioPlayer.pause();
  627. audioPlayer.currentTime = 0;
  628. }
  629. console.log("YouTube Split: Оверлей убран.");
  630. }
  631. }
  632.  
  633. // Инициализация аудио плеера и установка громкости
  634. function initAudioPlayer() {
  635. if (splitSoundUrl && splitSoundUrl !== 'ВАША_ПРЯМАЯ_ССЫЛКА_НА_ЗВУКОВОЙ_ФАЙЛ_ТУТ') {
  636. if (!audioPlayer || audioPlayer.src !== splitSoundUrl) {
  637. if (audioPlayer) {
  638. audioPlayer.pause();
  639. audioPlayer = null;
  640. }
  641. audioPlayer = new Audio(splitSoundUrl);
  642. audioPlayer.preload = 'auto';
  643. audioPlayer.onerror = (e) => console.error("YouTube Split: Не удалось загрузить или воспроизвести звук:", e);
  644. console.log("YouTube Split: Аудио плеер инициализирован.");
  645.  
  646. // Применяем сохраненную громкость после создания плеера
  647. let savedVolume = localStorage.getItem(localStorageVolumeKey);
  648. if (savedVolume !== null) {
  649. audioPlayer.volume = parseFloat(savedVolume);
  650. } else {
  651. audioPlayer.volume = 0.5; // Громкость по умолчанию
  652. }
  653. }
  654. } else {
  655. console.warn("YouTube Split: URL звука для сплита не указан или является плейсхолдером.");
  656. if (audioPlayer) {
  657. audioPlayer.pause();
  658. audioPlayer = null;
  659. }
  660. }
  661. }
  662.  
  663. // --- Главная функция, вызываемая по интервалу для поиска элементов и настройки ---
  664. function setupElementsAndPanel() {
  665. // Внедряем стили и инициализируем аудио плеер при каждой попытке,
  666. // функции сами проверят, нужно ли это делать.
  667. injectStyles();
  668. initAudioPlayer();
  669.  
  670. // Пытаемся найти видео и контейнер для панели
  671. video = document.querySelector("video"); // Обновляем ссылку на видео
  672. const primaryContainer = document.querySelector("ytd-watch-flexy #primary");
  673. const panel = document.getElementById("split-control-panel");
  674.  
  675.  
  676. if (video && primaryContainer) {
  677. // Элементы найдены
  678. if (!panelAdded) { // Проверяем наш флаг
  679. // Если панель еще не добавлена
  680. console.log("YouTube Split: Видео и контейнер #primary найдены. Добавляем панель.");
  681. addControlPanel(primaryContainer); // Передаем найденный контейнер
  682. } else {
  683. // Если панель уже добавлена, убедимся, что она на своем месте
  684. ensurePanelPosition();
  685. // updateSplitDisplay() НЕ вызывается здесь, чтобы не сбрасывать поле ввода
  686. }
  687.  
  688. } else {
  689. // Элементы еще не найдены
  690. // Если панель была добавлена, но контейнер исчез (например, навигация),
  691. // удалим панель и сбросим флаг.
  692. if (panelAdded) {
  693. console.log("YouTube Split: Необходимые элементы отсутствуют, удаляем панель и сбрасываем состояние.");
  694. const existingPanel = document.getElementById("split-control-panel");
  695. if(existingPanel) existingPanel.remove();
  696. panelAdded = false; // Сбрасываем флаг
  697. // Не сбрасываем splitMinutes на null здесь
  698. // splitCheckIntervalId и overlay очищаются в urlObserver или checkSplitCondition
  699. }
  700. video = null; // Сбрасываем видео, если оно пропало
  701. }
  702. }
  703.  
  704. // --- Запуск скрипта ---
  705.  
  706. // Запускаем интервал для поиска элементов и настройки.
  707. // Этот интервал будет работать постоянно на страницах *://www.youtube.com/watch*
  708. // и будет пытаться добавить панель и найти видео/контейнер каждые 500мс.
  709. if (!setupIntervalId) {
  710. setupIntervalId = setInterval(setupElementsAndPanel, 500); // Проверяем каждые 500мс
  711. console.log("YouTube Split: Запущен основной интервал поиска элементов.");
  712. }
  713.  
  714.  
  715. // Очистка при уходе со страницы или навигации по SPA
  716. let lastUrl = location.href;
  717. const urlObserver = new MutationObserver(() => {
  718. if (location.href !== lastUrl) {
  719. lastUrl = location.href;
  720. console.log("YouTube Split: URL changed, cleaning up and re-initializing.");
  721.  
  722. // Очищаем предыдущие интервалы
  723. stopSplitCheckInterval(); // Останавливаем проверку сплита
  724. if (setupIntervalId) { // Останавливаем основной интервал поиска элементов
  725. clearInterval(setupIntervalId);
  726. setupIntervalId = null;
  727. }
  728.  
  729. // Останавливаем звук и убираем оверлей
  730. if (audioPlayer) {
  731. audioPlayer.pause();
  732. }
  733. removeOverlay();
  734.  
  735. // Удаляем старую панель, если она есть
  736. const oldPanel = document.getElementById("split-control-panel");
  737. if (oldPanel) {
  738. oldPanel.remove();
  739. }
  740. // Удаляем старые стили, чтобы обновились, если нужно
  741. const oldStyles = document.getElementById("yt-split-styles");
  742. if(oldStyles) oldStyles.remove();
  743.  
  744. // Сбрасываем глобальные переменные состояния
  745. splitMinutes = null; // Сплит не активен по умолчанию на новой странице
  746. video = null; // Видео будет найдено заново в setupElementsAndPanel
  747. splitTriggered = false;
  748. panelAdded = false; // Сбрасываем флаг добавления панели
  749.  
  750. // Перезапускаем основной интервал для нового видео
  751. if (!setupIntervalId) {
  752. setupIntervalId = setInterval(setupElementsAndPanel, 500);
  753. console.log("YouTube Split: Перезапущен основной интервал поиска элементов после смены URL.");
  754. }
  755. // initAudioPlayer будет вызван в setupElementsAndPanel
  756. }
  757. });
  758.  
  759. // Наблюдаем за изменениями в body для отслеживания SPA навигации.
  760. urlObserver.observe(document.body, {
  761. childList: true,
  762. subtree: true
  763. });
  764.  
  765.  
  766. // Очистка при закрытии вкладки
  767. window.addEventListener('beforeunload', function() {
  768. console.log("YouTube Split: Очистка при выгрузке страницы.");
  769. stopSplitCheckInterval();
  770. if (setupIntervalId) {
  771. clearInterval(setupIntervalId);
  772. setupIntervalId = null;
  773. }
  774. if (audioPlayer) {
  775. audioPlayer.pause();
  776. audioPlayer = null; // Удаляем объект Audio при окончательной выгрузке
  777. }
  778. if (urlObserver) {
  779. urlObserver.disconnect();
  780. }
  781. });
  782.  
  783.  
  784. })();