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

})();