您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
开启你的B站弹幕职人之路!优化高级弹幕发送面板,增加多种高级弹幕样式
// ==UserScript== // @name Bilibili高级弹幕增强 // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description 开启你的B站弹幕职人之路!优化高级弹幕发送面板,增加多种高级弹幕样式 // @author 淡い光 // @license MIT // @match *://*.bilibili.com/* // @grant none // @run-at document-start // ==/UserScript== /** * 添加新字体选项到弹幕字体选择列表 */ function addNewFonts() { // 监听弹幕面板切换 const observer = new MutationObserver((mutations) => { mutations.forEach(mutation => { if (mutation.target.classList && mutation.target.classList.contains('bui-dropdown-name') && mutation.target.textContent === '高级弹幕') { console.log("切换到高级弹幕面板"); // 当切换到高级弹幕面板时 setTimeout(setupFontSelector, 1); // setTimeout(setupEnhancedSendButton, 1); } }); }); // 开始观察文档变化,包含文本内容的变化 observer.observe(document.body, { childList: true, subtree: true, characterData: true, characterDataOldValue: true }); } /** * 设置字体选择器的事件监听 */ function setupFontSelector() { const fontSelect = document.querySelector('.bpx-player-adv-danmaku-font-family-select'); if (fontSelect && !fontSelect.dataset.enhanced) { fontSelect.dataset.enhanced = 'true'; fontSelect.addEventListener('mouseenter', () => { insertFonts(); }, { once: true }); // 只在第一次悬停时执行 } } /** * 插入新字体到字体列表 */ function insertFonts() { console.log("插入字体"); const fontList = document.querySelector('.bpx-player-adv-danmaku-font-family ul.bui-select-list'); if (!fontList) return; // 避免重复添加字体 if (!document.querySelector('[data-value="KaiTi"]')) { const newFonts = [ { value: 'KaiTi', text: '楷体' }, { value: 'YouYuan', text: '幼圆' }, { value: 'STCaiyun', text: '华文彩云' }, ]; newFonts.forEach(font => { const li = document.createElement('li'); li.className = 'bui-select-item'; li.setAttribute('data-value', font.value); li.textContent = font.text; // 插入到列表的最后 fontList.appendChild(li); }); // 添加点击事件处理 fontList.addEventListener('click', (e) => { const item = e.target.closest('.bui-select-item'); if (item) { // 更新选中状态 fontList.querySelectorAll('.bui-select-item').forEach(el => { el.classList.remove('bui-select-item-active'); }); item.classList.add('bui-select-item-active'); // 更新显示的文本和字体选择器的状态 const fontSelect = document.querySelector('.bpx-player-adv-danmaku-font-family-select'); if (fontSelect) { // 更新显示文本 const resultText = fontSelect.querySelector('.bui-select-result'); if (resultText) { resultText.textContent = item.textContent; } // 触发字体选择事件 const event = new CustomEvent('fontChange', { detail: { value: item.getAttribute('data-value'), text: item.textContent } }); fontSelect.dispatchEvent(event); } } }); } } /** * 设置颜色选择器 */ function setupColorPicker() { // 获取原面板的颜色选择器区域 const colorPickerResult = document.querySelector('.bui-color-picker-result'); if (!colorPickerResult || colorPickerResult.dataset.enhanced) return; colorPickerResult.dataset.enhanced = 'true'; // 创建新的颜色选择器 const colorInput = document.createElement('input'); colorInput.type = 'color'; colorInput.className = 'enhanced-color-picker'; // 设置样式 - 完全隐藏但保持可用 colorInput.style.cssText = ` width: 0; height: 0; padding: 0; border: none; position: absolute; visibility: hidden; `; // 创建容器并添加颜色选择器 const colorPickerContainer = document.createElement('div'); colorPickerContainer.style.cssText = ` position: absolute; left: 0; top: 105px; height: 1px; overflow: hidden; opacity: 0; `; colorPickerContainer.appendChild(colorInput); colorPickerResult.appendChild(colorPickerContainer); // 获取原有的颜色显示区域和输入框 const originalDisplay = colorPickerResult.querySelector('.bui-color-picker-display'); const colorTextInput = colorPickerResult.querySelector('.bui-color-picker-input input'); if (originalDisplay) { // 确保鼠标样式显示为可点击 originalDisplay.style.cursor = 'pointer'; // 点击原有显示区域时触发颜色选择器 originalDisplay.addEventListener('click', () => { // 在打开颜色选择器前,先同步当前颜色 const currentColor = colorTextInput.value.toUpperCase(); colorInput.value = currentColor; colorInput.click(); }); } // 监听颜色变化 colorInput.addEventListener('input', (e) => { const hexColor = e.target.value.toUpperCase(); if (colorTextInput) { colorTextInput.value = hexColor; // 触发原面板的颜色更新事件 colorTextInput.dispatchEvent(new Event('input', { bubbles: true })); } // 更新原有显示区域的背景色 if (originalDisplay) { originalDisplay.style.background = hexColor; } }); } /** * 设置发送样式弹幕按钮和样式弹幕区域 */ function setupEnhancedSendButton() { console.log("设置发送样式弹幕按钮和样式弹幕区域"); // 检查是否已存在增强功能 if (document.querySelector('.enhanced-danmaku-container')) return; // 添加隐藏的localDmFile元素 const localDmFile = document.createElement('div'); localDmFile.id = 'localDmFile'; localDmFile.style.display = 'none'; document.body.appendChild(localDmFile); // 添加数字输入框的上下箭头事件处理 function setupNumberStepper(container) { // 获取原面板的按百分比复选框 const getPercentCheckbox = () => document.querySelector('.bpx-player-adv-danmaku-pos-percent input'); // 获取是否按百分比 const getIsPercent = () => { const checkbox = getPercentCheckbox(); return checkbox && checkbox.checked; }; // 更新输入框的默认值 function updateDefaultValues(isPercent) { const inputs = { 'shadow-offset-x': { percent: '0.003', normal: '3' }, 'shadow-offset-y': { percent: '0.003', normal: '3' }, 'stroke-spacing': { percent: '0.002', normal: '2' } }; Object.entries(inputs).forEach(([className, values]) => { const input = container.querySelector(`.${className}`); if (input) { const currentValue = parseFloat(input.value); // 只有当值等于另一个模式的默认值时才更新 if (currentValue === parseFloat(isPercent ? values.normal : values.percent)) { input.value = isPercent ? values.percent : values.normal; } } }); } container.querySelectorAll('.bpx-player-adv-danmaku-spinner.bui-input').forEach(spinner => { const input = spinner.querySelector('input'); const upArrow = spinner.querySelector('.bui-input-stepper-up'); const downArrow = spinner.querySelector('.bui-input-stepper-down'); // 获取步进值和范围限制 const defaultStep = parseInt(spinner.dataset.step) || 1; const min = parseInt(spinner.dataset.min); const max = parseInt(spinner.dataset.max); // 点击上箭头 upArrow.addEventListener('click', () => { const step = getIsPercent() ? 0.001 : defaultStep; let value = parseFloat(input.value) || 0; value = parseFloat((value + step).toFixed(3)); if (!isNaN(max) && value > max) value = max; input.value = value; input.dispatchEvent(new Event('input', { bubbles: true })); }); // 点击下箭头 downArrow.addEventListener('click', () => { const step = getIsPercent() ? 0.001 : defaultStep; let value = parseFloat(input.value) || 0; value = parseFloat((value - step).toFixed(3)); if (!isNaN(min) && value < min) value = min; input.value = value; input.dispatchEvent(new Event('input', { bubbles: true })); }); }); // 监听原面板按百分比复选框的变化 const percentCheckbox = getPercentCheckbox(); if (percentCheckbox) { // 监听复选框的change事件 percentCheckbox.addEventListener('change', () => { const isPercent = getIsPercent(); // 更新默认值 updateDefaultValues(isPercent); // 更新所有输入框的值格式 container.querySelectorAll('.bpx-player-adv-danmaku-spinner.bui-input input').forEach(input => { const value = parseFloat(input.value) || 0; input.value = isPercent ? value.toFixed(3) : Math.round(value); }); }); // 初始化时检查一次 updateDefaultValues(getIsPercent()); } } // 创建样式弹幕和参数区域的容器 const enhancedContainer = document.createElement('div'); enhancedContainer.className = 'bpx-player-adv-danmaku-group enhanced-danmaku-container'; // 修改阴影参数区域的HTML,添加默认值 enhancedContainer.innerHTML = ` <div class="bpx-player-adv-danmaku-group-row"> <span class="bpx-player-adv-danmaku-title">样式弹幕</span> <div class="enhanced-style-buttons"> <span class="bpx-player-adv-danmaku-btn bui bui-button active" data-style="shadow"> <div class="bui-area bui-button-small">立体阴影</div> </span> <span class="bpx-player-adv-danmaku-btn bui bui-button" data-style="stroke"> <div class="bui-area bui-button-small">颜色描边</div> </span> <span class="bpx-player-adv-danmaku-btn bui bui-button" data-style="background"> <div class="bui-area bui-button-small">文字背景</div> </span> <span class="bpx-player-adv-danmaku-btn bui bui-button" data-style="normal"> <div class="bui-area bui-button-small">无样式</div> </span> <span class="bpx-player-adv-danmaku-btn bui bui-button" data-style="ascii"style=" padding-top: 0px; margin-top: 8px;"> <div class="bui-area bui-button-small">字符画</div> </span> </div> </div> <div class="enhanced-params-area"> <div class="shadow-params" style="display: block;"> <div class="bpx-player-adv-danmaku-group-row" style="padding-top: 0px;"> <div class="bpx-player-adv-danmaku-group-item shadow-item"> <span class="bpx-player-adv-danmaku-title">阴影颜色</span> <div class="bpx-player-adv-danmaku-color-picker bui bui-color-picker"> <div class="bui-area"> <div class="bui-color-picker-wrap"> <div class="bui-color-picker-result" style="!important; margin-bottom: 0px;"> <span class="bui-color-picker-input bui bui-input"> <div class="bui-area"> <div class="bui-input-wrap"> <input class="shadow-color bui-input-input" type="text" value="#222222"> </div> </div> </span> <span class="bui-color-picker-display" style="background: #222222"></span> </div> </div> </div> </div> </div> <div class="bpx-player-adv-danmaku-group-item shadow-item"> <span class="bpx-player-adv-danmaku-title">偏移X</span> <span class="bpx-player-adv-danmaku-spinner bui bui-input" data-value="3" data-min="0" data-max="9999" data-step="1"> <div class="bui-area"> <div class="bui-input-wrap"> <input class="shadow-offset-x bui-input-input" type="number" value="3"> <div class="bui-input-stepper"> <div class="bui-input-stepper-half bui-input-stepper-up"> <span class="bui-input-arrow bui-input-arrow-up"></span> </div> <div class="bui-input-stepper-half bui-input-stepper-down"> <span class="bui-input-arrow bui-input-arrow-down"></span> </div> </div> </div> </div> </span> </div> <div class="bpx-player-adv-danmaku-group-item shadow-item"> <span class="bpx-player-adv-danmaku-title">偏移Y</span> <span class="bpx-player-adv-danmaku-spinner bui bui-input" data-value="3" data-min="0" data-max="9999" data-step="1"> <div class="bui-area"> <div class="bui-input-wrap"> <input class="shadow-offset-y bui-input-input" type="number" value="3"> <div class="bui-input-stepper"> <div class="bui-input-stepper-half bui-input-stepper-up"> <span class="bui-input-arrow bui-input-arrow-up"></span> </div> <div class="bui-input-stepper-half bui-input-stepper-down"> <span class="bui-input-arrow bui-input-arrow-down"></span> </div> </div> </div> </div> </span> </div> </div> </div> <div class="stroke-params" style="display: none;"> <div class="bpx-player-adv-danmaku-group-row" style="padding-top: 0px;"> <div class="bpx-player-adv-danmaku-group-item stroke-item"> <span class="bpx-player-adv-danmaku-title">描边颜色</span> <div class="bpx-player-adv-danmaku-color-picker bui bui-color-picker"> <div class="bui-area"> <div class="bui-color-picker-wrap"> <div class="bui-color-picker-result" style="!important; margin-bottom: 0px;"> <span class="bui-color-picker-input bui bui-input"> <div class="bui-area"> <div class="bui-input-wrap"> <input class="stroke-color bui-input-input" type="text" value="#00AEEC"> </div> </div> </span> <span class="bui-color-picker-display" style="background: #00AEEC"></span> </div> </div> </div> </div> </div> <div class="bpx-player-adv-danmaku-group-item stroke-item"> <span class="bpx-player-adv-danmaku-title">间距</span> <span class="bpx-player-adv-danmaku-spinner bui bui-input" data-value="2" data-min="0" data-max="9999" data-step="1"> <div class="bui-area"> <div class="bui-input-wrap"> <input class="stroke-spacing bui-input-input" type="number" value="2"> <div class="bui-input-stepper"> <div class="bui-input-stepper-half bui-input-stepper-up"> <span class="bui-input-arrow bui-input-arrow-up"></span> </div> <div class="bui-input-stepper-half bui-input-stepper-down"> <span class="bui-input-arrow bui-input-arrow-down"></span> </div> </div> </div> </div> </span> </div> </div> </div> <div class="background-params" style="display: none;"> <div class="bpx-player-adv-danmaku-group-row" style="padding-top: 0px;"> <div class="bpx-player-adv-danmaku-group-item background-item"> <span class="bpx-player-adv-danmaku-title">背景颜色</span> <div class="bpx-player-adv-danmaku-color-picker bui bui-color-picker"> <div class="bui-area"> <div class="bui-color-picker-wrap"> <div class="bui-color-picker-result" style="!important; margin-bottom: 0px;"> <span class="bui-color-picker-input bui bui-input"> <div class="bui-area"> <div class="bui-input-wrap"> <input class="background-color bui-input-input" type="text" value="#222222"> </div> </div> </span> <span class="bui-color-picker-display" style="background: #222222"></span> </div> </div> </div> </div> </div> <div class="bpx-player-adv-danmaku-group-item background-item"> <span class="bpx-player-adv-danmaku-title">背景字符</span> <span class="bpx-player-adv-danmaku-spinner" style="width: 50px !important;"> <input type="text" class="background-char bui-input-input" style="width: 50px !important;" value="█" maxlength="1"> </span> </div> <div class="bpx-player-adv-danmaku-group-item background-item"> <span class="bpx-player-adv-danmaku-checkbox bui bui-checkbox"> <div class="bui-area" style="padding-top: 26px;"> <input class="bui-checkbox-input vertical-text" type="checkbox" aria-label="转竖列"> <label class="bui-checkbox-label"> <span class="bui-checkbox-icon bui-checkbox-icon-default"> <svg xmlns="http://www.w3.org/2000/svg" data-pointer="none" viewBox="0 0 32 32"> <path d="M8 6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2H8zm0-2h16c2.21 0 4 1.79 4 4v16c0 2.21-1.79 4-4 4H8c-2.21 0-4-1.79-4-4V8c0-2.21 1.79-4 4-4z"></path> </svg> </span> <span class="bui-checkbox-icon bui-checkbox-icon-selected"> <svg xmlns="http://www.w3.org/2000/svg" data-pointer="none" viewBox="0 0 32 32"> <path d="m13 18.25-1.8-1.8c-.6-.6-1.65-.6-2.25 0s-.6 1.5 0 2.25l2.85 2.85c.318.318.762.468 1.2.448.438.02.882-.13 1.2-.448l8.85-8.85c.6-.6.6-1.65 0-2.25s-1.65-.6-2.25 0l-7.8 7.8zM8 4h16c2.21 0 4 1.79 4 4v16c0 2.21-1.79 4-4 4H8c-2.21 0-4-1.79-4-4V8c0-2.21 1.79-4 4-4z"></path> </svg> </span> <span class="bui-checkbox-name">转竖列</span> </label> </div> </span> </div> </div> </div> <div class="normal-params" style="display: none;"> </div> <div class="ascii-params" style="display: none;"> <div class="bpx-player-adv-danmaku-group-row" style="padding-top: 0px;"> <div class="bpx-player-adv-danmaku-group-item" style="width: 100%; margin-bottom: 10px;"> <div class="bpx-player-adv-danmaku-text-input bui bui-input" style="width: calc(100%);"> <div class="bui-area"> <div class="bui-input-wrap"> <textarea class="ascii-content bui-input-input" type="text" placeholder="请输入字符画内容" style="min-height: 80px; font-family: '黑体', sans-serif; white-space: pre;!important;"> ◥◣ ◢◤ \n ◥◣ ◢◤ \n ◢████████████◣\n◢██████████████◣\n██ ██\n██ ◢█ █◣ ██\n██ ◢█◤ ◥█◣ ██\n██ █◤ ◥█ ██\n██ ██\n██ ██\n██ ︶︶ ██\n██ ██\n◥██████████████◤\n ◥████████████◤ \n ◥◤ ◥◤ </textarea> <span class="ascii-char-count" style="position: absolute; right: 18px; bottom: -10px; color: #99a2aa; font-size: 12px;">字数:0</span> </div> </div> </div> </div> </div> <div class="bpx-player-adv-danmaku-group-row" style="padding-top: 0px;"> <div class="bpx-player-adv-danmaku-group-item" style="margin-right: 20px;"> <span class="bpx-player-adv-danmaku-title">发送方式</span> <span class="ascii-send-mode-select bui bui-select" data-enhanced="true"> <div class="bui-area"> <div class="bui-select-wrap"> <div class="bui-select-border"> <div class="bui-select-header"> <span class="bui-select-result">更换Y坐标每行发送</span> <span class="bui-select-arrow"> <span class="bui-select-arrow-down"></span> </span> </div> <div class="bui-select-list-wrap" style=""> <ul class="bui-select-list" style="height: 0px; border: none;"> <li class="bui-select-item bui-select-item-active" data-value="line">更换Y坐标每行发送</li> <li class="bui-select-item" data-value="coord">相同坐标换行发送</li> </ul> </div> </div> </div> </div> </span> </div> <div class="bpx-player-adv-danmaku-group-item ascii-line-spacing" style="margin-right: 0px;"> <span class="bpx-player-adv-danmaku-title">每行间距</span> <span class="bpx-player-adv-danmaku-spinner bui bui-input" data-value="30" data-min="0" data-max="999" data-step="1" style="width: 58px;"> <div class="bui-area"> <div class="bui-input-wrap"> <input class="line-spacing bui-input-input" type="number" value="36"> <div class="bui-input-stepper"> <div class="bui-input-stepper-half bui-input-stepper-up"> <span class="bui-input-arrow bui-input-arrow-up"></span> </div> <div class="bui-input-stepper-half bui-input-stepper-down"> <span class="bui-input-arrow bui-input-arrow-down"></span> </div> </div> </div> </div> </span> </div> <div class="bpx-player-adv-danmaku-group-item ascii-sync-font-size" style="margin-top: 24px;"> <span class="bpx-player-adv-danmaku-checkbox bui bui-checkbox sync-font-size"style="margin-left: 8px;> <div class="bui-area"> <input class="bui-checkbox-input" type="checkbox" aria-label="同步字号" checked> <label class="bui-checkbox-label"> <span class="bui-checkbox-icon bui-checkbox-icon-default"> <svg xmlns="http://www.w3.org/2000/svg" data-pointer="none" viewBox="0 0 32 32"> <path d="M8 6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2H8zm0-2h16c2.21 0 4 1.79 4 4v16c0 2.21-1.79 4-4 4H8c-2.21 0-4-1.79-4-4V8c0-2.21 1.79-4 4-4z"></path> </svg> </span> <span class="bui-checkbox-icon bui-checkbox-icon-selected"> <svg xmlns="http://www.w3.org/2000/svg" data-pointer="none" viewBox="0 0 32 32"> <path d="m13 18.25-1.8-1.8c-.6-.6-1.65-.6-2.25 0s-.6 1.5 0 2.25l2.85 2.85c.318.318.762.468 1.2.448.438.02.882-.13 1.2-.448l8.85-8.85c.6-.6.6-1.65 0-2.25s-1.65-.6-2.25 0l-7.8 7.8zM8 4h16c2.21 0 4 1.79 4 4v16c0 2.21-1.79 4-4 4H8c-2.21 0-4-1.79-4-4V8c0-2.21 1.79-4 4-4z"></path> </svg> </span> <span class="bui-checkbox-name">同步字号</span> </label> </div> </span> </div> </div> </div> </div> <div class="bpx-player-adv-danmaku-group-row bpx-player-adv-danmaku-send"> <span class="bpx-player-adv-danmaku-btn bpx-player-adv-danmaku-send-test bui bui-button enhanced-send-btn"> <div class="bui-area bui-button-large">发送样式弹幕</div> </span> </div> <div class="bpx-player-adv-danmaku-group-row enhanced-send-status-row"> <span class="enhanced-send-status"></span> </div> `; // 添加样式 const style = document.createElement('style'); style.textContent = ` .enhanced-danmaku-container { margin-top: 0; padding: 20px 0; padding-top: 0px; } .enhanced-style-buttons { display: inline-block; } .enhanced-style-buttons .bpx-player-adv-danmaku-btn { margin-right: 8px; } .enhanced-style-buttons .active { background-color: #00a1d6; color: #fff; } .enhanced-params-area { margin: 8px 0; } .enhanced-params-area .bpx-player-adv-danmaku-group-item { margin-right: 16px; } .enhanced-params-area .bpx-player-adv-danmaku-spinner { display: inline-block; vertical-align: middle; } .enhanced-params-area input { width: 80px; height: 24px; padding: 0 8px; border: 1px solid #e3e5e7; border-radius: 2px; font-size: 12px; } .enhanced-send-btn.disabled { opacity: 0.5 !important; pointer-events: none !important; background-color: #e3e5e7 !important; } .enhanced-send-status { display: none; vertical-align: middle; margin-left: 10px; color: #666; font-size: 12px; line-height: 32px; } .enhanced-params-area .shadow-item, .enhanced-params-area .stroke-item { margin-right: 24px !important; } .enhanced-params-area .shadow-item:last-child, .enhanced-params-area .stroke-item:last-child { margin-right: 0 !important; } .enhanced-style-buttons .bpx-player-adv-danmaku-btn.active { background-color: #00a1d6 !important; color: #fff !important; } .preview-danmaku-btn { margin-top: 10px; text-align: center; } .preview-danmaku-btn .bui-area { background-color: #00a1d6; color: #fff; border-radius: 4px; cursor: pointer; } .preview-danmaku-btn .bui-area:hover { background-color: #00b5e5; } `; document.head.appendChild(style); // 组装并插入元素 enhancedContainer.appendChild(style); // 监听高级弹幕面板的加载 const observer = new MutationObserver((mutations, obs) => { const groupWrap = document.querySelector('.bpx-player-adv-danmaku-group-wrap'); if (groupWrap) { obs.disconnect(); // 找到最后一个group const lastGroup = groupWrap.querySelector('.bpx-player-adv-danmaku-group:last-child'); if (lastGroup) { // 插入到最后一个group后面 lastGroup.parentNode.insertBefore(enhancedContainer, lastGroup.nextSibling); // 样式弹幕添加事件监听 setupStyleEventListeners(enhancedContainer); // 设置数字输入框的上下箭头事件 setupNumberStepper(enhancedContainer); // 设置颜色选择器 setupColorPicker(); // 设置样式弹幕的颜色选择器 setupStyleColorPickers(enhancedContainer); // 设置字符画按钮点击事件 setupAsciiButton(enhancedContainer); // 设置同步字号 setupSyncFontSize(document,enhancedContainer); } } }); observer.observe(document.body, { childList: true, subtree: true }); // 创建预览弹幕区域 const previewArea = document.createElement('div'); previewArea.className = 'bpx-player-adv-danmaku-group-row bpx-player-adv-danmaku-send'; previewArea.innerHTML = ` <span class="bpx-player-adv-danmaku-btn bpx-player-adv-danmaku-send-test bui bui-button upload-danmaku-btn"> <div class="bui-area bui-button-large">上传弹幕文件</div> <input type="file" accept=".json,.xml" style="display: none;"> </span> <span class="bpx-player-adv-danmaku-btn bpx-player-adv-danmaku-send-test bui bui-button preview-danmaku-btn disabled"> <div class="bui-area bui-button-large">预览本地弹幕</div> </span> <span class="preview-danmaku-filename"></span> `; // enhancedContainer.appendChild(previewArea); // 添加预览区域样式 if (!document.querySelector('#enhanced-danmaku-preview-style')) { const previewStyle = document.createElement('style'); previewStyle.id = 'enhanced-danmaku-preview-style'; previewStyle.textContent = ` .preview-danmaku-btn, .upload-danmaku-btn { margin-right: 10px; } .preview-danmaku-btn .bui-area, .upload-danmaku-btn .bui-area { background-color: #00a1d6; color: #fff; border-radius: 4px; cursor: pointer; } .preview-danmaku-btn.disabled .bui-area { background-color: #b8b8b8; cursor: not-allowed; } .preview-danmaku-btn .bui-area:hover, .upload-danmaku-btn .bui-area:hover { background-color: #00b5e5; } .preview-danmaku-btn.disabled .bui-area:hover { background-color: #b8b8b8; } .preview-danmaku-filename { color: #666; font-size: 12px; line-height: 32px; margin-left: 10px; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } `; document.head.appendChild(previewStyle); } // 获取元素 const fileInput = previewArea.querySelector('input[type="file"]'); const previewBtn = previewArea.querySelector('.preview-danmaku-btn'); const uploadBtn = previewArea.querySelector('.upload-danmaku-btn'); const filenameSpan = previewArea.querySelector('.preview-danmaku-filename'); let currentDanmakuList = null; // 监听文件选择 fileInput.addEventListener('change', async (e) => { const file = e.target.files[0]; if (!file) return; try { const content = await file.text(); let danmakuList = []; if (file.name.endsWith('.xml')) { danmakuList = parseXMLDanmaku(content); } else if (file.name.endsWith('.json')) { danmakuList = parseJSONDanmaku(content); } if (danmakuList.length > 0) { currentDanmakuList = danmakuList; filenameSpan.textContent = `${file.name} (${danmakuList.length}条弹幕)`; previewBtn.classList.remove('disabled'); } } catch (error) { console.error('加载弹幕文件失败:', error); alert('加载弹幕文件失败'); } }); // 上传按钮点击事件 uploadBtn.addEventListener('click', () => { fileInput.click(); }); // 预览按钮点击事件 previewBtn.addEventListener('click', () => { if (previewBtn.classList.contains('disabled')) return; // 预览弹幕 previewDanmaku(currentDanmakuList); }); // 创建测试样式按钮 const testStyleBtn = document.createElement('span'); testStyleBtn.className = 'bpx-player-adv-danmaku-btn bpx-player-adv-danmaku-send-test bui bui-button'; testStyleBtn.innerHTML = '<div class="bui-area bui-button-large">测试样式效果</div>'; // 将测试按钮插入到发送样式弹幕按钮前面 const enhancedButton = enhancedContainer.querySelector('.enhanced-send-btn'); enhancedButton.parentNode.insertBefore(testStyleBtn, enhancedButton); // 测试按钮点击事件 testStyleBtn.addEventListener('click', async () => { // 获取当前选择的弹幕样式参数 const baseParams = await getBaseDanmakuParams(); const params = getAdvancedDanmakuParams(); // 获取当前选中的样式按钮 const buttonContainer = document.querySelector('.enhanced-style-buttons'); const activeButton = buttonContainer.querySelector('.active'); let currentStyle = activeButton.getAttribute('data-style'); // 获取字符画发送方式 const sendMode = document.querySelector('.ascii-send-mode-select .bui-select-item-active').getAttribute('data-value'); if (currentStyle === 'shadow' || currentStyle === 'stroke' || currentStyle === 'background' || currentStyle === 'normal') { if (!params.text) { alert('请输入弹幕内容'); return; } } if (currentStyle === 'ascii') { if (sendMode === 'line') { currentStyle = 'ascii-line'; } else if (sendMode === 'coord') { currentStyle = 'ascii-coord'; } } let istest = true; testStyle(baseParams, params, currentStyle, istest, sendMode) }); // 在Y轴翻转后面添加Z轴跟随移动角度复选框 const zRotateFollowHtml = ` <span class="bpx-player-adv-danmaku-checkbox bui bui-checkbox z-rotate-follow" style="margin-top: 25px;margin-left: 13px;> <div class="bui-area"> <input class="bui-checkbox-input" type="checkbox" aria-label="Z轴跟随移动角度"> <label class="bui-checkbox-label"> <span class="bui-checkbox-icon bui-checkbox-icon-default"> <svg xmlns="http://www.w3.org/2000/svg" data-pointer="none" viewBox="0 0 32 32"> <path d="M8 6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2H8zm0-2h16c2.21 0 4 1.79 4 4v16c0 2.21-1.79 4-4 4H8c-2.21 0-4-1.79-4-4V8c0-2.21 1.79-4 4-4z"></path> </svg> </span> <span class="bui-checkbox-icon bui-checkbox-icon-selected"> <svg xmlns="http://www.w3.org/2000/svg" data-pointer="none" viewBox="0 0 32 32"> <path d="m13 18.25-1.8-1.8c-.6-.6-1.65-.6-2.25 0s-.6 1.5 0 2.25l2.85 2.85c.318.318.762.468 1.2.448.438.02.882-.13 1.2-.448l8.85-8.85c.6-.6.6-1.65 0-2.25s-1.65-.6-2.25 0l-7.8 7.8zM8 4h16c2.21 0 4 1.79 4 4v16c0 2.21-1.79 4-4 4H8c-2.21 0-4-1.79-4-4V8c0-2.21 1.79-4 4-4z"></path> </svg> </span> <span class="bui-checkbox-name">Z轴跟随移动角度</span> </label> </div> </span> `; // 在setupEnhancedSendButton函数中添加以下代码 setTimeout(() => { const rotateYContainer = document.querySelector('.bpx-player-adv-danmaku-rotateY'); if (rotateYContainer) { rotateYContainer.insertAdjacentHTML('afterend', zRotateFollowHtml); // 添加坐标变化监听 const zRotateFollow = document.querySelector('.z-rotate-follow input'); const zRotateInput = document.querySelector('.bpx-player-adv-danmaku-rotateZ input'); function updateZRotate() { if (!zRotateFollow.checked) return; const startX = parseFloat(document.querySelector('.bpx-player-adv-danmaku-spinner[data-key="startX"] input').value) || 0; const startY = parseFloat(document.querySelector('.bpx-player-adv-danmaku-spinner[data-key="startY"] input').value) || 0; const endX = parseFloat(document.querySelector('.bpx-player-adv-danmaku-spinner[data-key="endX"] input').value) || 0; const endY = parseFloat(document.querySelector('.bpx-player-adv-danmaku-spinner[data-key="endY"] input').value) || 0; // 计算角度 let angle; if (startX >= 0 && startX <= 1 && startY >= 0 && startY <= 1 && endX >= 0 && endX <= 1 && endY >= 0 && endY <= 1) { // 考虑屏幕比16:9,计算实际坐标 const screenWidth = 16; const screenHeight = 9; const actualStartX = startX * screenWidth; const actualStartY = startY * screenHeight; const actualEndX = endX * screenWidth; const actualEndY = endY * screenHeight; const dx = actualEndX - actualStartX; const dy = actualEndY - actualStartY; angle = Math.atan2(dy, dx) * (180 / Math.PI); } else { const dx = endX - startX; const dy = endY - startY; angle = Math.atan2(dy, dx) * (180 / Math.PI); } // 将角度转换为0-360范围 angle = (angle + 360) % 360; // if (angle > 0) { // angle = 360 - angle; // } // 更新Z轴翻转输入框并触发原本绑定的事件 zRotateInput.value = Math.round(angle); // 创建并触发input事件以更新UI const event = new Event('input', { bubbles: true }); zRotateInput.dispatchEvent(event); // 触发change事件以确保所有绑定的事件都被调用 zRotateInput.dispatchEvent(new Event('change', { bubbles: true })); } // 监听坐标输入变化 const coordInputs = [ 'startX', 'startY', 'endX', 'endY' ].forEach(key => { const input = document.querySelector(`.bpx-player-adv-danmaku-spinner[data-key="${key}"] input`); input.addEventListener('input', updateZRotate); }); // 监听复选框状态变化 zRotateFollow.addEventListener('change', updateZRotate); } }, 200); } /** * 更新字符数 * @param {Element} enhancedContainer */ function updateCharCount(enhancedContainer) { const contentTextarea = enhancedContainer.querySelector('.ascii-content'); const charCountSpan = enhancedContainer.querySelector('.ascii-char-count'); const content = contentTextarea.value; const charCount = [...content].length; // 使用扩展运算符正确计算Unicode字符 charCountSpan.innerHTML = `字数:${charCount}`; // 更新title属性以显示完整内容 contentTextarea.title = content; } /** * 设置同步字号 * @param {Document} document * @param {Element} enhancedContainer */ function setupSyncFontSize(document,enhancedContainer) { // 处理发送方式选择 const sendModeSelect = enhancedContainer.querySelector('.ascii-send-mode-select'); const lineSpacingContainer = enhancedContainer.querySelector('.ascii-line-spacing'); const syncFontSizeCheckbox = enhancedContainer.querySelector('.sync-font-size input'); const fontSizeInput = document.querySelector('.bpx-player-adv-danmaku-font-size input'); // 处理发送方式选择 sendModeSelect.addEventListener('click', (e) => { const item = e.target.closest('.bui-select-item'); if (item) { // 更新选中状态 sendModeSelect.querySelectorAll('.bui-select-item').forEach(el => { el.classList.remove('bui-select-item-active'); }); item.classList.add('bui-select-item-active'); // 更新显示的文本 const resultText = sendModeSelect.querySelector('.bui-select-result'); resultText.textContent = item.textContent; // 根据选择显示/隐藏行间距输入框 lineSpacingContainer.style.display = item.getAttribute('data-value') === 'line' ? '' : 'none'; } }); // 处理同步字号勾选框 syncFontSizeCheckbox.addEventListener('change', () => { if (syncFontSizeCheckbox.checked) { // 同步字号 const lineSpacingInput = document.querySelector('.line-spacing'); lineSpacingInput.value = fontSizeInput.value; } }); // 监听字体大小变化 fontSizeInput.addEventListener('input', () => { if (syncFontSizeCheckbox.checked) { const lineSpacingInput = document.querySelector('.line-spacing'); lineSpacingInput.value = fontSizeInput.value; } }); // 监听每行间距变化 const lineSpacingInput = document.querySelector('.line-spacing'); lineSpacingInput.addEventListener('input', () => { if (syncFontSizeCheckbox.checked) { fontSizeInput.value = lineSpacingInput.value; } }); } function setupAsciiButton(enhancedContainer) { // 处理发送方式选择 const sendModeSelect = enhancedContainer.querySelector('.ascii-send-mode-select'); const lineSpacingContainer = enhancedContainer.querySelector('.ascii-line-spacing'); const syncFontSizeContainer = enhancedContainer.querySelector('.ascii-sync-font-size'); // 同步字号勾选框 const syncFontSizeCheckbox = document.querySelector('.sync-font-size input'); // 处理下拉框的显示/隐藏 const selectHeader = sendModeSelect.querySelector('.bui-select-header'); const selectList = sendModeSelect.querySelector('.bui-select-list-wrap'); const selectListUl = sendModeSelect.querySelector('.bui-select-list'); // 处理字符画内容的字数统计和悬浮展示 const contentTextarea = enhancedContainer.querySelector('.ascii-content'); const charCountSpan = enhancedContainer.querySelector('.ascii-char-count'); // const defaultAscii = ``; // contentTextarea.value = defaultAscii; // 监听输入事件 contentTextarea.addEventListener('input', () => updateCharCount(enhancedContainer)); // 初始化字数统计 updateCharCount(enhancedContainer); selectHeader.addEventListener('click', () => { // 切换下拉列表的显示状态 const isVisible = selectList.style.display === 'block'; if (!isVisible) { // 显示下拉列表时设置正确的高度 selectList.style.display = 'block'; // 每个选项24px高,2个选项就是48px selectListUl.style.height = '48px'; selectListUl.style.border = '1px solid #e3e5e7'; } else { // 隐藏时重置样式 selectList.style.display = 'none'; selectListUl.style.height = '0px'; selectListUl.style.border = 'none'; } // 添加/移除active类 sendModeSelect.classList.toggle('active'); }); // 处理选项点击 selectList.addEventListener('click', (e) => { const item = e.target.closest('.bui-select-item'); if (item) { // 更新选中状态 sendModeSelect.querySelectorAll('.bui-select-item').forEach(el => { el.classList.remove('bui-select-item-active'); }); item.classList.add('bui-select-item-active'); // 更新显示的文本 const resultText = sendModeSelect.querySelector('.bui-select-result'); resultText.textContent = item.textContent; // 隐藏下拉列表 selectList.style.display = 'none'; selectListUl.style.height = '0px'; selectListUl.style.border = 'none'; sendModeSelect.classList.remove('active'); // 根据选择显示/隐藏行间距输入框 lineSpacingContainer.style.display = item.getAttribute('data-value') === 'line' ? '' : 'none'; syncFontSizeContainer.style.display = item.getAttribute('data-value') === 'line' ? '' : 'none'; } }); // 点击外部关闭下拉列表 document.addEventListener('click', (e) => { if (!sendModeSelect.contains(e.target)) { selectList.style.display = 'none'; selectListUl.style.height = '0px'; selectListUl.style.border = 'none'; sendModeSelect.classList.remove('active'); } }); // 处理字符画按钮点击时也要重置下拉列表状态 const asciiBtn = enhancedContainer.querySelector('[data-style="ascii"]'); const asciiParams = enhancedContainer.querySelector('.ascii-params'); asciiBtn.addEventListener('click', () => { // 隐藏其他参数区域 enhancedContainer.querySelectorAll('.enhanced-params-area > div').forEach(div => { if (div !== asciiParams) { div.style.display = 'none'; } }); // 显示字符画参数区域 asciiParams.style.display = 'block'; // 重置下拉列表状态 selectList.style.display = 'none'; selectListUl.style.height = '0px'; selectListUl.style.border = 'none'; sendModeSelect.classList.remove('active'); }); } /** * 测试弹幕样式 * @param {Object} baseParams 基础弹幕参数 * @param {Object} params 高级弹幕参数 * @param {string} currentStyle 当前选中的弹幕样式 * @param {boolean} istest 是否是测试弹幕 */ function testStyle(baseParams, params, currentStyle, istest) { const baseTime = istest ? Math.floor(window.player.getCurrentTime() * 1000) : parseFloat(baseParams.progress); // 转换为弹幕对象数组 let testDanmakus = []; // 基础弹幕对象 const baseDanmaku = { stime: baseTime, mode: 7, size: baseParams.fontSize, date: baseTime, pool: 0, uhash: '', }; // 根据特效类型生成不同的弹幕组合 switch (currentStyle) { case 'shadow': // 阴影效果:生成2条弹幕 const shadowColor = document.querySelector('.shadow-color').value; const offsetX = parseFloat(document.querySelector('.shadow-offset-x').value); const offsetY = parseFloat(document.querySelector('.shadow-offset-y').value); const shadowParams = { ...params }; shadowParams.startX = trimTrailingZeros((parseFloat(params.startX) + offsetX).toFixed(3)); shadowParams.startY = trimTrailingZeros((parseFloat(params.startY) + offsetY).toFixed(3)); shadowParams.endX = trimTrailingZeros((parseFloat(params.endX) + offsetX).toFixed(3)); shadowParams.endY = trimTrailingZeros((parseFloat(params.endY) + offsetY).toFixed(3)); shadowParams.color = shadowColor; testDanmakus = [ { ...baseDanmaku, dmid: `test_shadow_bg_${baseTime}`, color: convertColorToDecimal(shadowColor), text: buildAdvancedDanmakuText(shadowParams) }, { ...baseDanmaku, stime: baseDanmaku.stime + 0.001, dmid: `test_shadow_main_${baseTime}`, color: baseParams.color, text: buildAdvancedDanmakuText(params) } ]; break; case 'stroke': testDanmakus = []; const strokeColor = document.querySelector('.stroke-color').value; const spacing = document.querySelector('.stroke-spacing').value; if (!strokeColor || !spacing) { alert('请填写描边颜色和间距'); return; } // 发送9条弹幕 for (let i = 0; i < 9; i++) { const currentParams = { ...params }; currentParams.stroke = 0; if (i < 8) { // 前8条是描边,支持小数坐标 const positions = [ { x: -1, y: -1 }, // 左上 { x: 0, y: -1 }, // 中上 { x: 1, y: -1 }, // 右上 { x: -1, y: 0 }, // 左中 { x: 1, y: 0 }, // 右中 { x: -1, y: 1 }, // 左下 { x: 0, y: 1 }, // 中下 { x: 1, y: 1 } // 右下 ]; const offsetX = parseFloat(spacing) * positions[i].x; const offsetY = parseFloat(spacing) * positions[i].y; currentParams.startX = trimTrailingZeros((parseFloat(params.startX) + offsetX).toFixed(3)); currentParams.startY = trimTrailingZeros((parseFloat(params.startY) + offsetY).toFixed(3)); currentParams.endX = trimTrailingZeros((parseFloat(params.endX) + offsetX).toFixed(3)); currentParams.endY = trimTrailingZeros((parseFloat(params.endY) + offsetY).toFixed(3)); testDanmakus.push({ ...baseDanmaku, dmid: `test_stroke_${baseTime}_${i}`, color: convertColorToDecimal(strokeColor), text: buildAdvancedDanmakuText(currentParams) }); } else { // 最后一条是原始弹幕,时间延迟0.001秒 testDanmakus.push({ ...baseDanmaku, stime: baseDanmaku.stime + 0.001, dmid: `test_stroke_${baseTime}_${i}`, color: baseParams.color, text: buildAdvancedDanmakuText(currentParams) }); } } break; case 'background': const backgroundColor = document.querySelector('.background-color').value; const backgroundChar = document.querySelector('.background-char').value; const isVertical = document.querySelector('.vertical-text').checked; if (!backgroundColor || !backgroundChar) { alert('请填写背景颜色和背景字符'); return; } // 发送背景弹幕 const backgroundParams = { ...params }; let newText = params.text.replace(/\\n/g, ''); // 清除换行符获取文本长度 let bgText = backgroundChar.repeat(newText.length); let originalText = params.text; // 如果勾选了转竖列,转换背景字符和原文本 if (isVertical) { if (!bgText.includes('\\n')) { bgText = convertToVertical(bgText); } console.log("originalText:", originalText); debugger; if (!originalText.includes('\\n')) { originalText = convertToVertical(originalText); } params.text = originalText; // 使用可能转换后的文本 } backgroundParams.text = bgText; backgroundParams.fontFamily = 'SimHei'; backgroundParams.stroke = 0; // 背景效果:生成2条弹幕,一个文字一个背景 testDanmakus = [ { ...baseDanmaku, dmid: `test_bg_back_${baseTime}`, color: convertColorToDecimal(backgroundColor), // 黑色背景 text: buildAdvancedDanmakuText(backgroundParams) }, { ...baseDanmaku, stime: baseDanmaku.stime + 0.001, dmid: `test_bg_text_${baseTime}`, color: baseParams.color, text: buildAdvancedDanmakuText(params) } ]; break; case 'normal': testDanmakus = [ { ...baseDanmaku, dmid: `test_normal_${baseTime}`, color: baseParams.color, text: buildAdvancedDanmakuText(params) } ]; break; case 'ascii-line': testDanmakus = []; const asciiContent = document.querySelector('.ascii-content').value; // 分行处理 const lines = asciiContent.split('\n').filter(line => line); // 更换Y坐标每行发送模式 const lineSpacing = parseInt(document.querySelector('.line-spacing').value) || 36; let currentY = parseFloat(params.startY); let currentEndY = parseFloat(params.endY); // 逐行发送 for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (!line) continue; const asciiParams = { ...params, text: line, startY: currentY, endY: currentEndY }; // 更新Y坐标 currentY += lineSpacing; currentEndY += lineSpacing; testDanmakus.push({ ...baseDanmaku, dmid: `test_ascii_${baseTime}`, color: baseParams.color, text: buildAdvancedDanmakuText(asciiParams) }); } break; case 'ascii-coord': testDanmakus = []; const coordAsciiContent = document.querySelector('.ascii-content').value; // 分行处理 const coordLines = coordAsciiContent.split('\n'); // 逐行发送 for (let i = 0; i < coordLines.length; i++) { let line = coordLines[i]; if (!line) continue; line = '\n'.repeat(i) + line; // 将每行弹幕之间插入换行符 const asciiParams = { ...params, text: line }; testDanmakus.push({ ...baseDanmaku, dmid: `test_ascii_coord_${baseTime}`, color: baseParams.color, text: buildAdvancedDanmakuText(asciiParams) }); } break; } console.log(testDanmakus); // 预览这些测试弹幕 previewDanmaku(testDanmakus); // if (istest) { // console.log("测试弹幕持续时间:", params.duration); // setTimeout(() => { // console.log("删除测试弹幕"); // removePreviewDanmaku(testDanmakus); // }, params.duration * 1000); // } } /** * 加载日期选择器 */ function loadDatePicker() { // 用于存储查找元素的任务配置 let task = {}; /** * 查找指定选择器的元素并执行回调 * @param {Object} param0 配置对象 * @param {string} param0.selector CSS选择器 * @param {Element} param0.context 查找上下文,默认为document * @param {Function} param0.callback 找到元素后的回调函数 */ function getElement({ selector, context = document, callback }) { const elem = context.querySelector(selector); if (elem) { callback(elem); } } // 创建MutationObserver用于监听DOM变化 let ob = new MutationObserver(function (recode) { getElement(task); }); // Promise链式调用,按顺序查找并操作各个元素 return new Promise(resolve => { console.log("查找弹幕盒子"); // 1. 查找弹幕盒子 task.selector = "#danmukuBox"; task.callback = resolve; getElement(task); }).then(danmukuBox => new Promise(resolve => { // 2. 监听弹幕盒子的变化,查找折叠面板 ob.disconnect(); ob.observe(danmukuBox, { "childList": true, "subtree": true }); task.selector = "div.bui-collapse-wrap"; task.context = danmukuBox; task.callback = resolve; getElement(task); })).then(collapseWrap => new Promise(resolve => { // 3. 如果面板折叠则展开,查找历史按钮 if (collapseWrap.classList.contains("bui-collapse-wrap-folded")) { collapseWrap.querySelector("div.bui-collapse-header").click(); } task.selector = "div.bpx-player-dm-btn-history"; task.context = collapseWrap; task.callback = resolve; getElement(task); })).then(datePickerBtn => new Promise(resolve => { // 4. 监听历史按钮变化,点击显示日期选择器 ob.disconnect(); ob.observe(datePickerBtn, { "attributes": true, "childList": true, "subtree": true }); // 修改这里:使用Promise确保点击事件完成后再继续 return new Promise(clickResolve => { datePickerBtn.click(); // 给一点时间让日期选择器显示出来 setTimeout(() => { task.selector = "div.bpx-player-date-picker.bpx-player-show"; task.context = datePickerBtn; task.callback = (elem) => { clickResolve(elem); resolve(elem); }; getElement(task); }, 100); }); })).then(datePicker => { console.log("关闭日期选择器", datePicker); // 确保日期选择器存在并且是显示状态 if (datePicker && datePicker.classList.contains('bpx-player-show')) { const historyBtn = datePicker.closest("div.bpx-player-dm-btn-history"); if (historyBtn) { historyBtn.click(); console.log("日期选择器已关闭"); } else { console.error("未找到历史按钮"); } } else { console.error("日期选择器未处于显示状态"); } }).finally(() => { ob.disconnect(); }); } /** * 注入弹幕到播放器 */ function injectDanmaku(danmakuList) { // 将弹幕数据保存到隐藏元素中 const localDmFile = document.getElementById('localDmFile'); if (!localDmFile) { console.error('未找到本地弹幕容器'); return; } // 使用原生方法保存数据 localDmFile.setAttribute('data-decode-msg', JSON.stringify(danmakuList)); // 先加载日期选择器,然后触发点击事件 loadDatePicker().then(() => { // 模拟点击历史弹幕面板第一天的记录来触发加载 const datePickerSelector = "#danmukuBox div.bpx-player-dm-btn-history div.bpx-player-date-picker"; const datePicker = document.querySelector(datePickerSelector); if (!datePicker) return; const daySpan = datePicker.querySelector("div.bpx-player-date-picker-day-content span.bpx-player-date-picker-day"); if (!daySpan) return; const fakeElement = daySpan.cloneNode(true); fakeElement.setAttribute('data-timestamp', String(new Date().setHours(0, 0, 0, 0) / 1e3)); fakeElement.setAttribute('data-action', 'changeDay'); const fakeEvent = new MouseEvent("click"); Object.defineProperty(fakeEvent, "target", { value: fakeElement }); datePicker.dispatchEvent(fakeEvent); // 延迟一下再自动播放,确保弹幕已经注入 setTimeout(() => { autoPlayAfterPreview(); }, 100); }).catch(error => { console.error('加载日期选择器失败:', error); }); } /** * 劫持B站的弹幕历史记录加载功能 */ function hookLoadHistory() { // 使用Object.defineProperty劫持全局对象的allHistory属性 Object.defineProperty(Object.prototype, "allHistory", { set(v) { // 删除原有属性,防止递归调用 delete Object.prototype.allHistory; let that = this; this.allHistory = v; // 使用Proxy代理allHistory,拦截获取操作 this.allHistory = new Proxy(this.allHistory, { get(target, prop) { // 清空高级弹幕列表 that.dmListStore.basList = []; const basDanmaku = that.nodes.basDanmaku.querySelector('div.bas-danmaku'); if (basDanmaku) basDanmaku.innerHTML = ""; // 如果不是获取第一条历史记录,直接返回原值 if (prop !== "0") { return target[prop]; } else { // 获取本地弹幕数据 const localDmFile = document.getElementById('localDmFile'); const localRemoveDmFile = document.getElementById('localRemoveDmFile'); let decodeMsg = localDmFile ? localDmFile.getAttribute('data-decode-msg') : null; let decodeRemoveMsg = localRemoveDmFile ? localRemoveDmFile.getAttribute('data-decode-msg') : null; if (decodeMsg) { // 如果是字符串则解析为对象 if (typeof decodeMsg === 'string') { decodeMsg = JSON.parse(decodeMsg); } // 删除弹幕不为空则删除 // if (decodeRemoveMsg) { // debugger; // console.log("decodeRemoveMsg:", JSON.parse(decodeRemoveMsg)); // if (typeof decodeRemoveMsg === 'string') { // decodeRemoveMsg = JSON.parse(decodeRemoveMsg); // for (let i = 0; i < decodeMsg.length; i++) { // for (let j = 0; j < decodeRemoveMsg.length; j++) { // if (decodeRemoveMsg[j].dmid == decodeMsg[i].dmid) { // let text = decodeMsg[i].text; // console.log("decodeMsg[i].text:", text); // let newText = text.split(","); // // 将第五个参数值置为空字符串 // if (newText.length >= 5) { // newText[4] = "\"\""; // } // decodeMsg[i].text = newText.join(","); // 更新text字段 // console.log("更新后的text:", decodeMsg[i].text); // } // } // } // } // } // 合并本地弹幕和历史弹幕 return target[prop].then(originList => { // 如果原始列表存在,则合并 if (Array.isArray(originList)) { let newList = [...decodeMsg, ...originList].sort((a, b) => a.stime - b.stime); return newList; } // 否则只返回本地弹幕 return decodeMsg; }); } else { // 否则返回原始历史记录 return target[prop]; } } }, }); }, get() { return this._allHistory; }, configurable: true, }); } /** * 注入弹幕到播放器 */ function removePreviewDanmaku(danmakuList) { // 将弹幕数据保存到隐藏元素中 const localRemoveDmFile = document.getElementById('localRemoveDmFile'); if (!localRemoveDmFile) { console.error('未找到本地弹幕容器'); return; } // 使用原生方法保存数据 localRemoveDmFile.setAttribute('data-decode-msg', JSON.stringify(danmakuList)); // 先加载日期选择器,然后触发点击事件 loadDatePicker().then(() => { // 模拟点击历史弹幕面板第一天的记录来触发加载 const datePickerSelector = "#danmukuBox div.bpx-player-dm-btn-history div.bpx-player-date-picker"; const datePicker = document.querySelector(datePickerSelector); if (!datePicker) return; const daySpan = datePicker.querySelector("div.bpx-player-date-picker-day-content span.bpx-player-date-picker-day"); if (!daySpan) return; const fakeElement = daySpan.cloneNode(true); fakeElement.setAttribute('data-timestamp', String(new Date().setHours(0, 0, 0, 0) / 1e3)); fakeElement.setAttribute('data-action', 'changeDay'); const fakeEvent = new MouseEvent("click"); Object.defineProperty(fakeEvent, "target", { value: fakeElement }); datePicker.dispatchEvent(fakeEvent); // 延迟一下再自动播放,确保弹幕已经注入 setTimeout(() => { autoPlayAfterPreview(); }, 100); }).catch(error => { console.error('加载日期选择器失败:', error); }); // 刷新弹幕 const player = window.player; if (player && player.reloadDanmaku) { player.reloadDanmaku(); } } /** * 样式弹幕添加事件监听 */ function setupStyleEventListeners(container) { // 默认选中阴影效果 let currentStyle = 'shadow'; // 样式切换事件 const styleButtons = container.querySelector('.enhanced-style-buttons'); styleButtons.addEventListener('click', (e) => { const button = e.target.closest('.bpx-player-adv-danmaku-btn'); if (!button) return; // 更新按钮状态 styleButtons.querySelectorAll('.bpx-player-adv-danmaku-btn').forEach(btn => { btn.classList.remove('active'); }); button.classList.add('active'); currentStyle = button.dataset.style; // 显示对应的参数区域 container.querySelector('.stroke-params').style.display = currentStyle === 'stroke' ? 'block' : 'none'; container.querySelector('.shadow-params').style.display = currentStyle === 'shadow' ? 'block' : 'none'; container.querySelector('.background-params').style.display = currentStyle === 'background' ? 'block' : 'none'; container.querySelector('.normal-params').style.display = currentStyle === 'normal' ? 'block' : 'none'; container.querySelector('.ascii-params').style.display = currentStyle === 'ascii' ? 'block' : 'none'; }); // 发送按钮事件 const enhancedButton = container.querySelector('.enhanced-send-btn'); enhancedButton.addEventListener('click', async () => { if (!currentStyle) { alert('请选择一个样式'); return; } if (currentStyle === 'shadow') { await sendShadowDanmaku(currentStyle); } else if (currentStyle === 'stroke') { await sendStrokeDanmaku(currentStyle); } else if (currentStyle === 'background') { await sendBackgroundDanmaku(currentStyle); } else if (currentStyle === 'normal') { await sendNormalDanmaku(currentStyle); } else if (currentStyle === 'ascii') { await sendAsciiDanmaku(currentStyle); } }); } /** * 发送字符画弹幕 */ async function sendAsciiDanmaku(currentStyle) { // 获取字符画内容 const container = document.querySelector('.enhanced-danmaku-container'); const button = container.querySelector('.enhanced-send-btn'); const status = container.querySelector('.enhanced-send-status'); const asciiContent = document.querySelector('.ascii-content').value; if (!asciiContent.trim()) { alert('请输入字符画内容'); return; } // 获取发送方式 const sendMode = document.querySelector('.ascii-send-mode-select .bui-select-item-active').getAttribute('data-value'); // 获取基础参数 const baseParams = await getBaseDanmakuParams(); if (!baseParams) return; const params = getAdvancedDanmakuParams(); if (sendMode === 'line') { // 分行处理 const lines = asciiContent.split('\n'); // 更换Y坐标每行发送模式 const lineSpacing = parseInt(document.querySelector('.line-spacing').value) || 36; let currentY = parseFloat(params.startY); let currentEndY = parseFloat(params.endY); let baseTime = 5 * (lines.length - 1); // 基础时间:1个间隔,5秒 let estimatedTime = baseTime; await updateSendStatus(button, status, `共${lines.length}条弹幕,预计发送${estimatedTime}秒。正在发送中`, true); let hasFailure = false; let failureCount = 0; // 逐行发送 for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (!line) continue; const asciiParams = { ...params, text: line, startY: currentY, endY: currentEndY }; // 更新Y坐标 currentY += lineSpacing; currentEndY += lineSpacing; try { await sendDanmakuWithRetry(asciiParams, baseParams.color, baseParams.fontSize); } catch (error) { console.error('阴影弹幕发送失败:' + error.message); hasFailure = true; failureCount++; // 只在第一次失败时更新预计时间 if (failureCount === 1) { estimatedTime = baseTime + 21; // 增加一次重试的时间 await updateSendStatus(button, status, `共${lines.length}条弹幕,预计发送${estimatedTime}秒。正在发送中`, true); } } const waitTime = hasFailure ? 21000 : 5000; await new Promise(resolve => setTimeout(resolve, waitTime)); } // 发送完成 await updateSendStatus(button, status, `共${lines.length}条弹幕,已${hasFailure ? '部分' : '全部'}发送完成`); // 立即预览弹幕 let istest = false; testStyle(baseParams, asciiParams, 'ascii-line', istest) setTimeout(() => { updateSendStatus(button, status, ''); }, 3000); } else if (sendMode === 'coord') { debugger; // 分行处理 const coordLines = asciiContent.split('\n'); let baseTime = 5 * (coordLines.length - 1); // 基础时间:1个间隔,5秒 let estimatedTime = baseTime; await updateSendStatus(button, status, `共${coordLines.length}条弹幕,预计发送${estimatedTime}秒。正在发送中`, true); let hasFailure = false; let failureCount = 0; // 逐行发送 for (let i = 0; i < coordLines.length; i++) { let line = coordLines[i]; if (!line) continue; line = '\\n'.repeat(i) + line; // 将每行弹幕之间插入换行符 if (!line) continue; const asciiParams = { ...params, text: line }; try { await sendDanmakuWithRetry(asciiParams, baseParams.color, baseParams.fontSize); } catch (error) { console.error('阴影弹幕发送失败:' + error.message); hasFailure = true; failureCount++; // 只在第一次失败时更新预计时间 if (failureCount === 1) { estimatedTime = baseTime + 21; // 增加一次重试的时间 await updateSendStatus(button, status, `共${lines.length}条弹幕,预计发送${estimatedTime}秒。正在发送中`, true); } } const waitTime = hasFailure ? 21000 : 5000; await new Promise(resolve => setTimeout(resolve, waitTime)); } // 发送完成 await updateSendStatus(button, status, `共${coordLines.length}条弹幕,已${hasFailure ? '部分' : '全部'}发送完成`); // 立即预览弹幕 let istest = false; testStyle(baseParams, asciiParams, 'ascii-coord', istest) setTimeout(() => { updateSendStatus(button, status, ''); }, 3000); } else if (sendMode === 'all') { // 整体发送模式 const text = lines.join(''); const params = { ...baseParams, text: text }; try { await sendDanmaku(params); showMessage('字符画弹幕发送完成'); } catch (error) { console.error('发送失败:', error); showMessage('发送失败,请重试'); } } } // 辅助函数:更新发送进度 function updateSendingProgress(current) { const statusEl = document.querySelector('.enhanced-send-status'); if (statusEl) { const total = parseInt(statusEl.textContent.split('/')[1]); statusEl.textContent = `发送进度:${current}/${total}`; } } /** * 获取基础弹幕参数 */ async function getBaseDanmakuParams() { // 尝试多种方式获取 aid 和 cid let aid = null; let cid = null; // 方法1: 从 __INITIAL_STATE__ 获取 if (window.__INITIAL_STATE__) { aid = window.__INITIAL_STATE__.aid; cid = window.__INITIAL_STATE__.epInfo?.cid || window.__INITIAL_STATE__.cid; } // 方法2: 从 URL 获取 BV号,然后查询视频信息获取aid和cid if (!aid || !cid) { try { // 从 URL 中提取 bvid const bvidMatch = window.location.pathname.match(/\/video\/(BV[\w]+)/); if (bvidMatch) { const bvid = bvidMatch[1]; // 获取分P号,默认为1 const urlParams = new URLSearchParams(window.location.search); const p = parseInt(urlParams.get('p')) || 1; // 调用API获取视频信息 const response = await fetch(`https://api.bilibili.com/x/web-interface/view?bvid=${bvid}`); const data = await response.json(); if (data.code === 0) { aid = data.data.aid; // 根据分P获取对应的cid if (data.data.pages && data.data.pages.length >= p) { cid = data.data.pages[p - 1].cid; } } } } catch (error) { console.error('获取视频信息失败:', error); } } if (!aid || !cid) { alert('获取视频信息失败,请刷新页面重试'); return null; } const hexColor = document.querySelector('.bui-color-picker-input input').value; const color = convertColorToDecimal(hexColor); const fontSize = parseInt(document.querySelector('.bpx-player-adv-danmaku-font-size input').value); // 优先使用时间输入框的值 const inputTime = parseTimeInput(); const progress = inputTime !== null ? inputTime : Math.floor(window.player.getCurrentTime() * 1000); // 尝试多种方式获取 csrf token let csrf = document.cookie.match(/bili_jct=([^;]+)/)?.[1]; if (!csrf) { // 尝试从页面全局变量获取 csrf = window.bili_jct || window.CSRF_TOKEN; } if (!csrf) { alert('获取CSRF Token失败,请确保已登录'); return null; } return { aid, cid, color, fontSize, progress, csrf }; } /** * 获取高级弹幕参数 */ function getAdvancedDanmakuParams() { // 获取所有参数 const startX = document.querySelector('.bpx-player-adv-danmaku-spinner[data-key="startX"] input').value; const startY = document.querySelector('.bpx-player-adv-danmaku-spinner[data-key="startY"] input').value; const sOpacity = document.querySelector('.bpx-player-adv-danmaku-spinner[data-key="sOpacity"] input').value; const eOpacity = document.querySelector('.bpx-player-adv-danmaku-spinner[data-key="eOpacity"] input').value; const duration = document.querySelector('.bpx-player-adv-danmaku-spinner[data-key="duration"] input').value; const text = document.querySelector('.bpx-player-adv-danmaku-text-input textarea').value; const zRotate = document.querySelector('.bpx-player-adv-danmaku-spinner[data-key="zRotate"] input').value; const yRotate = document.querySelector('.bpx-player-adv-danmaku-spinner[data-key="yRotate"] input').value; const endX = document.querySelector('.bpx-player-adv-danmaku-spinner[data-key="endX"] input').value; const endY = document.querySelector('.bpx-player-adv-danmaku-spinner[data-key="endY"] input').value; const aTime = document.querySelector('.bpx-player-adv-danmaku-spinner[data-key="aTime"] input').value; const aDelay = document.querySelector('.bpx-player-adv-danmaku-spinner[data-key="aDelay"] input').value; const stroke = document.querySelector('.bpx-player-adv-danmaku-font-stroke input').checked ? 1 : 0; const family = document.querySelector('.bpx-player-adv-danmaku-font-family-select .bui-select-result').textContent; const linearSpeedUp = document.querySelector('.bpx-player-adv-danmaku-speedup input').checked ? 1 : 0; return { startX, startY, sOpacity, eOpacity, duration, text, zRotate, yRotate, endX, endY, aTime, aDelay, stroke, family, linearSpeedUp }; } /** * 发送阴影效果弹幕 */ async function sendShadowDanmaku(currentStyle) { const container = document.querySelector('.enhanced-danmaku-container'); const button = container.querySelector('.enhanced-send-btn'); const status = container.querySelector('.enhanced-send-status'); try { const params = getAdvancedDanmakuParams(); if (!params.text) { alert('请输入弹幕内容'); return; } const shadowColor = document.querySelector('.shadow-color').value; const offsetX = parseFloat(document.querySelector('.shadow-offset-x').value); const offsetY = parseFloat(document.querySelector('.shadow-offset-y').value); if (!shadowColor || isNaN(offsetX) || isNaN(offsetY)) { alert('请填写阴影颜色和偏移值'); return; } const baseParams = await getBaseDanmakuParams(); if (!baseParams) return; // 更新发送状态,阴影效果共2条弹幕 let baseTime = 5; // 基础时间:1个间隔,5秒 let estimatedTime = baseTime; await updateSendStatus(button, status, `共2条弹幕,预计发送${estimatedTime}秒。正在发送中`, true); let hasFailure = false; let failureCount = 0; // 发送阴影弹幕,支持小数坐标 const shadowParams = { ...params }; shadowParams.startX = trimTrailingZeros((parseFloat(params.startX) + offsetX).toFixed(3)); shadowParams.startY = trimTrailingZeros((parseFloat(params.startY) + offsetY).toFixed(3)); shadowParams.endX = trimTrailingZeros((parseFloat(params.endX) + offsetX).toFixed(3)); shadowParams.endY = trimTrailingZeros((parseFloat(params.endY) + offsetY).toFixed(3)); try { await sendDanmakuWithRetry(shadowParams, convertColorToDecimal(shadowColor), baseParams.fontSize); } catch (error) { console.error('阴影弹幕发送失败:' + error.message); hasFailure = true; failureCount++; // 只在第一次失败时更新预计时间 if (failureCount === 1) { estimatedTime = baseTime + 21; // 增加一次重试的时间 await updateSendStatus(button, status, `共2条弹幕,预计发送${estimatedTime}秒。正在发送中`, true); } } const waitTime = hasFailure ? 21000 : 5000; await new Promise(resolve => setTimeout(resolve, waitTime)); // 发送原始弹幕 const progress = baseParams.progress + 1; try { await sendDanmakuWithRetry(params, baseParams.color, baseParams.fontSize, progress); } catch (error) { console.error('原始弹幕发送失败:' + error.message); hasFailure = true; failureCount++; } // 发送完成 await updateSendStatus(button, status, `共2条弹幕,已${hasFailure ? '部分' : '全部'}发送完成`); // 立即预览弹幕 let istest = false; testStyle(baseParams, params, currentStyle, istest) setTimeout(() => { updateSendStatus(button, status, ''); }, 3000); } catch (error) { // 发送失败 await updateSendStatus(button, status, '发送失败:' + error.message); setTimeout(() => { updateSendStatus(button, status, ''); }, 3000); } } /** * 发送描边效果弹幕 */ async function sendStrokeDanmaku(currentStyle) { const container = document.querySelector('.enhanced-danmaku-container'); const button = container.querySelector('.enhanced-send-btn'); const status = container.querySelector('.enhanced-send-status'); try { const params = getAdvancedDanmakuParams(); if (!params.text) { alert('请输入弹幕内容'); return; } const strokeColor = document.querySelector('.stroke-color').value; const spacing = document.querySelector('.stroke-spacing').value; if (!strokeColor || !spacing) { alert('请填写描边颜色和间距'); return; } const baseParams = await getBaseDanmakuParams(); if (!baseParams) return; // 更新发送状态,描边效果共9条弹幕 let baseTime = 8 * 5; // 基础时间:8个间隔,每个5秒 let estimatedTime = baseTime; await updateSendStatus(button, status, `共9条弹幕,预计发送${estimatedTime}秒。正在发送中`, true); let hasFailure = false; let failureCount = 0; // 发送9条弹幕 for (let i = 0; i < 9; i++) { const currentParams = { ...params }; currentParams.stroke = 0; if (i < 8) { // 前8条是描边,支持小数坐标 const positions = [ { x: -1, y: -1 }, // 左上 { x: 0, y: -1 }, // 中上 { x: 1, y: -1 }, // 右上 { x: -1, y: 0 }, // 左中 { x: 1, y: 0 }, // 右中 { x: -1, y: 1 }, // 左下 { x: 0, y: 1 }, // 中下 { x: 1, y: 1 } // 右下 ]; const offsetX = parseFloat(spacing) * positions[i].x; const offsetY = parseFloat(spacing) * positions[i].y; currentParams.startX = trimTrailingZeros((parseFloat(params.startX) + offsetX).toFixed(3)); currentParams.startY = trimTrailingZeros((parseFloat(params.startY) + offsetY).toFixed(3)); currentParams.endX = trimTrailingZeros((parseFloat(params.endX) + offsetX).toFixed(3)); currentParams.endY = trimTrailingZeros((parseFloat(params.endY) + offsetY).toFixed(3)); try { await sendDanmakuWithRetry(currentParams, convertColorToDecimal(strokeColor), baseParams.fontSize); } catch (error) { console.error(`第${i + 1}条弹幕发送失败:${error.message}`); hasFailure = true; failureCount++; // 只在第一次失败时更新预计时间 if (failureCount === 1) { estimatedTime = baseTime + 21; // 增加一次重试的时间 await updateSendStatus(button, status, `共9条弹幕,预计发送${estimatedTime}秒。正在发送中`, true); } continue; } } else { // 最后一条是原始弹幕,时间延迟0.001秒 const progress = baseParams.progress + 1; try { await sendDanmakuWithRetry(currentParams, baseParams.color, baseParams.fontSize, progress); } catch (error) { console.error(`最后一条弹幕发送失败:${error.message}`); hasFailure = true; failureCount++; } } if (i < 8) { const waitTime = hasFailure ? 21000 : 5000; await new Promise(resolve => setTimeout(resolve, waitTime)); } } // 发送完成 await updateSendStatus(button, status, `共9条弹幕,已${hasFailure ? '部分' : '全部'}发送完成`); // 立即预览弹幕 let istest = false; testStyle(baseParams, params, currentStyle, istest) setTimeout(() => { updateSendStatus(button, status, ''); }, 3000); } catch (error) { // 发送失败 await updateSendStatus(button, status, '发送失败:' + error.message); setTimeout(() => { updateSendStatus(button, status, ''); }, 3000); } } /** * 发送文字背景弹幕 */ async function sendBackgroundDanmaku(currentStyle) { const container = document.querySelector('.enhanced-danmaku-container'); const button = container.querySelector('.enhanced-send-btn'); const status = container.querySelector('.enhanced-send-status'); try { const params = getAdvancedDanmakuParams(); if (!params.text) { alert('请输入弹幕内容'); return; } const backgroundColor = document.querySelector('.background-color').value; const backgroundChar = document.querySelector('.background-char').value; const isVertical = document.querySelector('.vertical-text').checked; if (!backgroundColor || !backgroundChar) { alert('请填写背景颜色和背景字符'); return; } const baseParams = await getBaseDanmakuParams(); if (!baseParams) return; // 更新发送状态,文字背景效果共2条弹幕 let baseTime = 5; // 基础时间:1个间隔,5秒 let estimatedTime = baseTime; await updateSendStatus(button, status, `共2条弹幕,预计发送${estimatedTime}秒。正在发送中`, true); let hasFailure = false; let failureCount = 0; // 发送背景弹幕 const backgroundParams = { ...params }; let bgText = backgroundChar.repeat(params.text.length); let originalText = params.text; // 如果勾选了转竖列,转换背景字符和原文本 if (isVertical) { bgText = convertToVertical(bgText); originalText = convertToVertical(originalText); } backgroundParams.text = bgText; backgroundParams.fontFamily = 'SimHei'; backgroundParams.stroke = 0; try { await sendDanmakuWithRetry(backgroundParams, convertColorToDecimal(backgroundColor), baseParams.fontSize); } catch (error) { console.error('背景弹幕发送失败:' + error.message); hasFailure = true; failureCount++; // 只在第一次失败时更新预计时间 if (failureCount === 1) { estimatedTime = baseTime + 21; // 增加一次重试的时间 await updateSendStatus(button, status, `共2条弹幕,预计发送${estimatedTime}秒。正在发送中`, true); } } const waitTime = hasFailure ? 21000 : 5000; await new Promise(resolve => setTimeout(resolve, waitTime)); // 发送原始弹幕 const progress = baseParams.progress + 1; params.text = originalText; // 使用可能转换后的文本 try { await sendDanmakuWithRetry(params, baseParams.color, baseParams.fontSize, progress); } catch (error) { console.error('原始弹幕发送失败:' + error.message); hasFailure = true; failureCount++; } // 发送完成 await updateSendStatus(button, status, `共2条弹幕,已${hasFailure ? '部分' : '全部'}发送完成`); // 立即预览弹幕 let istest = false; testStyle(baseParams, params, currentStyle, istest) setTimeout(() => { updateSendStatus(button, status, ''); }, 3000); } catch (error) { // 发送失败 await updateSendStatus(button, status, '发送失败:' + error.message); setTimeout(() => { updateSendStatus(button, status, ''); }, 3000); } } /** * 发送普通弹幕 */ async function sendNormalDanmaku(currentStyle) { const container = document.querySelector('.enhanced-danmaku-container'); const button = container.querySelector('.enhanced-send-btn'); const status = container.querySelector('.enhanced-send-status'); try { const params = getAdvancedDanmakuParams(); if (!params.text) { alert('请输入弹幕内容'); return; } const baseParams = await getBaseDanmakuParams(); if (!baseParams) return; // 更新发送状态,文字背景效果共2条弹幕 let baseTime = 5; // 基础时间:1个间隔,5秒 let estimatedTime = baseTime; await updateSendStatus(button, status, `共1条弹幕,预计发送${estimatedTime}秒。正在发送中`, true); let hasFailure = false; // 发送原始弹幕 try { await sendDanmakuWithRetry(params, baseParams.color, baseParams.fontSize); } catch (error) { console.error('原始弹幕发送失败:' + error.message); hasFailure = true; } // 发送完成 await updateSendStatus(button, status, `共1条弹幕,已${hasFailure ? '部分' : '全部'}发送完成`); // 立即预览弹幕 let istest = false; testStyle(baseParams, params, currentStyle, istest) setTimeout(() => { updateSendStatus(button, status, ''); }, 3000); } catch (error) { // 发送失败 await updateSendStatus(button, status, '发送失败:' + error.message); setTimeout(() => { updateSendStatus(button, status, ''); }, 3000); } } /** * 发送单条弹幕 */ async function sendDanmaku(params, color, fontSize, progress = null) { const baseParams = await getBaseDanmakuParams(); if (!baseParams) return; const formData = new URLSearchParams({ type: 1, oid: baseParams.cid, msg: buildAdvancedDanmakuText(params, fontSize), aid: baseParams.aid, progress: progress || baseParams.progress, color: color, fontsize: fontSize, pool: 0, mode: 7, rnd: Math.floor(Date.now() / 1000), csrf: baseParams.csrf }); // 打印当前弹幕发送时间 console.log(`当前弹幕发送时间: ${new Date().toLocaleString()}`); const response = await fetch('https://api.bilibili.com/x/v2/dm/post', { method: 'POST', body: formData, credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); const result = await response.json(); if (result.code !== 0) { throw new Error(result.message); } } /** * 发送单条弹幕(带重试) */ async function sendDanmakuWithRetry(params, color, fontSize, progress = null, retryCount = 1) { try { await sendDanmaku(params, color, fontSize, progress); return true; } catch (error) { if (retryCount > 0) { // 失败后等待21秒重试 await new Promise(resolve => setTimeout(resolve, 21000)); return sendDanmakuWithRetry(params, color, fontSize, progress, retryCount - 1); } throw error; } } /** * 更新发送状态显示 */ async function updateSendStatus(button, status, text, isLoading = false) { // 更新按钮状态 if (isLoading) { button.classList.add('disabled'); button.style.pointerEvents = 'none'; } else { button.classList.remove('disabled'); button.style.pointerEvents = 'auto'; } // 更新状态文本显示 if (text) { status.style.display = 'inline-block'; if (isLoading) { let dots = 0; const interval = setInterval(() => { status.textContent = text + '.'.repeat(dots + 1); dots = (dots + 1) % 3; }, 500); // 保存interval ID到status元素 status.dataset.intervalId = interval; } else { // 清除现有的interval const intervalId = parseInt(status.dataset.intervalId); if (intervalId) { clearInterval(intervalId); delete status.dataset.intervalId; } status.textContent = text; } } else { status.style.display = 'none'; status.textContent = ''; // 清除interval const intervalId = parseInt(status.dataset.intervalId); if (intervalId) { clearInterval(intervalId); delete status.dataset.intervalId; } } } /** * 解析XML格式的弹幕文件 */ function parseXMLDanmaku(content) { const parser = new DOMParser(); const xml = parser.parseFromString(content, 'text/xml'); const danmakuList = []; xml.querySelectorAll('d').forEach(d => { const p = d.getAttribute('p').split(','); danmakuList.push({ stime: parseFloat(p[0]) * 1000, mode: parseInt(p[1]), size: parseInt(p[2]), color: parseInt(p[3]), date: parseInt(p[4]), pool: parseInt(p[5]), uhash: p[6], dmid: p[7], text: d.textContent }); }); return danmakuList; } /** * 解析JSON格式的弹幕文件 */ function parseJSONDanmaku(content) { const data = JSON.parse(content); return data.map(item => ({ stime: item.progress || item.stime * 1000, mode: item.mode, size: item.fontsize, color: item.color, date: item.ctime, pool: 0, uhash: '', dmid: item.dmid || '', text: item.text })); } /** * 解析时间输入框的值,转换为毫秒 */ function parseTimeInput() { /* * 支持格式: * - 秒数(如:5) * - 时间格式: * - M:SS(如:6:05) * - H:MM:SS(如:1:00:01) * - MM:SS(如:06:05) * - HH:MM:SS(如:01:00:01) * - 带毫秒的时间格式: * - M:SS.SSS * - H:MM:SS.SSS * - MM:SS.SSS * - HH:MM:SS.SSS * */ const timeInput = document.querySelector('.bpx-player-adv-danmaku-showtime-input input'); if (!timeInput || !timeInput.value.trim()) return null; const value = timeInput.value.trim(); // 尝试解析时间格式 if (value.includes(':')) { // 先处理可能存在的毫秒部分 let mainPart = value; let milliseconds = 0; if (value.includes('.')) { const [timePart, msPart] = value.split('.'); mainPart = timePart; // 将毫秒部分标准化为3位数 milliseconds = parseInt((msPart + '000').slice(0, 3)); } const parts = mainPart.split(':').map(Number); let totalMilliseconds = milliseconds; // 检查每个部分是否为有效数字 if (parts.some(isNaN)) return null; if (parts.length === 2) { // M:SS 或 MM:SS 格式 const [minutes, seconds] = parts; if (seconds >= 60) return null; // 秒数不能超过60 totalMilliseconds += (minutes * 60 + seconds) * 1000; return totalMilliseconds; } else if (parts.length === 3) { // H:MM:SS 或 HH:MM:SS 格式 const [hours, minutes, seconds] = parts; if (minutes >= 60 || seconds >= 60) return null; // 分秒不能超过60 totalMilliseconds += (hours * 3600 + minutes * 60 + seconds) * 1000; return totalMilliseconds; } } // 尝试解析秒数(支持小数) const seconds = parseFloat(value); if (!isNaN(seconds)) { return Math.floor(seconds * 1000); } return null; } /** * 组装高级弹幕文本 */ function buildAdvancedDanmakuText(params, fontSize) { const text = `[${params.startX},${params.startY},"${params.sOpacity}-${params.eOpacity}",${params.duration},"${params.text}",${params.zRotate},${params.yRotate},${params.endX},${params.endY},${params.aTime},${params.aDelay},${params.stroke},"${params.family}",${params.linearSpeedUp}]`; return text; } /** * 将文本转换为竖列格式 * @param {string} text - 原始文本 * @returns {string} - 转换后的竖列文本 */ function convertToVertical(text) { return text.split('').join('\\n'); } /** * 将16进制颜色代码转换为10进制 * @param {string} hexColor - 16进制颜色代码,例如 "#FFFFFF" * @returns {number} 10进制颜色值 */ function convertColorToDecimal(hexColor) { // 移除#号并转换为10进制 return parseInt(hexColor.replace('#', ''), 16); } /** * 去除数字字符串末尾多余的0 * @param {string} numStr - 数字字符串 * @returns {string} - 处理后的数字字符串 */ function trimTrailingZeros(numStr) { return numStr.replace(/\.?0+$/, ''); } /** * 监听并修改弹幕发送时间 */ function setupDanmakuTimeModifier() { // 使用 Proxy 拦截 XMLHttpRequest const XHRProxy = new Proxy(XMLHttpRequest, { construct(target) { const xhr = new target(); // 保存原始的 open 方法 const originalOpen = xhr.open; xhr.open = function (method, url, ...args) { // 标记弹幕发送请求 if (url.includes('/x/v2/dm/post')) { xhr._isDanmakuRequest = true; } return originalOpen.call(xhr, method, url, ...args); }; // 保存原始的 send 方法 const originalSend = xhr.send; xhr.send = function (body) { if (xhr._isDanmakuRequest && body) { const timeInput = document.querySelector('.bpx-player-adv-danmaku-showtime-input input'); if (timeInput && timeInput.value && timeInput.value.includes('.')) { console.log('检测到小数点时间:', timeInput.value); const inputTime = parseTimeInput(); if (inputTime !== null) { const formData = new URLSearchParams(body); formData.set('progress', inputTime); body = formData.toString(); console.log('出现时间修改后的请求参数:', body); } } const durationInput = document.querySelector('.bpx-player-adv-danmaku-duration .bui-input-input'); if (durationInput) { const durationValue = parseFloat(durationInput.value); if (durationValue > 10) { const formData = new URLSearchParams(body); const msgArray = JSON.parse(formData.get('msg')); msgArray[3] = durationValue; // 替换第四个值为新的持续时间 formData.set('msg', JSON.stringify(msgArray)); body = formData.toString(); console.log('生存时间修改后的请求参数:', body); } } const animationTimeInput = document.querySelector('.bpx-player-adv-danmaku-animation-time input'); if (animationTimeInput) { const animationTimeValue = parseFloat(animationTimeInput.value); if (animationTimeValue > 10000) { const formData = new URLSearchParams(body); const msgArray = JSON.parse(formData.get('msg')); msgArray[9] = animationTimeValue; // 替换第十个值为新的运动耗时 formData.set('msg', JSON.stringify(msgArray)); body = formData.toString(); console.log('运动耗时修改后的请求参数:', body); } } const animationDelayInput = document.querySelector('.bpx-player-adv-danmaku-animation-delay input'); if (animationDelayInput) { const animationDelayValue = parseFloat(animationDelayInput.value); if (animationDelayValue > 10000) { const formData = new URLSearchParams(body); const msgArray = JSON.parse(formData.get('msg')); msgArray[10] = animationDelayValue; // 替换第十一个值为新的延迟时间 formData.set('msg', JSON.stringify(msgArray)); body = formData.toString(); console.log('延迟时间修改后的请求参数:', body); } } const startXInput = document.querySelector('.bpx-player-adv-danmaku-pos-start .bpx-player-adv-danmaku-spinner[data-key="startX"] input'); if (startXInput) { const startXValue = parseFloat(startXInput.value); if (startXValue < 0) { const formData = new URLSearchParams(body); const msgArray = JSON.parse(formData.get('msg')); msgArray[0] = startXValue; // 替换第十二个值为新的起始X formData.set('msg', JSON.stringify(msgArray)); body = formData.toString(); } } const startYInput = document.querySelector('.bpx-player-adv-danmaku-pos-start .bpx-player-adv-danmaku-spinner[data-key="startY"] input'); if (startYInput) { const startYValue = parseFloat(startYInput.value); if (startYValue < 0) { const formData = new URLSearchParams(body); const msgArray = JSON.parse(formData.get('msg')); msgArray[1] = startYValue; // 替换第十三个值为新的起始Y formData.set('msg', JSON.stringify(msgArray)); body = formData.toString(); } } const endXInput = document.querySelector('.bpx-player-adv-danmaku-pos-end .bpx-player-adv-danmaku-spinner[data-key="endX"] input'); if (endXInput) { const endXValue = parseFloat(endXInput.value); if (endXValue < 0) { const formData = new URLSearchParams(body); const msgArray = JSON.parse(formData.get('msg')); msgArray[7] = endXValue; // 替换第七个值为新的结束X formData.set('msg', JSON.stringify(msgArray)); body = formData.toString(); } } const endYInput = document.querySelector('.bpx-player-adv-danmaku-pos-end .bpx-player-adv-danmaku-spinner[data-key="endY"] input'); if (endYInput) { const endYValue = parseFloat(endYInput.value); if (endYValue < 0) { const formData = new URLSearchParams(body); const msgArray = JSON.parse(formData.get('msg')); msgArray[8] = endYValue; // 替换第八个值为新的结束Y formData.set('msg', JSON.stringify(msgArray)); body = formData.toString(); body = formData.toString(); console.log('结束Y修改后的请求参数:', body); body = formData.toString(); console.log('结束Y修改后的请求参数:', body); } } } return originalSend.call(xhr, body); }; return xhr; } }); // 替换原始的 XMLHttpRequest window.XMLHttpRequest = XHRProxy; // 监听发送按钮以便调试 const observer = new MutationObserver((mutations, obs) => { const sendButton = document.querySelector('.bpx-player-adv-danmaku-send-send .bui-area.bui-button-large'); if (sendButton && !sendButton.dataset.enhanced) { sendButton.dataset.enhanced = 'true'; sendButton.addEventListener('click', () => { const timeInput = document.querySelector('.bpx-player-adv-danmaku-showtime-input input'); if (timeInput && timeInput.value) { const inputTime = parseTimeInput(); } }, true); } }); // 开始观察文档变化 observer.observe(document.body, { childList: true, subtree: true }); } /** * 设置样式弹幕的颜色选择器 */ function setupStyleColorPickers(container) { const colorPickers = { '.shadow-color': '.shadow-params', '.stroke-color': '.stroke-params', '.background-color': '.background-params', }; // 添加自定义样式 const style = document.createElement('style'); style.textContent = ` .enhanced-danmaku-container .bui-color-picker-input { width: 70px !important; } .enhanced-danmaku-container .bui-color-picker-input input { width: 100% !important; } `; document.head.appendChild(style); Object.entries(colorPickers).forEach(([inputSelector, paramsSelector]) => { const colorInput = container.querySelector(`${paramsSelector} ${inputSelector}`); const displayArea = colorInput.closest('.bui-color-picker-result').querySelector('.bui-color-picker-display'); // 创建隐藏的颜色选择器 const colorPicker = document.createElement('input'); colorPicker.type = 'color'; colorPicker.style.cssText = ` width: 0; height: 0; padding: 0; border: none; position: absolute; visibility: hidden; `; displayArea.parentNode.appendChild(colorPicker); // 点击显示区域时触发颜色选择器 displayArea.style.cursor = 'pointer'; displayArea.addEventListener('click', () => { colorPicker.value = colorInput.value; colorPicker.click(); }); // 监听颜色变化 colorPicker.addEventListener('input', (e) => { const hexColor = e.target.value.toUpperCase(); colorInput.value = hexColor; displayArea.style.background = hexColor; colorInput.dispatchEvent(new Event('input', { bubbles: true })); }); // 监听输入框变化 colorInput.addEventListener('input', (e) => { const hexColor = e.target.value.toUpperCase(); displayArea.style.background = hexColor; }); }); } // 在预览弹幕时自动播放 function autoPlayAfterPreview() { console.log("自动播放"); // 获取播放按钮和音量按钮 const playButton = document.querySelector('.bpx-player-ctrl-play'); const volumeButton = document.querySelector('.bpx-player-ctrl-volume-icon'); if (!playButton) { return; } // 检查当前音量状态 const playerContainer = document.querySelector('.bpx-player-container'); const isMuted = playerContainer.classList.contains('bpx-player-volume-0'); let needRestoreVolume = false; // 如果当前有声音,则先静音并延迟播放 if (!isMuted) { console.log("1.临时静音"); volumeButton.click(); needRestoreVolume = true; // 延迟执行后续操作,确保静音生效 setTimeout(() => { playVideo(); }, 100); } else { // 已经是静音状态,直接播放 playVideo(); } function playVideo() { // 如果当前是暂停状态,模拟点击播放按钮 if (playerContainer.classList.contains('bpx-state-paused')) { console.log("2.点击播放按钮"); playButton.click(); // 确保弹幕容器也取消暂停状态 const danmakuContainer = document.querySelector('.bpx-player-row-dm-wrap'); if (danmakuContainer) { danmakuContainer.classList.remove('bili-danmaku-x-paused'); } // 短暂延迟后暂停播放 setTimeout(() => { console.log("3.暂停播放"); playButton.click(); // 如果之前有声音,恢复声音 if (needRestoreVolume) { console.log("4.恢复声音"); volumeButton.click(); } // 保持弹幕继续显示 if (danmakuContainer) { danmakuContainer.classList.remove('bili-danmaku-x-paused'); } }, 500); } } } // 修改预览弹幕的处理函数 function previewDanmaku(currentDanmakuList) { if (!currentDanmakuList) { return; } injectDanmaku(currentDanmakuList); } (function () { 'use strict'; // 初始化劫持弹幕历史记录功能 hookLoadHistory(); // 等待 DOM 加载完成 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } function init() { // 确保 document.body 存在 if (!document.body) { setTimeout(init, 100); return; } // 添加隐藏的localDmFile元素 const localDmFile = document.createElement('div'); localDmFile.id = 'localDmFile'; localDmFile.style.display = 'none'; document.body.appendChild(localDmFile); const localRemoveDmFile = document.createElement('div'); localRemoveDmFile.id = 'localRemoveDmFile'; localRemoveDmFile.style.display = 'none'; document.body.appendChild(localRemoveDmFile); console.log("启用高级弹幕加强功能"); // 监听弹幕列表加载 const listObserver = new MutationObserver((mutations, obs) => { const collapseWrap = document.querySelector('.bui-collapse-wrap'); if (collapseWrap) { // 如果是折叠状态,点击展开 if (collapseWrap.classList.contains('bui-collapse-wrap-folded')) { const header = collapseWrap.querySelector('.bui-collapse-header'); if (header) { header.click(); } } obs.disconnect(); } }); // 开始观察文档变化 listObserver.observe(document.body, { childList: true, subtree: true }); // addNewFonts(); // 监听视频播放器加载 const observer = new MutationObserver((mutations, obs) => { const dropdownName = document.querySelector('.bui-dropdown-name'); if (dropdownName && dropdownName.textContent === '高级弹幕') { obs.disconnect(); setupFontSelector(); setupEnhancedSendButton(); bypassDurationLimit(); } }); setupDanmakuTimeModifier(); observer.observe(document.body, { childList: true, subtree: true }); } })(); // 方案1: 移除输入限制 function removeInputLimit() { const durationInput = document.querySelector('.bpx-player-adv-danmaku-duration input'); if (durationInput) { // 克隆并替换节点来移除所有事件监听器 const newInput = durationInput.cloneNode(true); durationInput.parentNode.replaceChild(newInput, durationInput); // 移除max属性 newInput.removeAttribute('max'); // 更新dataset newInput.closest('.bpx-player-adv-danmaku-spinner').dataset.max = '99999'; } } // 方案2: 监听并阻止值被重置 function bypassDurationLimit() { // 需要解除限制的输入框选择器列表 const limitedInputs = [ '.bpx-player-adv-danmaku-duration input', // 生存时间 '.bpx-player-adv-danmaku-animation-time input', // 运动耗时 '.bpx-player-adv-danmaku-animation-delay input', // 延迟时间 '.bpx-player-adv-danmaku-pos-start .bpx-player-adv-danmaku-spinner[data-key="startX"] input', // 起始X '.bpx-player-adv-danmaku-pos-start .bpx-player-adv-danmaku-spinner[data-key="startY"] input', // 起始Y '.bpx-player-adv-danmaku-pos-end .bpx-player-adv-danmaku-spinner[data-key="endX"] input', // 结束X '.bpx-player-adv-danmaku-pos-end .bpx-player-adv-danmaku-spinner[data-key="endY"] input' // 结束Y ]; limitedInputs.forEach(selector => { const input = document.querySelector(selector); if (!input) return; let userValue = input.value; // 使用MutationObserver监听值的变化 const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'attributes' && mutation.attributeName === 'value') { if (input.value !== userValue) { input.value = userValue; } } }); }); // 监听value属性变化 observer.observe(input, { attributes: true, attributeFilter: ['value'] }); // 监听用户输入 input.addEventListener('input', (e) => { userValue = e.target.value; }); // 监听失焦事件 input.addEventListener('blur', () => { setTimeout(() => { if (input.value !== userValue) { input.value = userValue; } }, 0); }); }); }