☰

Jable video control bar tweak

UI tweak for video control bar

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Violentmonkey installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Violentmonkey installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Userscripts installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey installieren.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

(Ich habe bereits einen Benutzerstil Verwaltung, ich möchte ihn installieren!)

// ==UserScript==
// @name         Jable video control bar tweak
// @namespace    http://tampermonkey.net
// @version      7.0.4
// @description  UI tweak for video control bar
// @author       php
// @match        *://jable.tv/videos/*
// @grant        none
// @run-at       document-end
// @license       MIT
// ==/UserScript==

(function() {
    'use strict';

    let hideTimeout = null;

    // Add CSS for Custom Skip Buttons AND Vertical Volume Bar
    const style = document.createElement('style');
    style.innerHTML = `
        /* Force player to stay visible during interactions */
        .plyr.keep-showing .plyr__controls {
            opacity: 1 !important;
            visibility: visible !important;
            pointer-events: auto !important;
            transform: translateY(0) !important;
        }

        /* NEW: Skip buttons styles moved from JS to CSS for cleaner code */
        .custom-skip-btn {
            width: 32px !important;
            height: 32px !important;
            min-width: 32px !important;
            padding: 0 !important;
            flex-shrink: 0 !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
        }

        /* Strip residual padding/flex from the container so it tightly wraps the button */
        .plyr__controls__item.plyr__volume {
            position: relative !important;
            overflow: visible !important;
            flex: 0 0 auto !important;
            min-width: auto !important;
            width: auto !important;
            padding: 0 !important;
            margin: 0 !important;
            display: flex !important;
            justify-content: center !important;
        }

        /* Take the volume range out of normal document flow */
        .plyr__controls__item.plyr__volume input[data-plyr="volume"] {
            position: absolute !important;
            bottom: 84px !important;
            left: calc(50% - 60px) !important;

            width: 120px !important;
            height: 32px !important;
            margin: 0 !important;

            transform: rotate(-90deg) !important;
            transform-origin: center center !important;

            touch-action: none !important; /* NEW: Disables native screen scrolling while touching the bar */

            opacity: 0;
            visibility: hidden;
            pointer-events: none;
            transition: opacity 0.25s ease, visibility 0.25s ease !important;
            z-index: 100 !important;

            flex: none !important;
            max-width: none !important;
        }

        /* Invisible hover bridge to cover the empty gap between the icon and the bar */
        .plyr__controls__item.plyr__volume.show-volume::after {
            content: '';
            position: absolute;
            bottom: 100%; /* Starts exactly at the top of the volume icon container, never covering the button */
            left: -20px;
            right: -20px;
            height: 80px; /* Reaches up to cover the empty space to the slider */
            z-index: 99;
        }

        /* Show state applied via JavaScript */
        .plyr__controls__item.plyr__volume.show-volume input[data-plyr="volume"] {
            opacity: 1 !important;
            visibility: visible !important;
            pointer-events: auto !important;
        }
    `;
    document.head.appendChild(style);

    function injectUI() {
        const controlBar = document.querySelector('.plyr__controls');
        const video = document.querySelector('video');
        const plyrContainer = document.querySelector('.plyr');

        // --- Feature 1: Skip Buttons ---
        if (controlBar && video && !document.querySelector('.custom-skip-btn')) {
            const createBtn = (seconds, label) => {
                const btn = document.createElement('button');
                btn.type = 'button';
                btn.className = 'plyr__controls__item plyr__control custom-skip-btn';
                btn.innerHTML = label;

                const handleClick = (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    video.currentTime += seconds;
                    if (plyrContainer) {
                        if (hideTimeout) clearTimeout(hideTimeout);
                        plyrContainer.classList.add('keep-showing');
                        hideTimeout = setTimeout(() => plyrContainer.classList.remove('keep-showing'), 5000);
                    }
                };

                btn.addEventListener('click', handleClick);
                btn.addEventListener('touchstart', handleClick, { passive: false });

                return btn;
            };

            const rwBtn = createBtn(-10, '<svg aria-hidden="true" focusable="false"><use xlink:href="#plyr-rewind"></use></svg><span class="plyr__sr-only">Rewind 10s</span>');
            const ffBtn = createBtn(10, '<svg aria-hidden="true" focusable="false"><use xlink:href="#plyr-fast-forward"></use></svg><span class="plyr__sr-only">Forward 10s</span>');
            const playBtn = controlBar.querySelector('button[data-plyr="play"]');

            if (playBtn) {
                playBtn.before(rwBtn);
                playBtn.after(ffBtn);
            }
        }

        // --- Feature 2: Vertical Volume Bar Logic ---
        const volumeContainer = document.querySelector('.plyr__volume');

        if (volumeContainer && !volumeContainer.classList.contains('custom-vol-enabled')) {
            volumeContainer.classList.add('custom-vol-enabled');
            const volumeBtn = volumeContainer.querySelector('button[data-plyr="mute"]');
            const isHoverable = () => window.matchMedia('(hover: hover)').matches;

            // 1. Desktop: Hover Interactions
            let hoverTimer = null;
            const volumeInput = volumeContainer.querySelector('input[data-plyr="volume"]');

            const keepOpen = () => {
                if (isHoverable()) {
                    clearTimeout(hoverTimer); // Cancels closing whenever mouse is on icon OR bar
                    volumeContainer.classList.add('show-volume');
                }
            };

            const delayClose = () => {
                if (isHoverable()) {
                    clearTimeout(hoverTimer);
                    // Grants a 300ms window to move between the icon and slider
                    hoverTimer = setTimeout(() => {
                        volumeContainer.classList.remove('show-volume');
                    }, 300);
                }
            };

            // Listen on the volume icon container
            volumeContainer.addEventListener('mouseenter', keepOpen);
            volumeContainer.addEventListener('mouseleave', delayClose);

            // Listen on the vertical volume slider itself
            if (volumeInput) {
                volumeInput.addEventListener('mouseenter', keepOpen);
                volumeInput.addEventListener('mouseleave', delayClose);
            }
            // 2. Mobile: Click/Touch Interactions
            if (volumeBtn) {
                volumeBtn.addEventListener('click', () => {
                    if (!isHoverable()) volumeContainer.classList.add('show-volume');
                });
            }

            // 3. Mobile: Close when clicking elsewhere
            document.addEventListener('click', (e) => {
                if (volumeContainer.classList.contains('show-volume')) {
                    // Close if clicked completely outside the control bar OR clicked another interactive element in the control bar
                    if (!controlBar.contains(e.target) || (!volumeContainer.contains(e.target) && e.target.closest('button, input'))) {
                        volumeContainer.classList.remove('show-volume');
                    }
                }
            });

            // 4. Mobile: Vertical Volume Calculation
            //const volumeInput = volumeContainer.querySelector('input[data-plyr="volume"]');

            if (volumeInput && !volumeInput.dataset.touchBound) {
                volumeInput.dataset.touchBound = "true";

                const handleVerticalTouch = (e) => {
                    if (!e.touches || e.touches.length === 0) return;

                    // Stop the video player from applying its horizontal math
                    e.stopPropagation();
                    e.stopImmediatePropagation();

                    // Calculate the vertical position
                    const rect = volumeInput.getBoundingClientRect();
                    const clientY = e.touches[0].clientY;

                    // Figure out the percentage from bottom (0) to top (1)
                    let pct = (rect.bottom - clientY) / rect.height;
                    pct = Math.max(0, Math.min(1, pct)); // Keep it between 0 and 1

                    // Apply the new volume level and update the pink fill bar
                    volumeInput.value = pct;
                    volumeInput.style.setProperty('--value', `${pct * 100}%`);

                    // Tell the video player that the volume changed
                    volumeInput.dispatchEvent(new Event('input', { bubbles: true }));
                };

                // capture: true intercepts the touch before the video player sees it
                volumeInput.addEventListener('touchstart', handleVerticalTouch, { passive: false, capture: true });
                volumeInput.addEventListener('touchmove', handleVerticalTouch, { passive: false, capture: true });
            }
        }
    }

    const observer = new MutationObserver(injectUI);
    observer.observe(document.body, { childList: true, subtree: true });

    injectUI();
})();