Youtube - Floating Related Videos Pane

Scroll related videos while still watching videos.

当前为 2023-02-10 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Youtube - Floating Related Videos Pane
// @namespace    Nomicwave
// @version      1.0.1
// @description  Scroll related videos while still watching videos.
// @author       Nomicwave
// @license      MIT License <https://opensource.org/licenses/MIT>
// @match        https://www.youtube.com/watch?*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @run-at       document-end
// ==/UserScript==

const dom = {
    playerContainer: null,
    playerControls: null,
    theaterContainer: null,
    toggle: null
}

const floatState = {
    disabled: 'disabled',
    normal: 'normal',
    always: 'always',
}

const floatClass = {
    enabled: 'frvp-enabled',
    theater: 'frvp-theater-mode',
}

function loadFloatStyle() {
    const cssText = `
    .frvp-enabled #secondary {
        position: sticky !important;
        top: 56px;
        height: calc(100vh - 56px);
    }

    .frvp-enabled ytd-item-section-renderer.style-scope.ytd-watch-next-secondary-results-renderer {
        box-shadow: inset 0 10px 15px -10px rgb(0 0 0 / 40%);
        overflow: hidden;
        overflow-y: overlay;
        overscroll-behavior-y: contain;
        height: calc(100vh - 56px - var(--ytd-margin-6x) - 51px);
        margin: 0 calc(var(--ytd-margin-6x) * -1) 0 calc(var(--ytd-margin-6x) * -1);
        padding: 0 var(--ytd-margin-6x) 0 var(--ytd-margin-6x);
    }

    .frvp-enabled.frvp-theater-mode ytd-item-section-renderer.style-scope.ytd-watch-next-secondary-results-renderer {
        height: calc(100vh - 56px - 51px);
    }

    .frvp-enabled ytd-item-section-renderer.style-scope.ytd-watch-next-secondary-results-renderer::-webkit-scrollbar {
        width: 10px;
    }

    .frvp-enabled ytd-item-section-renderer.style-scope.ytd-watch-next-secondary-results-renderer::-webkit-scrollbar-thumb {
        background: var(--yt-spec-badge-chip-background);
        background-repeat: no-repeat;
        border-radius: 10px;
    }

    .frvp-enabled ytd-item-section-renderer.style-scope.ytd-watch-next-secondary-results-renderer::-webkit-scrollbar-track {
        background: transparent;
    }`

    let style = document.createElement('style');
    style.type = 'text/css';
    if (style.styleSheet) {
        style.styleSheet.cssText = cssText;
    } else {
        style.appendChild(document.createTextNode(cssText));
    }

    document.getElementsByTagName('head')[0].appendChild(style);
};

function createToggleButton() {
    let div = document.createElement('div');
    div.innerHTML = (`
    <div id="float-state-toggle" class="style-scope ytd-menu-renderer" button-renderer="" style="margin-left: 8px;">
		<div>
			<button class="yt-spec-button-shape-next yt-spec-button-shape-next--tonal yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-leading " aria-label="Share" style="">
				<div class="yt-spec-button-shape-next__icon" aria-hidden="true" style="margin-left: 0; margin-right: 0;">
					<div style="width: 24px; height: 24px;">
						<svg viewBox="0 0 20 20" preserveAspectRatio="xMidYMid meet" focusable="false" class="style-scope yt-icon" style="pointer-events: none; display: block; width: 100%; height: 100%;">
							<g mirror-in-rtl="" class="style-scope yt-icon">
								<path
									d="M19.629,9.655c-0.021-0.589-0.088-1.165-0.21-1.723h-3.907V7.244h1.378V6.555h-2.756V5.866h2.067V5.177h-0.689V4.488h-1.378V3.799h0.689V3.11h-1.378V2.421h0.689V1.731V1.294C12.88,0.697,11.482,0.353,10,0.353c-5.212,0-9.446,4.135-9.629,9.302H19.629z M6.555,2.421c1.522,0,2.756,1.234,2.756,2.756S8.077,7.933,6.555,7.933S3.799,6.699,3.799,5.177S5.033,2.421,6.555,2.421z"
									class="style-scope yt-icon"></path>
								<path
									d="M12.067,18.958h-0.689v-0.689h2.067v-0.689h0.689V16.89h2.067v-0.689h0.689v-0.689h-1.378v-0.689h-2.067v-0.689h1.378v-0.689h2.756v-0.689h-1.378v-0.689h3.218c0.122-0.557,0.189-1.134,0.21-1.723H0.371c0.183,5.167,4.418,9.302,9.629,9.302c0.711,0,1.401-0.082,2.067-0.227V18.958z"
									class="style-scope yt-icon"></path>
							</g>
						</svg>
						<!--css-build:shady-->
					</div>
				</div>
			</button>
		</div>
		<tp-yt-paper-tooltip fit-to-visible-bounds="" offset="8" role="tooltip" tabindex="-1" style="inset: 83.5px auto auto 594.664px;">
			<!--css-build:shady-->
			<div id="tooltip" class="style-scope tp-yt-paper-tooltip frvp-toggle-tooltip" style="text-transform:capitalize;">Toggle floating pane (disable, normal, always)</div>
		</tp-yt-paper-tooltip>
	</div>`).trim();
    return div.firstChild;
}

