// ==UserScript== // @name Bilibili 直播弹幕发送时间显示 // @name:en Bilibili live danmaku timestamp display // @description 显示直播弹幕的发送时间 // @version 0.2.3 // @author Yiero // @match https://live.bilibili.com/* // @run-at document-body // @license GPL-3 // @namespace https://github.com/AliubYiero/TamperMonkeyScripts // @grant GM_addStyle // @grant GM_addElement // ==/UserScript== function elementWaiter(selector, config = {}) { const { parent = document.body, timeoutPerSecond = 20, delayPerSecond = 0.5 } = config; return new Promise((resolve, reject) => { const returnElement = (selector2) => { setTimeout(() => { const element2 = document.querySelector(selector2); if (!element2) { reject(new Error("Void Element")); return; } window.dispatchEvent(new CustomEvent("ElementUpdate", { detail: element2 })); resolve(element2); }, delayPerSecond * 1e3); }; const element = document.querySelector(selector); if (element) { returnElement(selector); return; } if (MutationObserver) { const timer2 = timeoutPerSecond && window.setTimeout(() => { observer.disconnect(); returnElement(selector); }, timeoutPerSecond * 1e3); const observeElementCallback = (mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((addNode) => { if (addNode.nodeType !== Node.ELEMENT_NODE) { return; } const addedElement = addNode; const element2 = addedElement.matches(selector) ? addedElement : addedElement.querySelector(selector); if (element2) { timer2 && clearTimeout(timer2); returnElement(selector); } }); }); }; const observer = new MutationObserver(observeElementCallback); observer.observe(parent, { subtree: true, childList: true }); return; } const intervalDelay = 100; let intervalCounter = 0; const maxIntervalCounter = Math.ceil(timeoutPerSecond * 1e3 / intervalDelay); const timer = window.setInterval(() => { if (++intervalCounter > maxIntervalCounter) { clearInterval(timer); returnElement(selector); return; } const element2 = parent.querySelector(selector); if (element2) { clearInterval(timer); returnElement(selector); } }, intervalDelay); }); } const handleDanmakuDisplay = async (callback) => { const observer = new MutationObserver((records) => { for (const record of records) { for (let addedNode of record.addedNodes) { if (addedNode.nodeType !== Node.ELEMENT_NODE) { continue; } const element = addedNode; callback(element); } } }); const container = await elementWaiter("#chat-items"); observer.observe(container, { childList: true }); }; const danmakuTimeStyle = `/* \u8BA9\u5F39\u5E55\u65F6\u957F\u5C45\u53F3 */ .danmaku-item-right { display: flex !important; align-items: flex-end; } /* \u5F39\u5E55\u65F6\u957F */ .danmaku-time { margin-left: auto; font-size: 10px; color: #9499a0; } /* \u53D1\u9001\u5F39\u5E55\u65F6\u7684\u73B0\u5B9E\u65F6\u957F */ .danmaku-time-real { display: inherit !important; } /* \u53D1\u9001\u5F39\u5E55\u65F6\u7684\u76F4\u64AD\u65F6\u957F */ .danmaku-time-live { display: none !important; } .danmaku-item:hover { & .danmaku-time { color: #00aeec; } & .danmaku-time-real { display: none !important; } & .danmaku-time-live { display: inherit !important; } } `; const addDanmakuTimeStyle = () => { GM_addStyle(danmakuTimeStyle); }; (() => { addDanmakuTimeStyle(); handleDanmakuDisplay((element) => { const { timestamp, ts } = element.dataset; if (!timestamp) { return; } const danmakuTextNode = element.querySelector(".danmaku-item-right"); if (!danmakuTextNode) { return; } const sendTime = ts === "0" ? Number(timestamp) : Number(timestamp) * 1e3; const sendTimeStr = new Date(sendTime).toLocaleTimeString(); GM_addElement(danmakuTextNode, "span", { textContent: sendTimeStr, class: "danmaku-time danmaku-time-real" }); const liveTime = sendTime - __NEPTUNE_IS_MY_WAIFU__.roomInitRes.data.live_time * 1e3; const liveTimeDate = new Date(liveTime); const padStart = (num) => String(num).padStart(2, "0"); const liveTimeStr = `${padStart(liveTimeDate.getUTCHours())}:${padStart(liveTimeDate.getUTCMinutes())}:${padStart(liveTimeDate.getUTCSeconds())}`; GM_addElement(danmakuTextNode, "span", { textContent: liveTimeStr, class: "danmaku-time danmaku-time-live" }); }); })();