// ==UserScript==
// @name B站字幕下载器
// @namespace https://space.bilibili.com/398910090
// @version 1.0
// @description 下载B站视频字幕,支持用户上传字幕和AI字幕,支持JSON、SRT、VTT格式
// @match *://*.bilibili.com/video/*
// @match *://*.bilibili.com/bangumi/play/*
// @icon https://www.bilibili.com/favicon.ico
// @grant GM_xmlhttpRequest
// @grant GM_download
// ==/UserScript==
(function() {
'use strict';
// 字幕格式枚举
const SUBTITLE_FORMATS = {
JSON: 'json',
SRT: 'srt',
VTT: 'vtt'
};
// 存储拦截到的字幕URL
let interceptedSubtitleUrls = [];
// 添加全局样式
function addGlobalStyles() {
const style = document.createElement('style');
style.textContent = `
/* 全局样式,确保下拉选项可见 */
.bilibili-subtitle-download-confirm select {
background-color: rgba(25, 26, 27, 0.98) !important;
color: white !important;
}
.bilibili-subtitle-download-confirm select option {
background-color: rgba(25, 26, 27, 0.98) !important;
color: white !important;
}
/* InfoBar 样式 */
.bilibili-subtitle-infobar {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(25, 26, 27, 0.98);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
padding: 12px 16px;
color: white;
font-size: 14px;
z-index: 2147483647;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.8);
backdrop-filter: blur(10px);
min-width: 200px;
max-width: 400px;
text-align: center;
transition: all 0.3s ease;
}
/* InfoBar 不同类型的样式 */
.bilibili-subtitle-infobar.info {
border-left: 4px solid #00a1d6;
}
.bilibili-subtitle-infobar.success {
border-left: 4px solid #52c41a;
}
.bilibili-subtitle-infobar.warning {
border-left: 4px solid #faad14;
}
.bilibili-subtitle-infobar.error {
border-left: 4px solid #f5222d;
}
`;
document.head.appendChild(style);
}
// 显示信息提示条 (InfoBar)
function showInfoBar(message, type = 'info', duration = 3000) {
// 移除已存在的InfoBar
const existingInfoBar = document.querySelector('.bilibili-subtitle-infobar');
if (existingInfoBar) {
existingInfoBar.remove();
}
// 创建新的InfoBar
const infoBar = document.createElement('div');
infoBar.className = `bilibili-subtitle-infobar ${type}`;
infoBar.textContent = message;
// 添加到页面
document.body.appendChild(infoBar);
// 设置自动移除
if (duration > 0) {
setTimeout(() => {
if (infoBar.parentNode) {
// 添加淡出动画
infoBar.style.opacity = '0';
infoBar.style.transform = 'translate(-50%, -50%) scale(0.9)';
// 动画完成后移除元素
setTimeout(() => {
if (infoBar.parentNode) {
infoBar.remove();
}
}, 300);
}
}, duration);
}
// 点击关闭
infoBar.addEventListener('click', () => {
if (infoBar.parentNode) {
infoBar.remove();
}
});
return infoBar;
}
// 初始化函数
function init() {
// 添加全局样式
addGlobalStyles();
// 启动网络请求拦截
setupNetworkInterception();
// 等待字幕选择器加载
waitForSubtitleSelector();
}
// 等待字幕选择器加载
function waitForSubtitleSelector() {
let timeoutId;
const checkInterval = setInterval(() => {
// 先尝试查找字幕选择器面板
const subtitlePanel = document.querySelector('.bpx-player-ctrl-subtitle-menu-left') ||
document.querySelector('.bpx-player-ctrl-subtitle-menu-origin') ||
// 如果找不到面板,尝试查找字幕选项本身
document.querySelector('.bpx-player-ctrl-subtitle-language-item');
if (subtitlePanel) {
clearInterval(checkInterval);
clearTimeout(timeoutId);
// 如果找到的是字幕选项而不是面板,尝试找到其父面板
const actualPanel = subtitlePanel.closest('.bpx-player-ctrl-subtitle-menu-left') ||
subtitlePanel.closest('.bpx-player-ctrl-subtitle-menu-origin') ||
subtitlePanel.parentElement;
createDownloadInterface(actualPanel);
}
}, 500);
// 设置超时
timeoutId = setTimeout(() => {
clearInterval(checkInterval);
console.error('字幕选择器未找到,脚本可能无法正常工作');
}, 30000); // 延长超时时间到30秒
}
// 设置网络请求拦截
function setupNetworkInterception() {
// 拦截XMLHttpRequest
const originalXHROpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
// 检查是否是字幕请求
if (isSubtitleUrl(url)) {
interceptedSubtitleUrls.push(url);
}
return originalXHROpen.apply(this, arguments);
};
// 拦截fetch
const originalFetch = window.fetch;
window.fetch = function(url, options) {
// 检查是否是字幕请求
if (typeof url === 'string' && isSubtitleUrl(url)) {
interceptedSubtitleUrls.push(url);
} else if (typeof url === 'object' && url.url && isSubtitleUrl(url.url)) {
interceptedSubtitleUrls.push(url.url);
}
return originalFetch.apply(this, arguments);
};
}
// 检查是否是字幕URL
function isSubtitleUrl(url) {
return url.includes('subtitle') || url.includes('ai_subtitle');
}
// 创建下载界面
function createDownloadInterface(subtitlePanel) {
// 添加下载按钮到每个字幕选项
addDownloadButtons();
// 监听字幕列表变化,动态添加下载按钮
const observer = new MutationObserver(() => {
addDownloadButtons();
});
observer.observe(subtitlePanel, { childList: true, subtree: true });
// 15秒后停止监听
setTimeout(() => {
observer.disconnect();
}, 15000);
}
// 添加下载按钮
function addDownloadButtons() {
const subtitleItems = document.querySelectorAll('.bpx-player-ctrl-subtitle-language-item');
if (subtitleItems.length === 0) {
console.error('未找到字幕选项');
return;
}
subtitleItems.forEach(item => {
// 避免重复添加
if (item.querySelector('.bilibili-subtitle-download-btn')) {
return;
}
// 创建下载按钮
const downloadBtn = document.createElement('button');
downloadBtn.className = 'bilibili-subtitle-download-btn';
downloadBtn.textContent = '下载';
downloadBtn.style.cssText = `
/* 按钮背景色 - 半透明深灰色 */
background: transparent;
/* 移除边框 */
border: none;
/* 文字颜色 */
color: white;
/* 鼠标悬停时的指针样式 */
cursor: pointer;
/* 字体大小 - 相对单位,支持DPI缩放 */
font-size: 0.85em;
/* 内边距 - 垂直方向较小,确保背景高度与文字平齐 */
padding: 0.1em 0.5em;
/* 外边距 - 左侧间距,与字幕文本保持距离 */
margin: 0 0 0 0.6em;
/* 圆角边框 */
border-radius: 3px;
/* 过渡动画 - 所有属性变化时的平滑过渡 */
transition: all 0.2s ease;
/* 移除下划线 */
text-decoration: none;
/* 移除焦点轮廓 */
outline: none;
/* 最小宽度 - 允许按钮收缩到内容宽度 */
min-width: 0;
/* 文字居中 */
text-align: center;
/* 显示方式 - 使用flex布局确保文字垂直居中 */
display: inline-flex;
/* 垂直对齐 - 文字在按钮内垂直居中 */
align-items: center;
/* 水平对齐 - 文字在按钮内水平居中 */
justify-content: center;
/* 行高 - 控制文字行高,影响按钮整体高度 */
line-height: 1.6;
/* 高度 - 自适应内容 */
height: auto;
/* 最小高度 - 自适应内容 */
min-height: auto;
/* 垂直对齐 - 与相邻元素(字幕文本)垂直居中对齐 */
vertical-align: middle;
/* 盒模型 - 确保padding不会增加按钮总宽度 */
box-sizing: border-box;
/* 定位 - 相对定位,用于微调位置 */
position: relative;
/* 顶部偏移 - 0表示不偏移,确保与字幕文本对齐 */
top: 0;
`;
// 绑定点击事件
downloadBtn.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止事件冒泡,避免触发字幕选择
showFormatMenu(e.target);
});
// 添加悬停效果
downloadBtn.addEventListener('mouseenter', () => {
// 鼠标悬停时背景色变为半透明蓝色(B站主题色)
// downloadBtn.style.backgroundColor = 'rgba(0, 161, 214, 0.3)';
// 鼠标悬停时文字颜色变为蓝色(B站主题色)
downloadBtn.style.color = '#00a1d6';
});
downloadBtn.addEventListener('mouseleave', () => {
// 鼠标离开时恢复原始背景色(半透明深灰色)
// downloadBtn.style.backgroundColor = 'rgba(35, 37, 39, 0.8)';
// 鼠标离开时恢复原始文字颜色(白色)
downloadBtn.style.color = 'white';
});
// 添加到字幕选项
item.appendChild(downloadBtn);
});
}
// 显示格式选择菜单
function showFormatMenu(button) {
// 移除已存在的菜单
const existingMenu = document.querySelector('.bilibili-subtitle-download-menu');
if (existingMenu) {
existingMenu.remove();
}
// 创建菜单
const menu = document.createElement('div');
menu.className = 'bilibili-subtitle-download-menu';
menu.style.cssText = `
position: absolute;
background-color: rgba(25, 26, 27, 0.98);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
padding: 8px 0;
z-index: 2147483647;
font-size: 13px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.6);
min-width: 160px;
backdrop-filter: blur(10px);
`;
// 获取按钮位置
const rect = button.getBoundingClientRect();
// 计算菜单位置,确保不会超出视窗
let left = rect.left + window.pageXOffset;
let top = rect.bottom + window.pageYOffset;
// 如果菜单右侧超出视窗,调整左位置
const menuWidth = 160;
if (left + menuWidth > window.innerWidth + window.pageXOffset) {
left = window.innerWidth + window.pageXOffset - menuWidth - 10;
}
// 如果菜单底部超出视窗,调整上位置
const menuHeight = 115; // 估算高度
if (top + menuHeight > window.innerHeight + window.pageYOffset) {
top = rect.top + window.pageYOffset - menuHeight - 5;
}
menu.style.left = `${left}px`;
menu.style.top = `${top}px`;
// 添加菜单标题
const menuTitle = document.createElement('div');
menuTitle.textContent = '选择下载格式';
menuTitle.style.cssText = `
padding: 0 12px 8px 12px;
color: rgba(255, 255, 255, 0.6);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 4px;
`;
menu.appendChild(menuTitle);
// 添加格式选项
const formats = [
{ name: 'JSON (原始格式)', value: SUBTITLE_FORMATS.JSON, icon: '📄' },
{ name: 'SRT (SubRip)', value: SUBTITLE_FORMATS.SRT, icon: '📝' },
{ name: 'VTT (WebVTT)', value: SUBTITLE_FORMATS.VTT, icon: '🌐' }
];
formats.forEach(format => {
const option = document.createElement('div');
option.innerHTML = `${format.icon} ${format.name}`;
option.style.cssText = `
padding: 8px 12px;
color: white;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
`;
option.addEventListener('mouseenter', () => {
option.style.backgroundColor = 'rgba(0, 161, 214, 0.2)';
option.style.color = '#00a1d6';
});
option.addEventListener('mouseleave', () => {
option.style.backgroundColor = 'transparent';
option.style.color = 'white';
});
option.addEventListener('click', () => {
downloadSubtitle(format.value);
menu.remove();
});
menu.appendChild(option);
});
// 添加到页面
document.body.appendChild(menu);
// 点击页面其他地方关闭菜单
document.addEventListener('click', function closeMenu(e) {
if (!menu.contains(e.target) && e.target !== button) {
menu.remove();
document.removeEventListener('click', closeMenu);
}
});
// 按ESC键关闭菜单
document.addEventListener('keydown', function closeMenuOnEsc(e) {
if (e.key === 'Escape') {
menu.remove();
document.removeEventListener('keydown', closeMenuOnEsc);
document.removeEventListener('click', closeMenu);
}
});
}
// 获取字幕URL
function getSubtitleUrls() {
const urls = [];
// 1. 从初始脚本标签中提取
const scripts = document.querySelectorAll('script');
scripts.forEach(script => {
const content = script.textContent;
if (!content) return;
// 匹配用户上传字幕URL (包含bfs/subtitle路径)
const userSubtitleMatch = content.match(/https?:\/\/[^\s"]*subtitle\/[^\s"]*\.json\?auth_key=[^\s"]*/g);
if (userSubtitleMatch) {
urls.push(...userSubtitleMatch);
}
// 匹配AI字幕URL (包含bfs/ai_subtitle路径)
const aiSubtitleMatch = content.match(/https?:\/\/[^\s"]*ai_subtitle\/[^\s"]*\?auth_key=[^\s"]*/g);
if (aiSubtitleMatch) {
urls.push(...aiSubtitleMatch);
}
});
// 2. 添加拦截到的URL
urls.push(...interceptedSubtitleUrls);
// 3. 去重并过滤有效的字幕URL
const uniqueUrls = [...new Set(urls)].filter(url => {
return url && isSubtitleUrl(url) && url.includes('auth_key');
});
return uniqueUrls;
}
// 获取视频标题
function getVideoTitle() {
// 尝试从不同位置获取视频标题
const titleElements = [
'.video-title h1',
'.media-title',
'.bangumi-title'
];
for (const selector of titleElements) {
const element = document.querySelector(selector);
if (element) {
return element.textContent.trim();
}
}
// 如果都没找到,使用默认标题
return 'bilibili_subtitle';
}
// 下载字幕
function downloadSubtitle(format) {
const urls = getSubtitleUrls();
console.log('检测到的字幕URL:', urls);
if (urls.length === 0) {
console.error('未找到字幕URL');
showInfoBar('未找到字幕URL,请先选择字幕(点击字幕按钮),然后再重试下载', 'error');
return;
}
// 显示正在下载提示
showInfoBar('正在下载字幕数据...', 'info', 0);
// 使用第一个找到的字幕URL
const subtitleUrl = urls[0];
const videoTitle = getVideoTitle();
// 获取字幕数据
GM_xmlhttpRequest({
method: 'GET',
url: subtitleUrl,
onload: function(response) {
// 移除正在下载的提示
const loadingInfoBar = document.querySelector('.bilibili-subtitle-infobar.info');
if (loadingInfoBar && loadingInfoBar.textContent.includes('正在下载字幕数据')) {
loadingInfoBar.remove();
}
try {
const subtitleData = JSON.parse(response.responseText);
let content, filename, mimeType;
// 移除视频标题中可能存在的扩展名
const titleWithoutExt = videoTitle.replace(/\.[^/.]+$/, '');
// 根据选择的格式转换字幕
switch (format) {
case SUBTITLE_FORMATS.JSON:
content = JSON.stringify(subtitleData, null, 2);
filename = `${titleWithoutExt}.json`;
mimeType = 'application/json';
break;
case SUBTITLE_FORMATS.SRT:
content = jsonToSrt(subtitleData);
filename = `${titleWithoutExt}.srt`;
mimeType = 'text/srt';
break;
case SUBTITLE_FORMATS.VTT:
content = jsonToVtt(subtitleData);
filename = `${titleWithoutExt}.vtt`;
mimeType = 'text/vtt';
break;
}
// 下载字幕文件
downloadFile(content, filename, mimeType, subtitleData, format);
} catch (error) {
console.error('处理字幕数据时出错:', error);
showInfoBar('处理字幕数据时出错:' + error.message, 'error');
}
},
onerror: function() {
// 移除正在下载的提示
const loadingInfoBar = document.querySelector('.bilibili-subtitle-infobar.info');
if (loadingInfoBar && loadingInfoBar.textContent.includes('正在下载字幕数据')) {
loadingInfoBar.remove();
}
console.error('下载字幕数据失败');
showInfoBar('下载字幕数据失败', 'error');
}
});
}
// 获取字幕主体数据(兼容用户上传字幕和AI字幕)
function getSubtitleBody(subtitleData) {
if (subtitleData && subtitleData.body) {
return subtitleData.body;
} else if (subtitleData && subtitleData.data && subtitleData.data.body) {
return subtitleData.data.body;
} else {
throw new Error('无法找到字幕主体数据');
}
}
// JSON转SRT格式
function jsonToSrt(subtitleData) {
let srt = '';
const body = getSubtitleBody(subtitleData);
body.forEach((item, index) => {
srt += `${index + 1}\n`;
srt += `${formatTime(item.from)} --> ${formatTime(item.to)}\n`;
srt += `${item.content}\n\n`;
});
return srt;
}
// JSON转VTT格式
function jsonToVtt(subtitleData) {
let vtt = 'WEBVTT\n\n';
const body = getSubtitleBody(subtitleData);
body.forEach((item, index) => {
vtt += `${index + 1}\n`;
vtt += `${formatTime(item.from).replace(',', '.')} --> ${formatTime(item.to).replace(',', '.')}\n`;
vtt += `${item.content}\n\n`;
});
return vtt;
}
// 格式化时间
function formatTime(seconds) {
const date = new Date(seconds * 1000);
const hours = date.getUTCHours().toString().padStart(2, '0');
const minutes = date.getUTCMinutes().toString().padStart(2, '0');
const secs = date.getUTCSeconds().toString().padStart(2, '0');
const milliseconds = Math.floor(date.getUTCMilliseconds()).toString().padStart(3, '0');
return `${hours}:${minutes}:${secs},${milliseconds}`;
}
// 显示下载确认窗口
function showDownloadConfirm(content, filename, mimeType, originalData, currentFormat) {
// 移除已存在的确认窗口
const existingConfirm = document.querySelector('.bilibili-subtitle-download-confirm');
if (existingConfirm) {
existingConfirm.remove();
}
// 从文件名中提取基本名称(不含扩展名)
const baseFilename = filename.replace(/\.[^/.]+$/, '');
// 当前格式信息
let currentContent = content;
let currentMimeType = mimeType;
let currentFilename = filename;
let currentFileSize = new Blob([content], { type: mimeType }).size;
let currentFileSizeStr = currentFileSize < 1024 ? `${currentFileSize} B` : `${(currentFileSize / 1024).toFixed(2)} KB`;
// 创建确认窗口
const confirmWindow = document.createElement('div');
confirmWindow.className = 'bilibili-subtitle-download-confirm';
confirmWindow.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(25, 26, 27, 0.98);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
padding: 20px;
z-index: 2147483647;
font-size: 13px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.8);
backdrop-filter: blur(10px);
min-width: 300px;
color: white;
`;
// 创建标题
const title = document.createElement('div');
title.textContent = '下载字幕';
title.style.cssText = `
font-size: 16px;
font-weight: bold;
margin-bottom: 16px;
text-align: center;
`;
confirmWindow.appendChild(title);
// 创建文件名输入
const filenameLabel = document.createElement('div');
filenameLabel.textContent = '文件名:';
filenameLabel.style.cssText = `
margin-bottom: 8px;
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
`;
confirmWindow.appendChild(filenameLabel);
const filenameInput = document.createElement('input');
filenameInput.type = 'text';
filenameInput.value = filename;
filenameInput.style.cssText = `
width: 100%;
padding: 8px;
margin-bottom: 16px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 4px;
color: white;
font-size: 13px;
box-sizing: border-box;
`;
confirmWindow.appendChild(filenameInput);
// 创建格式选择下拉框
const formatLabel = document.createElement('div');
formatLabel.textContent = '格式:';
formatLabel.style.cssText = `
margin-bottom: 8px;
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
`;
confirmWindow.appendChild(formatLabel);
const formatSelect = document.createElement('select');
formatSelect.style.cssText = `
width: 100%;
padding: 8px;
margin-bottom: 16px;
background: rgba(25, 26, 27, 0.98);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 4px;
color: white;
font-size: 13px;
box-sizing: border-box;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
cursor: pointer;
`;
// 创建文件大小信息
const sizeInfo = document.createElement('div');
sizeInfo.innerHTML = `文件大小: ${currentFileSizeStr}`;
sizeInfo.style.cssText = `
margin-bottom: 16px;
text-align: center;
font-size: 12px;
`;
confirmWindow.appendChild(sizeInfo);
// 为下拉框创建包装器以实现自定义样式
const selectWrapper = document.createElement('div');
selectWrapper.style.cssText = `
position: relative;
width: 100%;
`;
// 定义格式选项
const formatOptions = [
{ value: SUBTITLE_FORMATS.JSON, name: 'JSON', mimeType: 'application/json' },
{ value: SUBTITLE_FORMATS.SRT, name: 'SRT', mimeType: 'text/srt' },
{ value: SUBTITLE_FORMATS.VTT, name: 'VTT', mimeType: 'text/vtt' }
];
// 添加格式选项
formatOptions.forEach(option => {
const opt = document.createElement('option');
opt.value = option.value;
opt.textContent = option.name;
opt.setAttribute('data-mime', option.mimeType);
if (option.value === currentFormat) {
opt.selected = true;
}
formatSelect.appendChild(opt);
});
// 将下拉框移动到包装器中,插入到sizeInfo之前
confirmWindow.insertBefore(selectWrapper, sizeInfo);
selectWrapper.appendChild(formatSelect);
// 添加下载方式选项
const downloadMethodLabel = document.createElement('div');
downloadMethodLabel.textContent = '下载方式:';
downloadMethodLabel.style.cssText = `
margin-bottom: 8px;
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
`;
confirmWindow.appendChild(downloadMethodLabel);
const downloadMethods = document.createElement('div');
downloadMethods.style.cssText = `
margin-bottom: 20px;
display: flex;
flex-direction: column;
gap: 6px;
`;
confirmWindow.appendChild(downloadMethods);
// 定义下载方式选项
const downloadOptions = [
{ value: 'direct', name: '直接下载' },
{ value: 'newtab', name: '新标签页打开' },
{ value: 'clipboard', name: '复制到剪贴板' }
];
// 创建单选按钮组
const downloadMethodGroup = document.createElement('div');
downloadMethodGroup.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
`;
downloadMethods.appendChild(downloadMethodGroup);
// 默认选择直接下载
let selectedDownloadMethod = 'direct';
downloadOptions.forEach(option => {
const optionContainer = document.createElement('div');
optionContainer.style.cssText = `
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 4px;
border-radius: 3px;
transition: background-color 0.2s ease;
`;
// 添加悬停效果
optionContainer.addEventListener('mouseenter', () => {
optionContainer.style.backgroundColor = 'rgba(0, 161, 214, 0.1)';
});
optionContainer.addEventListener('mouseleave', () => {
optionContainer.style.backgroundColor = 'transparent';
});
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'subtitle-download-method';
radio.value = option.value;
radio.checked = option.value === selectedDownloadMethod;
radio.style.cssText = `
accent-color: #00a1d6;
`;
const label = document.createElement('label');
label.textContent = option.name;
label.style.cssText = `
cursor: pointer;
font-size: 13px;
`;
// 添加点击事件
optionContainer.addEventListener('click', () => {
radio.checked = true;
selectedDownloadMethod = option.value;
});
optionContainer.appendChild(radio);
optionContainer.appendChild(label);
downloadMethodGroup.appendChild(optionContainer);
});
// 当格式改变时更新内容和文件名
formatSelect.addEventListener('change', () => {
const newFormat = formatSelect.value;
const selectedOption = formatOptions.find(opt => opt.value === newFormat);
if (selectedOption) {
// 根据新格式重新转换字幕
let newContent;
switch (newFormat) {
case SUBTITLE_FORMATS.JSON:
newContent = JSON.stringify(originalData, null, 2);
break;
case SUBTITLE_FORMATS.SRT:
newContent = jsonToSrt(originalData);
break;
case SUBTITLE_FORMATS.VTT:
newContent = jsonToVtt(originalData);
break;
}
// 更新当前内容、MIME类型和文件名
currentContent = newContent;
currentMimeType = selectedOption.mimeType;
currentFilename = `${baseFilename}.${newFormat}`;
// 更新文件名输入框
filenameInput.value = currentFilename;
// 更新文件大小
currentFileSize = new Blob([newContent], { type: currentMimeType }).size;
currentFileSizeStr = currentFileSize < 1024 ? `${currentFileSize} B` : `${(currentFileSize / 1024).toFixed(2)} KB`;
sizeInfo.innerHTML = `文件大小: ${currentFileSizeStr}`;
}
});
// 创建按钮容器
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
justify-content: space-between;
gap: 10px;
`;
confirmWindow.appendChild(buttonContainer);
// 创建取消按钮
const cancelBtn = document.createElement('button');
cancelBtn.textContent = '取消';
cancelBtn.style.cssText = `
flex: 1;
padding: 8px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 4px;
color: white;
cursor: pointer;
transition: all 0.2s ease;
`;
cancelBtn.addEventListener('click', () => {
confirmWindow.remove();
});
buttonContainer.appendChild(cancelBtn);
// 创建下载按钮
const downloadBtn = document.createElement('button');
downloadBtn.textContent = '下载';
downloadBtn.style.cssText = `
flex: 1;
padding: 8px;
background: rgba(0, 161, 214, 0.8);
border: 1px solid rgba(0, 161, 214, 0.8);
border-radius: 4px;
color: white;
cursor: pointer;
transition: all 0.2s ease;
`;
downloadBtn.addEventListener('click', () => {
const newFilename = filenameInput.value.trim() || currentFilename;
confirmWindow.remove();
// 根据选择的下载方式执行不同操作
switch (selectedDownloadMethod) {
case 'direct':
// 使用自定义下载逻辑
customDownload(currentContent, newFilename, currentMimeType);
break;
case 'newtab':
// 在新标签页打开
const blob = new Blob([currentContent], { type: currentMimeType });
const url = URL.createObjectURL(blob);
window.open(url, '_blank');
// 释放URL对象
setTimeout(() => {
URL.revokeObjectURL(url);
}, 100);
break;
case 'clipboard':
// 复制到剪贴板
navigator.clipboard.writeText(currentContent)
.then(() => {
showInfoBar('字幕内容已复制到剪贴板!', 'success');
})
.catch(err => {
console.error('复制失败:', err);
showInfoBar('复制失败,请手动复制!', 'error');
});
break;
}
});
buttonContainer.appendChild(downloadBtn);
// 添加悬停效果
cancelBtn.addEventListener('mouseenter', () => {
cancelBtn.style.background = 'rgba(255, 255, 255, 0.2)';
});
cancelBtn.addEventListener('mouseleave', () => {
cancelBtn.style.background = 'rgba(255, 255, 255, 0.1)';
});
downloadBtn.addEventListener('mouseenter', () => {
downloadBtn.style.background = 'rgba(0, 161, 214, 1)';
});
downloadBtn.addEventListener('mouseleave', () => {
downloadBtn.style.background = 'rgba(0, 161, 214, 0.8)';
});
// 添加到页面
document.body.appendChild(confirmWindow);
// 自动聚焦到文件名输入框
filenameInput.focus();
filenameInput.select();
// 按ESC键关闭
const handleEsc = (e) => {
if (e.key === 'Escape') {
confirmWindow.remove();
document.removeEventListener('keydown', handleEsc);
}
};
document.addEventListener('keydown', handleEsc);
// 点击外部关闭
const handleOutsideClick = (e) => {
if (!confirmWindow.contains(e.target)) {
confirmWindow.remove();
document.removeEventListener('click', handleOutsideClick);
}
};
setTimeout(() => {
document.addEventListener('click', handleOutsideClick);
}, 100);
}
// 自定义下载方法
function customDownload(content, filename, mimeType) {
const blob = new Blob([content], { type: mimeType });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// 显示下载完成提示
showInfoBar(`字幕文件 "${filename}" 下载完成!`, 'success');
// 释放URL对象
setTimeout(() => {
URL.revokeObjectURL(link.href);
}, 100);
}
// 下载文件 - 现在直接调用确认窗口
function downloadFile(content, filename, mimeType, originalData, currentFormat) {
showDownloadConfirm(content, filename, mimeType, originalData, currentFormat);
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();