YouTube Channel Scroll Saver

Saves and restores scroll position on YouTube channel videos pages

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴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
    };
})();