// ==UserScript== // @name Awesome Video List // @namespace http://tampermonkey.net/ // @version 0.0.1 // @description 跨视频片段playlist + 分享码同步 // @author You // @match https://www.bilibili.com/video/* // @match https://www.bilibili.com/list/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @run-at document-end // ==/UserScript== (function() { 'use strict'; console.log('[AVL] 脚本开始加载...'); const CONFIG = { maxFragments: 200, playlistStorageKey: 'avl_current_playlist', playlistIndexKey: 'avl_playlist_index' }; class DataManager { constructor() { this.storageKey = 'awesome_video_list'; console.log('[AVL] DataManager 初始化'); this.data = this.load(); this.currentPlaylist = this.loadCurrentPlaylist(); console.log('[AVL] DataManager 初始化完成,数据加载完成'); } load() { try { console.log('[AVL] DataManager.load 开始加载数据'); const data = JSON.parse(GM_getValue(this.storageKey, '{}')); console.log('[AVL] DataManager.load 数据加载完成,数据项数:', Object.keys(data).length); return data; } catch(e) { console.error('[AVL] DataManager.load 加载数据失败:', e); return {}; } } save() { console.log('[AVL] DataManager.save 保存数据,数据项数:', Object.keys(this.data).length); GM_setValue(this.storageKey, JSON.stringify(this.data)); console.log('[AVL] DataManager.save 数据保存完成'); } loadCurrentPlaylist() { try { console.log('[AVL] DataManager.loadCurrentPlaylist 开始加载播放列表'); const playlist = JSON.parse(GM_getValue(CONFIG.playlistStorageKey, '[]')); console.log('[AVL] DataManager.loadCurrentPlaylist 播放列表加载完成,片段数:', playlist.length); return playlist; } catch(e) { console.error('[AVL] DataManager.loadCurrentPlaylist 加载播放列表失败:', e); return []; } } saveCurrentPlaylist() { console.log('[AVL] DataManager.saveCurrentPlaylist 保存播放列表,片段数:', this.currentPlaylist.length); GM_setValue(CONFIG.playlistStorageKey, JSON.stringify(this.currentPlaylist)); console.log('[AVL] DataManager.saveCurrentPlaylist 播放列表保存完成'); } getFragments(bvid) { return this.data[bvid]?.fragments || []; } getFragment(bvid, fragmentId) { return this.getFragments(bvid).find(f => f.id === fragmentId); } addFragment(bvid, videoTitle, fragment) { if (!this.data[bvid]) { this.data[bvid] = { bvid: bvid, title: videoTitle, url: `https://bilibili.com/video/${bvid}`, fragments: [] }; } const exists = this.data[bvid].fragments.some(f => Math.abs(f.start - fragment.start) < 3 && Math.abs(f.end - fragment.end) < 3 ); if (exists) return false; fragment.id = Date.now().toString(36) + Math.random().toString(36).substr(2); fragment.createdAt = new Date().toISOString(); this.data[bvid].fragments.push(fragment); this.data[bvid].fragments.sort((a, b) => a.start - b.start); this.save(); return true; } deleteFragment(bvid, fragmentId) { if (!this.data[bvid]) return; this.data[bvid].fragments = this.data[bvid].fragments.filter(f => f.id !== fragmentId); this.save(); this.removeFromPlaylist(bvid, fragmentId); } addToPlaylist(bvid, fragmentId) { const fragment = this.getFragment(bvid, fragmentId); const video = this.data[bvid]; if (!fragment || !video) return false; const exists = this.currentPlaylist.some(item => item.bvid === bvid && item.fragmentId === fragmentId ); if (exists) return false; this.currentPlaylist.push({ bvid: bvid, fragmentId: fragmentId, title: fragment.title, videoTitle: video.title, start: fragment.start, end: fragment.end, tags: fragment.tags || [] }); this.saveCurrentPlaylist(); return true; } removeFromPlaylist(bvid, fragmentId) { const idx = this.currentPlaylist.findIndex(item => item.bvid === bvid && item.fragmentId === fragmentId ); if (idx > -1) { this.currentPlaylist.splice(idx, 1); this.adjustIndexAfterRemove(idx); this.saveCurrentPlaylist(); } } removeFromPlaylistByIndex(index) { if (index >= 0 && index < this.currentPlaylist.length) { this.currentPlaylist.splice(index, 1); this.adjustIndexAfterRemove(index); this.saveCurrentPlaylist(); } } adjustIndexAfterRemove(removedIndex) { const currentIdx = parseInt(GM_getValue(CONFIG.playlistIndexKey, '0')) || 0; if (removedIndex < currentIdx) { GM_setValue(CONFIG.playlistIndexKey, (currentIdx - 1).toString()); } else if (removedIndex === currentIdx && currentIdx >= this.currentPlaylist.length) { GM_setValue(CONFIG.playlistIndexKey, Math.max(0, this.currentPlaylist.length - 1).toString()); } } movePlaylistItem(index, direction) { const newIndex = index + direction; if (newIndex < 0 || newIndex >= this.currentPlaylist.length) return false; [this.currentPlaylist[index], this.currentPlaylist[newIndex]] = [this.currentPlaylist[newIndex], this.currentPlaylist[index]]; const currentIdx = parseInt(GM_getValue(CONFIG.playlistIndexKey, '0')) || 0; if (index === currentIdx) { GM_setValue(CONFIG.playlistIndexKey, newIndex.toString()); } else if (newIndex === currentIdx) { GM_setValue(CONFIG.playlistIndexKey, index.toString()); } this.saveCurrentPlaylist(); return true; } clearPlaylist() { this.currentPlaylist = []; GM_setValue(CONFIG.playlistIndexKey, '0'); this.saveCurrentPlaylist(); } getCurrentPlaylistIndex() { return parseInt(GM_getValue(CONFIG.playlistIndexKey, '0')) || 0; } setCurrentPlaylistIndex(index) { GM_setValue(CONFIG.playlistIndexKey, index.toString()); } exportCompactData() { const compactData = {}; Object.entries(this.data).forEach(([bvid, video]) => { compactData[bvid] = { t: video.title, f: video.fragments.map(f => [ Math.floor(f.start), Math.floor(f.end), f.title, (f.tags || []).join(','), f.note || '' ]) }; }); const exportObj = { v: 1, t: Date.now(), data: compactData, playlist: this.currentPlaylist.map(p => [p.bvid, p.fragmentId]) }; return exportObj; } generateShareCode() { const data = this.exportCompactData(); const json = JSON.stringify(data); const base64 = btoa(unescape(encodeURIComponent(json))); return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } async uploadToPastebinService() { const code = this.generateShareCode(); return new Promise((resolve, reject) => { // 使用标准的 FormData(浏览器原生 multipart/form-data) const formData = new FormData(); formData.append('content', code); formData.append('syntax', 'text'); formData.append('expiry_days', '7'); // 7天后过期 GM_xmlhttpRequest({ method: 'POST', url: 'https://dpaste.com/api/v2/', data: formData, headers: { 'Accept': 'text/plain' }, responseType: 'text', onload: (response) => { console.log('[AVL] dpaste 响应状态:', response.status); console.log('[AVL] dpaste 返回内容:', response.responseText); if (response.status === 200 || response.status === 201) { const url = response.responseText.trim(); // 验证返回的是 URL 格式 if (url.startsWith('https://dpaste.com/')) { resolve(url); } else { reject(new Error('dpaste 返回格式异常: ' + url.substring(0, 100))); } } else { reject(new Error(`dpaste HTTP ${response.status}: ${response.responseText}`)); } }, onerror: (err) => { console.error('[AVL] dpaste 网络错误:', err); reject(new Error('无法连接到 dpaste.com,请检查网络或 CSP 限制')); }, ontimeout: () => { reject(new Error('dpaste 请求超时(15秒)')); }, timeout: 15000 }); }); } // 从 dpaste 链接获取原始分享码内容 async fetchFromDpaste(url) { // 提取 key,支持 https://dpaste.com/223UMASZF 或 223UMASZF let key = url.trim(); if (key.includes('dpaste.com/')) { key = key.split('dpaste.com/').pop().split('/')[0].split('?')[0]; } // 确保是合法 key(通常是字母数字组合) if (!key || !/^[A-Z0-9]+$/i.test(key)) { throw new Error('无效的 dpaste 链接格式'); } const rawUrl = `https://dpaste.com/${key}.txt`; console.log('[AVL] 正在从 dpaste 获取:', rawUrl); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: rawUrl, headers: { 'Accept': 'text/plain' }, responseType: 'text', onload: (response) => { if (response.status === 200) { resolve(response.responseText); } else if (response.status === 404) { reject(new Error('该 dpaste 链接已过期或被删除')); } else { reject(new Error(`HTTP ${response.status}`)); } }, onerror: (err) => { reject(new Error('网络请求失败,无法获取分享内容')); }, timeout: 10000 }); }); } parseShareCode(code) { try { code = code.trim().replace(/\s/g, ''); let base64 = code.replace(/-/g, '+').replace(/_/g, '/'); while (base64.length % 4) base64 += '='; const json = decodeURIComponent(escape(atob(base64))); return JSON.parse(json); } catch(e) { console.error('[AVL] 分享码解析失败:', e); return null; } } importFromShareCode(shareCode) { const imported = this.parseShareCode(shareCode); if (!imported || !imported.data) return { success: false, error: '无效的分享码' }; let addedVideos = 0; let addedFragments = 0; let skippedFragments = 0; Object.entries(imported.data).forEach(([bvid, video]) => { if (!this.data[bvid]) { this.data[bvid] = { bvid: bvid, title: video.t, url: `https://bilibili.com/video/${bvid}`, fragments: [] }; addedVideos++; } video.f.forEach(fData => { const [start, end, title, tagsStr, note] = fData; const tags = tagsStr ? tagsStr.split(',') : []; const exists = this.data[bvid].fragments.some(existing => Math.abs(existing.start - start) < 3 && Math.abs(existing.end - end) < 3 ); if (!exists) { this.data[bvid].fragments.push({ id: Date.now().toString(36) + Math.random().toString(36).substr(2), start: start, end: end, title: title, tags: tags, note: note, createdAt: new Date().toISOString() }); addedFragments++; } else { skippedFragments++; } }); this.data[bvid].fragments.sort((a, b) => a.start - b.start); }); let addedPlaylist = 0; if (imported.playlist && Array.isArray(imported.playlist)) { imported.playlist.forEach(([bvid, fragmentId]) => { const exists = this.currentPlaylist.some(p => p.bvid === bvid && p.fragmentId === fragmentId ); if (!exists) { const video = this.data[bvid]; const fragment = video?.fragments.find(f => f.id === fragmentId); if (video && fragment) { this.currentPlaylist.push({ bvid: bvid, fragmentId: fragmentId, title: fragment.title, videoTitle: video.title, start: fragment.start, end: fragment.end, tags: fragment.tags || [] }); addedPlaylist++; } } }); } this.save(); this.saveCurrentPlaylist(); return { success: true, stats: { addedVideos, addedFragments, skippedFragments, addedPlaylist } }; } formatTime(seconds) { const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return `${m}:${s.toString().padStart(2, '0')}`; } } class PlaylistEngine { constructor(dataManager, uiManager) { console.log('[AVL] PlaylistEngine 初始化'); this.dm = dataManager; this.ui = uiManager; this.isNavigating = false; this.videoEndHandler = null; this.currentFragmentEndTime = null; this.attachVideoEndListener(); console.log('[AVL] PlaylistEngine 初始化完成'); } getCurrentItem() { const idx = this.dm.getCurrentPlaylistIndex(); return this.dm.currentPlaylist[idx]; } peekNext() { const idx = this.dm.getCurrentPlaylistIndex(); return this.dm.currentPlaylist[idx + 1]; } async playNext() { console.log('[AVL] PlaylistEngine.playNext 开始执行'); if (this.isNavigating) { console.log('[AVL] PlaylistEngine.playNext 正在导航中,跳过执行'); return; } const currentIdx = this.dm.getCurrentPlaylistIndex(); const nextIdx = currentIdx + 1; console.log('[AVL] PlaylistEngine.playNext 当前索引:', currentIdx, '下一个索引:', nextIdx); if (nextIdx >= this.dm.currentPlaylist.length) { console.log('[AVL] PlaylistEngine.playNext 播放列表已完成'); this.ui.showToast("🎉 播放列表已完成!"); return; } const nextItem = this.dm.currentPlaylist[nextIdx]; const currentBvid = this.ui.getBvid(); console.log('[AVL] PlaylistEngine.playNext 下一项:', nextItem, '当前BVID:', currentBvid); this.isNavigating = true; try { if (nextItem.bvid !== currentBvid) { console.log('[AVL] PlaylistEngine.playNext 需要跳转到不同视频:', nextItem.bvid); this.ui.showToast(`正在跳转到: ${nextItem.videoTitle}...`); await this.navigateToVideo(nextItem.bvid, nextItem.start); this.dm.setCurrentPlaylistIndex(nextIdx); } else { console.log('[AVL] PlaylistEngine.playNext 同一视频内跳转到:', nextItem.start); this.ui.seekTo(nextItem.start); this.dm.setCurrentPlaylistIndex(nextIdx); this.ui.renderPlaylist(); } } finally { this.isNavigating = false; console.log('[AVL] PlaylistEngine.playNext 执行完成'); } } async jumpToIndex(index) { console.log('[AVL] PlaylistEngine.jumpToIndex 开始执行,索引:', index); if (index < 0 || index >= this.dm.currentPlaylist.length) { console.log('[AVL] PlaylistEngine.jumpToIndex 索引无效,跳过执行'); return; } const item = this.dm.currentPlaylist[index]; const currentBvid = this.ui.getBvid(); console.log('[AVL] PlaylistEngine.jumpToIndex 目标项:', item, '当前BVID:', currentBvid); if (item.bvid !== currentBvid) { console.log('[AVL] PlaylistEngine.jumpToIndex 需要跳转到不同视频:', item.bvid); this.isNavigating = true; this.ui.showToast(`正在加载: ${item.videoTitle}...`); await this.navigateToVideo(item.bvid, item.start); this.dm.setCurrentPlaylistIndex(index); this.isNavigating = false; } else { console.log('[AVL] PlaylistEngine.jumpToIndex 同一视频内跳转到:', item.start); this.ui.seekTo(item.start); this.dm.setCurrentPlaylistIndex(index); } this.ui.renderPlaylist(); console.log('[AVL] PlaylistEngine.jumpToIndex 执行完成'); } navigateToVideo(bvid, startTime) { console.log('[AVL] PlaylistEngine.navigateToVideo 开始执行,BVID:', bvid, '开始时间:', startTime); return new Promise((resolve) => { const url = `https://www.bilibili.com/video/${bvid}?t=${Math.floor(startTime)}`; console.log('[AVL] PlaylistEngine.navigateToVideo 设置恢复播放会话存储'); sessionStorage.setItem('avl_resume_playlist', JSON.stringify({ index: this.dm.getCurrentPlaylistIndex(), time: startTime, timestamp: Date.now() })); let checked = false; const checkVideoLoaded = () => { if (checked) { console.log('[AVL] PlaylistEngine.navigateToVideo 视频已检查,跳过重复检查'); return; } const newBvid = this.ui.getBvid(); console.log('[AVL] PlaylistEngine.navigateToVideo 检查视频加载状态,当前BVID:', newBvid); if (newBvid === bvid) { checked = true; console.log('[AVL] PlaylistEngine.navigateToVideo 目标视频已加载,等待播放器准备'); setTimeout(() => { const video = document.querySelector('video'); if (video && video.readyState >= 1) { console.log('[AVL] PlaylistEngine.navigateToVideo 视频已准备就绪,跳转到指定时间'); this.ui.seekTo(startTime); resolve(); } else { console.log('[AVL] PlaylistEngine.navigateToVideo 视频未准备就绪,开始轮询检查'); const wait = setInterval(() => { const v = document.querySelector('video'); if (v && v.readyState >= 1) { console.log('[AVL] PlaylistEngine.navigateToVideo 轮询检查到视频已准备就绪,跳转到指定时间'); clearInterval(wait); this.ui.seekTo(startTime); resolve(); } }, 100); setTimeout(() => { console.log('[AVL] PlaylistEngine.navigateToVideo 轮询检查超时,强制resolve'); clearInterval(wait); resolve(); }, 5000); } }, 800); return true; } return false; }; if (this.ui.getBvid() === bvid) { console.log('[AVL] PlaylistEngine.navigateToVideo 已在目标视频页面,直接跳转到指定时间'); this.ui.seekTo(startTime); resolve(); return; } console.log('[AVL] PlaylistEngine.navigateToVideo 开始观察DOM变化以检测页面加载'); const observer = new MutationObserver(() => { if (checkVideoLoaded()) { console.log('[AVL] PlaylistEngine.navigateToVideo 检测到视频加载完成,断开观察器'); observer.disconnect(); } }); observer.observe(document.body, { subtree: true, childList: true }); console.log('[AVL] PlaylistEngine.navigateToVideo 跳转到URL:', url); window.location.href = url; setTimeout(() => { console.log('[AVL] PlaylistEngine.navigateToVideo 导航超时,断开观察器并resolve'); observer.disconnect(); resolve(); }, 6000); }); } attachVideoEndListener() { const checkAndAttach = () => { const video = document.querySelector('video'); if (video && !this.videoEndHandler) { this.videoEndHandler = () => { // 检查当前播放时间是否已达到当前片段的结束时间 const currentTime = video.currentTime; const currentItem = this.getCurrentItem(); if (currentItem && currentTime >= currentItem.end) { console.log('[AVL] 片段播放结束,自动播放下一个片段'); this.playNext(); } }; video.addEventListener('timeupdate', this.videoEndHandler); console.log('[AVL] 已附加视频时间更新事件监听器'); } }; checkAndAttach(); setInterval(checkAndAttach, 1000); } playPrev() { console.log('[AVL] PlaylistEngine.playPrev 开始执行'); const idx = this.dm.getCurrentPlaylistIndex(); console.log('[AVL] PlaylistEngine.playPrev 当前索引:', idx); if (idx > 0) { console.log('[AVL] PlaylistEngine.playPrev 跳转到上一个片段,索引:', idx - 1); this.jumpToIndex(idx - 1); } else { console.log('[AVL] PlaylistEngine.playPrev 当前已是第一个片段'); this.ui.showToast("已经是第一个片段"); } } } class UIManager { constructor(dataManager) { console.log('[AVL] UIManager 初始化'); this.dm = dataManager; this.engine = new PlaylistEngine(dataManager, this); this.tempFragment = null; this.currentTab = 'mark'; this.init(); console.log('[AVL] UIManager 初始化完成'); } init() { console.log('[AVL] UIManager.init 开始执行'); this.injectStyles(); this.createFloatingButton(); this.observeVideoChange(); this.checkResumeFromNavigation(); console.log('[AVL] UIManager.init 执行完成'); } checkResumeFromNavigation() { console.log('[AVL] UIManager.checkResumeFromNavigation 开始检查是否需要恢复播放'); const resume = sessionStorage.getItem('avl_resume_playlist'); if (resume) { console.log('[AVL] UIManager.checkResumeFromNavigation 找到恢复数据'); const data = JSON.parse(resume); if (Date.now() - data.timestamp < 30000) { console.log('[AVL] UIManager.checkResumeFromNavigation 恢复数据有效,设置播放列表索引:', data.index); this.dm.setCurrentPlaylistIndex(data.index); setTimeout(() => { this.seekTo(data.time); this.showToast(`已恢复播放位置 #${data.index + 1}`); }, 1000); } else { console.log('[AVL] UIManager.checkResumeFromNavigation 恢复数据已过期'); } sessionStorage.removeItem('avl_resume_playlist'); } else { console.log('[AVL] UIManager.checkResumeFromNavigation 未找到恢复数据'); } } injectStyles() { console.log('[AVL] UIManager.injectStyles 开始注入样式'); const css = ` .avl-panel { position: fixed; right: 20px; top: 80px; width: 380px; max-height: 85vh; background: rgba(255, 255, 255, 0.98); backdrop-filter: blur(12px); border-radius: 16px; box-shadow: 0 12px 40px rgba(0,0,0,0.2); z-index: 99999; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 14px; color: #333; overflow: hidden; display: none; flex-direction: column; border: 1px solid rgba(0,0,0,0.08); } .avl-header { padding: 16px 20px; background: linear-gradient(135deg, #00a1d6, #00b5e5); color: white; font-weight: bold; display: flex; justify-content: space-between; align-items: center; } .avl-tabs { display: flex; background: rgba(255,255,255,0.1); margin-top: 12px; border-radius: 8px; overflow: hidden; } .avl-tab { flex: 1; padding: 8px; text-align: center; cursor: pointer; font-size: 13px; opacity: 0.8; transition: all 0.2s; } .avl-tab.active { background: rgba(255,255,255,0.2); opacity: 1; font-weight: 600; } .avl-close { cursor: pointer; opacity: 0.8; font-size: 20px; margin-left: 12px; } .avl-close:hover { opacity: 1; } .avl-body { padding: 0; overflow-y: auto; max-height: calc(85vh - 120px); } .avl-tab-content { display: none; padding: 16px; } .avl-tab-content.active { display: block; } .avl-section { margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px solid #f0f0f0; } .avl-section:last-child { border-bottom: none; } .avl-section-title { font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; color: #999; margin-bottom: 12px; font-weight: 600; } .avl-btn { background: #00a1d6; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; margin-right: 8px; margin-bottom: 8px; transition: all 0.2s; display: inline-flex; align-items: center; gap: 4px; } .avl-btn:hover { background: #0089b5; transform: translateY(-1px); } .avl-btn.secondary { background: #f1f2f3; color: #666; } .avl-btn.secondary:hover { background: #e3e5e7; } .avl-btn.success { background: #00b5e5; } .avl-btn.danger { background: #fb7299; } .avl-btn.small { padding: 6px 12px; font-size: 12px; } .avl-input-group { margin-bottom: 12px; } .avl-input { width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 13px; box-sizing: border-box; margin-top: 4px; } .avl-input:focus { outline: none; border-color: #00a1d6; } .avl-share-box { background: linear-gradient(135deg, #f6f7f8, #ffffff); border: 2px dashed #00a1d6; border-radius: 12px; padding: 20px; text-align: center; margin: 16px 0; } .avl-share-code-display { font-family: 'Courier New', monospace; font-size: 14px; background: white; padding: 12px; border-radius: 8px; border: 1px solid #e0e0e0; word-break: break-all; line-height: 1.6; color: #333; margin: 12px 0; max-height: 200px; overflow-y: auto; } .avl-share-stats { font-size: 12px; color: #666; margin-top: 8px; } .avl-share-input { width: 100%; min-height: 120px; font-family: monospace; font-size: 13px; padding: 12px; border: 2px solid #ddd; border-radius: 8px; resize: vertical; box-sizing: border-box; } .avl-share-input:focus { border-color: #00a1d6; outline: none; } .avl-modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 100000; display: none; align-items: center; justify-content: center; } .avl-modal-overlay.active { display: flex; } .avl-modal { background: white; border-radius: 16px; padding: 24px; width: 90%; max-width: 500px; max-height: 80vh; overflow-y: auto; box-shadow: 0 20px 60px rgba(0,0,0,0.3); } .avl-modal h3 { margin: 0 0 16px 0; color: #00a1d6; } .avl-playlist-stats { background: #f6f7f8; padding: 12px; border-radius: 8px; margin-bottom: 16px; display: flex; justify-content: space-between; align-items: center; font-size: 13px; } .avl-playlist-item { background: white; border: 1px solid #f0f0f0; border-radius: 10px; padding: 12px; margin-bottom: 10px; position: relative; cursor: pointer; transition: all 0.2s; display: flex; gap: 10px; align-items: flex-start; } .avl-playlist-item:hover { border-color: #00a1d6; } .avl-playlist-item.active { background: linear-gradient(135deg, rgba(0,161,214,0.05), rgba(0,181,229,0.1)); border-color: #00a1d6; border-left: 3px solid #00a1d6; } .avl-playlist-number { width: 24px; height: 24px; background: #f1f2f3; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; color: #666; flex-shrink: 0; } .avl-playlist-item.active .avl-playlist-number { background: #00a1d6; color: white; } .avl-playlist-content { flex: 1; min-width: 0; } .avl-playlist-title { font-weight: 500; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .avl-playlist-meta { font-size: 11px; color: #999; display: flex; gap: 8px; flex-wrap: wrap; } .avl-playlist-video-title { color: #00a1d6; font-weight: 500; } .avl-playlist-time { font-family: monospace; background: rgba(0,161,214,0.1); color: #00a1d6; padding: 2px 6px; border-radius: 4px; } .avl-playlist-controls { display: flex; gap: 4px; opacity: 0; transition: opacity 0.2s; } .avl-playlist-item:hover .avl-playlist-controls { opacity: 1; } .avl-playlist-btn { width: 28px; height: 28px; border: none; background: #f1f2f3; border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 12px; color: #666; } .avl-playlist-btn:hover { background: #e3e5e7; color: #333; } .avl-playlist-btn.delete:hover { background: #ffe6ea; color: #fb7299; } .avl-empty-state { text-align: center; padding: 40px 20px; color: #999; } .avl-empty-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.3; } .avl-fragment-item { background: #f6f7f8; border-radius: 8px; padding: 12px; margin-bottom: 10px; border-left: 3px solid #00a1d6; } .avl-fragment-time { font-family: monospace; color: #00a1d6; font-weight: bold; font-size: 12px; margin-bottom: 4px; } .avl-fragment-title { font-weight: 500; margin-bottom: 8px; } .avl-fragment-tags { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 8px; } .avl-tag { background: rgba(0, 161, 214, 0.1); color: #00a1d6; padding: 2px 6px; border-radius: 4px; font-size: 11px; } .avl-player-bar { position: fixed; bottom: 0; left: 0; right: 0; background: white; border-top: 1px solid #f0f0f0; padding: 12px 20px; display: none; align-items: center; gap: 16px; z-index: 99998; box-shadow: 0 -4px 20px rgba(0,0,0,0.1); } .avl-player-bar.active { display: flex; } .avl-player-info { flex: 1; font-size: 14px; } .avl-player-title { font-weight: 500; color: #333; margin-bottom: 2px; } .avl-player-subtitle { font-size: 12px; color: #999; } .avl-player-buttons { display: flex; gap: 8px; } .avl-fab { position: fixed; right: 30px; bottom: 100px; width: 56px; height: 56px; background: #00a1d6; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 12px rgba(0, 161, 214, 0.4); z-index: 99999; font-size: 24px; border: none; transition: all 0.3s; } .avl-fab:hover { transform: scale(1.1) rotate(90deg); box-shadow: 0 6px 20px rgba(0, 161, 214, 0.5); } .avl-fab.has-playlist { background: #00b5e5; animation: pulse 2s infinite; } @keyframes pulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(0, 181, 229, 0.4); } 50% { box-shadow: 0 0 0 10px rgba(0, 181, 229, 0); } } .avl-toast { position: fixed; bottom: 100px; left: 50%; transform: translateX(-50%) translateY(20px); background: rgba(0,0,0,0.85); color: white; padding: 12px 24px; border-radius: 24px; z-index: 100001; font-size: 14px; opacity: 0; transition: all 0.3s; pointer-events: none; } .avl-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); } `; const style = document.createElement('style'); style.textContent = css; document.head.appendChild(style); } createFloatingButton() { console.log('[AVL] UIManager.createFloatingButton 开始创建浮动按钮'); const btn = document.createElement('button'); btn.className = 'avl-fab'; btn.innerHTML = '✦'; btn.title = 'Awesome Video List'; btn.onclick = () => this.togglePanel(); document.body.appendChild(btn); this.fab = btn; this.updateFabState(); console.log('[AVL] UIManager.createFloatingButton 浮动按钮创建完成'); } updateFabState() { console.log('[AVL] UIManager.updateFabState 开始更新浮动按钮状态,当前播放列表长度:', this.dm.currentPlaylist.length); if (this.dm.currentPlaylist.length > 0) { this.fab.classList.add('has-playlist'); this.fab.title = `播放列表 (${this.dm.currentPlaylist.length})`; console.log('[AVL] UIManager.updateFabState 设置为有播放列表状态'); } else { this.fab.classList.remove('has-playlist'); this.fab.title = 'Awesome Video List'; console.log('[AVL] UIManager.updateFabState 设置为无播放列表状态'); } } // 关键修复:createPanel 不再使用 onclick="window.AVL...",而是使用 id 和内部绑定 createPanel() { console.log('[AVL] UIManager.createPanel 开始创建面板'); const panel = document.createElement('div'); panel.className = 'avl-panel'; panel.id = 'avl-panel'; // 使用 id 替代 onclick panel.innerHTML = `