// ==UserScript== // @name Bilibili独轮车 // @description Bilibili独轮车, 按照指定间隔发布弹幕 // @version 1.0.0 // @author Yiero // @match https://live.bilibili.com/* // @tag bilibili // @tag live // @tag speak // @license GPL-3 // @namespace https://github.com/AliubYiero/Yiero_WebScripts // @noframes // @grant GM_unregisterMenuCommand // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_addValueChangeListener // @grant GM_removeValueChangeListener // ==/UserScript== (function() { "use strict"; /* * @module : @yiero/gmlib * @author : Yiero * @version : 0.1.23 * @description : GM Lib for Tampermonkey * @keywords : tampermonkey, lib, scriptcat, utils * @license : MIT * @repository : git+https://github.com/AliubYiero/GmLib.git */ var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); const _gmMenuCommand = class _gmMenuCommand2 { constructor() { } /** * 获取一个菜单按钮 */ static get(title) { const commandButton = this.list.find((commandButton2) => commandButton2.title === title); if (!commandButton) { throw new Error("\u83DC\u5355\u6309\u94AE\u4E0D\u5B58\u5728"); } return commandButton; } /** * 创建一个带有状态的菜单按钮 */ static createToggle(details) { this.create(details.active.title, () => { this.toggleActive(details.active.title); this.toggleActive(details.inactive.title); details.active.onClick(); this.render(); }, true).create(details.inactive.title, () => { this.toggleActive(details.active.title); this.toggleActive(details.inactive.title); details.inactive.onClick(); this.render(); }, false); return _gmMenuCommand2; } /** * 手动激活一个菜单按钮 */ static click(title) { const commandButton = this.get(title); commandButton.onClick(); return _gmMenuCommand2; } /** * 创建一个菜单按钮 */ static create(title, onClick, isActive = true) { if (this.list.some((commandButton) => commandButton.title === title)) { throw new Error("\u83DC\u5355\u6309\u94AE\u5DF2\u5B58\u5728"); } this.list.push({ title, onClick, isActive, id: 0 }); return _gmMenuCommand2; } /** * 删除一个菜单按钮 */ static remove(title) { this.list = this.list.filter((commandButton) => commandButton.title !== title); return _gmMenuCommand2; } /** * 修改两个菜单按钮的顺序 */ static swap(title1, title2) { const index1 = this.list.findIndex((commandButton) => commandButton.title === title1); const index2 = this.list.findIndex((commandButton) => commandButton.title === title2); if (index1 === -1 || index2 === -1) { throw new Error("\u83DC\u5355\u6309\u94AE\u4E0D\u5B58\u5728"); } [this.list[index1], this.list[index2]] = [this.list[index2], this.list[index1]]; return _gmMenuCommand2; } /** * 修改一个菜单按钮 */ static modify(title, details) { const commandButton = this.get(title); details.onClick && (commandButton.onClick = details.onClick); details.isActive && (commandButton.isActive = details.isActive); return _gmMenuCommand2; } /** * 切换菜单按钮激活状态 */ static toggleActive(title) { const commandButton = this.get(title); commandButton.isActive = !commandButton.isActive; return _gmMenuCommand2; } /** * 渲染所有激活的菜单按钮 */ static render() { this.list.forEach((commandButton) => { GM_unregisterMenuCommand(commandButton.id); if (commandButton.isActive) { commandButton.id = GM_registerMenuCommand(commandButton.title, commandButton.onClick); } }); } }; __publicField(_gmMenuCommand, "list", []); let gmMenuCommand = _gmMenuCommand; class GmStorage { constructor(key, defaultValue) { __publicField(this, "listenerId", 0); this.key = key; this.defaultValue = defaultValue; this.key = key; this.defaultValue = defaultValue; } /** * 获取当前存储的值 * * @alias get() */ get value() { return this.get(); } /** * 获取当前存储的值 */ get() { return GM_getValue(this.key, this.defaultValue); } /** * 给当前存储设置一个新值 */ set(value) { return GM_setValue(this.key, value); } /** * 移除当前键 */ remove() { GM_deleteValue(this.key); } /** * 监听元素更新, 同时只能存在 1 个监听器 */ updateListener(callback) { this.removeListener(); this.listenerId = GM_addValueChangeListener(this.key, (key, oldValue, newValue, remote) => { callback({ key, oldValue, newValue, remote }); }); } /** * 移除元素更新回调 */ removeListener() { GM_removeValueChangeListener(this.listenerId); } } class UnicycleConfigDialog { constructor(options = {}) { this.host = null; this.shadow = null; this.isOpen = false; this.options = { defaultText: "", defaultRepeatMin: 1, defaultRepeatMax: 1, defaultMin: 500, defaultMax: 1e3, onSave: () => { }, onCancel: () => { }, ...options }; this.init(); } init() { this.host = document.createElement("div"); this.host.id = "unicycle-dialog-host"; this.host.style.all = "initial"; this.host.style.display = "contents"; document.body.appendChild(this.host); this.shadow = this.host.attachShadow({ mode: "open" }); this.render(); this.bindEvents(); } render() { if (!this.shadow) return; this.shadow.innerHTML = `

