// ==UserScript== // @name AI Video Parser Pro - 智能视频解析助手 // @namespace http://tampermonkey.net/ // @version 2.0.0 // @description 支持B站、YouTube等主流视频网站的AI智能解析工具,提供视频内容摘要、要点提取等功能。全新UI升级,支持紫蓝渐变主题、毛玻璃效果、粒子动画、拖拽面板、快捷键操作等。 // @author AI Assistant // @match *://*.bilibili.com/video/* // @match *://*.bilibili.com/bangumi/play/* // @match *://*.youtube.com/watch* // @match *://*.youtube.com/shorts* // @match *://*.bilibili.com/list/* // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @connect * // @run-at document-end // @license MIT // ==/UserScript== (function() { 'use strict'; // ==================== 版本信息 ==================== const VERSION = { current: '2.0.0', checkUrl: 'https://api.github.com/repos/ai-video-parser/releases/latest', // 示例更新检查地址 releaseDate: '2024-01-15' }; // ==================== 配置管理 ==================== const CONFIG = { // 默认API配置(用户可在设置中修改) API_URL: GM_getValue('api_url', 'https://api.openai.com/v1/chat/completions'), API_KEY: GM_getValue('api_key', ''), MODEL: GM_getValue('model', 'gpt-4o'), // 解析设置 MAX_HISTORY: 50, // 最大历史记录数 AUTO_PARSE: false, // 是否自动解析 PARSE_ON_OPEN: false, // 打开面板时自动解析 // UI设置 THEME: 'dark', // 主题: dark/light GLASS_EFFECT: true, // 毛玻璃效果 PANEL_WIDTH: 520, // 面板宽度 PANEL_MAX_HEIGHT: 85, // 面板最大高度(vh百分比) // 存储键名 STORAGE_KEY: 'ai_video_parser_history', SETTINGS_KEY: 'ai_video_parser_settings' }; // ==================== 工具函数 ==================== /** * 创建防抖函数 * @param {Function} fn - 需要防抖的函数 * @param {number} delay - 延迟时间(ms) * @returns {Function} 防抖后的函数 */ const debounce = (fn, delay = 300) => { let timer = null; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; }; /** * 格式化时间戳 * @param {number} seconds - 秒数 * @returns {string} 格式化后的时间字符串 */ const formatDuration = (seconds) => { if (!seconds || seconds <= 0) return '00:00'; const hrs = Math.floor(seconds / 3600); const mins = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); if (hrs > 0) return `${hrs}:${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`; return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`; }; /** * 格式化日期 * @param {Date|number} date - 日期对象或时间戳 * @returns {string} 格式化后的日期字符串 */ const formatDate = (date) => { const d = new Date(date); const now = new Date(); const diff = now - d; const days = Math.floor(diff / (1000 * 60 * 60 * 24)); if (days === 0) { const hours = Math.floor(diff / (1000 * 60 * 60)); if (hours === 0) { const mins = Math.floor(diff / (1000 * 60)); return mins < 1 ? '刚刚' : `${mins}分钟前`; } return `${hours}小时前`; } if (days === 1) return '昨天'; if (days < 7) return `${days}天前`; return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; }; /** * 增强的Markdown渲染器,支持关键词高亮和折叠 * @param {string} md - Markdown文本 * @returns {string} HTML字符串 */ const renderMarkdown = (md) => { if (!md) return ''; // 关键词高亮列表 const keywords = ['重要', '核心', '关键', '亮点', '特色', '优势', '建议', '注意', '总结']; let html = md // 转义HTML特殊字符 .replace(/&/g, '&') .replace(//g, '>') // 代码块 .replace(/```(\w+)?\n([\s\S]*?)```/g, '
$2
') // 行内代码 .replace(/`([^`]+)`/g, '$1') // 标题 - 添加折叠功能 .replace(/^### (.*$)/gim, '

$1

') .replace(/^## (.*$)/gim, '

$1

') .replace(/^# (.*$)/gim, '

$1

