YouTube - Miniplayer

Zeigt einen Miniplayer, sobald Sie am Video vorbeiscrollen.

Skript installieren?
Vom Ersteller vorgeschlagenes Skript

Ihnen könnte auch YouTube - Zeitindikatoren gefallen.

Skript installieren
// ==UserScript==
// @name                    YouTube - Miniplayer
// @name:fr                 YouTube - Lecteur réduit
// @name:es                 YouTube - Minirreproductor
// @name:de                 YouTube - Miniplayer
// @name:it                 YouTube - Miniplayer
// @name:zh-CN              YouTube - Miniplayer
// @namespace               https://gist.github.com/4lrick/5a54e121bdc9056a7551529669d65ae6
// @version                 1.1
// @description             Shows a mini player when you scroll past the video.
// @description:fr          Affiche un lecteur réduit quand vous faites défiler sous la vidéo.
// @description:es          Muestra un minirreproductor al desplazarte más allá del video.
// @description:de          Zeigt einen Miniplayer, sobald Sie am Video vorbeiscrollen.
// @description:it          Mostra un miniplayer quando scorri oltre il video.
// @description:zh-CN       在滚动越过视频时显示迷你播放器。
// @author                  4lrick
// @match                   https://www.youtube.com/*
// @icon                    https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant                   none
// @license                 GPL-3.0-only
// ==/UserScript==