\u72EC\u8F6E\u8F66\u914D\u7F6E

0/40
~ \u6B21
~ ms
`; } /** * 显示错误信息并标红输入框 */ showError(input, errorEl, message) { if (input) input.classList.add("error"); if (errorEl) { errorEl.textContent = message; errorEl.classList.add("visible"); } } /** * 清除单个输入框的错误状态 */ clearError(input, errorEl) { if (input) input.classList.remove("error"); if (errorEl) { errorEl.textContent = ""; errorEl.classList.remove("visible"); } } /** * 清除所有错误状态 */ clearAllErrors() { if (!this.shadow) return; const inputs = this.shadow.querySelectorAll(".input"); const errors = this.shadow.querySelectorAll(".error-message"); inputs.forEach((input) => input.classList.remove("error")); errors.forEach((error) => { error.textContent = ""; error.classList.remove("visible"); }); } bindEvents() { if (!this.shadow) return; const btnClose = this.shadow.getElementById("btn-close"); const btnCancel = this.shadow.getElementById("btn-cancel"); const btnSave = this.shadow.getElementById("btn-save"); const textInput = this.shadow.getElementById("config-text"); const charCountDisplay = this.shadow.getElementById("char-count"); const repeatMinInput = this.shadow.getElementById("config-repeat-min"); const repeatMaxInput = this.shadow.getElementById("config-repeat-max"); const minInput = this.shadow.getElementById("config-min"); const maxInput = this.shadow.getElementById("config-max"); const textError = this.shadow.getElementById("text-error"); const repeatError = this.shadow.getElementById("repeat-error"); const intervalError = this.shadow.getElementById("interval-error"); const updateCharCount = () => { if (charCountDisplay && textInput) { charCountDisplay.textContent = `${textInput.value.length}/40`; } }; textInput == null ? void 0 : textInput.addEventListener("input", () => { updateCharCount(); this.clearError(textInput, textError); }); repeatMinInput == null ? void 0 : repeatMinInput.addEventListener("input", () => this.clearError(repeatMinInput, repeatError)); repeatMaxInput == null ? void 0 : repeatMaxInput.addEventListener("input", () => this.clearError(repeatMaxInput, repeatError)); minInput == null ? void 0 : minInput.addEventListener("input", () => this.clearError(minInput, intervalError)); maxInput == null ? void 0 : maxInput.addEventListener("input", () => this.clearError(maxInput, intervalError)); const handleClose = () => { this.options.onCancel(); this.close(); }; btnClose == null ? void 0 : btnClose.addEventListener("click", handleClose); btnCancel == null ? void 0 : btnCancel.addEventListener("click", handleClose); btnSave == null ? void 0 : btnSave.addEventListener("click", () => { this.clearAllErrors(); const text = (textInput == null ? void 0 : textInput.value.trim()) || ""; const repeatMin = parseInt((repeatMinInput == null ? void 0 : repeatMinInput.value) || "1"); const repeatMax = parseInt((repeatMaxInput == null ? void 0 : repeatMaxInput.value) || "1"); const minVal = parseInt((minInput == null ? void 0 : minInput.value) || "0"); const maxVal = parseInt((maxInput == null ? void 0 : maxInput.value) || "0"); let hasError = false; let firstErrorInput = null; if (!text) { this.showError(textInput, textError, "\u6587\u672C\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A"); hasError = true; firstErrorInput = firstErrorInput || textInput; } if (!hasError && (isNaN(repeatMin) || repeatMin < 1)) { this.showError(repeatMinInput, repeatError, "\u6700\u5C0F\u91CD\u590D\u6B21\u6570\u4E0D\u80FD\u5C0F\u4E8E 1"); hasError = true; firstErrorInput = firstErrorInput || repeatMinInput; } if (!hasError && (isNaN(repeatMax) || repeatMax < repeatMin)) { this.showError(repeatMaxInput, repeatError, "\u6700\u5927\u91CD\u590D\u6B21\u6570\u4E0D\u80FD\u5C0F\u4E8E\u6700\u5C0F\u503C"); hasError = true; firstErrorInput = firstErrorInput || repeatMaxInput; } if (!hasError && (isNaN(minVal) || minVal < 500)) { this.showError(minInput, intervalError, "\u6700\u5C0F\u95F4\u9694\u4E0D\u80FD\u5C0F\u4E8E 500ms"); hasError = true; firstErrorInput = firstErrorInput || minInput; } if (!hasError && (isNaN(maxVal) || maxVal < minVal)) { this.showError(maxInput, intervalError, "\u6700\u5927\u95F4\u9694\u4E0D\u80FD\u5C0F\u4E8E\u6700\u5C0F\u503C"); hasError = true; firstErrorInput = firstErrorInput || maxInput; } if (hasError) { firstErrorInput == null ? void 0 : firstErrorInput.focus(); return; } const configData = { text, repeatCount: { min: repeatMin, max: repeatMax }, interval: { min: minVal, max: maxVal } }; this.options.onSave(configData); this.close(); }); if (repeatMinInput) repeatMinInput.value = String(this.options.defaultRepeatMin); if (repeatMaxInput) repeatMaxInput.value = String(this.options.defaultRepeatMax); if (minInput) minInput.value = String(this.options.defaultMin); if (maxInput) maxInput.value = String(this.options.defaultMax); if (textInput) { textInput.value = this.options.defaultText; updateCharCount(); } } open() { if (!this.shadow || this.isOpen) return; const container = this.shadow.getElementById("dialog-container"); container == null ? void 0 : container.classList.add("open"); this.isOpen = true; this.resetForm(); this.clearAllErrors(); setTimeout(() => { var _a; const textInput = (_a = this.shadow) == null ? void 0 : _a.getElementById("config-text"); textInput == null ? void 0 : textInput.focus(); }, 100); } close() { if (!this.shadow) return; const container = this.shadow.getElementById("dialog-container"); container == null ? void 0 : container.classList.remove("open"); this.isOpen = false; } resetForm() { if (!this.shadow) return; const textInput = this.shadow.getElementById("config-text"); const repeatMinInput = this.shadow.getElementById("config-repeat-min"); const repeatMaxInput = this.shadow.getElementById("config-repeat-max"); const minInput = this.shadow.getElementById("config-min"); const maxInput = this.shadow.getElementById("config-max"); const charCountDisplay = this.shadow.getElementById("char-count"); if (textInput) textInput.value = this.options.defaultText; if (repeatMinInput) repeatMinInput.value = String(this.options.defaultRepeatMin); if (repeatMaxInput) repeatMaxInput.value = String(this.options.defaultRepeatMax); if (minInput) minInput.value = String(this.options.defaultMin); if (maxInput) maxInput.value = String(this.options.defaultMax); if (charCountDisplay && textInput) { charCountDisplay.textContent = `${textInput.value.length}/40`; } } updateOptions(options) { this.options = { ...this.options, ...options }; } destroy() { var _a; this.close(); if ((_a = this.host) == null ? void 0 : _a.parentNode) { this.host.parentNode.removeChild(this.host); } this.host = null; this.shadow = null; } get isDialogOpen() { return this.isOpen; } } const configStore = new GmStorage("config", { text: "", interval: { min: 5e3, max: 6e3 }, repeatCount: { min: 1, max: 3 } }); function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } class Danmaku { constructor() { this.chatInput = null; this.submitButton = null; this.focusEvent = new Event("focus"); this.inputEvent = new Event("input"); this.changeEvent = new Event("change"); this.blurEvent = new Event("blur"); } init() { this.chatInput = document.querySelector("textarea.chat-input.border-box"); this.submitButton = document.querySelector(".bl-button.live-skin-highlight-button-bg.live-skin-button-text"); } send(content) { if (!(this.chatInput && this.submitButton)) { this.init(); return; } this.chatInput.dispatchEvent(this.focusEvent); this.chatInput.value = content; this.chatInput.dispatchEvent(this.inputEvent); this.chatInput.dispatchEvent(this.changeEvent); this.chatInput.dispatchEvent(this.blurEvent); this.submitButton.click(); } } const danmaku = new Danmaku(); const main = async () => { const config = configStore.get(); const dialog = new UnicycleConfigDialog({ defaultText: config.text, defaultMin: config.interval.min, defaultMax: config.interval.max, defaultRepeatMin: config.repeatCount.min, defaultRepeatMax: config.repeatCount.max, onSave: (config2) => { configStore.set(config2); dialog.updateOptions({ defaultText: config2.text, defaultMin: config2.interval.min, defaultMax: config2.interval.max, defaultRepeatMin: config2.repeatCount.min, defaultRepeatMax: config2.repeatCount.max }); }, onCancel: () => { } }); danmaku.init(); let timer = 0; const sendDanmaku = (config2, maxContentLength = 40) => { danmaku.send(config2.text.repeat(getRandomInt(config2.repeatCount.min, config2.repeatCount.max)).slice(0, maxContentLength)); const nextSendTime = getRandomInt(config2.interval.min, config2.interval.max); timer = window.setTimeout( () => sendDanmaku(config2, maxContentLength), nextSendTime ); }; gmMenuCommand.createToggle({ active: { title: "\u5F00\u542F\u72EC\u8F6E\u8F66", onClick() { const config2 = configStore.get(); let maxContentLength = 40; const contentLengthElement = document.querySelector(".input-limit-hint.p-absolute"); if (contentLengthElement) { const matches = contentLengthElement.innerText.match(new RegExp("(?<=\\/)\\d+")); if (Array.isArray(matches) && matches[0]) { maxContentLength = Number(matches[0]); } } timer = window.setTimeout( () => sendDanmaku(config2, maxContentLength), getRandomInt(config2.interval.min, config2.interval.max) ); } }, inactive: { title: "\u5173\u95ED\u72EC\u8F6E\u8F66", onClick() { clearTimeout(timer); } } }).create("\u72EC\u8F6E\u8F66\u914D\u7F6E", () => { dialog.open(); }).render(); }; main().catch((error) => { console.error(error); }); })();