') // 粗体和斜体 .replace(/\*\*\*(.*?)\*\*\*/g, '$1') .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\*(.*?)\*/g, '$1') // 删除线 .replace(/~~(.*?)~~/g, '$1') // 引用 .replace(/^> (.*$)/gim, '
$1
') // 无序列表 .replace(/^\s*[-•*] (.*$)/gim, '
  • $1
  • ') // 有序列表 .replace(/^\s*(\d+)\. (.*$)/gim, '
  • $2
  • ') // 链接 .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') // 图片 .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1') // 分割线 .replace(/^---+$/gim, '
    ') // 表格(简单支持) .replace(/^\|(.+)\|$/gim, (match, p1) => { const cells = p1.split('|').map(c => c.trim()).filter(c => c); return '' + cells.map(c => `${c}`).join('') + ''; }); // 处理列表包裹 html = html.replace(/(
  • .*<\/li>\n?)+/gs, match => ``); // 处理表格包裹 html = html.replace(/(.*<\/tr>\n?)+/gs, match => `${match}
    `); // 段落处理(将剩余文本包裹为段落) const lines = html.split('\n'); html = lines.map(line => { if (line.trim() && !line.match(/^<[a-z]/)) { return `

    ${line}

    `; } return line; }).join(''); // 关键词高亮 keywords.forEach(keyword => { const regex = new RegExp(`(${keyword})`, 'g'); html = html.replace(regex, '$1'); }); return html; }; // ==================== 视频信息提取器 ==================== /** * 视频信息提取器基类 */ class VideoExtractor { constructor() { this.platform = 'unknown'; } /** * 检查是否匹配当前页面 * @returns {boolean} */ match() { return false; } /** * 提取视频信息 * @returns {Object|null} */ extract() { return null; } /** * 获取视频描述文本 * @returns {string} */ getDescription() { return ''; } } /** * B站视频信息提取器 */ class BilibiliExtractor extends VideoExtractor { constructor() { super(); this.platform = 'bilibili'; } match() { return location.hostname.includes('bilibili.com') && (location.pathname.includes('/video/') || location.pathname.includes('/bangumi/play/') || location.pathname.includes('/list/')); } extract() { const videoData = this.#getVideoData(); if (!videoData) return null; return { platform: 'bilibili', videoId: videoData.bvid || videoData.aid?.toString() || '', title: videoData.title || document.querySelector('.video-title')?.textContent?.trim() || '', author: videoData.owner?.name || document.querySelector('.up-name')?.textContent?.trim() || document.querySelector('.up-info__name')?.textContent?.trim() || '', authorId: videoData.owner?.mid?.toString() || '', duration: videoData.duration || 0, description: this.getDescription(), cover: videoData.pic || '', url: location.href, tags: this.#getTags() }; } #getVideoData() { // 尝试从window.__INITIAL_STATE__获取 try { const state = window.__INITIAL_STATE__; if (state) { if (state.videoData) return state.videoData; if (state.mediaInfo) return state.mediaInfo; if (state.epInfo) { return { ...state.epInfo, title: state.epInfo.title || state.epInfo.long_title, owner: { name: state.mediaInfo?.up_info?.uname } }; } } } catch (e) {} // 尝试从script标签获取 try { const scripts = document.querySelectorAll('script'); for (const script of scripts) { const text = script.textContent; if (text.includes('__INITIAL_STATE__')) { const match = text.match(/__INITIAL_STATE__\s*=\s*({.+?});/); if (match) { const data = JSON.parse(match[1]); if (data.videoData) return data.videoData; } } } } catch (e) {} return null; } #getTags() { const tags = []; document.querySelectorAll('.tag-link, .tag').forEach(el => { tags.push(el.textContent.trim()); }); return tags; } getDescription() { // 尝试多种方式获取描述 const selectors = [ '.desc-info-text', '.video-desc .desc-info', '#v_desc .desc-info', '.video-desc-container', '[data-report-id="abstract"]' ]; for (const selector of selectors) { const el = document.querySelector(selector); if (el) { return el.textContent.trim(); } } // 尝试从meta获取 const metaDesc = document.querySelector('meta[name="description"]'); if (metaDesc) { return metaDesc.getAttribute('content') || ''; } return ''; } } /** * YouTube视频信息提取器 */ class YouTubeExtractor extends VideoExtractor { constructor() { super(); this.platform = 'youtube'; } match() { return location.hostname.includes('youtube.com') && (location.pathname.includes('/watch') || location.pathname.includes('/shorts')); } extract() { const videoData = this.#getVideoData(); if (!videoData) return null; return { platform: 'youtube', videoId: videoData.videoId || this.#extractVideoId(), title: videoData.title || document.querySelector('h1.title')?.textContent?.trim() || '', author: videoData.author || document.querySelector('#owner-name a')?.textContent?.trim() || document.querySelector('ytd-channel-name a')?.textContent?.trim() || '', authorId: videoData.authorId || '', duration: videoData.lengthSeconds || 0, description: this.getDescription(), cover: videoData.thumbnail?.thumbnails?.[0]?.url || '', url: location.href, tags: videoData.keywords || [] }; } #getVideoData() { // 尝试从ytInitialPlayerResponse获取 try { if (window.ytInitialPlayerResponse) { const data = window.ytInitialPlayerResponse; return { videoId: data.videoDetails?.videoId, title: data.videoDetails?.title, author: data.videoDetails?.author, lengthSeconds: parseInt(data.videoDetails?.lengthSeconds || 0), thumbnail: data.videoDetails?.thumbnail, keywords: data.videoDetails?.keywords || [] }; } } catch (e) {} // 尝试从ytInitialData获取(页面数据) try { if (window.ytInitialData) { // 复杂结构,简化处理 } } catch (e) {} return null; } #extractVideoId() { const urlParams = new URLSearchParams(location.search); return urlParams.get('v') || location.pathname.split('/').pop() || ''; } getDescription() { // 尝试获取视频描述 const selectors = [ '#description-inline-expander .yt-core-attributed-string', '#description .ytd-video-secondary-info-renderer', 'ytd-expander yt-formatted-string.content' ]; for (const selector of selectors) { const el = document.querySelector(selector); if (el) { return el.textContent.trim(); } } // 尝试从meta获取 const metaDesc = document.querySelector('meta[name="description"]'); if (metaDesc) { return metaDesc.getAttribute('content') || ''; } return ''; } } // ==================== 历史记录管理 ==================== /** * 历史记录管理器 */ class HistoryManager { constructor() { this.key = CONFIG.STORAGE_KEY; this.maxItems = CONFIG.MAX_HISTORY; } /** * 获取所有历史记录 * @returns {Array} */ getAll() { try { const data = GM_getValue(this.key, '[]'); return JSON.parse(data); } catch (e) { console.error('[AI Video Parser] 解析历史记录失败:', e); return []; } } /** * 添加历史记录 * @param {Object} record - 解析记录 */ add(record) { const history = this.getAll(); const item = { id: Date.now().toString(36) + Math.random().toString(36).substr(2, 9), timestamp: Date.now(), ...record }; // 去重:同一视频ID只保留最新记录 const filtered = history.filter(h => h.videoId !== record.videoId); filtered.unshift(item); // 限制数量 while (filtered.length > this.maxItems) { filtered.pop(); } GM_setValue(this.key, JSON.stringify(filtered)); return item; } /** * 删除指定记录 * @param {string} id - 记录ID */ remove(id) { const history = this.getAll(); const filtered = history.filter(h => h.id !== id); GM_setValue(this.key, JSON.stringify(filtered)); } /** * 清空历史记录 */ clear() { GM_setValue(this.key, '[]'); } /** * 根据ID获取记录 * @param {string} id * @returns {Object|null} */ getById(id) { return this.getAll().find(h => h.id === id) || null; } } // ==================== AI解析服务 ==================== /** * AI解析服务 */ class AIParserService { constructor() { this.apiUrl = CONFIG.API_URL; this.apiKey = CONFIG.API_KEY; this.model = CONFIG.MODEL; } /** * 更新配置 */ updateConfig() { this.apiUrl = GM_getValue('api_url', CONFIG.API_URL); this.apiKey = GM_getValue('api_key', CONFIG.API_KEY); this.model = GM_getValue('model', CONFIG.MODEL); } /** * 构建解析提示词 * @param {Object} videoInfo - 视频信息 * @returns {string} */ buildPrompt(videoInfo) { return `请对以下视频内容进行深度解析,提供结构化的分析结果: ## 视频信息 - 标题:${videoInfo.title} - 作者:${videoInfo.author} - 时长:${formatDuration(videoInfo.duration)} - 平台:${videoInfo.platform} ## 视频描述 ${videoInfo.description || '无描述信息'} ## 解析要求 请从以下几个维度进行分析: ### 1. 内容概述 用2-3句话概括视频的核心内容和主题。 ### 2. 关键要点 提取3-5个视频中的关键信息点或核心观点。 ### 3. 内容结构 分析视频的组织结构和叙述逻辑。 ### 4. 亮点与特色 指出视频的突出特点、创新点或值得注意的地方。 ### 5. 适用人群 分析该视频最适合的受众群体。 ### 6. 相关推荐 基于内容主题,推荐相关的学习方向或补充内容。 请用中文回答,使用Markdown格式,确保内容准确、有见地。`; } /** * 执行解析(模拟/真实) * @param {Object} videoInfo - 视频信息 * @param {Function} onProgress - 进度回调 * @returns {Promise} */ async parse(videoInfo, onProgress) { this.updateConfig(); // 如果没有配置API Key,返回模拟数据 if (!this.apiKey || this.apiKey === 'your-api-key-here') { return this.#mockParse(videoInfo, onProgress); } return this.#realParse(videoInfo, onProgress); } /** * 模拟解析(用于演示) */ async #mockParse(videoInfo, onProgress) { const steps = [ { progress: 10, message: '正在分析视频元数据...' }, { progress: 25, message: '正在提取关键信息...' }, { progress: 40, message: '正在构建分析框架...' }, { progress: 55, message: '正在生成内容概述...' }, { progress: 70, message: '正在提炼核心要点...' }, { progress: 85, message: '正在优化输出格式...' }, { progress: 100, message: '解析完成!' } ]; for (const step of steps) { await this.#delay(300 + Math.random() * 400); onProgress?.(step.progress, step.message); } // 生成模拟的解析结果 return this.#generateMockResult(videoInfo); } /** * 真实API解析 */ async #realParse(videoInfo, onProgress) { onProgress?.(5, '正在准备请求...'); const prompt = this.buildPrompt(videoInfo); return new Promise((resolve, reject) => { const requestBody = { model: this.model, messages: [ { role: 'system', content: '你是一位专业的视频内容分析师,擅长提取视频的核心价值,并以结构化的方式呈现分析结果。' }, { role: 'user', content: prompt } ], temperature: 0.7, max_tokens: 2000 }; onProgress?.(15, '正在发送请求到AI服务...'); GM_xmlhttpRequest({ method: 'POST', url: this.apiUrl, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}` }, data: JSON.stringify(requestBody), onload: (response) => { try { onProgress?.(80, '正在处理响应...'); if (response.status !== 200) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = JSON.parse(response.responseText); const content = data.choices?.[0]?.message?.content; if (!content) { throw new Error('API返回数据格式异常'); } onProgress?.(100, '解析完成!'); resolve(content); } catch (error) { reject(new Error(`解析失败: ${error.message}`)); } }, onerror: (error) => { reject(new Error(`网络请求失败: ${error.message || '未知错误'}`)); }, onprogress: (progress) => { if (progress.lengthComputable) { const percent = Math.round((progress.loaded / progress.total) * 60); onProgress?.(15 + percent, '正在接收数据...'); } } }); }); } /** * 生成模拟结果 */ #generateMockResult(videoInfo) { const title = videoInfo.title || '未知视频'; const author = videoInfo.author || '未知作者'; return `# ${title} — AI智能解析 ## 1. 内容概述 本视频由**${author}**制作,围绕"${title}"这一主题展开。视频通过清晰的结构和生动的表达方式,向观众传递了核心信息与专业见解。 ## 2. 关键要点 - **核心主题明确**:视频围绕中心主题层层递进,逻辑清晰 - **信息密度适中**:在有限时长内有效传递了关键知识点 - **表达风格独特**:作者形成了个人鲜明的表达特色 - **实用价值突出**:内容具有较强的参考价值和实践指导意义 ## 3. 内容结构 | 部分 | 说明 | |:---|:---| | 开场引入 | 快速建立观众兴趣,点明主题 | | 主体论述 | 多角度展开,提供详实信息 | | 案例支撑 | 具体实例增强说服力 | | 总结升华 | 回顾要点,强化记忆 | ## 4. 亮点与特色 > 该视频在内容策划和视觉呈现方面表现出色,能够有效抓住观众注意力并保持 engagement。 - ✅ 选题具有时效性和话题性 - ✅ 信息组织有序,易于理解吸收 - ✅ 节奏把控得当,观看体验良好 ## 5. 适用人群 - 对该领域感兴趣的**初学者** - 希望**系统了解**相关知识的进阶观众 - 需要**快速获取信息**的职场人士 ## 6. 相关推荐 基于视频内容,建议关注以下方向进行深入学习: 1. 该作者的**其他系列作品** 2. 相关领域的**经典教程和文献** 3. **实践项目**以巩固所学知识 --- *本解析由AI自动生成,仅供参考。实际内容请以视频为准。*`; } /** * 延迟函数 */ #delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } // ==================== 粒子背景效果 ==================== /** * 粒子背景管理器 */ class ParticleBackground { constructor(container) { this.container = container; this.canvas = null; this.ctx = null; this.particles = []; this.animationId = null; this.isActive = false; } /** * 初始化粒子画布 */ init() { if (this.canvas) return; this.canvas = document.createElement('canvas'); this.canvas.className = 'avp-particle-canvas'; this.canvas.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 0; opacity: 0.6; `; this.container.style.position = 'relative'; this.container.insertBefore(this.canvas, this.container.firstChild); this.ctx = this.canvas.getContext('2d'); this.#resize(); // 创建粒子 this.#createParticles(); // 开始动画 this.isActive = true; this.#animate(); // 监听resize window.addEventListener('resize', () => this.#resize()); } /** * 创建粒子 */ #createParticles() { const count = 30; this.particles = []; for (let i = 0; i < count; i++) { this.particles.push({ x: Math.random() * this.canvas.width, y: Math.random() * this.canvas.height, vx: (Math.random() - 0.5) * 0.5, vy: (Math.random() - 0.5) * 0.5, radius: Math.random() * 2 + 1, opacity: Math.random() * 0.5 + 0.2, color: this.#getRandomColor() }); } } /** * 获取随机颜色(紫蓝渐变色调) */ #getRandomColor() { const colors = [ 'rgba(99, 102, 241, ', // 主紫色 'rgba(129, 140, 248, ', // 浅紫色 'rgba(79, 70, 229, ', // 深紫色 'rgba(59, 130, 246, ', // 蓝色 'rgba(147, 51, 234, ' // 紫红色 ]; return colors[Math.floor(Math.random() * colors.length)]; } /** * 调整画布大小 */ #resize() { const rect = this.container.getBoundingClientRect(); this.canvas.width = rect.width; this.canvas.height = rect.height; } /** * 动画循环 */ #animate() { if (!this.isActive) return; this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.particles.forEach(p => { // 更新位置 p.x += p.vx; p.y += p.vy; // 边界反弹 if (p.x < 0 || p.x > this.canvas.width) p.vx *= -1; if (p.y < 0 || p.y > this.canvas.height) p.vy *= -1; // 绘制粒子 this.ctx.beginPath(); this.ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2); this.ctx.fillStyle = p.color + p.opacity + ')'; this.ctx.fill(); }); // 绘制连线 this.#drawConnections(); this.animationId = requestAnimationFrame(() => this.#animate()); } /** * 绘制粒子间的连线 */ #drawConnections() { const maxDistance = 100; for (let i = 0; i < this.particles.length; i++) { for (let j = i + 1; j < this.particles.length; j++) { const dx = this.particles[i].x - this.particles[j].x; const dy = this.particles[i].y - this.particles[j].y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < maxDistance) { const opacity = (1 - distance / maxDistance) * 0.15; this.ctx.beginPath(); this.ctx.moveTo(this.particles[i].x, this.particles[i].y); this.ctx.lineTo(this.particles[j].x, this.particles[j].y); this.ctx.strokeStyle = `rgba(99, 102, 241, ${opacity})`; this.ctx.lineWidth = 0.5; this.ctx.stroke(); } } } } /** * 销毁粒子效果 */ destroy() { this.isActive = false; if (this.animationId) { cancelAnimationFrame(this.animationId); } if (this.canvas && this.canvas.parentNode) { this.canvas.parentNode.removeChild(this.canvas); } this.canvas = null; this.ctx = null; this.particles = []; } } // ==================== UI组件 ==================== /** * 样式注入器 - 全面升级版本 */ class StyleInjector { static inject() { const styleId = 'ai-video-parser-styles-v2'; if (document.getElementById(styleId)) return; const style = document.createElement('style'); style.id = styleId; style.textContent = ` /* ===== CSS变量系统 ===== */ :root { /* 紫蓝渐变主色调 */ --avp-primary: #6366f1; --avp-primary-light: #818cf8; --avp-primary-lighter: #a5b4fc; --avp-primary-dark: #4f46e5; --avp-primary-darker: #3730a3; /* 背景色系 */ --avp-bg: #0f0f23; --avp-bg-secondary: #1a1a2e; --avp-bg-tertiary: #16213e; --avp-surface: rgba(26, 26, 46, 0.9); --avp-surface-hover: rgba(36, 36, 62, 0.95); /* 文字色系 */ --avp-text: #f1f5f9; --avp-text-secondary: #cbd5e1; --avp-text-muted: #94a3b8; --avp-text-dim: #64748b; /* 功能色 */ --avp-success: #10b981; --avp-warning: #f59e0b; --avp-error: #ef4444; --avp-info: #3b82f6; /* 玻璃效果 */ --avp-glass: rgba(255, 255, 255, 0.03); --avp-glass-hover: rgba(255, 255, 255, 0.06); --avp-glass-border: rgba(255, 255, 255, 0.08); --avp-glass-border-hover: rgba(255, 255, 255, 0.15); /* 阴影系统 */ --avp-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3); --avp-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); --avp-shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.5); --avp-shadow-glow: 0 0 40px rgba(99, 102, 241, 0.15); /* 圆角系统 */ --avp-radius-sm: 8px; --avp-radius: 16px; --avp-radius-lg: 24px; /* 过渡动画 */ --avp-transition-fast: all 0.15s cubic-bezier(0.4, 0, 0.2, 1); --avp-transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); --avp-transition-slow: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); } /* ===== 动态渐变背景 ===== */ .avp-gradient-bg { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 99998; opacity: 0; transition: opacity 0.5s ease; background: radial-gradient(ellipse at 20% 80%, rgba(99, 102, 241, 0.08) 0%, transparent 50%), radial-gradient(ellipse at 80% 20%, rgba(59, 130, 246, 0.06) 0%, transparent 50%), radial-gradient(ellipse at 50% 50%, rgba(147, 51, 234, 0.04) 0%, transparent 70%); } .avp-gradient-bg.active { opacity: 1; } /* ===== 浮动按钮 - 呼吸灯效 ===== */ .avp-float-btn { position: fixed; right: 28px; bottom: 140px; width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(135deg, var(--avp-primary) 0%, var(--avp-primary-dark) 50%, var(--avp-primary-darker) 100%); border: none; cursor: pointer; z-index: 99999; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 20px rgba(99, 102, 241, 0.4), 0 0 0 4px rgba(99, 102, 241, 0.1), var(--avp-shadow); transition: var(--avp-transition); animation: avp-float-in 0.6s cubic-bezier(0.34, 1.56, 0.64, 1), avp-breathe 3s ease-in-out infinite; } .avp-float-btn:hover { transform: scale(1.12) translateY(-3px); box-shadow: 0 8px 30px rgba(99, 102, 241, 0.6), 0 0 0 6px rgba(99, 102, 241, 0.15), var(--avp-shadow); animation-play-state: paused; } .avp-float-btn:active { transform: scale(0.95); } .avp-float-btn svg { width: 26px; height: 26px; fill: white; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2)); } /* 按钮上的脉冲光环 */ .avp-float-btn::before { content: ''; position: absolute; inset: -4px; border-radius: 50%; background: linear-gradient(135deg, var(--avp-primary) 0%, var(--avp-primary-light) 100%); opacity: 0; z-index: -1; animation: avp-pulse-ring 3s ease-out infinite; } @keyframes avp-float-in { from { opacity: 0; transform: scale(0) translateY(30px); } to { opacity: 1; transform: scale(1) translateY(0); } } @keyframes avp-breathe { 0%, 100% { box-shadow: 0 4px 20px rgba(99, 102, 241, 0.4), 0 0 0 4px rgba(99, 102, 241, 0.1), var(--avp-shadow); } 50% { box-shadow: 0 6px 25px rgba(99, 102, 241, 0.5), 0 0 0 6px rgba(99, 102, 241, 0.15), var(--avp-shadow); } } @keyframes avp-pulse-ring { 0% { transform: scale(1); opacity: 0.5; } 100% { transform: scale(1.5); opacity: 0; } } /* ===== 解析面板 - 弹性动画 ===== */ .avp-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.9) translateY(20px); width: var(--avp-panel-width, 520px); max-width: 92vw; max-height: 88vh; background: linear-gradient(145deg, rgba(26, 26, 46, 0.95) 0%, rgba(15, 15, 35, 0.98) 100%); border-radius: var(--avp-radius); border: 1px solid var(--avp-glass-border); backdrop-filter: blur(24px) saturate(200%); -webkit-backdrop-filter: blur(24px) saturate(200%); box-shadow: var(--avp-shadow-glow), var(--avp-shadow-lg), inset 0 1px 0 rgba(255, 255, 255, 0.05); z-index: 100000; display: flex; flex-direction: column; opacity: 0; pointer-events: none; transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1), transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); overflow: hidden; } .avp-panel.active { opacity: 1; pointer-events: all; transform: translate(-50%, -50%) scale(1) translateY(0); } .avp-panel.dragging { transition: none; cursor: grabbing; } /* ===== 面板头部 - 拖拽区域 ===== */ .avp-panel-header { display: flex; align-items: center; justify-content: space-between; padding: 18px 24px; border-bottom: 1px solid var(--avp-glass-border); background: linear-gradient(90deg, rgba(99,102,241,0.08) 0%, rgba(59,130,246,0.04) 50%, transparent 100%); cursor: grab; user-select: none; position: relative; z-index: 2; } .avp-panel-header:active { cursor: grabbing; } .avp-panel-header::after { content: ''; position: absolute; bottom: 0; left: 24px; right: 24px; height: 1px; background: linear-gradient(90deg, transparent, var(--avp-primary-light), transparent); opacity: 0.3; } .avp-panel-title { display: flex; align-items: center; gap: 12px; font-size: 16px; font-weight: 600; color: var(--avp-text); letter-spacing: 0.3px; } .avp-panel-title-icon { width: 32px; height: 32px; border-radius: var(--avp-radius-sm); background: linear-gradient(135deg, var(--avp-primary) 0%, var(--avp-primary-dark) 100%); display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); } .avp-panel-title-icon svg { width: 18px; height: 18px; fill: white; } .avp-version-badge { font-size: 11px; font-weight: 500; color: var(--avp-text-dim); background: var(--avp-glass); padding: 2px 8px; border-radius: 12px; margin-left: 8px; } .avp-panel-actions { display: flex; gap: 8px; } .avp-btn-icon { width: 34px; height: 34px; border-radius: var(--avp-radius-sm); border: 1px solid var(--avp-glass-border); background: var(--avp-glass); color: var(--avp-text-muted); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: var(--avp-transition); position: relative; overflow: hidden; } .avp-btn-icon::before { content: ''; position: absolute; inset: 0; background: linear-gradient(135deg, var(--avp-primary) 0%, var(--avp-primary-light) 100%); opacity: 0; transition: var(--avp-transition); } .avp-btn-icon:hover { border-color: var(--avp-glass-border-hover); color: var(--avp-text); transform: translateY(-2px); } .avp-btn-icon:hover::before { opacity: 0.1; } .avp-btn-icon:active { transform: scale(0.95); } .avp-btn-icon svg { width: 18px; height: 18px; fill: currentColor; position: relative; z-index: 1; } /* ===== 面板内容区 ===== */ .avp-panel-body { flex: 1; overflow-y: auto; padding: 0; position: relative; scrollbar-width: thin; scrollbar-color: var(--avp-primary) transparent; } .avp-panel-body::-webkit-scrollbar { width: 5px; } .avp-panel-body::-webkit-scrollbar-track { background: transparent; } .avp-panel-body::-webkit-scrollbar-thumb { background: linear-gradient(180deg, var(--avp-primary) 0%, var(--avp-primary-dark) 100%); border-radius: 3px; } /* ===== 视频信息卡片 ===== */ .avp-video-info { padding: 20px 24px; border-bottom: 1px solid var(--avp-glass-border); position: relative; z-index: 1; } .avp-video-title { font-size: 15px; font-weight: 600; color: var(--avp-text); line-height: 1.6; margin-bottom: 14px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .avp-video-meta { display: flex; flex-wrap: wrap; gap: 10px; font-size: 12px; color: var(--avp-text-muted); } .avp-video-meta-item { display: flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 20px; background: var(--avp-glass); border: 1px solid var(--avp-glass-border); transition: var(--avp-transition-fast); } .avp-video-meta-item:hover { background: var(--avp-glass-hover); border-color: var(--avp-glass-border-hover); } .avp-video-meta-item svg { width: 13px; height: 13px; fill: currentColor; opacity: 0.7; } .avp-platform-badge { display: inline-flex; align-items: center; gap: 5px; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 600; background: linear-gradient(135deg, rgba(99, 102, 241, 0.15) 0%, rgba(59, 130, 246, 0.1) 100%); color: var(--avp-primary-light); border: 1px solid rgba(99, 102, 241, 0.2); } /* ===== 解析区域 ===== */ .avp-parse-section { padding: 20px 24px; position: relative; z-index: 1; } .avp-section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; } .avp-section-title { font-size: 14px; font-weight: 600; color: var(--avp-text); display: flex; align-items: center; gap: 10px; letter-spacing: 0.5px; } .avp-section-title::before { content: ''; width: 4px; height: 18px; border-radius: 2px; background: linear-gradient(180deg, var(--avp-primary) 0%, var(--avp-primary-light) 50%, var(--avp-info) 100%); } /* ===== 进度条 - 流光效果 ===== */ .avp-progress { display: none; margin-bottom: 20px; padding: 16px; border-radius: var(--avp-radius-sm); background: var(--avp-glass); border: 1px solid var(--avp-glass-border); } .avp-progress.active { display: block; animation: avp-fade-in-up 0.4s ease; } .avp-progress-bar { height: 6px; background: rgba(255, 255, 255, 0.05); border-radius: 3px; overflow: hidden; position: relative; } .avp-progress-fill { height: 100%; width: 0%; background: linear-gradient(90deg, var(--avp-primary) 0%, var(--avp-primary-light) 50%, var(--avp-primary) 100%); border-radius: 3px; transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1); position: relative; overflow: hidden; } /* 流光效果 */ .avp-progress-fill::after { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); animation: avp-shimmer 1.5s ease-in-out infinite; } @keyframes avp-shimmer { 0% { left: -100%; } 100% { left: 100%; } } .avp-progress-text { font-size: 12px; color: var(--avp-text-muted); text-align: center; margin-top: 10px; display: flex; align-items: center; justify-content: center; gap: 8px; } .avp-progress-dots { display: flex; gap: 4px; } .avp-progress-dots span { width: 6px; height: 6px; border-radius: 50%; background: var(--avp-primary); animation: avp-dot-bounce 1.4s ease-in-out infinite; } .avp-progress-dots span:nth-child(2) { animation-delay: 0.2s; } .avp-progress-dots span:nth-child(3) { animation-delay: 0.4s; } @keyframes avp-dot-bounce { 0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; } 40% { transform: scale(1); opacity: 1; } } /* ===== 解析结果 - 渐入动画 ===== */ .avp-result { display: none; font-size: 14px; line-height: 1.9; color: var(--avp-text); animation: avp-fade-in-up 0.5s ease; } .avp-result.active { display: block; } @keyframes avp-fade-in-up { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } /* Markdown样式增强 */ .avp-result h1, .avp-result h2, .avp-result h3 { color: var(--avp-text); margin: 20px 0 14px; font-weight: 600; position: relative; padding-left: 12px; } .avp-result h1 { font-size: 20px; } .avp-result h2 { font-size: 17px; } .avp-result h3 { font-size: 15px; } .avp-result h1::before, .avp-result h2::before, .avp-result h3::before { content: ''; position: absolute; left: 0; top: 4px; bottom: 4px; width: 3px; border-radius: 2px; background: linear-gradient(180deg, var(--avp-primary) 0%, var(--avp-primary-light) 100%); } /* 可折叠标题 */ .avp-result .md-foldable { cursor: pointer; user-select: none; transition: var(--avp-transition-fast); } .avp-result .md-foldable:hover { color: var(--avp-primary-light); } .avp-result .md-fold-icon { display: inline-block; margin-right: 6px; font-size: 10px; color: var(--avp-primary); transition: transform 0.3s ease; } .avp-result .md-foldable.collapsed .md-fold-icon { transform: rotate(-90deg); } .avp-result .md-foldable + * { transition: var(--avp-transition); overflow: hidden; } .avp-result .md-foldable.collapsed + * { max-height: 0; margin: 0; opacity: 0; } .avp-result p { margin: 12px 0; color: var(--avp-text-secondary); } .avp-result strong { color: var(--avp-text); font-weight: 600; } /* 关键词高亮 */ .avp-result .md-keyword { background: linear-gradient(135deg, rgba(99, 102, 241, 0.15) 0%, rgba(129, 140, 248, 0.1) 100%); color: var(--avp-primary-light); padding: 2px 6px; border-radius: 4px; font-weight: 500; border: 1px solid rgba(99, 102, 241, 0.15); } .avp-result blockquote { margin: 16px 0; padding: 16px 20px; border-left: 3px solid var(--avp-primary); background: linear-gradient(90deg, rgba(99, 102, 241, 0.05) 0%, transparent 100%); border-radius: 0 var(--avp-radius-sm) var(--avp-radius-sm) 0; color: var(--avp-text-secondary); font-style: italic; } .avp-result ul, .avp-result ol { margin: 12px 0; padding-left: 24px; } .avp-result li { margin: 8px 0; color: var(--avp-text-secondary); position: relative; } .avp-result li::marker { color: var(--avp-primary); } .avp-result code { padding: 3px 8px; border-radius: 6px; background: var(--avp-bg-tertiary); color: var(--avp-primary-light); font-size: 13px; font-family: 'SF Mono', Monaco, monospace; border: 1px solid rgba(99, 102, 241, 0.1); } .avp-result pre { padding: 20px; border-radius: var(--avp-radius-sm); background: var(--avp-bg-tertiary); overflow-x: auto; margin: 16px 0; border: 1px solid var(--avp-glass-border); } .avp-result pre code { background: none; padding: 0; color: var(--avp-text); border: none; } /* 表格样式增强 */ .avp-result table, .avp-result .md-table { width: 100%; border-collapse: separate; border-spacing: 0; margin: 16px 0; font-size: 13px; border-radius: var(--avp-radius-sm); overflow: hidden; border: 1px solid var(--avp-glass-border); } .avp-result th { padding: 12px 16px; text-align: left; font-weight: 600; color: var(--avp-text); background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(59, 130, 246, 0.05) 100%); border-bottom: 1px solid var(--avp-glass-border); } .avp-result td { padding: 12px 16px; color: var(--avp-text-secondary); border-bottom: 1px solid var(--avp-glass-border); } .avp-result tr:last-child td { border-bottom: none; } .avp-result tr:hover td { background: var(--avp-glass-hover); } .avp-result hr { border: none; height: 1px; background: linear-gradient(90deg, transparent, var(--avp-glass-border), transparent); margin: 20px 0; } .avp-result a { color: var(--avp-primary-light); text-decoration: none; position: relative; transition: var(--avp-transition-fast); } .avp-result a::after { content: ''; position: absolute; bottom: -2px; left: 0; width: 0; height: 1px; background: linear-gradient(90deg, var(--avp-primary) 0%, var(--avp-primary-light) 100%); transition: width 0.3s ease; } .avp-result a:hover { color: var(--avp-primary-lighter); } .avp-result a:hover::after { width: 100%; } /* ===== 空状态 - 更友好的设计 ===== */ .avp-empty { text-align: center; padding: 48px 24px; color: var(--avp-text-dim); animation: avp-fade-in-up 0.5s ease; } .avp-empty-icon { width: 80px; height: 80px; margin: 0 auto 20px; border-radius: 50%; background: linear-gradient(135deg, var(--avp-glass) 0%, rgba(99, 102, 241, 0.05) 100%); display: flex; align-items: center; justify-content: center; border: 1px solid var(--avp-glass-border); position: relative; } .avp-empty-icon::before { content: ''; position: absolute; inset: -2px; border-radius: 50%; background: linear-gradient(135deg, var(--avp-primary) 0%, var(--avp-primary-light) 100%); opacity: 0.1; animation: avp-spin-slow 10s linear infinite; } @keyframes avp-spin-slow { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .avp-empty-icon svg { width: 36px; height: 36px; fill: var(--avp-primary-light); opacity: 0.6; } .avp-empty-title { font-size: 16px; font-weight: 600; margin-bottom: 10px; color: var(--avp-text-secondary); } .avp-empty-desc { font-size: 13px; line-height: 1.7; max-width: 280px; margin: 0 auto; } .avp-empty-hint { margin-top: 16px; padding: 10px 16px; border-radius: var(--avp-radius-sm); background: var(--avp-glass); border: 1px solid var(--avp-glass-border); font-size: 12px; color: var(--avp-text-muted); display: inline-flex; align-items: center; gap: 6px; } .avp-empty-hint kbd { padding: 2px 8px; border-radius: 4px; background: var(--avp-bg-tertiary); border: 1px solid var(--avp-glass-border); font-family: inherit; font-size: 11px; color: var(--avp-primary-light); } /* ===== 操作按钮 - 微交互 ===== */ .avp-actions { display: flex; gap: 10px; padding: 18px 24px; border-top: 1px solid var(--avp-glass-border); background: linear-gradient(180deg, transparent 0%, rgba(0,0,0,0.1) 100%); position: relative; z-index: 1; } .avp-btn { flex: 1; display: flex; align-items: center; justify-content: center; gap: 8px; padding: 12px 18px; border-radius: var(--avp-radius-sm); border: 1px solid var(--avp-glass-border); background: var(--avp-glass); color: var(--avp-text); font-size: 13px; font-weight: 500; cursor: pointer; transition: var(--avp-transition); position: relative; overflow: hidden; } .avp-btn::before { content: ''; position: absolute; inset: 0; background: linear-gradient(135deg, var(--avp-primary) 0%, var(--avp-primary-light) 100%); opacity: 0; transition: var(--avp-transition); } .avp-btn:hover { border-color: var(--avp-glass-border-hover); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } .avp-btn:hover::before { opacity: 0.05; } .avp-btn:active { transform: scale(0.97) translateY(0); } .avp-btn svg { width: 16px; height: 16px; fill: currentColor; position: relative; z-index: 1; } .avp-btn span { position: relative; z-index: 1; } .avp-btn-primary { background: linear-gradient(135deg, var(--avp-primary) 0%, var(--avp-primary-dark) 100%); border-color: transparent; color: white; box-shadow: 0 4px 15px rgba(99, 102, 241, 0.3); } .avp-btn-primary:hover { box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4); } .avp-btn-primary::before { display: none; } .avp-btn-success { background: linear-gradient(135deg, var(--avp-success) 0%, #059669 100%); border-color: transparent; color: white; } /* ===== 设置面板 ===== */ .avp-settings { display: none; padding: 20px 24px; } .avp-settings.active { display: block; animation: avp-fade-in-up 0.4s ease; } .avp-form-group { margin-bottom: 20px; } .avp-form-label { display: block; font-size: 13px; font-weight: 500; color: var(--avp-text); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; } .avp-form-label::before { content: ''; width: 3px; height: 14px; border-radius: 2px; background: linear-gradient(180deg, var(--avp-primary) 0%, var(--avp-primary-light) 100%); } .avp-form-input { width: 100%; padding: 12px 16px; border-radius: var(--avp-radius-sm); border: 1px solid var(--avp-glass-border); background: var(--avp-bg-secondary); color: var(--avp-text); font-size: 13px; transition: var(--avp-transition); box-sizing: border-box; font-family: inherit; } .avp-form-input:focus { outline: none; border-color: var(--avp-primary); box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); } .avp-form-input::placeholder { color: var(--avp-text-dim); } /* ===== 历史记录 ===== */ .avp-history { display: none; } .avp-history.active { display: block; animation: avp-fade-in-up 0.4s ease; } .avp-history-list { padding: 0 24px; } .avp-history-item { display: flex; align-items: center; gap: 14px; padding: 14px; border-radius: var(--avp-radius-sm); cursor: pointer; transition: var(--avp-transition); margin-bottom: 10px; border: 1px solid transparent; } .avp-history-item:hover { background: var(--avp-glass-hover); border-color: var(--avp-glass-border); transform: translateX(4px); } .avp-history-thumb { width: 88px; height: 66px; border-radius: var(--avp-radius-sm); background: var(--avp-bg-tertiary); object-fit: cover; flex-shrink: 0; border: 1px solid var(--avp-glass-border); } .avp-history-info { flex: 1; min-width: 0; } .avp-history-title { font-size: 13px; font-weight: 500; color: var(--avp-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 6px; } .avp-history-meta { font-size: 12px; color: var(--avp-text-dim); display: flex; align-items: center; gap: 8px; } .avp-history-platform { padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 600; text-transform: uppercase; } .avp-history-platform.bilibili { background: rgba(251, 114, 153, 0.15); color: #fb7299; } .avp-history-platform.youtube { background: rgba(255, 0, 0, 0.15); color: #ff0000; } /* ===== 遮罩层 ===== */ .avp-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); z-index: 99999; opacity: 0; pointer-events: none; transition: opacity 0.4s ease; } .avp-overlay.active { opacity: 1; pointer-events: all; } /* ===== Toast消息 - 滑入滑出 ===== */ .avp-toast { position: fixed; top: 24px; left: 50%; transform: translateX(-50%) translateY(-30px); padding: 14px 28px; border-radius: var(--avp-radius-sm); background: linear-gradient(145deg, var(--avp-surface) 0%, var(--avp-bg-secondary) 100%); border: 1px solid var(--avp-glass-border); color: var(--avp-text); font-size: 14px; font-weight: 500; z-index: 100001; opacity: 0; pointer-events: none; transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); box-shadow: var(--avp-shadow); display: flex; align-items: center; gap: 10px; max-width: 90vw; } .avp-toast.active { opacity: 1; transform: translateX(-50%) translateY(0); } .avp-toast-success { border-left: 3px solid var(--avp-success); } .avp-toast-error { border-left: 3px solid var(--avp-error); } .avp-toast-info { border-left: 3px solid var(--avp-info); } .avp-toast-icon { width: 20px; height: 20px; flex-shrink: 0; } /* ===== 快捷键提示 ===== */ .avp-shortcuts-hint { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); padding: 10px 20px; border-radius: var(--avp-radius-sm); background: var(--avp-surface); border: 1px solid var(--avp-glass-border); color: var(--avp-text-muted); font-size: 12px; z-index: 100002; opacity: 0; pointer-events: none; transition: var(--avp-transition); display: flex; gap: 16px; } .avp-shortcuts-hint.active { opacity: 1; } .avp-shortcuts-hint kbd { padding: 2px 8px; border-radius: 4px; background: var(--avp-bg-tertiary); border: 1px solid var(--avp-glass-border); font-family: inherit; font-size: 11px; color: var(--avp-primary-light); } /* ===== 动画集合 ===== */ @keyframes avp-spin { to { transform: rotate(360deg); } } .avp-spinning { animation: avp-spin 1s linear infinite; } @keyframes avp-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .avp-pulse { animation: avp-pulse 2s ease-in-out infinite; } /* ===== 粒子画布 ===== */ .avp-particle-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; } /* ===== 复制成功反馈 ===== */ .avp-copy-feedback { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.8); padding: 20px 40px; border-radius: var(--avp-radius); background: linear-gradient(145deg, var(--avp-surface) 0%, var(--avp-bg-secondary) 100%); border: 1px solid var(--avp-glass-border); color: var(--avp-success); font-size: 16px; font-weight: 600; z-index: 100003; opacity: 0; pointer-events: none; transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); display: flex; align-items: center; gap: 12px; box-shadow: var(--avp-shadow-lg); } .avp-copy-feedback.active { opacity: 1; transform: translate(-50%, -50%) scale(1); } .avp-copy-feedback svg { width: 28px; height: 28px; fill: currentColor; } `; document.head.appendChild(style); } } /** * 主应用类 - 全面升级版本 */ class AIVideoParser { constructor() { this.extractors = [ new BilibiliExtractor(), new YouTubeExtractor() ]; this.currentExtractor = null; this.currentVideoInfo = null; this.parserService = new AIParserService(); this.historyManager = new HistoryManager(); this.isParsing = false; this.elements = {}; this.particleBg = null; this.dragState = { isDragging: false, startX: 0, startY: 0, offsetX: 0, offsetY: 0 }; } /** * 初始化应用 */ init() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.#setup()); } else { this.#setup(); } } /** * 设置应用 */ #setup() { this.currentExtractor = this.extractors.find(e => e.match()); if (!this.currentExtractor) return; StyleInjector.inject(); this.#createUI(); this.#registerMenuCommands(); this.#observePageChanges(); this.#setupKeyboardShortcuts(); } /** * 创建UI元素 */ #createUI() { this.#createGradientBg(); this.#createFloatButton(); this.#createPanel(); this.#createOverlay(); this.#createToast(); this.#createCopyFeedback(); this.#createShortcutsHint(); } /** * 创建动态渐变背景 */ #createGradientBg() { const bg = document.createElement('div'); bg.className = 'avp-gradient-bg'; bg.id = 'avp-gradient-bg'; document.body.appendChild(bg); this.elements.gradientBg = bg; } /** * 创建浮动按钮 */ #createFloatButton() { const btn = document.createElement('button'); btn.className = 'avp-float-btn'; btn.innerHTML = ` `; btn.title = 'AI视频解析 (Ctrl+Shift+A)'; btn.addEventListener('click', () => this.#openPanel()); document.body.appendChild(btn); this.elements.floatButton = btn; } /** * 创建解析面板 */ #createPanel() { const panel = document.createElement('div'); panel.className = 'avp-panel'; panel.id = 'avp-panel'; panel.innerHTML = `
    AI视频解析 v${VERSION.current}
    正在获取视频信息...
    --
    解析结果
    准备解析...
    准备就绪
    点击下方"开始解析"按钮,或使用快捷键获取AI智能分析结果
    Ctrl + Enter 开始解析
    `; document.body.appendChild(panel); this.elements.panel = panel; // 初始化粒子背景 const panelBody = panel.querySelector('.avp-panel-body'); this.particleBg = new ParticleBackground(panelBody); // 绑定拖拽 this.#setupDrag(); // 绑定面板内的事件 this.#bindPanelEvents(); } /** * 设置拖拽功能 */ #setupDrag() { const header = document.getElementById('avp-panel-header'); const panel = this.elements.panel; if (!header || !panel) return; header.addEventListener('mousedown', (e) => { if (e.target.closest('.avp-btn-icon')) return; this.dragState.isDragging = true; this.dragState.startX = e.clientX; this.dragState.startY = e.clientY; const rect = panel.getBoundingClientRect(); this.dragState.offsetX = rect.left + rect.width / 2; this.dragState.offsetY = rect.top + rect.height / 2; panel.classList.add('dragging'); }); document.addEventListener('mousemove', (e) => { if (!this.dragState.isDragging) return; const dx = e.clientX - this.dragState.startX; const dy = e.clientY - this.dragState.startY; const newLeft = this.dragState.offsetX + dx; const newTop = this.dragState.offsetY + dy; panel.style.left = `${newLeft}px`; panel.style.top = `${newTop}px`; panel.style.transform = 'translate(-50%, -50%)'; }); document.addEventListener('mouseup', () => { if (this.dragState.isDragging) { this.dragState.isDragging = false; panel.classList.remove('dragging'); } }); } /** * 创建遮罩层 */ #createOverlay() { const overlay = document.createElement('div'); overlay.className = 'avp-overlay'; overlay.addEventListener('click', () => this.#closePanel()); document.body.appendChild(overlay); this.elements.overlay = overlay; } /** * 创建提示消息 */ #createToast() { const toast = document.createElement('div'); toast.className = 'avp-toast'; document.body.appendChild(toast); this.elements.toast = toast; } /** * 创建复制成功反馈 */ #createCopyFeedback() { const feedback = document.createElement('div'); feedback.className = 'avp-copy-feedback'; feedback.innerHTML = ` 已复制到剪贴板 `; document.body.appendChild(feedback); this.elements.copyFeedback = feedback; } /** * 创建快捷键提示 */ #createShortcutsHint() { const hint = document.createElement('div'); hint.className = 'avp-shortcuts-hint'; hint.innerHTML = ` ESC 关闭面板 Ctrl+Enter 开始解析 H 历史记录 S 设置 `; document.body.appendChild(hint); this.elements.shortcutsHint = hint; } /** * 绑定面板事件 */ #bindPanelEvents() { this.elements.panel.querySelector('[data-action="close"]')?.addEventListener('click', () => this.#closePanel()); this.elements.panel.querySelector('[data-action="settings"]')?.addEventListener('click', () => this.#showSettings()); this.elements.panel.querySelector('[data-action="history"]')?.addEventListener('click', () => this.#showHistory()); document.getElementById('avp-btn-parse')?.addEventListener('click', () => this.#startParse()); document.getElementById('avp-btn-copy')?.addEventListener('click', () => this.#copyResult()); document.getElementById('avp-btn-export')?.addEventListener('click', () => this.#exportResult()); // 绑定折叠功能 this.#setupFoldable(); } /** * 设置可折叠标题 */ #setupFoldable() { document.addEventListener('click', (e) => { const foldable = e.target.closest('.md-foldable'); if (!foldable) return; foldable.classList.toggle('collapsed'); }); } /** * 设置键盘快捷键 */ #setupKeyboardShortcuts() { document.addEventListener('keydown', (e) => { // ESC关闭面板 if (e.key === 'Escape') { this.#closePanel(); return; } // 仅面板打开时生效的快捷键 if (!this.elements.panel?.classList.contains('active')) { // Ctrl+Shift+A 打开面板 if (e.ctrlKey && e.shiftKey && e.key === 'A') { e.preventDefault(); this.#openPanel(); } return; } // Ctrl+Enter 开始解析 if (e.ctrlKey && e.key === 'Enter') { e.preventDefault(); this.#startParse(); return; } // H 打开历史记录 if (e.key === 'h' || e.key === 'H') { const active = document.querySelector('.avp-history.active'); if (active) { this.#showMainContent(); } else { this.#showHistory(); } return; } // S 打开设置 if (e.key === 's' || e.key === 'S') { const active = document.getElementById('avp-settings-panel')?.classList.contains('active'); if (active) { this.#showMainContent(); } else { this.#showSettings(); } } }); } /** * 打开面板 */ #openPanel() { this.currentVideoInfo = this.currentExtractor.extract(); if (!this.currentVideoInfo) { this.#showToast('无法获取视频信息,请刷新页面重试', 'error'); return; } this.#updateVideoInfo(); this.elements.panel.classList.add('active'); this.elements.overlay.classList.add('active'); this.elements.gradientBg.classList.add('active'); // 显示快捷键提示 this.elements.shortcutsHint.classList.add('active'); setTimeout(() => { this.elements.shortcutsHint.classList.remove('active'); }, 4000); // 初始化粒子背景 if (this.particleBg && !this.particleBg.canvas) { this.particleBg.init(); } this.#resetParseState(); } /** * 关闭面板 */ #closePanel() { this.elements.panel.classList.remove('active'); this.elements.overlay.classList.remove('active'); this.elements.gradientBg.classList.remove('active'); this.elements.shortcutsHint.classList.remove('active'); // 重置面板位置 setTimeout(() => { this.elements.panel.style.left = '50%'; this.elements.panel.style.top = '50%'; this.elements.panel.style.transform = ''; }, 500); this.#showMainContent(); } /** * 更新视频信息展示 */ #updateVideoInfo() { const info = this.currentVideoInfo; document.getElementById('avp-video-title').textContent = info.title || '未知标题'; const platformNames = { 'bilibili': '哔哩哔哩', 'youtube': 'YouTube' }; const platformEl = document.getElementById('avp-platform'); platformEl.textContent = platformNames[info.platform] || info.platform; const metaEl = document.getElementById('avp-video-meta'); metaEl.innerHTML = ` ${platformNames[info.platform] || info.platform} ${info.author ? ` ${info.author} ` : ''} ${info.duration > 0 ? ` ${formatDuration(info.duration)} ` : ''} `; } /** * 重置解析状态 */ #resetParseState() { this.isParsing = false; document.getElementById('avp-progress')?.classList.remove('active'); document.getElementById('avp-result')?.classList.remove('active'); document.getElementById('avp-empty').style.display = 'block'; document.getElementById('avp-progress-fill').style.width = '0%'; document.getElementById('avp-progress-text').innerHTML = ` 准备解析... `; document.getElementById('avp-result').innerHTML = ''; } /** * 开始解析 */ async #startParse() { if (this.isParsing) return; this.isParsing = true; const emptyEl = document.getElementById('avp-empty'); const progressEl = document.getElementById('avp-progress'); const resultEl = document.getElementById('avp-result'); emptyEl.style.display = 'none'; progressEl.classList.add('active'); resultEl.classList.remove('active'); try { const result = await this.parserService.parse( this.currentVideoInfo, (progress, message) => { document.getElementById('avp-progress-fill').style.width = `${progress}%`; document.getElementById('avp-progress-text').innerHTML = ` ${message} `; } ); this.historyManager.add({ videoId: this.currentVideoInfo.videoId, platform: this.currentVideoInfo.platform, title: this.currentVideoInfo.title, author: this.currentVideoInfo.author, cover: this.currentVideoInfo.cover, result: result, duration: this.currentVideoInfo.duration, url: this.currentVideoInfo.url }); progressEl.classList.remove('active'); resultEl.innerHTML = renderMarkdown(result); resultEl.classList.add('active'); this.currentResult = result; // 自动折叠二级以下标题 this.#autoCollapseHeadings(); } catch (error) { this.#showToast(`解析失败: ${error.message}`, 'error'); progressEl.classList.remove('active'); emptyEl.style.display = 'block'; } finally { this.isParsing = false; } } /** * 自动折叠二级以下标题 */ #autoCollapseHeadings() { const h3s = document.querySelectorAll('.avp-result h3'); h3s.forEach(h => h.classList.add('collapsed')); } /** * 复制结果 */ #copyResult() { const resultEl = document.getElementById('avp-result'); if (!resultEl.textContent.trim()) { this.#showToast('暂无解析结果', 'error'); return; } const text = this.currentResult || resultEl.textContent; navigator.clipboard.writeText(text).then(() => { this.#showCopyFeedback(); }).catch(() => { const textarea = document.createElement('textarea'); textarea.value = text; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); this.#showCopyFeedback(); }); } /** * 显示复制成功反馈 */ #showCopyFeedback() { const feedback = this.elements.copyFeedback; feedback.classList.add('active'); setTimeout(() => { feedback.classList.remove('active'); }, 1500); } /** * 导出结果 */ #exportResult() { const resultEl = document.getElementById('avp-result'); if (!resultEl.textContent.trim()) { this.#showToast('暂无解析结果', 'error'); return; } const info = this.currentVideoInfo; const timestamp = new Date().toLocaleString('zh-CN'); const content = `# ${info.title} > 作者:${info.author || '未知'} > 平台:${info.platform} > 解析时间:${timestamp} > 原链接:${info.url} --- ${this.currentResult || resultEl.innerHTML} --- *由 AI Video Parser v${VERSION.current} 生成* `; const blob = new Blob([content], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `解析结果_${info.title.slice(0, 50)}.md`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); this.#showToast('已导出Markdown文件', 'success'); } /** * 显示设置面板 */ #showSettings() { const mainContent = this.elements.panel.querySelector('.avp-main-content'); const settingsPanel = document.getElementById('avp-settings-panel'); const historyPanel = document.getElementById('avp-history-panel'); const isActive = settingsPanel.classList.contains('active'); mainContent.style.display = isActive ? 'block' : 'none'; settingsPanel.classList.toggle('active', !isActive); historyPanel.classList.remove('active'); if (!isActive) { document.getElementById('setting-api-url').value = GM_getValue('api_url', CONFIG.API_URL); document.getElementById('setting-api-key').value = GM_getValue('api_key', CONFIG.API_KEY); document.getElementById('setting-model').value = GM_getValue('model', CONFIG.MODEL); } else { this.#saveSettings(); } } /** * 保存设置 */ #saveSettings() { const apiUrl = document.getElementById('setting-api-url').value.trim(); const apiKey = document.getElementById('setting-api-key').value.trim(); const model = document.getElementById('setting-model').value.trim(); if (apiUrl) GM_setValue('api_url', apiUrl); if (apiKey) GM_setValue('api_key', apiKey); if (model) GM_setValue('model', model); this.parserService.updateConfig(); this.#showToast('设置已保存', 'success'); } /** * 显示历史记录 */ #showHistory() { const mainContent = this.elements.panel.querySelector('.avp-main-content'); const settingsPanel = document.getElementById('avp-settings-panel'); const historyPanel = document.getElementById('avp-history-panel'); mainContent.style.display = 'none'; settingsPanel.classList.remove('active'); historyPanel.classList.add('active'); this.#renderHistory(); } /** * 渲染历史记录 */ #renderHistory() { const listEl = document.getElementById('avp-history-list'); const history = this.historyManager.getAll(); if (history.length === 0) { listEl.innerHTML = `
    暂无历史记录
    解析过的视频将显示在这里
    `; return; } listEl.innerHTML = history.map(item => `
    ${item.title || '未知视频'}
    ${item.platform} ${item.author || ''} · ${formatDate(item.timestamp)}
    `).join(''); listEl.querySelectorAll('.avp-history-item').forEach(item => { item.addEventListener('click', () => { const record = this.historyManager.getById(item.dataset.id); if (record) { this.#showMainContent(); this.currentVideoInfo = { videoId: record.videoId, platform: record.platform, title: record.title, author: record.author, cover: record.cover, duration: record.duration, url: record.url || '' }; this.currentResult = record.result; this.#updateVideoInfo(); document.getElementById('avp-empty').style.display = 'none'; document.getElementById('avp-progress').classList.remove('active'); const resultEl = document.getElementById('avp-result'); resultEl.innerHTML = renderMarkdown(record.result); resultEl.classList.add('active'); } }); }); } /** * 显示主内容 */ #showMainContent() { this.elements.panel.querySelector('.avp-main-content').style.display = 'block'; document.getElementById('avp-settings-panel').classList.remove('active'); document.getElementById('avp-history-panel').classList.remove('active'); } /** * 显示提示消息 */ #showToast(message, type = 'success') { const toast = this.elements.toast; const icons = { success: '', error: '', info: '' }; toast.innerHTML = `${icons[type] || icons.success}${message}`; toast.className = `avp-toast avp-toast-${type} active`; clearTimeout(this.toastTimer); this.toastTimer = setTimeout(() => { toast.classList.remove('active'); }, 3000); } /** * 注册菜单命令 */ #registerMenuCommands() { GM_registerMenuCommand('⚙️ 打开设置', () => { this.#openPanel(); setTimeout(() => this.#showSettings(), 100); }); GM_registerMenuCommand('📋 查看历史', () => { this.#openPanel(); setTimeout(() => this.#showHistory(), 100); }); GM_registerMenuCommand('🗑️ 清空历史', () => { if (confirm('确定要清空所有历史记录吗?')) { this.historyManager.clear(); this.#showToast('历史记录已清空', 'success'); } }); } /** * 监听页面变化(SPA支持) */ #observePageChanges() { let lastUrl = location.href; const observer = new MutationObserver(debounce(() => { const currentUrl = location.href; if (currentUrl !== lastUrl) { lastUrl = currentUrl; this.currentExtractor = this.extractors.find(e => e.match()); if (this.currentExtractor) { setTimeout(() => { this.currentVideoInfo = this.currentExtractor.extract(); }, 1000); } } }, 500)); observer.observe(document.body, { childList: true, subtree: true }); } } // ==================== 启动应用 ==================== const app = new AIVideoParser(); app.init(); })();