SOOP 클립 및 라이브 깔끔하게 캡쳐

Adds a controller visibility toggle button and capture button to hide the player controller section when taking screenshots in the SOOP Live and SOOP Clip windows.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         SOOP 클립 및 라이브 깔끔하게 캡쳐
// @name:ko-KR   SOOP 클립 및 라이브 깔끔하게 캡쳐
// @name:en-US   SOOP Live & Clip Clean Capture Helper
// @name:ja-JP   SOOP 切り抜き&生配信 すっきりスクショ
// @namespace    http://tampermonkey.net/
// @version      2.1.2
// @license      MIT
// @description       Adds a controller visibility toggle button and capture button to hide the player controller section when taking screenshots in the SOOP Live and SOOP Clip windows.
// @description:ko-KR SOOP 라이브 및 클립 창에서 화면 캡쳐시 플레이어 컨트롤러 부분을 보이지 않게 할 수 있도록 컨트롤러 비활성화/캡쳐 버튼을 추가합니다.
// @description:ja-JP SOOPのライブやクリップ画面で、プレイヤーコントロールを非表示にするボタンとキャプチャボタンを追加します。
// @author       Linseed, Gemini
// @match        https://stbbs.sooplive.co.kr/*
// @match        https://play.sooplive.co.kr/*
// @match        https://vod.sooplive.co.kr/*
// @exclude      https://mul.live/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_addValueChangeListener
// @require      https://github.com/PRO-2684/GM_config/releases/download/v1.2.2/config.js#md5=fca1967de605996e44d14d2eab403706
// ==/UserScript==

