YouTube Channel Scroll Saver

Saves and restores scroll position on YouTube channel videos pages

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==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
    };
})();