YouTube Channel Scroll Saver

Saves and restores scroll position on YouTube channel videos pages

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name            YouTube Channel Scroll Saver
// @name:de         YouTube Channel Scroll Saver
// @namespace       https://www.youtube.com/
// @version         1.6.1
// @description     Saves and restores scroll position on YouTube channel videos pages
// @description:de  Speichert und stellt die Scrollposition auf der Video-Seite des YouTube-Kanals wieder her
// @author          Kamikaze (https://github.com/Kamiikaze)
// @supportURL      https://github.com/Kamiikaze/Tampermonkey/issues
// @icon            https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @match           https://www.youtube.com/@*/videos
// @match           https://www.youtube.com/*
// @require         https://greasyfork.org/scripts/455253-kamikaze-script-utils/code/Kamikaze'%20Script%20Utils.js
// @require         https://cdnjs.cloudflare.com/ajax/libs/toastify-js/1.12.0/toastify.min.js
// @resource        toastifyCss https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css
// @grant           GM_xmlhttpRequest
// @grant           GM_getResourceText
// @grant           GM_addStyle
// @license         MIT
// ==/UserScript==

// Autoscroll to last postion on this channel.
// Leave it false if you wan to click a button to manual scroll
const doAutoscroll = false

// Save scroll position at most every 3 second
const saveDelay = 3000



/* global Logger waitForElm notify */



const SCRIPT_NAME = "YT Scroll Saver"
const log = new Logger(SCRIPT_NAME, 4);

// Load remote JS
GM_xmlhttpRequest({
    method : "GET",
    // from other domain than the @match one (.org / .com):
    url : "https://cdnjs.cloudflare.com/ajax/libs/toastify-js/1.12.0/toastify.min.js",
    onload : (ev) =>
    {
        let e = document.createElement('script');
        e.innerText = ev.responseText;
        document.head.appendChild(e);
    }
});

// Load remote CSS
const extCss = GM_getResourceText("toastifyCss");
GM_addStyle(extCss);


(function() {
    // https://stackoverflow.com/questions/61964265/getting-error-this-document-requires-trustedhtml-assignment-in-chrome
    if (window.trustedTypes && window.trustedTypes.createPolicy && !window.trustedTypes.defaultPolicy) {
        window.trustedTypes.createPolicy('default', {
            createHTML: string => string
            // Optional, only needed for script (url) tags
            //,createScriptURL: string => string
            //,createScript: string => string,
        });
    }

    let isScrolling = false;
    let btnAdded = false;
    let saveTimeout = null;
    let lastUrl = location.href;

    function getChannelUsername() {
        const match = window.location.pathname.match(/@([^/]+)/);
        return match ? match[1] : null;
    }

    function saveScrollPosition() {
        const username = getChannelUsername();
        if (!username) return;

        const scrollPosition = window.scrollY;
        if ( scrollPosition > 800 ) {
            localStorage.setItem(`yt_scroll_${username}`, scrollPosition);
            notify(`[YT Scroll Saver] Saved position: ${scrollPosition}px for @${username}`, 3000)
            log.debug(`Saved position: ${scrollPosition}px for @${username}`);
        } else {
            log.debug(`Scroll pos is below 800 (${scrollPosition}). Don't save pos.`);
            return
        }
    }

    function loadScrollPosition() {
        const username = getChannelUsername();
        if (!username) {
            isScrolling = false
            return
        };

        const savedPosition = parseInt(localStorage.getItem(`yt_scroll_${username}`) || "0", 10);
        if (savedPosition <= 0) {
            isScrolling = false
            return
        };

        notify(`[YT Scroll Saver] Trying to restore position: ${savedPosition}px for @${username}`)
        log.debug(`[YT Scroll Saver] Trying to restore position: ${savedPosition}px for @${username}`);

        if (!btnAdded) createManualScrollBtn(savedPosition)
        if (doAutoscroll) scrollTo(savedPosition)
    }

    function scrollTo(pos) {
        let attempts = 0;
        const maxAttempts = 20; // 500ms * 20 = 10 seconds max

        const scrollInterval = setInterval(() => {
            if (window.scrollY >= pos || attempts >= maxAttempts) {
                clearInterval(scrollInterval);
                isScrolling = false
                notify(`[[YT Scroll Saver] Scroll position reached or max attempts hit. {Pos: ${window.scrollY}, Saved: ${pos}}`)
                log.debug(`[YT Scroll Saver] Scroll position reached or max attempts hit. {Pos: ${window.scrollY}, Saved: ${pos}}`);
                return;
            }
            notify(`[YT Scroll Saver] Scrolling.. `, 1000)
            log.debug(`[YT Scroll Saver] Scrolling.. `);
            window.scrollTo(0, pos);
            attempts++;
        }, 500);
    }

    async function createManualScrollBtn(pos) {
        const chipList = await waitForElm("iron-selector")
        const btn = document.createElement("button")

        btn.addEventListener( 'click', () => scrollTo(pos) )
        btn.innerText = `Scroll to: ${pos}`
        btn.style = `
        background-color: transparent;
        padding: 8px;
        border-radius: 10px;
        margin: 0 30px;
        color: #f1f1f1;
        border-color: rgba(255, 255, 255, 0.2);
        outline: none !important;
        cursor: pointer;
`

        chipList.append(btn)
        btnAdded = true
    }

    function checkUrlChange() {
        if (location.href !== lastUrl) {
            lastUrl = location.href;
            if (window.location.pathname.includes('/videos')) {
                log.debug("[YT Scroll Saver] Detected navigation to a channel's /videos page.");
                isScrolling = true
                setTimeout(loadScrollPosition, 1000);
            } else {
                btnAdded = false
            }
            log.debug("btnAdded",btnAdded)
        }
    }

    // Attach scroll event listener
    window.addEventListener('scroll', () => {
        if (!window.location.pathname.includes('/videos') || isScrolling) return;
        if (saveTimeout) clearTimeout(saveTimeout);
        saveTimeout = setTimeout(saveScrollPosition, saveDelay);
    });

    // Watch for SPA navigation changes
    const observer = new MutationObserver(checkUrlChange);
    observer.observe(document.body, { childList: true, subtree: true });

    // Restore scroll position after page loads
    if (window.location.pathname.includes('/videos')) {
        isScrolling = true
        notify(`[YT Scroll Saver] Loading scroll position.`)
        log.debug(`[YT Scroll Saver] Loading scroll position.`);
        setTimeout(loadScrollPosition, 1000); // Delay to allow initial content to load
    };
})();