SOOP (숲) - 목록 탐색 자동 PIP (V14.10 Stable Focus)

V14.7 기반 + 배경 클릭 시 페이지 이동 버그 수정 및 검색창 닫기 최적화

当前为 2025-11-21 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         SOOP (숲) - 목록 탐색 자동 PIP (V14.10 Stable Focus)
// @namespace    http://tampermonkey.net/
// @version      14.10
// @description  V14.7 기반 + 배경 클릭 시 페이지 이동 버그 수정 및 검색창 닫기 최적화
// @author       Gemini
// @license      MIT
// @match        https://play.sooplive.co.kr/*
// @match        https://vod.sooplive.co.kr/*
// @match        https://www.sooplive.co.kr/*
// @grant        GM_addStyle
// @grant        unsafeWindow
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // ============================================================
    // [A] window.open 오버라이딩 (새 창 원천 차단 - V14.7 동일)
    // ============================================================
    const originalOpen = window.open;
    const targetWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;

    function hijackWindowOpen(url, name, specs) {
        if (typeof url === 'string' && url.includes('/search')) {
            executeSearchUrl(url);
            return null;
        }
        return originalOpen.call(window, url, name, specs);
    }
    window.open = hijackWindowOpen;
    targetWindow.open = hijackWindowOpen;


    // ============================================================
    // [B] 환경 판별
    // ============================================================
    const isIframe = window.self !== window.top;
    const isPlayerDomain = location.hostname.includes('play.sooplive.co.kr') || location.hostname.includes('vod.sooplive.co.kr');

    // ============================================================
    // [C] 탐색창 (Iframe) 내부 로직
    // ============================================================
    if (isIframe) {
        const style = document.createElement('style');
        style.innerHTML = `
            video, .thumbs_box .thumb, .broad_thumb, iframe[title*="광고"] { display: none !important; }
            .thumbs_box { background: #222; min-height: 100px; }
        `;
        document.head.appendChild(style);

        function sendTheme() {
            const isDark = document.documentElement.classList.contains('play-theme-dark') ||
                           document.body.classList.contains('dark') ||
                           getComputedStyle(document.body).backgroundColor === 'rgb(20, 21, 23)';
            window.parent.postMessage({ type: 'SOOP_THEME', isDarkMode: isDark }, '*');
        }
        window.addEventListener('load', sendTheme);
        setInterval(sendTheme, 2000);

        // [핵심 수정] 클릭 이벤트 완전 분리 (충돌 방지)
        document.addEventListener('click', function(e) {
            const link = e.target.closest('a');

            // 1. 링크가 아닌 배경을 클릭한 경우
            if (!link) {
                // 부모에게 '배경 클릭됨' 신호만 전송 (이동 로직 없음)
                window.parent.postMessage({ type: 'SOOP_BG_CLICK_SIMPLE' }, '*');
                return;
            }

            // 2. 링크를 클릭한 경우 -> 부모 창 이동
            if (link && link.href) {
                if (link.href.includes('play.sooplive.co.kr') || link.href.includes('vod.sooplive.co.kr')) {
                    e.preventDefault();
                    window.top.location.href = link.href;
                }
            }
        }, true);
        return;
    }

    // ============================================================
    // [D] 메인 플레이어 로직 (부모 창)
    // ============================================================
    if (!isPlayerDomain) return;

    let isPipActive = false;

    // --- 1. CSS 스타일 ---
    GM_addStyle(`
        body.real-pip-mode #player_area {
            position: fixed !important; z-index: 999999 !important;
            width: 480px !important; height: 270px !important;
            bottom: auto !important; right: auto !important;
            border: none !important;
            box-shadow: 0 15px 50px rgba(0,0,0,0.9);
            background: #000; border-radius: 12px; overflow: hidden;
            cursor: move !important;
            transform: translate3d(0,0,0);
            will-change: top, left;
            contain: strict;
        }
        body.real-pip-mode #player_area.is-dragging { transition: none !important; }
        body.real-pip-mode #player_area video,
        body.real-pip-mode #player_area .player_cover {
            pointer-events: none !important; object-fit: contain !important;
            width: 100% !important; height: 100% !important;
        }
        body.real-pip-mode .play_control_box {
            display: block !important; pointer-events: auto !important;
            bottom: 0 !important; position: absolute !important;
            width: 100% !important; background: linear-gradient(to top, rgba(0,0,0,0.8), transparent);
            z-index: 20 !important; cursor: default !important;
        }
        body.real-pip-mode .play_control_box * { pointer-events: auto !important; }

        body.real-pip-mode #web_chatting,
        body.real-pip-mode .header_area,
        body.real-pip-mode .sidebar_area,
        body.real-pip-mode .start_ad_area,
        body.real-pip-mode #action_bar,
        body.real-pip-mode .btn_chat_open,
        body.real-pip-mode .btn_chat_fold,
        body.real-pip-mode .btn_expand,
        body.real-pip-mode button[class*="chat"],
        body.real-pip-mode .chat_layer,
        body.real-pip-mode .btn_sidebar
        { display: none !important; }

        #soop-real-frame {
            display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            z-index: 100; border: none; background: #fff;
        }
        body.real-pip-mode #soop-real-frame { display: block; }
        body.real-pip-mode.iframe-dark #soop-real-frame { background: #141517; }

        #pip-control-bar {
            display: none; position: absolute; top: 0; left: 0; width: 100%; height: 40px;
            background: linear-gradient(to bottom, rgba(0,0,0,0.6), transparent);
            z-index: 1000000; justify-content: flex-end; align-items: center;
            padding-right: 10px; opacity: 0; transition: opacity 0.2s;
            pointer-events: auto !important; cursor: default;
        }
        body.real-pip-mode #player_area:hover #pip-control-bar { opacity: 1; }
        body.real-pip-mode #pip-control-bar { display: flex; }

        .pip-ctrl-btn {
            background: rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.3);
            color: white; margin-left: 8px; padding: 5px 12px;
            border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: bold;
        }
        .pip-ctrl-btn:hover { background: rgba(255,255,255,0.3); }
    `);

    // --- 2. 검색 실행 헬퍼 ---
    function executeSearchUrl(url) {
        if (isPipActive) {
            const frame = document.getElementById('soop-real-frame');
            if (frame) frame.src = url;
        } else {
            startPip(url);
        }
        // 검색 실행 직후 포커스 해제 (드롭다운 닫기 유도)
        if (document.activeElement instanceof HTMLElement) {
            document.activeElement.blur();
        }
    }

    function executeSearchKeyword(keyword) {
        if (!keyword || keyword.trim() === '') return;
        const searchUrl = 'https://www.sooplive.co.kr/search?keyword=' + encodeURIComponent(keyword);
        executeSearchUrl(searchUrl);
    }

    // --- 3. Form Submit 이벤트 ---
    document.addEventListener('submit', function(e) {
        const form = e.target;
        const isSearchForm = (form.action && form.action.includes('search')) ||
                             form.querySelector('input[name="szKeyword"]') ||
                             form.querySelector('input[name="keyword"]');

        if (isSearchForm) {
            if (form.target === '_blank') form.removeAttribute('target');
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();

            const input = form.querySelector('input[type="text"]');
            if (input && input.value) {
                executeSearchKeyword(input.value);
            }
            return false;
        }
    }, true);

    // --- 4. 클릭 이벤트 리스너 ---
    document.addEventListener('click', function(e) {
        const target = e.target;
        const link = target.closest('a');
        const text = (target.innerText || '').replace(/\s/g, '');

        // [A] 검색 버튼 클릭 감지
        const searchBtn = target.closest('.btn_search') ||
                          target.closest('.btn_search_submit') ||
                          (target.tagName === 'BUTTON' && (target.type === 'submit' || target.className.includes('search')));

        if (searchBtn) {
            const header = searchBtn.closest('.header_area') || searchBtn.closest('#header') || searchBtn.closest('header');
            if (header) {
                const input = header.querySelector('input[type="text"]');
                if (input && input.value) {
                    e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
                    executeSearchKeyword(input.value);
                    return;
                }
            }
        }

        // [B] 일반 링크 탐색 로직
        let targetUrl = null;

        if (link && link.href) {
            if (link.href.includes('/live/all')) targetUrl = 'https://www.sooplive.co.kr/live/all';
            else if (link.href.includes('/my/favorite')) targetUrl = 'https://www.sooplive.co.kr/my/favorite';
            else if (link.href.includes('/directory/category')) targetUrl = 'https://www.sooplive.co.kr/directory/category';
            else if (link.classList.contains('logo') || link.id === 'logo') targetUrl = 'https://www.sooplive.co.kr/';
        }

        if (!targetUrl) {
            const isLogo = target.closest('.logo') || target.closest('.header_logo') || target.closest('#logo');
            if (isLogo) targetUrl = 'https://www.sooplive.co.kr/';
            else if (text.includes('즐겨찾기') || text === 'MY') targetUrl = 'https://www.sooplive.co.kr/my/favorite';
            else if (text === '전체') targetUrl = 'https://www.sooplive.co.kr/live/all';
            else if (text === '카테고리') targetUrl = 'https://www.sooplive.co.kr/directory/category';
        }

        if (targetUrl) {
            e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
            executeSearchUrl(targetUrl);
        }
    }, true);


    // --- 5. PIP 제어 ---
    function startPip(url) {
        const player = document.getElementById('player_area');
        if (!player) return alert('플레이어를 찾을 수 없습니다.');

        isPipActive = true;
        document.body.classList.add('real-pip-mode');

        let frame = document.getElementById('soop-real-frame');
        if (!frame) {
            frame = document.createElement('iframe');
            frame.id = 'soop-real-frame';
            document.body.appendChild(frame);
        }
        if(url) frame.src = url;

        player.style.top = (window.innerHeight - 300) + 'px';
        player.style.left = (window.innerWidth - 500) + 'px';

        createControls(player);
        makeDraggable(player);
    }

    function stopPip() {
        isPipActive = false;
        document.body.classList.remove('real-pip-mode');
        const player = document.getElementById('player_area');
        if(player) {
            player.style.top = ''; player.style.left = '';
            player.style.width = ''; player.style.height = '';
        }
    }

    function exitPipMode() {
        const frame = document.getElementById('soop-real-frame');
        const targetUrl = frame ? frame.src : 'https://www.sooplive.co.kr';
        window.location.href = targetUrl;
    }

    // --- 6. 컨트롤 바 ---
    function createControls(player) {
        if (document.getElementById('pip-control-bar')) return;
        const bar = document.createElement('div');
        bar.id = 'pip-control-bar';
        bar.onmousedown = (e) => e.stopPropagation();

        const restoreBtn = document.createElement('button');
        restoreBtn.className = 'pip-ctrl-btn';
        restoreBtn.innerText = '⤢ 복귀';
        restoreBtn.onclick = stopPip;

        const exitBtn = document.createElement('button');
        exitBtn.className = 'pip-ctrl-btn';
        exitBtn.innerText = '✖ 종료';
        exitBtn.style.color = '#ff6b6b';
        exitBtn.style.borderColor = '#ff6b6b';
        exitBtn.onclick = exitPipMode;

        bar.appendChild(restoreBtn);
        bar.appendChild(exitBtn);
        player.appendChild(bar);
    }

    // --- 7. 드래그 ---
    function makeDraggable(elmnt) {
        let startX = 0, startY = 0, initialLeft = 0, initialTop = 0;
        elmnt.onmousedown = dragMouseDown;

        function dragMouseDown(e) {
            if (!isPipActive) return;
            if (e.target.closest('.play_control_box') ||
                e.target.closest('#pip-control-bar') ||
                e.target.tagName === 'INPUT' ||
                e.target.tagName === 'BUTTON') {
                return;
            }
            e.preventDefault();
            elmnt.classList.add('is-dragging');

            startX = e.clientX;
            startY = e.clientY;
            initialLeft = elmnt.offsetLeft;
            initialTop = elmnt.offsetTop;

            document.onmouseup = closeDragElement;
            document.onmousemove = elementDrag;
        }

        function elementDrag(e) {
            e.preventDefault();
            requestAnimationFrame(() => {
                const dx = e.clientX - startX;
                const dy = e.clientY - startY;
                elmnt.style.top = (initialTop + dy) + "px";
                elmnt.style.left = (initialLeft + dx) + "px";
            });
        }

        function closeDragElement() {
            document.onmouseup = null;
            document.onmousemove = null;
            elmnt.classList.remove('is-dragging');
        }
    }

    // [핵심 수정] 메시지 수신 -> 안전하게 포커스만 해제
    window.addEventListener('message', function(e) {
        if (!e.data) return;

        if (e.data.type === 'SOOP_THEME') {
            if (e.data.isDarkMode) document.body.classList.add('iframe-dark');
            else document.body.classList.remove('iframe-dark');
        }

        // [Safety] 배경 클릭 신호 수신 시
        if (e.data.type === 'SOOP_BG_CLICK_SIMPLE') {
            // 1. 입력창에 가있는 커서(포커스)를 뺍니다. (이것만으로 대부분의 드롭다운은 닫힘)
            if (document.activeElement && document.activeElement.tagName === 'INPUT') {
                document.activeElement.blur();
            }

            // 2. 혹시 안 닫히면, 'PIP 컨트롤 바'를 가짜 클릭합니다.
            // (이건 우리 스크립트가 만든 요소라 사이트 내비게이션에 절대 영향 안 줌)
            const ctrlBar = document.getElementById('pip-control-bar');
            if (ctrlBar) ctrlBar.click();
        }
    });

})();