Написан с нуля. Нативный IntersectionObserver. Счетчик по умолчанию 1. Безопасный вывод 3 видео ряда. Сетка не ломается.
// ==UserScript==
// @name YouTube Subscriptions Scroller
// @namespace http://tampermonkey.net
// @version 1.0
// @description Написан с нуля. Нативный IntersectionObserver. Счетчик по умолчанию 1. Безопасный вывод 3 видео ряда. Сетка не ломается.
// @author Wind173 with Google AI
// @match https://www.youtube.com/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
// Создаем абсолютно независимый и чистый каркас панели
const container = document.createElement('div');
container.id = 'yt-modern-counter-widget';
container.style.cssText = 'position: fixed !important; top: 70px !important; left: 260px !important; z-index: 2147483647 !important; display: flex !important; align-items: center !important; background-color: #1f1f1f !important; color: #ffffff !important; padding: 6px 12px !important; border-radius: 20px !important; box-shadow: 0 6px 20px rgba(0,0,0,0.8) !important; border: 1px solid #444 !important; font-family: Roboto, Arial, sans-serif !important; gap: 8px !important;';
// Инпут: значение по умолчанию строго 1
const input = document.createElement('input');
input.type = 'number';
input.value = '1';
input.min = '1';
input.style.cssText = 'width: 55px !important; padding: 5px 8px !important; border-radius: 12px !important; border: 1px solid #555 !important; background-color: #0f0f0f !important; color: #fff !important; text-align: center !important; font-size: 13px !important; outline: none !important;';
const btn = document.createElement('button');
btn.innerText = 'Go ⬇️';
btn.style.cssText = 'padding: 6px 14px !important; background-color: #ff0000 !important; color: #fff !important; border: none !important; border-radius: 12px !important; cursor: pointer !important; font-weight: bold !important; font-size: 13px !important;';
// Единое информационное поле, куда мы запишем три чистых номера текущего ряда
const positionBadge = document.createElement('div');
positionBadge.innerText = '№ 1, 2, 3';
positionBadge.style.cssText = 'padding: 5px 10px !important; border-radius: 12px !important; background-color: #0088ff !important; color: #fff !important; font-size: 13px !important; font-weight: bold !important; min-width: 95px !important; text-align: center !important;';
container.appendChild(input);
container.appendChild(btn);
container.appendChild(positionBadge);
let scrollInterval = null;
let observer = null;
let visibleVideos = new Map(); // Хранилище для видимых на экране роликов
// Чистая и быстрая фильтрация видео без Shorts
function getValidVideos() {
const items = document.querySelectorAll('ytd-rich-item-renderer, ytd-grid-video-renderer, ytd-video-renderer');
const videos = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
const isShorts = item.closest('ytd-rich-shelf-renderer') || item.closest('ytd-reel-shelf-renderer');
if (!isShorts) {
const link = item.querySelector('a#video-title-link, a#video-title, a[href*="/watch"]');
if (!link || !link.getAttribute('href').includes('/shorts/')) {
videos.push(item);
}
}
}
return videos;
}
// Современный и легкий трекер видимости элементов на экране
function initObserver() {
if (observer) observer.disconnect();
visibleVideos.clear();
observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
visibleVideos.set(entry.target, entry.boundingClientRect.top);
} else {
visibleVideos.delete(entry.target);
}
});
// Определяем, какие ролики сейчас находятся ближе всего к центру видимой зоны
const allCurrentVideos = getValidVideos();
if (allCurrentVideos.length === 0) return;
let earliestVisibleIdx = Infinity;
visibleVideos.forEach((top, element) => {
const idx = allCurrentVideos.indexOf(element);
if (idx !== -1 && idx < earliestVisibleIdx) {
earliestVisibleIdx = idx;
}
});
if (earliestVisibleIdx !== Infinity) {
let n1 = earliestVisibleIdx + 1;
let n2 = n1 + 1;
let n3 = n1 + 2;
// Записываем номера трех роликов текущего видимого ряда
positionBadge.innerText = '№ ' + n1 + ', ' + n2 + ', ' + n3;
}
}, {
threshold: 0.1 // Срабатывает, если видно хотя бы 10% карточки ролика
});
// Подключаем слежку ко всем существующим видео
getValidVideos().forEach(video => observer.observe(video));
}
// Автоматическое добавление новых подгружаемых роликов под наблюдение видеокарты
const mutationObserver = new MutationObserver(() => {
if (!window.location.href.includes('/feed/subscriptions')) return;
if (observer) {
getValidVideos().forEach(video => observer.observe(video));
}
});
// Функция плавной промотки ленты до цели
function startAutoScroll() {
const targetNum = parseInt(input.value, 10);
if (isNaN(targetNum) || targetNum <= 0) return;
let lastHeight = document.documentElement.scrollHeight;
let noChangeCount = 0;
clearInterval(scrollInterval);
input.disabled = true;
btn.style.backgroundColor = '#333';
scrollInterval = setInterval(() => {
const videos = getValidVideos();
btn.innerText = '⏳ ' + videos.length + '/' + targetNum;
// Если цель достигнута — останавливаемся и центрируем ролик на экране
if (videos.length >= targetNum) {
clearInterval(scrollInterval);
btn.innerText = 'Go ⬇️';
btn.style.backgroundColor = '#ff0000';
input.disabled = false;
if (videos[targetNum - 1]) {
videos[targetNum - 1].scrollIntoView({ behavior: 'smooth', block: 'center' });
}
return;
}
// Иначе — продолжаем листать вниз
window.scrollTo(0, document.documentElement.scrollHeight);
const app = document.querySelector('ytd-app');
if (app) app.scrollTop = app.scrollHeight;
let currentHeight = document.documentElement.scrollHeight;
if (currentHeight === lastHeight) {
noChangeCount++;
if (noChangeCount >= 15) { // Конец ленты подписок
clearInterval(scrollInterval);
btn.innerText = 'Go ⬇️';
btn.style.backgroundColor = '#ff0000';
input.disabled = false;
}
} else {
noChangeCount = 0;
lastHeight = currentHeight;
}
}, 250);
}
btn.onclick = function(e) {
e.preventDefault();
startAutoScroll();
};
// Контроль присутствия интерфейса при переходах по страницам YouTube
setInterval(() => {
const isSubscriptions = window.location.href.includes('/feed/subscriptions');
const exists = document.getElementById('yt-modern-counter-widget');
if (isSubscriptions && !exists) {
document.body.appendChild(container);
initObserver();
mutationObserver.observe(document.body, { childList: true, subtree: true });
} else if (!isSubscriptions && exists) {
clearInterval(scrollInterval);
if (observer) observer.disconnect();
mutationObserver.disconnect();
container.remove();
}
}, 1000);
})();