// ==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);
})();