QU video caption copy
// ==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);
}
})();