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