DLive Tools

DLive 화면 수정 및 렉 요소 최적화

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         DLive Tools
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  DLive 화면 수정 및 렉 요소 최적화
// @author       Dacchi
// @match        https://dlive.tv/*
// @grant        none
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const defaultSettings = {
        hideTopNav: true,
        hideDonation: true,
        hideTopContributors: true,
        hideLike: true,
        emoteScale: 50,
        chatWidth: 350
    };

    let settings = { ...defaultSettings };
    try {
        const saved = localStorage.getItem('dlive-custom-settings');
        if (saved) settings = { ...settings, ...JSON.parse(saved) };
    } catch (e) {}

    const saveSettings = () => {
        localStorage.setItem('dlive-custom-settings', JSON.stringify(settings));
    };

    // CSS
    const injectCustomCSS = () => {
        if (document.getElementById('dlive-custom-style')) return;
        const style = document.createElement('style');
        style.id = 'dlive-custom-style';
        style.textContent = `
            .dplayer,.dplayer-video-wrap{width:100%!important;height:100%!important;max-height:100vh!important;flex-grow:1!important}
            .dplayer-video{width:100%!important;height:100%!important;max-height:100vh!important;object-fit:contain!important}
            #liveContainer{display:flex!important;height:100%!important;flex-grow:1!important}
            #liveContainer>div{flex-grow:1!important;max-width:100%!important;height:100%!important}
            .v-content__wrap>div>div{max-width:100%!important;height:100%!important}

            body.setting-hide-nav .dlive-top-nav-target{
                position:fixed!important;top:0!important;left:0!important;width:100%!important;height:56px!important;
                z-index:10000!important;transition:transform .3s ease-in-out,opacity .3s ease-in-out!important;
                transform:translateY(-100%)!important;opacity:0!important;background-color:rgb(15,18,20)!important}
            body.setting-hide-nav #livestream-info{
                position:fixed!important;top:56px!important;left:0!important;width:100%!important;z-index:9999!important;
                transition:transform .3s ease-in-out,opacity .3s ease-in-out!important;
                transform:translateY(calc(-100% - 56px))!important;opacity:0!important;
                background-color:rgba(18,18,18,.95)!important;padding:10px!important;box-sizing:border-box!important}
            body.setting-hide-nav .dlive-top-nav-target.show-element,
            body.setting-hide-nav #livestream-info.show-element{transform:translateY(0)!important;opacity:1!important}

            body.setting-hide-like .like-button{display:none!important}
            body.setting-hide-donation .donation-wrapper{display:none!important}
            body.setting-hide-contributors .top-contributors,
            body.setting-hide-contributors .chatroom-header{display:none!important}

            .emote-img{
                max-width:calc(80px*(var(--dlive-emote-scale)/100))!important;
                max-height:calc(80px*(var(--dlive-emote-scale)/100))!important;
                height:auto!important;width:auto!important}

            .dlive-resizer{width:8px;cursor:ew-resize;background-color:transparent;z-index:1000;position:relative;flex-shrink:0;transition:background-color .2s}
            .dlive-resizer:hover,.dlive-resizer.active{background-color:rgba(255,204,0,.5)}
            body.is-resizing{user-select:none!important;cursor:ew-resize!important}
            body.is-resizing iframe,body.is-resizing video{pointer-events:none!important}

            .dlive-settings-wrapper{position:fixed;top:28px;left:50%;transform:translate(-50%,-50%);z-index:10001;font-family:sans-serif;display:flex;flex-direction:column;align-items:center}
            .dlive-settings-btn{width:36px;height:36px;background-color:#1a1c1f;color:#ffcc00;border:1px solid #333;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 5px rgba(0,0,0,.5);transition:all .2s;font-size:18px;line-height:1}
            .dlive-settings-btn:hover{background-color:#2a2c2f;transform:scale(1.05)}
            .dlive-settings-panel{position:absolute;top:46px;left:50%;transform:translateX(-50%);width:280px;background-color:#1a1c1f;color:#e0e0e0;border:1px solid #333;border-radius:12px;padding:16px;display:none;box-shadow:0 8px 24px rgba(0,0,0,.8)}
            .dlive-settings-panel.open{display:block}
            .dlive-settings-header{font-size:14px;font-weight:bold;margin-bottom:12px;color:#fff;border-bottom:1px solid #333;padding-bottom:8px}
            .dlive-setting-item{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;font-size:13px}
            .dlive-setting-slider-wrap{margin-bottom:15px}
            .dlive-setting-slider-wrap label{display:flex;justify-content:space-between;font-size:13px;margin-bottom:8px}

            .dlive-switch{position:relative;display:inline-block;width:34px;height:20px}
            .dlive-switch input{opacity:0;width:0;height:0}
            .dlive-slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:#444;transition:.4s;border-radius:20px}
            .dlive-slider:before{position:absolute;content:"";height:14px;width:14px;left:3px;bottom:3px;background-color:white;transition:.4s;border-radius:50%}
            input:checked+.dlive-slider{background-color:#ffcc00}
            input:checked+.dlive-slider:before{transform:translateX(14px)}

            input[type=range].dlive-range{-webkit-appearance:none;appearance:none;width:100%;height:6px;background:#555;border-radius:3px;outline:none;margin:0}
            input[type=range].dlive-range::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;height:16px;width:16px;border-radius:50%;background:#ffcc00;cursor:pointer;margin-top:-5px}
            input[type=range].dlive-range::-webkit-slider-runnable-track{width:100%;height:6px;cursor:pointer;background:#555;border-radius:3px}
            #dlive-hover-trigger {position:fixed;top:0;left:0;width:100%;height:30px;z-index:9998;background:transparent;}
        `;
        document.head.appendChild(style);
    };

    const applySettings = () => {
        const cl = document.body.classList;
        cl.toggle('setting-hide-nav',          !!settings.hideTopNav);
        cl.toggle('setting-hide-like',         !!settings.hideLike);
        cl.toggle('setting-hide-donation',     !!settings.hideDonation);
        cl.toggle('setting-hide-contributors', !!settings.hideTopContributors);
        document.documentElement.style.setProperty('--dlive-emote-scale', settings.emoteScale);
        applyChatWidth(settings.chatWidth);
    };

    let _chatPanel = null;
    const getChatPanel = () => {
        if (_chatPanel && document.contains(_chatPanel)) return _chatPanel;
        const chatBody = document.querySelector('.chatbody');
        if (!chatBody) return null;
        let el = chatBody;
        while (el?.parentElement) {
            if (window.getComputedStyle(el.parentElement).display === 'flex' &&
                window.getComputedStyle(el.parentElement).flexDirection === 'row') break;
            el = el.parentElement;
        }
        _chatPanel = el ?? null;
        return _chatPanel;
    };

    const applyChatWidth = (width) => {
        const panel = getChatPanel();
        if (!panel) return;
        const w = width + 'px';
        
        panel.style.width = w;
        panel.style.minWidth = w;
        panel.style.maxWidth = w;
        panel.style.flex = 'none';
    };

    // 설정 UI
    const injectSettingsUI = () => {
        if (document.getElementById('dlive-settings-wrapper')) return true;
        
        const topNav = document.querySelector('.dlive-top-nav-target');
        if (!topNav) return false;

        const wrapper = document.createElement('div');
        wrapper.id = 'dlive-settings-wrapper';
        wrapper.className = 'dlive-settings-wrapper';
        wrapper.innerHTML = `
            <div class="dlive-settings-btn" id="dlive-settings-btn">⚙️</div>
            <div class="dlive-settings-panel" id="dlive-settings-panel">
                <div class="dlive-settings-header">UI 설정</div>
                <div class="dlive-setting-item"><span>상단바 자동 숨김</span>
                    <label class="dlive-switch"><input type="checkbox" id="toggle-topnav" ${settings.hideTopNav?'checked':''}><span class="dlive-slider"></span></label></div>
                <div class="dlive-setting-item"><span>보물상자 숨기기</span>
                    <label class="dlive-switch"><input type="checkbox" id="toggle-donation" ${settings.hideDonation?'checked':''}><span class="dlive-slider"></span></label></div>
                <div class="dlive-setting-item"><span>기부자 숨기기</span>
                    <label class="dlive-switch"><input type="checkbox" id="toggle-contributors" ${settings.hideTopContributors?'checked':''}><span class="dlive-slider"></span></label></div>
                <div class="dlive-setting-item"><span>하트 숨기기</span>
                    <label class="dlive-switch"><input type="checkbox" id="toggle-like" ${settings.hideLike?'checked':''}><span class="dlive-slider"></span></label></div>
                <div class="dlive-setting-slider-wrap">
                    <label><span>GIF 채팅 크기</span><span id="scale-display">${settings.emoteScale}%</span></label>
                    <input type="range" id="slider-emote" class="dlive-range" min="10" max="100" value="${settings.emoteScale}">
                </div>
                <div class="dlive-setting-slider-wrap">
                    <label><span>채팅창 가로 크기</span><span id="width-display">${settings.chatWidth}px</span></label>
                    <input type="range" id="slider-chatwidth" class="dlive-range" min="200" max="800" value="${settings.chatWidth}">
                </div>
            </div>`;
        topNav.appendChild(wrapper);

        document.getElementById('dlive-settings-btn').addEventListener('click', () => {
            document.getElementById('dlive-settings-panel').classList.toggle('open');
        });

        const bindToggle = (id, key) => {
            document.getElementById(id)?.addEventListener('change', e => {
                settings[key] = e.target.checked;
                applySettings();
                saveSettings();
            });
        };
        bindToggle('toggle-topnav',        'hideTopNav');
        bindToggle('toggle-donation',      'hideDonation');
        bindToggle('toggle-contributors',  'hideTopContributors');
        bindToggle('toggle-like',          'hideLike');

        const emoteSlider   = document.getElementById('slider-emote');
        const scaleDisplay  = document.getElementById('scale-display');
        const widthSlider   = document.getElementById('slider-chatwidth');
        const widthDisplay  = document.getElementById('width-display');

        emoteSlider?.addEventListener('input', e => {
            scaleDisplay.textContent = e.target.value + '%';
            settings.emoteScale = +e.target.value;
            document.documentElement.style.setProperty('--dlive-emote-scale', settings.emoteScale);
        });
        emoteSlider?.addEventListener('change', saveSettings);

        widthSlider?.addEventListener('input', e => {
            widthDisplay.textContent = e.target.value + 'px';
            settings.chatWidth = +e.target.value;
            applyChatWidth(settings.chatWidth);
        });
        widthSlider?.addEventListener('change', saveSettings);
        
        return true;
    };

    let _navTagged = false;
    const tagElements = () => {
        if (_navTagged) return true;
        const navMenu = document.querySelector('.nav-bar-menu');
        if (!navMenu) return false;
        
        let topNav = navMenu.closest('header,nav,[class*="header"],[id*="nav"]') ?? navMenu;
        topNav.classList.add('dlive-top-nav-target');
        navMenu.dataset.tagged = 'true';
        _navTagged = true;
        return true;
    };

    let _hoverSetupDone = false;
    const setupHoverEffect = () => {
        if (_hoverSetupDone) return true;
        
        if (!document.getElementById('dlive-hover-trigger')) {
            const trigger = document.createElement('div');
            trigger.id = 'dlive-hover-trigger';
            document.body.appendChild(trigger);
        }

        let navShown = false;
        let hideTimeout = null;

        const toggleNav = (show) => {
            if (show === navShown) return;
            navShown = show;
            document.querySelector('.dlive-top-nav-target')?.classList.toggle('show-element', show);
            document.getElementById('livestream-info')?.classList.toggle('show-element', show);
        };

        document.addEventListener('mouseover', e => {
            if (!settings.hideTopNav) return;
            
            const isHoveringNav = e.target.closest('#livestream-info, .dlive-top-nav-target, #dlive-settings-wrapper, #dlive-hover-trigger');
            
            if (isHoveringNav) {
                if (hideTimeout) clearTimeout(hideTimeout);
                toggleNav(true);
            }
        });

        document.addEventListener('mouseout', e => {
            if (!settings.hideTopNav) return;
            
            const isLeavingNav = !e.relatedTarget || !e.relatedTarget.closest('#livestream-info, .dlive-top-nav-target, #dlive-settings-wrapper, #dlive-hover-trigger');
            
            if (isLeavingNav) {
                if (hideTimeout) clearTimeout(hideTimeout);
                hideTimeout = setTimeout(() => {
                    toggleNav(false);
                }, 100);
            }
        });

        _hoverSetupDone = true;
        return true;
    };

    const setupResizer = () => {
        if (document.querySelector('.dlive-resizer')) return true;
        const chatPanel = getChatPanel();
        if (!chatPanel?.parentElement) return false;

        const container = chatPanel.parentElement;
        const resizer = document.createElement('div');
        resizer.className = 'dlive-resizer';
        container.insertBefore(resizer, chatPanel);

        let rafId = null;

        const onMouseMove = (e) => {
            if (rafId) return;
            rafId = requestAnimationFrame(() => {
                rafId = null;
                const rect = container.getBoundingClientRect();
                const w = Math.round(rect.right - e.clientX);
                if (w < 200 || w > 800) return;

                applyChatWidth(w);
                settings.chatWidth = w;

                const slider  = document.getElementById('slider-chatwidth');
                const display = document.getElementById('width-display');
                if (slider)  slider.value = w;
                if (display) display.textContent = w + 'px';
            });
        };

        const onMouseUp = () => {
            document.querySelector('.dlive-resizer')?.classList.remove('active');
            document.body.classList.remove('is-resizing');
            document.removeEventListener('mousemove', onMouseMove);
            document.removeEventListener('mouseup', onMouseUp);
            saveSettings();
        };

        resizer.addEventListener('mousedown', e => {
            resizer.classList.add('active');
            document.body.classList.add('is-resizing');
            e.preventDefault();
            
            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);
        });
        
        return true;
    };

    const runInitializer = () => {
        let attempts = 0;
        
        setupHoverEffect();

        const initInterval = setInterval(() => {
            attempts++;
            
            const isTagged = tagElements();
            const isSettingsInjected = injectSettingsUI();
            const isResizerSetup = setupResizer();
            
            applySettings();

            if ((isTagged && isSettingsInjected && isResizerSetup) || attempts > 30) {
                clearInterval(initInterval);
            }
        }, 500);
    };

    const autoConfirmAge = () => {
        const rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;

        const tryConfirm = () => {
            const checkboxes = document.querySelectorAll('.agree-check input[type="checkbox"]');
            if (checkboxes.length < 2) return false;

            const [cb1, cb2] = checkboxes;
            if (!cb1.checked) cb1.click();

            setTimeout(() => {
                if (!cb2.checked) cb2.click();

                setTimeout(() => {
                    const agreeBtn = document.querySelector('.btn.agree');
                    if (agreeBtn) agreeBtn.click();
                }, rand(350, 450));
            }, rand(350, 450));

            return true;
        };

        if (!tryConfirm()) {
            let attempts = 0;
            const checkInterval = setInterval(() => {
                attempts++;
                if (tryConfirm() || attempts > 15) {
                    clearInterval(checkInterval);
                }
            }, 1000);
        }
    };

    const init = () => {
        injectCustomCSS();
        runInitializer();
        autoConfirmAge();
    };

    document.readyState === 'loading'
        ? document.addEventListener('DOMContentLoaded', init)
        : init();
})();