Panopto Caption Copy

QU video caption copy

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Panopto Caption Copy
// @namespace    http://tampermonkey.net/
// @version      6.1
// @description  QU video caption copy
// @author       Gemini
// @match        *://*.panopto.com/*
// @match        *://queensu.ca.panopto.com/*
// @grant        none
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    console.log('Panopto Caption Dot Started...');

    // ==========================================
    // 1. 核心网络拦截 (逻辑不变)
    // ==========================================
    const originalFetch = window.fetch;
    window.fetch = async function(...args) {
        const response = await originalFetch.apply(this, args);
        const url = (args[0] instanceof Request ? args[0].url : args[0]) || '';

        if (url.includes('DeliveryInfo')) {
            const clone = response.clone();
            clone.text().then(text => processResponse(url, text)).catch(() => {});
        }
        return response;
    };

    const originalOpen = XMLHttpRequest.prototype.open;
    const originalSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.open = function(method, url) {
        this._hookUrl = url;
        return originalOpen.apply(this, arguments);
    };
    XMLHttpRequest.prototype.send = function(body) {
        this.addEventListener('load', function() {
            if (this._hookUrl && this._hookUrl.includes('DeliveryInfo')) {
                processResponse(this._hookUrl, this.responseText);
            }
        });
        return originalSend.apply(this, arguments);
    };

    // ==========================================
    // 2. 数据处理
    // ==========================================
    function processResponse(url, text) {
        if (text.trim().startsWith('<')) return; // 忽略 XML 报错

        try {
            const json = JSON.parse(text);
            let captions = null;
            if (Array.isArray(json)) {
                captions = json;
            } else if (json.Delivery && json.Delivery.Captions) {
                captions = json.Delivery.Captions;
            }

            if (captions && captions.length > 0) {
                console.log(`捕获字幕: ${captions.length} 行`);
                createBlueDot(captions);
            }
        } catch (e) {}
    }

    // ==========================================
    // 3. 极简蓝点 UI (核心修改)
    // ==========================================
    function createBlueDot(captions) {
        // 防止重复创建
        if (document.getElementById('caption-blue-dot')) return;

        const dot = document.createElement('div');
        dot.id = 'caption-blue-dot';

        // 初始样式:半透明蓝点
        dot.style.cssText = `
            position: fixed;
            top: 20px;
            left: 20px;
            width: 12px;
            height: 12px;
            background-color: #2196F3; /* 蓝色 */
            border-radius: 50%;
            opacity: 0.3; /* 半透明 */
            z-index: 2147483647;
            cursor: pointer;
            transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
            box-shadow: 0 0 0 rgba(33, 150, 243, 0);
            display: flex;
            align-items: center;
            justify-content: center;
            color: transparent;
            font-family: sans-serif;
            font-size: 12px;
            white-space: nowrap;
            overflow: hidden;
        `;

        // 鼠标悬停效果:变大、变亮、显示提示
        dot.onmouseenter = () => {
            dot.style.opacity = '1';
            dot.style.width = 'auto'; // 宽度自适应
            dot.style.height = '30px';
            dot.style.borderRadius = '15px'; // 变成胶囊形状
            dot.style.padding = '0 12px';
            dot.style.boxShadow = '0 4px 10px rgba(33, 150, 243, 0.4)';
            dot.style.color = 'white';
            dot.innerText = `复制字幕 (${captions.length})`;
        };

        // 鼠标移出效果:变回小圆点
        dot.onmouseleave = () => {
            // 如果刚复制完,延迟一点再变回去,体验更好
            if (dot.innerText.includes('已复制')) return;
            resetDotStyle();
        };

        function resetDotStyle() {
            dot.style.opacity = '0.3';
            dot.style.width = '12px';
            dot.style.height = '12px';
            dot.style.borderRadius = '50%';
            dot.style.padding = '0';
            dot.style.boxShadow = 'none';
            dot.style.color = 'transparent';
            dot.innerText = '';
        }

        // 成功反馈动画
        function showSuccess() {
            dot.style.backgroundColor = '#4CAF50'; // 变绿
            dot.innerText = '✅ 已复制!';

            setTimeout(() => {
                dot.style.backgroundColor = '#2196F3'; // 变回蓝
                if (!dot.matches(':hover')) {
                    resetDotStyle();
                } else {
                    dot.innerText = `复制字幕 (${captions.length})`;
                }
            }, 1500);
        }

        // 兼容性复制函数 (Fallback)
        function fallbackCopyTextToClipboard(text) {
            var textArea = document.createElement("textarea");
            textArea.value = text;

            // 避免滚动到底部
            textArea.style.top = "0";
            textArea.style.left = "0";
            textArea.style.position = "fixed";
            textArea.style.opacity = "0"; // 不可见

            document.body.appendChild(textArea);
            textArea.focus();
            textArea.select();

            try {
                var successful = document.execCommand('copy');
                if (successful) {
                    showSuccess();
                } else {
                    alert('复制失败:传统方法也被拦截。请尝试手动下载。');
                }
            } catch (err) {
                console.error('Fallback copy failed', err);
                alert('复制失败,请检查浏览器权限');
            }

            document.body.removeChild(textArea);
        }

        // 点击事件:复制纯文本
        dot.onclick = () => {
            const fullText = captions
                .filter(c => c.Caption && c.Caption.trim())
                .map(c => c.Caption.trim())
                .join(' '); // 用空格连接

            // 优先尝试现代 API
            if (navigator.clipboard && navigator.clipboard.writeText) {
                navigator.clipboard.writeText(fullText).then(() => {
                    showSuccess();
                }).catch(err => {
                    console.warn('Clipboard API failed, attempting fallback...', err);
                    // 如果现代 API 失败(比如因为权限),使用传统方法
                    fallbackCopyTextToClipboard(fullText);
                });
            } else {
                // 如果不支持现代 API,直接使用传统方法
                fallbackCopyTextToClipboard(fullText);
            }
        };

        document.body.appendChild(dot);
    }

})();