// ==UserScript== // @name B站字幕下载器 // @namespace https://github.com // @version 1.1 // @author Ace // @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) { // 直接调用下载函数,使用默认格式 downloadSubtitle(SUBTITLE_FORMATS.JSON); } // 获取字幕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'; // 创建下载按钮引用 let downloadBtn; // 更新按钮文本函数 function updateDownloadButtonText() { if (!downloadBtn) return; switch (selectedDownloadMethod) { case 'direct': downloadBtn.textContent = '下载'; break; case 'newtab': downloadBtn.textContent = '打开'; break; case 'clipboard': downloadBtn.textContent = '复制'; break; } } 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; // 更新按钮文本 updateDownloadButtonText(); }); 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); // 创建下载按钮 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(); } })();