(function() {
    'use strict';

    const configDesc = {
        "$default": {
            autoClose: false
        },
        saveMethod: {
            name: "Save as",
            title: "저장 방식을 설정합니다.",
            type: "enum",
            options: ["Clipboard", "File", "Both"],
        },
        shownUIs:{
            name: "Exclude UI from hiding",
            title: "잠금 상태에서 숨기지 않을 UI를 선택합니다. 이걸 어떻게 설정하든, 캡쳐 버튼을 누른 순간은 모든 방해요소가 숨겨집니다",
            type: "folder",
            items: {
                hideControl: {
                    name: "Controls (play, pause, etc)",
                    title: "플레이어 컨트롤러 (재생, 일시정지 등) UI입니다.",
                    type: "enum",
                    options: ["Hide all", "Hide only gradient", "Show all"],
                    default: 0
                },
                hidePlayerInfo: {
                    name: "Player info (profile pic, title, viewers)",
                    title: "스트리머 프로필, 방송 제목, 시청자 수 등 꽉찬 화면에서 영상을 가리는 UI입니다.",
                    type: "enum",
                    options: ["Hide all", "Hide only gradient", "Show all"],
                    default: 0
                },

                hideViewControl: {
                    name: "Chat button & Related videos",
                    title: "전체 / 꽉찬 화면 시 우상단에 표시되는 채팅창 열기 버튼 및 관련 영상 버튼 UI입니다.",
                    type: "enum",
                    options: ["Hide", "Show"],
                    default: 0
                },
                hideCatchUI: {
                    name: "Catch making UI(wip)",
                    title: "Catch 제작 시, 제외되는 영역을 어둡게 하는 요소입니다.",
                    type: "enum",
                    options: ["Hide", "Show"],
                    default: 1
                }
            }
        }
    }
    const config = new GM_config(configDesc);

    // --- 스크립트 설정 ---
    const CHECK_INTERVAL = 500; // 플레이어 요소를 찾기 위한 반복 확인 간격 (ms)

    /**
     * 필요한 DOM 요소들이 로드될 때까지 기다립니다.
     */
    const waitForElements = setInterval(() => {
        const videoLayer = document.getElementById('videoLayer');
        const ctrlBox = document.querySelector('.player_ctrlBox');
        const titleElement = document.querySelector('.u_clip_title');
        const nicknameElement = document.querySelector('.nickname');
        const playerInfo = document.getElementById('player_info');
        const viewCtrl = document.querySelector('.view_ctrl');

        if (videoLayer && ctrlBox && (titleElement || nicknameElement)) {
            clearInterval(waitForElements);
            main(videoLayer, ctrlBox, titleElement, nicknameElement, playerInfo, viewCtrl);
        }
    }, CHECK_INTERVAL);

    /**
     * 스크립트의 메인 로직을 실행합니다.
     */
    function main(videoLayer, ctrlBox, titleElement, nicknameElement, playerInfo, viewCtrl) {
        // Define elements to be controlled by lock state
        const controlledElements = [
            { element: ctrlBox, type: 'control', configKey: 'shownUIs.hideControl' },
            { element: playerInfo, type: 'info', configKey: 'shownUIs.hidePlayerInfo' },
            { element: viewCtrl, type: 'control', configKey: 'shownUIs.hideViewControl' }
        ];

        let isLocked = false;

        // Insert containers containing lock and capture buttons.
        const container = document.createElement('div');
        container.id = 'soop-script-container';
        
        if (titleElement) {
            titleElement.after(container);
        } else if (nicknameElement) {
            nicknameElement.after(container);
        }

        // Create buttons inside the container.
        const lockButton = document.createElement('button');
        lockButton.id = 'lockBtn';
        lockButton.textContent = '🔓';
        lockButton.title = '컨트롤러 잠금/해제 (Alt + L)';
        container.appendChild(lockButton);

        const captureButton = document.createElement('button');
        captureButton.id = 'captureBtn';
        captureButton.textContent = '📷';
        captureButton.title = '현재 화면 캡쳐 (Alt + P)';
        container.appendChild(captureButton);

        GM_addStyle(`
            #soop-script-container { display: inline-flex; gap: 8px; margin-left: 10px; vertical-align: middle; }
            #soop-script-container button { padding: 4px 8px; font-size: 16px; border: 1px solid #ccc; border-radius: 6px; background-color: rgba(240, 240, 240, 0.85); cursor: pointer; box-shadow: 0 1px 3px rgba(0,0,0,0.1); transition: all 0.2s ease-in-out; line-height: 1; }
            #soop-script-container button:hover { background-color: rgba(255, 255, 255, 1); transform: translateY(-1px); }
            #player .player_ctrlBox.no-gradient { background: none !important; }
            #player_info.no-gradient { background: none !important; }
            #player .view_ctrl.no-gradient { background: none !important; }
        `);

        const setLockState = (locked) => {
            isLocked = locked;
            lockButton.textContent = isLocked ? '🔒' : '🔓';
            
            if (!isLocked) {
                // Show everything when unlocked
                controlledElements.forEach(({ element }) => {
                    element.style.display = '';
                    // Only try to remove class if it exists
                    element.classList.remove('no-gradient');
                });
                return;
            }

            // Update visibility based on configuration when locked
            controlledElements.forEach(({ element, configKey }) => {
                const setting = config.get(configKey);

                // Reset styles before applying new ones
                element.style.display = '';
                element.classList.remove('no-gradient');

                switch (setting) {
                    case 0: // Hide all
                        element.style.display = 'none';
                        element.classList.add('no-gradient');
                        break;
                    case 1: // Hide only background gradient
                        element.classList.add('no-gradient');
                        break;
                    case 2: // Show all
                        // Do nothing, already reset
                        break;
                }
            });
        };

        lockButton.addEventListener('click', () => setLockState(!isLocked));
        config.addEventListener("set", (_e) => setLockState(isLocked));  // update UI when config changes

        captureButton.addEventListener('click', async () => {
            const originalLockState = isLocked;

                //if (!originalLockState) {
                //    setLockState(true);
                //    await new Promise(resolve => setTimeout(resolve, 100));
                //}

            try {
                const videoElement = videoLayer.querySelector('video');
                if (!videoElement) throw new Error('플레이어에서 <video> 요소를 찾을 수 없습니다.');

                const canvas = document.createElement('canvas');
                canvas.width = videoElement.videoWidth;
                canvas.height = videoElement.videoHeight;
                const ctx = canvas.getContext('2d');
                ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);

                // --- 설정값에 따라 저장 방식 분기 ---
                const saveMethod = config.get('saveMethod');

                const saveToClipboard = (canvasEl) => {
                    return new Promise((resolve, reject) => {
                        canvasEl.toBlob(async (blob) => {
                            if (blob) {
                                try {
                                    await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
                                    resolve();
                                } catch (err) {
                                    reject(err);
                                }
                            } else {
                                reject(new Error('Canvas toBlob returned null.'));
                            }
                        }, 'image/png');
                    });
                };

                const saveToFile = (canvasEl) => {
                    const link = document.createElement('a');
                    const timestamp = new Date().toISOString().slice(0, 19).replace(/[-T:]/g, '');
                    link.download = `soop-capture-${timestamp}.png`;
                    link.href = canvasEl.toDataURL('image/png');
                    link.click();
                };

                if (saveMethod == 0 || saveMethod == 2) {
                    try {
                        await saveToClipboard(canvas);
                    } catch(e) {
                        console.error('클립보드 저장 실패:', e);
                        alert('클립보드 저장에 실패했습니다. 브라우저 콘솔을 확인해주세요.');
                    }
                }
                if (saveMethod == 1 || saveMethod == 2) {
                    saveToFile(canvas);
                }

            } catch (error) {
                console.error('스크립트 캡쳐 오류:', error);
                alert('화면 캡쳐에 실패했습니다. 비디오가 다른 도메인에서 재생되는 경우(CORS) 캡쳐가 불가능할 수 있습니다. 브라우저 콘솔을 확인해주세요.');
            } finally {
                // Restore original UI state
                if (!originalLockState) {
                    setLockState(false);
                }
                // equivalent to: setLockState(originalLockState), but stated in this way for clarity
            }
        });

        document.addEventListener('keydown', (event) => {
            if (!event.altKey) return;
            switch (event.key.toLowerCase()) {
                case 'l':
                    event.preventDefault();
                    lockButton.click();
                    break;
                case 'c':
                    event.preventDefault();
                    captureButton.click();
                    break;
            }
        });
    }
})();