Greasy Fork

Greasy Fork is available in English.

弹幕结尾自动添加desuwa

要开开发者模式!代码爆改b站@少女乐队抹茶大芭菲,原作者@阿琴-kotori

// ==UserScript==
// @name         弹幕结尾自动添加desuwa
// @namespace    https://space.bilibili.com/28106105?spm_id_from=333.1007.0.0
// @version      2.3
// @description  要开开发者模式!代码爆改b站@少女乐队抹茶大芭菲,原作者@阿琴-kotori
// @author       ysl&akoto
// @match        *://www.douyu.com/*
// @grant        GM_openInTab
// @grant        GM_xmlhttpRequest
// @grant        GM_setClipboard
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_listValues
// @grant        GM_deleteValue
// @grant        GM_cookie
// @grant        GM_registerMenuCommand
// @grant        unsafeWindow
// ==/UserScript==

(function() {
    'use strict';
 // --- 配置读取 ---
const SUFFIX_CONFIG_KEY = 'desuwa_suffix_text'; // 油猴存储后缀的键名
const ENABLED_CONFIG_KEY = 'desuwa_script_enabled'; // 油猴存储启用状态的键名
let currentSuffix = GM_getValue(SUFFIX_CONFIG_KEY, 'desuwa'); // 当前后缀,默认 'desuwa'
let isScriptEnabled = GM_getValue(ENABLED_CONFIG_KEY, true); // 脚本是否启用,默认 true

// --- 常量定义 ---
const MAX_LENGTH = 66; // 弹幕最大长度
const INPUT_SELECTORS = '.inputView-1f53d9, .inputView-2a65aa'; // 标准模式和全屏模式的输入框选择器
const SEND_BUTTON_SELECTORS = '.sendDanmu-741305, .sendDanmu-592760'; // 发送按钮选择器

// --- 状态变量 ---
let lastProcessedValue = null; // 上一次脚本处理后的输入框最终值,用于避免不必要的DOM操作和事件递归
let isIMETyping = false; // 标记用户是否正在使用输入法进行组词

// --- 菜单配置 ---
function registerMenuCommands() {
    GM_registerMenuCommand(`${isScriptEnabled ? '禁用' : '启用'}脚本`, () => {
        isScriptEnabled = !isScriptEnabled;
        GM_setValue(ENABLED_CONFIG_KEY, isScriptEnabled);
        alert(`脚本已${isScriptEnabled ? '启用' : '禁用'}。\n刷新页面后生效。`);
        location.reload();
    });
    GM_registerMenuCommand(`设置后缀 (当前: ${currentSuffix})`, () => {
        const newSuffix = prompt('请输入新的弹幕后缀文本:', currentSuffix);
        if (newSuffix !== null && newSuffix.trim() !== "") {
            currentSuffix = newSuffix.trim();
            GM_setValue(SUFFIX_CONFIG_KEY, currentSuffix);
            alert('后缀已更新为: ' + currentSuffix);
        } else if (newSuffix !== null) {
            alert('后缀不能为空。');
        }
    });
}
registerMenuCommands();

if (!isScriptEnabled) {
    console.log('弹幕后缀脚本:脚本已禁用。');
    return;
}

/**
 * 核心函数:尝试规范化输入框内容,确保其以指定后缀结尾。
 * @param {HTMLInputElement|HTMLTextAreaElement} inputElement 目标输入框元素
 * @param {string} reason 调用此函数的原因 (用于调试或特定逻辑)
 */
function ensureSuffix(inputElement, reason = "unknown") {
    if (!inputElement || typeof inputElement.value === 'undefined') return;

    // 如果用户正在输入法组词,并且是由 'input' 事件触发的,则暂时不处理,等待组词结束
    if (isIMETyping && reason.startsWith("input")) {
        return;
    }

    const currentValue = inputElement.value;

    // 如果当前值与上次处理完的值相同 (通常意味着是脚本自身操作或无变化),则跳过
    if (reason.startsWith("input") && currentValue === lastProcessedValue) {
        return;
    }

    let baseText = currentValue; // 基础文本,即用户实际输入的部分
    let originalEndsWithSuffix = false; // 原始输入是否已经带有后缀

    // 检查当前值是否以设定的后缀结尾
    if (currentValue.endsWith(currentSuffix)) {
        baseText = currentValue.substring(0, currentValue.length - currentSuffix.length);
        originalEndsWithSuffix = true;
    }

    // 如果剥离后缀后,基础文本为空(用户可能删光了内容或只输入了后缀)
    if (baseText.trim().length === 0) {
        // 只有当原始值就是后缀,或者原始值本身就为空时,才清空输入框
        if (originalEndsWithSuffix || currentValue.trim().length === 0) {
            if (inputElement.value !== "") { // 仅在需要时修改 DOM
                inputElement.value = "";
            }
            lastProcessedValue = ""; // 记录处理后的状态
        } else {
            // 用户可能只输入了空格等,不是我们的后缀,保留,并记录为已处理
            lastProcessedValue = currentValue;
        }
        return;
    }

    // 计算添加后缀后的理想文本,并处理长度限制
    const availableLengthForBase = MAX_LENGTH - currentSuffix.length;
    if (availableLengthForBase < 0) {
        console.error('弹幕后缀脚本:错误,后缀本身已超出最大长度限制!');
        lastProcessedValue = currentValue; // 无法处理,记录原始值
        return;
    }
    const idealFinalText = baseText.slice(0, availableLengthForBase) + currentSuffix;

    // 只有当计算出的理想文本与当前输入框的值不同时,才更新DOM,以减少操作和光标跳动
    if (currentValue !== idealFinalText) {
        let selectionStart = -1, selectionEnd = -1;
        try { // 保存当前光标位置
            selectionStart = inputElement.selectionStart;
            selectionEnd = inputElement.selectionEnd;
        } catch(e) {/* 某些情况下获取会失败,忽略 */}

        inputElement.value = idealFinalText; // 更新输入框的值

        try { // 尝试恢复或调整光标位置
            if (selectionStart !== -1) { // 仅当成功获取光标位置时
                if (originalEndsWithSuffix && selectionStart <= baseText.length) {
                    // 如果原先有后缀,且光标在基础文本内,则尝试保持原位
                    inputElement.setSelectionRange(selectionStart, selectionEnd);
                } else {
                    // 其他情况(如新输入或光标在后缀后),将光标置于新添加的后缀之前
                    const cursorPos = idealFinalText.length - currentSuffix.length;
                    inputElement.setSelectionRange(cursorPos, cursorPos);
                }
            }
        } catch (e) { /* 设置光标失败,忽略 */ }
    }
    lastProcessedValue = inputElement.value; // 记录本次处理后的最终值
}

// --- 事件监听器 ---

// 输入法开始组词
document.addEventListener('compositionstart', function(e) {
    if (!isScriptEnabled) return;
    const target = e.target;
    if (target && target.matches(INPUT_SELECTORS)) {
        isIMETyping = true;
    }
}, true);

// 输入法结束组词
document.addEventListener('compositionend', function(e) {
    if (!isScriptEnabled) return;
    const target = e.target;
    if (target && target.matches(INPUT_SELECTORS)) {
        isIMETyping = false;
        // 组词结束后,立即处理一次输入框内容,确保后缀正确
        setTimeout(() => { // 使用setTimeout确保DOM值已更新为选中的汉字
            if (document.activeElement === target) {
                lastProcessedValue = null; // 强制重新评估和处理
                ensureSuffix(target, "compositionend_event");
            }
        }, 0);
    }
}, true);

// 实时输入监听
document.addEventListener('input', function(e) {
    if (!isScriptEnabled) return;
    const target = e.target;
    // 检查事件对象是否表明输入法正在组词 (e.isComposing 是 InputEvent 的属性)
    const eventIndicatesComposing = (e instanceof InputEvent && e.isComposing);

    if (target && target.matches(INPUT_SELECTORS)) {
        // 如果事件表明正在组词,或我们的全局标志位为true,则跳过
        if (eventIndicatesComposing || isIMETyping) {
            return;
        }
        ensureSuffix(target, "input_event");
    }
}, true);

// 输入框获得焦点时 (处理粘贴或浏览器填充等情况)
document.addEventListener('focusin', function(e) {
    if (!isScriptEnabled) return;
    const target = e.target;
    if (target && target.matches(INPUT_SELECTORS)) {
        isIMETyping = false; // 焦点切换,认为组词已结束
        setTimeout(() => {
             if (document.activeElement === target) {
                lastProcessedValue = null; // 强制重新评估
                ensureSuffix(target, "focusin_event");
             }
        }, 50); // 稍作延迟,确保值已稳定
    }
}, true);

// 点击发送按钮前 (用mousedown尝试更早介入)
document.addEventListener('mousedown', function(e) {
    if (!isScriptEnabled) return;
    const sendButton = e.target.closest(SEND_BUTTON_SELECTORS);
    if (sendButton) {
        const inputElement = document.querySelector(INPUT_SELECTORS); // 通常只有一个相关输入框
        if (inputElement) {
             isIMETyping = false; // 点击按钮,认为组词已结束
             lastProcessedValue = null; // 强制重新评估
             ensureSuffix(inputElement, "mousedown_on_send_button");
        }
    }
}, true);

// 按下回车键时
document.addEventListener('keydown', function(e) {
    if (!isScriptEnabled) return;
    // 确保不是在输入法组词过程中按回车 (e.isComposing 是 KeyboardEvent 的属性)
    if (e.key !== 'Enter' || (e instanceof KeyboardEvent && e.isComposing)) {
        return;
    }
    const activeElement = document.activeElement;
    if (activeElement && activeElement.matches(INPUT_SELECTORS)) {
        isIMETyping = false; // 按回车(非组词),认为组词已结束
        lastProcessedValue = null; // 强制重新评估
        ensureSuffix(activeElement, "enter_keydown");
        // 注意:此处不阻止默认行为 (e.preventDefault()),让斗鱼自行处理回车发送
    }
}, true);
    // 监听发送按钮点击事件
    document.addEventListener('click', function(e) {
        if (e.target.closest('.ChatSend-button')) {
            processMessage();
        }
    });

    // 监听回车键发送
    document.addEventListener('keydown', function(e) {
        if (e.key === 'Enter' && document.activeElement.classList.contains('ChatSend-txt')) {
            processMessage();
        }
    });

    function processMessage() {
        const textarea = document.querySelector('.ChatSend-txt');
        if (!textarea) return;

        // 添加后缀并处理长度限制
        const maxLength = 66;
        const suffix = currentSuffix;
        const baseText = textarea.value.replace(/desuwa$/, ''); // 避免重复添加

        // 计算可用长度
        const availableLength = maxLength - suffix.length;
        const finalText = baseText.slice(0, availableLength) + suffix;

        // 更新输入框内容
        textarea.value = finalText;

        // 触发输入事件(部分网站需要)
        const event = new Event('input', { bubbles: true });
        textarea.dispatchEvent(event);
    }

console.log('弹幕后缀脚本:已加载并设置监听器。脚本当前状态:', isScriptEnabled ? '启用' : '禁用', ', 后缀:', currentSuffix);
})();