function waitForElement(selector, exist = true) {
    return new Promise(resolve => {
        if (!!document.querySelector(selector)) {
            return resolve(document.querySelector(selector));
        }

        const observer = new MutationObserver(mutations => {
            if (!!document.querySelector(selector) === exist) {
                if (exist) resolve(document.querySelector(selector));
                else resolve(exist);
                observer.disconnect();
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    });
}

function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

function getFloatState() {
    return localStorage.getItem('floating-pane') ?? floatState.normal;
}

function setFloatState(state) {
    localStorage.setItem('floating-pane', state);
    floatStateChanged(state);
    dom.toggle.getElementsByClassName('frvp-toggle-tooltip')[0].innerText = state;
}

function floatStateChanged(state) {
    if (state === floatState.disabled) {
        observer.disconnect();
        document.body.classList.remove(floatClass.enabled, floatClass.theater);
    } else {
        if (state === floatState.normal && !(dom.theaterContainer.childNodes.length) ||
            state === floatState.always) {
            document.body.classList.add(floatClass.enabled);
            if (dom.theaterContainer.childNodes.length)
                document.body.classList.add(floatClass.theater);
        }
        observer.observe(dom.theaterContainer, { childList: true });
    }
}

const observer = new MutationObserver(async function (e) {
    await delay(1); // delays immediate changes
    // to allow the player state to change first
    let state = getFloatState();
    if (state === floatState.normal) {
        if (e[0].addedNodes.length) document.body.classList.remove(floatClass.enabled);
        else document.body.classList.add(floatClass.enabled);
    } else if (state === floatState.always) {
        if (e[0].addedNodes.length) document.body.classList.add(floatClass.theater);
        else document.body.classList.remove(floatClass.theater);
    }
});

async function instateToggleButton() {

    console.log('Ok');
    let clearSignal = await waitForElement('#float-state-toggle', false);
    dom.playerControls = await waitForElement('ytd-app > #content > #page-manager > ytd-watch-flexy > #columns #primary > #primary-inner > #below > ytd-watch-metadata > #above-the-fold > #top-row > #actions > #actions-inner > #menu > ytd-menu-renderer > #top-level-buttons-computed');

    dom.toggle = createToggleButton();
    dom.toggle.addEventListener('click', function (e) {
        let states = Object.values(floatState);
        let currentState = getFloatState();
        let currentStateIndex = states.indexOf(currentState)
        let nextStateIndex = (currentStateIndex + 1) % (states.length);
        let nextState = states[nextStateIndex];
        setFloatState(nextState);
    });

    let state = getFloatState();
    floatStateChanged(state);

    dom.toggle.getElementsByClassName('frvp-toggle-tooltip')[0].innerText = state;
    dom.playerControls.appendChild(dom.toggle)

    observeUrlChange();
}

const observeUrlChange = () => {
    let oldHref = document.location.href;
    const body = document.querySelector("body");
    const observer = new MutationObserver(mutations => {
        mutations.forEach(() => {
            if (oldHref !== document.location.href) {
                oldHref = document.location.href;
                instateToggleButton();
                observer.disconnect();
            }
        });
    });
    observer.observe(body, { childList: true, subtree: true });
};

(async function() {

    dom.playerContainer = await waitForElement('#player-container-inner');
    dom.theaterContainer = await waitForElement('#player-theater-container');

    loadFloatStyle();
    instateToggleButton();
})();