(function () {
    'use strict';

    const CFG = {
        rawVideoSelector: '.html5-video-container',
        mainPlayerSelector: '#movie_player',
        miniPlayerClass: 'mpu-mini-player',
        widthPx: 480,
        scrollThreshold: 300,
        autohideDelayMs: 2000,
        transitionDurationMs: 300,
    };

    function injectStyle() {
        if (document.getElementById('mpu-style')) return;
        const style = document.createElement('style');
        style.id = 'mpu-style';
        style.textContent = `
        /* Mini player container */
        .${CFG.miniPlayerClass} {
            position: fixed !important;
            width: ${CFG.widthPx}px !important;
            height: ${Math.round(CFG.widthPx * 9 / 16)}px !important;
            right: 16px !important;
            bottom: 16px !important;
            background: #000;
            overflow: hidden !important;
            border-radius: 12px !important;
            opacity: 0;
            transform: scale(0.8) !important;
            transition: opacity ${CFG.transitionDurationMs}ms ease, transform ${CFG.transitionDurationMs}ms ease !important;
            user-select: none;
        }
        .${CFG.miniPlayerClass}.visible {
            opacity: 1;
            transform: scale(1) !important;
        }

        /* Ensure the video fills the mini player */
        .${CFG.miniPlayerClass} ${CFG.rawVideoSelector},
        .${CFG.miniPlayerClass} video.html5-main-video {
            width: 100% !important;
            height: 100% !important;
            left: 0 !important;
        }
        .${CFG.miniPlayerClass} ${CFG.rawVideoSelector} {
            position: static !important;
        }

        /* Controls bar */
        .${CFG.miniPlayerClass} .mpu-controls {
            position: absolute !important;
            left: 0; right: 0; bottom: 0;
            display: flex; align-items: center; gap: 8px;
            color: #fff; font: 12px/1.2 system-ui, sans-serif;
            user-select: none; pointer-events: auto;
            padding: 4px;
        }

        /* Bottom buttons */
        .${CFG.miniPlayerClass} .mpu-btn {
            cursor: pointer;
        }
        .${CFG.miniPlayerClass} .mpu-btn-play,
        .${CFG.miniPlayerClass} .mpu-btn-fullscreen {
            background: transparent; border: none;
            display: flex;
        }
        .${CFG.miniPlayerClass} .mpu-btn-play::before {
            content: '';
            width: 28px; height: 28px;
            background-image: url('data:image/svg+xml;utf8,<svg viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg"><path d="M12,26 L18.5,22 L18.5,14 L12,10 Z M18.5,22 L25,18 L25,18 L18.5,14 Z" fill="%23fff"/></svg>');
            background-size: contain;
            background-repeat: no-repeat;
        }
        .${CFG.miniPlayerClass} .mpu-btn-play.playing::before {
            background-image: url('data:image/svg+xml;utf8,<svg viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg"><path d="M12,26 L16,26 L16,10 L12,10 Z M21,26 L25,26 L25,10 L21,10 Z" fill="%23fff"/></svg>');
        }
        .${CFG.miniPlayerClass} .mpu-btn-fullscreen { margin-left: auto; }
        .${CFG.miniPlayerClass} .mpu-btn-fullscreen::before {
            content: '';
            width: 28px; height: 28px;
            background-image: url('data:image/svg+xml;utf8,<svg viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg"><path d="M10,16 h2 v-4 h4 v-2 h-6 v6 z" fill="%23fff"/><path d="M20,10 v2 h4 v4 h2 v-6 h-6 z" fill="%23fff"/><path d="M24,24 h-4 v2 h6 v-6 h-2 v4 z" fill="%23fff"/><path d="M12,20 h-2 v6 h6 v-2 h-4 v-4 z" fill="%23fff"/></svg>');
            background-size: contain;
            background-repeat: no-repeat;
        }
        .${CFG.miniPlayerClass} .mpu-btn-fullscreen.fullscreen::before {
            background-image: url('data:image/svg+xml;utf8,<svg viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg"><path d="M14,14 h-4 v2 h6 v-6 h-2 v4 z" fill="%23fff"/><path d="M22,14 v-4 h-2 v6 h6 v-2 h-4 z" fill="%23fff"/><path d="M20,26 h2 v-4 h4 v-2 h-6 v6 z" fill="%23fff"/><path d="M10,22 h4 v4 h2 v-6 h-6 v2 z" fill="%23fff"/></svg>');
        }

        /* Top buttons */
        .${CFG.miniPlayerClass} .mpu-btn-top {
            position: absolute; top: 0 !important;
            background: transparent; border: none;
            display: flex;
            padding: 8px;
        }
        .${CFG.miniPlayerClass} .mpu-btn-close { right: 0; }
        .${CFG.miniPlayerClass} .mpu-btn-scroll-up::before {
            content: '';
            width: 24px; height: 24px;
            background-image: url('data:image/svg+xml;utf8,<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g transform="translate(12,12) scale(-1,1) translate(-12,-12)"><path d="M19,19 L5,19 L5,5 L12,5 L12,3 L5,3 C3.89,3 3,3.9 3,5 L3,19 C3,20.1 3.89,21 5,21 L19,21 C20.1,21 21,20.1 21,19 L21,12 L19,12 L19,19 Z M14,3 L14,5 L17.59,5 L7.76,14.83 L9.17,16.24 L19,6.41 L19,10 L21,10 L21,3 L14,3 Z" fill="%23fff"/></g></svg>');
            background-size: contain;
            background-repeat: no-repeat;
        }
        .${CFG.miniPlayerClass} .mpu-btn-close::before {
            content: '';
            width: 24px; height: 24px;
            background-image: url('data:image/svg+xml;utf8,<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" fill="%23fff"/></svg>');
            background-size: contain;
            background-repeat: no-repeat;
        }

        /* Autohide behavior */
        .${CFG.miniPlayerClass} .mpu-controls,
        .${CFG.miniPlayerClass} .mpu-btn-top,
        .${CFG.miniPlayerClass} .mpu-progress-container,
        .${CFG.miniPlayerClass} .ytp-gradient { transition: opacity 150ms ease; }

        .${CFG.miniPlayerClass}.ytp-autohide .mpu-controls,
        .${CFG.miniPlayerClass}.ytp-autohide .mpu-btn-top,
        .${CFG.miniPlayerClass}.ytp-autohide .mpu-progress-container,
        .${CFG.miniPlayerClass}.ytp-autohide .ytp-gradient {
            opacity: 0; pointer-events: none;
        }

        /* Progress bar */
        .${CFG.miniPlayerClass} .mpu-progress-container {
        position: absolute; left: 8px; right: 8px; bottom: 40px;
        }
        @-moz-document url-prefix() {
            .${CFG.miniPlayerClass} .mpu-progress-container {
            bottom: 30px;
            }
        }

        .${CFG.miniPlayerClass} .mpu-slider {
            --bar-h: 4px;
            --thumb: 13px;
            --fill: 0%;

            --accent: var(--yt-spec-static-brand-red, #f03);
            --accent2: #ff2791;
            --track-bg: rgba(255,255,255,0.2);
            --fill-gradient: linear-gradient(to right, var(--accent) 80%, var(--accent2) 100%);

            -webkit-appearance: none;
            width: -webkit-fill-available;
            width: -moz-available;
            cursor: pointer;
            background: none;
        }
        .${CFG.miniPlayerClass} .mpu-slider:hover { --bar-h: 6px; }

        .${CFG.miniPlayerClass} .mpu-slider::-webkit-slider-runnable-track {
            height: var(--bar-h);
            background:
            var(--fill-gradient) 0 50% / var(--fill) var(--bar-h) no-repeat,
            linear-gradient(var(--track-bg), var(--track-bg)) 0 50% / 100% var(--bar-h) no-repeat;
        }
        .${CFG.miniPlayerClass} .mpu-slider::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: var(--thumb); height: var(--thumb);
            border-radius: 50%;
            background: var(--accent);
            margin-top: calc((var(--bar-h) - var(--thumb)) / 2);
        }

        .${CFG.miniPlayerClass} .mpu-slider::-moz-range-track {
            height: var(--bar-h);
            background: var(--track-bg);
        }
        .${CFG.miniPlayerClass} .mpu-slider::-moz-range-progress {
            height: var(--bar-h);
            background: var(--fill-gradient);
        }
        .${CFG.miniPlayerClass} .mpu-slider::-moz-range-thumb {
            width: var(--thumb); height: var(--thumb);
            border: 0; border-radius: 50%;
            background: var(--accent);
        }

        /* Gradient behind controls */
        .${CFG.miniPlayerClass} .ytp-gradient {
            position: absolute; left: 0; right: 0;
            height: 64px; pointer-events: none;
            background: linear-gradient(to top, rgba(0,0,0,0.9), rgba(0,0,0,0));
        }

    `;
        document.head.appendChild(style);
    }

    let rawVideo;
    let miniPlayer;
    let playerInitialLocation = null;
    let controls = null;
    let isPlayerHidden = false;
    let autohideTimer = null;
    let observer = null;
    let loopStarted = false;
    let isDragging = false;
    let dragStartX = 0;
    let dragStartY = 0;
    let dragStartLeft = 0;
    let dragStartTop = 0;

    function restoreFromMiniPlayer() {
        if (!rawVideo) return;
        if (!playerInitialLocation) return;
        const parent = playerInitialLocation.parentNode;
        if (!parent) return;
        parent.insertBefore(rawVideo, playerInitialLocation);
        if (miniPlayer) {
            miniPlayer.classList.remove('visible');
            setTimeout(() => {
                miniPlayer.remove();
                miniPlayer = null;
                controls = null;
            }, CFG.transitionDurationMs);
        }
        playerInitialLocation = null;
    }

    function attachAutohideListeners() {
        miniPlayer.addEventListener('mousemove', showControlsNow);
        miniPlayer.addEventListener('mouseenter', showControlsNow);
        miniPlayer.addEventListener('touchstart', showControlsNow, { passive: true });
        miniPlayer.addEventListener('focusin', showControlsNow);
        miniPlayer.addEventListener('mouseleave', scheduleAutohide);
    }

    function attachFullscreenListener() {
        if (!controls) return;
        const { fullscreenBtn } = controls;
        function onFullscreenChange() {
            if (document.fullscreenElement) {
                fullscreenBtn.classList.add('fullscreen');
                fullscreenBtn.title = 'Exit full screen';
            } else {
                fullscreenBtn.classList.remove('fullscreen');
                fullscreenBtn.title = 'Full screen';
            }
        }
        document.addEventListener('fullscreenchange', onFullscreenChange);
    }

    function updatePlayIcon() {
        if (!controls) return;
        const video = getVideo();
        const isPlaying = video && !video.paused;
        if (isPlaying) {
            controls.playBtn.classList.add('playing');
            controls.playBtn.title = 'Pause';
        } else {
            controls.playBtn.classList.remove('playing');
            controls.playBtn.title = 'Play';
        }
    }

    function attachVideoListeners() {
        const video = getVideo();
        if (!video) return;
        video.addEventListener('play', updatePlayIcon);
        video.addEventListener('pause', updatePlayIcon);
        video.addEventListener('timeupdate', () => { updateBars(); updateTime(); });
        updatePlayIcon();
        updateBars();
        updateTime();
    }

    function formatTime(t) {
        if (!isFinite(t) || t < 0) return '--:--';
        const s = Math.floor(t % 60).toString().padStart(2, '0');
        const m = Math.floor((t / 60) % 60).toString();
        const h = Math.floor(t / 3600);
        return h > 0 ? `${h}:${m.padStart(2, '0')}:${s}` : `${m}:${s}`;
    }

    function updateTime() {
        if (!controls) return;
        const { time } = controls;
        const video = getVideo();
        if (video) {
            time.textContent = `${formatTime(video.currentTime)} / ${formatTime(video.duration)}`;
        } else {
            time.textContent = '--:-- / --:--';
        }
    }

    function isLive() {
        const mainPlayer = document.querySelector(CFG.mainPlayerSelector);
        if (!mainPlayer) return false;
        const timeDisplay = mainPlayer.querySelector('.ytp-time-display');
        return !!(timeDisplay && timeDisplay.classList.contains('ytp-live'));
    }

    function updateBars() {
        if (!controls) return;
        const { progressBar } = controls;
        const video = getVideo();
        if (!video) return;
        let progressFraction = Math.max(0, Math.min(1, video.currentTime / video.duration));
        progressBar.slider.value = progressFraction;
        progressBar.slider.style.setProperty('--fill', `${progressFraction * 100}%`);
    }

    function attachControlListeners() {
        if (!controls) return;
        const { playBtn, fullscreenBtn, scrollUpBtn, closeBtn, progressBar } = controls;

        function onPlayClick() {
            const video = getVideo();
            if (!video) return;
            if (video.paused) video.play(); else video.pause();
            showControlsNow();
        }

        function onFullscreenClick() {
            if (document.fullscreenElement) document.exitFullscreen();
            else displayMiniPlayer().requestFullscreen();
            showControlsNow();
        }

        function onUpClick() {
            window.scrollTo({ top: 0, behavior: 'smooth' });
            showControlsNow();
        }

        function onCloseClick() {
            isPlayerHidden = true;
            toggleMiniPlayer(false);
        }

        function onProgressInput(event) {
            const newProgressFraction = parseFloat(event.target.value);
            const video = getVideo();
            if (!video) return;
            video.currentTime = newProgressFraction * video.duration;
            updateBars();
            updateTime();
            showControlsNow();
        }

        playBtn.addEventListener('click', onPlayClick);
        fullscreenBtn.addEventListener('click', onFullscreenClick);
        scrollUpBtn.addEventListener('click', onUpClick);
        closeBtn.addEventListener('click', onCloseClick);
        progressBar.slider.addEventListener('input', onProgressInput);
    }

    function createGradient(position = 'bottom') {
        const gradient = document.createElement('div');
        gradient.className = 'ytp-gradient';
        if (position === 'top') {
            gradient.style.top = '-20px';
            gradient.style.transform = 'rotate(180deg)';
        } else {
            gradient.style.bottom = '-5px';
        }
        return gradient;
    }

    function createProgressBar() {
        const progressContainer = document.createElement('div');
        progressContainer.className = 'mpu-progress-container';

        const slider = document.createElement('input');
        slider.className = 'mpu-slider';
        slider.type = 'range';
        slider.min = '0';
        slider.max = '1';
        slider.step = '0.001';
        slider.value = '0';

        progressContainer.appendChild(slider);

        return {
            progressContainer,
            slider
        };
    }

    function createButton(type, text) {
        const btn = document.createElement('button');
        let className = `mpu-btn mpu-btn-${type}`;

        if (type === 'scroll-up' || type === 'close') {
            className += ' mpu-btn-top';
        }

        btn.className = className;
        btn.title = text;
        return btn;
    }

    function displayControls() {
        if (controls && controls.root && miniPlayer && miniPlayer.contains(controls.root)) return controls;

        const mainPlayer = document.querySelector(CFG.mainPlayerSelector);
        if (!mainPlayer) return null;

        const bottomGradient = createGradient('bottom');
        const topGradient = createGradient('top');
        miniPlayer.appendChild(bottomGradient);
        miniPlayer.appendChild(topGradient);

        const controlsRoot = document.createElement('div');
        controlsRoot.className = 'mpu-controls';

        const playBtn = createButton('play', 'Play/Pause');
        const fullscreenBtn = createButton('fullscreen', 'Full screen');
        const scrollUpBtn = createButton('scroll-up', 'Scroll to top');
        const closeBtn = createButton('close', 'Close');

        const time = document.createElement('span');
        time.textContent = '--:-- / --:--';

        controlsRoot.append(playBtn, time, fullscreenBtn);
        miniPlayer.appendChild(controlsRoot);

        const progressBar = createProgressBar();
        miniPlayer.appendChild(progressBar.progressContainer);

        miniPlayer.appendChild(scrollUpBtn);
        miniPlayer.appendChild(closeBtn);
        controls = { root: controlsRoot, playBtn, fullscreenBtn, time, scrollUpBtn, closeBtn, progressBar, bottomGradient, topGradient };
        return controls;
    }

    function onTimestampClickPreserveScroll(event) {
        if (isPlayerHidden) return;
        const anchor = event.target?.closest('a[href]');
        if (!anchor?.closest('#comments, ytd-comments')) return;

        const text = anchor.textContent?.trim();
        if (!text) return;

        const parts = text.split(':').map(p => parseInt(p, 10));
        if (parts.some(n => isNaN(n))) return;

        let seconds;
        if (parts.length === 2) {
            seconds = parts[0] * 60 + parts[1];
        } else if (parts.length === 3) {
            seconds = parts[0] * 3600 + parts[1] * 60 + parts[2];
        } else {
            return;
        }

        const videoElement = rawVideo?.querySelector('video');
        if (!videoElement?.duration) return;

        event.preventDefault();
        event.stopPropagation();

        videoElement.currentTime = Math.max(0, Math.min(videoElement.duration, seconds));
        showControlsNow();
    }

    function clearAutohideTimer() {
        if (autohideTimer) {
            clearTimeout(autohideTimer); autohideTimer = null;
        }
    }

    function scheduleAutohide() {
        clearAutohideTimer();
        autohideTimer = setTimeout(() => {
            if (miniPlayer) miniPlayer.classList.add('ytp-autohide');
        }, CFG.autohideDelayMs);
    }

    function showControlsNow() {
        miniPlayer.classList.remove('ytp-autohide');
        scheduleAutohide();
    }

    function getVideo() {
        return rawVideo ? rawVideo.querySelector('video') : null;
    }

    function onMiniPlayerBackgroundClick(event) {
        if (event.button !== 0) return;

        const mouseMovement = Math.abs(event.clientX - dragStartX) + Math.abs(event.clientY - dragStartY);
        if (mouseMovement > 1) return;

        const target = event.target;
        if (!target) return;
        if (isTargetInteractive(target)) return;
        const videoElement = getVideo();
        if (!videoElement) return;
        if (videoElement.paused) videoElement.play(); else videoElement.pause();
        showControlsNow();
    }

    function handleDragEnd() {
        isDragging = false;
        document.removeEventListener('mousemove', handleDragMove);
        document.removeEventListener('mouseup', handleDragEnd);
    }

    function handleDragMove(event) {
        if (!isDragging) return;

        const deltaX = event.clientX - dragStartX;
        const deltaY = event.clientY - dragStartY;

        const newLeft = Math.max(0, Math.min(window.innerWidth - CFG.widthPx, dragStartLeft + deltaX));
        const newTop = Math.max(0, Math.min(window.innerHeight - Math.round(CFG.widthPx * 9 / 16), dragStartTop + deltaY));

        miniPlayer.style.left = newLeft + 'px';
        miniPlayer.style.top = newTop + 'px';
    }

    function isTargetInteractive(target) {
        if (!target || !target.closest) return false;
        return !!(
            target.closest('.mpu-progress-container') ||
            target.closest('button')
        );
    }

    function handleDragStart(event) {
        if (event.button !== 0 || isTargetInteractive(event.target) || document.fullscreenElement === miniPlayer) return;

        isDragging = true;
        dragStartX = event.clientX;
        dragStartY = event.clientY;

        const rect = miniPlayer.getBoundingClientRect();
        dragStartLeft = rect.left;
        dragStartTop = rect.top;

        document.addEventListener('mousemove', handleDragMove);
        document.addEventListener('mouseup', handleDragEnd);

        event.preventDefault();
    }

    function displayMiniPlayer() {
        if (miniPlayer && document.body.contains(miniPlayer)) return miniPlayer;
        miniPlayer = document.createElement('div');
        miniPlayer.className = CFG.miniPlayerClass;
        document.body.appendChild(miniPlayer);
        miniPlayer.addEventListener('mousedown', handleDragStart);
        miniPlayer.addEventListener('click', onMiniPlayerBackgroundClick);
        document.addEventListener('click', onTimestampClickPreserveScroll, true);

        requestAnimationFrame(() => {
            miniPlayer.classList.add('visible');
        });

        return miniPlayer;
    }

    function moveToMiniPlayer() {
        if (!rawVideo || !window.location.pathname.includes('watch')) return;

        const mainPlayer = document.querySelector(CFG.mainPlayerSelector);
        const activeVideo = mainPlayer ? mainPlayer.querySelector(CFG.rawVideoSelector) : null;
        const offlineSlate = mainPlayer ? mainPlayer.querySelector('.ytp-offline-slate') : null;
        if (!activeVideo || (offlineSlate && offlineSlate.style.display !== 'none')) return;
        if (isLive()) return;

        rawVideo = activeVideo;

        playerInitialLocation = rawVideo.nextSibling;
        displayMiniPlayer().appendChild(rawVideo);
        displayControls();
        attachControlListeners();
        attachVideoListeners();
        attachFullscreenListener();
        attachAutohideListeners();
        showControlsNow();
    }

    function toggleMiniPlayer(enabled) {
        if (!rawVideo) return;

        if (enabled) {
            moveToMiniPlayer();
        } else {
            restoreFromMiniPlayer();
        }
    }

    function handleVisibility(inView) {
        if (!inView) isPlayerHidden = false;
        if (isPlayerHidden && inView) return;
        toggleMiniPlayer(inView);
    }

    function tick() {
        if (!rawVideo) return;

        handleVisibility(window.scrollY > CFG.scrollThreshold);
        requestAnimationFrame(tick);
    }

    function checkVideoExists() {
        const mainPlayer = document.querySelector(CFG.mainPlayerSelector);
        const inWatchPage = window.location.pathname.includes('watch');
        const foundVideo = mainPlayer ? mainPlayer.querySelector(CFG.rawVideoSelector) : null;
        const newVideo = (inWatchPage && foundVideo) ? foundVideo : null;
        if (rawVideo !== newVideo) {
            rawVideo = newVideo;
        }
        if (!rawVideo) return;
        if (!loopStarted) {
            loopStarted = true;
            if (observer) observer.disconnect();
            tick();
        }
    }

    function init() {
        injectStyle();
        if (observer) observer.disconnect();
        observer = new MutationObserver(checkVideoExists);
        observer.observe(document.body, { childList: true, subtree: true });
    }

    init();
})();