Panopto Caption Copy

QU video caption copy

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);
    }

})();