// ==UserScript== // @name B站合集列表管理器(抽屉面板) // @namespace http://tampermonkey.net/ // @version 4.0 // @description 侧边抽屉式面板,可视化勾选视频,按脚本列表顺序播放。MemoryList 名称标识,跨视频自动恢复。 // @author Anonymity喵 // @match *://www.bilibili.com/* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_notification // @run-at document-start // @license MIT // ==/UserScript== (function() { 'use strict'; const STORAGE_KEY = 'bili_force_video_data_v3'; // 旧版兼容,不再主用 const MEMORY_STORAGE_KEY = 'bili_force_memory_list'; // 新版名称+位图存储 const LOG_PREFIX = '[列表管理]'; // ========== 存储操作 ========== function getAllData() { try { const data = GM_getValue(STORAGE_KEY, '{}'); return JSON.parse(data); } catch (e) { console.warn(LOG_PREFIX, '读取存储失败', e); return {}; } } function saveAllData(data) { GM_setValue(STORAGE_KEY, JSON.stringify(data)); } // 新版 MemoryList 操作 function getMemoryList() { try { const data = GM_getValue(MEMORY_STORAGE_KEY, '{}'); return JSON.parse(data); } catch (e) { console.warn(LOG_PREFIX, '读取 MemoryList 失败', e); return {}; } } function saveMemoryList(data) { GM_setValue(MEMORY_STORAGE_KEY, JSON.stringify(data)); } // 获取页面合集标题 function getListName() { const titleEl = document.querySelector('.video-pod__header .title'); if (titleEl) return titleEl.textContent.trim(); // 备选:从路径或meta获取 const metaTitle = document.querySelector('meta[property="og:title"]'); if (metaTitle) { let t = metaTitle.getAttribute('content') || ''; t = t.replace(/^[^_]*_/,'').trim(); // 去掉作者前缀 return t || '未知合集'; } return '未知合集'; } // 兼容旧版(不再使用,但保留函数以防报错) function getCollectionIdFromPage() { const container = document.querySelector('.video-pod__list'); if (!container) return null; const items = container.querySelectorAll('[data-key]'); const bvs = new Set(); items.forEach(el => { const bv = el.getAttribute('data-key'); if (bv && bv.startsWith('BV')) bvs.add(bv); }); if (bvs.size === 0) return null; const sorted = Array.from(bvs).sort(); return 'list_' + sorted.join('|'); } // 从 MemoryList 按名称恢复勾选 function restoreCheckedSetFromMemoryList(listName, videoData) { const memory = getMemoryList(); // 查找同名列表 let matchedKey = null; for (const key in memory) { if (memory[key] && memory[key][0] === listName) { matchedKey = key; break; } } if (!matchedKey) return null; const bitmap = memory[matchedKey][1] || ''; const checkedSet = new Set(); let idx = 0; // 位图索引 for (const v of videoData) { if (v.isMulti && v.subVideos.length > 0) { for (const sv of v.subVideos) { if (idx < bitmap.length && bitmap[idx] === '1') { checkedSet.add(sv.id); } idx++; } } else { if (idx < bitmap.length && bitmap[idx] === '1') { checkedSet.add(v.id); } idx++; } } return checkedSet; } // 基于当前勾选状态更新 MemoryList function updateMemoryListFromCheckedSet() { const listName = getListName(); const memory = getMemoryList(); // 检查是否已存在同名 let existingKey = null; for (const key in memory) { if (memory[key] && memory[key][0] === listName) { existingKey = key; break; } } if (existingKey) { // 已存在,提示并拒绝覆盖(这里直接返回,不保存) showToast('❌ 该列表已添加,无法重复添加'); console.warn(LOG_PREFIX, `列表"${listName}"已存在(${existingKey}),拒绝覆盖`); return false; } // 生成位图字符串 let bitmap = ''; for (const v of panelState.videoData) { if (v.isMulti && v.subVideos.length > 0) { for (const sv of v.subVideos) { bitmap += panelState.checkedSet.has(sv.id) ? '1' : '0'; } } else { bitmap += panelState.checkedSet.has(v.id) ? '1' : '0'; } } // 分配新代号 const keys = Object.keys(memory); const newKey = 'l' + (keys.length + 1); memory[newKey] = [listName, bitmap]; saveMemoryList(memory); console.log(LOG_PREFIX, `MemoryList 已更新: ${newKey} -> ${listName}, 位图长度: ${bitmap.length}`); return true; } // 从 MemoryList 获取当前列表的勾选ID(用于播放跳转) function getCheckedIdsFromMemoryList(listName, videoData) { const memory = getMemoryList(); let matchedKey = null; for (const key in memory) { if (memory[key] && memory[key][0] === listName) { matchedKey = key; break; } } if (!matchedKey) return []; const bitmap = memory[matchedKey][1] || ''; const ids = []; let idx = 0; for (const v of videoData) { if (v.isMulti && v.subVideos.length > 0) { for (const sv of v.subVideos) { if (idx < bitmap.length && bitmap[idx] === '1') { ids.push(sv.id); } idx++; } } else { if (idx < bitmap.length && bitmap[idx] === '1') { ids.push(v.id); } idx++; } } return ids; } // 兼容旧版读取(已不再使用,但保留 fallback) function getListForCurrentCollection() { // 优先从 MemoryList 获取 const listName = getListName(); const videoData = panelState.videoData.length ? panelState.videoData : parsePageVideoList(); const ids = getCheckedIdsFromMemoryList(listName, videoData); if (ids.length > 0) return ids; // 回退旧版 const cid = getCollectionIdFromPage(); if (!cid) return []; const data = getAllData(); return Array.isArray(data[cid]) ? data[cid] : []; } function saveListForCurrentCollection(list) { // 仅保留兼容,实际保存走 MemoryList const cid = getCollectionIdFromPage(); if (!cid) return false; const data = getAllData(); data[cid] = list; saveAllData(data); return true; } function getCurrentBVID() { const match = location.pathname.match(/BV[0-9A-Za-z]+/); return match ? match[0] : null; } // ========== 解析页面视频列表 ========== function parsePageVideoList() { const container = document.querySelector('.video-pod__list'); if (!container) return []; const items = container.querySelectorAll('.pod-item'); const videos = []; items.forEach((item, index) => { const bv = item.getAttribute('data-key'); if (!bv || !bv.startsWith('BV')) return; const isMulti = item.classList.contains('multi-p'); const titleEl = item.querySelector('.simple-base-item.normal .title-txt, .simple-base-item.head .title-txt, .simple-base-item.active .title-txt'); const durationEl = item.querySelector('.single-p .duration, .multi-p .head .duration'); const title = titleEl ? titleEl.textContent.trim() : '未知标题'; const duration = durationEl ? durationEl.textContent.trim() : ''; if (isMulti) { const pageList = item.querySelector('.page-list'); const subItems = pageList ? pageList.querySelectorAll('.page-item.sub') : []; const subVideos = []; subItems.forEach((sub, subIdx) => { const subTitleEl = sub.querySelector('.title-txt'); const subDurationEl = sub.querySelector('.duration'); subVideos.push({ id: bv + '::' + subIdx, bv: bv, pIndex: subIdx, title: subTitleEl ? subTitleEl.textContent.trim() : '分P' + (subIdx + 1), duration: subDurationEl ? subDurationEl.textContent.trim() : '', isSub: true }); }); videos.push({ id: bv, bv: bv, title: title, duration: duration, isMulti: true, subVideos: subVideos, expanded: false }); } else { videos.push({ id: bv, bv: bv, title: title, duration: duration, isMulti: false, subVideos: [] }); } }); return videos; } // ========== 创建UI ========== function injectStyles() { const styleId = 'bili-force-drawer-styles'; if (document.getElementById(styleId)) return; const style = document.createElement('style'); style.id = styleId; style.textContent = ` #bili-force-trigger { position: fixed; right: 0; top: 50%; transform: translateY(-50%) translateX(92px); z-index: 99998; display: flex; align-items: center; justify-content: flex-start; width: 120px; height: 44px; background: linear-gradient(135deg, #00a1d6, #0088b0); border-radius: 8px 0 0 8px; cursor: pointer; user-select: none; box-shadow: -3px 2px 12px rgba(0,0,0,0.2); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); padding-left: 10px; box-sizing: border-box; gap: 8px; overflow: hidden; } #bili-force-trigger:hover { transform: translateY(-50%) translateX(0); } #bili-force-trigger.panel-open { transform: translateY(-50%) translateX(0); } #bili-force-trigger .trigger-icon { flex-shrink: 0; width: 22px; height: 22px; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 16px; } #bili-force-trigger .trigger-text { color: #fff; font-size: 13px; font-weight: 500; white-space: nowrap; letter-spacing: 0.5px; opacity: 0; transition: opacity 0.25s ease 0.05s; } #bili-force-trigger:hover .trigger-text, #bili-force-trigger.panel-open .trigger-text { opacity: 1; } #bili-force-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.45); z-index: 99997; opacity: 0; pointer-events: none; transition: opacity 0.35s ease; } #bili-force-overlay.active { opacity: 1; pointer-events: auto; } #bili-force-drawer { position: fixed; top: 0; right: 0; width: 440px; max-width: 92vw; height: 100%; z-index: 99999; background: #1a1a2e; color: #e0e0e0; display: flex; flex-direction: column; transform: translateX(100%); transition: transform 0.38s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: -4px 0 30px rgba(0,0,0,0.5); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; } #bili-force-drawer.active { transform: translateX(0); } #bili-force-drawer .drawer-header { flex-shrink: 0; padding: 18px 20px 14px; border-bottom: 1px solid rgba(255,255,255,0.1); display: flex; flex-direction: column; gap: 12px; } #bili-force-drawer .drawer-header .header-top { display: flex; align-items: center; justify-content: space-between; } #bili-force-drawer .drawer-title { font-size: 17px; font-weight: 600; color: #fff; letter-spacing: 0.5px; } #bili-force-drawer .drawer-close { width: 32px; height: 32px; border: none; background: rgba(255,255,255,0.08); color: #ccc; border-radius: 6px; cursor: pointer; font-size: 18px; display: flex; align-items: center; justify-content: center; transition: all 0.2s; line-height: 1; } #bili-force-drawer .drawer-close:hover { background: rgba(255,255,255,0.18); color: #fff; } #bili-force-drawer .header-actions { display: flex; gap: 8px; flex-wrap: wrap; } #bili-force-drawer .action-btn { padding: 7px 15px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.05); color: #ccc; cursor: pointer; font-size: 12px; font-weight: 500; transition: all 0.2s; white-space: nowrap; } #bili-force-drawer .action-btn:hover { background: rgba(255,255,255,0.12); color: #fff; border-color: rgba(255,255,255,0.35); } #bili-force-drawer .action-btn.btn-save { background: #00a1d6; border-color: #00a1d6; color: #fff; font-weight: 600; } #bili-force-drawer .action-btn.btn-save:hover { background: #00b8f0; border-color: #00b8f0; } #bili-force-drawer .action-btn.btn-save.saved-flash { background: #52c41a; border-color: #52c41a; } #bili-force-drawer .list-info { font-size: 11px; color: #999; padding: 0 4px; } #bili-force-drawer .list-info span { color: #00a1d6; font-weight: 600; } #bili-force-drawer .drawer-body { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 8px 0; scroll-behavior: smooth; } #bili-force-drawer .drawer-body::-webkit-scrollbar { width: 5px; } #bili-force-drawer .drawer-body::-webkit-scrollbar-track { background: transparent; } #bili-force-drawer .drawer-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 10px; } #bili-force-drawer .drawer-body::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.25); } #bili-force-drawer .video-item { display: flex; align-items: center; padding: 10px 20px; cursor: pointer; transition: background 0.15s; gap: 10px; border-bottom: 1px solid rgba(255,255,255,0.04); min-height: 48px; } #bili-force-drawer .video-item:hover { background: rgba(255,255,255,0.04); } #bili-force-drawer .video-item.sub-item { padding-left: 50px; background: rgba(255,255,255,0.015); } #bili-force-drawer .video-item.sub-item:hover { background: rgba(255,255,255,0.05); } #bili-force-drawer .video-item .checkbox-wrap { flex-shrink: 0; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; } #bili-force-drawer .video-item input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; accent-color: #00a1d6; margin: 0; } #bili-force-drawer .video-item .video-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; } #bili-force-drawer .video-item .video-title { font-size: 13px; color: #d0d0d0; line-height: 1.4; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #bili-force-drawer .video-item .video-meta { font-size: 11px; color: #777; display: flex; gap: 8px; } #bili-force-drawer .video-item .expand-icon { flex-shrink: 0; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; color: #888; transition: transform 0.25s; font-size: 12px; cursor: pointer; } #bili-force-drawer .video-item .expand-icon.expanded { transform: rotate(90deg); } #bili-force-drawer .video-item .bv-tag { font-size: 10px; color: #666; background: rgba(255,255,255,0.05); padding: 1px 6px; border-radius: 3px; flex-shrink: 0; } #bili-force-drawer .video-item.current-playing { background: rgba(0,161,214,0.12); border-left: 3px solid #00a1d6; } #bili-force-drawer .video-item.current-playing .video-title { color: #00a1d6; font-weight: 500; } #bili-force-drawer .drawer-footer { flex-shrink: 0; padding: 12px 20px; border-top: 1px solid rgba(255,255,255,0.1); display: flex; gap: 8px; } #bili-force-drawer .drawer-footer .action-btn { flex: 1; text-align: center; padding: 10px; font-size: 13px; } #bili-force-toast { position: fixed; top: 20px; left: 50%; transform: translateX(-50%) translateY(-120px); z-index: 100000; background: #333; color: #fff; padding: 10px 22px; border-radius: 8px; font-size: 14px; pointer-events: none; transition: transform 0.35s cubic-bezier(0.18, 0.89, 0.32, 1.05); box-shadow: 0 4px 20px rgba(0,0,0,0.4); } #bili-force-toast.show { transform: translateX(-50%) translateY(0); } `; document.head.appendChild(style); } function createToast() { let toast = document.getElementById('bili-force-toast'); if (!toast) { toast = document.createElement('div'); toast.id = 'bili-force-toast'; document.body.appendChild(toast); } return toast; } function showToast(msg, duration = 2000) { const toast = createToast(); toast.textContent = msg; toast.classList.add('show'); clearTimeout(toast._timeout); toast._timeout = setTimeout(() => { toast.classList.remove('show'); }, duration); } function createTriggerButton() { let trigger = document.getElementById('bili-force-trigger'); if (trigger && document.body.contains(trigger)) return trigger; if (trigger) trigger.remove(); trigger = document.createElement('div'); trigger.id = 'bili-force-trigger'; trigger.innerHTML = ` 列表管理 `; document.body.appendChild(trigger); return trigger; } function createOverlay() { let overlay = document.getElementById('bili-force-overlay'); if (!overlay) { overlay = document.createElement('div'); overlay.id = 'bili-force-overlay'; document.body.appendChild(overlay); } return overlay; } function createDrawer() { let drawer = document.getElementById('bili-force-drawer'); if (!drawer) { drawer = document.createElement('div'); drawer.id = 'bili-force-drawer'; drawer.innerHTML = `
📋 播放列表管理
已勾选 0 个视频
`; document.body.appendChild(drawer); } return drawer; } // ========== 面板逻辑 ========== let panelState = { open: false, videoData: [], checkedSet: new Set(), expandedMultis: new Set() }; function renderVideoList() { const drawer = document.getElementById('bili-force-drawer'); if (!drawer) return; const body = drawer.querySelector('.drawer-body'); if (!body) return; const currentBv = getCurrentBVID(); const videoData = panelState.videoData; let html = ''; if (videoData.length === 0) { html = '
当前页面未检测到合集视频列表
'; } else { videoData.forEach((v, idx) => { if (v.isMulti && v.subVideos.length > 0) { const isExpanded = panelState.expandedMultis.has(v.bv); const anySubChecked = v.subVideos.some(sv => panelState.checkedSet.has(sv.id)); const allSubChecked = v.subVideos.every(sv => panelState.checkedSet.has(sv.id)); const isCurrentParent = currentBv === v.bv; html += `
${escapeHtml(v.title)}📁 ${v.subVideos.length}个分P${v.duration} ${v.bv}
`; if (isExpanded) { v.subVideos.forEach((sv, sIdx) => { const checked = panelState.checkedSet.has(sv.id); const isCurrentSub = currentBv === sv.bv; html += `
${escapeHtml(sv.title)}⏱ ${sv.duration} P${sv.pIndex + 1}
`; }); } } else { const checked = panelState.checkedSet.has(v.id); const isCurrent = currentBv === v.bv; html += `
${escapeHtml(v.title)}⏱ ${v.duration} ${v.bv}
`; } }); } body.innerHTML = html; updateCheckedCount(); updateSaveButtonState(); } function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } function updateCheckedCount() { const drawer = document.getElementById('bili-force-drawer'); if (!drawer) return; const countEl = drawer.querySelector('.checked-count'); if (countEl) { countEl.textContent = panelState.checkedSet.size; } } function updateSaveButtonState() { const drawer = document.getElementById('bili-force-drawer'); if (!drawer) return; const savedList = new Set(getListForCurrentCollection()); const currentSet = panelState.checkedSet; const isChanged = savedList.size !== currentSet.size || ![...savedList].every(v => currentSet.has(v)); const saveBtns = drawer.querySelectorAll('.btn-save'); saveBtns.forEach(btn => { if (isChanged) { btn.textContent = '💾 保存列表'; btn.classList.remove('saved-flash'); btn.style.opacity = '1'; } else { btn.textContent = '✅ 已保存'; btn.classList.add('saved-flash'); } }); } function collectCheckedIds() { const drawer = document.getElementById('bili-force-drawer'); if (!drawer) return []; const checkboxes = drawer.querySelectorAll('.drawer-body input[type="checkbox"]:checked'); const ids = []; checkboxes.forEach(cb => { const id = cb.getAttribute('data-id'); if (id && cb.getAttribute('data-is-parent') !== '1') { ids.push(id); } }); return ids; } function openPanel() { const trigger = document.getElementById('bili-force-trigger'); const drawer = document.getElementById('bili-force-drawer'); const overlay = document.getElementById('bili-force-overlay'); panelState.videoData = parsePageVideoList(); panelState.expandedMultis.clear(); // 优先从 MemoryList 恢复勾选状态 const listName = getListName(); const restored = restoreCheckedSetFromMemoryList(listName, panelState.videoData); if (restored) { panelState.checkedSet = restored; console.log(LOG_PREFIX, `从 MemoryList 恢复勾选: ${listName} (${restored.size} 项)`); } else { // 回退旧方式 const savedList = getListForCurrentCollection(); panelState.checkedSet = new Set(savedList); } if (drawer) drawer.classList.add('active'); if (overlay) overlay.classList.add('active'); if (trigger) trigger.classList.add('panel-open'); panelState.open = true; renderVideoList(); updateCheckedCount(); console.log(LOG_PREFIX, `面板已打开,检测到 ${panelState.videoData.length} 个视频(含多P分组)`); } function closePanel() { const trigger = document.getElementById('bili-force-trigger'); const drawer = document.getElementById('bili-force-drawer'); const overlay = document.getElementById('bili-force-overlay'); if (drawer) drawer.classList.remove('active'); if (overlay) overlay.classList.remove('active'); if (trigger) trigger.classList.remove('panel-open'); panelState.open = false; } function togglePanel() { if (panelState.open) { closePanel(); } else { openPanel(); } } function saveCurrentList() { // 更新 MemoryList(含重复检查) const success = updateMemoryListFromCheckedSet(); if (!success) return; // 提示已弹出 // 同步更新旧版存储(兼容,可省略) const ids = [...panelState.checkedSet]; saveListForCurrentCollection(ids); updateCheckedCount(); updateSaveButtonState(); showToast(`✅ 列表已保存!共 ${ids.length} 个视频`); console.log(LOG_PREFIX, `保存列表: ${ids.length} 个条目`, ids); checkAndHijack(); } // ========== 事件绑定 ========== function bindEvents() { const drawer = document.getElementById('bili-force-drawer'); const overlay = document.getElementById('bili-force-overlay'); const trigger = document.getElementById('bili-force-trigger'); if (trigger) { trigger.addEventListener('click', (e) => { e.stopPropagation(); togglePanel(); }); } if (overlay) { overlay.addEventListener('click', () => { closePanel(); }); } if (drawer) { drawer.addEventListener('click', (e) => { const target = e.target; if (target.closest('.drawer-close') || target.closest('.btn-close-panel')) { closePanel(); return; } if (target.closest('.btn-save')) { saveCurrentList(); if (target.closest('.btn-save-bottom')) { setTimeout(closePanel, 400); } return; } // 全选 if (target.closest('.btn-select-all')) { const allIds = []; panelState.videoData.forEach(v => { if (v.isMulti && v.subVideos.length > 0) { v.subVideos.forEach(sv => allIds.push(sv.id)); } else { allIds.push(v.id); } if (v.isMulti) panelState.expandedMultis.add(v.bv); }); panelState.checkedSet = new Set(allIds); renderVideoList(); updateCheckedCount(); updateSaveButtonState(); return; } // 反选 if (target.closest('.btn-invert')) { const allIds = []; panelState.videoData.forEach(v => { if (v.isMulti && v.subVideos.length > 0) { v.subVideos.forEach(sv => allIds.push(sv.id)); } else { allIds.push(v.id); } if (v.isMulti) panelState.expandedMultis.add(v.bv); }); const newSet = new Set(panelState.checkedSet); allIds.forEach(id => { if (newSet.has(id)) newSet.delete(id); else newSet.add(id); }); panelState.checkedSet = newSet; renderVideoList(); updateCheckedCount(); updateSaveButtonState(); return; } // 取消全选 if (target.closest('.btn-deselect-all')) { panelState.videoData.forEach(v => { if (v.isMulti) panelState.expandedMultis.add(v.bv); }); panelState.checkedSet.clear(); renderVideoList(); updateCheckedCount(); updateSaveButtonState(); return; } // 多P展开/折叠 const expandIcon = target.closest('.expand-icon'); if (expandIcon) { const parentItem = expandIcon.closest('.video-item'); if (parentItem) { const bv = parentItem.getAttribute('data-bv'); if (bv) { if (panelState.expandedMultis.has(bv)) { panelState.expandedMultis.delete(bv); } else { panelState.expandedMultis.add(bv); } renderVideoList(); } } return; } // 多P父级行点击(非复选框区域) const multiParent = target.closest('.multi-parent'); if (multiParent && !target.closest('input[type="checkbox"]') && !target.closest('.expand-icon')) { const bv = multiParent.getAttribute('data-bv'); if (bv) { if (panelState.expandedMultis.has(bv)) { panelState.expandedMultis.delete(bv); } else { panelState.expandedMultis.add(bv); } renderVideoList(); } return; } // 复选框变化 if (target.closest('input[type="checkbox"]')) { setTimeout(() => { panelState.checkedSet = new Set(collectCheckedIds()); updateCheckedCount(); updateSaveButtonState(); const allParentCbs = drawer.querySelectorAll('input[data-is-parent="1"]'); allParentCbs.forEach(pCb => { const bv = pCb.getAttribute('data-id'); const subCbs = drawer.querySelectorAll(`input[data-id^="${bv}::"]`); if (subCbs.length > 0) { const allChecked = Array.from(subCbs).every(c => c.checked); const anyChecked = Array.from(subCbs).some(c => c.checked); pCb.checked = allChecked; pCb.indeterminate = anyChecked && !allChecked; } }); }, 50); return; } // 点击视频项跳转(非复选框、非展开图标) const videoItem = target.closest('.video-item'); if (videoItem && !target.closest('input[type="checkbox"]') && !target.closest('.expand-icon') && !target.closest('.multi-parent')) { const bv = videoItem.getAttribute('data-bv'); const pIndex = videoItem.getAttribute('data-pindex'); if (bv) { navigateToVideo(bv, pIndex); setTimeout(closePanel, 600); } } }); } document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && panelState.open) { closePanel(); } }); } // ========== 视频跳转(URL导航,支持分P) ========== function navigateToVideo(bv, pIndex) { if (!bv) return; let newPath = location.pathname.replace(/BV[0-9A-Za-z]+/, bv); let search = location.search; if (pIndex !== null && pIndex !== undefined) { const params = new URLSearchParams(search); params.set('p', pIndex + 1); search = '?' + params.toString(); } else { const params = new URLSearchParams(search); params.delete('p'); search = params.toString() ? '?' + params.toString() : ''; } const newUrl = newPath + search + (location.hash || ''); if (newUrl === location.href) return; window.history.pushState({}, '', newUrl); window.dispatchEvent(new PopStateEvent('popstate')); console.log(LOG_PREFIX, `URL导航到: ${bv}${pIndex !== null ? ' P' + (parseInt(pIndex) + 1) : ''}`); } // ========== 可见性劫持 ========== function hijackVisibility() { try { Object.defineProperty(document, 'hidden', { configurable: true, get: () => false, set: undefined }); } catch (e) {} try { Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => 'visible', set: undefined }); } catch (e) {} const blockEvent = (e) => e.stopImmediatePropagation(); document.addEventListener('visibilitychange', blockEvent, true); window.addEventListener('visibilitychange', blockEvent, true); } function checkAndHijack() { const bv = getCurrentBVID(); if (!bv) return; const allCheckedIds = []; // 从 MemoryList 检查当前视频是否在任意列表中 const memory = getMemoryList(); const videoData = panelState.videoData.length ? panelState.videoData : parsePageVideoList(); for (const key in memory) { const bitmap = memory[key][1] || ''; let idx = 0; for (const v of videoData) { if (v.isMulti && v.subVideos.length > 0) { for (const sv of v.subVideos) { if (sv.bv === bv && idx < bitmap.length && bitmap[idx] === '1') { allCheckedIds.push(sv.id); } idx++; } } else { if (v.bv === bv && idx < bitmap.length && bitmap[idx] === '1') { allCheckedIds.push(v.id); } idx++; } } } if (allCheckedIds.length > 0) { hijackVisibility(); console.log(LOG_PREFIX, `当前视频 ${bv} 在 MemoryList 中,已开启强制画面加载`); } } // ========== 视频结束监听(全局捕获) ========== function setupVideoEndListener() { document.addEventListener('ended', onVideoEnded, true); console.log(LOG_PREFIX, '全局视频结束监听已启动'); } function onVideoEnded(e) { const video = e.target; if (!video.closest('#bilibili-player') && !video.closest('.bpx-player-video-wrap')) return; const currentBv = getCurrentBVID(); if (!currentBv) return; const list = getListForCurrentCollection(); if (list.length === 0) { console.log(LOG_PREFIX, '未找到当前合集的播放列表,跳过自动连播'); return; } const pureCurrentBv = currentBv; let currentIndex = -1; for (let i = 0; i < list.length; i++) { const listBv = list[i].includes('::') ? list[i].split('::')[0] : list[i]; if (listBv === pureCurrentBv) { currentIndex = i; break; } } if (currentIndex >= 0 && currentIndex < list.length - 1) { e.stopImmediatePropagation(); e.preventDefault(); const nextId = list[currentIndex + 1]; const nextBv = nextId.includes('::') ? nextId.split('::')[0] : nextId; const nextPIndex = nextId.includes('::') ? parseInt(nextId.split('::')[1]) : null; console.log(LOG_PREFIX, `视频结束,脚本列表跳转: ${currentBv} → ${nextId}`); setTimeout(() => { navigateToVideo(nextBv, nextPIndex); }, 300); } else if (currentIndex >= 0 && currentIndex >= list.length - 1) { console.log(LOG_PREFIX, '脚本列表已播放完毕'); showToast('📋 脚本列表已播放完毕'); } } // ========== 初始化 ========== function init() { injectStyles(); function onBodyReady() { createTriggerButton(); createOverlay(); createDrawer(); bindEvents(); checkAndHijack(); setupVideoEndListener(); console.log(LOG_PREFIX, '侧边抽屉面板已就绪 (MemoryList 模式)'); } function waitForBody() { if (document.body) { onBodyReady(); } else { setTimeout(waitForBody, 50); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', waitForBody); } else { waitForBody(); } let bodyWatchInitialized = false; function initBodyWatch() { if (bodyWatchInitialized || !document.body) return; bodyWatchInitialized = true; const bodyObserver = new MutationObserver(() => { const trigger = document.getElementById('bili-force-trigger'); const drawer = document.getElementById('bili-force-drawer'); const overlay = document.getElementById('bili-force-overlay'); if ((!trigger || !document.body.contains(trigger)) && document.body) { createTriggerButton(); bindEvents(); } if ((!drawer || !document.body.contains(drawer)) && document.body) { createDrawer(); bindEvents(); } if ((!overlay || !document.body.contains(overlay)) && document.body) { createOverlay(); } }); bodyObserver.observe(document.body, { childList: true }); } const waitForBodyObserver = setInterval(() => { if (document.body) { initBodyWatch(); clearInterval(waitForBodyObserver); } }, 100); let lastUrl = location.href; const urlObserver = new MutationObserver(() => { if (lastUrl !== location.href) { lastUrl = location.href; setTimeout(() => { checkAndHijack(); if (panelState.open) { panelState.videoData = parsePageVideoList(); panelState.expandedMultis.clear(); const listName = getListName(); const restored = restoreCheckedSetFromMemoryList(listName, panelState.videoData); panelState.checkedSet = restored || new Set(getListForCurrentCollection()); renderVideoList(); updateCheckedCount(); } }, 1200); } }); const waitForUrlObserver = setInterval(() => { if (document.body) { urlObserver.observe(document.body, { subtree: true, childList: true }); clearInterval(waitForUrlObserver); } }, 100); } // ========== 菜单命令 ========== GM_registerMenuCommand('📋 打开列表管理面板', () => { if (!panelState.open) { openPanel(); } showToast('面板已打开'); }); GM_registerMenuCommand('🗑 清除当前合集列表', () => { const listName = getListName(); const memory = getMemoryList(); let removed = false; for (const key in memory) { if (memory[key] && memory[key][0] === listName) { delete memory[key]; removed = true; break; } } if (removed) { saveMemoryList(memory); panelState.checkedSet.clear(); if (panelState.open) { renderVideoList(); updateCheckedCount(); updateSaveButtonState(); } GM_notification({ text: `已清除列表"${listName}"`, timeout: 2500, title: '列表管理' }); showToast('🗑 列表已清除'); } else { GM_notification({ text: '未找到当前合集列表', timeout: 2000, title: '列表管理' }); } }); GM_registerMenuCommand('📊 查看所有 MemoryList(控制台)', () => { const memory = getMemoryList(); const keys = Object.keys(memory); console.log(LOG_PREFIX, 'MemoryList 内容:'); if (keys.length === 0) { console.log(' 空'); } else { keys.forEach(key => { console.log(` ${key}: ${memory[key][0]} -> 位图长度 ${memory[key][1].length}`); }); } GM_notification({ text: `共 ${keys.length} 个列表,详情见控制台`, timeout: 3000, title: '列表管理' }); }); GM_registerMenuCommand('🗑 清除所有 MemoryList', () => { const memory = getMemoryList(); const count = Object.keys(memory).length; if (count === 0) { GM_notification({ text: '列表已为空', timeout: 2000, title: '列表管理' }); } else { saveMemoryList({}); panelState.checkedSet.clear(); if (panelState.open) { renderVideoList(); updateCheckedCount(); updateSaveButtonState(); } GM_notification({ text: `已清除 ${count} 个列表`, timeout: 3000, title: '列表管理' }); showToast('🗑 所有列表已清除'); } }); init(); })();