抖音弹幕加一助手

实现抖音弹幕鼠标悬停加一功能,体验类似于斗鱼加一功能

// ==UserScript==
// @name         抖音弹幕加一助手
// @namespace    http://tampermonkey.net/
// @version      1.16
// @description  实现抖音弹幕鼠标悬停加一功能,体验类似于斗鱼加一功能
// @author       A1LExX
// @match        https://live.douyin.com/*
// @match        https://www.douyin.com/root/live/*
// @match        https://www.douyin.com/*
// @grant        none
// @license      GPL-3.0-only 
// ==/UserScript==
// ==Changelog==
// v1.16 测试更新推送
// v1.9  修复按钮垂直居中、视觉对齐问题,优化体验
// v1.8  支持过滤系统弹幕,优化兼容性
// v1.7  初始版本,实现弹幕加一功能
// ==/Changelog==

(function () {
    'use strict';

    // --------- 配置 ---------
    const DANMU_SELECTOR = 'xg-danmu div'; // 弹幕单条
    const DANMU_CONTAINER_SELECTOR = 'xg-danmu';
    const BUTTON_TEXT = '+1';
    const BUTTON_ID = '__douyin_plusone_btn__';
    const BUTTON_STYLE = `
        position: fixed;
        z-index: 99999;
        display: none;
        padding: 2px 8px;
        font-size: 14px;
        background: #ff2c55;
        color: #fff;
        border: none;
        border-radius: 12px;
        cursor: pointer;
        box-shadow: 0 2px 8px rgba(0,0,0,0.15);
        transition: opacity 0.2s;
        opacity: 0.95;
        pointer-events: auto;
    `;

    const FILTER_KEYWORDS = [
        '送出'
    ];

    function isFilteredDanmu(text) {
        return FILTER_KEYWORDS.some(keyword => text.includes(keyword));
    }

    // --------- 工具函数 ---------
    // 递归提取弹幕文本(含 emoji)
    function extractDanmuText(node) {
        if (!node) return '';
        if (node.nodeType === Node.TEXT_NODE) return node.textContent;
        if (node.nodeType === Node.ELEMENT_NODE) {
            if (node.tagName === 'IMG' && node.alt) return node.alt;
            return Array.from(node.childNodes).map(extractDanmuText).join('');
        }
        return '';
    }

    // 查找输入框和发送按钮
    function findInputAndSendBtn() {
        // 输入框是 Slate 编辑器,内容区有 contenteditable="true"
        const input = document.querySelector('[contenteditable="true"]');
        // 发送按钮通常有 data-e2e="chat-send-btn"
        const sendBtn = document.querySelector('[data-e2e="chat-send-btn"]');
        return { input, sendBtn };
    }

    // 填入输入框并自动发送
    function fillAndSend(content) {
        const input = document.querySelector('.zone-container.editor-kit-container[contenteditable="true"]');
        if (input && content) {
            // 只操作 ace-line 下的 span
            const aceLine = input.querySelector('.ace-line');
            if (aceLine) {
                const span = aceLine.querySelector('span[data-string]');
                if (span) {
                    span.textContent = content;
                }
            }
            input.focus();
            // 只触发一次 input 事件
            input.dispatchEvent(new InputEvent('input', {bubbles: true, composed: true}));
            // 200ms后点击发送按钮
            setTimeout(waitAndSend, 200);
        }
    }

    // 自动点击发送按钮
    function waitAndSend() {
        const btn = document.querySelector('.webcast-chatroom___send-btn');
        if (!btn) return;
        if (btn.classList.contains('disable') || btn.hasAttribute('disabled')) {
            setTimeout(waitAndSend, 50);
        } else {
            let clickable = btn;
            if (btn.tagName.toLowerCase() === 'svg' && btn.parentElement) {
                clickable = btn.parentElement;
            }
            ['pointerdown', 'mousedown', 'mouseup', 'click'].forEach(type => {
                clickable.dispatchEvent(new MouseEvent(type, {bubbles: true, cancelable: true, view: window}));
            });
            // 再尝试模拟回车
            const input = document.querySelector('.zone-container.editor-kit-container[contenteditable="true"]');
            if (input) {
                ['keydown', 'keypress', 'keyup'].forEach(type => {
                    const event = new KeyboardEvent(type, {bubbles: true, cancelable: true, key: 'Enter', code: 'Enter', keyCode: 13, which: 13});
                    input.dispatchEvent(event);
                });
            }
        }
    }

    // --------- "加一"按钮 ---------
    let plusOneBtn = document.getElementById(BUTTON_ID);
    if (!plusOneBtn) {
        plusOneBtn = document.createElement('button');
        plusOneBtn.id = BUTTON_ID;
        plusOneBtn.innerText = BUTTON_TEXT;
        plusOneBtn.setAttribute('style', BUTTON_STYLE);
        document.body.appendChild(plusOneBtn);
    }

    let currentDanmu = null;
    let danmuState = { transform: '', transition: '' };
    let hideTimer = null;
    let plusOneText = ''; // 全局变量

    // 按钮点击
    plusOneBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        console.log('加一按钮被点击');
        console.log('将要填充的内容:', plusOneText);
        fillAndSend(plusOneText);
    });

    // 鼠标在按钮上时不消失
    plusOneBtn.addEventListener('mouseenter', () => {
        if (hideTimer) clearTimeout(hideTimer);
    });
    plusOneBtn.addEventListener('mouseleave', () => {
        hideTimer = setTimeout(() => {
            plusOneBtn.style.display = 'none';
            if (currentDanmu) {
                currentDanmu.style.transform = danmuState.transform;
                currentDanmu.style.transition = danmuState.transition;
                currentDanmu = null;
            }
        }, 200);
    });

    // 鼠标移动时检测是否在弹幕上
    document.addEventListener('mousemove', (e) => {
        const danmuDivs = Array.from(document.querySelectorAll(DANMU_SELECTOR));
        let found = false;
        for (const div of danmuDivs) {
            const rect = div.getBoundingClientRect();
            if (
                e.clientX >= rect.left &&
                e.clientX <= rect.right &&
                e.clientY >= rect.top &&
                e.clientY <= rect.bottom
            ) {
                const text = extractDanmuText(div);
                if (isFilteredDanmu(text)) {
                    plusOneBtn.style.display = 'none';
                    continue;
                }
                found = true;
                if (currentDanmu !== div) {
                    // 恢复上一个弹幕动画
                    if (currentDanmu) {
                        currentDanmu.style.transform = danmuState.transform;
                        currentDanmu.style.transition = danmuState.transition;
                    }
                    currentDanmu = div;
                    // 记录当前弹幕动画状态
                    danmuState.transform = div.style.transform;
                    danmuState.transition = div.style.transition;
                    // 冻结当前弹幕
                    const computedTransform = getComputedStyle(div).transform;
                    div.style.transform = computedTransform;
                    div.style.transition = 'none';
                    // 定位按钮(更靠近弹幕右侧)
                    plusOneBtn.style.display = 'block';
                    // 先显示按钮以便获取宽度
                    plusOneBtn.style.left = '-9999px';
                    plusOneBtn.style.top = '-9999px';
                    setTimeout(() => {
                        const btnWidth = plusOneBtn.offsetWidth;
                        const btnHeight = plusOneBtn.offsetHeight;
                        plusOneBtn.style.left = (rect.right - btnWidth - 4) + 'px';
                        plusOneBtn.style.top = (rect.top + (rect.height - btnHeight) / 2 + 3) + 'px';
                    }, 0);
                    plusOneText = text; // 存储内容
                }
                break;
            }
        }
        if (!found && !plusOneBtn.matches(':hover')) {
            plusOneBtn.style.display = 'none';
            if (currentDanmu) {
                currentDanmu.style.transform = danmuState.transform;
                currentDanmu.style.transition = danmuState.transition;
                currentDanmu = null;
            }
        }
    });

    // 兼容性处理
    window.addEventListener('scroll', () => {
        plusOneBtn.style.display = 'none';
        if (currentDanmu) {
            currentDanmu.style.transform = danmuState.transform;
            currentDanmu.style.transition = danmuState.transition;
            currentDanmu = null;
        }
    });
    window.addEventListener('resize', () => {
        plusOneBtn.style.display = 'none';
        if (currentDanmu) {
            currentDanmu.style.transform = danmuState.transform;
            currentDanmu.style.transition = danmuState.transition;
            currentDanmu = null;
        }
    });

    // --------- 过滤系统弹幕(可选) ---------
    // 若需过滤系统弹幕,可在 extractDanmuText 后加判断
    // 例如:if (text.includes('欢迎')) return; // 过滤欢迎类弹幕

    function showPlusBtnForDanmu(danmuDiv) {
        const rect = danmuDiv.getBoundingClientRect();
        const danmuText = extractDanmuText(danmuDiv);
        if (isFilteredDanmu(danmuText)) {
            hidePlusBtn();
            return;
        }
        if (!plusOneBtn) {
            plusOneBtn = document.createElement('button');
            plusOneBtn.innerText = '加一';
            plusOneBtn.style.position = 'fixed';
            plusOneBtn.style.zIndex = '99999';
            plusOneBtn.style.background = '#ff2c55';
            plusOneBtn.style.color = '#fff';
            plusOneBtn.style.border = 'none';
            plusOneBtn.style.borderRadius = '16px';
            plusOneBtn.style.padding = '4px 16px';
            plusOneBtn.style.cursor = 'pointer';
            plusOneBtn.style.fontSize = '15px';
            plusOneBtn.style.fontWeight = 'bold';
            plusOneBtn.style.opacity = '0.96';
            plusOneBtn.style.boxShadow = '0 2px 8px rgba(0,0,0,0.18)';
            plusOneBtn.style.transition = 'background 0.2s, box-shadow 0.2s, opacity 0.2s';
            plusOneBtn.style.pointerEvents = 'auto';
            plusOneBtn.addEventListener('mouseenter', () => {
                plusOneBtn.style.background = '#d81b4c';
                plusOneBtn.style.boxShadow = '0 4px 16px rgba(0,0,0,0.22)';
                plusOneBtn.style.opacity = '1';
            });
            plusOneBtn.addEventListener('mouseleave', () => {
                plusOneBtn.style.background = '#ff2c55';
                plusOneBtn.style.boxShadow = '0 2px 8px rgba(0,0,0,0.18)';
                plusOneBtn.style.opacity = '0.96';
            });
            plusOneBtn.addEventListener('mousedown', e => { e.stopPropagation(); e.preventDefault(); });
            plusOneBtn.addEventListener('mouseup', e => { e.stopPropagation(); e.preventDefault(); });
            plusOneBtn.addEventListener('click', e => {
                e.stopPropagation();
                e.preventDefault();
                const content = extractDanmuText(danmuDiv);
                fillAndSend(content);
                plusOneBtn.style.display = 'none';
            });
            document.body.appendChild(plusOneBtn);
        }
        // 先显示按钮以便获取高度
        plusOneBtn.style.display = 'block';
        plusOneBtn.style.left = '-9999px';
        plusOneBtn.style.top = '-9999px';
        setTimeout(() => {
            const btnWidth = plusOneBtn.offsetWidth;
            const btnHeight = plusOneBtn.offsetHeight;
            plusOneBtn.style.left = (rect.right - btnWidth - 4) + 'px';
            plusOneBtn.style.top = (rect.top + (rect.height - btnHeight) / 2 + 3) + 'px';
        }, 0);

        // 悬停弹幕样式:圆角+淡黑色边框
        danmuDiv.style.boxShadow = '0 0 0 4px rgba(0,0,0,0.18)';
        danmuDiv.style.borderRadius = '16px';

        // 暂停弹幕动画
        if (pausedDanmu && pausedDanmu !== danmuDiv) {
            // 恢复上一个弹幕
            pausedDanmu.style.transition = pausedTransition;
            pausedDanmu.style.transform = pausedTransform;
            pausedDanmu.style.boxShadow = '';
            pausedDanmu.style.borderRadius = '';
            pausedDanmu = null;
        }
        if (danmuDiv) {
            pausedDanmu = danmuDiv;
            pausedTransition = danmuDiv.style.transition;
            pausedTransform = danmuDiv.style.transform;
            // 立即暂停动画
            danmuDiv.style.transition = 'none';
            // 保持当前位置
            // 不需要修改 transform,直接保留当前 transform 即可
        }
    }

    function hidePlusBtn() {
        if (plusOneBtn) {
            plusOneBtn.style.display = 'none';
        }
        // 恢复弹幕动画和样式
        if (pausedDanmu) {
            pausedDanmu.style.transition = pausedTransition;
            pausedDanmu.style.transform = pausedTransform;
            pausedDanmu.style.boxShadow = '';
            pausedDanmu.style.borderRadius = '';
            pausedDanmu = null;
        }
        currentDanmu = null;
    }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址