Jable video control bar tweak

UI tweak for video control bar

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Jable video control bar tweak
// @namespace    http://tampermonkey.net
// @version      7.0.2
// @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;


            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;
        }

        /* 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
            volumeContainer.addEventListener('mouseenter', () => isHoverable() && volumeContainer.classList.add('show-volume'));
            volumeContainer.addEventListener('mouseleave', () => isHoverable() && volumeContainer.classList.remove('show-volume'));

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

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

    injectUI();
})();