// ==UserScript== // @name DouyuEx-RL 斗鱼直播间增强插件 (Reborn Lite) // @namespace https://github.com/xayahcore/DouyuEx-RL // @homepageURL https://github.com/xayahcore/DouyuEx-RL // @supportURL https://github.com/xayahcore/DouyuEx-RL/issues // @icon data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTA4cHgiIGhlaWdodD0iMTA4cHgiIHZpZXdCb3g9IjAgMCAxMDggMTA4IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgogICAgPHRpdGxlPueyvueBteeQgzwvdGl0bGU+CiAgICA8ZyBpZD0i6aG16Z2iLTEiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnIGlkPSLnsr7ngbXnkIMiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAuODMwNzY5LCAwLjgzMDc2OSkiIGZpbGwtcnVsZT0ibm9uemVybyI+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik01My4xNjkyMzA3LDEwNi4zMzg0NjEgQzIzLjgyNzY5MjIsMTA2LjMzODQ2MSAwLDgyLjUxMDc2OTIgMCw1My4xNjkyMzA3IEMwLDUxLjAwMzA3NjkgMS43NzIzMDc3NSw0OS4yMzA3NjkyIDMuOTM4NDYxNSw0OS4yMzA3NjkyIEwzMy40NzY5MjMsNDkuMjMwNzY5MiBDMzUuNjQzMDc2OSw0OS4yMzA3NjkyIDM3LjQxNTM4NDUsNTEuMDAzMDc3IDM3LjQxNTM4NDYsNTMuMTY5MjMwNyBDMzcuNDE1Mzg0Niw2MS44MzM4NDYxIDQ0LjUwNDYxNTQsNjguOTIzMDc2OSA1My4xNjkyMzA3LDY4LjkyMzA3NjkgQzYxLjgzMzg0NjEsNjguOTIzMDc2OSA2OC45MjMwNzY5LDYxLjgzMzg0NjEgNjguOTIzMDc2OSw1My4xNjkyMzA3IEM2OC45MjMwNzY5LDUxLjAwMzA3NjkgNzAuNjk1Mzg0Niw0OS4yMzA3NjkyIDcyLjg2MTUzODUsNDkuMjMwNzY5MiBMMTAyLjQsNDkuMjMwNzY5MiBDMTA0LjU2NjE1NCw0OS4yMzA3NjkyIDEwNi4zMzg0NjEsNTEuMDAzMDc3IDEwNi4zMzg0NjEsNTMuMTY5MjMwNyBDMTA2LjMzODQ2MSw4Mi41MTA3NjkyIDgyLjUxMDc2OTIsMTA2LjMzODQ2MSA1My4xNjkyMzA3LDEwNi4zMzg0NjEgWiIgaWQ9Iui3r+W+hCIgZmlsbD0iIzMzMzYzQSI+PC9wYXRoPgogICAgICAgICAgICA8cGF0aCBkPSJNOC4wNzM4NDYxMiw1Ny4xMDc2OTIyIEMxMC4wNDMwNzY5LDgwLjI0NjE1MzcgMjkuNTM4NDYxNSw5OC40NjE1Mzg1IDUzLjE2OTIzMDcsOTguNDYxNTM4NSBDNzYuOCw5OC40NjE1Mzg1IDk2LjI5NTM4NDYsODAuMjQ2MTUzOSA5OC4yNjQ2MTU0LDU3LjEwNzY5MjIgTDc2LjUwNDYxNTQsNTcuMTA3NjkyMiBDNzQuNjMzODQ2MSw2OC4yMzM4NDYxIDY0Ljg4NjE1MzksNzYuOCA1My4xNjkyMzA3LDc2LjggQzQxLjQ1MjMwNzYsNzYuOCAzMS43MDQ2MTU0LDY4LjIzMzg0NjEgMjkuODMzODQ2MSw1Ny4xMDc2OTIyIEw4LjA3Mzg0NjEyLDU3LjEwNzY5MjIgWiIgaWQ9Iui3r+W+hCIgZmlsbD0iI0ZGRkZGRiI+PC9wYXRoPgogICAgICAgICAgICA8cGF0aCBkPSJNNTMuMTY5MjMwOCwzLjkzODQ2MTUgQzI1Ljk5Mzg0NjEsMy45Mzg0NjE1IDMuOTM4NDYxNSwyNS45OTM4NDYxIDMuOTM4NDYxNSw1My4xNjkyMzA3IEwzMy40NzY5MjMsNTMuMTY5MjMwNyBDMzMuNDc2OTIzLDQyLjMzODQ2MTUgNDIuMzM4NDYxNSwzMy40NzY5MjMgNTMuMTY5MjMwOCwzMy40NzY5MjMgQzY0LDMzLjQ3NjkyMyA3Mi44NjE1Mzg1LDQyLjMzODQ2MTUgNzIuODYxNTM4NSw1My4xNjkyMzA3IEwxMDIuNCw1My4xNjkyMzA3IEMxMDIuNCwyNS45OTM4NDYxIDgwLjM0NDYxNTQsMy45Mzg0NjE1IDUzLjE2OTIzMDgsMy45Mzg0NjE1IFoiIGlkPSLot6/lvoQiIGZpbGw9IiNENjA5MDkiPjwvcGF0aD4KICAgICAgICAgICAgPHBhdGggZD0iTTEwMi40LDU3LjEwNzY5MjIgTDcyLjg2MTUzODUsNTcuMTA3NjkyMiBDNzAuNjk1Mzg0Niw1Ny4xMDc2OTIyIDY4LjkyMzA3Nyw1NS4zMzUzODQ1IDY4LjkyMzA3NjksNTMuMTY5MjMwNyBDNjguOTIzMDc2OSw0NC41MDQ2MTU0IDYxLjgzMzg0NjEsMzcuNDE1Mzg0NiA1My4xNjkyMzA3LDM3LjQxNTM4NDYgQzQ0LjUwNDYxNTQsMzcuNDE1Mzg0NiAzNy40MTUzODQ2LDQ0LjUwNDYxNTQgMzcuNDE1Mzg0Niw1My4xNjkyMzA3IEMzNy40MTUzODQ2LDU1LjMzNTM4NDYgMzUuNjQzMDc2OSw1Ny4xMDc2OTIyIDMzLjQ3NjkyMyw1Ny4xMDc2OTIyIEwzLjkzODQ2MTUsNTcuMTA3NjkyMiBDMS43NzIzMDc2Miw1Ny4xMDc2OTIyIDAsNTUuMzM1Mzg0NSAwLDUzLjE2OTIzMDcgQzAsMjMuODI3NjkyMiAyMy44Mjc2OTIzLDAgNTMuMTY5MjMwNywwIEM4Mi41MTA3NjkyLDAgMTA2LjMzODQ2MSwyMy44Mjc2OTIyIDEwNi4zMzg0NjEsNTMuMTY5MjMwNyBDMTA2LjMzODQ2MSw1NS4zMzUzODQ2IDEwNC41NjYxNTQsNTcuMTA3NjkyMiAxMDIuNCw1Ny4xMDc2OTIyIFoiIGlkPSLot6/lvoQiIGZpbGw9IiMzMzM2M0EiPjwvcGF0aD4KICAgICAgICAgICAgPHBhdGggZD0iTTc2LjUwNDYxNTQsNDkuMjMwNzY5MyBMOTguMzYzMDc2OSw0OS4yMzA3NjkzIEM5Ni4yOTUzODQ2LDI2LjA5MjMwNzYgNzYuOCw3Ljg3NjkyMyA1My4xNjkyMzA3LDcuODc2OTIzIEMyOS41Mzg0NjE1LDcuODc2OTIzIDEwLjA0MzA3NjksMjYuMDkyMzA3NiA4LjA3Mzg0NjEyLDQ5LjIzMDc2OTMgTDI5LjkzMjMwNzYsNDkuMjMwNzY5MyBDMzEuNzA0NjE1NCwzOC4xMDQ2MTU0IDQxLjQ1MjMwNzYsMjkuNTM4NDYxNSA1My4xNjkyMzA3LDI5LjUzODQ2MTUgQzY0Ljg4NjE1MzksMjkuNTM4NDYxNSA3NC42MzM4NDYxLDM4LjEwNDYxNTQgNzYuNTA0NjE1NCw0OS4yMzA3NjkzIEw3Ni41MDQ2MTU0LDQ5LjIzMDc2OTMgWiIgaWQ9Iui3r+W+hCIgZmlsbD0iI0Q2MDkwOSI+PC9wYXRoPgogICAgICAgICAgICA8cGF0aCBkPSJNNTMuMTY5MjMwNyw3Ni44IEM0MC4xNzIzMDc2LDc2LjggMjkuNTM4NDYxNSw2Ni4xNjYxNTM5IDI5LjUzODQ2MTUsNTMuMTY5MjMwNyBDMjkuNTM4NDYxNSw0MC4xNzIzMDc2IDQwLjE3MjMwNzYsMjkuNTM4NDYxNSA1My4xNjkyMzA3LDI5LjUzODQ2MTUgQzY2LjE2NjE1MzksMjkuNTM4NDYxNSA3Ni44LDQwLjE3MjMwNzYgNzYuOCw1My4xNjkyMzA3IEM3Ni44LDY2LjE2NjE1MzkgNjYuMTY2MTUzOSw3Ni44IDUzLjE2OTIzMDcsNzYuOCBaIiBpZD0i6Lev5b6EIiBmaWxsPSIjMzMzNjNBIj48L3BhdGg+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik01My4xNjkyMzA3LDM3LjQxNTM4NDYgQzQ0LjUwNDYxNTQsMzcuNDE1Mzg0NiAzNy40MTUzODQ2LDQ0LjUwNDYxNTQgMzcuNDE1Mzg0Niw1My4xNjkyMzA3IEMzNy40MTUzODQ2LDYxLjgzMzg0NjEgNDQuNTA0NjE1NCw2OC45MjMwNzY5IDUzLjE2OTIzMDcsNjguOTIzMDc2OSBDNjEuODMzODQ2MSw2OC45MjMwNzY5IDY4LjkyMzA3NjksNjEuODMzODQ2MSA2OC45MjMwNzY5LDUzLjE2OTIzMDcgQzY4LjkyMzA3NjksNDQuNTA0NjE1NCA2MS44MzM4NDYxLDM3LjQxNTM4NDYgNTMuMTY5MjMwNywzNy40MTUzODQ2IEw1My4xNjkyMzA3LDM3LjQxNTM4NDYgWiIgaWQ9Iui3r+W+hCIgZmlsbD0iI0ZGRkZGRiI+PC9wYXRoPgogICAgICAgICAgICA8cGF0aCBkPSJNNDMuMzIzMDc2OSw1My4xNjkyMzA3IEM0My4zMjMwNzY5LDU4LjYwNzExMTQgNDcuNzMxMzUwMSw2My4wMTUzODQ2IDUzLjE2OTIzMDcsNjMuMDE1Mzg0NiBDNTguNjA3MTExNCw2My4wMTUzODQ2IDYzLjAxNTM4NDYsNTguNjA3MTExNCA2My4wMTUzODQ2LDUzLjE2OTIzMDcgQzYzLjAxNTM4NDYsNDcuNzMxMzUwMSA1OC42MDcxMTE0LDQzLjMyMzA3NjkgNTMuMTY5MjMwNyw0My4zMjMwNzY5IEM0Ny43MzEzNTAxLDQzLjMyMzA3NjkgNDMuMzIzMDc2OSw0Ny43MzEzNTAxIDQzLjMyMzA3NjksNTMuMTY5MjMwNyBaIiBpZD0i6Lev5b6EIiBmaWxsPSIjMzMzNjNBIj48L3BhdGg+CiAgICAgICAgPC9nPgogICAgPC9nPgo8L3N2Zz4= // @version 2026.11.14.01 // @description DouyuEx-RL 纯净重构版:源头全链路无缝最高画质(彻底告别开播二次切流卡顿)/ 活跃榜贡献值原生极速注入 / 物理单例长连接多路复用 / 优雅阻断P2P上传 / 彻底去除弹窗广告 / 剥离历史冗余死重 // @author xayahcore // @match *://*.douyu.com/0* // @match *://*.douyu.com/1* // @match *://*.douyu.com/2* // @match *://*.douyu.com/3* // @match *://*.douyu.com/4* // @match *://*.douyu.com/5* // @match *://*.douyu.com/6* // @match *://*.douyu.com/7* // @match *://*.douyu.com/8* // @match *://*.douyu.com/9* // @match *://*.douyu.com/beta/* // @match *://*.douyu.com/topic/* // @match *://www.douyu.com/member/cp/getFansBadgeList // @match *://passport.douyu.com/* // @match *://msg.douyu.com/* // @match *://yuba.douyu.com/* // @match *://v.douyu.com/* // @match *://cz.douyu.com/* // @require https://registry.npmmirror.com/flv.js/1.6.2/files/dist/flv.min.js // @require https://fastly.jsdelivr.net/npm/svgaplayerweb@2.3.1/build/svga.min.js // @require https://registry.npmmirror.com/gif.js/0.2.0/files/dist/gif.js // @require https://registry.npmmirror.com/three/0.80.0/files/build/three.min.js // @require https://registry.npmmirror.com/xlsx/0.16.4/files/dist/xlsx.full.min.js // @require https://registry.npmmirror.com/dompurify/2.3.6/files/dist/purify.min.js // @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 // @run-at document-start // @connect douyucdn.cn // @connect douyu.com // @connect qq.com // @connect douyuex.com // @connect dy.roozj.com // @connect bilibili.com // @connect huya.com // @connect shadiao.app // @connect registry.npmmirror.com // @connect fastly.jsdelivr.net // @connect cdn.jsdelivr.net // @connect raw.githubusercontent.com // @connect update.greasyfork.org // @connect greasyfork.org // @connect scriptcat.org // @license MIT // ==/UserScript== function init() { initPkg_Safe("PopupPlayer_MergeDanmaku_ScriptHook", initPkg_PopupPlayer_MergeDanmaku_ScriptHook), initPkg_Safe("Shield_RemoveRepeatedDanmaku_ScriptHook", initPkg_Shield_RemoveRepeatedDanmaku_ScriptHook), initPkg_Safe("ShowDanmaku_ScriptHook", initPkg_ShowDanmaku_ScriptHook), initPkg_Safe("ScriptHook", initScriptHook), initPkg_Safe("ResponseHook", initResponseHook), initPkg_Safe("ShowDanmaku", initPkg_ShowDanmaku), initPkg_Safe("Update_checkVersion", Update_checkVersion), initPkg_Safe("KillP2P", initKillP2P), initPkg_Safe("HighestVideoQuality", initHighestVideoQuality), initPkg_Safe("NoLogin", initPkg_NoLogin), initPkg_Safe("Console", initPkg_Console), initPkg_Safe("Menu", initPkg_Menu), initPkg_Safe("Reset", initPkg_Reset), initPkg_Safe("FollowList", initPkg_FollowList); } function initPkg() { initPkg_Safe("DanmakuTail", initPkg_DanmakuTail), initPkg_Safe("ExIcon", initPkg_ExIcon), initPkg_Safe("ExPanel", initPkg_ExPanel), initPkg_Safe("RealAudience", initPkg_RealAudience), initPkg_Safe("CopyRealLive", initPkg_CopyRealLive), initPkg_Safe("TitleTime", initPkg_TitleTime), initPkg_Safe("RoomRevenue", initPkg_RoomRevenue), initPkg_Safe("RemoveAD", removeAD), initPkg_Safe("RemoveAD_Optimize", initPkg_RemoveAD), initPkg_Safe("FullScreen", initFullScreen), initPkg_Safe("Shield", initPkg_Shield), initPkg_Safe("BagInfo", initPkg_BagInfo), initPkg_Safe("Update", initPkg_Update), initPkg_Safe("Monitor", initPkg_Monitor), initPkg_Safe("Lottery", initPkg_Lottery), initPkg_Safe("PopupPlayer", initPkg_PopupPlayer), initPkg_Safe("PopupPlayer_MultiScreen", initPkg_PopupPlayer_MultiScreen), initPkg_Safe("PopupPlayer_MergeDanmaku", initPkg_PopupPlayer_MergeDanmaku), initPkg_Safe("LiveTool", initPkg_LiveTool), initPkg_Safe("VideoTools", initPkg_VideoTools), initPkg_Safe("ExpandTool", initPkg_ExpandTool), initPkg_Safe("Refresh", initPkg_Refresh), initPkg_Safe("BarrageLoop", initPkg_BarrageLoop), initPkg_Safe("FansContinue", initPkg_FansContinue), initPkg_Safe("Sign", initPkg_Sign), initPkg_Safe("BarragePanel", initPkg_BarragePanel), initPkg_Safe("AccountList", initPkg_AccountList), initPkg_Safe("ChatTools", initPkg_ChatTools), initPkg_Safe("MonthCost", initPkg_MonthCost), initPkg_Safe("RoomVip", initPkg_RoomVip), initPkg_Safe("DanmakuCollect", initPkg_DanmakuCollect), initPkg_Safe("RestoreYuba", initPkg_RestoreYuba), initPkg_Safe("ChangeDanmakuLengthLimit", initPkg_ChangeDanmakuLengthLimit), initPkg_Safe("CheckAnchorPocket", initPkg_CheckAnchorPocket), initPkg_Safe("LastLiveTime", initPkg_LastLiveTime), initPkg_Safe("FansBadgeList", initPkg_FansBadgeList), initPkg_Safe("VolumeMouseScrolling", initPkg_VolumeMouseScrolling); } function initPkg_Safe(name, fn) { try { if ("function" === typeof fn) fn(); } catch (e) { const err = e || {}; console.error("[DouyuEx] 初始化失败:" + name + " —— " + (err.message || err)); } } function initPkg_Timer() { initPkg_LevelTask_Timer(), initPkg_DisableCloseJump_Timer(); } function initTimer() { initPkg_Timer(); } function initStyles() { let host = document.head || document.documentElement; if (!host) return document.addEventListener("DOMContentLoaded", initStyles, { once: true }), void 0; let style = document.createElement("style"); style.appendChild(document.createTextNode(` body{position:relative;} #ex-accountList-wrap { left: -152px; top: -16px; /* max-height: 330px; overflow-y: scroll; scrollbar-width: none; -ms-overflow-style: none; */ -webkit-transition: all .2s cubic-bezier(.22,.58,.12,.98); -o-transition: all cubic-bezier(.22,.58,.12,.98) .2s; -moz-transition: all cubic-bezier(.22,.58,.12,.98) .2s; transition: all .2s cubic-bezier(.22,.58,.12,.98); -webkit-transform-origin: 80% 0; -moz-transform-origin: 80% 0; -ms-transform-origin: 80% 0; -o-transform-origin: 80% 0; transform-origin: 80% 0; -webkit-animation: scale-in-ease .5s cubic-bezier(.22,.58,.12,.98); -moz-animation: scale-in-ease cubic-bezier(.22,.58,.12,.98) .5s; -o-animation: scale-in-ease cubic-bezier(.22,.58,.12,.98) .5s; animation: scale-in-ease .5s cubic-bezier(.22,.58,.12,.98); } /* #ex-accountList-wrap::-webkit-scrollbar { display: none; } */ .ex-accountList-item { padding: 10px; display: flex; border-radius: 10px; align-items: center; } .ex-accountList-item:hover { background-color: rgb(244,244,244); } #ex-accountList-iframe { display: none; } #ex-accountList-iframe2 { display: none; } #ex-accountList-item-add { padding: 10px; text-align: center; margin-bottom:0px; border-radius: 10px; } #ex-accountList-item-add:hover { background-color: rgb(244,244,244); } .ex-accountList-item__imgWrap { flex: 0 0 25%; } .ex-accountList-item__img { width: 50px; height: 50px; border-radius: 50%; } .ex-accountList-item__name { line-height: 50px; flex: 0 0 55%; } .ex-accountList-item__btn { height: 30px; width: 50px; border-radius: 10px; align-items: center; flex: auto; text-align: center; line-height: 28px; color: white; background-color: rgb(245,108,108); } .ex-accountList-item__btn:hover { background-color: rgb(247,137,137); } #ex-accountList-icon:hover > #ex-accountList-wrap { display: block; } #ex-accountList-content { background-color: white; }.bag-info { position: absolute; background-color: rgba(0, 0, 0, 0.6); color: white; /* 原来是写死的 20×20 方块:只够显示 1~2 位数字,写上日期/「剩 29 天」就被裁成看不清的黑块。 改成按内容自适应,保证胶囊里能完整读出来。 */ min-width: 20px; max-width: 100%; padding: 0 3px; border-radius: 3px; font-size: 11px; font-weight: 800; line-height: 14px; white-space: nowrap; overflow: hidden; box-sizing: border-box; text-align: center; z-index: 10; bottom: 0; } .bag-button { color: rgb(255, 255, 255); text-align: center; height: 15px; line-height: 15px; cursor: pointer; margin-left: 5px; background: rgb(70, 171, 255); border-radius: 9px; padding: 0px 10px; right: 20px; }.barragePanel__funcPanel { position: absolute; width: 232px; height: 270px; display: block; background: white; overflow-y: scroll; } .barragePanel__funcPanel::-webkit-scrollbar {display:none} .barragePanel__muteTime { /* position: absolute; left: 25px; top: 123px; */ z-index: 5; } .danmuContent-25f266 { pointer-events: auto !important; } .thirdBtn-06cde5, .fourBtn-0845d4 { /* 加宽按钮方便点击 */ margin-left: 0px !important; margin-right: 0px !important; padding: 0 17px !important; }#copy-real-live { cursor: pointer; } .Title-row-span, .Title-row-icon { display: flex; align-items: center; justify-content: center; } .Title-row-icon { margin-right: 4px; } .live-next-body #copy-real-live { margin-left: 4px; }.ChatBarrageCollect .TagItem { height: auto !important; } .ChatBarrageCollect .TagItem-txt { overflow: auto !important; white-space: normal !important; text-overflow: clip !important; }/* ==================== 一级菜单精灵球样式与最右定位 ==================== */ .ex-icon { display: inline-flex !important; align-items: center !important; justify-content: center !important; vertical-align: middle !important; margin-left: auto !important; /* 核心:在 flex/流式布局中强推到最右侧 */ margin-right: 8px !important; order: 9999 !important; /* 核心:Flexbox 排序最后,钉在最右侧 */ user-select: none !important; cursor: pointer !important; position: relative !important; transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) !important; } .ex-icon a { display: flex !important; justify-content: center !important; align-items: center !important; width: 38px !important; height: 38px !important; border-radius: 50% !important; background: rgba(255, 255, 255, 0.25) !important; backdrop-filter: blur(12px) !important; -webkit-backdrop-filter: blur(12px) !important; transition: all 0.22s cubic-bezier(0.34, 1.56, 0.64, 1) !important; } .ex-icon:hover a { background: rgba(255, 255, 255, 0.6) !important; box-shadow: 0 4px 14px rgba(255, 102, 0, 0.35) !important; transform: scale(1.15) rotate(15deg) !important; } .ex-icon:active a { transform: scale(0.92) rotate(0deg) !important; } .ex-icon svg { width: 28px !important; height: 28px !important; display: block !important; transition: transform 0.22s ease !important; } /* ========================================================================== 文件地图(2026-11-05 整理) 1) L1/L2 令牌 —— 间距/尺寸/圆角/字号/颜色,唯一数值来源 2) 面板外壳 —— .miuix-modal / __body / __header / 滚动与遮罩 3) 旧版组件类 —— fans-panel__* / popup-panel__* / livetool__* 等 4) L3 组件层 .ui-* —— 卡片 / 行 / 按钮 / 单元格 / 操作条 5) 面板内定制 —— 各面板的内容级类名(登录态/资源格等) 6) 清理批次 A–E —— 2026-11 的统一化与债务清理(按主题归并) ⚠ 顺序即层叠:同权重下越靠后越优先。改顺序前先跑 tools/ui-diff.mjs 比对。 ========================================================================== */ /* ========================================================================== DouyuEx-RL MIUIX 统一设计系统 (Single Source of Truth) 涵盖 8 个三级控制台面板、二级 Dock 胶囊与全局控件,全量 1:1 复刻工业质感 ========================================================================== */ /* ========================================================================== L1 · 刻度(primitive):只放数字与色值,不含语义、不被面板直接引用 L2 · 语义(semantic):面板与组件只认这一层,改语义不用碰像素 L3 · 组件(component):.ui-* 只消费 L2 ========================================================================== */ :root { /* ---- L1 间距刻度(4px 基准,2px 细分) ---- */ --dx-space-0: 0px; --dx-space-1: 2px; --dx-space-2: 4px; --dx-space-3: 6px; --dx-space-4: 8px; --dx-space-5: 10px; --dx-space-6: 12px; --dx-space-8: 16px; --dx-space-10: 20px; --dx-space-20: 40px; /* ---- L1 尺寸刻度 ---- */ --dx-size-2: 2px; --dx-size-4: 4px; --dx-size-6: 6px; --dx-size-8: 8px; --dx-size-10: 10px; --dx-size-12: 12px; --dx-size-14: 14px; --dx-size-18: 18px; --dx-size-20: 20px; --dx-size-26: 26px; --dx-size-32: 32px; --dx-size-38: 38px; --dx-size-40: 40px; --dx-size-44: 44px; --dx-size-46: 46px; --dx-size-56: 56px; /* ---- L1 圆角刻度 ---- */ --dx-radius-4: 4px; --dx-radius-6: 6px; --dx-radius-8: 8px; --dx-radius-10: 10px; --dx-radius-14: 14px; --dx-radius-22: 22px; --dx-radius-full: 50%; /* ---- L1 字阶 / 字重 / 行高 ---- */ --dx-font-11: 11px; --dx-font-115: 11.5px; --dx-font-12: 12px; --dx-font-14: 14px; --dx-font-16: 16px; --dx-weight-400: 400; --dx-weight-500: 500; --dx-weight-600: 600; --dx-weight-700: 700; --dx-leading-tight: 1.3; --dx-leading-normal: 1.5; /* ---- L1 层级 / 动效 ---- */ --dx-z-sticky: 3; --dx-z-menu: 50; --dx-z-dock: 10000; --dx-z-panel: 100030; --dx-dur-fast: 0.18s; --dx-dur-base: 0.2s; --dx-dur-slow: 0.3s; --dx-ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* ---- L1 颜色(墨 / 面 / 强调 / 状态) ---- */ --dx-ink-1: #0f172a; --dx-ink-2: #1e293b; --dx-ink-muted: #334155; --dx-ink-inverse: #ffffff; --dx-accent: #0066ff; --dx-accent-deep: #0047b3; --dx-state-ok: #047857; --dx-state-warn: #92400e; --dx-state-danger: #b91c1c; --dx-tint: 255, 255, 255; /* 白片基色(改深浅只动这里) */ /* ---- L2 语义:表面 ---- */ --dx-surface-panel: rgba(var(--dx-tint), 0.65); --dx-surface-card: rgba(var(--dx-tint), 0.42); --dx-surface-row: rgba(var(--dx-tint), 0.82); --dx-surface-bar: rgba(var(--dx-tint), 0.94); --dx-surface-row-hover: rgba(var(--dx-tint), 0.97); --dx-border-card: 1px solid rgba(var(--dx-tint), 0.75); --dx-border-row: 1px solid rgba(var(--dx-tint), 0.95); --dx-border-panel: 1px solid rgba(var(--dx-tint), 0.85); /* ---- L2 语义:文字 ---- */ --dx-text-title: var(--dx-ink-1); --dx-text-body: var(--dx-ink-2); --dx-text-muted: var(--dx-ink-2); --dx-text-on-accent: var(--dx-ink-inverse); /* ---- L2 语义:间距/尺寸(组件只认这些) ---- */ --dx-space-row-gap: var(--dx-space-3); --dx-space-card-pad-y: var(--dx-space-4); --dx-space-card-pad-x: var(--dx-space-6); --dx-space-card-gap: var(--dx-space-4); --dx-size-icon: var(--dx-size-38); --dx-size-control: var(--dx-size-44); } :root { --miuix-modal-width: 380px; --miuix-modal-height: 370px; --miuix-modal-radius: 22px; --miuix-modal-bg: var(--dx-surface-panel); --miuix-modal-blur: blur(40px) saturate(220%); --miuix-modal-border: var(--dx-border-panel); --miuix-modal-shadow: inset 0 1px 1.5px 0 rgba(255, 255, 255, 0.95), 0 16px 40px rgba(15, 23, 42, 0.14), 0 2px 8px rgba(15, 23, 42, 0.06); --miuix-card-bg: var(--dx-surface-card); --miuix-card-border: var(--dx-border-card); --miuix-card-radius: 14px; --miuix-item-bg: var(--dx-surface-row); --miuix-item-hover: var(--dx-surface-row-hover); --miuix-item-border: var(--dx-border-row); --miuix-blue: var(--dx-accent); --miuix-blue-glow: rgba(0, 102, 255, 0.4); --miuix-spring: cubic-bezier(0.34, 1.56, 0.64, 1); } /* ========================================================================== 1. 二级 Dock 启动坞 (76px 晶透微胶囊 - 38px 圆角 - 56px 底座) ========================================================================== */ .ex-panel { position: absolute; bottom: 32px; right: 0px; min-width: max-content !important; width: max-content !important; height: 76px !important; border-radius: 38px !important; padding: 10px 18px !important; box-sizing: border-box !important; background: var(--miuix-modal-bg) !important; backdrop-filter: var(--miuix-modal-blur) !important; -webkit-backdrop-filter: var(--miuix-modal-blur) !important; border: 1px solid rgba(255, 255, 255, 0.88) !important; box-shadow: inset 0 1px 1.5px 0 rgba(255, 255, 255, 0.98), 0 16px 40px rgba(15, 23, 42, 0.14) !important; z-index: 10000 !important; user-select: none; display: none; overflow: visible; } /* 二级菜单展开/折叠弹簧阻尼微上浮动画 */ .ex-panel.miuix-dock-in, .ex-panel[style*="display: flex"], .ex-panel[style*="display: block"] { animation: miuix-dock-spring-in 0.28s var(--miuix-spring) forwards !important; transform-origin: bottom right !important; } .ex-panel.miuix-dock-out { animation: miuix-dock-spring-out 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards !important; pointer-events: none !important; } @keyframes miuix-dock-spring-in { 0% { opacity: 0; transform: translateY(14px) scale(0.92); } 65% { opacity: 1; transform: translateY(-2px) scale(1.02); } 100% { opacity: 1; transform: translateY(0) scale(1); } } @keyframes miuix-dock-spring-out { 0% { opacity: 1; transform: translateY(0) scale(1); } 100% { opacity: 0; transform: translateY(10px) scale(0.94); } } .ex-panel__close { position: absolute !important; top: -9px !important; right: -9px !important; z-index: 10001 !important; width: 26px !important; height: 26px !important; padding: 0 !important; margin: 0 !important; border: 1px solid rgba(0, 0, 0, 0.08) !important; border-radius: 50% !important; background: #ffffff !important; color: var(--dx-text-body) !important; font-size: var(--dx-font-16) !important; font-weight: 700 !important; line-height: 24px !important; text-align: center !important; cursor: pointer !important; display: flex !important; align-items: center !important; justify-content: center !important; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.14) !important; transition: all 0.2s var(--miuix-spring) !important; } .ex-panel__close:hover { background: #ef4444 !important; border-color: #ef4444 !important; color: #ffffff !important; transform: scale(1.15) rotate(90deg) !important; box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4) !important; } .ex-panel.ex-panel--floating { position: fixed; z-index: 10000; } .ex-panel__wrap { display: flex !important; align-items: center !important; justify-content: center !important; gap: 10px !important; width: 100% !important; height: 100% !important; position: relative !important; z-index: 1 !important; } .ex-panel__wrap > div { width: 56px !important; height: 56px !important; border-radius: var(--dx-radius-14) !important; display: flex !important; align-items: center !important; justify-content: center !important; position: relative !important; transition: all 0.22s var(--miuix-spring) !important; cursor: pointer !important; border: 1px solid rgba(255, 255, 255, 0.7) !important; background: rgba(255, 255, 255, 0.45) !important; box-sizing: border-box !important; } .ex-panel__wrap > div:hover { background: rgba(255, 255, 255, 0.92) !important; transform: translateY(-2.5px) !important; border-color: rgba(0, 102, 255, 0.35) !important; box-shadow: 0 6px 18px rgba(0, 102, 255, 0.24), inset 0 1px 1px #fff !important; } .ex-panel__wrap > div:active { transform: translateY(0) scale(0.96) !important; } /* 磁吸指示器小胶囊 (24x4px 生机蓝) */ .ex-panel__indicator { position: absolute !important; bottom: 3px !important; left: 50% !important; width: 24px !important; height: 4px !important; border-radius: 2px !important; background: var(--miuix-blue) !important; box-shadow: 0 0 8px rgba(0, 102, 255, 0.65) !important; transform: translateX(-50%) scaleX(0) !important; opacity: 0 !important; pointer-events: none !important; transition: all 0.2s var(--miuix-spring) !important; } .ex-panel__wrap > div.is-active .ex-panel__indicator, .ex-panel__wrap > div.ex-dock-active .ex-panel__indicator { transform: translateX(-50%) scaleX(1) !important; opacity: 1 !important; } .ex-panel__icon { display: flex !important; align-items: center !important; justify-content: center !important; position: relative !important; width: 56px !important; height: 56px !important; padding: 0 !important; margin: 0 !important; } .ex-panel__icon svg, .ex-panel__icon img { width: 38px !important; height: 38px !important; max-width: 38px !important; max-height: 38px !important; object-fit: contain !important; display: block !important; pointer-events: none !important; transition: transform 0.22s var(--miuix-spring) !important; /* Phosphor 图标是 currentColor 线稿,统一给一个中性墨色;旧的内联彩色 SVG 自带 fill,不受影响 */ color: #334155 !important; } .ex-panel__wrap > div:hover .ex-panel__icon svg, .ex-panel__wrap > div:hover .ex-panel__icon img { transform: scale(1.08) !important; } .ex-panel__tip { display: none; background: #ff3b30; border-radius: 50%; width: 8px; height: 8px; top: 6px; right: 6px; position: absolute; box-shadow: 0 0 6px rgba(255, 59, 48, 0.6); } /* ========================================================================== 2. 全量 8 个三级控制台统一尺寸与通透磨砂 (380×370 齐平三维铁壁) ========================================================================== */ .miuix-modal, .sign-panel, .fans-continue-panel, .extool, .livetool, .bloop, .exlottery, .popup-player-panel, .exupdate-panel { width: var(--miuix-modal-width) !important; max-width: calc(100vw - 24px) !important; height: var(--miuix-modal-height) !important; max-height: var(--miuix-modal-height) !important; min-height: var(--miuix-modal-height) !important; box-sizing: border-box !important; padding: 0 !important; overflow: clip !important; /* 不创建滚动容器:否则点内部单选/输入会被 scrollIntoView 把整块面板滚走(白板 bug) */ display: none; z-index: 100030 !important; border-radius: var(--miuix-modal-radius) !important; background: var(--miuix-modal-bg) !important; backdrop-filter: var(--miuix-modal-blur) !important; -webkit-backdrop-filter: var(--miuix-modal-blur) !important; border: var(--miuix-modal-border) !important; box-shadow: var(--miuix-modal-shadow) !important; color: var(--dx-text-title) !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif !important; flex-direction: column !important; } /* 划过瞬间淡入 + 微上浮优雅动画 */ .miuix-modal.miuix-modal-in, .sign-panel.miuix-modal-in, .fans-continue-panel.miuix-modal-in, .extool.miuix-modal-in, .livetool.miuix-modal-in, .bloop.miuix-modal-in, .exlottery.miuix-modal-in, .popup-player-panel.miuix-modal-in, .exupdate-panel.miuix-modal-in, .miuix-modal[style*="display: block"], .miuix-modal[style*="display:block"], .miuix-modal[style*="display: flex"], .miuix-modal[style*="display:flex"], .sign-panel[style*="display: block"], .sign-panel[style*="display:flex"], .fans-continue-panel[style*="display: block"], .fans-continue-panel[style*="display:flex"], .extool[style*="display: block"], .extool[style*="display:flex"], .livetool[style*="display: block"], .livetool[style*="display:flex"], .bloop[style*="display: block"], .bloop[style*="display:flex"], .exlottery[style*="display: block"], .exlottery[style*="display:flex"], .popup-player-panel[style*="display: block"], .popup-player-panel[style*="display:flex"], .exupdate-panel[style*="display: block"], .exupdate-panel[style*="display:flex"] { display: flex !important; flex-direction: column !important; opacity: 1 !important; visibility: visible !important; pointer-events: auto !important; animation: miuix-modal-in 0.18s cubic-bezier(0.16, 1, 0.3, 1) forwards !important; } @keyframes miuix-modal-in { 0% { opacity: 0; transform: translateY(8px); } 100% { opacity: 1; transform: translateY(0); } } /* ========================================================================== 3. Level 3 模态吸顶 Header (顶栏满幅左右贴合,绝无漏缝) ========================================================================== */ .miuix-modal__header { flex: 0 0 auto !important; width: 100% !important; margin: 0 !important; padding: 12px 16px !important; box-sizing: border-box !important; display: flex !important; align-items: center !important; justify-content: space-between !important; background: rgba(255, 255, 255, 0.45) !important; backdrop-filter: blur(28px) saturate(190%) !important; -webkit-backdrop-filter: blur(28px) saturate(190%) !important; border-bottom: 1px solid rgba(0, 0, 0, 0.05) !important; box-shadow: inset 0 1px 1px 0 rgba(255, 255, 255, 0.95), 0 2px 6px rgba(0, 0, 0, 0.03) !important; z-index: 50 !important; border-top-left-radius: var(--dx-radius-22) !important; border-top-right-radius: var(--dx-radius-22) !important; border-bottom-left-radius: 0 !important; border-bottom-right-radius: 0 !important; } .miuix-modal__title-box { display: flex !important; align-items: center !important; } .miuix-modal__title { font-size: var(--dx-font-14) !important; font-weight: 700 !important; color: var(--dx-text-title) !important; letter-spacing: -0.2px !important; } .miuix-modal__close { position: relative !important; width: 26px !important; height: 26px !important; border-radius: var(--dx-radius-8) !important; background: rgba(0, 0, 0, 0.06) !important; border: none !important; color: #475569 !important; display: flex !important; align-items: center !important; justify-content: center !important; cursor: pointer !important; font-size: var(--dx-font-16) !important; font-weight: 600 !important; line-height: 1 !important; text-align: center !important; user-select: none !important; padding: 0 !important; margin: 0 !important; box-sizing: border-box !important; transition: background-color 0.18s var(--miuix-spring), color 0.18s ease, transform 0.2s var(--miuix-spring) !important; } .miuix-modal__close:hover { background: #ef4444 !important; color: #ffffff !important; transform: scale(1.1) rotate(90deg) !important; box-shadow: 0 2px 8px rgba(239, 68, 68, 0.35) !important; } .miuix-modal__close:active { transform: scale(0.95) !important; } /* ========================================================================== 4. 滚动条起始点统一在顶栏正下方:内容承载容器 (.miuix-modal__body) ========================================================================== */ .miuix-modal__body { display: block !important; padding-bottom: 0 !important; /* 粘底栏自己盖到下沿,body 不再留缝 */ flex: 1 1 auto !important; min-height: 0 !important; max-height: calc(var(--miuix-modal-height) - 52px) !important; width: 100% !important; overflow-y: overlay !important; overflow-x: hidden !important; box-sizing: border-box !important; padding: 10px 0 0 0 !important; /* 底部交给粘底栏,bar 与面板下沿齐平 */ /* 底部留白必须小:sticky 相对 padding box 定位,留大会把按钮顶到内容中间 */ /* 底部让出粘底栏:滚到底时最后一行不被压 */ } /* 统一 4px 极细微质感滚动条 */ .miuix-modal__body::-webkit-scrollbar, .sign-panel::-webkit-scrollbar, .fans-continue-panel::-webkit-scrollbar, .extool::-webkit-scrollbar, .livetool::-webkit-scrollbar, .bloop::-webkit-scrollbar, .exlottery::-webkit-scrollbar, .popup-player-panel::-webkit-scrollbar, .exupdate-panel::-webkit-scrollbar { width: 4px !important; } .miuix-modal__body::-webkit-scrollbar-thumb, .sign-panel::-webkit-scrollbar-thumb, .fans-continue-panel::-webkit-scrollbar-thumb, .extool::-webkit-scrollbar-thumb, .livetool::-webkit-scrollbar-thumb, .bloop::-webkit-scrollbar-thumb, .exlottery::-webkit-scrollbar-thumb, .popup-player-panel::-webkit-scrollbar-thumb, .exupdate-panel::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.18) !important; border-radius: var(--dx-radius-6) !important; } .miuix-modal__body::-webkit-scrollbar-track, .sign-panel::-webkit-scrollbar-track, .fans-continue-panel::-webkit-scrollbar-track, .extool::-webkit-scrollbar-track, .livetool::-webkit-scrollbar-track, .bloop::-webkit-scrollbar-track, .exlottery::-webkit-scrollbar-track, .popup-player-panel::-webkit-scrollbar-track, .exupdate-panel::-webkit-scrollbar-track { background: transparent !important; } /* ========================================================================== 5. 通用四级卡片规范 (.fans-panel__card, .miuix-card, .livetool__cell) ========================================================================== */ .fans-panel__card, .popup-panel__card, .exupdate-panel__card, .miuix-card, .livetool__cell { margin: 0 12px 10px 12px !important; padding: 10px 14px !important; box-sizing: border-box !important; background: var(--miuix-card-bg) !important; border: var(--miuix-card-border) !important; border-radius: var(--miuix-card-radius) !important; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02) !important; backdrop-filter: blur(12px) !important; -webkit-backdrop-filter: blur(12px) !important; width: auto !important; transition: all 0.2s var(--miuix-spring) !important; } .fans-panel__card:hover, .popup-panel__card:hover, .exupdate-panel__card:hover, .miuix-card:hover, .livetool__cell:hover { background: rgba(255, 255, 255, 0.75) !important; border-color: rgba(0, 102, 255, 0.35) !important; box-shadow: 0 4px 12px rgba(0, 102, 255, 0.08) !important; } .fans-panel__card-header, .popup-panel__card-header, .exupdate-panel__card-header, .miuix-card__header { display: flex !important; align-items: center !important; justify-content: space-between !important; margin-bottom: 8px !important; padding-bottom: 4px !important; border-bottom: 1px solid rgba(0, 0, 0, 0.05) !important; } .fans-panel__card-title, .popup-panel__card-title, .exupdate-panel__card-title, .miuix-card__title { font-size: var(--dx-font-14) !important; font-weight: 700 !important; color: var(--dx-text-title) !important; } /* ========================================================================== 5.5 直播间工具抽屉内部结构(单元格 / 标题 / 药丸操作 / 右侧开关) 原 LiveTool.css 独有声明全部上收至此,配合 iOS 折叠披露指示器。 ========================================================================== */ .livetool__cell { display: flex !important; align-items: center !important; justify-content: space-between !important; flex-wrap: wrap !important; gap: 8px 10px !important; } .livetool__cell_title { display: flex !important; align-items: center !important; gap: 8px !important; flex: 1 1 auto !important; min-width: 0 !important; margin: 0 !important; padding: 0 !important; cursor: pointer !important; user-select: none !important; } /* 抽屉标题文字:全项目统一色值,五个抽屉严格一致(不再有宝蓝色遗留) */ .livetool__cell_title > span[id$="__title"] { display: inline-flex !important; align-items: center !important; gap: 6px !important; font-size: var(--dx-font-14) !important; font-weight: 600 !important; line-height: 1.3 !important; color: var(--dx-text-title) !important; transition: color 0.18s ease !important; } /* iOS 披露指示器:折叠时 ▸ 指向右,展开时 ▾ 指向下 */ .livetool__cell_title > span[id$="__title"]::after { content: "" !important; width: 5.5px !important; height: 5.5px !important; border-right: 1.6px solid #94a3b8 !important; border-bottom: 1.6px solid #94a3b8 !important; transform: rotate(-45deg) !important; transition: transform 0.26s var(--miuix-spring), border-color 0.18s ease !important; flex-shrink: 0 !important; box-sizing: border-box !important; } .livetool__cell_title:hover > span[id$="__title"]::after { border-color: #64748b !important; } .livetool__cell_title.is-open > span[id$="__title"] { color: var(--dx-accent-deep) !important; /* S4:深蓝墨(玻璃上 ≈5.9:1),亮蓝不再当文字 */ } .livetool__cell_title.is-open > span[id$="__title"]::after { transform: rotate(45deg) !important; border-color: var(--dx-accent-deep) !important; /* S4:深蓝墨(玻璃上 ≈5.9:1),亮蓝不再当文字 */ } /* 标题旁的小胶囊操作按钮 (导出 / 导入 / 名单 / 面板) */ .livetool__cell_title span:not([id$="__title"]) { font-size: var(--dx-font-11) !important; font-weight: 600 !important; color: var(--dx-accent-deep) !important; /* S4:深蓝墨(玻璃上 ≈5.9:1),亮蓝不再当文字 */ background: rgba(0, 102, 255, 0.12) !important; border: 1px solid rgba(0, 102, 255, 0.25) !important; padding: 2px 7px !important; border-radius: var(--dx-radius-6) !important; cursor: pointer !important; box-shadow: 0 1px 3px rgba(0, 102, 255, 0.08) !important; transition: all 0.18s var(--miuix-spring) !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; } .livetool__cell_title span:not([id$="__title"]):hover { background: var(--miuix-blue) !important; color: #ffffff !important; transform: translateY(-1px) !important; box-shadow: 0 3px 8px rgba(0, 102, 255, 0.35) !important; } /* 右侧开关容器 */ .livetool__cell_option { display: flex !important; align-items: center !important; justify-content: flex-end !important; flex: 0 0 auto !important; margin-left: auto !important; padding: 0 !important; } /* 弹簧开关定位容器(原 .onoffswitch 包裹层,已并入统一引擎) */ .livetool__cell_switch { display: flex !important; align-items: center !important; flex-shrink: 0 !important; } /* 弹幕投票的「重复投票」行内开关标签 */ .vote__repeat-label { display: inline-flex !important; align-items: center !important; gap: 6px !important; margin-right: 10px !important; font-size: var(--dx-font-115) !important; font-weight: 600 !important; color: #334155 !important; cursor: pointer !important; user-select: none !important; } /* 抽屉展开后的内容面板(原 LiveTool.css 独有声明,已上收) display: none 是五个抽屉的默认收起态,必须由本处统一声明: Enter/Mute/Gift/Reply 此前各自在包级 CSS 里写了同一条,唯独弹幕投票漏了, 导致它的面板从加载起就露着(且折叠箭头读不到内联 display,状态与实际相反)。 注意这里不能加 !important —— 展开时各抽屉是通过内联 style.display = "block" 切显隐的,!important 会压过内联声明让面板再也打不开。 */ .vote__panel, .enter__panel, .mute__panel, .gift__panel, .reply__panel { display: none; width: 100% !important; max-width: 100% !important; box-sizing: border-box !important; margin: 8px 0 2px 0 !important; padding: 10px 12px !important; border-radius: var(--dx-radius-10) !important; background: rgba(255, 255, 255, 0.55) !important; border: 1px solid rgba(255, 255, 255, 0.75) !important; backdrop-filter: blur(8px) !important; -webkit-backdrop-filter: blur(8px) !important; } /* 抽屉内的下拉框补充规范(与统一输入框同族) */ .livetool select { background: rgba(255, 255, 255, 0.85) !important; border: 1px solid rgba(0, 0, 0, 0.12) !important; border-radius: var(--dx-radius-6) !important; padding: 2px 6px !important; font-size: var(--dx-font-12) !important; outline: none !important; color: var(--dx-text-title) !important; } /* 抽屉内的按钮微质感(含删除操作的危险红态) */ .livetool input[type="button"] { border: none !important; border-radius: var(--dx-radius-6) !important; padding: 3px 10px !important; font-size: var(--dx-font-115) !important; font-weight: 600 !important; cursor: pointer !important; color: #ffffff !important; background: linear-gradient(135deg, #2b7fff, #0055ff) !important; box-shadow: 0 2px 6px rgba(0, 102, 255, 0.28) !important; transition: all 0.18s var(--miuix-spring) !important; outline: none !important; } .livetool input[type="button"]:hover { background: linear-gradient(135deg, #3d8bff, #004de6) !important; transform: translateY(-1px) !important; box-shadow: 0 4px 10px rgba(0, 102, 255, 0.4) !important; } .livetool input[id$="__del"] { background: rgba(239, 68, 68, 0.14) !important; border: 1px solid rgba(239, 68, 68, 0.28) !important; color: #ef4444 !important; box-shadow: none !important; } .livetool input[id$="__del"]:hover { background: #ef4444 !important; color: #ffffff !important; border-color: #ef4444 !important; } /* 版本更新日志列表(原 Update.css 内联规则上收至统一引擎) */ .exupdate-list { margin: 0 !important; padding: 0 !important; list-style: none !important; font-size: var(--dx-font-115) !important; line-height: 18px !important; color: #334155 !important; }.exupdate-list li { margin-bottom: var(--dx-space-4) !important; word-break: break-word !important; padding-left: 1.1em !important; text-indent: -1.1em !important; } .exupdate-list li:last-child { margin-bottom: 0 !important; } /* ========================================================================== 6. 选项列表与选项项 (.sign-options-list, .sign-option-item, .miuix-item) ========================================================================== */ .sign-options-list { display: flex !important; flex-direction: column !important; gap: 6px !important; margin-top: 2px !important; } .sign-option-item, .miuix-item { display: flex !important; align-items: center !important; justify-content: space-between !important; gap: 12px !important; padding: 7px 10px !important; border-radius: var(--dx-radius-10) !important; background: var(--miuix-item-bg) !important; border: var(--miuix-item-border) !important; cursor: pointer !important; user-select: none !important; transition: all 0.2s var(--miuix-spring) !important; box-sizing: border-box !important; } .sign-option-item:hover, .miuix-item:hover { background: var(--miuix-item-hover) !important; border-color: rgba(0, 102, 255, 0.35) !important; transform: translateY(-1px) !important; box-shadow: 0 3px 8px rgba(0, 102, 255, 0.08) !important; } .sign-option-text { display: flex !important; flex-direction: column !important; flex: 1 1 auto !important; min-width: 0 !important; } .sign-option-title { font-size: var(--dx-font-12) !important; font-weight: 600 !important; color: var(--dx-text-title) !important; line-height: 1.3 !important; } .sign-option-desc { font-size: var(--dx-font-115) !important; color: var(--dx-text-body) !important; line-height: 1.2 !important; margin-top: 1px !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; } /* ========================================================================== 7. 统一澎湃蓝弹簧开关规范 (Switch: 宽 38px / 高 22px / 圆角 22px) ========================================================================== */ .sign-checkbox, .miuix-switch, input[type="checkbox"].sign-checkbox { -webkit-appearance: none !important; appearance: none !important; position: relative !important; width: 36px !important; min-width: 36px !important; height: 20px !important; background: rgba(0, 0, 0, 0.14) !important; border-radius: var(--dx-radius-22) !important; cursor: pointer !important; outline: none !important; border: none !important; box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1) !important; transition: background-color 0.28s var(--miuix-spring), box-shadow 0.28s ease !important; flex-shrink: 0 !important; margin: 0 !important; padding: 0 !important; display: inline-block !important; vertical-align: middle !important; } .sign-checkbox::before, .miuix-switch::before, input[type="checkbox"].sign-checkbox::before { content: "" !important; display: block !important; position: absolute !important; top: 2px !important; left: 2px !important; width: 16px !important; height: 16px !important; background: #ffffff !important; border-radius: 50% !important; box-shadow: 0 1px 2px rgba(15, 23, 42, 0.22) !important; transition: transform 0.32s var(--miuix-spring), width 0.2s ease !important; } .sign-checkbox:active::before, .miuix-switch:active::before, input[type="checkbox"].sign-checkbox:active::before { width: 19px !important; } .sign-checkbox:checked, .miuix-switch:checked, input[type="checkbox"].sign-checkbox:checked { background: var(--miuix-blue) !important; box-shadow: 0 2px 8px var(--miuix-blue-glow) !important; } .sign-checkbox:checked::before, .miuix-switch:checked::before, input[type="checkbox"].sign-checkbox:checked::before { transform: translateX(16px) !important; } .sign-checkbox:checked:active::before, .miuix-switch:checked:active::before, input[type="checkbox"].sign-checkbox:checked:active::before { transform: translateX(12px) !important; } /* ========================================================================== 7.5 统一分段胶囊控制器 (.miuix-seg) 用于互斥单选(同屏播放模式 / 自动钓鱼模式 / 小尾巴位置等)。 原生 radio 圆点被彻底抹除,仅保留语义与 :checked 状态; 选中态依赖 input:checked + span 相邻兄弟选择器,不依赖 :has(),兼容性最稳。 ========================================================================== */ .miuix-seg { display: flex !important; align-items: stretch !important; gap: 2px !important; padding: 2px !important; margin-top: 2px !important; background: rgba(0, 0, 0, 0.06) !important; border-radius: var(--dx-radius-8) !important; box-sizing: border-box !important; } .miuix-seg__item { flex: 1 1 0 !important; min-width: 0 !important; display: flex !important; align-items: center !important; justify-content: center !important; margin: 0 !important; padding: 0 !important; cursor: pointer !important; user-select: none !important; border-radius: var(--dx-radius-8) !important; } /* 原生圆点彻底隐藏(不用 display:none,保留键盘可聚焦与语义) */ .miuix-seg__item > input[type="radio"] { position: absolute !important; width: 1px !important; height: 1px !important; margin: -1px !important; padding: 0 !important; border: 0 !important; overflow: hidden !important; clip: rect(0 0 0 0) !important; -webkit-appearance: none !important; appearance: none !important; opacity: 0 !important; } .miuix-seg__label { display: block !important; width: 100% !important; padding: 5px 10px !important; box-sizing: border-box !important; text-align: center !important; font-size: var(--dx-font-115) !important; font-weight: 600 !important; line-height: 1.4 !important; color: var(--dx-text-body) !important; border-radius: var(--dx-radius-8) !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; transition: background-color 0.2s var(--miuix-spring), color 0.18s ease, box-shadow 0.2s ease, transform 0.18s var(--miuix-spring) !important; } .miuix-seg__item:hover > .miuix-seg__label { color: var(--dx-text-title) !important; } .miuix-seg__item > input[type="radio"]:checked + .miuix-seg__label { background: #ffffff !important; color: var(--dx-accent-deep) !important; /* S4:深蓝墨(玻璃上 ≈5.9:1),亮蓝不再当文字 */ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12) !important; } .miuix-seg__item:active > .miuix-seg__label { transform: scale(0.97) !important; } .miuix-seg__item > input[type="radio"]:focus-visible + .miuix-seg__label { box-shadow: 0 0 0 2px var(--miuix-blue-glow) !important; } /* 禁用态(自动钓鱼运行中锁定模式、小尾巴启用后锁定位置选项) */ .miuix-seg__item > input[type="radio"]:disabled + .miuix-seg__label { opacity: 0.45 !important; pointer-events: none !important; } /* ========================================================================== 8. 通用按钮微质感 (大弹簧操作按钮、胶囊操作按钮、送出按钮) ========================================================================== */ /* 操作区容器统一规范:左右留白必须与四级卡片保持一致(12px), 否则同一面板内的按钮会比卡片更宽/更窄,造成跨面板宽度不一致。 */ .fans-panel__action-wrap, .popup-panel__action-wrap, .exupdate-panel__action-wrap { padding: 0 12px !important; margin-top: 10px !important; margin-bottom: 8px !important; box-sizing: border-box !important; } .ex-btn-primary, .fans-panel__submit-btn, .popup-panel__submit-btn, .exupdate-panel__submit-btn { width: 100% !important; height: 44px !important; border-radius: var(--dx-radius-10) !important; font-size: var(--dx-font-14) !important; font-weight: 600 !important; color: #ffffff !important; background: linear-gradient(135deg, #2b7fff, #0055ff) !important; box-shadow: 0 4px 14px rgba(0, 102, 255, 0.35) !important; border: none !important; cursor: pointer !important; transition: all 0.18s var(--miuix-spring) !important; outline: none !important; box-sizing: border-box !important; } .ex-btn-primary:hover, .fans-panel__submit-btn:hover, .popup-panel__submit-btn:hover, .exupdate-panel__submit-btn:hover { background: linear-gradient(135deg, #3d8bff, #004de6) !important; transform: translateY(-1.5px) !important; box-shadow: 0 6px 18px rgba(0, 102, 255, 0.45) !important; } .ex-btn-primary:active, .fans-panel__submit-btn:active, .popup-panel__submit-btn:active, .exupdate-panel__submit-btn:active { transform: translateY(0) scale(0.96) !important; } .ex-btn-primary:disabled, .fans-panel__submit-btn:disabled, .popup-panel__submit-btn:disabled, .exupdate-panel__submit-btn:disabled { background: #94a3b8 !important; box-shadow: none !important; cursor: not-allowed !important; transform: none !important; } /* 胶囊辅助小按钮 (选择道具/选择礼物/粘贴) */ .ex-picker-btn, .popup-panel__paste-btn { /* 2026-11-05 债务清理:本体直接就是次级胶囊,不再靠外部 !important 覆盖 */ border: 1px solid rgba(15, 23, 42, 0.14) !important; background: rgba(255, 255, 255, 0.92) !important; color: var(--dx-accent-deep) !important; border-radius: var(--dx-radius-8) !important; height: 28px !important; padding: 0 12px !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; font-size: var(--dx-font-115) !important; font-weight: 600 !important; line-height: 1 !important; cursor: pointer !important; outline: none !important; box-shadow: none !important; transition: background-color 0.18s ease, border-color 0.18s ease, color 0.18s ease !important; } .ex-picker-btn:hover, .popup-panel__paste-btn:hover { background: rgba(0, 102, 255, 0.18) !important; transform: translateY(-1px) !important; } /* 送出/执行小按钮 */ .ex-send-btn { border: 1px solid rgba(15, 23, 42, 0.14) !important; background: rgba(255, 255, 255, 0.92) !important; color: var(--dx-accent-deep) !important; border-radius: var(--dx-radius-6) !important; padding: 4px 12px !important; font-size: var(--dx-font-115) !important; font-weight: 600 !important; cursor: pointer !important; outline: none !important; box-shadow: 0 2px 6px rgba(0, 102, 255, 0.3) !important; transition: all 0.18s var(--miuix-spring) !important; } .ex-send-btn:hover { background: linear-gradient(135deg, #3d8bff, #004de6) !important; transform: translateY(-1px) !important; box-shadow: 0 4px 10px rgba(0, 102, 255, 0.4) !important; } .ex-send-btn:active { transform: scale(0.95) !important; } /* ========================================================================== 9. 通用输入框、下拉选单与文本域样式隔离 (绝不污染斗鱼原生播放器) ========================================================================== */ .miuix-modal input[type="text"], .miuix-modal input[type="number"], .miuix-modal textarea, .miuix-modal select, .sign-panel input[type="text"], .sign-panel input[type="number"], .fans-continue-panel input[type="number"], .extool input[type="text"], .extool input[type="number"], .livetool input[type="text"], .livetool input[type="number"], .bloop input[type="text"], .popup-player-panel input[type="text"] { border: 1px solid rgba(0, 0, 0, 0.12) !important; border-radius: var(--dx-radius-6) !important; background: rgba(255, 255, 255, 0.8) !important; padding: 3px 6px !important; font-size: var(--dx-font-115) !important; outline: none !important; color: var(--dx-text-title) !important; box-sizing: border-box !important; transition: all 0.18s ease !important; } .miuix-modal input[type="text"]:focus, .miuix-modal input[type="number"]:focus, .miuix-modal textarea:focus, .miuix-modal select:focus, .sign-panel input[type="text"]:focus, .extool input[type="text"]:focus, .livetool input[type="text"]:focus, .bloop input[type="text"]:focus, .popup-player-panel input[type="text"]:focus { border-color: var(--dx-accent-deep) !important; /* S4:深蓝墨(玻璃上 ≈5.9:1),亮蓝不再当文字 */ box-shadow: 0 0 0 2px var(--miuix-blue-glow) !important; } /* 状态标签与任务日志终端 */ .sign-status-tag { font-size: var(--dx-font-11) !important; color: #10b981 !important; font-weight: 600 !important; } .sign-log-box { min-height: 52px !important; max-height: 72px !important; margin-top: 4px !important; overflow-y: auto !important; font-size: var(--dx-font-11) !important; color: #334155 !important; background: rgba(241, 245, 249, 0.5) !important; border: 1px solid rgba(0, 0, 0, 0.04) !important; border-radius: var(--dx-radius-8) !important; padding: 6px 10px !important; line-height: 1.45 !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; box-sizing: border-box !important; } .sign-log-box::-webkit-scrollbar { width: 4px !important; } .sign-log-box::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.2) !important; border-radius: var(--dx-radius-6) !important; } .sign-log-box::-webkit-scrollbar-track { background: transparent !important; } /* ========================================================================== 10. MIUIX 礼物徽章选择组件 (可点击礼物徽章替代繁琐 ID 输入) ========================================================================== */ .miuix-gift-row { display: flex !important; align-items: center !important; justify-content: space-between !important; gap: 8px !important; margin-top: 2px !important; } .miuix-gift-badge { display: inline-flex !important; align-items: center !important; gap: 6px !important; background: rgba(255, 255, 255, 0.75) !important; border: 1px solid rgba(0, 102, 255, 0.25) !important; border-radius: var(--dx-radius-8) !important; padding: 3px 8px !important; cursor: pointer !important; user-select: none !important; transition: all 0.2s var(--miuix-spring) !important; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04) !important; max-width: 170px !important; } .miuix-gift-badge:hover { background: rgba(255, 255, 255, 0.95) !important; border-color: var(--dx-accent-deep) !important; /* S4:深蓝墨(玻璃上 ≈5.9:1),亮蓝不再当文字 */ transform: translateY(-1px) !important; box-shadow: 0 3px 8px var(--miuix-blue-glow) !important; } .miuix-gift-badge:active { transform: scale(0.96) !important; } .miuix-gift-badge__icon { width: 24px !important; height: 24px !important; object-fit: contain !important; border-radius: var(--dx-radius-6) !important; flex-shrink: 0 !important; } .miuix-gift-badge__name { font-size: var(--dx-font-115) !important; font-weight: 600 !important; color: var(--dx-text-title) !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; } .miuix-gift-badge__arrow { font-size: var(--dx-font-14) !important; color: var(--dx-accent-deep) !important; /* S4:深蓝墨(玻璃上 ≈5.9:1),亮蓝不再当文字 */ font-weight: bold !important; line-height: 1 !important; margin-left: 2px !important; }.miuix-gift-action-wrap { display: flex !important; align-items: center !important; gap: 6px !important; margin-left: auto !important; flex: 1 0 100%; justify-content: flex-end; } /* ========================================================================== 11. Level 5 模态大选择器 (礼物大盘与背包双流资产选择器) ========================================================================== */ .ex-gift-picker-mask { position: fixed !important; top: 0 !important; left: 0 !important; width: 100vw !important; height: 100vh !important; background: rgba(15, 23, 42, 0.42) !important; backdrop-filter: blur(8px) !important; -webkit-backdrop-filter: blur(8px) !important; z-index: 100080 !important; transition: opacity 0.2s ease !important; } .ex-gift-picker-modal { position: fixed !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%) !important; width: 520px !important; max-width: 92vw !important; height: 420px !important; max-height: 85vh !important; border-radius: var(--dx-radius-22) !important; background: rgba(255, 255, 255, 0.8) !important; backdrop-filter: blur(40px) saturate(220%) !important; -webkit-backdrop-filter: blur(40px) saturate(220%) !important; border: 1px solid rgba(255, 255, 255, 0.95) !important; box-shadow: inset 0 1px 1.5px rgba(255, 255, 255, 0.95), 0 24px 60px rgba(15, 23, 42, 0.25) !important; display: flex !important; flex-direction: column !important; overflow: hidden !important; z-index: 100090 !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; color: var(--dx-text-title) !important; user-select: none !important; } .ex-gift-picker-modal.miuix-modal-in { animation: miuix-picker-in 0.24s cubic-bezier(0.16, 1, 0.3, 1) forwards !important; } .ex-gift-picker-modal.miuix-modal-out { animation: miuix-picker-out 0.16s cubic-bezier(0.16, 1, 0.3, 1) forwards !important; } @keyframes miuix-picker-in { 0% { opacity: 0; transform: translate(-50%, -46%) scale(0.96); } 100% { opacity: 1; transform: translate(-50%, -50%) scale(1); } } @keyframes miuix-picker-out { 0% { opacity: 1; transform: translate(-50%, -50%) scale(1); } 100% { opacity: 0; transform: translate(-50%, -46%) scale(0.96); } } .ex-gift-picker__header { flex: 0 0 54px !important; display: flex !important; align-items: center !important; justify-content: space-between !important; padding: 0 16px !important; background: rgba(255, 255, 255, 0.5) !important; border-bottom: 1px solid rgba(0, 0, 0, 0.06) !important; gap: 12px !important; } .ex-gift-picker__tabs { display: flex !important; background: rgba(0, 0, 0, 0.06) !important; border-radius: var(--dx-radius-8) !important; padding: 2px !important; gap: 2px !important; } .ex-gift-picker__tab { border: none !important; background: transparent !important; padding: 4px 12px !important; font-size: var(--dx-font-12) !important; font-weight: 600 !important; color: var(--dx-text-body) !important; border-radius: var(--dx-radius-8) !important; cursor: pointer !important; transition: all 0.18s ease !important; outline: none !important; } .ex-gift-picker__tab.is-active { background: #ffffff !important; color: var(--dx-accent-deep) !important; /* S4:深蓝墨(玻璃上 ≈5.9:1),亮蓝不再当文字 */ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1) !important; } .ex-gift-picker__search { flex: 1 1 auto !important; height: 32px !important; border-radius: var(--dx-radius-8) !important; border: 1px solid rgba(0, 0, 0, 0.12) !important; background: rgba(255, 255, 255, 0.85) !important; padding: 4px 10px !important; font-size: var(--dx-font-12) !important; color: var(--dx-text-title) !important; outline: none !important; transition: all 0.18s ease !important; } .ex-gift-picker__search:focus { border-color: var(--dx-accent-deep) !important; /* S4:深蓝墨(玻璃上 ≈5.9:1),亮蓝不再当文字 */ box-shadow: 0 0 0 2px var(--miuix-blue-glow) !important; } .ex-gift-picker__close { border: none !important; background: transparent !important; font-size: var(--dx-font-16) !important; line-height: 1 !important; color: var(--dx-text-body) !important; cursor: pointer !important; width: 28px !important; height: 28px !important; border-radius: var(--dx-radius-6) !important; display: flex !important; align-items: center !important; justify-content: center !important; transition: all 0.16s ease !important; } .ex-gift-picker__close:hover { background: rgba(0, 0, 0, 0.08) !important; color: var(--dx-text-title) !important; } .ex-gift-picker__body { flex: 1 1 auto !important; overflow-y: auto !important; padding: 14px !important; box-sizing: border-box !important; } .ex-gift-picker__body::-webkit-scrollbar { width: 4px !important; } .ex-gift-picker__body::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.2) !important; border-radius: var(--dx-radius-6) !important; } .ex-gift-grid { display: grid !important; grid-template-columns: repeat(4, 1fr) !important; gap: 10px !important; } .ex-gift-cell { display: flex !important; flex-direction: column !important; align-items: center !important; justify-content: center !important; padding: 10px 8px !important; border-radius: var(--dx-radius-14) !important; background: rgba(255, 255, 255, 0.6) !important; border: 1px solid rgba(255, 255, 255, 0.9) !important; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04) !important; cursor: pointer !important; transition: all 0.18s var(--miuix-spring) !important; text-align: center !important; } .ex-gift-cell:hover { background: rgba(255, 255, 255, 0.95) !important; border-color: rgba(0, 102, 255, 0.4) !important; transform: translateY(-2px) !important; box-shadow: 0 4px 12px var(--miuix-blue-glow) !important; } .ex-gift-cell:active { transform: scale(0.96) !important; } .ex-gift-cell__img { width: 44px !important; height: 44px !important; object-fit: contain !important; margin-bottom: 6px !important; } .ex-gift-cell__name { font-size: var(--dx-font-115) !important; font-weight: 600 !important; color: var(--dx-text-title) !important; line-height: 1.25 !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; width: 100% !important; } .ex-gift-cell__price { font-size: var(--dx-font-11) !important; color: var(--dx-text-body) !important; margin-top: 2px !important; } /* ========================================================================== 12. 弹幕小尾巴(聊天栏按钮 / 独立微悬浮面板 / 面板内控件) 原 DanmakuTail.css 独有声明全部上收至此,实现全文件归并。 ========================================================================== */ /* 聊天工具栏的「尾」按钮 */ .ChatToolBar-DanmakuTail { display: inline-block !important; vertical-align: middle !important; width: 18px !important; height: 18px !important; border-radius: var(--dx-radius-6) !important; margin-right: 8px !important; color: #bbb !important; cursor: pointer !important; } .ChatToolBar-DanmakuTail-tip { width: 18px !important; height: 18px !important; text-align: center !important; line-height: 18px !important; border-radius: var(--dx-radius-6) !important; font-size: var(--dx-font-12) !important; color: #fff !important; cursor: pointer !important; box-sizing: border-box !important; background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAMAAADW3miqAAAAPFBMVEUAAADKy8zKy8zExMzKy83Ky83KzMzKzM7Kys3Ky8zKy83Jy8zJy83Ky8zLzMzJzMzIyM3MzMzKzMzKy8zUpkOsAAAAE3RSTlMA5PgF2qdrQ+rqysOOhzw3HBnqZkz8cgAAAN5JREFUOMuNlEkOAyEMBNs2y+xL/P+/JgoJaGCAqZOFSk0fLOOL7M7ONLBGeDCTdZsgcq6kdzAtBwLiSauMPoR5ow2Mx4dj1CZ0ArJoh1Ww/Quh5Be1w3F0apaD1a5kMSVJLyRphuknEYaGpIEXuC7pD4YWnaITuUiIdKRkVCRNRlUq+te+a0p4Ujy9ZkMpIaeUgJrEld6aYAxZJS2nF+hGwnUwmHOp/HSCzffybn1dubyZww47aQfaIGtPWgQ4O1Hj8fRgQPzYKOQFgWMhvlfWExHZnJ3M9RzSbN0eYt4fdzsuAVjiHAAAAABJRU5ErkJggg==) 50% / 18px 18px no-repeat !important; } [data-mantine-color-scheme=dark] .ChatToolBar-DanmakuTail-tip { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAABGdBTUEAALGPC/xhBQAACklpQ0NQc1JHQiBJRUM2MTk2Ni0yLjEAAEiJnVN3WJP3Fj7f92UPVkLY8LGXbIEAIiOsCMgQWaIQkgBhhBASQMWFiApWFBURnEhVxILVCkidiOKgKLhnQYqIWotVXDjuH9yntX167+3t+9f7vOec5/zOec8PgBESJpHmomoAOVKFPDrYH49PSMTJvYACFUjgBCAQ5svCZwXFAADwA3l4fnSwP/wBr28AAgBw1S4kEsfh/4O6UCZXACCRAOAiEucLAZBSAMguVMgUAMgYALBTs2QKAJQAAGx5fEIiAKoNAOz0ST4FANipk9wXANiiHKkIAI0BAJkoRyQCQLsAYFWBUiwCwMIAoKxAIi4EwK4BgFm2MkcCgL0FAHaOWJAPQGAAgJlCLMwAIDgCAEMeE80DIEwDoDDSv+CpX3CFuEgBAMDLlc2XS9IzFLiV0Bp38vDg4iHiwmyxQmEXKRBmCeQinJebIxNI5wNMzgwAABr50cH+OD+Q5+bk4eZm52zv9MWi/mvwbyI+IfHf/ryMAgQAEE7P79pf5eXWA3DHAbB1v2upWwDaVgBo3/ldM9sJoFoK0Hr5i3k4/EAenqFQyDwdHAoLC+0lYqG9MOOLPv8z4W/gi372/EAe/tt68ABxmkCZrcCjg/1xYW52rlKO58sEQjFu9+cj/seFf/2OKdHiNLFcLBWK8ViJuFAiTcd5uVKRRCHJleIS6X8y8R+W/QmTdw0ArIZPwE62B7XLbMB+7gECiw5Y0nYAQH7zLYwaC5EAEGc0Mnn3AACTv/mPQCsBAM2XpOMAALzoGFyolBdMxggAAESggSqwQQcMwRSswA6cwR28wBcCYQZEQAwkwDwQQgbkgBwKoRiWQRlUwDrYBLWwAxqgEZrhELTBMTgN5+ASXIHrcBcGYBiewhi8hgkEQcgIE2EhOogRYo7YIs4IF5mOBCJhSDSSgKQg6YgUUSLFyHKkAqlCapFdSCPyLXIUOY1cQPqQ28ggMor8irxHMZSBslED1AJ1QLmoHxqKxqBz0XQ0D12AlqJr0Rq0Hj2AtqKn0UvodXQAfYqOY4DRMQ5mjNlhXIyHRWCJWBomxxZj5Vg1Vo81Yx1YN3YVG8CeYe8IJAKLgBPsCF6EEMJsgpCQR1hMWEOoJewjtBK6CFcJg4Qxwicik6hPtCV6EvnEeGI6sZBYRqwm7iEeIZ4lXicOE1+TSCQOyZLkTgohJZAySQtJa0jbSC2kU6Q+0hBpnEwm65Btyd7kCLKArCCXkbeQD5BPkvvJw+S3FDrFiOJMCaIkUqSUEko1ZT/lBKWfMkKZoKpRzame1AiqiDqfWkltoHZQL1OHqRM0dZolzZsWQ8ukLaPV0JppZ2n3aC/pdLoJ3YMeRZfQl9Jr6Afp5+mD9HcMDYYNg8dIYigZaxl7GacYtxkvmUymBdOXmchUMNcyG5lnmA+Yb1VYKvYqfBWRyhKVOpVWlX6V56pUVXNVP9V5qgtUq1UPq15WfaZGVbNQ46kJ1Bar1akdVbupNq7OUndSj1DPUV+jvl/9gvpjDbKGhUaghkijVGO3xhmNIRbGMmXxWELWclYD6yxrmE1iW7L57Ex2Bfsbdi97TFNDc6pmrGaRZp3mcc0BDsax4PA52ZxKziHODc57LQMtPy2x1mqtZq1+rTfaetq+2mLtcu0W7eva73VwnUCdLJ31Om0693UJuja6UbqFutt1z+o+02PreekJ9cr1Dund0Uf1bfSj9Rfq79bv0R83MDQINpAZbDE4Y/DMkGPoa5hpuNHwhOGoEctoupHEaKPRSaMnuCbuh2fjNXgXPmasbxxirDTeZdxrPGFiaTLbpMSkxeS+Kc2Ua5pmutG003TMzMgs3KzYrMnsjjnVnGueYb7ZvNv8jYWlRZzFSos2i8eW2pZ8ywWWTZb3rJhWPlZ5VvVW16xJ1lzrLOtt1ldsUBtXmwybOpvLtqitm63Edptt3xTiFI8p0in1U27aMez87ArsmuwG7Tn2YfYl9m32zx3MHBId1jt0O3xydHXMdmxwvOuk4TTDqcSpw+lXZxtnoXOd8zUXpkuQyxKXdpcXU22niqdun3rLleUa7rrStdP1o5u7m9yt2W3U3cw9xX2r+00umxvJXcM970H08PdY4nHM452nm6fC85DnL152Xlle+70eT7OcJp7WMG3I28Rb4L3Le2A6Pj1l+s7pAz7GPgKfep+Hvqa+It89viN+1n6Zfgf8nvs7+sv9j/i/4XnyFvFOBWABwQHlAb2BGoGzA2sDHwSZBKUHNQWNBbsGLww+FUIMCQ1ZH3KTb8AX8hv5YzPcZyya0RXKCJ0VWhv6MMwmTB7WEY6GzwjfEH5vpvlM6cy2CIjgR2yIuB9pGZkX+X0UKSoyqi7qUbRTdHF09yzWrORZ+2e9jvGPqYy5O9tqtnJ2Z6xqbFJsY+ybuIC4qriBeIf4RfGXEnQTJAntieTE2MQ9ieNzAudsmjOc5JpUlnRjruXcorkX5unOy553PFk1WZB8OIWYEpeyP+WDIEJQLxhP5aduTR0T8oSbhU9FvqKNolGxt7hKPJLmnVaV9jjdO31D+miGT0Z1xjMJT1IreZEZkrkj801WRNberM/ZcdktOZSclJyjUg1plrQr1zC3KLdPZisrkw3keeZtyhuTh8r35CP5c/PbFWyFTNGjtFKuUA4WTC+oK3hbGFt4uEi9SFrUM99m/ur5IwuCFny9kLBQuLCz2Lh4WfHgIr9FuxYji1MXdy4xXVK6ZHhp8NJ9y2jLspb9UOJYUlXyannc8o5Sg9KlpUMrglc0lamUycturvRauWMVYZVkVe9ql9VbVn8qF5VfrHCsqK74sEa45uJXTl/VfPV5bdra3kq3yu3rSOuk626s91m/r0q9akHV0IbwDa0b8Y3lG19tSt50oXpq9Y7NtM3KzQM1YTXtW8y2rNvyoTaj9nqdf13LVv2tq7e+2Sba1r/dd3vzDoMdFTve75TsvLUreFdrvUV99W7S7oLdjxpiG7q/5n7duEd3T8Wej3ulewf2Re/ranRvbNyvv7+yCW1SNo0eSDpw5ZuAb9qb7Zp3tXBaKg7CQeXBJ9+mfHvjUOihzsPcw83fmX+39QjrSHkr0jq/dawto22gPaG97+iMo50dXh1Hvrf/fu8x42N1xzWPV56gnSg98fnkgpPjp2Snnp1OPz3Umdx590z8mWtdUV29Z0PPnj8XdO5Mt1/3yfPe549d8Lxw9CL3Ytslt0utPa49R35w/eFIr1tv62X3y+1XPK509E3rO9Hv03/6asDVc9f41y5dn3m978bsG7duJt0cuCW69fh29u0XdwruTNxdeo94r/y+2v3qB/oP6n+0/rFlwG3g+GDAYM/DWQ/vDgmHnv6U/9OH4dJHzEfVI0YjjY+dHx8bDRq98mTOk+GnsqcTz8p+Vv9563Or59/94vtLz1j82PAL+YvPv655qfNy76uprzrHI8cfvM55PfGm/K3O233vuO+638e9H5ko/ED+UPPR+mPHp9BP9z7nfP78L/eE8/stRzjPAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAJcEhZcwAACxMAAAsTAQCanBgAAAIxSURBVFiF1dixS1tRFMfxT16fuomDHcQs4h8giMVFXPo/lDpI5y710T+gf0K5dOlcuhUKHZ2Di7QInUW6JDjYIXSLDulwk5Imz5f3EpO0P8jy3rn3fDnv3HNzTq3b7RpWlmVL2MUeDlDHFpaxipWRRfnq4Bfavd8VzvAd5yGEu+EFtWGgLMs28ArH2CzpuIo6+IlPeBtCaOYC9aJyiA8zAsnTDY7Q6EcrGXh5iPdzhIHHPZ+H/Qe1brcry7I6LnoGi1ALT0II14/a7fYS3uDpgmCIB+Xu9PS0kWIfz4qsQwi1h/KcZdnosY46xpcEO1ifB8yY/Taxl4h1pmxdmbUOUmyXtS4Id6EqRLmeYm0GG0+qrUQFoIfQmCgvJ+KR+xdgYDVVIaGLNhz3OUvm30paFqas42lVGaiKJjmVlYGyLOsWRWnS0jAx0KDTPLBpc+n/yqE5Xap/aaIcGn42CH6f4zI2EwENb17l3aDNfVCVgOZwl0nFLmBstR533IvWVTDvpGLfNPJfOs/5tPlRwu5Xgtti6Gq6L4olo9tO8cMDtz5T5Fo7QXOc1bTXQYX9rhKx157aQVnoArsOzmonJyf7+Gy+HWueWnieiB3rxwXDEIcP5/1WegNfLS5KN9gNITQTCCFc4wUuFwBziaP+WGZw+tHAS5F2Xmr1fDb6D/IGVnW8Fvv9dbPpalti3r7rfZ0/GgHqQS2JQ4gdsdXeFvu3NZON9G7FAtwUy8w3XOSN9H4DKHngjnga2TgAAAAASUVORK5CYII=) 50% / 18px 18px no-repeat !important; } .ChatToolBar-DanmakuTail-tip-active, .ChatToolBar-DanmakuTail-tip:hover { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAMAAADW3miqAAAAPFBMVEUAAAD6aDP7ZTD2cUHxeU/pjGvmkHPfjnH4azj4bDruf1fshWHrhmLpjW3njm3kk3/ugFnugVn/XSP+YCg/+VboAAAAEnRSTlMA/P3z4XQyCPr5za2namIOx8bnayGzAAAA3klEQVQ4y43U2w6EIAxF0dNyc7xP+///Ook6EEHA/UTMSukDEUdhna0hVokpD8ZOy4aY/5I8pTT6iwRHUo1dOJD7SKOPO+5iaUYe2EbptAcs/4VQdo1aMWk0NTXDShdZmITkVkIGQ38SgRtIzhhaR3KlkGKnaGI3hFgHJVFBkkQVFfvXrmsivFk8fc0OJUJeiYAa0sreklJwtpKUJwY9INwPA0yOyksNbP4un57vXD7ezOiElaQTLQh7D40b4Duj2L/9YSA4bizkAs78SPpMdo/YtkzWDPffIRk7r+eYH6dAOJwNbfZEAAAAAElFTkSuQmCC) 50% / 18px 18px no-repeat !important; } .ChatToolBar-DanmakuTail-Panel { position: fixed !important; width: 270px !important; border-radius: var(--dx-radius-14) !important; background: rgba(255, 255, 255, 0.78) !important; backdrop-filter: blur(36px) saturate(220%) !important; -webkit-backdrop-filter: blur(36px) saturate(220%) !important; border: 1px solid rgba(255, 255, 255, 0.95) !important; box-shadow: inset 0 1px 1.5px rgba(255, 255, 255, 0.95), 0 16px 40px rgba(15, 23, 42, 0.16) !important; padding: 12px 14px !important; box-sizing: border-box !important; z-index: 100050 !important; display: none; color: var(--dx-text-title) !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; user-select: none !important; } .DanmakuTail-input { width: 100% !important; height: 32px !important; border-radius: var(--dx-radius-8) !important; border: 1px solid rgba(0, 0, 0, 0.12) !important; background: rgba(255, 255, 255, 0.85) !important; padding: 4px 8px !important; font-size: var(--dx-font-12) !important; color: var(--dx-text-title) !important; outline: none !important; box-sizing: border-box !important; margin-bottom: 8px !important; } .DanmakuTail-input:focus { border-color: var(--dx-accent-deep) !important; /* S4:深蓝墨(玻璃上 ≈5.9:1),亮蓝不再当文字 */ box-shadow: 0 0 0 2px var(--miuix-blue-glow) !important; } .DanmakuTail-checkbox-label { display: flex !important; align-items: center !important; justify-content: space-between !important; font-size: var(--dx-font-12) !important; font-weight: 600 !important; color: var(--dx-text-title) !important; cursor: pointer !important; border-top: 1px solid rgba(0, 0, 0, 0.05) !important; padding-top: 8px !important; } .ChatToolBar-DanmakuTail-Panel.miuix-modal-in { animation: miuix-modal-in 0.18s cubic-bezier(0.16, 1, 0.3, 1) forwards !important; } .DanmakuTail-header { display: flex !important; align-items: center !important; justify-content: space-between !important; margin-bottom: 8px !important; } .DanmakuTail-title { font-size: var(--dx-font-14) !important; font-weight: 700 !important; color: var(--dx-text-title) !important; letter-spacing: -0.2px !important; } .DanmakuTail-close { font-size: var(--dx-font-16) !important; line-height: 1 !important; color: var(--dx-text-body) !important; cursor: pointer !important; width: 20px !important; height: 20px !important; border-radius: var(--dx-radius-6) !important; display: flex !important; align-items: center !important; justify-content: center !important; transition: all 0.16s ease !important; } .DanmakuTail-close:hover { background: rgba(0, 0, 0, 0.08) !important; color: var(--dx-text-title) !important; } /* ========================================================================== 13. PostbirdAlertBox 通用提示/确认/输入弹框 (MIUIX 统一居中模态设计系统) ========================================================================== */ .postbird-box-container { width: 100vw !important; height: 100vh !important; position: fixed !important; top: 0 !important; left: 0 !important; z-index: 100060 !important; background: rgba(15, 23, 42, 0.42) !important; backdrop-filter: blur(8px) !important; -webkit-backdrop-filter: blur(8px) !important; display: none; align-items: center !important; justify-content: center !important; user-select: none !important; } .postbird-box-container.active { display: flex !important; } .postbird-box-dialog { display: flex !important; align-items: center !important; justify-content: center !important; width: 100% !important; height: 100% !important; } .postbird-box-content { min-width: 360px !important; max-width: 440px !important; border-radius: var(--dx-radius-22) !important; background: rgba(255, 255, 255, 0.78) !important; backdrop-filter: blur(40px) saturate(220%) !important; -webkit-backdrop-filter: blur(40px) saturate(220%) !important; border: 1px solid rgba(255, 255, 255, 0.95) !important; box-shadow: inset 0 1px 1.5px rgba(255, 255, 255, 0.98), 0 20px 50px rgba(15, 23, 42, 0.2) !important; overflow: hidden !important; position: relative !important; display: flex !important; flex-direction: column !important; animation: miuix-modal-in 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; margin: 0 !important; top: 0 !important; left: 0 !important; transform: none !important; } .postbird-box-header { width: 100% !important; padding: 12px 16px !important; box-sizing: border-box !important; border-bottom: 1px solid rgba(0, 0, 0, 0.06) !important; background: rgba(255, 255, 255, 0.45) !important; display: flex !important; align-items: center !important; justify-content: space-between !important; } .postbird-box-title { font-size: var(--dx-font-14) !important; font-weight: 700 !important; color: var(--dx-text-title) !important; letter-spacing: -0.2px !important; } .postbird-box-close-btn { cursor: pointer !important; font-size: var(--dx-font-16) !important; line-height: 1 !important; color: var(--dx-text-body) !important; width: 24px !important; height: 24px !important; border-radius: var(--dx-radius-6) !important; display: flex !important; align-items: center !important; justify-content: center !important; transition: all 0.16s ease !important; } .postbird-box-close-btn:hover { background: rgba(0, 0, 0, 0.08) !important; color: var(--dx-text-title) !important; } .postbird-box-text { padding: 16px 20px !important; font-size: var(--dx-font-14) !important; color: #334155 !important; line-height: 1.5 !important; text-align: left !important; box-sizing: border-box !important; } .postbird-prompt-input { width: 100% !important; height: 36px !important; border-radius: var(--dx-radius-8) !important; border: 1px solid rgba(0, 0, 0, 0.14) !important; background: rgba(255, 255, 255, 0.9) !important; padding: 6px 10px !important; font-size: var(--dx-font-12) !important; color: var(--dx-text-title) !important; outline: none !important; box-sizing: border-box !important; margin-top: 8px !important; transition: all 0.18s ease !important; } .postbird-prompt-input:focus { border-color: var(--dx-accent-deep) !important; /* S4:深蓝墨(玻璃上 ≈5.9:1),亮蓝不再当文字 */ box-shadow: 0 0 0 2px var(--miuix-blue-glow) !important; } .postbird-box-footer { width: 100% !important; height: 46px !important; border-top: 1px solid rgba(0, 0, 0, 0.06) !important; background: rgba(255, 255, 255, 0.45) !important; display: flex !important; align-items: center !important; justify-content: flex-end !important; gap: 8px !important; padding: 0 16px !important; box-sizing: border-box !important; position: relative !important; } .postbird-box-footer .btn-footer { height: 32px !important; border-radius: var(--dx-radius-8) !important; padding: 0 14px !important; font-size: var(--dx-font-12) !important; font-weight: 600 !important; cursor: pointer !important; border: none !important; outline: none !important; transition: all 0.16s ease !important; } .postbird-box-footer .btn-footer-ok { background: linear-gradient(135deg, #2b7fff, #0055ff) !important; color: #ffffff !important; box-shadow: 0 2px 6px var(--miuix-blue-glow) !important; } .postbird-box-footer .btn-footer-ok:hover { background: linear-gradient(135deg, #3d8bff, #004de6) !important; transform: translateY(-1px) !important; } .postbird-box-footer .btn-footer-cancel { background: rgba(0, 0, 0, 0.05) !important; color: #475569 !important; } .postbird-box-footer .btn-footer-cancel:hover { background: rgba(0, 0, 0, 0.1) !important; color: var(--dx-text-title) !important; } /* ========================================================================== 14. 版本更新控制台多态操作按钮状态微交互 ========================================================================== */ .exupdate-state-btn--ack, .exupdate-state-btn--check { background: linear-gradient(135deg, #1d5fd1, #0047b3) !important; /* S4:白字 5.9–7.6 */ color: #ffffff !important; box-shadow: 0 4px 14px rgba(0, 102, 255, 0.35) !important; } .exupdate-state-btn--ack:hover, .exupdate-state-btn--check:hover { background: linear-gradient(135deg, #174fb0, #003d99) !important; box-shadow: 0 6px 18px rgba(0, 102, 255, 0.45) !important; transform: translateY(-1px) !important; } .exupdate-state-btn--checking { background: #475569 !important; /* S4:白字 5.6 */ color: #ffffff !important; box-shadow: none !important; cursor: wait !important; } .exupdate-state-btn--latest { background: linear-gradient(135deg, #0b7a57, #047857) !important; /* S4:白字 ≥4.6 */ color: #ffffff !important; box-shadow: 0 4px 14px rgba(16, 185, 129, 0.3) !important; } .exupdate-state-btn--latest:hover { background: linear-gradient(135deg, #06654a, #065f46) !important; transform: translateY(-1px) !important; } .exupdate-state-btn--upgrade { background: linear-gradient(135deg, #a14a08, #92400e) !important; /* S4:白字 ≥5.3 */ color: #ffffff !important; box-shadow: 0 4px 14px rgba(245, 158, 11, 0.4) !important; } .exupdate-state-btn--upgrade:hover { background: linear-gradient(135deg, #92400e, #7c2d12) !important; transform: translateY(-1px) !important; } .exupdate-state-btn--error { background: linear-gradient(135deg, #b91c1c, #991b1b) !important; /* S4:白字 ≥4.5 */ color: #ffffff !important; box-shadow: 0 4px 14px rgba(239, 68, 68, 0.3) !important; } .exupdate-state-btn--error:hover { background: linear-gradient(135deg, #991b1b, #7f1d1d) !important; transform: translateY(-1px) !important; } /* ========================================================================== 15. 面板专有控件(原 ExpandTool.css / BarrageLoopStyle.css 独有声明上收) ========================================================================== */ .extool input[type="text"]:focus, .extool input[type="number"]:focus { border-color: var(--dx-accent-deep) !important; /* S4:深蓝墨(玻璃上 ≈5.9:1),亮蓝不再当文字 */ box-shadow: 0 0 0 2px var(--miuix-blue-glow) !important; } .bloop__preset_row { display: flex !important; align-items: center !important; gap: 8px !important; margin-bottom: 8px !important; } .bloop__preset_row select#bloop__select { flex: 1 1 auto !important; height: 28px !important; border-radius: var(--dx-radius-8) !important; border: 1px solid rgba(0, 0, 0, 0.12) !important; /* 用 background-color 而非 background 简写,避免重置统一引擎自绘的下拉箭头 */ background-color: rgba(255, 255, 255, 0.85) !important; font-size: var(--dx-font-12) !important; color: var(--dx-text-title) !important; outline: none !important; padding: 0 8px !important; } .bloop__preset_row input[type="button"]#bloop__save { flex-shrink: 0 !important; height: 28px !important; padding: 0 10px !important; font-size: var(--dx-font-115) !important; font-weight: 600 !important; color: var(--dx-accent-deep) !important; background: rgba(255, 255, 255, 0.92) !important; border: 1px solid rgba(15, 23, 42, 0.14) !important; border-radius: var(--dx-radius-6) !important; cursor: pointer !important; box-shadow: 0 2px 6px rgba(0, 102, 255, 0.28) !important; transition: all 0.16s ease !important; }.bloop__preset_row input[type="button"]#bloop__save:hover { background: #ffffff !important; transform: none !important; background-image: none !important; border-color: rgba(0, 71, 179, 0.35) !important; }.bloop__preset_row input[type="button"]#bloop__delete { flex-shrink: 0 !important; height: 28px !important; padding: 0 10px !important; font-size: var(--dx-font-115) !important; font-weight: 600 !important; color: var(--dx-state-danger) !important; background: rgba(255, 255, 255, 0.92) !important; border: 1px solid rgba(185, 28, 28, 0.28) !important; border-radius: var(--dx-radius-8) !important; cursor: pointer !important; transition: all 0.16s ease !important; background-image: none !important; box-shadow: none !important; } .bloop__preset_row input[type="button"]#bloop__delete:hover { background: #ef4444 !important; color: #ffffff !important; transform: translateY(-1px) !important; } .bloop textarea#bloop__textarea { width: 100% !important; height: 72px !important; box-sizing: border-box !important; border-radius: var(--dx-radius-8) !important; border: 1px solid rgba(0, 0, 0, 0.12) !important; background: rgba(255, 255, 255, 0.75) !important; padding: 6px 8px !important; font-size: var(--dx-font-12) !important; color: var(--dx-text-title) !important; line-height: 1.4 !important; outline: none !important; resize: none !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; transition: all 0.18s ease !important; } .bloop textarea#bloop__textarea:focus { border-color: var(--dx-accent-deep) !important; /* S4:深蓝墨(玻璃上 ≈5.9:1),亮蓝不再当文字 */ background: rgba(255, 255, 255, 0.95) !important; box-shadow: 0 0 0 2px var(--miuix-blue-glow) !important; } .bloop textarea#bloop__textarea:disabled { background: rgba(0, 0, 0, 0.05) !important; color: var(--dx-text-body) !important; cursor: not-allowed !important; } .bloop__param_grid { display: grid !important; grid-template-columns: 1fr 1fr !important; gap: 8px 12px !important; }.bloop__param_item { display: flex !important; align-items: center !important; gap: 6px !important; font-size: var(--dx-font-115) !important; color: #334155 !important; flex-wrap: nowrap !important; row-gap: var(--dx-space-2); } /* ========================================================================== 16. 全站抽奖信息房间卡片(原 Lottery.css 独有声明上收) ========================================================================== */ .lottery__item { display: flex !important; padding: 8px 6px !important; border-bottom: 1px solid rgba(0, 0, 0, 0.05) !important; color: var(--dx-text-title) !important; align-items: center !important; gap: 8px !important; text-decoration: none !important; transition: all 0.16s ease !important; } .lottery__a { text-decoration: none !important; display: block !important; } .lottery__a:hover .lottery__item { background: rgba(255, 255, 255, 0.7) !important; border-radius: var(--dx-radius-8) !important; transform: translateY(-1px) !important; } .lottery__img { position: relative !important; width: 90px !important; height: 54px !important; flex-shrink: 0 !important; } .lottery__img img { width: 90px !important; height: 54px !important; object-fit: cover !important; border-radius: var(--dx-radius-6) !important; display: block !important; } .lottery__anchor { position: absolute !important; top: 2px !important; left: 2px !important; background: rgba(0, 0, 0, 0.55) !important; color: #fff !important; font-size: var(--dx-font-11) !important; padding: 1px 4px !important; border-radius: var(--dx-radius-6) !important; max-width: 80px !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; } .lottery__expireTime { position: absolute !important; bottom: 2px !important; right: 2px !important; background: rgba(0, 0, 0, 0.55) !important; color: #fff !important; font-size: var(--dx-font-11) !important; padding: 1px 4px !important; border-radius: var(--dx-radius-6) !important; } .lottery__info { display: flex !important; flex-direction: column !important; gap: 2px !important; overflow: hidden !important; flex: 1 1 auto !important; } .lottery__prize { font-size: var(--dx-font-12) !important; font-weight: 700 !important; color: var(--dx-text-title) !important; white-space: nowrap !important; text-overflow: ellipsis !important; overflow: hidden !important; } .lottery__jointext { font-size: var(--dx-font-11) !important; color: var(--dx-text-body) !important; white-space: nowrap !important; text-overflow: ellipsis !important; overflow: hidden !important; } .lottery__condition { font-size: var(--dx-font-115) !important; font-weight: 600 !important; } /* ========================================================================== 17. 下拉框与数字输入框的原生外观抹除(MIUIX 自绘控件) 浏览器不允许自定义 select 展开后的选项列表,此处仅统一「收起态」外观: appearance:none 抹掉系统边框/箭头,改用内联 SVG 自绘澎湃箭头 + 磨砂底 + 聚焦环。 ========================================================================== */ .miuix-modal select, .sign-panel select, .fans-continue-panel select, .extool select, .livetool select, .bloop select, .popup-player-panel select, .exlottery select, .exupdate-panel select { -webkit-appearance: none !important; appearance: none !important; padding-right: 22px !important; background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%2364748b' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") !important; background-repeat: no-repeat !important; background-position: right 7px center !important; background-size: 10px 6px !important; cursor: pointer !important; text-overflow: ellipsis !important; } .miuix-modal select:hover, .sign-panel select:hover, .fans-continue-panel select:hover, .extool select:hover, .livetool select:hover, .bloop select:hover { border-color: rgba(0, 102, 255, 0.35) !important; } .miuix-modal select:focus, .sign-panel select:focus, .fans-continue-panel select:focus, .extool select:focus, .livetool select:focus, .bloop select:focus, .popup-player-panel select:focus, .exlottery select:focus, .exupdate-panel select:focus { border-color: var(--dx-accent-deep) !important; /* S4:深蓝墨(玻璃上 ≈5.9:1),亮蓝不再当文字 */ box-shadow: 0 0 0 2px var(--miuix-blue-glow) !important; background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%230066ff' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") !important; } /* 数字输入的上下微调箭头:磨砂面板里露出系统控件会破坏质感,统一抹除 */ .miuix-modal input[type="number"]::-webkit-outer-spin-button, .miuix-modal input[type="number"]::-webkit-inner-spin-button, .extool input[type="number"]::-webkit-outer-spin-button, .extool input[type="number"]::-webkit-inner-spin-button, .livetool input[type="number"]::-webkit-outer-spin-button, .livetool input[type="number"]::-webkit-inner-spin-button, .fans-continue-panel input[type="number"]::-webkit-outer-spin-button, .fans-continue-panel input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none !important; appearance: none !important; margin: 0 !important; } .miuix-modal input[type="number"], .extool input[type="number"], .livetool input[type="number"], .fans-continue-panel input[type="number"] { -moz-appearance: textfield !important; text-align: center !important; } /* ========================================================================== 18. 主播配置信息悬浮卡(原 MetaData.css 独有声明上收 + MIUIX 重制) 原实现为 2020 年的死白样式(1px #e5e5e5 边框 / 纯白底 / 硬编码 left:199px), 此处换装为与全站一致的磨砂玻璃卡,并补上三种状态的提示样式。 ========================================================================== */ #ex-metadata { position: relative !important; cursor: pointer !important; } #ex-metadata > .metadata__wrap { display: none; } #ex-metadata:hover > .metadata__wrap { display: block !important; } .metadata__wrap { position: absolute !important; left: 100% !important; top: 0 !important; margin: 0 0 0 6px !important; padding: 8px 10px !important; min-width: 132px !important; max-width: 240px !important; box-sizing: border-box !important; list-style: none !important; border-radius: var(--dx-radius-14) !important; background: rgba(255, 255, 255, 0.78) !important; backdrop-filter: blur(28px) saturate(200%) !important; -webkit-backdrop-filter: blur(28px) saturate(200%) !important; border: 1px solid rgba(255, 255, 255, 0.95) !important; box-shadow: inset 0 1px 1.5px rgba(255, 255, 255, 0.95), 0 12px 30px rgba(15, 23, 42, 0.18) !important; z-index: 100010 !important; color: var(--dx-text-title) !important; font-size: var(--dx-font-115) !important; line-height: 1.45 !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif !important; white-space: normal !important; word-break: break-all !important; user-select: none !important; } .metadata__wrap > li { padding: 3px 0 !important; color: var(--dx-text-title) !important; } .metadata__wrap > li + li { border-top: 1px solid rgba(0, 0, 0, 0.05) !important; } /* 「探测失败 重试」提示中的动作文字 */ .metadata__retry { font-style: normal !important; margin-left: 4px !important; font-weight: 600 !important; color: var(--dx-accent-deep) !important; /* S4:深蓝墨(玻璃上 ≈5.9:1),亮蓝不再当文字 */ } /* ========================================================================== 20. 多屏(1:1 复刻斗鱼原生「多屏」) 格子区不是模态:它直接改造页面自带的 #js-player-multiContainer 与 5 个槽位 (槽 0 就是斗鱼原生播放器本体),所以本节样式全部以 .layout-Player-multiPlayer / .layout-Player-videoEntity 为落点,用 ms- 前缀区分自有节点。 几何(top/left/width/height 百分比与 z 序)由引擎写内联样式,因为原生靠 .is-multiN 后代规则驱动,宿主样式表缺失时会整块失灵;内联更稳且取值与原生完全一致。 本节只负责"格子里面长什么样":房名胶囊、加载层、四态文案、拖拽落点与占位块。 注意:本节禁止写死模态宽高(产物级断言认定模态尺寸只能声明一次), 也禁止出现反引号与美元花括号插值(CSS 会被塞进模板字符串,插值起始符会直接截断它)。 ========================================================================== */ /* 槽位内视频:撑满并保持比例(原生只给 canvas/video 打了 100% 尺寸) */ .layout-Player-multiContainer .layout-Player-multiPlayer > video.ms-slot__video { width: 100% !important; height: 100% !important; object-fit: contain !important; display: block !important; background: #000 !important; } /* 换档过渡层:新流先叠在旧画面之上(透明)起播,出画面后再淡入、随后旧画面被移除。 于是换清晰度看到的是"画面轻轻跳一下",而不是黑屏 + 转圈。 */ .layout-Player-multiContainer .layout-Player-multiPlayer > video.ms-slot__video--next { position: absolute !important; top: 0 !important; left: 0 !important; opacity: 0 !important; z-index: 2 !important; transition: opacity 0.25s ease !important; } .layout-Player-multiContainer .layout-Player-multiPlayer > video.ms-slot__video--next.is-live { opacity: 1 !important; } /* 房名胶囊:类名与取值 1:1 对齐原生 .nameAndIcon / .nameIcon / .nameTxt */ .layout-Player-multiContainer .ms-slot__name { position: absolute !important; left: 10px !important; bottom: 20px !important; z-index: 2 !important; display: flex !important; align-items: center !important; max-width: calc(100% - 20px) !important; padding: 2px !important; background: rgba(0, 0, 0, 0.6) !important; border-radius: var(--dx-radius-14) !important; font-size: var(--dx-font-14) !important; color: #fff !important; cursor: pointer !important; } .layout-Player-multiContainer .ms-slot__name-icon { flex: 0 0 auto !important; width: 20px !important; height: 20px !important; border-radius: var(--dx-radius-22) !important; background-repeat: no-repeat !important; background-size: cover !important; background-position: center !important; background-color: #444 !important; } .layout-Player-multiContainer .ms-slot__name-text { margin: 0 4px !important; max-width: 120px !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; } /* 加载层:原生 .loadingView 是 #333 满铺 + 居中 flex */ .layout-Player-multiContainer .ms-slot__loading { position: absolute !important; left: 0 !important; top: 0 !important; width: 100% !important; height: 100% !important; z-index: 3 !important; display: none !important; align-items: center !important; justify-content: center !important; background: #333 !important; } .layout-Player-multiContainer .ms-slot--loading > .ms-slot__loading, .layout-Player-multiContainer .ms-slot--notice > .ms-slot__loading { display: flex !important; } /* 文案态:不整块盖住画面,只压一层暗底,让已经出画/黑屏的格子都还能看清 */ .layout-Player-multiContainer .ms-slot--notice > .ms-slot__loading { background: rgba(0, 0, 0, 0.55) !important; } .layout-Player-multiContainer .ms-slot__loading-inner { width: 100% !important; text-align: center !important; } /* 转圈:原生用 220x128 雪碧图 steps(19);该图是构建期哈希资源无法稳定引用, 这里用同占位的纯 CSS 环代替,时长同为 1.5s,文案与层级完全一致 */ .layout-Player-multiContainer .ms-slot__spin { width: 34px !important; height: 34px !important; margin: 0 auto 18px !important; border: 3px solid rgba(255, 255, 255, 0.16) !important; border-top-color: #fff !important; border-radius: 50% !important; animation: ms-slot-spin 1.5s linear infinite !important; } .layout-Player-multiContainer .ms-slot--notice .ms-slot__spin { display: none !important; } @keyframes ms-slot-spin { to { transform: rotate(360deg); } } .layout-Player-multiContainer .ms-slot__tip { margin: auto !important; color: #ccc !important; font-size: var(--dx-font-14) !important; text-align: center !important; } /* 「重新加载」:原生 .reload 的 86x34 / 2px 橙边 / hover 反色,3 秒后才出现 */ .layout-Player-multiContainer .ms-slot__reload { display: none !important; width: 86px !important; height: 34px !important; margin: 20px auto 0 !important; line-height: 34px !important; font-size: var(--dx-font-14) !important; color: #ff5d23 !important; background: transparent !important; border: 2px solid #ff5d23 !important; border-radius: var(--dx-radius-8) !important; cursor: pointer !important; user-select: none !important; transition: none !important; } .layout-Player-multiContainer .ms-slot__reload.is-visible { display: block !important; } .layout-Player-multiContainer .ms-slot__reload:hover { background: #ff5d23 !important; color: #fff !important; } /* 拖拽落点高亮:原生只挂 is-cur 而样式表只定义了 .cur(全站 372 份样式表 0 命中), 即原生高亮是失效的。这里两个类都接,并加内发光,小格子上也能一眼看清落点。 */ .layout-Player-multiContainer .layout-Player-multiPlayer.cur, .layout-Player-multiContainer .layout-Player-multiPlayer.is-cur { border: 2px solid #ff5d23 !important; box-shadow: inset 0 0 0 1px rgba(255, 93, 35, 0.45), 0 0 12px rgba(255, 93, 35, 0.35) !important; } /* 占位块:被拖起后留在原位的空框(原生 #333 + 2px 白边 + pointer-events:none)。 它刻意不带 is-multiN 类,所以必须自己把基础态的 scale(0) 与 hidden 放开。 */ .layout-Player-multiContainer .layout-Player-multiPlayer.ms-slot--placeholder { background: #333 !important; border: 2px solid #fff !important; box-sizing: border-box !important; pointer-events: none !important; transform: scale(1) !important; visibility: visible !important; } /* 被拖起的那一格:原生 .dragging-player 已给到 416x234 + translate(-50%,-50%), 这里只补层级与光标,避免拖到控制条附近时被压住 */ .layout-Player-multiContainer .dragging-player.ms-slot--dragging { z-index: 99 !important; cursor: grabbing !important; } /* ========================================================================== 22. 同屏播放面板的「多屏观看」页 面板本体是浅色玻璃(--miuix-modal-bg 为白),所以这一节全部走浅色口径; 格子区压在视频上,用深色口径。两套不混用。 ========================================================================== */ .popup-panel__page[hidden] { display: none !important; } /* 同屏里的房间数量:跟在标题后面的小胶囊 */ .popup-ms__count { margin-left: 6px !important; padding: 1px 6px !important; font-style: normal !important; font-size: var(--dx-font-12) !important; font-weight: 700 !important; color: var(--dx-accent-deep) !important; /* S4:深蓝墨(玻璃上 ≈5.9:1),亮蓝不再当文字 */ background: rgba(0, 102, 255, 0.1) !important; border-radius: var(--dx-radius-6) !important; } /* 页面整体可滚:房间多时不会把内容顶出面板;纵向 flex 让底部操作区能粘住 */ .popup-panel__page { display: flex !important; flex-direction: column !important; gap: 12px !important; overflow-y: auto !important; overflow-x: hidden !important; } /* 面板内容高于面板时,让页面自己滚(否则底部会被裁掉:实测 5 间房时内容 437px > 面板高度) */ .popup-player-panel .miuix-modal__body { display: flex !important; flex-direction: column !important; min-height: 0 !important; }.popup-player-panel .popup-panel__page { flex: 1 1 auto; min-height: 0; } /* 与第 4 节的全局滚动条同一口径(4px / rgba(0,0,0,0.18) / 4px 圆角) */ .popup-panel__page::-webkit-scrollbar, .popup-ms__rooms::-webkit-scrollbar, .popup-ms__results::-webkit-scrollbar { width: 4px !important; } .popup-panel__page::-webkit-scrollbar-thumb, .popup-ms__rooms::-webkit-scrollbar-thumb, .popup-ms__results::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.18) !important; border-radius: var(--dx-radius-6) !important; } /* 已加入多屏的列表自己滚,页面其余部分保持不动(外房两行 ≈ 58px/个,五房时滚动看全) */ .popup-ms__rooms { max-height: 168px !important; overflow-y: auto !important; padding-right: 2px !important; } /* 「上次配置 · 一键恢复」一行 */ .popup-ms__restore { display: flex !important; align-items: center !important; gap: 8px !important; margin: 6px 0 2px !important; } .popup-ms__restore .popup-panel__paste-btn { flex: 0 0 auto !important; white-space: nowrap !important; } /* hidden 必须真的收起:上面的 display:flex !important 会压过 UA 样式表的 [hidden], 没有上次配置时那一行仍会白占 8px 版面(对照 .popup-panel__page[hidden] 的写法) */ .popup-ms__restore[hidden] { display: none !important; } .popup-ms__restore-text { flex: 1 1 auto !important; min-width: 0 !important; font-size: var(--dx-font-11) !important; color: rgba(15, 23, 42, 0.6) !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; } .popup-ms__rooms, .popup-ms__results { display: flex !important; flex-direction: column !important; gap: 6px !important; } .popup-ms__results { max-height: 132px !important; overflow-y: auto !important; overscroll-behavior: contain !important; } /* 已加入的每一行。外房是两行:第一行房名/房号/移除,第二行是"怎么合进来" (合弹幕 · 合画面 · 档位角标 · 音量);主房间只有第一行(它没有"合入"一说)。 单行时 5px+20px+5px = 30px,与改造前完全等高。 */ .popup-ms__room { display: flex !important; flex-direction: column !important; justify-content: center !important; gap: 4px !important; min-height: 30px !important; padding: 5px 8px !important; box-sizing: border-box !important; background: var(--miuix-item-bg) !important; border: var(--miuix-item-border) !important; border-radius: var(--dx-radius-8) !important; } .popup-ms__room-main { display: flex !important; align-items: center !important; gap: 8px !important; min-height: 20px !important; } .popup-ms__room-sub { display: flex !important; align-items: center !important; gap: 6px !important; } .popup-ms__room-grow { flex: 1 1 auto !important; } /* 每房间的「合入」胶囊(弹幕 / 画面):按下 = 亮蓝 + 实心圆点,弹起 = 灰 + 淡圆点 */ .popup-ms__chip { flex: 0 0 auto !important; height: 20px !important; padding: 0 8px !important; font-size: var(--dx-font-11) !important; line-height: 18px !important; color: rgba(15, 23, 42, 0.55) !important; background: rgba(15, 23, 42, 0.04) !important; border: 1px solid rgba(15, 23, 42, 0.12) !important; border-radius: var(--dx-radius-10) !important; cursor: pointer !important; white-space: nowrap !important; transition: color 0.18s ease, background-color 0.18s ease, border-color 0.18s ease, transform 0.18s var(--miuix-spring) !important; } .popup-ms__chip::before { content: "" !important; display: inline-block !important; width: 5px !important; height: 5px !important; margin-right: 4px !important; vertical-align: 1px !important; border-radius: 50% !important; background: currentColor !important; opacity: 0.4 !important; } .popup-ms__chip[aria-pressed="true"] { color: var(--dx-accent-deep) !important; /* S4:深蓝墨(玻璃上 ≈5.9:1),亮蓝不再当文字 */ background: rgba(0, 102, 255, 0.1) !important; border-color: rgba(0, 102, 255, 0.35) !important; } .popup-ms__chip[aria-pressed="true"]::before { opacity: 1 !important; } .popup-ms__chip:hover { color: var(--dx-text-title) !important; border-color: rgba(15, 23, 42, 0.24) !important; } .popup-ms__chip[aria-pressed="true"]:hover { color: var(--dx-accent-deep) !important; /* S4:深蓝墨(玻璃上 ≈5.9:1),亮蓝不再当文字 */ } .popup-ms__chip:active { transform: scale(0.94) !important; } /* 这个房间此刻用的档位(自动档显示的是"选出来的那一档",缩窗口会跟着变) */ .popup-ms__room-qn { flex: 0 0 auto !important; padding: 1px 6px !important; font-size: var(--dx-font-11) !important; color: var(--dx-accent-deep) !important; /* S4:深蓝墨(玻璃上 ≈5.9:1),亮蓝不再当文字 */ background: rgba(0, 102, 255, 0.08) !important; border-radius: var(--dx-radius-6) !important; white-space: nowrap !important; } /* 外房清晰度:与编辑条顶栏那颗胶囊同一处状态,放这里是"不用先开编辑条也能看见、能改" */ .popup-ms__qrow { display: flex !important; align-items: center !important; gap: 8px !important; margin-top: 8px !important; } .popup-ms__qlabel { flex: 0 0 auto !important; font-size: var(--dx-font-11) !important; color: rgba(15, 23, 42, 0.6) !important; white-space: nowrap !important; } .popup-ms__quality.miuix-seg { flex: 1 1 auto !important; margin: 0 !important; } .popup-ms__quality .miuix-seg__label { padding: 4px 4px !important; font-size: var(--dx-font-11) !important; } .popup-ms__room-name { flex: 1 1 auto !important; min-width: 0 !important; font-size: var(--dx-font-12) !important; color: var(--dx-text-title) !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; } .popup-ms__room-rid { flex: 0 0 auto !important; font-size: var(--dx-font-11) !important; color: rgba(15, 23, 42, 0.55) !important; } .popup-ms__room-del { flex: 0 0 auto !important; width: 20px !important; height: 20px !important; padding: 0 !important; font-size: var(--dx-font-14) !important; line-height: 1 !important; color: rgba(15, 23, 42, 0.5) !important; background: transparent !important; border: none !important; border-radius: 50% !important; cursor: pointer !important; transition: all 0.18s var(--miuix-spring) !important; } .popup-ms__room-del:hover { color: #fff !important; background: #ff5d23 !important; } /* 每房间音量:细轨 + 圆形滑块(MIUIX),已填充部分用 --vol 变量着色 */ .popup-ms__room-vol { flex: 0 0 auto !important; width: 62px !important; height: 16px !important; margin: 0 !important; padding: 0 !important; background: transparent !important; -webkit-appearance: none !important; appearance: none !important; cursor: pointer !important; outline: none !important; } .popup-ms__room-vol::-webkit-slider-runnable-track { height: 4px !important; border-radius: 2px !important; background: linear-gradient(to right, var(--miuix-blue) var(--vol, 0%), rgba(15, 23, 42, 0.16) var(--vol, 0%)) !important; } .popup-ms__room-vol::-webkit-slider-thumb { -webkit-appearance: none !important; appearance: none !important; width: 12px !important; height: 12px !important; margin-top: -4px !important; border-radius: 50% !important; background: #ffffff !important; border: 1px solid rgba(15, 23, 42, 0.18) !important; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.24) !important; transition: transform 0.16s var(--miuix-spring) !important; } .popup-ms__room-vol:hover::-webkit-slider-thumb, .popup-ms__room-vol:active::-webkit-slider-thumb { transform: scale(1.15) !important; } .popup-ms__room-volnum { flex: 0 0 auto !important; width: 24px !important; font-size: var(--dx-font-11) !important; text-align: right !important; color: rgba(15, 23, 42, 0.55) !important; font-variant-numeric: tabular-nums !important; } /* 搜索框撑满卡片宽度:.popup-panel__input-box 原本没有任何样式,input 用的是 浏览器默认宽度(约 169px),卡片右边空一大块。特异性与第 9 节的 .popup-player-panel input[type="text"] 相同,本节在后面,所以这边生效。 */ .popup-panel__input-box { display: flex !important; align-items: center !important; margin-top: 6px !important; }.popup-panel__input-box input[type="text"] { width: 100% !important; height: 28px !important; padding: 4px 10px !important; border-radius: var(--dx-radius-8) !important; background: rgba(255, 255, 255, 0.85) !important; font-size: var(--dx-font-12) !important; box-sizing: border-box !important; flex: 1 1 auto; min-width: 0; } /* 快捷来源按钮并排 */ .popup-ms__src { display: flex !important; gap: 8px !important; margin-top: 8px !important; } .popup-ms__src-btn { flex: 1 1 0 !important; justify-content: center !important; } /* 搜索结果行。行本体是真正的 ' + "" + ""; let sendgiftRow = '
' + '
' + '' + '弱鸡' + '弱鸡' + '›' + "
" + '
' + '数量:' + '' + '间隔:' + '' + '' + "
" + "
"; let cardGift = ExUI.card({ title: "送礼与背包", asideHtml: '点击图标更换', body: ExpandTool_columnItem(ExpandTool_inlineHead("背包道具赠送", "支持道具一键拉取", "font-size: 11.5px; color: #1e293b;") + clearbagRow) + ExpandTool_columnItem(ExpandTool_inlineHead("打榜批量送礼", "全站大盘礼物池", "font-size: 11.5px; color: #1e293b;") + sendgiftRow), legacy: ExpandTool_cardShadow() }); let cardMonitor = ExpandTool_uiAttrs(ExUI.card({ title: "在线工具", asideHtml: '第三方站点', body: ExUI.row({ title: "在线弹幕助手", desc: "在单独网页查看本房间的弹幕数据(第三方数据站 douyuex.com)", trail: ExpandTool_uiAttrs(ExUI.btn("打开", { variant: "quiet", id: "extool__monitor_open", legacy: "ex-picker-btn" }), { title: "在新标签页打开在线弹幕助手" }) }), legacy: ExpandTool_cardShadow() }), { id: "extool__monitor_card" }); a.innerHTML = `
${cardPlayback} ${cardShield} ${cardHang} ${cardGift} ${cardMonitor}
`; let b = document.getElementsByClassName("layout-Player-chat")[0] || document.querySelector(".Barrage-main") || document.body; if (b) b.insertBefore(a, b.childNodes[0]); if ("function" === typeof ensureMiuixPanelHeader) ensureMiuixPanelHeader(a, "扩展功能"); } function ExpandTool_insertIcon() { let a = document.createElement("div"); a.className = "extool-icon", a.innerHTML = ''; let b = document.getElementsByClassName("ex-panel__wrap")[0]; if (b) b.insertBefore(a, b.childNodes[0]); } function initPkg_ExpandTool_Func() { let icon = document.getElementsByClassName("extool-icon")[0]; if (icon) icon.addEventListener("click", function() { showExRightPanel("扩展功能", this); }); } function initPkg_ExpandTool_MonitorLink() { const btn = document.getElementById("extool__monitor_open"); if (!btn) { if ("function" === typeof ExGuard_warn) ExGuard_warn("扩展功能", "未找到在线弹幕助手按钮(#extool__monitor_open),本次不绑定"); return false; } if ("1" === btn.getAttribute("data-extool-monitor-bound")) return true; return btn.setAttribute("data-extool-monitor-bound", "1"), btn.addEventListener("click", function(e) { if (e && "function" === typeof e.stopPropagation) e.stopPropagation(); const roomId = ExpandTool_monitorRoomId(); if (!roomId) return ExpandTool_monitorHint("没拿到有效的房间号,暂时打不开在线弹幕助手", "warning", "rid 为空或不合法,在线弹幕助手无法拼出地址"), void 0; if ("function" !== typeof openPage) return ExpandTool_monitorHint("当前环境不支持打开新标签页,在线弹幕助手打不开", "error", "openPage 不可用,在线弹幕助手打不开"), void 0; try { openPage("https://www.douyuex.com/" + roomId); } catch (err) { ExpandTool_monitorHint("打开在线弹幕助手失败,可稍后重试", "error", "打开在线弹幕助手失败", err); } }), true; } function ExpandTool_monitorRoomId() { if ("undefined" === typeof rid || null === rid || void 0 === rid) return ""; const text = String(rid).trim(); if (!/^[0-9]+$/.test(text)) return ""; return text; } function ExpandTool_monitorHint(message, type, warnText, err) { if ("function" === typeof showMessage) try { return showMessage(message, type || "info"), void 0; } catch (e) {} if ("function" === typeof ExGuard_warn) ExGuard_warn("扩展功能", warnText || message, err); } let exAutoBarrageColorTimer = null; function initPkg_ExpandTool_AutoBarrageColor() { ExpandTool_AutoBarrageColor_insertFunc(), initPkg_ExpandTool_AutoBarrageColor_Set(); } function getAutoBarrageColor() { return document.getElementById("extool__autobarragecolor").checked; } function ExpandTool_AutoBarrageColor_insertFunc() { document.getElementById("extool__autobarragecolor").addEventListener("click", function() { if (saveData_AutoBarrageColor(), getAutoBarrageColor()) selectHighestUnlockedBarrageColor(); }); } function saveData_AutoBarrageColor() { let data = { isAutoBarrageColor: getAutoBarrageColor() }; localStorage.setItem("ExSave_AutoBarrageColor", JSON.stringify(data)); } function initPkg_ExpandTool_AutoBarrageColor_Set() { let ret = localStorage.getItem("ExSave_AutoBarrageColor"); if (null != ret) { let retJson = JSON.parse(ret); if (retJson.isAutoBarrageColor) document.getElementById("extool__autobarragecolor").checked = retJson.isAutoBarrageColor, selectHighestUnlockedBarrageColor(); } } function selectHighestUnlockedBarrageColor() { if (exAutoBarrageColorTimer) clearInterval(exAutoBarrageColorTimer), exAutoBarrageColorTimer = null; let count = 0; let opened = false; exAutoBarrageColorTimer = setInterval(() => { if (count++, count > 100) return clearInterval(exAutoBarrageColorTimer), exAutoBarrageColorTimer = null, void 0; let isMatch = false; let switcher = document.getElementsByClassName("FansBarrageSwitcher")[0]; if (!switcher) switcher = document.getElementsByClassName("MatchSystemFansBarrageSwitcher")[0], isMatch = true; if (!switcher) return; let itemClass = isMatch ? "MatchSystemFansBarrageColor-item" : "FansBarrageColor-item"; if (!opened) return switcher.click(), opened = true, void 0; let items = document.getElementsByClassName(itemClass); if (!items.length) return; let lastUnlocked = null; for (let i = 0; i < items.length; i++) if (!items[i].classList.contains("is-lock")) lastUnlocked = items[i]; if (!lastUnlocked) return; clearInterval(exAutoBarrageColorTimer), exAutoBarrageColorTimer = null, lastUnlocked.click(); }, 500); } let autoFishInfo = []; let baitId = null; let nextFishEndTime = 0; let isFishing = false; let timerAutoFish = 0; const AUTOFISH_REQUEST_TIMEOUT = 15e3; const AUTOFISH_MAX_FAIL = 3; let autoFishFailCount = 0; function AutoFish_requestJson(url, options, tag) { return new Promise(resolve => { let settled = false; const finish = value => { if (settled) return; settled = true, clearTimeout(timer), resolve(value); }; const timer = setTimeout(() => { if ("function" === typeof ExGuard_warn) ExGuard_warn("自动钓鱼", (tag || url) + " 超过 " + AUTOFISH_REQUEST_TIMEOUT + "ms 未返回,按失败处理"); finish(null); }, AUTOFISH_REQUEST_TIMEOUT); let request; try { request = fetch(url, options); } catch (e) { if ("function" === typeof ExGuard_warn) ExGuard_warn("自动钓鱼", (tag || url) + " 请求无法发起", e); return finish(null), void 0; } Promise.resolve(request).then(res => res.json()).then(ret => finish(ret)).catch(err => { if ("function" === typeof ExGuard_warn) ExGuard_warn("自动钓鱼", (tag || url) + " 请求失败", err); finish(null); }); }); } function AutoFish_abort(message) { const checkbox = document.getElementById("extool__autofish_start"); if (checkbox) checkbox.checked = false; if (AutoFish_lockMode(false), saveData_AutoFish(), message) showMessage(message, "error"); return null; } function initPkg_ExpandTool_AutoFish() { ExpandTool_AutoFish_insertFunc(), ExpandTool_AutoFish_Set(); } function ExpandTool_AutoFish_insertFunc() { document.querySelectorAll('input[name="autofish_mode"]').forEach(radio => { radio.addEventListener("change", saveData_AutoFish); }), document.getElementById("extool__autofish_start").addEventListener("click", async () => { saveData_AutoFish(); const isStart = document.getElementById("extool__autofish_start").checked; if (AutoFish_lockMode(isStart), !isStart) return clearTimeout(timerAutoFish), clearInterval(timerAutoFish), void 0; showMessage("【自动钓鱼】开始自动钓鱼", "info"), autoFishInfo = await AutoFish_getFishInfo(); const homepageRes = await AutoFish_getHomepageData(); if (!Array.isArray(autoFishInfo) || !homepageRes) return autoFishInfo = [], AutoFish_abort("【自动钓鱼】活动信息获取失败,请稍后重试"); if (homepageRes.data) { let baitData = (homepageRes.data.baits || []).find(item => item.inUse); if (!baitData) return AutoFish_abort("【自动钓鱼】请设置鱼饵"); baitId = baitData.id; const myCh = homepageRes.data.myCh; if (!myCh) return AutoFish_abort("【自动钓鱼】请设置形象"); } else return AutoFish_abort("【自动钓鱼】未能获取活动信息"); if (saveData_AutoFish(), 0 == homepageRes.data.fishing.stat) isFishing = false, nextFishEndTime = 0; if (1 == homepageRes.data.fishing.stat) isFishing = true, nextFishEndTime = homepageRes.data.fishing.fishEtMs; if (2 == homepageRes.data.fishing.stat) await endFish(), await sleep(1e3); async function fishLoop() { const isStart = document.getElementById("extool__autofish_start")?.checked; if (!isStart) return; try { if (isInFishingTime()) if (isFishing) { const now = new Date().getTime(); if (now > nextFishEndTime) await endFish(); } else { const fishRes = await AutoFish_startFish(); if (!fishRes) { if (autoFishFailCount++, "function" === typeof ExGuard_warn) ExGuard_warn("自动钓鱼", "下杆请求失败(连续 " + autoFishFailCount + " 次)"); if (autoFishFailCount >= AUTOFISH_MAX_FAIL) return AutoFish_abort("【自动钓鱼】连续 " + AUTOFISH_MAX_FAIL + " 次请求失败,已停止"), void 0; } else if (autoFishFailCount = 0, 0 !== fishRes.error) { if (showMessage(`【自动钓鱼】` + fishRes.msg, "error"), console.log(fishRes, "钓鱼失败"), 1001007 == fishRes.error) await endFish(); else if (1005003 == fishRes.error) return AutoFish_abort(null); } else if (fishRes.data && fishRes.data.fishing) isFishing = true, nextFishEndTime = fishRes.data.fishing.fishEtMs; } } catch (err) { console.error("[DouyuEx] 自动钓鱼轮询异常:", err); } finally { const stillRunning = document.getElementById("extool__autofish_start")?.checked; if (stillRunning) timerAutoFish = setTimeout(fishLoop, 1500); } } fishLoop(); }); } async function endFish() { const fishRes = await AutoFish_endFish(); if (!fishRes) { if (autoFishFailCount++, "function" === typeof ExGuard_warn) ExGuard_warn("自动钓鱼", "收杆请求失败(连续 " + autoFishFailCount + " 次)"); if (autoFishFailCount >= AUTOFISH_MAX_FAIL) AutoFish_abort("【自动钓鱼】连续 " + AUTOFISH_MAX_FAIL + " 次请求失败,已停止"); return; } if (autoFishFailCount = 0, 0 !== fishRes.error) { console.log(fishRes, "收杆失败"); const homepageRes = await AutoFish_getHomepageData(); if (homepageRes && homepageRes.data && homepageRes.data.fishing && 0 == homepageRes.data.fishing.stat) isFishing = false, nextFishEndTime = 0; return; } let str = `【自动钓鱼】`; const fish = fishRes.data && fishRes.data.fish ? fishRes.data.fish : null; const fishList = Array.isArray(autoFishInfo) ? autoFishInfo : []; let fishData = fish ? fishList.find(item => item.fishId == fish.id) : null; if (fishData && fish) str += `获得${fishData.name}${fish.wei}斤`; const awards = fishRes.data && Array.isArray(fishRes.data.awards) ? fishRes.data.awards : []; if (awards.length > 0) for (let i = 0; i < awards.length; i++) { let award = awards[i]; str += `${fishData ? "," : ""}获得${award.awardName}x` + award.awardNum; } if ("【自动钓鱼】" !== str) showMessage(str, "success"); isFishing = false; } function AutoFish_getSave() { let data; try { data = JSON.parse(localStorage.getItem("ExSave_AutoFish")); } catch (e) { data = null; } if (!data || "object" !== typeof data) data = {}; if (!Array.isArray(data.rids)) data.rids = []; if (!data.modes || "object" !== typeof data.modes) data.modes = {}; return data; } function AutoFish_lockMode(lock) { document.querySelectorAll('input[name="autofish_mode"]').forEach(r => r.disabled = lock); } function saveData_AutoFish() { let checkbox = document.getElementById("extool__autofish_start"); let modeRadio = document.querySelector('input[name="autofish_mode"]:checked'); if (!checkbox || !modeRadio) return; let value = checkbox.checked; let mode = modeRadio.value; let data = AutoFish_getSave(); if (value) { if (!data.rids.includes(rid)) data.rids.push(rid); data.modes[rid] = mode; } else data.rids = data.rids.filter(item => item !== rid), delete data.modes[rid]; localStorage.setItem("ExSave_AutoFish", JSON.stringify(data)); } function AutoFish_getRids() { return AutoFish_getSave().rids; } function ExpandTool_AutoFish_Set() { let data = AutoFish_getSave(); if (!data.rids.includes(rid)) return; let mode = data.modes && data.modes[rid] ? data.modes[rid] : "all"; const modeRadio = document.querySelector(`input[name="autofish_mode"][value="${mode}"]`); if (!modeRadio) { if ("function" === typeof ExGuard_warn) ExGuard_warn("自动钓鱼", "存储里的模式值非法(" + mode + "),本次不自动开启"); return; } modeRadio.checked = true; const checkbox = document.getElementById("extool__autofish_start"); if (!checkbox) { if ("function" === typeof ExGuard_warn) ExGuard_warn("自动钓鱼", "面板里的开关不存在,本次不自动开启"); return; } checkbox.click(); } function AutoFish_getFishInfo() { return AutoFish_requestJson(`https://www.douyu.com/japi/revenuenc/web/actfans/achieve/accList?rid=${rid}&type=1&period=1`, { method: "GET", mode: "no-cors", cache: "default", credentials: "include" }, "成就列表").then(ret => { if (!ret) return null; return ret.data && Array.isArray(ret.data.accList) ? ret.data.accList : []; }); } function AutoFish_getHomepageData() { return AutoFish_requestJson(`https://www.douyu.com/japi/revenuenc/web/actfans/fishing/homePage?rid=${rid}&opt=1`, { method: "GET", mode: "no-cors", cache: "default", credentials: "include" }, "活动首页"); } function AutoFish_startFish() { return AutoFish_requestJson("https://www.douyu.com/japi/revenuenc/web/actfans/fishing/fishing", { method: "POST", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `ctn=${getCCN()}&rid=${rid}&baitId=${baitId}&ver=1.1` }, "下杆"); } function AutoFish_endFish() { return AutoFish_requestJson("https://www.douyu.com/japi/revenuenc/web/actfans/fishing/reelIn", { method: "POST", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `ctn=${getCCN()}&rid=` + rid }, "收杆"); } function isInFishingTime() { let modeRadio = document.querySelector('input[name="autofish_mode"]:checked'); let mode = modeRadio ? modeRadio.value : "all"; if ("all" === mode) return true; let now = new Date(); let hour = now.getHours(); let minute = now.getMinutes(); return hour >= 12 && minute < 30 || 0 === hour && minute < 30; } function initPkg_ExpandTool_ClearBag() { ExpandTool_ClearBag_insertDom(), initPkg_ExpandTool_ClearBag_Set(), ExpandTool_ClearBag_insertFunc(); } function initPkg_ExpandTool_ClearBag_Set() { let ret = localStorage.getItem("ExSave_ClearBag"); if (null != ret) try { let data = JSON.parse(ret); if (data.id) { let idEl = document.getElementById("extool__clearbag_id"); if (idEl) idEl.value = data.id; } if (data.name) { let nameEl = document.getElementById("extool__clearbag_name") || document.getElementById("extool__clearbag_selected_name"); if (nameEl) nameEl.textContent = data.name; } if (data.icon) { let iconEl = document.getElementById("extool__clearbag_icon"); if (iconEl) iconEl.src = data.icon; } if (data.count) { let cntEl = document.getElementById("extool__clearbag_cnt"); if (cntEl) cntEl.value = data.count; } } catch (e) {} } function saveData_ExpandTool_ClearBag() { let id = document.getElementById("extool__clearbag_id")?.value || "268"; let name = document.getElementById("extool__clearbag_name")?.textContent || document.getElementById("extool__clearbag_selected_name")?.textContent || "荧光棒"; let icon = document.getElementById("extool__clearbag_icon")?.src || ""; let count = document.getElementById("extool__clearbag_cnt")?.value || "1"; let data = { id: id, name: name, icon: icon, count: count }; localStorage.setItem("ExSave_ClearBag", JSON.stringify(data)); } function ExpandTool_ClearBag_insertDom() { if (document.getElementById("extool__clearbag_id")) return; let html = ""; html += '
', html += '', html += '(荧光棒)', html += '', html += ''; let a = document.createElement("div"); a.className = "extool__clearbag", a.innerHTML = html; let b = document.getElementsByClassName("extool")[0]; b.insertBefore(a, b.childNodes[0]); } function ExpandTool_ClearBag_insertFunc() { let pickerTrigger = document.getElementById("extool__clearbag_badge") || document.getElementById("extool__clearbag_picker_btn"); if (pickerTrigger) pickerTrigger.addEventListener("click", () => { if ("function" === typeof openGiftPicker) openGiftPicker("backpack", gift => { let idEl = document.getElementById("extool__clearbag_id"); if (idEl) idEl.value = gift.id; let nameEl = document.getElementById("extool__clearbag_name") || document.getElementById("extool__clearbag_selected_name"); if (nameEl) nameEl.textContent = gift.name; let iconEl = document.getElementById("extool__clearbag_icon"); if (iconEl && gift.icon) iconEl.src = gift.icon; if (gift.count) { let cntEl = document.getElementById("extool__clearbag_cnt"); if (cntEl) cntEl.value = gift.count; } saveData_ExpandTool_ClearBag(); }); }); let cntInput = document.getElementById("extool__clearbag_cnt"); if (cntInput) cntInput.addEventListener("input", saveData_ExpandTool_ClearBag), cntInput.addEventListener("change", saveData_ExpandTool_ClearBag); let sendBtn = document.getElementById("extool__clearbag_sendbtn"); if (sendBtn) sendBtn.addEventListener("click", async function() { if (true != confirm("确认送出?")) return; let id = document.getElementById("extool__clearbag_id").value; let n = Number(document.getElementById("extool__clearbag_cnt").value); showMessage("【背包送礼】执行中...", "info"); for (let i = 0; i < n; i++) await sleep(100).then(() => { sendGift_bag(id, 1, rid).then(data => { if ("success" != data.msg) showMessage("【背包送礼】" + rid + "赠送失败 " + data.msg, "error"), console.log(rid, data); }).catch(err => { showMessage("【背包送礼】" + rid + "赠送失败", "error"), console.log(rid, err); }); }); showMessage("【背包送礼】执行完毕!", "success"); }); } function getBagGifts(room_id, callback) { fetch("https://www.douyu.com/japi/prop/backpack/web/v5?rid=" + room_id, { method: "GET", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" } }).then(result => result.json()).then(ret => { callback(ret); }).catch(err => { console.log("请求失败!", err); }); } function initPkg_ExpandTool_FullScreen() { ExpandTool_FullScreen_insertFunc(), ExpandTool_HighestVideoQuality_insertFunc(), initPkg_ExpandTool_FullScreen_Set(), initPkg_ExpandTool_HighestVideoQuality_Set(); } function getFullScreen() { return document.getElementById("extool__fullscreen").checked; } function ExpandTool_FullScreen_insertFunc() { document.getElementById("extool__fullscreen").addEventListener("click", function() { if (saveData_FullScreen(), getFullScreen()) showMessage("刷新页面生效", "success"); }); } function saveData_FullScreen() { let data = { isFullScreen: getFullScreen() }; localStorage.setItem("ExSave_FullScreen", JSON.stringify(data)); } function initPkg_ExpandTool_FullScreen_Set() { let ret = localStorage.getItem("ExSave_FullScreen"); if (null != ret) { let retJson = JSON.parse(ret); if (retJson.isFullScreen) document.getElementById("extool__fullscreen").checked = retJson.isFullScreen; } } function initFullScreen() { let ret = localStorage.getItem("ExSave_FullScreen"); if (null == ret) return; let retJson = null; try { retJson = JSON.parse(ret); } catch (e) { return; } if (retJson && retJson.isFullScreen) ExpandTool_autoWebFullScreen(); } const FULLSCREEN_ICON_SIGN = "M20 25h6v-6"; const FULLSCREEN_WAIT_MS = 6e3; const FULLSCREEN_VERIFY_MS = 400; function ExpandTool_isWebFullScreen() { return !!(document.body && document.body.classList.contains("is-fullScreen")); } function ExpandTool_fullScreenCandidates() { let list = []; let icons = Array.prototype.slice.call(document.querySelectorAll(".icon-c8be96")); icons.forEach(function(el) { let svg = el.querySelector("svg"); if (svg && String(svg.innerHTML).indexOf(FULLSCREEN_ICON_SIGN) >= 0) list.push(el); }); let legacy = document.querySelector("div.wfs-2a8e83"); if (legacy) list.push(legacy); if (icons.length >= 2) list.push(icons[icons.length - 2]); if (icons.length >= 3) list.push(icons[icons.length - 3]); return list; } function ExpandTool_autoWebFullScreen() { let deadline = Date.now() + FULLSCREEN_WAIT_MS; let tried = []; !function tick() { if (ExpandTool_isWebFullScreen()) return console.info("[DouyuEx] 自动网页全屏:已进入"), void 0; if (Date.now() > deadline) return console.info("[DouyuEx] 自动网页全屏:等不到可用的控制条按钮,本次放弃"), void 0; let candidate = ExpandTool_fullScreenCandidates().filter(function(el) { if (!el || tried.indexOf(el) >= 0) return false; let rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null; return !rect || rect.width > 0; })[0]; if (!candidate) return setTimeout(tick, 300), void 0; tried.push(candidate); try { candidate.click(); } catch (e) {} setTimeout(tick, FULLSCREEN_VERIFY_MS); }(); } function getHighestVideoQuality() { return document.getElementById("extool__highestvideoquality").checked; } function ExpandTool_HighestVideoQuality_insertFunc() { document.getElementById("extool__highestvideoquality").addEventListener("click", function() { if (saveData_HighestVideoQuality(), getHighestVideoQuality()) showMessage("自动最高画质已开启(刷新生效)", "success"); }); } function saveData_HighestVideoQuality() { let data = { isHighestVideoQuality: getHighestVideoQuality() }; localStorage.setItem("ExSave_HighestVideoQuality", JSON.stringify(data)); } function initPkg_ExpandTool_HighestVideoQuality_Set() { let ret = localStorage.getItem("ExSave_HighestVideoQuality"); if (null != ret) { let retJson = JSON.parse(ret); if (retJson.isHighestVideoQuality) document.getElementById("extool__highestvideoquality").checked = retJson.isHighestVideoQuality; } } function initHighestVideoQuality() {} function highestVideoQuality() {} var COMMON_BACKPACK_PROPS = { "弱鸡": "https://gfs-op.douyucdn.cn/dygift/2018/11/27/d71e48b4ac91993b4aea75c0c0dd0f45.gif", "粉丝荧光棒": "https://gfs-op.douyucdn.cn/dygift/2019/05/30/ed7b5926f169266173584bbd11139815.gif", "荧光棒": "https://gfs-op.douyucdn.cn/dygift/2019/05/30/ed7b5926f169266173584bbd11139815.gif", "赞": "https://gfs-op.douyucdn.cn/dygift/2018/11/29/abe536f393466727e02422b7cc1fbf57.gif", "办卡": "https://gfs-op.douyucdn.cn/dygift/2026/02/04/fb706bd5c63844326420ef86bf87a3ea.webp", "稳": "https://gfs-op.douyucdn.cn/dygift/1705/88d8b9e6f3630f576b2512f458e38d72.gif", 666: "https://gfs-op.douyucdn.cn/dygift/1705/1f4f13a0c4f826620573e0a133df19f1.gif", "福袋": "https://gfs-op.douyucdn.cn/dygift/2019/07/23/bf92364f33190dfca0803450e1814674.gif" }; var _lastRoomGiftsMapByName = {}; function parseRoomGiftMap(giftDict) { var result = []; if (!giftDict) return result; for (var gid in giftDict) { if (!giftDict.hasOwnProperty(gid)) continue; var g = giftDict[gid]; var icon = g.pc_icon || g.gif_icon || g.himg || g.cimg || g.bimg || ""; if (icon && !icon.startsWith("http")) icon = "https://gfs-op.douyucdn.cn/dygift/" + icon; var priceVal = Number(g.price || g.pc || 0); var priceYc = priceVal / 100; result.push({ id: String(g.id || gid), name: g.name || "未知礼物", priceText: priceYc > 0 ? priceYc + " 鱼翅" : "免费", icon: icon || "https://gfs-op.douyucdn.cn/dygift/2019/05/30/ed7b5926f169266173584bbd11139815.gif", price: priceVal, stayTime: Number(g.stay_time || 4e3) }); } return result; } function parseRoomApiUniversalGifts(list) { var result = []; if (!Array.isArray(list)) return result; for (var i = 0; i < list.length; i++) { var g = list[i]; var icon = g.himg || g.mimg || ""; var priceYc = Number(g.pc || 0); result.push({ id: String(g.id), name: g.name || "通用礼物", priceText: priceYc > 0 ? priceYc + " 鱼翅" : "免费", icon: icon || "https://gfs-op.douyucdn.cn/dygift/2019/05/30/ed7b5926f169266173584bbd11139815.gif", price: 100 * priceYc, stayTime: 4e3 }); } return result; } function fetchCurrentRoomGifts(room_id, callback) { var numRid = String(room_id || rid || ""); var exclusiveGifts = []; var universalGifts = []; var doneCount = 0; function finish() { if (++doneCount < 2) return; var combinedMap = {}; universalGifts.forEach(g => { combinedMap[g.id] = g; }), exclusiveGifts.forEach(g => { combinedMap[g.id] = g; }); var merged = []; for (var id in combinedMap) if (combinedMap.hasOwnProperty(id)) merged.push(combinedMap[id]), _lastRoomGiftsMapByName[combinedMap[id].name] = combinedMap[id]; if (merged.sort((a, b) => b.price - a.price), "function" === typeof callback) callback(merged); } try { var winData = "undefined" !== typeof unsafeWindow && unsafeWindow.$DATA ? unsafeWindow.$DATA : "undefined" !== typeof window && window.$DATA ? window.$DATA : null; if (winData && winData.room_gift && winData.room_gift.gift) exclusiveGifts = parseRoomGiftMap(winData.room_gift.gift), finish(); else fetchBetardExclusive(); } catch (e) { fetchBetardExclusive(); } function fetchBetardExclusive() { var betardUrl = "https://www.douyu.com/betard/" + numRid; var handleBetard = json => { if (json && json.room_gift && json.room_gift.gift) exclusiveGifts = parseRoomGiftMap(json.room_gift.gift); finish(); }; if ("function" === typeof GM_xmlhttpRequest) GM_xmlhttpRequest({ method: "GET", url: betardUrl, responseType: "json", headers: { Referer: "https://www.douyu.com/" + numRid }, onload: res => { handleBetard(res.response || {}); }, onerror: () => { finish(); } }); else fetch(betardUrl, { credentials: "include" }).then(r => r.json()).then(handleBetard).catch(() => { finish(); }); } room_id = "https://open.douyucdn.cn/api/RoomApi/room/" + numRid; var handleRoomApi = json => { if (json && json.data && Array.isArray(json.data.gift)) universalGifts = parseRoomApiUniversalGifts(json.data.gift); finish(); }; if ("function" === typeof GM_xmlhttpRequest) GM_xmlhttpRequest({ method: "GET", url: room_id, responseType: "json", onload: res => { handleRoomApi(res.response || {}); }, onerror: () => { finish(); } }); else fetch(room_id).then(r => r.json()).then(handleRoomApi).catch(() => { finish(); }); } function fetchUserBackpackGifts(room_id, callback) { room_id = String(room_id || rid || ""); var bpUrl = "https://www.douyu.com/japi/prop/backpack/web/v5?rid=" + room_id; function parseBackpackList(list) { if (!Array.isArray(list)) list = []; return list.map(item => { var icon = item.pic || item.icon || item.small_pic || item.himg || ""; if (icon && !icon.startsWith("http")) icon = "https://gfs-op.douyucdn.cn/dygift/" + icon; var itemName = item.name || ""; if (!icon) for (var key in COMMON_BACKPACK_PROPS) if (-1 !== itemName.indexOf(key)) { icon = COMMON_BACKPACK_PROPS[key]; break; } if (!icon && _lastRoomGiftsMapByName[itemName]) icon = _lastRoomGiftsMapByName[itemName].icon; if (!icon) icon = "https://gfs-op.douyucdn.cn/dygift/2019/05/30/ed7b5926f169266173584bbd11139815.gif"; return { id: String(item.id), name: itemName || "背包道具", priceText: "拥有 ×" + String(item.count || 1), icon: icon, count: Number(item.count || 1) }; }); } function fallbackDomCheck() { try { var domCards = document.querySelectorAll(".ToolBarBackpack .ToolbarGiftCard, .ToolbarGiftArea-backpack .ToolbarGiftCard, .ToolBarBackpack-giftList .ToolbarGiftCard"); if (domCards && domCards.length > 0) { var domList = []; for (var i = 0; i < domCards.length; i++) { var card = domCards[i]; var img = card.querySelector(".ToolbarGiftCard-img, img"); var nameEl = card.querySelector(".ToolbarGiftCard-name"); var priceEl = card.querySelector(".ToolbarGiftCard-price"); var name = nameEl ? nameEl.textContent.trim() : card.getAttribute("title") || ""; var iconSrc = img ? img.src || img.getAttribute("data-src") || "" : ""; if (!iconSrc && name) for (var k in COMMON_BACKPACK_PROPS) if (-1 !== name.indexOf(k)) { iconSrc = COMMON_BACKPACK_PROPS[k]; break; } var count = priceEl ? parseInt(priceEl.textContent.replace(/[^0-9]/g, "")) || 1 : 1; var propId = card.getAttribute("data-id") || card.getAttribute("data-prop-id") || String(2e4 + i); if (name) domList.push({ id: String(propId), name: name, priceText: "拥有 ×" + String(count), icon: iconSrc || "https://gfs-op.douyucdn.cn/dygift/2019/05/30/ed7b5926f169266173584bbd11139815.gif", count: count }); } if (domList.length > 0) return callback(domList); } } catch (e) {} callback([]); } if ("function" === typeof GM_xmlhttpRequest) GM_xmlhttpRequest({ method: "GET", url: bpUrl, responseType: "json", headers: { Referer: "https://www.douyu.com/" + room_id }, onload: res => { if (res = res.response || {}, res = res.data && Array.isArray(res.data.list) ? res.data.list : Array.isArray(res.data) ? res.data : [], res.length > 0) return callback(parseBackpackList(res)); fallbackDomCheck(); }, onerror: () => { fallbackDomCheck(); } }); else fetch(bpUrl, { credentials: "include" }).then(r => r.json()).then(json => { if (json = json.data && Array.isArray(json.data.list) ? json.data.list : Array.isArray(json.data) ? json.data : [], json.length > 0) return callback(parseBackpackList(json)); fallbackDomCheck(); }).catch(() => { fallbackDomCheck(); }); } function openGiftPicker(type, onSelect) { var oldModal = document.querySelector(".ex-gift-picker-modal"); if (oldModal) oldModal.remove(); if (oldModal = document.querySelector(".ex-gift-picker-mask"), oldModal) oldModal.remove(); var mask = document.createElement("div"); mask.className = "ex-gift-picker-mask", document.body.appendChild(mask); var modal = document.createElement("div"); modal.className = "ex-gift-picker-modal miuix-modal-in", modal.innerHTML = `
正在加载礼物池...
`, document.body.appendChild(modal); var grid = modal.querySelector("#ex-gift-grid"); var tabRoom = modal.querySelector("#tab-room-gifts"); var tabBag = modal.querySelector("#tab-bag-gifts"); var searchInput = modal.querySelector("#ex-gift-picker-search"); oldModal = modal.querySelector(".ex-gift-picker__close"); var currentPool = []; function closeModal() { document.removeEventListener("keydown", onKeyDown), modal.classList.remove("miuix-modal-in"), modal.classList.add("miuix-modal-out"), mask.style.opacity = "0", setTimeout(() => { if (modal.parentNode) modal.parentNode.removeChild(modal); if (mask.parentNode) mask.parentNode.removeChild(mask); }, 160); } function onKeyDown(e) { if ("Escape" === e.key) closeModal(); } function renderGifts(gifts) { if (!grid) return; if (grid.innerHTML = "", !gifts || 0 === gifts.length) { var emptyText = tabBag && tabBag.classList.contains("is-active") ? "当前背包暂无道具(可前往直播间完成任务领取)" : "暂无匹配礼物"; return grid.innerHTML = `
${emptyText}
`, void 0; } var frag = document.createDocumentFragment(); gifts.forEach(g => { var cell = document.createElement("div"); cell.className = "ex-gift-cell", cell.setAttribute("data-gid", g.id), cell.title = (g.name || "") + " (" + (g.priceText || "") + ")", cell.innerHTML = `
${g.name || "未知礼物"}
${g.priceText || ""}
`, cell.addEventListener("click", e => { if (e.stopPropagation(), closeModal(), "function" === typeof onSelect) onSelect(g); }), frag.appendChild(cell); }), grid.appendChild(frag); } function filterAndRender() { var kw = searchInput && searchInput.value ? searchInput.value.trim().toLowerCase() : ""; if (!kw) renderGifts(currentPool); else { var filtered = currentPool.filter(g => g.name && -1 !== g.name.toLowerCase().indexOf(kw) || g.id && -1 !== String(g.id).indexOf(kw)); renderGifts(filtered); } } if (document.addEventListener("keydown", onKeyDown), oldModal.addEventListener("click", e => { e.stopPropagation(), closeModal(); }), mask.addEventListener("click", e => { e.stopPropagation(), closeModal(); }), searchInput) searchInput.addEventListener("input", filterAndRender); function loadRoomGifts() { if (!grid) return; if (grid.innerHTML = '
正在加载房间官方礼物...
', searchInput) searchInput.value = ""; fetchCurrentRoomGifts(rid, gifts => { currentPool = gifts || [], filterAndRender(); }); } function loadBackpackGifts() { if (!grid) return; if (grid.innerHTML = '
正在加载背包资产礼物...
', searchInput) searchInput.value = ""; fetchUserBackpackGifts(rid, gifts => { currentPool = gifts || [], filterAndRender(); }); } if (tabRoom.addEventListener("click", e => { if (e.stopPropagation(), tabRoom.classList.contains("is-active")) return; tabRoom.classList.add("is-active"), tabBag.classList.remove("is-active"), loadRoomGifts(); }), tabBag.addEventListener("click", e => { if (e.stopPropagation(), tabBag.classList.contains("is-active")) return; tabBag.classList.add("is-active"), tabRoom.classList.remove("is-active"), loadBackpackGifts(); }), "backpack" === type) tabBag.classList.add("is-active"), tabRoom.classList.remove("is-active"), loadBackpackGifts(); else tabRoom.classList.add("is-active"), tabBag.classList.remove("is-active"), loadRoomGifts(); } function initPkg_ExpandTool_P2P() { ExpandTool_P2P_insertFunc(), initPkg_ExpandTool_P2P_Set(); } function getP2P() { return document.getElementById("extool__p2p").checked; } function ExpandTool_P2P_insertFunc() { document.getElementById("extool__p2p").addEventListener("click", function() { if (saveData_P2P(), getP2P()) showMessage("阻止p2p上传成功,刷新页面生效", "success"); }); } function saveData_P2P() { let data = { isKillP2P: getP2P() }; localStorage.setItem("ExSave_P2P", JSON.stringify(data)); } function initPkg_ExpandTool_P2P_Set() { let ret = localStorage.getItem("ExSave_P2P"); if (null != ret) { let retJson = JSON.parse(ret); if (retJson.isKillP2P) document.getElementById("extool__p2p").checked = retJson.isKillP2P; } } function initKillP2P() { let ret = localStorage.getItem("ExSave_P2P"); if (null != ret) { let retJson = null; try { retJson = JSON.parse(ret); } catch (e) { if ("function" === typeof ExGuard_warn) ExGuard_warn("阻止P2P上传", "本地设置已损坏,按「不拦截」处理", e); return; } if (!retJson || "object" !== typeof retJson) return; if (retJson.isKillP2P) killP2P(); } } function GracefulP2PBlocker_fail(message) { const rejected = Promise.reject(new DOMException(message, "NotSupportedError")); try { rejected.catch(function() {}); } catch (e) {} return rejected; } window.openGiftPicker = openGiftPicker; class GracefulP2PBlocker { constructor() { this.connectionState = "failed", this.iceConnectionState = "failed", this.signalingState = "closed", this.iceGatheringState = "complete", this.localDescription = null, this.remoteDescription = null, this.onicecandidate = null, this.ontrack = null, this.ondatachannel = null; } createDataChannel() { return { send: function() {}, close: function() {}, addEventListener: function() {}, removeEventListener: function() {}, readyState: "closed" }; } createOffer() { return GracefulP2PBlocker_fail("WebRTC P2P disabled by user policy"); } createAnswer() { return GracefulP2PBlocker_fail("WebRTC P2P disabled by user policy"); } setLocalDescription() { return Promise.resolve(); } setRemoteDescription() { return Promise.resolve(); } addIceCandidate() { return Promise.resolve(); } addEventListener() {} removeEventListener() {} dispatchEvent() { return false; } close() {} getStats() { return Promise.resolve(new Map()); } } function killP2P() { let funNameList = [ "RTCPeerConnection", "webkitRTCPeerConnection", "mozRTCPeerConnection", "msRTCPeerConnection" ]; funNameList.forEach(name => { if ("undefined" === typeof unsafeWindow.RTCPeerConnection) unsafeWindow.RTCPeerConnection = unsafeWindow[name]; try { unsafeWindow[name] = GracefulP2PBlocker; } catch (err) {} try { window[name] = GracefulP2PBlocker; } catch (err) {} }); } let redpackets_room_arr = []; let redpacket_room_timer; const REDPACKET_ROOM_MAX_FAIL = 3; const REDPACKET_ROOM_MAX_GRAB = 5; let redpacketRoomFailCount = 0; function initPkg_ExpandTool_RedPacket_Room() { ExpandTool_RedPacket_Room_insertFunc(), ExpandTool_RedPacket_Room_Set(); } function ExpandTool_RedPacket_Room_stop(message) { const box = document.getElementById("extool__redpacekt_room_start"); if (box) box.checked = false; if (redpacket_room_timer) clearInterval(redpacket_room_timer); if (redpacket_room_timer = 0, redpacketRoomFailCount = 0, saveData_RedPacket_Room(), message) showMessage(message, "error"); } function ExpandTool_RedPacket_Room_insertFunc() { const box = document.getElementById("extool__redpacekt_room_start"); if (!box) { if ("function" === typeof ExGuard_warn) ExGuard_warn("礼物红包", "面板里的开关不存在,本次不绑定(其它扩展功能不受影响)"); return; } box.addEventListener("click", function() { verifyFans("5189167", 6).then(r => { if (true == r) { if (true == box.checked) { if (redpacketRoomFailCount = 0, redpacket_room_timer) clearInterval(redpacket_room_timer); redpacket_room_timer = setInterval(() => { getRoomRedPacketsList(rid); }, 6e4); } else { if (redpacket_room_timer) clearInterval(redpacket_room_timer); redpacket_room_timer = 0, redpacketRoomFailCount = 0; } saveData_RedPacket_Room(); } else box.checked = false, saveData_RedPacket_Room(), showMessage("本功能需拥有6级歆崽粉丝牌(5189167)才可使用", "error"); }).catch(err => { if ("function" === typeof ExGuard_warn) ExGuard_warn("礼物红包", "粉丝牌校验失败,已复位开关", err); ExpandTool_RedPacket_Room_stop("【礼物红包】网络异常,请重试"); }); }); } function getRoomRedPacketsList(room_id) { return fetch("https://www.douyu.com/japi/interactnc/web/propredpacket/getPrpList?type_id=1&room_id=" + room_id, { method: "GET", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" } }).then(res => res.json()).then(ret => { const list = ret && ret.data && Array.isArray(ret.data.list) ? ret.data.list : null; if (!list) throw new Error("红包列表结构不符合预期(缺少 data.list)"); if (redpacketRoomFailCount = 0, 0 === list.length) return; for (let i = 0; i < list.length; i++) { let rpid = list[i].activityid; let offset = redpackets_room_arr.indexOf(rpid); let startTime = list[i].startTime; let to = Number(startTime) - Math.round(new Date().getTime() / 1e3); if (to = 1e3 * to - 2e3, -1 == offset) if (redpackets_room_arr.push(rpid), to > 0) setTimeout(() => { getRoomRedPacket(rpid), getRoomRedPacket(rpid), getRoomRedPacket(rpid), showMessage("【礼物红包】抢红包执行完毕!", "success"); }, to); else getRoomRedPacket(rpid), getRoomRedPacket(rpid), getRoomRedPacket(rpid), showMessage("【礼物红包】抢红包执行完毕!", "success"); } }).catch(err => { if (redpacketRoomFailCount++, "function" === typeof ExGuard_warn) ExGuard_warn("礼物红包", "第 " + redpacketRoomFailCount + " 次拉取红包列表失败(每 60 秒重试一次)", err); if (redpacketRoomFailCount >= REDPACKET_ROOM_MAX_FAIL) ExpandTool_RedPacket_Room_stop("【礼物红包】连续 " + REDPACKET_ROOM_MAX_FAIL + " 次拉取失败,已自动关闭"); }); } function getRoomRedPacket(rpid, attempt) { const tried = Number(attempt) || 0; return fetch("https://www.douyu.com/japi/interactnc/web/propredpacket/grab_prp", { method: "POST", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: "activityid=" + rpid + "&ctn=" + getCCN() }).then(res => res.json()).then(ret => { const isSuc = ret && ret.data ? ret.data.isSuc : null; if (2 != isSuc) return; if (tried + 1 >= REDPACKET_ROOM_MAX_GRAB) { if ("function" === typeof ExGuard_warn) ExGuard_warn("礼物红包", "红包 " + rpid + " 补抢 " + REDPACKET_ROOM_MAX_GRAB + " 次仍未成功,放弃"); return; } getRoomRedPacket(rpid, tried + 1); }).catch(err => { if ("function" === typeof ExGuard_warn) ExGuard_warn("礼物红包", "抢红包请求失败(" + rpid + ")", err); }); } function saveData_RedPacket_Room() { const box = document.getElementById("extool__redpacekt_room_start"); if (!box) return; let data = { isGetRedPacket: box.checked }; localStorage.setItem("ExSave_RedPacket_Room", JSON.stringify(data)); } function ExpandTool_RedPacket_Room_Set() { let ret = localStorage.getItem("ExSave_RedPacket_Room"); if (null == ret) return; let retJson = null; try { retJson = JSON.parse(ret); } catch (e) { if ("function" === typeof ExGuard_warn) ExGuard_warn("礼物红包", "本地设置已损坏,按「未开启」处理", e); return; } if (!retJson || true != retJson.isGetRedPacket) return; verifyFans("5189167", 6).then(r => { if (true == r) { const box = document.getElementById("extool__redpacekt_room_start"); if (box) box.click(); } else ExpandTool_RedPacket_Room_stop("本功能需拥有6级歆崽粉丝牌(5189167)才可使用"); }).catch(err => { if ("function" === typeof ExGuard_warn) ExGuard_warn("礼物红包", "恢复上次开关状态时粉丝牌校验失败,已按关闭处理", err); ExpandTool_RedPacket_Room_stop(null); }); } function initPkg_ExpandTool_SendGift() { ExpandTool_SendGift_insertDom(), initPkg_ExpandTool_SendGift_Set(), ExpandTool_SendGift_insertFunc(); } function initPkg_ExpandTool_SendGift_Set() { let ret = localStorage.getItem("ExSave_SendGift"); if (null != ret) try { let data = JSON.parse(ret); if (data.id) { let idEl = document.getElementById("extool__sendgift_id"); if (idEl) idEl.value = data.id; } if (data.name) { let nameEl = document.getElementById("extool__sendgift_name") || document.getElementById("extool__sendgift_selected_name"); if (nameEl) nameEl.textContent = data.name; } if (data.icon) { let iconEl = document.getElementById("extool__sendgift_icon"); if (iconEl) iconEl.src = data.icon; } if (data.count) { let cntEl = document.getElementById("extool__sendgift_cnt"); if (cntEl) cntEl.value = data.count; } if ("undefined" !== typeof data.delay) { let delayEl = document.getElementById("extool__sendgift_delay"); if (delayEl) delayEl.value = data.delay; } } catch (e) {} } function saveData_ExpandTool_SendGift() { let id = document.getElementById("extool__sendgift_id")?.value || "20000"; let name = document.getElementById("extool__sendgift_name")?.textContent || document.getElementById("extool__sendgift_selected_name")?.textContent || "弱鸡"; let icon = document.getElementById("extool__sendgift_icon")?.src || ""; let count = document.getElementById("extool__sendgift_cnt")?.value || "1"; let delay = document.getElementById("extool__sendgift_delay")?.value || "0"; let data = { id: id, name: name, icon: icon, count: count, delay: delay }; localStorage.setItem("ExSave_SendGift", JSON.stringify(data)); } function ExpandTool_SendGift_insertDom() { if (document.getElementById("extool__sendgift_id")) return; let html = ""; html += '
', html += '', html += '(弱鸡)', html += '', html += '', html += ''; let a = document.createElement("div"); a.className = "extool__sendgift", a.innerHTML = html; let b = document.getElementsByClassName("extool")[0]; if (!b) { if ("function" === typeof ExGuard_warn) ExGuard_warn("送礼", "未找到 .extool 容器,打榜送礼面板本次不插入"); return false; } return b.insertBefore(a, b.childNodes[0]), true; } function ExpandTool_SendGift_insertFunc() { let pickerTrigger = document.getElementById("extool__sendgift_badge") || document.getElementById("extool__sendgift_picker_btn"); if (pickerTrigger) pickerTrigger.addEventListener("click", () => { if ("function" === typeof openGiftPicker) openGiftPicker("room", gift => { let idEl = document.getElementById("extool__sendgift_id"); if (idEl) idEl.value = gift.id; let nameEl = document.getElementById("extool__sendgift_name") || document.getElementById("extool__sendgift_selected_name"); if (nameEl) nameEl.textContent = gift.name; let iconEl = document.getElementById("extool__sendgift_icon"); if (iconEl && gift.icon) iconEl.src = gift.icon; saveData_ExpandTool_SendGift(); }); }); let cntInput = document.getElementById("extool__sendgift_cnt"); if (cntInput) cntInput.addEventListener("input", saveData_ExpandTool_SendGift), cntInput.addEventListener("change", saveData_ExpandTool_SendGift); let delayInput = document.getElementById("extool__sendgift_delay"); if (delayInput) delayInput.addEventListener("input", saveData_ExpandTool_SendGift), delayInput.addEventListener("change", saveData_ExpandTool_SendGift); let sendBtn = document.getElementById("extool__sendgift_btn"); if (sendBtn) sendBtn.addEventListener("click", async () => { if (true != confirm("确认送出?")) return; let gidEl = document.getElementById("extool__sendgift_id"); let gcntEl = document.getElementById("extool__sendgift_cnt"); let delayEl = document.getElementById("extool__sendgift_delay"); if (!gidEl || !gcntEl) return showMessage("【送礼】面板节点丢失,请刷新页面", "error"); let gid = gidEl.value; let total = Math.max(1, Number(gcntEl.value) || 1); let delay = Math.max(0, Number(delayEl ? delayEl.value : 0) || 0); let okCount = 0; let failCount = 0; let t_price = 0; showMessage("【送礼】执行中... 共 " + total + " 个", "info"); for (let i = 0; i < total; i++) { try { const ret = await sendGift_any(gid, 1, rid); if (ret && null != ret.data && "鱼翅不足" != ret.msg) okCount++, t_price += Number(ret.data.priceType) || 0; else failCount++, console.log("【送礼】" + gid + " 失败:" + (ret && ret.msg)); } catch (err) { failCount++, console.log("【送礼】" + gid + " 请求失败!", err); } if (delay > 0 && i < total - 1) await sleep(delay); } const summary = okCount > 0 ? "【送礼】完成:成功 " + okCount + "/" + total + (failCount > 0 ? ",失败 " + failCount : "") + ",详情可在 F12 控制台查看" : "【送礼】全部失败(0/" + total + "),详情可在 F12 控制台查看"; showMessage(summary, okCount > 0 ? "success" : "error"), console.log("【送礼】完成:成功 " + okCount + ",失败 " + failCount + ",总价值 " + t_price); }); } function sendGift_any(gid, count, rid) { return fetch("https://www.douyu.com/japi/gift/donate/mainsite/v1", { method: "POST", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: "giftId=" + gid + "&giftCount=" + count + "&roomId=" + rid + "&bizExt=%7B%22yzxq%22%3A%7B%7D%7D" }).then(res => res.json()); } function initPkg_ExpandTool_Shield() { const enterEl = document.getElementById("extool__removeEnter"); if (enterEl) enterEl.checked = "function" === typeof getLocalIsRemoveEnterBarrage ? getLocalIsRemoveEnterBarrage() : false, enterEl.addEventListener("change", function() { if ("undefined" !== typeof isRemoveEnterBarrage) isRemoveEnterBarrage = !!enterEl.checked; if ("function" === typeof Shield_applyRemoveEnter) Shield_applyRemoveEnter(); if ("function" === typeof saveRemoveEnterBarrage) saveRemoveEnterBarrage(); }); const bgEl = document.getElementById("extool__removeDanmakuBg"); if (bgEl) bgEl.checked = "function" === typeof getLocalIsRemoveDanmakuBackground ? getLocalIsRemoveDanmakuBackground() : false, bgEl.addEventListener("change", function() { if ("undefined" !== typeof isRemoveDanmakuBackground) isRemoveDanmakuBackground = !!bgEl.checked; if (isRemoveDanmakuBackground) { if ("function" === typeof removeDanmakuBackground) removeDanmakuBackground(); } else if ("function" === typeof StyleHook_remove) StyleHook_remove("Ex_Style_RemoveDanmakuBackground"); if ("function" === typeof saveRemoveDanmakuBackground) saveRemoveDanmakuBackground(); }); const repEl = document.getElementById("extool__removeRepeated"); const secEl = document.getElementById("extool__repeatedSeconds"); const zoomEl = document.getElementById("extool__enlargeDanmaku"); if (secEl) secEl.value = "function" === typeof getLocalRepeatedDanmakuSeconds ? getLocalRepeatedDanmakuSeconds() : 5, secEl.addEventListener("input", function() { let value = parseInt(secEl.value, 10); if (isNaN(value) || value < 1) value = 1; if (value > 300) value = 300; if (secEl.value = value, "undefined" !== typeof repeatedDanmakuSeconds) repeatedDanmakuSeconds = value; if ("function" === typeof setLocalRepeatedDanmakuSeconds) setLocalRepeatedDanmakuSeconds(value); if ("undefined" !== typeof isRemoveRepeatedDanmaku && isRemoveRepeatedDanmaku) { if ("undefined" !== typeof repeatedDanmakuDomHook && repeatedDanmakuDomHook) repeatedDanmakuDomHook.closeHook(), repeatedDanmakuDomHook = null; if ("function" === typeof stopRepeatedDanmakuCleanupTimer) stopRepeatedDanmakuCleanupTimer(); if ("function" === typeof removeRepeatedDanmaku) removeRepeatedDanmaku(); } }); if (zoomEl) zoomEl.checked = "function" === typeof getLocalIsEnlargeDanmaku ? getLocalIsEnlargeDanmaku() : false, zoomEl.addEventListener("change", function() { if ("undefined" !== typeof isEnlargeDanmaku) isEnlargeDanmaku = !!zoomEl.checked; if ("function" === typeof setLocalIsEnlargeDanmaku) setLocalIsEnlargeDanmaku(isEnlargeDanmaku); }); if (repEl) repEl.checked = "function" === typeof getLocalIsRemoveRepeatedDanmaku ? getLocalIsRemoveRepeatedDanmaku() : false, repEl.addEventListener("change", function() { if ("undefined" !== typeof isRemoveRepeatedDanmaku) isRemoveRepeatedDanmaku = !!repEl.checked; if (isRemoveRepeatedDanmaku) { if ("function" === typeof removeRepeatedDanmaku) removeRepeatedDanmaku(); } else { if ("undefined" !== typeof repeatedDanmakuDomHook && repeatedDanmakuDomHook) repeatedDanmakuDomHook.closeHook(), repeatedDanmakuDomHook = null; if ("function" === typeof stopRepeatedDanmakuCleanupTimer) stopRepeatedDanmakuCleanupTimer(); if ("function" === typeof StyleHook_remove) StyleHook_remove("Ex_Style_RemoveRepeatedDanmaku"), StyleHook_remove("Ex_Style_RemoveRepeatedDanmaku_Count"); } if ("function" === typeof saveRemoveRepeatedDanmaku) saveRemoveRepeatedDanmaku(); }); } function initPkg_ExpandTool_TabSwitch() { enableIgnoreAutoPause(), ExpandTool_TabSwitch_insertFunc(), initPkg_ExpandTool_TabSwitch_Set(); } function isTabSwitchEnabled() { try { const ret = localStorage.getItem("ExSave_TabSwitch"); if (null != ret) return !!JSON.parse(ret).isEnableTabSwitch; } catch (e) {} const el = document.getElementById("extool__tabSwitch"); return el ? el.checked : false; } function setTabSwitchEnabled(enabled) { localStorage.setItem("ExSave_TabSwitch", JSON.stringify({ isEnableTabSwitch: !!enabled })); const el = document.getElementById("extool__tabSwitch"); if (el) el.checked = !!enabled; if (enabled) enableTabSwitch(); } function getTabSwitch() { return isTabSwitchEnabled(); } function ExpandTool_TabSwitch_insertFunc() { document.getElementById("extool__tabSwitch").addEventListener("click", function() { const enabled = document.getElementById("extool__tabSwitch").checked; if (setTabSwitchEnabled(enabled), enabled) enableTabSwitch(); else if (showMessage("已关闭页面防挂机,请刷新页面生效", "info"), window.__pip_is_active__) PictureInPictureControl_stopTabAntiFreeze(); }); } function saveData_TabSwitch() { setTabSwitchEnabled(document.getElementById("extool__tabSwitch").checked); } function initPkg_ExpandTool_TabSwitch_Set() { let ret = localStorage.getItem("ExSave_TabSwitch"); if (null != ret) { let retJson = JSON.parse(ret); if (retJson.isEnableTabSwitch) document.getElementById("extool__tabSwitch").checked = retJson.isEnableTabSwitch, enableTabSwitch(); } } function enableTabSwitch() { Object.defineProperty(document, "hidden", { value: false, writable: false }), Object.defineProperty(document, "visibilityState", { value: "visible", writable: false }), Object.defineProperty(document, "webkitVisibilityState", { value: "visible", writable: false }), document.dispatchEvent(new Event("visibilitychange")), document.hasFocus = function() { return true; }, document.addEventListener("visibilitychange", function(e) { e.stopImmediatePropagation(); }, true, true); } function enableIgnoreAutoPause() { localStorage.setItem("freetimed", "1"); } var isGetTreasure = false; const TREASURE_DELAY_MIN = 500; const TREASURE_DELAY_DEFAULT = 3200; function initPkg_ExpandTool_Treasure() { ExpandTool_Treasure_insertFunc(), ExpandTool_Treasure_Set(); } function ExpandTool_Treasure_reset(message) { isGetTreasure = false; const box = document.getElementById("extool__treasure_start"); if (box) box.checked = false; if (saveData_Treasure(), message) showMessage(message, "error"); } function ExpandTool_Treasure_insertFunc() { const box = document.getElementById("extool__treasure_start"); if (!box) { if ("function" === typeof ExGuard_warn) ExGuard_warn("自动宝箱", "面板里的开关不存在,本次不绑定(其它扩展功能不受影响)"); return; } box.addEventListener("click", function() { verifyFans("5189167", 9).then(r => { if (true == r) { if (true == box.checked) isGetTreasure = true, getTreasure_Existing(); else isGetTreasure = false; saveData_Treasure(); } else box.checked = false, saveData_Treasure(), showMessage("本功能需拥有9级歆崽粉丝牌(5189167)才可使用", "error"); }).catch(err => { if ("function" === typeof ExGuard_warn) ExGuard_warn("自动宝箱", "粉丝牌校验失败,已复位开关", err); ExpandTool_Treasure_reset("【自动宝箱】网络异常,请重试"); }); }); } function saveData_Treasure() { const box = document.getElementById("extool__treasure_start"); if (!box) return; isGetTreasure = box.checked; const delayEl = document.getElementById("extool__treasure_delay"); let data = { isGetTreasure: isGetTreasure, treasureDelay: delayEl ? delayEl.value : String(TREASURE_DELAY_DEFAULT) }; localStorage.setItem("ExSave_Treasure", JSON.stringify(data)); } function ExpandTool_Treasure_Set() { let ret = localStorage.getItem("ExSave_Treasure"); if (null == ret) return; let retJson = null; try { retJson = JSON.parse(ret); } catch (e) { if ("function" === typeof ExGuard_warn) ExGuard_warn("自动宝箱", "本地设置已损坏,按「未开启」处理", e); return; } if (!retJson || "object" !== typeof retJson) return; const delayEl = document.getElementById("extool__treasure_delay"); if (delayEl) delayEl.value = "treasureDelay" in retJson ? retJson.treasureDelay : String(TREASURE_DELAY_DEFAULT); if (true != retJson.isGetTreasure) return; verifyFans("5189167", 9).then(r => { if (true == r) { const box = document.getElementById("extool__treasure_start"); if (box) box.click(); } else ExpandTool_Treasure_reset("本功能需拥有9级歆崽粉丝牌(5189167)才可使用"); }).catch(err => { if ("function" === typeof ExGuard_warn) ExGuard_warn("自动宝箱", "恢复上次开关状态时粉丝牌校验失败,已按关闭处理", err); ExpandTool_Treasure_reset(null); }); } function getTreasureDelay() { const delayEl = document.getElementById("extool__treasure_delay"); const raw = delayEl ? delayEl.value : ""; const value = Number(raw); if (!isFinite(value) || value <= 0) return TREASURE_DELAY_DEFAULT; return Math.max(TREASURE_DELAY_MIN, Math.floor(value)); } function getTreasure_Existing() { getTslist(data => { if (null == data) return; if (!data.list) { if ("function" === typeof ExGuard_warn) ExGuard_warn("自动宝箱", "宝箱列表为空或结构不符合预期,本次不处理"); return; } for (let i = 0; i < data.list.length - 1; i++) { let item = data.list[i]; let rpid = item.rpid; let ot = item.ot; let did = getCookieValue("dy_did"); let timeout = Number(ot) - Math.floor(Date.now() / 1e3); let a = document.createElement("div"); let idName = "Ex_Geetest_no" + String(treasureNum); a.id = idName; let b = document.getElementById("Ex_Geetest"); if (!b) { if ("function" === typeof ExGuard_warn) ExGuard_warn("自动宝箱", "缺少 #Ex_Geetest 容器,本次不处理宝箱"); return; } if (b.appendChild(a), timeout >= 0) timeout = 1e3 * timeout + getTreasureDelay(), treasureNum++, setTimeout(() => { getTreasure(rid, rpid, did, idName); }, timeout); else getTreasure(rid, rpid, did, idName); } }); } function getTslist(callback) { if (unsafeWindow.socketProxy?.socketStream?.subscribe) try { unsafeWindow.socketProxy.socketStream.subscribe("tslist", callback); } catch (e) {} else { let retry = 0; let t = setInterval(() => { if (retry++, unsafeWindow.socketProxy?.socketStream?.subscribe) { clearInterval(t); try { unsafeWindow.socketProxy.socketStream.subscribe("tslist", callback); } catch (e) {} } else if (retry > 30) clearInterval(t); }, 1e3); } } let ExPanel_anchorParent = null; let ExPanel_anchorNextSibling = null; const EX_THIRD_LEVEL_PANELS = ".sign-panel, .fans-continue-panel, .extool, .livetool, .bloop, .exlottery, .popup-player-panel, .exupdate-panel"; const EX_MENU_SURFACE_EXTRA = [ ".ex-panel", ".ex-icon", ".ChatToolBar-DanmakuTail-Panel", "#ex-vtoolbar-menu", ".ex-gift-picker-modal", ".ex-gift-picker-mask", ".ex-pip-menu-root", "[data-ex-menu-layer]", ".postbird-box-container" ]; const EX_MENU_SURFACE_SELECTOR = EX_MENU_SURFACE_EXTRA.concat(EX_THIRD_LEVEL_PANELS.split(",")).map(function(s) { return String(s).trim(); }).filter(function(s) { return !!s; }).join(", "); const EX_MENU_TRIGGER_SELECTOR = ".ex-icon, .ChatToolBar-DanmakuTail"; function ExPanel_closeAllThirdLevel() { const panels = document.querySelectorAll(EX_THIRD_LEVEL_PANELS); for (let i = 0; i < panels.length; i++) { const p = panels[i]; p.style.removeProperty("display"), p.style.setProperty("display", "none", "important"), p.classList.remove("miuix-modal-in"); } if ("function" === typeof updateDockActiveIndicator) updateDockActiveIndicator(); return panels.length; } function isAnyThirdLevelPanelOpen() { const panels = document.querySelectorAll(EX_THIRD_LEVEL_PANELS); for (let i = 0; i < panels.length; i++) { const p = panels[i]; let isShowing = "flex" === p.style.display || "block" === p.style.display || "none" !== window.getComputedStyle(p).display && "none" !== p.style.display; if (isShowing) return true; } return false; } function initPkg_ExPanel() { initPkg_ExPanel_insertDom(); let exPanelDOM = document.querySelector(`.ex-panel`); if (exPanelDOM) { const closeBtn = exPanelDOM.querySelector(".ex-panel__close"); if (closeBtn) closeBtn.addEventListener("click", e => { e.stopPropagation(), ExPanel_closeAllMenus(); }); } ExPanel_startSelfHeal(), ExPanel_bindClickOutside(), ExPanel_bindEsc(), ExPanel_bindDockObserver(); } var ExPanel_outsideClickBound = false; function ExPanel_elementOf(node) { if (!node) return null; if (1 === node.nodeType) return node; return node.parentElement || null; } function ExPanel_matchesAny(node, selector) { const el = ExPanel_elementOf(node); if (!el || "function" !== typeof el.closest) return false; try { return !!el.closest(selector); } catch (e) { return false; } } function ExPanel_isInsideMenu(node) { return ExPanel_matchesAny(node, EX_MENU_SURFACE_SELECTOR) || ExPanel_matchesAny(node, EX_MENU_TRIGGER_SELECTOR); } function ExPanel_markMenuLayer(node) { if (!node || 1 !== node.nodeType || "function" !== typeof node.setAttribute) return node || null; return node.setAttribute("data-ex-menu-layer", "1"), node; } function ExPanel_hasOpenMenu() { const dock = document.querySelector(".ex-panel"); if (dock && ("flex" === dock.style.display || "block" === dock.style.display)) return true; if (isAnyThirdLevelPanelOpen()) return true; const layers = document.querySelectorAll("[data-ex-menu-layer]"); for (let i = 0; i < layers.length; i++) if (ExPanel_panelIsShown(layers[i])) return true; return false; } function ExPanel_closeAllMenus() { let closed = ExPanel_closeAllThirdLevel(); const layers = document.querySelectorAll("[data-ex-menu-layer]"); for (let i = 0; i < layers.length; i++) { const layer = layers[i]; if (!ExPanel_panelIsShown(layer)) continue; layer.style.removeProperty("display"), layer.style.setProperty("display", "none", "important"), layer.classList.remove("miuix-modal-in"), closed++; } const dock = document.querySelector(".ex-panel"); if (dock && ("flex" === dock.style.display || "block" === dock.style.display)) hideExPanel(), closed++; if ("function" === typeof updateDockActiveIndicator) updateDockActiveIndicator(); return closed; } function ExPanel_onMenuOutsideMouseDown(e) { if (!e) return; if ("number" === typeof e.button && 0 !== e.button) return; if (ExPanel_isInsideMenu(e.target)) return; if (!ExPanel_hasOpenMenu()) return; ExPanel_closeAllMenus(); } function ExPanel_bindClickOutside() { if (ExPanel_outsideClickBound) return; if ("undefined" === typeof document || !document || !document.addEventListener) return; ExPanel_outsideClickBound = true, document.addEventListener("mousedown", ExPanel_onMenuOutsideMouseDown, true); } var ExPanel_escBound = false; function ExPanel_openThirdLevelPanels() { if ("undefined" === typeof document || !document.querySelectorAll) return []; const list = document.querySelectorAll(EX_THIRD_LEVEL_PANELS); const out = []; for (let i = 0; i < list.length; i++) { const p = list[i]; const st = p.style && p.style.display || ""; if ("flex" === st || "block" === st) out.push(p); } return out; } function ExPanel_onEscKeydown(e) { if (!e) return; if ("Escape" !== e.key && 27 !== e.keyCode) return; if ("undefined" !== typeof document && document.fullscreenElement) return; const open = ExPanel_openThirdLevelPanels(); if (open.length) { const top = open[open.length - 1]; if (top.style.removeProperty("display"), top.style.setProperty("display", "none", "important"), top.classList.remove("miuix-modal-in"), "function" === typeof updateDockActiveIndicator) updateDockActiveIndicator(); return e.stopPropagation(), void 0; } if ("function" === typeof ExPanel_hasOpenMenu && ExPanel_hasOpenMenu()) ExPanel_closeAllMenus(); } function ExPanel_bindEsc() { if (ExPanel_escBound) return; if ("undefined" === typeof document || !document || !document.addEventListener) return; ExPanel_escBound = true, document.addEventListener("keydown", ExPanel_onEscKeydown); } var ExPanel_dockObserver = null; var ExPanel_dockObserverTimer = 0; var ExPanel_dockObserverRetry = 0; function ExPanel_bindDockObserver() { if (ExPanel_dockObserver) return; if ("function" !== typeof MutationObserver) return; var wrap = document.querySelector(".ex-panel__wrap"); if (!wrap) { if (ExPanel_dockObserverRetry++ < 40) setTimeout(ExPanel_bindDockObserver, 250); return; } if (ExPanel_dockObserver = new MutationObserver(function() { if (ExPanel_dockObserverTimer) return; ExPanel_dockObserverTimer = setTimeout(function() { if (ExPanel_dockObserverTimer = 0, "function" === typeof ensureDockIndicators) ensureDockIndicators(); }, 30); }), ExPanel_dockObserver.observe(wrap, { childList: true }), "function" === typeof ensureDockIndicators) ensureDockIndicators(); } let ExPanel_dockNode = null; let ExPanel_healTimer = 0; const EXPANEL_HEAL_FAST_MS = 3e3; const EXPANEL_HEAL_SLOW_MS = 3e4; const EXPANEL_HEAL_QUIET_TICKS = 20; let ExPanel_healCadence = EXPANEL_HEAL_FAST_MS; let ExPanel_healQuietTicks = 0; function ExPanel_startSelfHeal() { if (ExPanel_healTimer) return; ExPanel_healCadence = EXPANEL_HEAL_FAST_MS, ExPanel_healQuietTicks = 0, ExPanel_healTimer = setInterval(ExPanel_healTick, ExPanel_healCadence); } function ExPanel_stopSelfHeal() { if (!ExPanel_healTimer) return; clearInterval(ExPanel_healTimer), ExPanel_healTimer = 0; } function ExPanel_applyHealCadence(ms) { if (ExPanel_healCadence = ms, !ExPanel_healTimer) return; clearInterval(ExPanel_healTimer), ExPanel_healTimer = setInterval(ExPanel_healTick, ExPanel_healCadence); } function ExPanel_healTick() { if (document.hidden) return; let healed = false; try { healed = ExPanel_healDock(); } catch (e) { if ("function" === typeof ExGuard_warn) ExGuard_warn("工具条", "自愈巡检抛错(本次跳过,下轮继续)", e); } if (healed) { if (ExPanel_healQuietTicks = 0, ExPanel_healCadence !== EXPANEL_HEAL_FAST_MS) ExPanel_applyHealCadence(EXPANEL_HEAL_FAST_MS); return; } if (ExPanel_healQuietTicks < EXPANEL_HEAL_QUIET_TICKS) if (ExPanel_healQuietTicks++, ExPanel_healQuietTicks >= EXPANEL_HEAL_QUIET_TICKS) ExPanel_applyHealCadence(EXPANEL_HEAL_SLOW_MS); } function ExPanel_healDock() { const liveDock = document.querySelector(".ex-panel"); if (liveDock && liveDock !== ExPanel_dockNode) ExPanel_dockNode = liveDock, ExPanel_saveAnchor(liveDock); const node = ExPanel_dockNode; if (!node) return false; if (document.contains(node)) { if (node.classList.contains("ex-panel--floating")) { const anchorNow = ExPanel_getGiftBarAnchor(); if (anchorNow && !ExPanel_isGiftBarHidden()) { if (ExPanel_restoreToGiftBar(), "function" === typeof ensureDockIndicators) ensureDockIndicators(); return console.warn("[DouyuEx] 工具条已从浮动状态归位到礼物流水条"), true; } } return false; } const anchor = ExPanel_getGiftBarAnchor(); if (anchor && !ExPanel_isGiftBarHidden()) { if (anchor.insertBefore(node, anchor.childNodes[0]), node.classList.remove("ex-panel--floating"), node.style.removeProperty("position"), node.style.removeProperty("left"), node.style.removeProperty("right"), node.style.removeProperty("top"), "function" === typeof ensureDockIndicators) ensureDockIndicators(); } else if (ExPanel_getFloatingHost().appendChild(node), node.classList.add("ex-panel--floating"), ExPanel_updateFloatingPosition(), "function" === typeof ensureDockIndicators) ensureDockIndicators(); return console.warn("[DouyuEx] 工具条被页面重绘移除,已重新挂回(按钮与事件原样保留)"), true; } function ExPanel_ensureAttached() { return ExPanel_healDock(), !!(ExPanel_dockNode && document.contains(ExPanel_dockNode)); } function ExPanel_getGiftBarAnchor() { return document.querySelector(".PlayerToolbar-ContentCell .PlayerToolbar-Wealth") || document.querySelector(".PlayerToolbar-ContentRow"); } function ExPanel_isGiftBarHidden() { const row = document.getElementsByClassName("PlayerToolbar-ContentRow")[0]; return !!(row && "hidden" === row.style.visibility); } function ExPanel_getFloatingHost() { return document.getElementById("js-player-dialog") || document.getElementsByClassName("room-Player-Box")[0] || document.body; } function ExPanel_saveAnchor(panel) { if (!ExPanel_anchorParent) ExPanel_anchorParent = panel.parentNode, ExPanel_anchorNextSibling = panel.nextSibling; } function ExPanel_updateFloatingPosition() { const panel = document.querySelector(".ex-panel.ex-panel--floating"); if (!panel) return; const playerToolbar = document.getElementById("js-player-toolbar"); const vtoolbarMenu = document.getElementById("ex-vtoolbar-menu"); const gap = 8; if (panel.style.position = "fixed", panel.style.top = "auto", vtoolbarMenu) { const menuRect = vtoolbarMenu.getBoundingClientRect(); panel.style.bottom = window.innerHeight - menuRect.top + gap + `px`; const panelWidth = panel.offsetWidth || panel.scrollWidth || 360; let left = menuRect.left + menuRect.width / 2 - panelWidth / 2; left = Math.max(8, Math.min(left, window.innerWidth - panelWidth - 8)), panel.style.left = left + `px`, panel.style.right = "auto"; } else if (playerToolbar) { const toolbarRect = playerToolbar.getBoundingClientRect(); panel.style.bottom = window.innerHeight - toolbarRect.top + gap + `px`; const panelWidth = panel.offsetWidth || panel.scrollWidth || 360; let left = toolbarRect.left + toolbarRect.width / 2 - panelWidth / 2; left = Math.max(8, Math.min(left, window.innerWidth - panelWidth - 8)), panel.style.left = left + `px`, panel.style.right = "auto"; } else panel.style.bottom = "76px", panel.style.right = "12px", panel.style.left = ""; } function ExPanel_attachToFloatingHost() { const panel = document.querySelector(".ex-panel"); if (!panel || panel.classList.contains("ex-panel--floating")) return ExPanel_updateFloatingPosition(), void 0; ExPanel_saveAnchor(panel), ExPanel_getFloatingHost().appendChild(panel), panel.classList.add("ex-panel--floating"), ExPanel_updateFloatingPosition(); } function ExPanel_restoreToGiftBar() { const panel = document.querySelector(".ex-panel"); const anchor = ExPanel_getGiftBarAnchor(); if (!panel || !anchor || !panel.classList.contains("ex-panel--floating")) return; if (ExPanel_anchorNextSibling && ExPanel_anchorNextSibling.parentNode === anchor) anchor.insertBefore(panel, ExPanel_anchorNextSibling); else anchor.insertBefore(panel, anchor.childNodes[0]); panel.classList.remove("ex-panel--floating"); } function ExPanel_syncHost() { if (ExPanel_isGiftBarHidden()) ExPanel_attachToFloatingHost(); else ExPanel_restoreToGiftBar(); } function ExPanel_onGiftBarHide() { const panel = document.querySelector(".ex-panel"); if (panel && "block" === panel.style.display) ExPanel_attachToFloatingHost(), ExPanel_updateFloatingPosition(); } function ExPanel_onGiftBarShow() { ExPanel_restoreToGiftBar(); } function initPkg_ExPanel_insertDom() { let a = document.createElement("div"); a.className = "ex-panel", a.dataset.dxBuild = "dx4", a.innerHTML = `
`; let b = ExPanel_getGiftBarAnchor(); if (!b) b = ExPanel_getFloatingHost(), a.classList.add("ex-panel--floating"); else { const domPlayerToolbar = document.querySelector(".PlayerToolbar"); if (domPlayerToolbar) a.style.bottom = domPlayerToolbar.offsetHeight + 4 + "px"; else a.style.bottom = "76px"; } if (b.insertBefore(a, b.childNodes[0]), ExPanel_dockNode = a, ExPanel_saveAnchor(a), ExPanel_isGiftBarHidden()) ExPanel_attachToFloatingHost(); } function ensureDockIndicators() { let dockWrap = document.querySelector(".ex-panel__wrap"); if (!dockWrap) return; const mapping = { "ex-sign": "一键签到", "fans-continue": "一键续牌", "extool-icon": "扩展功能", "livetool-icon": "直播间工具", "bloop-icon": "弹幕发送小助手", "ex-lottery": "全站抽奖信息", "exlottery-icon": "全站抽奖信息", "popup-player": "同屏播放", "ex-update": "版本更新" }; dockWrap.querySelectorAll(":scope > div").forEach(btn => { if (!btn.querySelector(".ex-panel__indicator")) { let ind = document.createElement("span"); ind.className = "ex-panel__indicator", btn.appendChild(ind); } if (!btn.dataset.dxA11y) { btn.dataset.dxA11y = "1", btn.setAttribute("role", "button"), btn.setAttribute("tabindex", "0"); let a11yLabel = ""; for (var k in mapping) if (btn.classList.contains(k)) { a11yLabel = mapping[k]; break; } if (a11yLabel) { if (!btn.getAttribute("aria-label")) btn.setAttribute("aria-label", a11yLabel); if (!btn.getAttribute("title")) btn.setAttribute("title", a11yLabel); } btn.addEventListener("keydown", function(e) { if ("Enter" === e.key || " " === e.key || 13 === e.keyCode || 32 === e.keyCode) e.preventDefault(), this.click(); }), btn.addEventListener("touchstart", function() { const opened = this.classList.contains("is-active") || this.classList.contains("ex-dock-active"); if (!opened) this.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); }, { passive: true }); } if ("function" === typeof ExIcoDock && "undefined" !== typeof EX_ICO_DOCK_MAP) { const icoHost = btn.querySelector(".ex-panel__icon"); if (icoHost && !icoHost.querySelector(".ex-ico")) { let icoName = ""; for (var key in EX_ICO_DOCK_MAP) if (btn.classList.contains(key)) { icoName = EX_ICO_DOCK_MAP[key]; break; } if (icoName) { const tip = icoHost.querySelector(".ex-panel__tip"); if (icoHost.innerHTML = ExIcoDock(icoName), tip) icoHost.appendChild(tip); } } } if (!btn.dataset.hoverBound) btn.dataset.hoverBound = "1", btn.style.cursor = "pointer", btn.addEventListener("mouseenter", () => { for (var cls in mapping) if (btn.classList.contains(cls)) { if ("function" === typeof showExRightPanel) showExRightPanel(mapping[cls], btn, true); break; } }); }); } const EX_DOCK_PANEL_MAP = [ [ "ex-sign", ".sign-panel" ], [ "fans-continue", ".fans-continue-panel" ], [ "extool-icon", ".extool" ], [ "livetool-icon", ".livetool" ], [ "bloop-icon", ".bloop" ], [ "ex-lottery", ".exlottery" ], [ "exlottery-icon", ".exlottery" ], [ "popup-player", ".popup-player-panel" ], [ "ex-update", ".exupdate-panel" ] ]; function ExPanel_panelIsShown(el) { if (!el) return false; const d = el.style.display; if ("flex" === d || "block" === d) return true; if ("none" === d) return false; try { return "none" !== window.getComputedStyle(el).display; } catch (e) { return false; } } function updateDockActiveIndicator() { const dockWrap = document.querySelector(".ex-panel__wrap"); if (!dockWrap) return; EX_DOCK_PANEL_MAP.forEach(function(pair) { const btn = dockWrap.querySelector(":scope > div." + pair[0]); if (!btn) return; const on = ExPanel_panelIsShown(document.querySelector(pair[1])); btn.classList.toggle("is-active", on), btn.classList.toggle("ex-dock-active", on); }); } function openExPanel() { let a = document.getElementsByClassName("ex-panel")[0]; if (!a) return; ExPanel_syncHost(), ensureDockIndicators(); const pendingClose = a.classList.contains("miuix-dock-out"); let isShown = "flex" === a.style.display || "block" === a.style.display; if (isShown && !pendingClose) return; if (a.classList.remove("miuix-dock-out"), a.classList.add("miuix-dock-in"), a.style.display = "flex", a.classList.contains("ex-panel--floating")) ExPanel_updateFloatingPosition(); } function hideExPanel() { const exPanelDOM = document.querySelector(".ex-panel"); if (!exPanelDOM) return; exPanelDOM.classList.remove("miuix-dock-in"), exPanelDOM.classList.add("miuix-dock-out"), setTimeout(() => { if (exPanelDOM.classList.contains("miuix-dock-out")) exPanelDOM.style.display = "none", exPanelDOM.classList.remove("miuix-dock-out"); }, 160); } function toggleExPanel() { let a = document.getElementsByClassName("ex-panel")[0]; if (!a) return; let isShown = ("flex" === a.style.display || "block" === a.style.display) && !a.classList.contains("miuix-dock-out"); if (isShown) hideExPanel(); else openExPanel(); } function showExPanel() { openExPanel(); } function ensureMiuixPanelHeader(el, title) { if (!el) return; el.classList.add("miuix-modal"), el.dataset.dxBuild = "dx4"; var oldCloses = el.querySelectorAll(".extool__close, .livetool__close, .bloop__close, #vote__result-close, .lottery__func, .ChatToolBar-DanmakuTail-title"); oldCloses.forEach(c => { c.style.setProperty("display", "none", "important"); }); var header = el.querySelector(".miuix-modal__header"); if (!header) header = document.createElement("div"), header.className = "miuix-modal__header", header.innerHTML = `
${title}
`, el.insertBefore(header, el.firstChild); else if (oldCloses = header.querySelector(".miuix-modal__title"), oldCloses) oldCloses.textContent = title; if ("function" === typeof ExIcoSvg && "undefined" !== typeof EX_ICO_TITLE_MAP) if (oldCloses = header.querySelector(".miuix-modal__title-box"), title = EX_ICO_TITLE_MAP[title], oldCloses && title) { var titleIco = oldCloses.querySelector(".ex-title-ico"); if (!titleIco) titleIco = document.createElement("span"), titleIco.className = "ex-title-ico", oldCloses.insertBefore(titleIco, oldCloses.firstChild); titleIco.innerHTML = ExIcoSvg(title, false); } var body = el.querySelector(":scope > .miuix-modal__body"); if (!body) body = document.createElement("div"), body.className = "miuix-modal__body", el.appendChild(body); var nodesToMove = []; for (var i = 0; i < el.childNodes.length; i++) { var node = el.childNodes[i]; if (node !== header && node !== body) nodesToMove.push(node); } if (nodesToMove.forEach(n => { body.appendChild(n); }), Array.prototype.forEach.call(body.querySelectorAll(".ui-actions"), bar => { bar.classList.add("ui-action-footer"), el.appendChild(bar); }), "function" === typeof ExIco_decorateCards) ExIco_decorateCards(body); if (oldCloses = header.querySelector(".miuix-modal__close"), oldCloses) oldCloses.onclick = function(e) { if (e.stopPropagation(), el.style.removeProperty("display"), el.style.setProperty("display", "none", "important"), el.classList.remove("miuix-modal-in"), "function" === typeof updateDockActiveIndicator) updateDockActiveIndicator(); }; } function openMiuixPanelCentered(panel, btnEl) { if (!panel) return; if (panel.parentNode !== document.body) document.body.appendChild(panel); var allPanels = document.querySelectorAll(".sign-panel, .fans-continue-panel, .extool, .livetool, .bloop, .exlottery, .popup-player-panel, .exupdate-panel, .ChatToolBar-DanmakuTail-Panel"); if (allPanels.forEach(p => { if (p !== panel) p.style.removeProperty("display"), p.style.setProperty("display", "none", "important"), p.classList.remove("miuix-modal-in"); }), "function" === typeof updateDockActiveIndicator) if (updateDockActiveIndicator(), btnEl) if (allPanels = btnEl.closest ? btnEl.closest(".ex-panel__wrap > div") : null, allPanels) allPanels.classList.add("is-active", "ex-dock-active"); allPanels = 0; try { if (allPanels = parseFloat(getComputedStyle(panel).width) || 0, !allPanels) { var cssW = getComputedStyle(document.documentElement).getPropertyValue("--miuix-modal-width"); allPanels = parseFloat(cssW) || 0; } } catch (e) {} if (!allPanels) allPanels = 380; panel.style.width = allPanels + "px", cssW = (window.innerWidth - allPanels) / 2; var bottom = 90; if (btnEl && "function" === typeof btnEl.getBoundingClientRect) if (btnEl = btnEl.getBoundingClientRect(), btnEl.width > 0 || btnEl.left > 0) cssW = btnEl.left + btnEl.width / 2 - allPanels / 2, cssW = Math.max(12, Math.min(window.innerWidth - allPanels - 12, cssW)), bottom = Math.max(20, window.innerHeight - btnEl.top + 12); panel.style.position = "fixed", panel.style.left = cssW + "px", panel.style.bottom = bottom + "px", panel.style.top = "auto", panel.style.right = "auto", panel.style.zIndex = "100030", panel.style.removeProperty("display"), panel.style.setProperty("display", "flex", "important"), panel.classList.remove("miuix-modal-out"), panel.classList.add("miuix-modal-in"); try { document.dispatchEvent(new CustomEvent("DouyuEx:thirdLevelOpened", { detail: { panel: panel } })); } catch (e) {} } window.ensureMiuixPanelHeader = ensureMiuixPanelHeader, window.openMiuixPanelCentered = openMiuixPanelCentered, window.openExPanel = openExPanel, window.ExPanel_closeAllThirdLevel = ExPanel_closeAllThirdLevel, window.ExPanel_closeAllMenus = ExPanel_closeAllMenus, window.ExPanel_isInsideMenu = ExPanel_isInsideMenu, window.ExPanel_markMenuLayer = ExPanel_markMenuLayer, window.ExPanel_bindClickOutside = ExPanel_bindClickOutside, window.isAnyThirdLevelPanelOpen = isAnyThirdLevelPanelOpen, window.hideExPanel = hideExPanel, window.toggleExPanel = toggleExPanel, window.showExPanel = showExPanel, window.isAnyThirdLevelPanelOpen = isAnyThirdLevelPanelOpen, window.updateDockActiveIndicator = updateDockActiveIndicator; var ExUI = (() => { var LEGACY_SETS = {}; var LEGACY_SINGLE = { actionWrap: "fans-panel__action-wrap" }; function lg(name, extra) { var base = LEGACY_SETS[name]; if (!base) return extra || {}; var out = {}; for (var k in base) out[k] = base[k]; if (extra) for (var j in extra) out[j] = extra[j]; return out; } function esc(s) { return String(null == s ? "" : s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } function withLegacy(base, legacy) { return legacy = null == legacy ? "" : String(legacy).trim(), legacy ? base + " " + legacy : base; } function card(o) { o = o || {}; var lg = o.legacy || {}; var head = ""; if (o.title || o.aside || o.asideHtml) head = '
' + '' + esc(o.title) + "" + (lg.aside ? '' + (o.asideHtml || esc(o.aside)) + "" : o.asideHtml ? o.asideHtml : o.aside ? '' + esc(o.aside) + "" : "") + "
"; return '
' + head + (lg.body || o.bodyId ? '
" + (o.body || "") + "
" : o.body || "") + "
"; } function row(o) { o = o || {}; var lg = o.legacy || {}; return '"; } function prose(html, cls) { return '
' + (html || "") + "
"; } function empty(text, cls) { return '
' + esc(text || "暂无数据") + "
"; } function btn(text, o) { o = o || {}; var v = "primary" === o.variant || "danger" === o.variant || "accent" === o.variant ? o.variant : "quiet"; return v = withLegacy("ui-btn ui-btn--" + v, o.legacy) + (o.cls ? " " + o.cls : ""), '"; } function field(o) { o = o || {}; var lg = o.legacy || {}; return '"; } function seg(o) { o = o || {}; var lg = o.legacy || {}; var items = Array.isArray(o.items) ? o.items : []; var name = o.name ? esc(o.name) : ""; return items = items.map(function(it, i) { return it = it || {}, i = it.id ? esc(it.id) : name ? name + "_" + i : "", '"; }).join(""), '
' + items + "
"; } function stat(o) { o = o || {}; var lg = o.legacy || {}; return '
' + '' + esc(null == o.value ? "--" : o.value) + "" + '' + esc(o.label) + "" + "
"; } function stack(o) { return o = o || {}, '
' + (o.html || "") + "
"; } function node(html) { var tpl = document.createElement("template"); return tpl.innerHTML = String(html || "").trim(), tpl.content.firstElementChild; } function nodes(html) { var tpl = document.createElement("template"); return tpl.innerHTML = String(html || "").trim(), Array.prototype.slice.call(tpl.content.children); } return { card: card, row: row, prose: prose, empty: empty, btn: btn, field: field, seg: seg, stat: stat, stack: stack, node: node, nodes: nodes, lg: lg, LG: LEGACY_SINGLE, esc: esc }; })(); if ("undefined" !== typeof window) window.ExUI = ExUI; var FansBadgeList_hook = null; function initPkg_FansBadgeList() { if (FansBadgeList_annotateRows()) return; if ("function" === typeof DomHook) { let lastRun = 0; try { if (FansBadgeList_hook = new DomHook("body", true, function() { const now = Date.now(); if (now - lastRun < 400) return; lastRun = now, FansBadgeList_annotateRows(); }), !FansBadgeList_hook.failed) return; } catch (e) { if ("function" === typeof ExGuard_warn) ExGuard_warn("粉丝牌页", "浮层监听挂不上,退回有上限轮询", e); } } if ("function" !== typeof ExGuard_pollFor) return; ExGuard_pollFor(function() { const rows = document.querySelectorAll(".fans-badge-list tr"); return rows.length > 1 ? document.querySelector(".fans-badge-list") : null; }, function() { FansBadgeList_annotateRows(); }, { intervalMs: 500, maxTries: 20, tag: "粉丝牌页", quiet: true }); } function FansBadgeList_annotateRows() { let nowTime = new Date().getTime(); let items = document.querySelectorAll(".fans-badge-list tr"); if (items.length <= 1) return false; let annotated = 0; let skipped = 0; for (let i = 1; i < items.length; i++) { let item = items[i]; if (item.dataset && "1" === item.dataset.exFansAnnotated) continue; try { let td = item.getElementsByTagName("td")[1]; if (!td) { skipped++; continue; } let raw = item.getAttribute("data-fans-gbdgts"); let tt = 1e3 * Number(raw); if (!raw || !isFinite(tt) || tt <= 0) { skipped++; continue; } let ttStr = dateFormat("yyyy-MM-dd hh:mm:ss", new Date(tt)); let days = Math.floor((nowTime - tt) / 864e5); let longTerm = days >= 300; if (td.innerHTML += ` 已获取 ${days} 天
` + ttStr, item.dataset) item.dataset.exFansAnnotated = "1"; annotated++; } catch (e) { skipped++; } } if (skipped > 0 && "function" === typeof ExGuard_warn) ExGuard_warn("粉丝牌页", "有 " + skipped + " 行结构不符合预期(缺列或缺获取时间),已跳过;成功注释 " + annotated + " 行"); return true; } let FansContinue_refreshBound = false; function initPkg_FansContinue() { initPkg_FansContinue_Dom(), initPkg_FansContinue_Func(), FansContinue_bindRefresh(); } function FansContinue_bindRefresh() { if (FansContinue_refreshBound) return; FansContinue_refreshBound = true, document.addEventListener("DouyuEx:thirdLevelOpened", function(ev) { if (ev = ev && ev.detail && ev.detail.panel, !ev || !ev.classList) return; if (ev.classList.contains("fans-continue-panel")) updateFansContinuePanel(); }); } function initPkg_FansContinue_Dom() { FansContinue_insertIcon(); } function FansContinue_insertIcon() { let a = document.createElement("div"); a.className = "fans-continue", a.innerHTML = ''; let b = document.getElementsByClassName("ex-panel__wrap")[0]; if (b) b.insertBefore(a, b.childNodes[0]); } function initPkg_FansContinue_Func() { let btn = document.getElementsByClassName("fans-continue")[0]; if (btn) btn.addEventListener("click", function(e) { e.stopPropagation(); let panel = document.querySelector(".fans-continue-panel"); if (!panel) createFansContinuePanel(), panel = document.querySelector(".fans-continue-panel"); if (panel) if ("block" === panel.style.display || "flex" === panel.style.display) panel.style.setProperty("display", "none", "important"); else { if ("function" === typeof openMiuixPanelCentered) openMiuixPanelCentered(panel, btn); else panel.style.setProperty("display", "flex", "important"); updateFansContinuePanel(); } }); } function updateFansContinuePanel() { var badgeNameEl = document.getElementById("fans-panel-badge-name"); var badgeCountEl = document.getElementById("fans-panel-badge-count"); var stickCountEl = document.getElementById("fans-panel-stick-count"); var statusEl = document.getElementById("fans-panel-badge-status"); var listEl = document.getElementById("fans-panel-badge-list"); if (badgeNameEl) badgeNameEl.textContent = "读取中…"; if (listEl) listEl.innerHTML = ExUI.empty("正在读取粉丝牌…", "fans-panel__badge-empty"); if ("function" === typeof fetchUserBackpackGifts) fetchUserBackpackGifts(rid, function(list) { var stickCount = 0; for (var idx = 0; idx < list.length; idx++) if ("268" === list[idx].id || "2358" === list[idx].id) { stickCount = list[idx].count; break; } if (stickCountEl) stickCountEl.textContent = String(stickCount); }); else if ("function" === typeof getBagGifts) getBagGifts(rid, function(ret) { var list = ret && ret.data && ret.data.list || []; var stickCount = 0; for (var idx = 0; idx < list.length; idx++) if (268 == list[idx].id || 2358 == list[idx].id) { stickCount = list[idx].count; break; } if (stickCountEl) stickCountEl.textContent = String(stickCount); }); fetch("https://www.douyu.com/member/cp/getFansBadgeList", { method: "GET", mode: "no-cors", cache: "default", credentials: "include" }).then(res => res.text()).then(html => { var badges = FansContinue_parseBadges(html); if (badgeCountEl) badgeCountEl.textContent = String(badges.length); if (!badges.length) { if (badgeNameEl) badgeNameEl.textContent = "无粉丝牌"; if (statusEl) statusEl.textContent = "无需续牌"; if (listEl) listEl.innerHTML = ExUI.empty("当前没有粉丝牌", "fans-panel__badge-empty"); return; } if (html = badges.filter(function(b) { return b.worn; })[0] || null, badgeNameEl) badgeNameEl.textContent = html ? html.name + " · Lv." + html.level : "未佩戴(按最高亲密度展示)"; if (statusEl) { var todaySum = 0; for (var ti = 0; ti < badges.length; ti++) todaySum += badges[ti].today || 0; statusEl.textContent = "+" + Math.round(10 * todaySum) / 10, statusEl.title = "今天在这些牌子上的亲密度合计"; } if (listEl) { var html2 = ""; for (var i = 0; i < badges.length; i++) { var b = badges[i]; var meta = "Lv." + b.level; if (isFinite(b.intimacy)) meta += " · 亲密 " + b.intimacy; if (meta += " · 今日 +" + (b.today || 0), isFinite(b.rank)) meta += " · 排名 " + b.rank; html2 += '
' + '
' + '' + FansContinue_escape(b.name) + (b.worn ? '佩戴中' : "") + "" + '' + FansContinue_escape(meta) + "" + "
" + '" + "
"; } listEl.innerHTML = html2; } }).catch(function() { if (badgeNameEl) badgeNameEl.textContent = "读取失败"; if (listEl) listEl.innerHTML = ExUI.empty("粉丝牌列表请求失败", "fans-panel__badge-empty"); }); } async function FansContinue_setWear(rid, type, btn) { if (!rid) return; if (btn) btn.disabled = true, btn.textContent = 1 === type ? "佩戴中…" : "卸下中…"; try { const ctn = "function" === typeof getCCN ? getCCN() : ""; const res = await fetch("https://www.douyu.com/member/cp/operBadge", { method: "POST", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: "rid=" + encodeURIComponent(rid) + "&type=" + type + "&ctn=" + encodeURIComponent(null == ctn ? "" : ctn) }); const data = await res.json(); const okFlag = data && (0 === Number(data.result) || 0 === Number(data.error)); if (okFlag) showMessage(1 === type ? "已佩戴该粉丝牌" : "已取消佩戴", "success"), updateFansContinuePanel(); else if (showMessage("操作失败:" + (data && (data.msg || data.result || data.error) || "接口无响应"), "error"), btn) btn.disabled = false, btn.textContent = 1 === type ? "佩戴" : "卸下"; } catch (e) { if ("function" === typeof ExGuard_warn) ExGuard_warn("续牌", "佩戴操作失败", e); if (showMessage("操作失败:请求异常", "error"), btn) btn.disabled = false, btn.textContent = 1 === type ? "佩戴" : "卸下"; } } function FansContinue_parseBadges(html) { var out = []; try { var doc = new DOMParser().parseFromString(html, "text/html"); var wrap = doc.getElementsByClassName("fans-badge-list")[0]; var last = wrap ? wrap.lastElementChild : null; if (!last || !last.children) return out; var rows = last.children; for (var i = 0; i < rows.length; i++) { var tr = rows[i]; if (!tr || "TR" !== tr.tagName) continue; var name = ""; var link = tr.querySelector("a[title]"); if (link) name = (link.getAttribute("title") || "").trim(); if (!name) { var txt = (tr.textContent || "").replace(/\s+/g, " ").trim(); name = txt.split(" ")[0] || "未知徽章"; } var level = parseInt(tr.getAttribute("data-fans-level"), 10); var intimacy = parseFloat(tr.getAttribute("data-fans-intimacy")); var rank = parseInt(tr.getAttribute("data-fans-rank"), 10); var tds = tr.children; var todayText = tds && tds.length > 3 ? (tds[3].textContent || "").trim() : ""; var today = parseFloat(todayText); var rowText = (tr.textContent || "").replace(/\s+/g, " "); out.push({ rid: tr.getAttribute("data-fans-room") || "", name: name, level: isNaN(level) ? 0 : level, intimacy: isNaN(intimacy) ? NaN : intimacy, today: isNaN(today) ? 0 : today, rank: isNaN(rank) ? NaN : rank, worn: -1 !== rowText.indexOf("取消佩戴") }); } } catch (e) { if ("function" === typeof ExGuard_warn) ExGuard_warn("续牌", "粉丝牌列表解析失败", e); } return out; } function FansContinue_escape(s) { return String(null == s ? "" : s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); } function createFansContinuePanel() { if (document.querySelector(".fans-continue-panel")) return; var p = document.createElement("div"); p.className = "fans-continue-panel miuix-modal"; var fcAssets = [ { label: "已有粉丝牌", value: "-", id: "fans-panel-badge-count", valueCls: "" }, { label: "背包荧光棒", value: "-", id: "fans-panel-stick-count", valueCls: "" }, { label: "今日亲密", value: "+0", id: "fans-panel-badge-status", valueCls: " fans-panel__status--ok" } ]; if (fcAssets = fcAssets.map(function(it) { return ExUI.stack({ legacy: "fans-panel__asset-item", html: '' + it.label + "" + '' + it.value + "" }); }).join(""), p.innerHTML = ExUI.stack({ legacy: "miuix-modal__body", html: ExUI.card({ title: "徽章与资产", asideHtml: '当前佩戴', body: fcAssets, legacy: ExUI.lg("card", { body: "fans-panel__asset-grid" }) }) + ExUI.card({ title: "粉丝牌明细", asideHtml: '亲密值 · 今日', body: ExUI.empty("打开面板时读取…", "fans-panel__badge-empty"), bodyId: "fans-panel-badge-list", legacy: ExUI.lg("card", { body: "fans-panel__badge-list" }) }) + ExUI.card({ title: "续牌赠送配置", body: '' + '
' + '' + '根 (0 = 平均分配背包余量)' + "
", legacy: ExUI.lg("card", { body: "fans-panel__input-group" }) }) + ExUI.stack({ html: ExUI.btn("立即开始续牌", { variant: "primary", id: "fans-panel-start-btn", legacy: ExUI.LG.submitBtn }), cls: "ui-actions" }) }), document.body.appendChild(p), "function" === typeof ensureMiuixPanelHeader) ensureMiuixPanelHeader(p, "一键续牌"); if ("function" === typeof ExUI_stickActionBar) ExUI_stickActionBar(p); try { updateFansContinuePanel(); } catch (e) { console.warn("[DouyuEx] 续牌面板初次读取失败", e); } let saved = localStorage.getItem("ExSave_FansContinue"); if (saved) { let inp = p.querySelector("#fans-panel-stick-input"); if (inp) inp.value = saved; } const badgeListEl = p.querySelector("#fans-panel-badge-list"); if (badgeListEl) badgeListEl.addEventListener("click", function(ev) { const btn = ev && ev.target && ev.target.closest ? ev.target.closest(".fans-panel__badge-wear") : null; if (!btn) return; ev.stopPropagation(); const rid = btn.getAttribute("data-rid"); const worn = "1" === btn.getAttribute("data-worn"); FansContinue_setWear(rid, worn ? 2 : 1, btn); }); let startBtn = p.querySelector("#fans-panel-start-btn"); if (startBtn) startBtn.addEventListener("click", function(e) { if (e.stopPropagation(), e = document.getElementById("fans-panel-stick-input")?.value || "0", e = Number(e), Number.isNaN(e) || e < 0) e = 0; localStorage.setItem("ExSave_FansContinue", String(e)), FansContinue_startSend(e); }); } function FansContinue_startSend(sendNum) { let giftId = 0; let count = 0; getBagGifts(rid, ret => { let chunkNum = ret.data?.list?.length || 0; if (0 == chunkNum) return showMessage("背包礼物为空", "error"), void 0; for (let i = 0; i < chunkNum; i++) if (268 == ret.data.list[i].id || 2358 == ret.data.list[i].id) { giftId = ret.data.list[i].id, count = ret.data.list[i].count; break; } if (0 == giftId) return showMessage("没有足够的荧光棒道具", "error"), void 0; fetch("https://www.douyu.com/member/cp/getFansBadgeList", { method: "GET", mode: "no-cors", cache: "default", credentials: "include" }).then(res => res.text()).then(async doc => { doc = new DOMParser().parseFromString(doc, "text/html"); let a = doc.getElementsByClassName("fans-badge-list")[0]?.lastElementChild; if (!a) return showMessage("未能获取粉丝牌列表", "error"), void 0; let n = a.children.length; if (0 === n) return showMessage("当前没有粉丝牌", "info"), void 0; if (0 == sendNum) sendNum = Math.floor(count / n); if (sendNum <= 0) sendNum = 1; showMessage(`正在为 ${n} 个房间一键续牌,每间 ${sendNum} 根...`, "info"); for (let i = 0; i < n; i++) { let targetRid = a.children[i].getAttribute("data-fans-room"); await sleep(250); try { let data = await sendGift_bag(giftId, sendNum, targetRid); if (data && "success" === data.msg) showMessage(`【续牌】房间 ${targetRid} 赠送成功`, "success"); else showMessage(`【续牌】房间 ${targetRid} 赠送失败: ` + (data?.msg || "错误"), "error"); } catch (err) { console.warn("[DouyuEx] 续牌单间异常:", targetRid, err); } } showMessage("一键续牌执行完毕!", "success"), updateFansContinuePanel(); }).catch(err => { showMessage("粉丝牌请求失败", "error"); }); }); } async function sendGift_bag(gid, count, targetRid) { const res = await fetch("https://www.douyu.com/japi/prop/donate/mainsite/v1", { method: "POST", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: "propId=" + gid + "&propCount=" + count + "&roomId=" + targetRid + "&bizExt=%7B%22yzxq%22%3A%7B%7D%7D" }); return await res.json(); } window.createFansContinuePanel = createFansContinuePanel, window.updateFansContinuePanel = updateFansContinuePanel; let followListHook; let followListBusy = false; let followListRetryBound = false; let followListRetryTimer = null; const FOLLOWLIST_LIMIT = 10; function initPkg_FollowList() { let tries = 0; let intID = setInterval(() => { if (getValidDom([ ".Header-follow-content", "#js-backpack-enter" ])) { if (followListHook = new DomHook(".Header-follow-content", false, handleFollowList), followListHook && followListHook.observer) return clearInterval(intID), FollowList_bindRetry(), void 0; followListHook = null; } if (++tries > 120) clearInterval(intID), console.warn("[DouyuEx] 关注列表容器等不到,列表增强本次不启用"); }, 1e3); } function handleFollowList(m) { let activeDom = document.getElementsByClassName("Header-follow-tab is-active")[0]; if (!activeDom) return; let active = activeDom.innerText; if ("正在直播" !== active) return FollowList_resetOwnNodes(), FollowList_setNativeVisible(true), void 0; let panel = document.getElementsByClassName("Header-follow-listWrap"); if (0 == panel.length) return; setNewFollowList(panel[0]); } function FollowList_bindRetry() { if (followListRetryBound) return; followListRetryBound = true, document.addEventListener("mouseover", function(e) { let target = e.target; if (!target || 1 !== target.nodeType || !target.closest) return; let menu = target.closest(".public-DropMenu"); if (!menu || (menu.textContent || "").indexOf("关注") < 0) return; clearTimeout(followListRetryTimer), followListRetryTimer = setTimeout(function() { let panel = document.getElementsByClassName("Header-follow-listWrap")[0]; if (panel && panel.getBoundingClientRect().height > 0) setNewFollowList(panel); }, 300); }, true); } function FollowList_setNativeVisible(visible) { let listBox = document.getElementsByClassName("Header-follow-listBox")[0]; if (listBox) listBox.style.display = visible ? "" : "none"; } function FollowList_resetOwnNodes() { document.querySelectorAll("#refreshFollowList, .ExFollowListItem, .ExFollowListEmpty").forEach(function(node) { node.remove(); }); } function FollowList_esc(value) { if ("undefined" !== typeof ExUI && ExUI && "function" === typeof ExUI.esc) return ExUI.esc(value); return String(null == value ? "" : value); } function FollowList_isVisibleRoom(item) { if (!item || "object" !== typeof item) return false; if (void 0 !== item.show_status && null !== item.show_status && 1 !== item.show_status && "1" !== item.show_status && true !== item.show_status) return false; if (void 0 !== item.videoLoop && null !== item.videoLoop && 0 !== item.videoLoop && "0" !== item.videoLoop && false !== item.videoLoop) return false; return true; } function FollowList_buildHtml(list, loadInCurrentPage) { let html = `
长按弹出同屏播放
`; let nowTime = Math.floor(Date.now() / 1e3); let rendered = 0; for (let i = 0; i < list.length; i++) { let item = list[i]; if (!FollowList_isVisibleRoom(item)) continue; let rid = FollowList_esc(item.room_id); let nickname = FollowList_esc(item.nickname); let roomName = FollowList_esc(item.room_name); let online = FollowList_esc(item.online); let avatar = FollowList_esc(String(null == item.avatar_small ? "" : item.avatar_small).replace("_big", "_small")); let showTime = Number(item.show_time); let played = isFinite(showTime) && showTime > 0 ? `已播${formatSeconds(Math.max(0, nowTime - showTime))}` : ""; if (html += `
  • ${nickname}

    ${online}${roomName}

    ${nickname}${played}

  • `, ++rendered >= FOLLOWLIST_LIMIT) break; } if (0 === rendered) html += `
  • 当前没有在播的关注
  • `; return { html: html, count: rendered }; } function FollowList_fail(message) { FollowList_setNativeVisible(true), showMessage("【关注列表】" + message, "error"); } async function setNewFollowList(panel) { if (!panel || followListBusy) return; followListBusy = true; try { let loadInCurrentPage = await GM_getValue("Ex_LoadInCurrentPage", false); let request = getFollowList(); let followList = "function" === typeof ExGuard_withTimeout ? await ExGuard_withTimeout(request, 8e3, "关注列表") : await request; if (!followList) return FollowList_fail("读取失败 · 可能是登录态失效或网络问题,稍后再试"), void 0; if ("0" !== String(followList.error)) return FollowList_fail("接口返回异常(error=" + followList.error + "),已保留原生列表"), void 0; if (!followList.data || !Array.isArray(followList.data.list)) return FollowList_fail("返回数据不完整,已保留原生列表"), void 0; let view = FollowList_buildHtml(followList.data.list, loadInCurrentPage); FollowList_resetOwnNodes(), panel.insertAdjacentHTML("beforeend", view.html), FollowList_setNativeVisible(false), FollowList_bindItems(panel); } catch (e) { ExGuard_warn("关注列表", "渲染失败,已保留原生列表", e), FollowList_fail("渲染失败,已保留原生列表"); } finally { followListBusy = false; } } function FollowList_bindItems(panel) { const loadInCurrentPageCheckbox = panel.querySelector("#loadInCurrentPageCheckbox"); if (loadInCurrentPageCheckbox) loadInCurrentPageCheckbox.addEventListener("change", async e => { const isChecked = e.target.checked; await GM_setValue("Ex_LoadInCurrentPage", isChecked), showMessage(`【关注列表】已${isChecked ? "开启" : "关闭"}当前页加载功能(${isChecked ? "当前页面直接加载关注的直播间" : "使用新网页打开关注的直播间"})`, "info"); }); let followListItems = panel.querySelectorAll(".ExFollowListItem"); for (let i = 0; i < followListItems.length; i++) { let cclick = new CClick(followListItems[i]); cclick.click(async event => { event.preventDefault(); const shouldLoadInCurrentPage = await GM_getValue("Ex_LoadInCurrentPage", false); const targetUrl = "https://www.douyu.com/" + followListItems[i].getAttribute("rid"); if (shouldLoadInCurrentPage) window.location.href = targetUrl; else openPage(targetUrl, true); }), followListItems[i].addEventListener("mousedown", event => { if (1 == event.button) openPage("https://www.douyu.com/" + followListItems[i].getAttribute("rid"), false); }); } } function getFollowList() { return new Promise(resolve => { fetch("https://www.douyu.com/wgapi/livenc/liveweb/follow/list?sort=1&cid1=0", { method: "GET", credentials: "include" }).then(res => res.json()).then(ret => { resolve(ret); }).catch(e => { console.warn("[DouyuEx] 关注列表读取失败:", e && e.message), resolve(null); }); }); } let ImageDanmaku_pollOpts = { intervalMs: 1e3, maxTries: 60, tag: "图片弹幕" }; function initPkg_ImageDanmaku() { try { initPkg_ImageDanmaku_Dom(); } catch (e) { ExGuard_warn("图片弹幕", "图标插入失败(不影响弹幕图片渲染)", e); } try { initPkg_ImageDanmaku_Func(); } catch (e) { ExGuard_warn("图片弹幕", "功能初始化失败", e); } } function initPkg_ImageDanmaku_Dom() { const chatTool = document.getElementsByClassName("ChatToolBar__right")[0]; if (!chatTool) return ExGuard_warn("图片弹幕", "聊天工具栏还没渲染,本次不插入上传图标"), false; const dom = document.createElement("div"); return dom.className = "BarrageFilter", dom.title = "添加图片弹幕(仅安装插件的用户可见)", dom.innerHTML = ` `, chatTool.insertBefore(dom, chatTool.firstChild), true; } function ImageDanmaku_getChatInput() { return document.getElementsByClassName("ChatSend-txt")[0] || null; } function initPkg_ImageDanmaku_Func() { const chatDom = ImageDanmaku_getChatInput(); if (!chatDom) ExGuard_warn("图片弹幕", "聊天输入框还没渲染,图片弹幕的上传功能本次不启用"); document.addEventListener("paste", event => { try { const inputDom = ImageDanmaku_getChatInput(); if (!inputDom) return; if (document.activeElement !== inputDom) return; const clipboard = event.clipboardData || event.originalEvent && event.originalEvent.clipboardData; if (!clipboard || !clipboard.items) return; const items = clipboard.items; for (let index = 0; index < items.length; index++) { const item = items[index]; if (!item) continue; if ("file" === item.kind && "string" === typeof item.type && 0 === item.type.indexOf("image/")) { const file = item.getAsFile ? item.getAsFile() : null; if (!file) continue; ImageDanmaku_uploadAndInsert(file, inputDom); break; } } } catch (e) { ExGuard_warn("图片弹幕", "粘贴图片时处理失败", e); } }), ExGuard_pollFor(function() { return document.getElementById("js-barrage-list"); }, function() { new DomHook("#js-barrage-list", false, m => { if (!m || m.length <= 0) return; if (!m[0].addedNodes || m[0].addedNodes.length <= 0) return; let dom = m[0].addedNodes[0]; if (!dom || !dom.getElementsByClassName) return; const contentDom = dom.getElementsByClassName("Barrage-content"); if (!contentDom || 0 === contentDom.length) return; const text = contentDom[0].innerHTML; if (!text || -1 === text.indexOf("[DouyuEx图片")) return; contentDom[0].innerHTML = text.replace(/\[DouyuEx图片(.*?)\]/g, (match, str) => getImageDanmakuHtml(str)); }); }, Object.assign({ tag: "图片弹幕" }, ImageDanmaku_pollOpts)), ExGuard_pollFor(function() { return document.getElementsByClassName("danmu-fbb2a3")[0]; }, function() { new DomHook(".danmu-fbb2a3", false, m => { if (!m || m.length <= 0) return; if (!m[0].addedNodes || m[0].addedNodes.length <= 0) return; let dom = m[0].addedNodes[0]; if (!dom || !dom.innerHTML) return; const text = dom.innerHTML; if (-1 === text.indexOf("[DouyuEx图片")) return; dom.innerHTML = text.replace(/\[DouyuEx图片(.*?)\]/g, (match, str) => getImageDanmakuHtml(str)); }); }, Object.assign({ tag: "图片弹幕" }, ImageDanmaku_pollOpts)); const svgDom = document.getElementById("ex-upload-image-svg"); const inputDom = document.getElementById("ex-upload-image-input"); if (svgDom && inputDom) svgDom.addEventListener("click", () => { inputDom.click(); }), inputDom.addEventListener("change", e => { try { const file = e && e.target && e.target.files ? e.target.files[0] : null; if (!file) return; const input = ImageDanmaku_getChatInput(); if (!input) return ExGuard_warn("图片弹幕", "聊天输入框不存在,图片无法插入"), void 0; ImageDanmaku_uploadAndInsert(file, input); } catch (err) { ExGuard_warn("图片弹幕", "选择图片后处理失败", err); } }); else ExGuard_warn("图片弹幕", "上传控件不存在(图标插入失败),本次不绑定点击"); } function ImageDanmaku_uploadAndInsert(file, chatDom) { return new Promise(resolve => { if (!chatDom) return ExGuard_warn("图片弹幕", "聊天输入框不存在,放弃本次上传"), resolve(false), void 0; chatDom.value += "[DouyuEx图片上传中]", ExGuard_withTimeout(getUploadImageUrl(file), 2e4, "图片弹幕").then(url => { if (!url) return ImageDanmaku_removePlaceholder(chatDom), ExGuard_warn("图片弹幕", "图片上传失败,已清掉「图片上传中」占位符"), resolve(false), void 0; addImageDanmaku(url, chatDom), resolve(true); }); }); } function ImageDanmaku_removePlaceholder(chatDom) { if (!chatDom || "string" !== typeof chatDom.value) return; chatDom.value = chatDom.value.replace("[DouyuEx图片上传中]", ""); } function getImageDanmakuHtml(str) { try { if (!isValidImageFile(str)) return ""; const split = str.split("."); if (!split[1]) return ""; const url = decompressImageUrl(split[0]); const realImageUrl = DOMPurify.sanitize(`https://img.douyucdn.cn/data/yuba/weibo/${url.slice(0, 4) + "/" + url.slice(4, 6) + "/" + url.slice(6, 8) + "/" + url}.200x0.` + split[1]); const imgHtml = ``; return DOMPurify.sanitize(imgHtml); } catch (e) { return ExGuard_warn("图片弹幕", "解析图片弹幕地址失败,原样保留文本", e), ""; } } function addImageDanmaku(url, chatDom) { const input = chatDom || ImageDanmaku_getChatInput(); if (!input) return ExGuard_warn("图片弹幕", "聊天输入框不存在,图片地址无处插入"), false; if ("string" !== typeof url || -1 === url.indexOf(".")) return ExGuard_warn("图片弹幕", "上传接口没有返回可用的图片地址,放弃插入"), ImageDanmaku_removePlaceholder(input), false; const split = url.split("."); try { input.value = input.value.replace("[DouyuEx图片上传中]", `[DouyuEx图片${compressImageUrl(split[0])}.${split[1]}]`); } catch (e) { return ExGuard_warn("图片弹幕", "压缩图片地址失败,放弃插入", e), ImageDanmaku_removePlaceholder(input), false; } return true; } function compressImageUrl(text) { let number = BigInt(text); return number.toString(36); } function decompressImageUrl(base36Str) { const base36Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; let decimal = 0n; let multiplier = 1n; for (let i = base36Str.length - 1; i >= 0; i--) { const char = base36Str[i].toUpperCase(); const charIndex = base36Chars.indexOf(char); if (-1 === charIndex) throw new Error(`Invalid base36 character: ` + char); decimal += BigInt(charIndex) * multiplier, multiplier *= 36n; } return decimal.toString(); } function getUploadPreRequest() { return ExGuard_xhr({ method: "GET", url: `https://yuba.douyu.com/wbapi/web/image/preRequest?source=×tamp=` + new Date().getTime(), responseType: "json", headers: { "dy-client": "pc", "dy-token": "undefined" === typeof dyToken ? "" : dyToken }, tag: "图片弹幕" }, 15e3); } function dataURLtoBlob(dataurl) { var dataurl = dataurl.split(","), mime = dataurl[0].match(/:(.*?);/)[1], bstr = atob(dataurl[1]), n = bstr.length, u8arr = new Uint8Array(n); while (n--) u8arr[n] = bstr.charCodeAt(n); return new Blob([ u8arr ], { type: mime }); } function getUploadImageUrl(file) { return new Promise(async (resolve, reject) => { try { const preRequestRet = await getUploadPreRequest(); const preRequestData = preRequestRet && preRequestRet.data; if (!preRequestData) return reject(new Error("上传预处理接口没有返回 data")), void 0; let formData = new FormData(); for (var key in preRequestData) formData.append(key, preRequestData[key]); formData.append("file", file); let request = new XMLHttpRequest(); request.open("POST", "https://img.douyucdn.cn/upload", true), request.timeout = 2e4, request.onload = function() { if (this.status >= 200 && this.status < 400) try { let json = JSON.parse(this.response); if (!json || "string" !== typeof json.uri) return reject(new Error("上传接口返回里没有 uri")), void 0; const ret = json.uri.split("/").pop().split("?")[0]; resolve(ret); } catch (error) { reject(error); } else reject(new Error("图片上传失败:" + this.status)); }, request.onerror = function() { reject(new Error("图片上传网络错误")); }, request.ontimeout = function() { reject(new Error("图片上传超时")); }, request.send(formData); } catch (e) { reject(e); } }); } function getImageDanmakuFromImgSrc(src) { const split = src.split("/"); const url = split.pop(); const split2 = url.split("."); return `[DouyuEx图片${compressImageUrl(split2[0])}.${split2[2]}]`; } function initPkg_LastLiveTime() { initPkg_LastLiveTime_Dom(); } const LASTLIVE_STATUS_RES = [ /show_status\\":(\d+)/, /"show_status":(\d+)/ ]; const LASTLIVE_TIME_RES = [ /show_time\\":(\d+)/, /"show_time":(\d+)/ ]; function LastLiveTime_pickNumber(resList, text) { if (!text) return null; for (let i = 0; i < resList.length; i++) { const m = String(text).match(resList[i]); if (m) return m[1]; } return null; } function LastLiveTime_readPageRoom() { try { const pageWin = "undefined" !== typeof unsafeWindow && unsafeWindow ? unsafeWindow : window; const room = pageWin && pageWin.$ROOM; if (room && "object" === typeof room && void 0 !== room.show_status && null !== room.show_status) { let showTimeNum = null; if (void 0 !== room.show_time && null !== room.show_time) { const n = Number(room.show_time); if (isFinite(n) && n > 0) showTimeNum = n; } return { isLive: "1" === String(room.show_status), showTime: showTimeNum, from: "页面全局 $ROOM" }; } } catch (e) {} const scripts = document && document.scripts ? document.scripts : []; let status = null; let showTime = null; for (let i = 0; i < scripts.length; i++) { let text = ""; try { text = scripts[i].textContent || ""; } catch (e) { continue; } if (!text) continue; if (null === status && -1 !== text.indexOf("show_status")) status = LastLiveTime_pickNumber(LASTLIVE_STATUS_RES, text); if (null === showTime && -1 !== text.indexOf("show_time")) showTime = LastLiveTime_pickNumber(LASTLIVE_TIME_RES, text); if (null !== status && null !== showTime) break; } const parsedTime = null === showTime ? null : parseInt(showTime, 10); return { isLive: "1" === status, showTime: isFinite(parsedTime) && parsedTime > 0 ? parsedTime : null, from: "内联脚本" }; } function initPkg_LastLiveTime_Dom() { const OVERLAY_ID = "ex-LastLiveTime-overlay"; const roomState = LastLiveTime_readPageRoom(); if (roomState.isLive) return; if (null === roomState.showTime) return; const showTime = 1e3 * roomState.showTime; const formattedTime = dateFormat("yyyy-MM-dd hh:mm:ss", new Date(showTime)); const agoText = timeago(showTime); let pollCount = 0; const maxPolls = 180; const timer = setInterval(() => { if (pollCount++, pollCount > maxPolls) { if (clearInterval(timer), "function" === typeof ExGuard_warn) ExGuard_warn("上次开播时间", "等待 .room-Player / .LastLiveTime 超过 " + maxPolls + " 秒,本次不显示"); return; } const player = document.querySelector(".room-Player"); const hasLastLiveTime = document.getElementsByClassName("LastLiveTime").length > 0; if (player && hasLastLiveTime) { if (clearInterval(timer), document.getElementById(OVERLAY_ID)) return; const overlay = document.createElement("div"); overlay.id = OVERLAY_ID, overlay.style.position = "absolute", overlay.style.top = "0", overlay.style.left = "0", overlay.style.width = "100%", overlay.style.height = "100%", overlay.style.display = "flex", overlay.style.justifyContent = "center", overlay.style.alignItems = "center", overlay.style.zIndex = "1", overlay.style.pointerEvents = "none"; const styleBlock = document.createElement("style"); styleBlock.textContent = ` .ex-llt-card { position: relative; padding: 32px 64px; border-radius: 16px; background: rgba(24, 24, 24, 0.65); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border: 1px solid rgba(255, 215, 0, 0.15); box-shadow: 0 16px 32px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.1); text-align: center; font-family: "PingFang SC", "Microsoft YaHei", sans-serif; pointer-events: auto; transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); animation: ex-llt-fade-in 0.5s cubic-bezier(0.25, 0.8, 0.25, 1) forwards; } .ex-llt-card:hover { transform: translateY(-6px) scale(1.02); box-shadow: 0 24px 48px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 215, 0, 0.35); background: rgba(30, 30, 30, 0.75); } @keyframes ex-llt-fade-in { 0% { opacity: 0; transform: translateY(20px) scale(0.95); } 100% { opacity: 1; transform: translateY(0) scale(1); } } .ex-llt-close { position: absolute; top: 14px; right: 14px; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border: none; border-radius: 50%; cursor: pointer; font-size: 22px; line-height: 1; color: rgba(255, 255, 255, 0.6); background: rgba(255, 255, 255, 0.05); transition: all 0.25s cubic-bezier(0.25, 0.8, 0.25, 1); } .ex-llt-close:hover { background: rgba(255, 69, 58, 0.9); color: #fff; transform: rotate(90deg) scale(1.1); box-shadow: 0 4px 12px rgba(255, 69, 58, 0.4); } .ex-llt-title { display: flex; align-items: center; justify-content: center; font-size: 15px; color: #e5b855; margin-bottom: 12px; text-shadow: 0 2px 4px rgba(0,0,0,0.6); font-weight: 500; } .ex-llt-time-ago { font-size: 36px; color: #eebb4d; font-weight: 900; letter-spacing: 2px; margin-bottom: 10px; text-shadow: 0 0 15px rgba(238, 187, 77, 0.35), 0 4px 12px rgba(0,0,0,0.6); } .ex-llt-time-exact { font-size: 15px; color: rgba(229, 184, 85, 0.75); letter-spacing: 1px; font-family: monospace; font-weight: 500; } `, overlay.appendChild(styleBlock); const timeCard = document.createElement("div"); timeCard.className = "ex-llt-card", timeCard.innerHTML = `
    上次开播时间
    ${agoText}
    ${formattedTime}
    `; const closeBtn = timeCard.querySelector(".ex-llt-close"); if (closeBtn) closeBtn.addEventListener("click", e => { e.stopPropagation(), overlay.style.opacity = "0", overlay.style.transition = "opacity 0.3s ease", timeCard.style.transform = "translateY(10px) scale(0.95)", setTimeout(() => overlay.remove(), 300); }); overlay.appendChild(timeCard), player.appendChild(overlay); } }, 1e3); } let levelTaskTimer = null; let levelTaskChecking = false; function initPkg_LevelTask_Timer() { if (levelTaskTimer) return; checkLevelTask(), levelTaskTimer = setInterval(checkLevelTask, 35 * 1e3); } function levelTask_showToast(text) { if ("function" === typeof showMessage) showMessage(text, "success"); else ExGuard_warn("等级任务", "showMessage 不可用,本次奖励提示被吞:" + text); } async function checkLevelTask() { if (levelTaskChecking) return; levelTaskChecking = true; try { const ids = await getLevelTaskIds(rid); if (!ids) return; const tasks = await getLevelTasks(ids); for (let i = 0; i < tasks.length; i++) { const task = tasks[i]; if (!task) continue; try { if (1 == task.taskStatus && 0 == task.prizeStatus) { const result = await finishLevelTask(rid, task.taskId); const prizes = ExGuard_asList(result); for (let j = 0; j < prizes.length; j++) { const prize = prizes[j]; if (!prize) continue; levelTask_showToast(`【等级任务】${task.name} 获得` + prize.name + prize.num); } } } catch (e) { ExGuard_warn("等级任务", `任务 ${task.name || task.taskId} 领取失败,跳过该任务`, e); } } } catch (e) { ExGuard_warn("等级任务", "本轮检查失败,下一轮继续", e); } finally { levelTaskChecking = false; } } function getLevelTaskIds(rid) { return new Promise(resolve => { fetch("https://www.douyu.com/japi/interactnc/web/userLevel/userLevelDetail?rid=" + rid, { method: "GET", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" } }).then(res => res.json()).then(ret => { const taskIds = ret && ret.data ? ret.data.taskIds : null; if (!Array.isArray(taskIds) || 0 === taskIds.length) return ExGuard_warn("等级任务", "任务列表接口没有返回 taskIds,本轮跳过"), resolve(""), void 0; resolve(taskIds.join(",")); }).catch(err => { ExGuard_warn("等级任务", "任务列表请求失败,本轮跳过", err), resolve(""); }); }); } function getLevelTasks(ids) { return new Promise(resolve => { fetch("https://www.douyu.com/japi/tasksys/userLevelTask/getTaskStatus?taskIds=" + ids, { method: "GET", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" } }).then(res => res.json()).then(ret => { const list = ret && ret.data ? ret.data.list : null; if (!Array.isArray(list)) return ExGuard_warn("等级任务", "任务状态接口没有返回 list,本轮跳过"), resolve([]), void 0; resolve(list); }).catch(err => { ExGuard_warn("等级任务", "任务状态请求失败,本轮跳过", err), resolve([]); }); }); } function finishLevelTask(rid, taskid) { return new Promise(resolve => { fetch("https://www.douyu.com/japi/tasksys/userLevelTask/getPrize", { method: "POST", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `ctn=${getCCN()}&taskIds=${taskid}&roomId=` + rid }).then(res => res.json()).then(ret => { const list = ret && ret.data ? ret.data.list : null; if (!Array.isArray(list)) return ExGuard_warn("等级任务", "领取奖励接口没有返回 list,本次按未领到处理"), resolve([]), void 0; resolve(list); }).catch(err => { ExGuard_warn("等级任务", "领取奖励请求失败,本次跳过", err), resolve([]); }); }); } let myLastBarrage = ""; let barrageSendCheckTimer = 0; let selfNodeTrackedCount = 0; let firstSelfNodeAt = 0; let barrageCheckDisabled = false; let barrageCheckNoticeShown = false; let lastSelfEchoAt = 0; let lastSelfEchoChannel = ""; let channelLastSeenAt = {}; const BARRAGE_CHECK_DELAY = 400; const BARRAGE_CHECK_GRACE_MS = 1200; const BARRAGE_ECHO_KEEP_MS = 1e4; const BARRAGE_ECHO_KEEP_MAX = 5; const BARRAGE_BREAKER_MIN_NODES = 5; const BARRAGE_BREAKER_MIN_MS = 5e3; let recentSelfEchoes = []; function rememberSelfEcho(text) { if (!text) return; let now = Date.now(); recentSelfEchoes.push({ text: text, at: now }); while (recentSelfEchoes.length > BARRAGE_ECHO_KEEP_MAX || recentSelfEchoes.length && now - recentSelfEchoes[0].at > BARRAGE_ECHO_KEEP_MS) recentSelfEchoes.shift(); } function hasSelfEcho(text) { if (!text) return false; let now = Date.now(); for (let i = 0; i < recentSelfEchoes.length; i++) { let echo = recentSelfEchoes[i]; if (echo.text === text && now - echo.at <= BARRAGE_ECHO_KEEP_MS) return true; } return false; } function getFieldValue(raw, key) { if (!raw) return ""; let m = String(raw).match(new RegExp("(?:^|/)" + key + "@=([^/]*)")); return m ? m[1] : ""; } function normalizeNick(s) { return String(s || "").replace(/[::]+$/g, "").replace(/\s+/g, " ").trim(); } function normalizeDanmakuText(s) { return String(s || "").replace(/\[DouyuEx图片[^\]]+\]/g, "").replace(/\[(?:emot:dy)?[^\]\s]{1,32}\]/g, "").replace(/\s+/g, " ").trim(); } function isSelfChatmsg(text) { if (!text) return false; let uid = getFieldValue(text, "uid"); if (my_uid && uid && String(uid) === String(my_uid)) return true; let nn = getFieldValue(text, "nn"); if (myName && nn && normalizeNick(nn) === normalizeNick(myName)) return true; if (myName && myName.length >= 3 && -1 !== text.indexOf(myName)) return true; return false; } function extractEchoText(rawPacket) { if (!rawPacket) return ""; let txt = ""; if ("function" === typeof stt_deserialize) { let data = stt_deserialize(rawPacket); if (data && !Array.isArray(data) && "string" === typeof data.txt) txt = data.txt; } if (!txt) if (txt = getFieldValue(rawPacket, "txt"), "function" === typeof stt_unescape) txt = stt_unescape(txt); return normalizeDanmakuText(txt); } function healDanmakuFailedMark(echoText) { if (!echoText) return; let tips = document.querySelectorAll(".ex-danmaku-blocked-tip"); for (let i = 0; i < tips.length; i++) { let tip = tips[i]; let contentEl = tip.previousSibling; if (!contentEl || 1 !== contentEl.nodeType) continue; if (normalizeDanmakuText(contentEl.innerText || contentEl.textContent) !== echoText) continue; if (contentEl.style.textDecoration = "", contentEl.style.textDecorationLine = "", contentEl.style.textDecorationColor = "", tip.parentNode) tip.parentNode.removeChild(tip); } } function recordSelfEcho(text) { myLastBarrage = text; let echoText = extractEchoText(text); rememberSelfEcho(echoText), healDanmakuFailedMark(echoText); } function judgeSelfDanmu(node, contentEl, localText, retry) { if (barrageCheckDisabled) return; if (!myLastBarrage) return; if (hasSelfEcho(localText)) return; let echoText = extractEchoText(myLastBarrage); if (!echoText) return; if (echoText === localText) return; if (retry < 1 && selfNodeTrackedCount >= 2) return setTimeout(function() { judgeSelfDanmu(node, contentEl, localText, retry + 1); }, BARRAGE_CHECK_GRACE_MS), void 0; markDanmakuFailed(node, contentEl, buildCheckDiagnostic(localText, echoText)); } function buildCheckDiagnostic(localText, echoText) { let now = Date.now(); let ago = function(t) { return t ? Math.round((now - t) / 1e3) + "s前" : "无"; }; let chans = Object.keys(channelLastSeenAt).map(function(k) { return k + " " + ago(channelLastSeenAt[k]); }).join(",") || "无"; return "回执文本=" + (echoText || "(空)") + ";本条文本=" + (localText || "(空)") + ";上次自身回执=" + ago(lastSelfEchoAt) + (lastSelfEchoChannel ? "(" + lastSelfEchoChannel + ")" : "") + ";通道活性:" + chans; } function markDanmakuFailed(node, contentEl, diagnostic) { if (!contentEl || !contentEl.parentNode) return; if (contentEl.style.textDecoration = "line-through gray 1px", contentEl.style.textDecorationLine = "line-through", contentEl.style.textDecorationColor = "gray", node.querySelector(".ex-danmaku-blocked-tip")) return; let tip = document.createElement("span"); if (tip.className = "ex-danmaku-blocked-tip", tip.textContent = "(可能发送失败)", tip.style.marginLeft = "4px", tip.style.color = "gray", tip.style.fontSize = "9px", tip.style.cursor = "pointer", tip.title = "该条弹幕发送失败/可能被系统屏蔽,不会被其他人看到(可能会误判)。" + (diagnostic ? "判定依据:" + diagnostic + "(若本条其实已发出,可截图此提示反馈)" : ""), contentEl.parentNode.insertBefore(tip, contentEl.nextSibling), diagnostic) try { console.log("[DouyuEx][弹幕发送检测] 判定发送失败:" + diagnostic); } catch (e) {} } function checkAndTrackSelfDanmu(node) { if (barrageCheckDisabled) return; if (!node || 1 !== node.nodeType) return; let hasSelf = node.classList.contains("is-self") || node.querySelector(".is-self"); if (!hasSelf) return; let contentEl = node.classList.contains("Barrage-content") ? node : node.querySelector(".Barrage-content"); if (!contentEl) return; let localText = normalizeDanmakuText(contentEl.innerText || contentEl.textContent); if (!localText) return; let nickEl = node.querySelector(".Barrage-nickName.is-self") || node.querySelector(".Barrage-nickName") || node.querySelector(".is-self"); let domNick = nickEl ? normalizeNick(nickEl.innerText || nickEl.textContent) : ""; if (domNick && !myName) myName = domNick; if (selfNodeTrackedCount++, !firstSelfNodeAt) firstSelfNodeAt = Date.now(); if (!myLastBarrage && selfNodeTrackedCount >= BARRAGE_BREAKER_MIN_NODES && Date.now() - firstSelfNodeAt > BARRAGE_BREAKER_MIN_MS) { if (barrageCheckDisabled = true, !barrageCheckNoticeShown) if (barrageCheckNoticeShown = true, "function" === typeof showMessage) showMessage("【弹幕发送检测】长时间未收到自身弹幕回执,已自动停用检测以避免误判", "error"); return; } clearTimeout(barrageSendCheckTimer), barrageSendCheckTimer = setTimeout(function() { judgeSelfDanmu(node, contentEl, localText, 0); }, BARRAGE_CHECK_DELAY); } function handleChatmsgPacket(msg, channel) { if (!msg || "string" !== typeof msg) return; if (-1 === msg.indexOf("type@=chatmsg")) return; let ch = channel || "主长连接"; if (channelLastSeenAt[ch] = Date.now(), !isSelfChatmsg(msg)) return; lastSelfEchoAt = Date.now(), lastSelfEchoChannel = ch, recordSelfEcho(msg); } function initPkg_LiveTool_BarrageSendCheck() { window.__onDouyuExChatmsg = handleChatmsgPacket; try { if ("undefined" !== typeof unsafeWindow && unsafeWindow) unsafeWindow.__onDouyuExChatmsg = handleChatmsgPacket; } catch (e) {} try { fetch("https://www.douyu.com/member/cp", { method: "GET", credentials: "include", cache: "no-store" }).then(function(res) { return res.text(); }).then(function(html) { let doc = new DOMParser().parseFromString(html, "text/html"); let el = doc.getElementsByClassName("uname_con")[0]; if (el && el.title && !myName) myName = normalizeNick(el.title); }).catch(function() {}); } catch (e) {} let waitTimer = setInterval(function() { let list = document.getElementById("js-barrage-list") || document.querySelector(".Barrage-list"); if (!list) return; clearInterval(waitTimer); let observer = new MutationObserver(function(mutations) { for (let m = 0; m < mutations.length; m++) { let record = mutations[m]; if (!record.addedNodes || 0 === record.addedNodes.length) continue; for (let i = 0; i < record.addedNodes.length; i++) checkAndTrackSelfDanmu(record.addedNodes[i]); } }); observer.observe(list, { childList: true, subtree: false }); }, 1e3); } function initPkg_LiveTool_BarrageSendCheck_Handle(text) { handleChatmsgPacket(text, "免登录"); } let barrageSpeed_count = 0; function initPkg_LiveTool_BarrageSpeed() { setInterval(() => { let barrageSpeed = Number(barrageSpeed_count / 5 * 60).toFixed(0); barrageSpeed_count = 0; const dom = document.getElementsByClassName("ChatSend-txt")[0]; if (!dom) return; const text = `弹幕时速:${barrageSpeed}条/分`; dom.placeholder = text + " 按↑↓查看历史弹幕 视频ctrl+滚轮缩放", dom.setAttribute("data-placeholder", text); }, 5e3); } function LiveTool_BarrageSpeed_Dom() { let a = document.createElement("div"); a.className = "barrageSpeed"; let html = ` 弹幕时速:**条/分 按↑↓查看历史弹幕 视频ctrl+滚轮缩放 `; a.innerHTML = html; let b = document.getElementsByClassName("Chat")[0]; if (b) b.insertBefore(a, b.childNodes[0]); else ExGuard_warn("弹幕时速", "未找到 .Chat 容器,时速条未插入(该功能不启用)"); } function initPkg_LiveTool_BarrageSpeed_Handle(text) { if ("chatmsg" == getType(text)) barrageSpeed_count++; } let isEnterOn = false; let enterWordList = []; function initPkg_LiveTool_Enter() { LiveTool_Enter_insertDom(), LiveTool_Enter_insertFunc(), initPkg_Enter_Set(); } function LiveTool_Enter_insertDom() { let cellTitle = ExUI.stack({ html: ` 进场欢迎 有人进直播间时自动发欢迎语 `, cls: "ui-cell__title" }); let cellOption = ExUI.stack({ html: ExUI.stack({ html: ` `, cls: "ui-cell__switch" }), cls: "ui-cell__option" }); let panel = ExUI.stack({ html: `
    `, legacy: "enter__panel" }); panel = panel.replace('class="ui-stack ', 'class="'); let a = ExUI.node(ExUI.stack({ html: cellTitle + cellOption + panel, cls: "ui-cell" })); let b = document.getElementsByClassName("livetool")[0]; if (b) b.insertBefore(a, b.childNodes[0]); else ExGuard_warn("自动欢迎", "未找到 .livetool 容器,面板未插入(该模块不启用)"); } function LiveTool_Enter_insertFunc() { document.getElementById("enter__export").addEventListener("click", () => { GM_setClipboard(JSON.stringify(enterWordList)), showMessage("【进场欢迎】导出完毕,已复制到剪贴板", "success"); }), document.getElementById("enter__import").addEventListener("click", () => { PostbirdAlertBox.prompt({ title: "请输入json文本(会覆盖原来的设置)", okBtn: "确定", onConfirm: function(data) { try { let parsed = JSON.parse(data || "[]"); let list = []; if (Array.isArray(parsed)) list = parsed.filter(item => item && "string" === typeof item.word); else if ("object" === typeof parsed && null !== parsed) for (var k in parsed) list.push({ level: Number(parsed[k]?.level || parsed[k]) || 1, word: k }); enterWordList = list, enterWordList.sort((a, b) => b.level - a.level), refreshEnterSelectOptions(), saveData_Enter(), showMessage("【进场欢迎】导入完毕", "success"); } catch (e) { showMessage("【进场欢迎】JSON格式错误", "error"); } }, onCancel: function(data) {} }); }), document.getElementById("enter__switch").addEventListener("click", () => { let ischecked = document.getElementById("enter__switch").checked; if (true == ischecked) isEnterOn = true; else isEnterOn = false; saveData_isEnter(); }), document.getElementById("enter__title").addEventListener("click", () => { let a = document.getElementsByClassName("enter__panel")[0]; if (!a) return ExGuard_warn("自动欢迎", "面板节点缺失,忽略本次展开"), void 0; if ("block" !== a.style.display && "flex" !== a.style.display) a.style.display = "flex", LiveTool_hideDrawerPanelsExcept("enter__panel"); else a.style.display = "none"; }), document.getElementById("enter__select").onclick = function() { if (0 == this.options.length || this.selectedIndex < 0) return; let item = enterWordList[this.selectedIndex]; if (!item) return; document.getElementById("enter__word").value = item.word || "", document.getElementById("enter__level").value = item.level || 1, localStorage.setItem("ExSave_LastEnterWord", item.word || ""); }, document.getElementById("enter__add").addEventListener("click", () => { document.getElementById("enter__select"); let word = document.getElementById("enter__word").value; let level = document.getElementById("enter__level").value; if ("" == word) return; if ("" == level) return; let isExist = false; for (var item of enterWordList) if (Number(level) === Number(item.level)) { isExist = true; break; } if (isExist) return showMessage("【进场欢迎】等级已存在", "error"), void 0; enterWordList.push({ level: level, word: word }), enterWordList.sort((a, b) => b.level - a.level), refreshEnterSelectOptions(), saveData_Enter(); }), document.getElementById("enter__del").addEventListener("click", () => { let select_wordList = document.getElementById("enter__select"); select_wordList.options[select_wordList.selectedIndex].text, enterWordList.splice(select_wordList.selectedIndex, 1), enterWordList.sort((a, b) => b.level - a.level), refreshEnterSelectOptions(), saveData_Enter(); }); } function saveData_Enter() { let data = enterWordList; localStorage.setItem("ExSave_Enter", JSON.stringify(data)); } function saveData_isEnter() { let ridArr = []; let ret = localStorage.getItem("ExSave_isEnter"); if (null != ret) { let retJson = JSON.parse(ret); if ("rooms" in retJson == true) ridArr = retJson.rooms; } let index = ridArr.indexOf(rid); if (true == isEnterOn) { if (-1 == index) ridArr.push(rid); } else if (index > -1) ridArr.splice(index, 1); let data = { rooms: ridArr }; localStorage.setItem("ExSave_isEnter", JSON.stringify(data)); } function refreshEnterSelectOptions() { let select_wordList = document.getElementById("enter__select"); select_wordList.options.length = 0; for (var item of enterWordList) select_wordList.options.add(new Option(`【${item.level}级】` + item.word, "")); } function initPkg_Enter_Set() { let ret = localStorage.getItem("ExSave_Enter"); if ("" == ret) return; if (document.getElementById("enter__select"), null != ret) { let retJson = JSON.parse(ret); if (!Array.isArray(retJson)) retJson = [], saveData_Enter(); enterWordList = retJson, refreshEnterSelectOptions(); } if (ret = localStorage.getItem("ExSave_isEnter"), null != ret) { let retJson = JSON.parse(ret); let ridArr = []; if ("rooms" in retJson == true) ridArr = retJson.rooms; if (-1 == ridArr.indexOf(rid)) isEnterOn = false; else isEnterOn = true; document.getElementById("enter__switch").checked = isEnterOn; } else isEnterOn = false, document.getElementById("enter__switch").checked = isEnterOn; } function initPkg_LiveTool_Enter_Handle(text) { if (false == isEnterOn) return; if ("uenter" == getType(text)) { let uid = getStrMiddle(text, "uid@=", "/"); if (uid == my_uid) return; let nn = getStrMiddle(text, "nn@=", "/"); let level = getStrMiddle(text, "level@=", "/"); for (var item of enterWordList) if (Number(level) >= Number(item.level)) { let reply = String(item.word).replace(//g, nn); sendBarrage(reply); break; } } } let isGiftOn = false; let giftWordList = {}; function initPkg_LiveTool_Gift() { LiveTool_Gift_insertDom(), LiveTool_Gift_insertFunc(), initPkg_Gift_Set(); } function LiveTool_Gift_insertDom() { let cellTitle = ExUI.stack({ html: ` 自动谢礼物 收到礼物后自动发送答谢弹幕 `, cls: "ui-cell__title" }); let cellOption = ExUI.stack({ html: ExUI.stack({ html: ` `, cls: "ui-cell__switch" }), cls: "ui-cell__option" }); let panel = ExUI.stack({ html: `
    `, legacy: "gift__panel" }); panel = panel.replace('class="ui-stack ', 'class="'), panel = panel.replace(/<\/div>$/, "
    "); let a = ExUI.node(ExUI.stack({ html: cellTitle + cellOption + panel, cls: "ui-cell" })); let b = document.getElementsByClassName("livetool")[0]; if (b) b.insertBefore(a, b.childNodes[0]); else ExGuard_warn("自动谢礼物", "未找到 .livetool 容器,面板未插入(该模块不启用)"); } function LiveTool_Gift_insertFunc() { document.getElementById("reply__show_gid").addEventListener("click", () => { console.log(` 背包礼物:http://webconf.douyucdn.cn/resource/common/prop_gift_list/prop_gift_config.json 鱼翅礼物:http://open.douyucdn.cn/api/RoomApi/room/4042402 `), showMessage("请按F12到控制台(console)查看礼物id", "success"); }), document.getElementById("gift__switch").addEventListener("click", () => { let ischecked = document.getElementById("gift__switch").checked; if (true == ischecked) isGiftOn = true; else isGiftOn = false; saveData_isGift(); }), document.getElementById("gift__title").addEventListener("click", () => { let a = document.getElementsByClassName("gift__panel")[0]; if (!a) return ExGuard_warn("自动谢礼物", "面板节点缺失,忽略本次展开"), void 0; if ("block" !== a.style.display && "flex" !== a.style.display) a.style.display = "flex", LiveTool_hideDrawerPanelsExcept("gift__panel"); else a.style.display = "none"; }), document.getElementById("gift__select").onclick = function() { if (0 == this.options.length) return; let giftId = this.options[this.selectedIndex].text; let reply = giftWordList[giftId].reply; document.getElementById("gift__giftId").value = giftId, document.getElementById("gift__reply").value = reply; }, document.getElementById("gift__add").addEventListener("click", () => { let select_wordList = document.getElementById("gift__select"); let giftId = document.getElementById("gift__giftId").value; let reply = document.getElementById("gift__reply").value; if ("" == giftId) return; giftWordList[giftId] = { reply: reply }, select_wordList.options.add(new Option(giftId, "")), saveData_Gift(); }), document.getElementById("gift__del").addEventListener("click", () => { let select_wordList = document.getElementById("gift__select"); let giftId = select_wordList.options[select_wordList.selectedIndex].text; delete giftWordList[giftId], select_wordList.options.remove(select_wordList.selectedIndex), saveData_Gift(); }), document.getElementById("gift__export").addEventListener("click", () => { GM_setClipboard(JSON.stringify(giftWordList)), showMessage("【自动谢礼物】导出完毕,已复制到剪贴板", "success"); }), document.getElementById("gift__import").addEventListener("click", () => { PostbirdAlertBox.prompt({ title: "请输入json文本(会覆盖原来的设置)", okBtn: "确定", onConfirm: function(data) { let select_wordList = document.getElementById("gift__select"); let obj = JSON.parse(data || "{}") || {}; if ("object" == typeof obj) { for (var key in giftWordList = { ...obj }, select_wordList.options.length = 0, giftWordList) if (giftWordList.hasOwnProperty(key)) select_wordList.options.add(new Option(key, "")); saveData_Gift(); } showMessage("【自动谢礼物】导入完毕", "success"); }, onCancel: function(data) {} }); }), document.getElementById("gift__template").addEventListener("click", () => { setAllGiftTemplate(); }); } function saveData_Gift() { let data = giftWordList; localStorage.setItem("ExSave_Gift", JSON.stringify(data)); } function saveData_isGift() { let ridArr = []; let ret = localStorage.getItem("ExSave_isGift"); if (null != ret) { let retJson = JSON.parse(ret); if ("rooms" in retJson == true) ridArr = retJson.rooms; } let index = ridArr.indexOf(rid); if (true == isGiftOn) { if (-1 == index) ridArr.push(rid); } else ridArr.splice(index, 1); let data = { rooms: ridArr }; localStorage.setItem("ExSave_isGift", JSON.stringify(data)); } function initPkg_Gift_Set() { let ret = localStorage.getItem("ExSave_Gift"); if (null != ret) { let retJson = JSON.parse(ret); giftWordList = retJson; let select_wordList = document.getElementById("gift__select"); for (var key in retJson) if (retJson.hasOwnProperty(key)) select_wordList.options.add(new Option(key, "")); } if (ret = localStorage.getItem("ExSave_isGift"), null != ret) { let retJson = JSON.parse(ret); let ridArr = []; if ("rooms" in retJson == true) ridArr = retJson.rooms; if (-1 == ridArr.indexOf(rid)) isGiftOn = false; else isGiftOn = true; document.getElementById("gift__switch").checked = isGiftOn; } else isGiftOn = false, document.getElementById("gift__switch").checked = isGiftOn; } function initPkg_LiveTool_Gift_Handle(text) { if (false == isGiftOn) return; let typeName = getType(text); if ("dgb" === typeName) { let uid = getStrMiddle(text, "uid@=", "/"); if (uid == my_uid) return; let nn = getStrMiddle(text, "nn@=", "/"); let gfid = getStrMiddle(text, "gfid@=", "/"); let gfcnt = getStrMiddle(text, "gfcnt@=", "/"); if (gfid in giftWordList) { let reply = giftWordList[gfid].reply; reply = String(reply).replace(//g, nn), reply = String(reply).replace(//g, gfcnt), sendBarrage(reply); } } else if ("dfobc" === typeName || "dfrbc" === typeName) { let uid = getStrMiddle(text, "uid@=", "/"); if (uid == my_uid) return; let nn = getStrMiddle(text, "nick@=", "/"); let gfid = "dfobc" === typeName ? "开通钻粉" : "续费钻粉"; let gfcnt = "1"; if (gfid in giftWordList) { let reply = giftWordList[gfid].reply; reply = String(reply).replace(//g, nn), reply = String(reply).replace(//g, gfcnt), sendBarrage(reply); } } } async function setAllGiftTemplate() { let ret = {}; let roomGiftObj = {}; let roomGift = await getRoomGiftTemplate(); for (let i = 0; i < roomGift.data.gift.length; i++) { let item = roomGift.data.gift[i]; roomGiftObj[item.id] = { reply: `感谢赠送的${item.name}x` }; } let bagGift = await getBagGiftTemplate(); let bagGiftObj = {}; for (var key in bagGift = bagGift.substring(0, bagGift.length - 2), bagGift = bagGift.replace("DYConfigCallback(", ""), bagGift = JSON.parse(bagGift || "{}") || {}, bagGift.data) bagGiftObj[key] = { reply: `感谢赠送的${bagGift.data[key].name}x` }; let diamondObj = { "开通钻粉": { reply: `感谢开通钻粉` }, "续费钻粉": { reply: `感谢续费钻粉` } }; ret = { ...roomGiftObj, ...bagGiftObj, ...diamondObj }, GM_setClipboard(JSON.stringify(ret)), showMessage("【自动谢礼物】礼物模板生成完毕,已复制到剪贴板,可直接导入", "success"); } function getRoomGiftTemplate() { return new Promise(resolve => { GM_xmlhttpRequest({ method: "GET", url: "http://open.douyucdn.cn/api/RoomApi/room/" + rid, responseType: "json", onload: function(response) { let ret = response.response; resolve(ret); } }); }); } function getBagGiftTemplate() { return new Promise(resolve => { GM_xmlhttpRequest({ method: "GET", url: "http://webconf.douyucdn.cn/resource/common/prop_gift_list/prop_gift_config.json", responseType: "text", onload: function(response) { let ret = response.response; resolve(ret); } }); }); } let timer_closing; let closingNum = 0; function initPkg_LiveTool_LiveNotice() {} function initPkg_LiveTool_LiveNotice_Handle(text) { if ("rss" == getType(text)) { let rid = getStrMiddle(text, "rid@=", "/"); let ss = getStrMiddle(text, "ss@=", "/"); let ivl = getStrMiddle(text, "ivl@=", "/"); if ("1" == ss && "0" == ivl) showMessageWindow("开播提醒", "直播间:" + rid + "开播了,点我签到", () => { signRoom(rid); }); } } function getRoomAvatar() { fetch("https://www.douyu.com/betard/" + rid, { method: "GET", mode: "no-cors", credentials: "include" }).then(res => res.json()).then(ret => { roomAvatar = ret.room.avatar.middle; }).catch(err => { console.log("请求失败!", err); }); } function initPkg_LiveTool() { LiveTool_tryInit("Dom", initPkg_LiveTool_Dom), LiveTool_tryInit("Module", initPkg_LiveTool_Module), LiveTool_tryInit("Func", initPkg_LiveTool_Func), LiveTool_tryInit("DrawerChevron", initPkg_LiveTool_DrawerChevron), LiveTool_tryInit("HandleFunc", initPkg_LiveTool_HandleFunc); } function LiveTool_tryInit(name, fn) { try { if ("function" === typeof fn) fn(); } catch (e) { console.error("[DouyuEx] 直播间工具初始化失败:" + name + " —— " + (e && e.message || e)); } } const LiveTool_DRAWER_PAIRS = [ [ "enter", "enter__panel" ], [ "mute", "mute__panel" ], [ "gift", "gift__panel" ], [ "reply", "reply__panel" ], [ "vote", "vote__panel" ] ]; function LiveTool_hideDrawerPanelsExcept(exceptPanelClass) { for (let i = 0; i < LiveTool_DRAWER_PAIRS.length; i++) { let panelClass = LiveTool_DRAWER_PAIRS[i][1]; if (panelClass === exceptPanelClass) continue; let panel = document.getElementsByClassName(panelClass)[0]; if (panel) panel.style.display = "none"; } } function LiveTool_syncDrawerChevrons() { LiveTool_DRAWER_PAIRS.forEach(function(pair) { let title = document.getElementById(pair[0] + "__title"); if (!title || !title.parentElement) return; let panel = document.getElementsByClassName(pair[1])[0]; let isOpen = !!(panel && ("block" === panel.style.display || "flex" === panel.style.display)); title.parentElement.classList.toggle("is-open", isOpen); }); } function initPkg_LiveTool_DrawerChevron() { let root = document.querySelector(".livetool"); if (!root || root.dataset.chevronBound) return; root.dataset.chevronBound = "1", root.addEventListener("click", function(e) { let t = e.target; if (!(t instanceof Element)) return; if (!t.closest(".ui-cell__title")) return; setTimeout(LiveTool_syncDrawerChevrons, 0); }); } function initPkg_LiveTool_Dom() { LiveTool_insertModal(), LiveTool_insertIcon(); } function initPkg_LiveTool_Module() { let modules = [ [ "Vote", initPkg_LiveTool_Vote ], [ "Enter", initPkg_LiveTool_Enter ], [ "Mute", initPkg_LiveTool_Mute ], [ "Gift", initPkg_LiveTool_Gift ], [ "Reply", initPkg_LiveTool_Reply ], [ "Treasure", initPkg_LiveTool_Treasure ], [ "BarrageSpeed", initPkg_LiveTool_BarrageSpeed ], [ "RankList", initPkg_LiveTool_RankList ], [ "BarrageSendCheck", initPkg_LiveTool_BarrageSendCheck ] ]; for (let i = 0; i < modules.length; i++) LiveTool_tryInit(modules[i][0], modules[i][1]); LiveTool_sortCells(), setTimeout(function() { var body = document.querySelector(".livetool .miuix-modal__body"); if (!body || body.dataset.sortBound) return; body.dataset.sortBound = "1"; var mo = new MutationObserver(function() { mo.disconnect(); try { LiveTool_sortCells(); } finally { mo.observe(body, { childList: true }); } }); mo.observe(body, { childList: true }), LiveTool_sortCells(); }, 0); } function LiveTool_insertModal() { let existing = document.querySelector(".livetool"); if (existing) return; let a = document.createElement("div"); a.className = "livetool miuix-modal"; let b = document.getElementsByClassName("layout-Player-chat")[0] || document.querySelector(".Barrage-main") || document.body; if (b) b.insertBefore(a, b.childNodes[0]); if ("function" === typeof ensureMiuixPanelHeader) ensureMiuixPanelHeader(a, "直播间工具"); } function LiveTool_insertIcon() { let a = document.createElement("div"); a.className = "livetool-icon", a.innerHTML = ''; let b = document.getElementsByClassName("ex-panel__wrap")[0]; if (b) b.insertBefore(a, b.childNodes[0]); else ExGuard_warn("直播间工具", "未找到 .ex-panel__wrap 容器,图标未插入(面板入口不显示)"); } function initPkg_LiveTool_Func() { let icon = document.getElementsByClassName("livetool-icon")[0]; if (!icon) return; icon.addEventListener("click", function() { showExRightPanel("直播间工具", this); }); } function initPkg_LiveTool_HandleFunc() { new Ex_WebSocket_UnLogin(rid, ret => { LiveTool_dispatchBarrage(ret); }); } function LiveTool_barrageHandlers() { return [ [ "开播提醒", initPkg_LiveTool_LiveNotice_Handle ], [ "关键词禁言", initPkg_LiveTool_Mute_Handle ], [ "关键词回复", initPkg_LiveTool_Reply_Handle ], [ "自动谢礼物", initPkg_LiveTool_Gift_Handle ], [ "自动抢宝箱", initPkg_LiveTool_Treasure_Handle ], [ "自动欢迎", initPkg_LiveTool_Enter_Handle ], [ "投票", initPkg_LiveTool_Vote_Handle ], [ "弹幕时速", initPkg_LiveTool_BarrageSpeed_Handle ], [ "排行榜", initPkg_LiveTool_RankList_Handle ], [ "弹幕回执", initPkg_LiveTool_BarrageSendCheck_Handle ] ]; } function LiveTool_reportHandlerError(name, err) { let e = err || {}; console.error("[DouyuEx] 弹幕处理器异常:" + name + " —— " + (e.message || e)); } function LiveTool_dispatchBarrage(ret) { let handlers = LiveTool_barrageHandlers(); let failed = []; for (let i = 0; i < handlers.length; i++) { let name = handlers[i][0]; let fn = handlers[i][1]; if ("function" !== typeof fn) continue; try { let out = fn(ret); if (out && "function" === typeof out.then) out.catch(function(err) { LiveTool_reportHandlerError(name + "(async)", err); }); } catch (err) { failed.push(name), LiveTool_reportHandlerError(name, err); } } return failed; } function LiveTool_parseKeywordRule(key) { let text = String(null == key ? "" : key); let open = text.indexOf("re("); if (-1 === open) return { kind: "literal", word: text }; let close = text.indexOf(")=", open + 3); if (-1 === close) return { kind: "invalid", source: text, reason: "缺少 )=期望匹配" }; return { kind: "regex", source: text.slice(open + 3, close), expect: text.slice(close + 2) }; } function LiveTool_keywordRuleHit(key, txt) { let rule = LiveTool_parseKeywordRule(key); if ("literal" === rule.kind) { if ("" === rule.word) return false; return -1 !== String(txt).indexOf(rule.word); } if ("regex" === rule.kind) { let regObj; try { regObj = new RegExp(rule.source, "g"); } catch (e) { return console.warn("[DouyuEx] 无效的正则关键词规则(已跳过):" + key + " —— " + (e && e.message || e)), false; } let result = regObj.exec(String(txt)); return !!(result && result[0] === rule.expect); } return false; } function getType(str) { return getStrMiddle(str, "type@=", "/"); } function selectOptionByValue(selectId, checkValue) { let select = document.getElementById(selectId); for (let i = 0; i < select.options.length; i++) if (select.options[i].value == checkValue) { select.options[i].selected = true; break; } } function LiveTool_cellHead(cell) { cell = cell.querySelector(".ui-cell__title"); var txt = cell ? String(cell.textContent).replace(/\s+/g, "") : ""; var keys = [ "关键词回复", "关键词禁言", "进场欢迎", "自动谢礼物", "弹幕投票" ]; for (var i = 0; i < keys.length; i++) if (0 === txt.indexOf(keys[i])) return keys[i]; return txt.slice(0, 8); } function LiveTool_sortCells() { var body = document.querySelector(".livetool .miuix-modal__body"); if (!body) return; var rank = { "关键词回复": 1, "关键词禁言": 2, "进场欢迎": 3, "自动谢礼物": 4, "弹幕投票": 5 }; var cells = Array.prototype.slice.call(body.querySelectorAll(":scope > .ui-cell")); if (cells.length < 2) return; cells.forEach(function(c, i) { c.dataset.exIdx = String(i); }); var before = cells.slice(); cells.sort(function(a, b) { var ta = LiveTool_cellHead(a); var tb = LiveTool_cellHead(b); if (ta = rank[ta.trim()] || 99, tb = rank[tb.trim()] || 99, ta !== tb) return ta - tb; return Number(a.dataset.exIdx) - Number(b.dataset.exIdx); }); var same = true; for (var i = 0; i < cells.length; i++) if (cells[i] !== before[i]) { same = false; break; } if (same) return; cells.forEach(function(c) { body.appendChild(c); }); } let isMuteOn = false; let muteWordList = {}; let muteIdList = {}; let muteIdListShow = []; function initPkg_LiveTool_Mute() { LiveTool_Mute_insertDom(), LiveTool_Mute_insertFunc(), initPkg_Mute_Set(); } function LiveTool_Mute_insertDom() { let cellTitle = ExUI.stack({ html: ` 关键词禁言 命中关键词的用户自动禁言 `, cls: "ui-cell__title" }); let cellOption = ExUI.stack({ html: ExUI.stack({ html: ` `, cls: "ui-cell__switch" }), cls: "ui-cell__option" }); let panel = ExUI.stack({ html: `
    `, legacy: "mute__panel" }); panel = panel.replace('class="ui-stack ', 'class="'), panel = panel.replace(/<\/div>$/, "
    "); let a = ExUI.node(ExUI.stack({ html: cellTitle + cellOption + panel, cls: "ui-cell" })); let b = document.getElementsByClassName("livetool")[0]; if (b) b.insertBefore(a, b.childNodes[0]); else ExGuard_warn("关键词禁言", "未找到 .livetool 容器,面板未插入(该模块不启用)"); } function LiveTool_Mute_insertFunc() { document.getElementById("mute__export").addEventListener("click", () => { GM_setClipboard(JSON.stringify(muteWordList)), showMessage("【关键词禁言】导出完毕,已复制到剪贴板", "success"); }), document.getElementById("mute__import").addEventListener("click", () => { PostbirdAlertBox.prompt({ title: "请输入json文本(会覆盖原来的设置)", okBtn: "确定", onConfirm: function(data) { let select_wordList = document.getElementById("mute__select"); let obj = JSON.parse(data || "{}") || {}; if ("object" == typeof obj) { for (var key in muteWordList = { ...obj }, select_wordList.options.length = 0, muteWordList) if (muteWordList.hasOwnProperty(key)) select_wordList.options.add(new Option(key, "")); saveData_Mute(); } showMessage("【关键词禁言】导入完毕", "success"); }, onCancel: function(data) {} }); }), document.getElementById("mute__idlist").addEventListener("click", () => { if (0 == muteIdListShow.length) return showMessage("暂无禁言名单", "warning"), void 0; console.log("【禁言名单】"); for (let i = 0; i < muteIdListShow.length; i++) { let item = muteIdListShow[i]; console.log("id:【" + item.id + "】 | uid:" + item.uid + " | 弹幕:" + item.barrage + " | 检测次数:" + item.count + " | 禁言时长:" + item.time + "分钟 | 禁言时间:" + item.ts); } showMessage("禁言名单已经输出在控制台,请按F12查看", "success"); }), document.getElementById("mute__delmute").addEventListener("click", async () => { if (0 == muteIdListShow.length) return showMessage("暂无禁言名单", "warning"), void 0; if (true != confirm("是否解禁名单上所有的id?")) return; for (let i = 0; i < muteIdListShow.length; i++) { let item = muteIdListShow[i]; await deleteMuteUser(rid, item.uid); } showMessage("解除禁言完毕", "success"); }), document.getElementById("mute__switch").addEventListener("click", () => { let ischecked = document.getElementById("mute__switch").checked; if (true == ischecked) isMuteOn = true; else isMuteOn = false; saveData_isMute(); }), document.getElementById("mute__title").addEventListener("click", () => { let a = document.getElementsByClassName("mute__panel")[0]; if (!a) return ExGuard_warn("关键词禁言", "面板节点缺失,忽略本次展开"), void 0; if ("block" !== a.style.display && "flex" !== a.style.display) a.style.display = "flex", LiveTool_hideDrawerPanelsExcept("mute__panel"); else a.style.display = "none"; }), document.getElementById("mute__select").onclick = function() { if (0 == this.options.length) return; let word = this.options[this.selectedIndex].text; let count = muteWordList[word].count; let time = muteWordList[word].time; document.getElementById("mute__word").value = word, document.getElementById("mute__count").value = count, selectOptionByValue("mute__time", time); }, document.getElementById("mute__add").addEventListener("click", () => { let select_time = document.getElementById("mute__time"); let select_wordList = document.getElementById("mute__select"); let word = document.getElementById("mute__word").value; let count = document.getElementById("mute__count").value; let time = select_time.options[select_time.selectedIndex].value; if ("" == word) return; muteWordList[word] = { count: count, time: time }, select_wordList.options.add(new Option(word, "")), saveData_Mute(); }), document.getElementById("mute__del").addEventListener("click", () => { let select_wordList = document.getElementById("mute__select"); let word = select_wordList.options[select_wordList.selectedIndex].text; delete muteWordList[word], select_wordList.options.remove(select_wordList.selectedIndex), saveData_Mute(); }); } function saveData_Mute() { let data = muteWordList; localStorage.setItem("ExSave_Mute", JSON.stringify(data)); } function saveData_isMute() { let ridArr = []; let ret = localStorage.getItem("ExSave_isMute"); if (null != ret) { let retJson = JSON.parse(ret); if ("rooms" in retJson == true) ridArr = retJson.rooms; } let index = ridArr.indexOf(rid); if (true == isMuteOn) { if (-1 == index) ridArr.push(rid); } else ridArr.splice(index, 1); let data = { rooms: ridArr }; localStorage.setItem("ExSave_isMute", JSON.stringify(data)); } async function initPkg_Mute_Set() { let ret = localStorage.getItem("ExSave_Mute"); if (null != ret) { let retJson = JSON.parse(ret); muteWordList = retJson; let select_wordList = document.getElementById("mute__select"); for (var key in retJson) if (retJson.hasOwnProperty(key)) select_wordList.options.add(new Option(key, "")); } if (ret = localStorage.getItem("ExSave_isMute"), null != ret) { let retJson = JSON.parse(ret); let ridArr = []; if ("rooms" in retJson == true) ridArr = retJson.rooms; if (-1 == ridArr.indexOf(rid)) isMuteOn = false; else isMuteOn = true; document.getElementById("mute__switch").checked = isMuteOn; } else isMuteOn = false, document.getElementById("mute__switch").checked = isMuteOn; } async function initPkg_LiveTool_Mute_Handle(text) { if (false == isMuteOn) return; if ("chatmsg" == getType(text)) { let uid = getStrMiddle(text, "uid@=", "/"); if (uid == my_uid) return; let nn = getStrMiddle(text, "nn@=", "/"); let txt = getStrMiddle(text, "txt@=", "/"); for (var key in muteWordList) { if ("" == key) continue; if (true != LiveTool_keywordRuleHit(key, txt)) continue; let muteItem = muteWordList[key]; if (!muteItem) continue; let maxCount = muteItem.count; let time = muteItem.time; if (muteIdList.hasOwnProperty(nn)) { let nextCount = Number(muteIdList[nn].count) + 1; if (nextCount >= maxCount) { await addMuteUser(rid, nn, time), showMessageWindow("禁言信息", "【" + nn + "】已被禁言" + time + "分钟" + "\n弹幕:" + txt, () => {}); let obj = { id: nn, uid: uid, barrage: txt, time: time, count: 1, ts: String(dateFormat("yyyy年MM月dd日hh时mm分ss秒 ", new Date())) }; muteIdListShow.push(obj), muteIdList[nn].count = 0; } else muteIdList[nn].count = String(nextCount); } else { let nextCount = 1; if (nextCount >= maxCount) { await addMuteUser(rid, nn, time), showMessageWindow("禁言信息", "【" + nn + "】已被禁言" + time + "分钟" + "\n弹幕:" + txt, () => {}); let obj = { id: nn, uid: uid, barrage: txt, time: time, count: 1, ts: String(dateFormat("yyyy年MM月dd日hh时mm分ss秒 ", new Date())) }; muteIdListShow.push(obj); } else muteIdList[nn] = { uid: uid, count: 1 }; } break; } } } function addMuteUser(roomid, name, ban_time) { return new Promise(resolve => { fetch("https://www.douyu.com/room/roomSetting/addMuteUser", { method: "POST", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: "ban_nickname=" + name + "&room_id=" + roomid + "&ban_time=" + ban_time + "&reason=7" }).then(res => res.json()).then(ret => { resolve(ret); }); }); } function deleteMuteUser(roomid, uid) { return new Promise(resolve => { fetch("https://www.douyu.com/room/roomSetting/deleteMuteUser", { method: "POST", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: "room_id=" + roomid + "&uid=" + uid }).then(res => res.json()).then(ret => { resolve(ret); }); }); } function getRoomAdminStatus() { return new Promise(resolve => { fetch("https://www.douyu.com/japi/firepower/apinc/roomAdmin/getStatus?rid=" + rid, { method: "GET", mode: "no-cors", credentials: "include" }).then(res => res.json()).then(ret => { let result = false; if ("0" == ret.error) if ("1" == ret.data.has) result = true; else result = false; else result = false; resolve(result); }).catch(err => { console.log("请求失败!", err); }); }); } let rankListData = { day: {}, week: {}, all: {} }; function initPkg_LiveTool_RankList() { new DomHook(".layout-Player-rankAll", false, m => { let dom = document.getElementsByClassName("RankAllMain-container"); if (dom.length > 0) if (Object.keys(rankListData.all).length > 0) setRankListData("all", document.querySelectorAll(".layout-Player-rankAll .ChatRankWeek-listItem--nickname")); }); } function initPkg_LiveTool_RankList_Handle(text) { if ("ranklist" == getType(text)) { let data = stt_deserialize(text); if (data.list_day) rankListData.day = getRankListData(data.list_day), setRankListData("day", document.querySelectorAll(".layout-Player-rank .ChatDayRank .ChatRankWeek-listItem--nickname")); if (data.list) rankListData.week = getRankListData(data.list), setRankListData("week", document.querySelectorAll(".layout-Player-rank .ChatRankWeek .ChatRankWeek-listItem--nickname")); if (data.list_all) rankListData.all = getRankListData(data.list_all); setRankListData(); } } function setRankListData(rankItemType, rankItems) { if (!rankItems) return; for (let i = "week" === rankItemType ? 10 : 0; i < rankItems.length; i++) { let rankItem = rankItems[i]; let rankItemName = rankItem.innerHTML.split("${rankContent}`; else rankItem.innerHTML = `${rankItemName}${rankContent}`; } } function getRankListData(list) { let obj = {}; for (let i = 0; i < list.length; i++) { let item = list[i]; obj[item.nickname] = Number(item.gold) / 100; } return obj; } let isReplyOn = false; let replyWordList = {}; let isReplyCD = false; let replyCd = 0; function initPkg_LiveTool_Reply() { LiveTool_Reply_insertDom(), LiveTool_Reply_insertFunc(), initPkg_Reply_Set(); } function LiveTool_Reply_insertDom() { let cellTitle = ExUI.stack({ html: ` 关键词回复 弹幕命中关键词时自动回复 `, cls: "ui-cell__title" }); let cellOption = ExUI.stack({ html: ExUI.stack({ html: ` `, cls: "ui-cell__switch" }), cls: "ui-cell__option" }); let panel = ExUI.stack({ html: `
    `, legacy: "reply__panel" }); panel = panel.replace('class="ui-stack ', 'class="'), panel = panel.replace(/<\/div>$/, "
    "); let a = ExUI.node(ExUI.stack({ html: cellTitle + cellOption + panel, cls: "ui-cell" })); let b = document.getElementsByClassName("livetool")[0]; if (b) b.insertBefore(a, b.childNodes[0]); else ExGuard_warn("关键词回复", "未找到 .livetool 容器,面板未插入(该模块不启用)"); } function LiveTool_Reply_insertFunc() { document.getElementById("reply__export").addEventListener("click", () => { GM_setClipboard(JSON.stringify(replyWordList)), showMessage("【关键词回复】导出完毕,已复制到剪贴板", "success"); }), document.getElementById("reply__import").addEventListener("click", () => { PostbirdAlertBox.prompt({ title: "请输入json文本(会覆盖原来的设置)", okBtn: "确定", onConfirm: function(data) { let select_wordList = document.getElementById("reply__select"); let obj = JSON.parse(data || "{}") || {}; if ("object" == typeof obj) { for (var key in replyWordList = { ...obj }, select_wordList.options.length = 0, replyWordList) if (replyWordList.hasOwnProperty(key)) select_wordList.options.add(new Option(key, "")); saveData_Reply(); } showMessage("【关键词回复】导入完毕", "success"); }, onCancel: function(data) {} }); }), document.getElementById("reply__switch").addEventListener("click", () => { replyCd = String(document.getElementById("reply__time").value) || 0; let ischecked = document.getElementById("reply__switch").checked; if (true == ischecked) isReplyOn = true; else isReplyOn = false; saveData_isReply(), saveData_ReplyCd(); }), document.getElementById("reply__title").addEventListener("click", () => { let a = document.getElementsByClassName("reply__panel")[0]; if (!a) return ExGuard_warn("关键词回复", "面板节点缺失,忽略本次展开"), void 0; if ("block" !== a.style.display && "flex" !== a.style.display) a.style.display = "flex", LiveTool_hideDrawerPanelsExcept("reply__panel"); else a.style.display = "none"; }), document.getElementById("reply__select").onclick = function() { if (0 == this.options.length) return; let word = this.options[this.selectedIndex].text; let reply = replyWordList[word].reply; document.getElementById("reply__word").value = word, document.getElementById("reply__reply").value = reply; }, document.getElementById("reply__add").addEventListener("click", () => { let select_wordList = document.getElementById("reply__select"); let word = document.getElementById("reply__word").value; let reply = document.getElementById("reply__reply").value; if ("" == word) return; replyWordList[word] = { reply: reply }, select_wordList.options.add(new Option(word, "")), saveData_Reply(); }), document.getElementById("reply__del").addEventListener("click", () => { let select_wordList = document.getElementById("reply__select"); let word = select_wordList.options[select_wordList.selectedIndex].text; delete replyWordList[word], select_wordList.options.remove(select_wordList.selectedIndex), saveData_Reply(); }); } function saveData_Reply() { let data = replyWordList; localStorage.setItem("ExSave_Reply", JSON.stringify(data)); } function saveData_ReplyCd() { let data = document.getElementById("reply__time").value; localStorage.setItem("ExSave_ReplyCd", data); } function saveData_isReply() { let ridArr = []; let ret = localStorage.getItem("ExSave_isReply"); if (null != ret) { let retJson = JSON.parse(ret); if ("rooms" in retJson == true) ridArr = retJson.rooms; } let index = ridArr.indexOf(rid); if (true == isReplyOn) { if (-1 == index) ridArr.push(rid); } else ridArr.splice(index, 1); let data = { rooms: ridArr }; localStorage.setItem("ExSave_isReply", JSON.stringify(data)); } function initPkg_Reply_Set() { let ret = localStorage.getItem("ExSave_Reply"); if (null != ret) { let retJson = JSON.parse(ret); replyWordList = retJson; let select_wordList = document.getElementById("reply__select"); for (var key in retJson) if (retJson.hasOwnProperty(key)) select_wordList.options.add(new Option(key, "")); } if (ret = localStorage.getItem("ExSave_isReply"), null != ret) { let retJson = JSON.parse(ret); let ridArr = []; if ("rooms" in retJson == true) ridArr = retJson.rooms; if (-1 == ridArr.indexOf(rid)) isReplyOn = false; else isReplyOn = true; document.getElementById("reply__switch").checked = isReplyOn; } else isReplyOn = false, document.getElementById("reply__switch").checked = isReplyOn; if (ret = localStorage.getItem("ExSave_ReplyCd"), null != ret) document.getElementById("reply__time").value = ret; } function initPkg_LiveTool_Reply_Handle(text) { if (false == isReplyOn) return; if ("chatmsg" == getType(text)) { let uid = getStrMiddle(text, "uid@=", "/"); if (uid == my_uid) return; let nn = getStrMiddle(text, "nn@=", "/"); let txt = getStrMiddle(text, "txt@=", "/"); for (var key in replyWordList) { if ("" == key) continue; if (true != LiveTool_keywordRuleHit(key, txt)) continue; let item = replyWordList[key]; if (!item) continue; let reply = String(item.reply).replace(//g, nn); if (reply = String(reply).replace(//g, txt), false == isReplyCD) if (sendBarrage(reply), replyCd > 0) isReplyCD = true, setTimeout(() => { isReplyCD = false; }, 1e3 * replyCd); break; } } } var treasureNum = 0; function initPkg_LiveTool_Treasure() { LiveTool_Treasure_insertModal(); } function LiveTool_Treasure_insertModal() { let a = document.createElement("div"); a.className = "livetool__Treasure", a.id = "Ex_Geetest"; let b = document.getElementsByClassName("Barrage-main")[0]; if (b) b.insertBefore(a, b.childNodes[0]); else ExGuard_warn("自动抢宝箱", "未找到 .Barrage-main 容器,宝箱挂载点未插入(自动抢宝箱不启用)"); } function initPkg_LiveTool_Treasure_Handle(text) { if (false == isGetTreasure) return; if ("tsboxb" == getType(text)) { let ot = getStrMiddle(text, "ot@=", "/"); let rpid = getStrMiddle(text, "rpid@=", "/"); let rid = getStrMiddle(text, "rid@=", "/"); let did = getCookieValue("dy_did"); let timeout = Number(ot) - Math.floor(Date.now() / 1e3); timeout = 1e3 * timeout + getTreasureDelay(); let b = document.getElementById("Ex_Geetest"); if (!b) return ExGuard_warn("自动抢宝箱", "宝箱挂载点不存在,跳过本次抢宝箱(不影响其它功能)"), void 0; treasureNum++; let a = document.createElement("div"); let idName = "Ex_Geetest_no" + String(treasureNum); a.id = idName, b.appendChild(a), setTimeout(() => { getTreasure(rid, rpid, did, idName); }, timeout); } } function Treasure_cleanupNode(idName) { let el = document.getElementById(idName); if (el) el.remove(); } function getTreasure(roomid, rpid, deviceid, idName) { GM_xmlhttpRequest({ method: "POST", url: "https://pcapi.douyucdn.cn/h5nc/member/getRedPacket?token=" + dyToken, data: "room_id=" + roomid + "&package_room_id=" + roomid + "&device_id=" + deviceid + "&packerid=" + rpid + "&version=1", responseType: "json", timeout: 15e3, headers: { "Content-Type": "application/x-www-form-urlencoded" }, onerror: function(err) { ExGuard_warn("自动抢宝箱", "领宝箱请求失败,本次跳过", err), Treasure_cleanupNode(idName); }, ontimeout: function() { ExGuard_warn("自动抢宝箱", "领宝箱请求超时,本次跳过"), Treasure_cleanupNode(idName); }, onload: function(response) { let ret = response && response.response; let data = ret && ret.data; if (!data) return ExGuard_warn("自动抢宝箱", "宝箱接口返回异常,本次跳过"), Treasure_cleanupNode(idName), void 0; if ("-1" == data.code && "0" != data.validate) { let v = null; try { v = JSON.parse(data.geetest.validate_str); } catch (e) { return ExGuard_warn("自动抢宝箱", "验证串解析失败,本次跳过", e), Treasure_cleanupNode(idName), void 0; } if (!v) return Treasure_cleanupNode(idName), void 0; let success = v.success; let challenge = v.challenge; let gt = v.gt; let handler = e => { document.getElementById(idName), e.appendTo("#" + idName), e.onSuccess(() => { let result = e.getValidate(); let geetest_challenge = result.geetest_challenge; let geetest_seccode = result.geetest_seccode; let geetest_validate = result.geetest_validate; let reqData = "room_id=" + roomid + "&package_room_id=" + roomid + "&device_id=" + deviceid + "&packerid=" + rpid + "&version=1"; reqData = reqData + "&geetest_challenge=" + geetest_challenge + "&geetest_validate=" + geetest_validate + "&geetest_seccode=" + encodeURIComponent(geetest_seccode), GM_xmlhttpRequest({ method: "POST", url: "https://pcapi.douyucdn.cn/h5nc/member/getRedPacket?token=" + dyToken, data: reqData, responseType: "json", timeout: 15e3, headers: { "Content-Type": "application/x-www-form-urlencoded" }, onerror: function(err) { ExGuard_warn("自动抢宝箱", "验证后的领奖请求失败", err), Treasure_cleanupNode(idName); }, ontimeout: function() { ExGuard_warn("自动抢宝箱", "验证后的领奖请求超时"), Treasure_cleanupNode(idName); }, onload: function(response) { let ret = response && response.response; let rdata = ret && ret.data; if (!rdata) return ExGuard_warn("自动抢宝箱", "领奖返回异常,本次跳过"), Treasure_cleanupNode(idName), void 0; let msg = ""; if ("" == rdata.prop_id) msg = "鱼丸x" + rdata.silver; else msg = rdata.prop_name + "x" + rdata.prop_count; if ("" != msg) showMessage("【宝箱】获得" + msg, "success"); Treasure_cleanupNode(idName); } }); }); }; if (void 0 != unsafeWindow.initGeetest) unsafeWindow.initGeetest({ gt: gt, challenge: challenge, offline: !success, product: "float" }, handler); else showMessage("宝箱验证初始化失败", "error"); } else { if ("领取失败" != data.msg && "验证码不正确" != data.msg) { let msg = ""; if ("" == data.prop_id) msg = "鱼丸x" + data.silver; else msg = data.prop_name + "x" + data.prop_count; if ("" != msg) showMessage("【宝箱】获得" + msg, "success"); } else showMessage("【宝箱】领取失败", "error"); Treasure_cleanupNode(idName); } } }); } function getTreasure_Verify(challenge, validate, seccode, divId) { let data = "room_id=" + roomid + "&package_room_id=" + roomid + "&device_id=" + deviceid + "&packerid=" + rpid + "&version=1"; data += "&geetest_challenge=" + challenge + "&geetest_validate=" + validate + "&geetest_seccode=" + encodeURIComponent(seccode), GM_xmlhttpRequest({ method: "POST", url: "https://pcapi.douyucdn.cn/h5nc/member/getRedPacket?token=" + dyToken, data: data, responseType: "json", headers: { "Content-Type": "application/x-www-form-urlencoded" }, onload: function(response) { if (response.response, null != document.getElementById(divId)) document.getElementById(divId).remove(); } }); } let isVoteOn = false; let voteList = {}; let voteNameList = {}; let voteWordList = {}; let voteTotalNum = 0; let timer_Vote; let isRepeat = false; function initPkg_LiveTool_Vote() { LiveTool_Vote_insertDom(), LiveTool_Vote_insertDom_VotePanel(), LiveTool_Vote_insertFunc(), initPkg_Vote_Set(); } function LiveTool_Vote_insertDom() { let cellTitle = ExUI.stack({ html: ` 弹幕投票 在直播间发起或参与弹幕投票 `, cls: "ui-cell__title" }); let cellOption = ExUI.stack({ html: ` ` + ExUI.stack({ html: ` `, cls: "ui-cell__switch" }) + ` `, cls: "ui-cell__option" }); let panel = ExUI.stack({ html: `
    `, legacy: "vote__panel" }); panel = panel.replace('class="ui-stack ', 'class="'), panel = panel.replace(/<\/div>$/, "
    "); let a = ExUI.node(ExUI.stack({ html: cellTitle + cellOption + panel, cls: "ui-cell" })); let b = document.getElementsByClassName("livetool")[0]; if (b) b.insertBefore(a, b.childNodes[0]), Vote_syncEmpty(); else ExGuard_warn("弹幕投票", "未找到 .livetool 容器,面板未插入(该模块不启用)"); } function LiveTool_Vote_insertFunc() { document.getElementById("vote__switch").addEventListener("click", () => { let ischecked = document.getElementById("vote__switch").checked; let selectDom = document.getElementById("vote__select"); let title = selectDom.options[selectDom.selectedIndex].text; let options = voteList[title].options; let time = voteList[title].time; if (true == ischecked) { let arr = String(options).split(" "); for (let i = 0; i < arr.length; i++) voteWordList[arr[i]] = { num: 0, index: i }; document.getElementById("vote__repeat").disabled = true, voteTotalNum = 0, initVoteOptionsPanel(title, options), isRepeat = document.getElementById("vote__repeat").checked, isVoteOn = true, timer_Vote = setTimeout(() => { voteNameList = {}, voteWordList = {}, isVoteOn = false, document.getElementById("vote__repeat").disabled = false, document.getElementById("vote__switch").checked = false; }, 1e3 * time); let resultPanel = document.getElementsByClassName("vote__result")[0]; if (resultPanel) resultPanel.style.display = "block"; else ExGuard_warn("弹幕投票", "投票结果面板缺失,本轮的展开已跳过"); } else clearTimeout(timer_Vote), voteNameList = {}, voteWordList = {}, isVoteOn = false, document.getElementById("vote__repeat").disabled = false; }), document.getElementById("vote__title").addEventListener("click", () => { let a = document.getElementsByClassName("vote__panel")[0]; if (!a) return ExGuard_warn("弹幕投票", "面板节点缺失,忽略本次展开"), void 0; if ("block" !== a.style.display && "flex" !== a.style.display) a.style.display = "flex", LiveTool_hideDrawerPanelsExcept("vote__panel"); else a.style.display = "none"; }), document.getElementById("vote__select").onclick = function() { if (0 == this.options.length) return; let title = this.options[this.selectedIndex].text; let options = voteList[title].options; let time = voteList[title].time; document.getElementById("vote__theme").value = title, document.getElementById("vote__options").value = options, document.getElementById("vote__time").value = time; }, document.getElementById("vote__add").addEventListener("click", () => { let select_wordList = document.getElementById("vote__select"); let title = document.getElementById("vote__theme").value; let options = document.getElementById("vote__options").value; let time = document.getElementById("vote__time").value; if ("" == title || "" == options || "" == time) { if ("function" === typeof showMessage) showMessage("主题 / 选项 / 限时都要填", "warning"); return; } if (voteList[title] = { options: options, time: time }, "1" === select_wordList.dataset.empty) select_wordList.remove(0), delete select_wordList.dataset.empty; var opt = document.createElement("option"); opt.textContent = title, opt.value = "", select_wordList.options.add(opt), Vote_syncEmpty(), saveDate_Vote(); }), document.getElementById("vote__del").addEventListener("click", () => { let select_wordList = document.getElementById("vote__select"); if (!select_wordList.options.length || select_wordList.selectedIndex < 0 || "1" === select_wordList.dataset.empty) return; let title = select_wordList.options[select_wordList.selectedIndex].text; delete voteList[title], select_wordList.options.remove(select_wordList.selectedIndex), saveDate_Vote(); }), document.getElementById("vote__show-result").addEventListener("click", () => { let a = document.getElementsByClassName("vote__result")[0]; if ("block" !== a.style.display && "flex" !== a.style.display) a.style.display = "flex"; else a.style.display = "none"; }); } function initPkg_Vote_Set() { document.getElementById("vote__switch").checked = isReplyOn; let ret = localStorage.getItem("ExSave_Vote"); if (null != ret) { let retJson = JSON.parse(ret); voteList = retJson; let select_wordList = document.getElementById("vote__select"); for (var key in retJson) if (retJson.hasOwnProperty(key)) select_wordList.options.add(new Option(key, "")); } } function initPkg_LiveTool_Vote_Handle(text) { if (false == isVoteOn) return; if ("chatmsg" == getType(text)) { let uid = getStrMiddle(text, "uid@=", "/"); let txt = getStrMiddle(text, "txt@=", "/"); if (isRepeat) { if (Object(voteWordList).hasOwnProperty(txt)) voteWordList[txt].num++, voteTotalNum++, changeOptionsData(); } else if (false == Object(voteNameList).hasOwnProperty(uid)) if (Object(voteWordList).hasOwnProperty(txt)) voteNameList[uid] = 0, voteWordList[txt].num++, voteTotalNum++, changeOptionsData(); } } function saveDate_Vote() { let data = voteList; localStorage.setItem("ExSave_Vote", JSON.stringify(data)); } function LiveTool_Vote_insertDom_VotePanel() { let a = document.createElement("div"); a.className = "vote__result"; let panel = `
    投票主题
    X
    `; a.innerHTML = panel; let b = getValidDom([ ".layout-Player-main", "main", ".Barrage-main" ]) || document.body; if (b) b.insertBefore(a, b.childNodes[0]); let box = document.getElementsByClassName("vote__result")[0]; box.onmousedown = function(event) { event.stopPropagation(); let xx = event.clientX - box.offsetLeft; let yy = event.clientY - box.offsetTop; let mouseX; let mouseY; function onMouseMove(event) { event.stopPropagation(), mouseX = event.clientX - xx, mouseY = event.clientY - yy, box.style.left = mouseX + "px", box.style.top = mouseY + "px"; } function onMouseUp(event) { event.stopPropagation(), document.removeEventListener("mousemove", onMouseMove), document.removeEventListener("mouseup", onMouseUp); } document.addEventListener("mousemove", onMouseMove), document.addEventListener("mouseup", onMouseUp); }, document.getElementById("vote__result-close").addEventListener("click", () => { let resultPanel = document.getElementsByClassName("vote__result")[0]; if (resultPanel) resultPanel.style.display = "none"; }); } function initVoteOptionsPanel(theme, options) { document.getElementById("vote__result-theme").innerText = theme, document.getElementById("vote__result-options").innerHTML = "", setVoteOptionsDom(options); } function setVoteOptionsDom(options) { let arr = options.split(" "); let b = document.getElementById("vote__result-options"); for (let i = 0; i < arr.length; i++) { let a = document.createElement("div"); a.className = "vote__option-wrap", a.innerHTML = `
    ${arr[i]}
    `, b.appendChild(a); } } function changeOptionsData() { for (var key in voteWordList) { let item = voteWordList[key]; let domNum = document.getElementsByClassName("vote__option-num")[item.index]; let domBar = document.getElementsByClassName("vote__progress-bar")[item.index]; let percent = String(Number(100 * Number(item.num / voteTotalNum)).toFixed(1)) + "%"; domNum.innerText = `${item.num}(${percent})`, domBar.style.width = percent; } } function Vote_syncEmpty() { var sel = document.getElementById("vote__select"); var del = document.getElementById("vote__del"); if (!sel) return; if (Array.prototype.filter.call(sel.options, function(o) { return "" !== o.value || !o.disabled; }), 0 === sel.options.length) { var o = document.createElement("option"); o.textContent = "暂无预设 · 填好后点添加", o.value = "", o.disabled = true, sel.add(o), sel.selectedIndex = 0, sel.dataset.empty = "1"; } if (del) del.disabled = "1" === sel.dataset.empty; } let myFansBadgeList = []; let lotteryHasNoticed = {}; let lotteryHTML = ""; let isLotteryNotice = false; let timer_lottery = 0; const LOTTERY_MAX_ROOMS = 24; function initPkg_Lottery() { setFansBadgeList().catch(function(e) { ExGuard_warn("抽奖", "粉丝牌列表加载失败(只影响「能否参与」的判断)", e); }); try { initPkg_Lottery_Dom(); } catch (e) { ExGuard_warn("抽奖", "面板或入口图标插入失败", e); } try { initPkg_Lottery_Func(); } catch (e) { ExGuard_warn("抽奖", "交互绑定失败(抽奖面板可能打不开)", e); } Lottery_Set(); } function initPkg_Lottery_Dom() { Lottery_insertModal(), Lottery_insertIcon(); } function Lottery_insertIcon() { let a = document.createElement("div"); a.className = "ex-lottery", a.innerHTML = ''; const b = document.getElementsByClassName("ex-panel__wrap")[0]; if (!b) return ExGuard_warn("抽奖", "胶囊条容器还没渲染,本次不插入入口图标"), false; return b.insertBefore(a, b.childNodes[0]), true; } function Lottery_insertModal() { let existing = document.querySelector(".exlottery"); if (existing) return; let a = document.createElement("div"); a.className = "exlottery miuix-modal"; let noticeRowHtml = ExUI.row({ title: "开启全站抽奖提醒", desc: "发现可参与的在播抽奖时实时通知", trail: '', legacy: ExUI.lg("row") }); let cardsHtml = ExUI.card({ title: "抽奖提醒", body: noticeRowHtml, legacy: ExUI.lg("card", { body: "sign-options-list" }) }) + ExUI.card({ title: "在播抽奖房间", legacy: ExUI.lg("card", { body: "lottery__wrap" }) }); a.innerHTML = ExUI.stack({ legacy: "miuix-modal__body", html: cardsHtml }); let refreshBtnHtml = ``; let refreshDom = ExUI.node(refreshBtnHtml); let heads = a.querySelectorAll(".ui-card__head"); let headDom = heads.length > 1 ? heads[1] : heads[0]; if (headDom && refreshDom) headDom.appendChild(refreshDom); let wrapDom = a.querySelector(".lottery__wrap"); if (wrapDom && wrapDom.parentNode) { let cardDom = wrapDom.parentNode; let nodataDom = ExUI.node('
    暂无数据
    '); if (nodataDom) cardDom.insertBefore(nodataDom, wrapDom); } let b = document.getElementsByClassName("layout-Player-chat")[0] || document.querySelector(".Barrage-main") || document.body; if (b) b.insertBefore(a, b.childNodes[0]); if ("function" === typeof ensureMiuixPanelHeader) ensureMiuixPanelHeader(a, "全站抽奖信息"); } function initPkg_Lottery_Func() { let dom_notice = document.getElementById("lottery-notice"); const iconDom = document.getElementsByClassName("ex-lottery")[0]; if (!iconDom) ExGuard_warn("抽奖", "入口图标不存在,抽奖面板无法打开"); else iconDom.addEventListener("click", function() { try { showExRightPanel("全站抽奖信息", this); let dom = document.getElementsByClassName("lottery__wrap")[0]; if (dom) dom.innerHTML = lotteryHTML; if (!lotteryHTML) initPkg_Lottery_Timer(); } catch (e) { ExGuard_warn("抽奖", "打开抽奖面板失败", e); } }); const refreshDom = document.getElementById("lottery-refresh"); if (!refreshDom) ExGuard_warn("抽奖", "刷新按钮不存在,面板刷新不可用"); else { const refreshHandler = function() { initPkg_Lottery_Timer(); }; refreshDom.addEventListener("click", "function" === typeof debounce ? debounce(refreshHandler, 3e3) : refreshHandler); } function startLotteryTimer() { stopLotteryTimer(), initPkg_Lottery_Timer(), timer_lottery = setInterval(() => { if (isLotteryNotice) initPkg_Lottery_Timer(); else stopLotteryTimer(); }, 6e4); } function stopLotteryTimer() { if (timer_lottery) clearInterval(timer_lottery), timer_lottery = 0; } if (!dom_notice) return ExGuard_warn("抽奖", "提醒开关不存在,抽奖提醒本次不可用"), void 0; dom_notice.addEventListener("click", () => { let ischecked = dom_notice.checked; if (true == ischecked) isLotteryNotice = true, startLotteryTimer(); else isLotteryNotice = false, stopLotteryTimer(); saveData_Lottery(); }); } async function initPkg_Lottery_Timer() { if (Object.keys(lotteryHasNoticed).length > 100) lotteryHasNoticed = {}; let html = ""; const hall = await ExGuard_withTimeout(getExLotteryList(), 15e3, "抽奖"); const list = hall && hall.data ? hall.data.list : null; if (!Array.isArray(list)) return ExGuard_warn("抽奖", "抽奖大厅接口没有返回 list,本次不更新列表"), lotteryHTML = "", lotteryRenderNodata(), void 0; const maxRooms = list.length > LOTTERY_MAX_ROOMS ? LOTTERY_MAX_ROOMS : list.length; if (maxRooms < list.length) ExGuard_warn("抽奖", "在播抽奖过多(" + list.length + " 个),本次只取前 " + LOTTERY_MAX_ROOMS + " 个"); for (let i = 0; i < maxRooms; i++) { const item = list[i]; try { if (0 !== item.status) continue; const lotteryInfo = await ExGuard_withTimeout(getExLotteryInfo(item.room_id), 12e3, "抽奖"); const data = lotteryInfo && lotteryInfo.data ? lotteryInfo.data : null; if (!data || !data.join_condition) { ExGuard_warn("抽奖", "房间 " + item.room_id + " 的抽奖详情不完整,跳过"); continue; } let condition = data.join_condition; let joinText = "command_content" in condition ? `发送弹幕` : `赠送 ${condition.gift_name}(${condition.gift_price})x` + condition.gift_num; let expireTime = Number(data.start_at) + Number(data.join_condition.expire_time); let expireText = getTimeDiff(1e3 * expireTime, new Date().getTime()); expireText = -1 == expireText ? "已结束" : "距结束:" + expireText; let canJoin = -1 !== myFansBadgeList.indexOf(String(item.room_id)) || condition.lottery_range <= 1; if (canJoin && isLotteryNotice) { let keyName = data.prize_name + `|` + data.start_at; if (!(keyName in lotteryHasNoticed)) lotteryHasNoticed[keyName] = 1, showMessage(`【${data.prize_name}x${data.prize_num}】${joinText}`, "special", { timeout: 100 }); } html += `
    ${item.anchor_name}
    ${expireText}
    ${data.prize_name}x${data.prize_num}
    ${joinText}
    ${getJoinCondition(condition)}
    `; } catch (e) { ExGuard_warn("抽奖", "房间 " + item.room_id + " 的抽奖信息获取失败,跳过", e); } } lotteryHTML = html, lotteryRenderNodata(); const dom = document.getElementsByClassName("lottery__wrap")[0]; if (dom) dom.innerHTML = lotteryHTML; } function lotteryRenderNodata() { const domNodata = document.getElementsByClassName("lottery__nodata")[0]; if (!domNodata) return; domNodata.style.display = "" !== lotteryHTML.trim() ? "none" : "block"; } function getJoinCondition(condition) { let ret = ""; switch (condition.lottery_range) { case 0: ret = "所有人可参与"; break; case 1: ret = "关注主播"; break; case 2: ret = "成为粉丝"; break; case 3: ret = "关注主播+成为粉丝"; break; default: break; } return ret; } function getExLotteryList() { return ExGuard_xhr({ method: "GET", url: "https://www.douyu.com/lapi/interact/lottery/getHallList", responseType: "json", tag: "抽奖" }, 15e3); } function getExLotteryInfo(room_id) { return new Promise((resolve, reject) => { fetch("https://www.douyu.com/member/lottery/activity_info?room_id=" + room_id, { method: "GET", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" } }).then(res => res.json()).then(ret => { resolve(ret); }).catch(err => { reject(err); }); }); } async function setFansBadgeList() { let ret = []; try { const doc = await fetch("https://www.douyu.com/member/cp/getFansBadgeList", { method: "GET", mode: "no-cors", cache: "default", credentials: "include" }).then(res => res.text()).then(text => new DOMParser().parseFromString(String(text || ""), "text/html")); const list = doc ? doc.getElementsByClassName("fans-badge-list")[0] : null; if (!list || !list.lastElementChild) return ExGuard_warn("抽奖", "粉丝牌列表结构不符合预期(未登录或页面改版),按「没有粉丝牌」处理"), myFansBadgeList = [], void 0; const last = list.lastElementChild; for (let i = 0; i < last.children.length; i++) { const roomId = last.children[i].getAttribute("data-fans-room"); if (roomId) ret.push(roomId); } myFansBadgeList = ret; } catch (e) { ExGuard_warn("抽奖", "粉丝牌列表请求失败,按「没有粉丝牌」处理", e), myFansBadgeList = []; } } function saveData_Lottery() { let data = { isNotice: isLotteryNotice }; localStorage.setItem("ExSave_Lottery", JSON.stringify(data)); } function Lottery_Set() { const saved = localStorage.getItem("ExSave_Lottery"); if (null == saved) return; let parsed = null; try { parsed = JSON.parse(saved); } catch (e) { return ExGuard_warn("抽奖", "本地存储的抽奖设置已损坏,按未开启处理", e), void 0; } if (parsed && true == parsed.isNotice) { const checkbox = document.getElementById("lottery-notice"); if (checkbox) checkbox.click(); else ExGuard_warn("抽奖", "提醒开关不存在,无法恢复上次的开启状态"); } } function initPkg_Menu() { if ("function" !== typeof GM_registerMenuCommand) { if ("function" === typeof ExGuard_warn) ExGuard_warn("菜单", "当前脚本管理器不支持 GM_registerMenuCommand,右键菜单项不可用(其它功能不受影响)"); return; } GM_registerMenuCommand("检查更新", function() { try { if ("function" === typeof showExRightPanel) return showExRightPanel("版本更新", null, true), void 0; if ("function" === typeof createExUpdatePanel) return createExUpdatePanel(), void 0; } catch (e) { if ("function" === typeof ExGuard_warn) ExGuard_warn("菜单", "打开版本更新面板失败", e); } if ("function" === typeof showMessage) showMessage("【版本更新】面板暂不可用,请刷新页面后从胶囊条进入", "error"); }); } const MONITOR_ENTRY_ENABLED = false; function initPkg_Monitor() { if (!MONITOR_ENTRY_ENABLED) return console.info("[DouyuEx] 「在线弹幕助手」入口已撤下:第三方数据站可用性无保证"), void 0; initPkg_Monitor_Dom(), initPkg_Monitor_Func(); } function initPkg_Monitor_Dom() { Monitor_insertIcon(); } function Monitor_insertIcon() { let a = document.createElement("div"); a.className = "ex-monitor", a.innerHTML = ''; let b = document.getElementsByClassName("ex-panel__wrap")[0]; if (!b) return false; return b.insertBefore(a, b.childNodes[0]), true; } function initPkg_Monitor_Func() { let icon = document.getElementsByClassName("ex-monitor")[0]; if (!icon) return; icon.addEventListener("click", function() { openPage("https://www.douyuex.com/" + String(rid)); }); } const MONTH_COST_CACHE_KEY = "ExSave_MonthCost"; const MONTH_COST_SEE_STATUS_KEY = "ExSave_MonthCost_SeeStatus"; let totalMonthCost = 0; let typeDetail = []; const MONTH_COST_MAX_RETRY = 3; let svg_see = ``; let svg_unsee = ``; let seeStatus = 0; function MonthCost_getContainerDom() { return document.getElementsByClassName("month-cost")[0]; } function MonthCost_getMoneyDom() { return document.getElementById("monthcost__money"); } function MonthCost_getIconDom() { return document.getElementsByClassName("monthcost__icon")[0]; } function MonthCost_renderSeeStatus() { const iconDom = MonthCost_getIconDom(); const moneyDom = MonthCost_getMoneyDom(); if (!iconDom || !moneyDom) return; if (1 === seeStatus) iconDom.innerHTML = svg_see; else moneyDom.innerText = "***", iconDom.innerHTML = svg_unsee; MonthCost_ContentAttrTitle(); } function initPkg_MonthCost() { if (initPkg_MonthCost_Dom(), initPkg_MonthCost_Func(), seeStatus = MonthCost_getSeeStatus(), MonthCost_renderSeeStatus(), 1 === seeStatus) MonthCost_updateCost(); } const MONTH_COST_ANCHOR_SELECTORS = [ "#js-backpack-enter", ".BackpackButton", ".PlayerToolbar-ContentRow .PlayerToolbar-ContentCell" ]; let monthCostAnchorWaiting = false; function MonthCost_findAnchor() { return getValidDom(MONTH_COST_ANCHOR_SELECTORS); } function initPkg_MonthCost_Dom() { if (!MonthCost_insertIcon()) MonthCost_waitAnchor(); } function MonthCost_insertIcon() { if (document.querySelector(".month-cost")) return MonthCost_bindIconClick(), true; let a = document.createElement("span"); a.className = "month-cost", a.innerHTML = ` 本月消费 *** 元 `, a.title = "数据每日更新,根据个人中心消费数据统计"; const anchor = MonthCost_findAnchor(); const parent = anchor ? anchor.parentElement : null; if (!parent) return false; return parent.insertBefore(a, parent.childNodes[0]), MonthCost_bindIconClick(), true; } function MonthCost_waitAnchor() { if (monthCostAnchorWaiting) return true; if ("function" !== typeof ExGuard_pollFor) { if ("function" === typeof ExGuard_warn) ExGuard_warn("本月消费", "没找到背包入口(" + MONTH_COST_ANCHOR_SELECTORS.join(" / ") + "),本月消费入口本次不显示"); return false; } return monthCostAnchorWaiting = true, ExGuard_pollFor(MonthCost_findAnchor, function() { monthCostAnchorWaiting = false, MonthCost_insertIcon(); }, { intervalMs: 500, maxTries: 20, tag: "本月消费" }), true; } function MonthCost_bindIconClick() { const iconDom = MonthCost_getIconDom(); if (!iconDom || "1" === iconDom.dataset.exClickBound) return false; return iconDom.dataset.exClickBound = "1", iconDom.addEventListener("click", () => { if (seeStatus = 1 === seeStatus ? 0 : 1, MonthCost_saveSeeStatus(), MonthCost_renderSeeStatus(), 1 === seeStatus) MonthCost_updateCost(); }), true; } function initPkg_MonthCost_Func() { MonthCost_bindIconClick(); } function MonthCost_saveSeeStatus() { localStorage.setItem(MONTH_COST_SEE_STATUS_KEY, String(seeStatus)); } function MonthCost_getSeeStatus() { let storage = localStorage.getItem(MONTH_COST_SEE_STATUS_KEY); if (null == storage) return 0; return 1 === Number(storage) ? 1 : 0; } function MonthCost_queryData(url) { return new Promise(resolve => { fetch(url, { method: "GET", mode: "no-cors", credentials: "include" }).then(res => res.json()).then(ret => { resolve(ret); }).catch(() => { resolve({ error: -1, data: [] }); }); }); } async function getMonthCost_gift() { const [ start, end ] = getMonthTimeRange(); const [ formattedStart, formattedEnd ] = getFormattedMonthTimeRange(); const direction = 1; let data = []; let hasMoreData = true; let id = 0; let retry = 0; while (hasMoreData) { let url = `https://www.douyu.com/wjapi/nc/exchange/consume/giftList?queryType=0&consumeType=0&startDate=${start}&endDate=${end}&tradeStartDate=${formattedStart}&tradeEndDate=${formattedEnd}&direction=` + direction; if (0 !== id) url += `&id=` + id; let ret = await MonthCost_queryData(url); if (1e3 == ret.error) { if (retry >= MONTH_COST_MAX_RETRY) { if ("function" === typeof ExGuard_warn) ExGuard_warn("本月消费", "消费明细接口连续返回限流(1000),已重试 " + retry + " 次,本次按已取到的数据统计"); hasMoreData = false; break; } retry++, await new Promise(resolve => setTimeout(resolve, 2e3)); continue; } retry = 0; let nextData = ret && ret.data || []; if (!Array.isArray(nextData)) nextData = []; if (data = data.concat(nextData), nextData.length < 20) hasMoreData = false; else { const lastItem = nextData[nextData.length - 1]; if (!lastItem || null == lastItem.id) { if ("function" === typeof ExGuard_warn) ExGuard_warn("本月消费", "消费明细末条缺少 id,无法继续翻页,本次按已取到的数据统计"); hasMoreData = false; } else id = lastItem.id; } } let categorizedData = {}; let skipped = 0; for (var item of data) { if (!item || null == item.amount) { skipped++; continue; } let money = Math.abs(Number(item.amount)); if (!isFinite(money)) { skipped++; continue; } totalMonthCost += money; let key = item.consumeTypeDesc || "其他"; if (!categorizedData[key]) categorizedData[key] = 0; categorizedData[key] += money; } if (skipped > 0 && "function" === typeof ExGuard_warn) ExGuard_warn("本月消费", "有 " + skipped + " 条消费记录结构不完整,已跳过"); for (var title in categorizedData) typeDetail.push({ title: title, money: categorizedData[title] }); } async function getMonthCost_diamondFans() { let type = 0; let page = 1; let data = []; let hasMoreData = true; let now = new Date(); let nowMonth = now.getMonth(); let nowYear = now.getFullYear(); while (hasMoreData) { let url = `https://www.douyu.com/japi/interactnc/web/dFansbadge/myLogs?type=${type}&page=` + page; let ret = await MonthCost_queryData(url); let list = ((ret || {}).data || {}).list || []; if (!Array.isArray(list)) list = []; for (var item of list) { if (!item || null == item.consumeTime) continue; let consumeDate = new Date(1e3 * item.consumeTime); if (isNaN(consumeDate.getTime())) continue; if (consumeDate.getMonth() === nowMonth && consumeDate.getFullYear() === nowYear) data.push(item); } const lastRaw = list[list.length - 1]; if (!lastRaw || null == lastRaw.consumeTime) break; let lastItemDate = new Date(1e3 * lastRaw.consumeTime); if (isNaN(lastItemDate.getTime())) break; if (list.length < 20 || lastItemDate.getMonth() !== nowMonth || lastItemDate.getFullYear() !== nowYear) hasMoreData = false; else page++; } let diamondFansMoney = 0; for (const item of data) { if (!item || null == item.consumeMoney) continue; const money = Math.abs(Number(item.consumeMoney)); if (!isFinite(money)) continue; diamondFansMoney += money; } if (diamondFansMoney > 0) typeDetail.push({ title: "钻粉充值/续费", money: diamondFansMoney }); totalMonthCost += diamondFansMoney; } async function getMonthCost() { totalMonthCost = 0, typeDetail = [], await getMonthCost_gift(), await getMonthCost_diamondFans(), MonthCost_saveData(), MonthCost_ContentAttrTitle(); let moneyDom = MonthCost_getMoneyDom(); if (moneyDom) moneyDom.innerText = String(totalMonthCost / 100); } function MonthCost_ContentAttrTitle() { let containerDom = MonthCost_getContainerDom(); if (!containerDom) return; if (1 === seeStatus && typeDetail.length > 0) { let titleContent = "数据每日更新,根据个人中心消费数据统计。\n--- ---\n"; typeDetail.forEach(detail => { titleContent += `${detail.title}: ${String(detail.money / 100)} 元\n`; }), containerDom.title = titleContent; } else containerDom.title = "数据每日更新,根据个人中心消费数据统计。"; } function getFormattedMonthTimeRange() { let now = new Date(); let nowMonth = now.getMonth(); let nowYear = now.getFullYear(); let monthStartDate = new Date(nowYear, nowMonth, 1); let formatDate = date => { let year = date.getFullYear(); let month = (date.getMonth() + 1).toString().padStart(2, "0"); let day = date.getDate().toString().padStart(2, "0"); return `${year}-${month}-` + day; }; return [ formatDate(monthStartDate), formatDate(now) ]; } function getMonthTimeRange() { let now = new Date(); let nowMonth = now.getMonth(); let nowYear = now.getFullYear(); let monthStartDate = new Date(nowYear, nowMonth, 1); return [ Math.round(new Date(monthStartDate).getTime() / 1e3).toString(), Math.round(now.getTime() / 1e3).toString() ]; } function MonthCost_saveData() { let data = { monthCost: totalMonthCost, updateTime: new Date().getTime(), typeDetail: typeDetail }; let storage = localStorage.getItem(MONTH_COST_CACHE_KEY); if (null !== storage) try { storage = JSON.parse(storage); } catch (e) { storage = {}; } else storage = {}; storage[my_uid] = data, localStorage.setItem(MONTH_COST_CACHE_KEY, JSON.stringify(storage)); } function MonthCost_getData() { let storage = localStorage.getItem(MONTH_COST_CACHE_KEY); if (null !== storage) try { storage = JSON.parse(storage); } catch (e) { storage = {}; } else storage = {}; return storage; } function MonthCost_isStale(updateTime, now) { const cached = updateTime ? new Date(updateTime) : null; if (!cached || isNaN(cached.getTime())) return true; const today = now || new Date(); return !(cached.getFullYear() === today.getFullYear() && cached.getMonth() === today.getMonth() && cached.getDate() === today.getDate()); } function MonthCost_updateCost() { if (1 !== seeStatus) return; let timeDiff = 1; let tmpCost = 0; let tmpTypeDetail = []; let storage = MonthCost_getData(); if (my_uid in storage) { let item = storage[my_uid] || {}; timeDiff = MonthCost_isStale(item.updateTime) ? 1 : 0, tmpCost = Number(item.monthCost) || 0, tmpTypeDetail = Array.isArray(item.typeDetail) ? item.typeDetail : []; let moneyDom = MonthCost_getMoneyDom(); if (moneyDom) moneyDom.innerText = String(tmpCost / 100); typeDetail = tmpTypeDetail, MonthCost_ContentAttrTitle(); } else { let moneyDom = MonthCost_getMoneyDom(); if (moneyDom) moneyDom.innerHTML = ``; } if (timeDiff >= 1) MonthCost_refreshCost(); } function MonthCost_refreshCost() { Promise.resolve().then(getMonthCost).catch(function(e) { if ("function" === typeof ExGuard_warn) ExGuard_warn("本月消费", "统计数据加载失败,金额暂时无法显示", e); const moneyDom = MonthCost_getMoneyDom(); if (moneyDom) moneyDom.innerText = "—"; }); } function NoLogin_applyDouyuRateRecord() { const NO_LOGIN_RATE_KEY = "rateRecordTime_h5p_room"; try { if ("undefined" === typeof localStorage || !localStorage) return; const raw = localStorage.getItem(NO_LOGIN_RATE_KEY); let data = raw ? JSON.parse(raw) : {}; if ("object" !== typeof data || null === data || Array.isArray(data)) data = {}; if ("v" === data.v) return; data.v = "v", localStorage.setItem(NO_LOGIN_RATE_KEY, JSON.stringify(data)); } catch (e) { if ("function" === typeof ExGuard_warn) ExGuard_warn("未登录优化", "无法写入 " + NO_LOGIN_RATE_KEY + "(本地存储不可用)", e); } } function initPkg_NoLogin() { NoLogin_applyDouyuRateRecord(); } let PopupPlayer_refreshBound = false; function PopupPlayer_bindRefresh() { if (PopupPlayer_refreshBound) return; PopupPlayer_refreshBound = true, document.addEventListener("DouyuEx:thirdLevelOpened", function(ev) { if (ev = ev && ev.detail && ev.detail.panel, ev && ev.classList && ev.classList.contains("popup-player-panel")) if ("function" === typeof PopupPlayer_renderMultiPage) PopupPlayer_renderMultiPage(); }); } function initPkg_PopupPlayer() { initPkg_PopupPlayer_Dom(), initPkg_PopupPlayer_Func(); } function initPkg_PopupPlayer_Dom() { PopupPlayer_insertIcon(); } function PopupPlayer_insertIcon() { let a = document.createElement("div"); a.className = "popup-player", a.innerHTML = ''; let b = document.getElementsByClassName("ex-panel__wrap")[0]; if (b) b.insertBefore(a, b.childNodes[0]); } function initPkg_PopupPlayer_Func() { let btn = document.getElementsByClassName("popup-player")[0]; if (btn) btn.addEventListener("click", function(e) { e.stopPropagation(); let panel = document.querySelector(".popup-player-panel"); if (!panel) createPopupPlayerPanel(), panel = document.querySelector(".popup-player-panel"); if (!panel) return; if (PopupPlayer_bindRefresh(), PopupPlayer_renderMultiPage(), "function" === typeof openMiuixPanelCentered) openMiuixPanelCentered(panel, btn); else panel.style.setProperty("display", "flex", "important"); }); } function createPopupPlayerPanel() { if (document.querySelector(".popup-player-panel")) return; var p = document.createElement("div"); p.className = "popup-player-panel miuix-modal"; var roomsCard = ExUI.stack({ cls: "ui-card", html: ExUI.stack({ cls: "ui-card__head", html: ExUI.stack({ cls: "ui-card__title", html: ExUI.esc("同屏里的房间 ") + '1 / 5' }) }) + '' + '' + '" }); var searchCard = ExUI.stack({ cls: "ui-card", html: ExUI.stack({ cls: "ui-card__head", html: ExUI.stack({ cls: "ui-card__title", html: ExUI.esc("添加房间") }) }) + '" + '" + '' }); var actionBar = ExUI.stack({ cls: "ui-actions", html: ExUI.btn("先加一个房间", { variant: "primary", id: "popup-ms-toggle-btn" }) }); if (p.innerHTML = ExUI.stack({ legacy: "miuix-modal__body", html: ExUI.stack({ legacy: "popup-panel__page", html: roomsCard + searchCard + actionBar }) }), roomsCard = p.querySelector(".popup-panel__page"), roomsCard) roomsCard.setAttribute("data-page", "multi"); if (document.body.appendChild(p), "function" === typeof ensureMiuixPanelHeader) ensureMiuixPanelHeader(p, "同屏播放"); if (PopupPlayer_bindEscClose(), "function" === typeof ExUI_stickActionBar) ExUI_stickActionBar(p); else { let actWrap = p.querySelector(".ui-actions"); if (actWrap) actWrap.style.position = "sticky", actWrap.style.bottom = "0", actWrap.style.zIndex = "3", actWrap.style.paddingTop = "8px"; } if (PopupPlayer_renderMultiPage(), PopupPlayer_initMultiPage(p), "function" === typeof MultiScreen_onChange) MultiScreen_onChange(function() { let el = document.querySelector(".popup-player-panel"); if (!el || "none" === el.style.display) return; PopupPlayer_renderMultiPage(), clearTimeout(msPanelSettleTimer), msPanelSettleTimer = setTimeout(function() { msPanelSettleTimer = 0; let e2 = document.querySelector(".popup-player-panel"); if (!e2 || "none" === e2.style.display) return; let ae = document.activeElement; if (ae && "INPUT" === ae.tagName && "range" === ae.type && e2.contains(ae)) return; PopupPlayer_renderMultiPage(); }, 700); }); } function PopupPlayer_initMultiPage(p) { let toggleBtn = p.querySelector("#popup-ms-toggle-btn"); if (toggleBtn) toggleBtn.addEventListener("click", function(e) { if (e.stopPropagation(), MultiScreen_getList().length > 1) MultiScreen_exit(), showMessage("已退出同屏 · 房间都记着,下次点「一键恢复」", "info"); else { if (e = p.querySelector("#popup-ms-search"), e) { if (e.scrollIntoView) e.scrollIntoView({ block: "nearest" }); e.focus(); } return; } PopupPlayer_renderMultiPage(); }); let searchBtn = p.querySelector("#popup-ms-search-btn"); let searchInp = p.querySelector("#popup-ms-search"); if (searchBtn && searchInp) searchBtn.addEventListener("click", function(e) { e.stopPropagation(), PopupPlayer_msSearch(searchInp.value.trim()); }), searchInp.addEventListener("keydown", function(e) { if ("Escape" === e.key || 27 === e.keyCode) return; if (e.stopPropagation(), "Enter" === e.key) PopupPlayer_msSearch(searchInp.value.trim()); }); Array.prototype.forEach.call(p.querySelectorAll(".popup-ms__src-btn"), function(btn) { btn.addEventListener("click", function(e) { if (e.stopPropagation(), "follow" === btn.getAttribute("data-src")) PopupPlayer_msLoadFollow(); else PopupPlayer_msLoadRecent(); }); }); } function PopupPlayer_msEsc(s) { return String(null == s ? "" : s).replace(/&/g, "&").replace(//g, ">"); } function PopupPlayer_renderMultiPage() { let countEl = document.getElementById("popup-ms-count"); let roomsEl = document.getElementById("popup-ms-rooms"); let toggleBtn = document.getElementById("popup-ms-toggle-btn"); if (!countEl || !roomsEl) return; let list = MultiScreen_getList(); let mainRid = MultiScreen_getMainRid(); if (!list.length && mainRid) list = [ { rid: mainRid, nn: "", avatar: "" } ]; countEl.textContent = list.length + " / " + MS_MAX_ROOMS, PopupPlayer_ensureNames(list); let roomsScroll = roomsEl.scrollTop; roomsEl.innerHTML = list.map(function(r) { let rid = PopupPlayer_msEsc(r.rid); let isMain = String(r.rid) === String(mainRid); let vol = Math.round(100 * MultiScreen_getVolume(r.rid, isMain)); let volTitle = isMain ? "当前直播间音量(斗鱼自己的音量条可能把它改回来)" : "这间房的音量(拖到 0 = 静音)"; let volHtml = '' + '' + vol + ""; let head = '"; if (isMain) return '"; let dmOn = MultiScreen_isRoomOn(r.rid, "danmaku"); let vdOn = MultiScreen_isRoomOn(r.rid, "video"); let qnName = MultiScreen_roomQnName(r.rid); let qnTitle = "auto" === MultiScreen_getQuality() ? "当前清晰度(自动:按每块画面的大小选,缩放窗口会跟着变)" : "当前清晰度(手动选择)"; let sub = '"; return '"; }).join(""), roomsEl.scrollTop = roomsScroll, Array.prototype.forEach.call(roomsEl.querySelectorAll(".popup-ms__room-vol"), function(inp) { let sync = function() { let v = Math.max(0, Math.min(100, Number(inp.value) || 0)); inp.style.setProperty("--vol", v + "%"); let num = inp.parentElement ? inp.parentElement.querySelector(".popup-ms__room-volnum") : null; if (num) num.textContent = String(v); MultiScreen_setVolume(inp.getAttribute("data-rid"), v / 100); }; inp.addEventListener("input", sync), [ "mousedown", "click", "pointerdown" ].forEach(function(t) { inp.addEventListener(t, function(e) { e.stopPropagation(); }); }); }), Array.prototype.forEach.call(roomsEl.querySelectorAll(".popup-ms__chip"), function(btn) { let stop = function(e) { e.stopPropagation(); }; [ "mousedown", "click", "pointerdown" ].forEach(function(t) { btn.addEventListener(t, stop); }), btn.addEventListener("click", function() { let rid = btn.getAttribute("data-rid"); let key = btn.getAttribute("data-key"); let wasOn = "true" === btn.getAttribute("aria-pressed"); if (!MultiScreen_setRoomFlag(rid, key, !wasOn)) return; if (PopupPlayer_renderMultiPage(), "danmaku" === key) showMessage(wasOn ? "已停止接收这间房的弹幕" : "已开始接收这间房的弹幕", "info"); else showMessage(wasOn ? "已关掉这间房的画面(只留弹幕)" : "已开始显示这间房的画面", "info"); }); }), Array.prototype.forEach.call(roomsEl.querySelectorAll(".popup-ms__room-del"), function(btn) { btn.addEventListener("click", function(e) { e.stopPropagation(); let row = btn.closest(".popup-ms__room"); MultiScreen_removeRoom(row.getAttribute("data-rid")), PopupPlayer_renderMultiPage(); }); }), PopupPlayer_renderQualityRow(); let restoreEl = document.getElementById("popup-ms-restore"); if (restoreEl) { let saved = MultiScreen_savedConfigForMainRoom(); if (!saved || !saved.length) restoreEl.hidden = true, restoreEl.innerHTML = ""; else { let names = saved.map(function(r) { return r.nn || "房间 " + r.rid; }).join("、"); let totalRooms = saved.length + 1; restoreEl.hidden = false, restoreEl.innerHTML = '上次同屏:' + totalRooms + " 个房间 · " + PopupPlayer_msEsc(names) + "" + ''; let rb = restoreEl.querySelector("#popup-ms-restore-btn"); if (rb) rb.addEventListener("click", function(e) { e.stopPropagation(); let before = MultiScreen_getList().length; MultiScreen_enter(saved), PopupPlayer_renderMultiPage(), showMessage(before > 1 ? "已恢复上次的同屏配置 · " + totalRooms + " 个房间(替换了原来的 " + before + " 间)" : "已恢复上次的同屏配置 · " + totalRooms + " 个房间", "success"); }); } } if (toggleBtn) { let inMulti = list.length > 1; toggleBtn.textContent = inMulti ? "退出同屏" : "去添加房间", toggleBtn.disabled = false, toggleBtn.title = inMulti ? "退出同屏(房间都记着,下次可一键恢复)" : "在下面搜房间号,加进来就自动开始"; } } let msPanelNameAsked = {}; let msPanelSettleTimer = 0; let msPanelEscBound = false; function PopupPlayer_bindEscClose() { if (msPanelEscBound) return; msPanelEscBound = true, document.addEventListener("keydown", function(e) { if ("Escape" !== e.key && 27 !== e.keyCode) return; if ("undefined" !== typeof ExPanel_escBound && ExPanel_escBound) return; if (document.fullscreenElement) return; let panel = document.querySelector(".popup-player-panel"); if (!panel) return; if ("flex" !== panel.style.display && "block" !== panel.style.display) return; if (panel.style.removeProperty("display"), panel.style.setProperty("display", "none", "important"), panel.classList.remove("miuix-modal-in"), "function" === typeof updateDockActiveIndicator) updateDockActiveIndicator(); }); } function PopupPlayer_readPageAnchorName() { let name = ""; try { let el = document.querySelector(".Title-anchorName") || document.querySelector(".anchorName__6NXv9") || document.querySelector('[class*="Title-anchorName"]'); if (el) name = String(el.textContent || "").trim(); } catch (e) { name = ""; } if (name) return name.slice(0, 40); let t = ""; try { t = String(document.title || "").trim(); } catch (e) { t = ""; } if (!t) return ""; if (t = t.replace(/[-_|–—]\s*斗鱼.*$/, "").trim(), t = t.replace(/^直播[·::\s]*/, "").trim(), t = t.replace(/的直播间$/, "").trim(), !t || t.length > 30 || -1 !== t.indexOf("斗鱼")) return ""; return t; } function PopupPlayer_ensureNames(list) { if ("function" !== typeof MultiScreen_resolveRoom) return; let mainRid = "function" === typeof MultiScreen_getMainRid ? String(MultiScreen_getMainRid()) : ""; list.forEach(function(r) { let k = String(r.rid); if (r.nn) return; let cached = "function" === typeof MultiScreen_getMetaName ? MultiScreen_getMetaName(k) : ""; if (cached) return r.nn = cached, void 0; if (mainRid && k === mainRid) { let pageName = PopupPlayer_readPageAnchorName(); if (pageName) return r.nn = pageName, void 0; } if (msPanelNameAsked[k]) return; msPanelNameAsked[k] = 1, MultiScreen_resolveRoom(r, function(meta) { if (!meta || !meta.nn || r.nn) return; r.nn = meta.nn, PopupPlayer_renderMultiPage(); }); }); } function PopupPlayer_chip(rid, key, on, label, title) { return '"; } function PopupPlayer_renderQualityRow() { let box = document.getElementById("popup-ms-quality"); if (!box || "function" !== typeof MultiScreen_getQuality) return; let cur = String(MultiScreen_getQuality()); box.innerHTML = MS_QUALITY_LIST.map(function(q) { let tip = "auto" === q.v ? "自动:按每块画面的实际大小选清晰度,大的给最高、小的自动降(每行右边那枚小标签就是它选出来的)" : "其它房间固定用「" + q.name + "」"; return '"; }).join(""), Array.prototype.forEach.call(box.querySelectorAll('input[name="ms_quality_panel"]'), function(radio) { radio.addEventListener("change", function() { if (radio.checked) MultiScreen_setQuality(radio.value); }); }); } function PopupPlayer_toggleRoom(room) { const rid = String(room && room.rid || ""); const mainRid = String(MultiScreen_getMainRid()); const list = MultiScreen_getList(); const selected = !!rid && list.some(function(r) { return String(r.rid) === rid; }); let ok = false; if (!rid || rid === mainRid); else if (!selected && room && 2 === +room.showStatus) { try { if (document.fullscreenElement && document.exitFullscreen) { const pr = document.exitFullscreen(); if (pr && pr.catch) pr.catch(function() {}); } } catch (e) {} showMessage("主播还没开播 · 等他上线再加", "warning"); } else if (!selected && list.length >= MS_MAX_ROOMS) showMessage("同屏最多 " + MS_MAX_ROOMS + " 个房间 · 先去掉一间再加", "warning"); else { if (selected) MultiScreen_removeRoom(rid); else MultiScreen_addRoom(room); ok = true; } return ok; } function PopupPlayer_msRenderResults(rows, emptyTip) { let box = document.getElementById("popup-ms-results"); if (!box) return; let keepScroll = box.scrollTop; if (!rows.length) return box.innerHTML = "" === emptyTip ? "" : '", void 0; let selected = {}; MultiScreen_getList().forEach(function(r) { selected[String(r.rid)] = 1; }), box.innerHTML = rows.map(function(r) { let rid = String(r.rid); let on = selected[rid]; let flag = on ? '同屏里' : 2 === +r.showStatus ? '没开播' : ""; return '"; }).join(""), box.scrollTop = keepScroll, Array.prototype.forEach.call(box.querySelectorAll(".popup-ms__result"), function(row) { row.addEventListener("click", function() { let rid = row.getAttribute("data-rid"); let room = rows.filter(function(x) { return String(x.rid) === rid; })[0]; if (!room) return; if (PopupPlayer_toggleRoom(room)) PopupPlayer_renderMultiPage(), PopupPlayer_msRenderResults(rows, ""); }); }); } function PopupPlayer_msSearch(kw) { if (!String(null == kw ? "" : kw).trim()) return showMessage("请输入房间号、链接或主播名", "info"), void 0; let q = String(kw).trim(); let linkMatch = q.match(/douyu\.com\/(\d+)/i); if (linkMatch) return PopupPlayer_msQuickAdd(linkMatch[1]), void 0; if (/^\d+$/.test(q)) return PopupPlayer_msQuickAdd(q), void 0; let box = document.getElementById("popup-ms-results"); if (box) box.innerHTML = ''; fetch("https://www.douyu.com/japi/search/api/getSearchRec?kw=" + encodeURIComponent(kw) + "&type=all", { method: "GET", mode: "no-cors", credentials: "include" }).then(function(r) { return r.json(); }).then(function(ret) { let d = ret && ret.data || ret || {}; let arr = d.roomResult || d.room && d.room.list || []; let rows = arr.map(function(it) { return { rid: String(it.rid || it.room_id || ""), nn: it.nickName || it.nickname || it.room_name || "", avatar: it.avatar || it.avatar_small || "", showStatus: void 0 === it.isLive ? void 0 : it.isLive ? 1 : 2, sub: it.cateName || "", hot: it.hot || "" }; }).filter(function(r) { return r.rid; }); PopupPlayer_msRenderResults(rows, "没找到相关直播间"); }).catch(function() { showMessage("没搜成 · 网络有点慢,检查一下网络再点一次「搜索」", "error"); }); } function PopupPlayer_msQuickAdd(rid) { let id = String(rid); if (!id) return; if (id === String(MultiScreen_getMainRid())) return showMessage("这就是当前直播间,已经在同屏里了", "info"), void 0; let exists = MultiScreen_getList().some(function(r) { return String(r.rid) === id; }); if (exists) return showMessage("这间房已经在同屏里了", "info"), void 0; if (PopupPlayer_toggleRoom({ rid: id, nn: "", avatar: "", showStatus: void 0 })) PopupPlayer_renderMultiPage(), PopupPlayer_msRenderResults([], ""); } function PopupPlayer_msLoadFollow() { if ("function" !== typeof getFollowList) return showMessage("读不到关注列表 · 这个功能没装上,刷新页面再试", "error"), PopupPlayer_msRenderResults([], "读不到关注列表 · 这个功能没装上,刷新页面再试"), void 0; PopupPlayer_msRenderResults([], "正在加载关注列表…"), getFollowList().then(function(ret) { if (!ret || "0" !== String(ret.error)) return PopupPlayer_msRenderResults([], "读不到关注列表 · 可能还没登录,也可能网络慢;点一次「关注在播」重试"), showMessage("读不到关注列表 · 可能是还没登录,也可能是网络有点慢", "error"), void 0; let arr = ret.data && ret.data.list || []; let rows = arr.filter(function(it) { return "1" == it.show_status && "0" == it.videoLoop; }).map(function(it) { return { rid: String(it.room_id), nn: it.nickname || it.room_name || "", avatar: String(it.avatar_small || "").replace("_big", "_small"), showStatus: 1, hot: it.online || "" }; }).slice(0, 50); PopupPlayer_msRenderResults(rows, "关注的主播现在都没开播"); }).catch(function() { PopupPlayer_msRenderResults([], "读不到关注列表 · 网络有点慢,稍后再点一次"); }); } function PopupPlayer_msLoadRecent() { let rows = MultiScreen_getRecent(); PopupPlayer_msRenderResults(rows, "还没有同屏观看记录"); } const MS_MERGE_COLORS = { 0: "#ffffff", 1: "#ff2e2e", 2: "#00ccff", 3: "#66ff00", 4: "#ff6600", 5: "#cc00ff", 6: "#f6447f" }; const MS_MERGE_RATE_WINDOW_MS = 1e3; const MS_MERGE_RATE_MAX = 12; const MS_MERGE_RATE_WARN_MS = 5e3; const MS_MERGE_BADGE_COLOR = "rgba(255,255,255,0.22)"; const MS_BUS_HOLD_MAX = 200; const MS_BUS_HOLD_TTL_MS = 15e3; let msMergeSpace = null; let msMergeCtors = {}; let msMergeHost = null; let msMergeHostVia = null; let msMergeInstalled = false; let msMergeConns = []; let msMergeRate = { hits: [] }; let msRateWarnAt = 0; let msMergeStats = { pushed: 0, dropped: 0, noTrack: 0, viaBus: 0, viaHost: 0, held: 0, expired: 0, pushError: 0 }; function initPkg_PopupPlayer_MergeDanmaku_ScriptHook() { scriptHook({ url: "/firstqueue", callback: MergeDanmaku_patchFirstqueue }); } function MergeDanmaku_patchFirstqueue(content) { const anchor = /\.prototype\.convertComment\s*=\s*function\s*\(\s*(\w+)\s*\)\s*\{/; if (!anchor.test(content)) return content; if (-1 !== content.indexOf("__ExDanmuHost")) return content; return content.replace(anchor, function(m, arg) { return m + "window.__ExDanmuHost=this;"; }); } function MergeDanmaku_patchBarrageGroup(content) { const anchor = /(\w+)\.subscribe\(\s*"chatmsg"/; if (!anchor.test(content)) return content; if (-1 !== content.indexOf("__ExChatBus")) return content; return content.replace(anchor, function(m, holder) { return "window.__ExChatBus=" + holder + ";" + m; }); } const MS_BOOTSTRAP_POLL_MS = 1500; const MS_BOOTSTRAP_MAX_TRIES = 40; let msBootTimer = 0; let msBootTries = 0; let msBootExpired = false; let msRendererProto = null; function MergeDanmaku_bootstrap() { if (!msMergeSpace || !msMergeCtors.scroll) { const nodes = MergeDanmaku_findDanmakuNodes(); if (!nodes.length) return false; let wrapped = !!msRendererProto; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (!node.comment) [ "mouseover", "mouseout" ].forEach(function(t) { if (node.comment) return; try { node.dispatchEvent(new MouseEvent(t, { bubbles: true, cancelable: true, view: window })); } catch (e) { try { node.dispatchEvent(new Event(t, { bubbles: true })); } catch (e2) {} } }); const cm = node.comment; if (!cm || !cm.space || "function" !== typeof cm.space.addComment) continue; if (msMergeSpace = cm.space, cm.data && cm.data.type && "function" === typeof cm.constructor) msMergeCtors[cm.data.type] = cm.constructor; if (wrapped = MergeDanmaku_wrapRenderer() || wrapped, msMergeCtors.scroll) break; } if (!msMergeSpace) return false; if (!msMergeCtors.scroll) return false; if (wrapped) console.log("[DouyuEx] 多屏弹幕合并已接入原生引擎"); else console.warn("[DouyuEx] 多屏弹幕合并已接上弹幕引擎,但渲染器没能包装(render 不在原型上?):" + "**飘屏的来源横条不会显示**;聊天区横条不受影响。可用 MergeDanmaku_getStats() 看 rendererWrapped。"); } return true; } function MergeDanmaku_findDanmakuNodes() { const out = []; const nodes = document.querySelectorAll('[class*="danmuItem"]'); for (let i = 0; i < nodes.length; i++) try { if (nodes[i].getBoundingClientRect().width > 0) out.push(nodes[i]); } catch (e) {} return out; } function MergeDanmaku_wrapRenderer() { const rd = msMergeSpace && msMergeSpace._renderer; if (!rd) return false; if (msRendererProto) return true; const proto = Object.getPrototypeOf(rd); if (!proto || "function" !== typeof proto.render) return false; msRendererProto = proto; const orig = proto.render; return proto.render = function(cm) { const r = orig.apply(this, arguments); try { MergeDanmaku_onRendered(cm); } catch (e) {} return r; }, true; } function MergeDanmaku_startBootstrap() { if (msBootTimer) return; msBootExpired = false; let done = false; const tick = function() { const engineReady = MergeDanmaku_bootstrap(); const hostReady = !!MergeDanmaku_getHost(); if (msBootTries++, engineReady && hostReady || msBootTries >= MS_BOOTSTRAP_MAX_TRIES) if (done = true, clearInterval(msBootTimer), msBootTimer = 0, engineReady && !hostReady) console.warn("[DouyuEx] 未能接入播放器自己的弹幕管线(React fiber 上溯失败)," + "其它房间的弹幕暂按自建数据上屏:颜色与来源横条正常,粉丝牌/等级/贵族底不显示。"); else if (!engineReady) msBootExpired = true, console.warn("[DouyuEx] 多屏弹幕合并自举到期:" + msBootTries + " 次仍未抓到原生弹幕引擎" + "(弹幕层关着或没有飘屏都算),来源横条暂不可用;其它房间的弹幕仍会经 socketProxy.socketStream 合入。" + "下一次其它房间的报文到来时会自动再试一次。ready=" + (msMergeSpace && msMergeCtors.scroll ? "true" : "false") + " streamVia=" + (msBusVia || "null")); }; if (tick(), !done) msBootTimer = setInterval(tick, MS_BOOTSTRAP_POLL_MS); } function MergeDanmaku_retryBootstrap() { if (!msBootExpired) return false; if (msMergeSpace && msMergeCtors.scroll) return false; return msBootTries = 0, msBootExpired = false, MergeDanmaku_startBootstrap(), true; } const MS_HOST_FIBER_MAX_HOPS = 120; function MergeDanmaku_walkFiberForHost(el) { if (!el || 1 !== el.nodeType) return null; let cur = el; let key = null; let anchor = null; while (cur && cur !== document.documentElement) { const keys = Object.keys(cur); for (let i = 0; i < keys.length; i++) { const k = keys[i]; if (0 === k.indexOf("__reactFiber$") || 0 === k.indexOf("__reactInternalInstance$")) { key = k, anchor = cur; break; } } if (key) break; cur = cur.parentElement; } if (!key) return null; let fiber = anchor[key]; let hops = 0; while (fiber && hops < MS_HOST_FIBER_MAX_HOPS) { const sn = fiber.stateNode; if (sn && "object" === typeof sn && "function" === typeof sn.convertComment && sn.cm) return sn; fiber = fiber.return, hops++; } return null; } function MergeDanmaku_findHostByFiber() { const seeds = []; const rd = msMergeSpace && msMergeSpace._renderer; if (rd && rd.dom) seeds.push(rd.dom); MergeDanmaku_findDanmakuNodes().forEach(function(n) { seeds.push(n); }); for (let i = 0; i < seeds.length; i++) { const host = MergeDanmaku_walkFiberForHost(seeds[i]); if (host) return host; } return null; } function MergeDanmaku_getHost() { const W = "undefined" !== typeof unsafeWindow ? unsafeWindow : window; const hooked = W.__ExDanmuHost; if (hooked && "function" === typeof hooked.convertComment && hooked.cm) return msMergeHost = hooked, msMergeHostVia = "hook", msMergeHost; if (msMergeHost && "function" === typeof msMergeHost.convertComment && msMergeHost.cm) return msMergeHost; if (msMergeHost = MergeDanmaku_findHostByFiber(), msMergeHostVia = msMergeHost ? "fiber" : null, msMergeHost) console.log("[DouyuEx] 已接入播放器自己的弹幕管线(粉丝牌/等级/贵族样式走原生)"); return msMergeHost; } function initPkg_PopupPlayer_MergeDanmaku() { if (msMergeInstalled) return; msMergeInstalled = true; const W = "undefined" !== typeof unsafeWindow ? unsafeWindow : window; W.MergeDanmaku_getStats = window.MergeDanmaku_getStats, W.__onDouyuExDanmakuRendered = function(cm) { try { MergeDanmaku_onRendered(cm); } catch (e) {} }, MultiScreen_onChange(function() { MergeDanmaku_syncConnections(); }), MergeDanmaku_syncConnections(), MergeDanmaku_startChatWatch(), MergeDanmaku_startBootstrap(); } function MergeDanmaku_onRendered(cm) { if (!cm || !cm.data) return; if (!msMergeSpace && cm.space && "function" === typeof cm.space.addComment) msMergeSpace = cm.space; const t = cm.data.type; if (t && !msMergeCtors[t] && "function" === typeof cm.constructor) msMergeCtors[t] = cm.constructor; const src = cm.data.extraData && cm.data.extraData.exMsSrcName; if (src) return MergeDanmaku_attachBadge(cm, src), void 0; const uid = cm.data.extraData && cm.data.extraData.uid; const text = String(cm.data.text || ""); if (!uid) return; let hit = MergeDanmaku_claimInjected("danmaku", function(x) { return x.uid === String(uid) && x.text === text; }); if (!hit) { const now = Date.now(); hit = MergeDanmaku_claimInjected("danmaku", function(x) { return x.uid === String(uid) && now - x.at < 3e3; }); } if (hit && hit.srcName) MergeDanmaku_attachBadge(cm, hit.srcName); } function MergeDanmaku_attachBadge(cm, name) { const raw = cm.display && cm.display.raw; if (!raw) return; if (raw.querySelector(".ex-ms-badge")) return; const label = MergeDanmaku_badgeText(name); if (!label) return; let host = raw.querySelector('[class*="textWrap"]') || raw; if (MergeDanmaku_isGradient(host) && host.parentElement) host = host.parentElement; const badge = document.createElement("span"); badge.className = "ex-ms-badge", badge.textContent = label, badge.title = String(name), host.insertBefore(badge, host.firstChild); } function MergeDanmaku_isGradient(el) { if (!el) return false; const cls = String(el.className || ""); if (-1 !== cls.indexOf("barrage-gradient")) return true; try { const st = el.style; return !!st && ("text" === st.webkitBackgroundClip || "text" === st.backgroundClip); } catch (e) { return false; } } function MergeDanmaku_badgeText(name) { return String(name || "").trim(); } function MergeDanmaku_connAlive(c) { if (!c || !c.ws) return false; const w = c.ws; try { if (true === w.closed) return false; if ("number" === typeof w.reconnectCount && "number" === typeof w.maxReconnect && w.reconnectCount >= w.maxReconnect) return false; const raw = w.ws; if (!raw) return true; const rs = raw.readyState; if ("number" === typeof rs && (2 === rs || 3 === rs) && "number" !== typeof w.reconnectCount) return false; } catch (e) { return false; } return true; } function MergeDanmaku_syncConnections() { const list = MultiScreen_getList(); const mainRid = MultiScreen_getMainRid(); const want = list.filter(function(r) { if (String(r.rid) === mainRid) return false; return false !== r.danmaku; }); const wantMap = {}; if (want.forEach(function(r) { wantMap[String(r.rid)] = r; }), msMergeConns = msMergeConns.filter(function(c) { if (wantMap[c.rid]) { if (MergeDanmaku_connAlive(c)) return true; try { if (c.ws && "function" === typeof c.ws.close) c.ws.close(); } catch (e) {} return false; } try { if (c.ws && "function" === typeof c.ws.close) c.ws.close(); else if (c.ws && c.ws.ws) c.ws.closed = true, c.ws.ws.close(); } catch (e) {} return false; }), msBusHold.length) msBusHold = msBusHold.filter(function(h) { const rid = h && h.item ? String(h.item.srcRid) : ""; return !!rid && !!wantMap[rid]; }); const have = {}; msMergeConns.forEach(function(c) { have[c.rid] = 1; }), want.forEach(function(r) { const rid = String(r.rid); if (have[rid]) return; if ("function" !== typeof Ex_WebSocket_UnLogin) return; const room = r; const ws = new Ex_WebSocket_UnLogin(rid, function(ret) { MergeDanmaku_handleMsg(room, ret); }); msMergeConns.push({ rid: rid, nn: room.nn || "", ws: ws }); }); } let msBusCache = null; let msBusVia = null; function MergeDanmaku_isSocketData(v) { try { return !!(v && "object" === typeof v && "function" === typeof v.push && "function" === typeof v.decoder && "function" === typeof v.subscribe); } catch (e) { return false; } } function MergeDanmaku_getStream() { const W = "undefined" !== typeof unsafeWindow ? unsafeWindow : window; const sp = W.socketProxy; if (msBusCache) { if (sp && sp.socketStream === msBusCache) if (MergeDanmaku_isSocketData(msBusCache)) return msBusCache; msBusCache = null, msBusVia = null; } if (sp && MergeDanmaku_isSocketData(sp.socketStream)) return msBusCache = sp.socketStream, msBusVia = "socketProxy", msBusCache; let keys = []; try { keys = Object.getOwnPropertyNames(W); } catch (e) { keys = []; } for (let i = 0; i < keys.length; i++) { const k = keys[i]; if ("window" === k || "self" === k || "top" === k || "parent" === k || "frames" === k) continue; let v; try { v = W[k]; } catch (e) { continue; } if (!v || "object" !== typeof v) continue; if (MergeDanmaku_isSocketData(v)) return msBusCache = v, msBusVia = "scan:" + k, msBusCache; let inner; try { inner = v.socketStream; } catch (e) { continue; } if (MergeDanmaku_isSocketData(inner)) return msBusCache = inner, msBusVia = "scan:" + k + ".socketStream", msBusCache; } return null; } let msInjectQueue = []; const MS_INJECT_TTL_MS = 2e4; function MergeDanmaku_rememberInjected(item) { const now = Date.now(); if (msInjectQueue.push({ uid: String(item.uid || ""), text: String(item.text || ""), cid: String(item.cid || ""), srcName: item.srcName || "", at: now }), msInjectQueue = msInjectQueue.filter(function(x) { return now - x.at < MS_INJECT_TTL_MS; }), msInjectQueue.length > 120) msInjectQueue = msInjectQueue.slice(-120); } function MergeDanmaku_claimInjected(kind, match) { const now = Date.now(); for (let i = 0; i < msInjectQueue.length; i++) { const x = msInjectQueue[i]; if (now - x.at > MS_INJECT_TTL_MS) continue; if (x[kind]) continue; if (!match(x)) continue; return x[kind] = true, x; } return null; } function MergeDanmaku_injectNative(item) { const ss = MergeDanmaku_getStream(); if (!ss || !item || !item.packet) return false; MergeDanmaku_rememberInjected(item); try { return ss.push(item.packet), msMergeStats.viaBus++, true; } catch (e) { if (msInjectQueue.length) msInjectQueue.pop(); return msBusCache = null, msBusVia = null, msMergeStats.pushError++, false; } } let msBusHold = []; let msBusHoldTimer = 0; function MergeDanmaku_holdForBus(item) { if (msBusHold.push({ item: item, at: Date.now() }), msBusHold.length > MS_BUS_HOLD_MAX) msBusHold.shift(); if (msMergeStats.held++, !msBusHoldTimer) msBusHoldTimer = setInterval(function() { if (MergeDanmaku_flushBusHold(), !msBusHold.length) clearInterval(msBusHoldTimer), msBusHoldTimer = 0; }, 500); } const MS_BUS_FLUSH_PER_TICK = 4; function MergeDanmaku_flushBusHold() { if (!msBusHold.length) return; const now = Date.now(); const ss = MergeDanmaku_getStream(); if (ss) { let sent = 0; while (msBusHold.length && sent < MS_BUS_FLUSH_PER_TICK) { const x = msBusHold.shift(); if (sent++, !MergeDanmaku_injectNative(x.item)) MergeDanmaku_dropHeld(x.item); } return; } const keep = []; msBusHold.forEach(function(x) { if (now - x.at >= MS_BUS_HOLD_TTL_MS) MergeDanmaku_dropHeld(x.item); else keep.push(x); }), msBusHold = keep; } function MergeDanmaku_dropHeld(item) { msMergeStats.expired++, MergeDanmaku_warnNoStream(); try { if (MergeDanmaku_feedViaHost(item)) msMergeStats.viaHost++; } catch (e) {} } let msWarnedNoStream = false; function MergeDanmaku_warnNoStream() { if (msWarnedNoStream) return; msWarnedNoStream = true, console.warn("[DouyuEx] 一直没等到 socketProxy.socketStream(斗鱼自己的报文入口)," + "这段时间其它房间的弹幕只在飘屏显示、聊天区没有对应条目;等它出现会自动恢复正常。"); } let msChatObserver = null; let msChatWatchTimer = 0; function MergeDanmaku_startChatWatch() { if (msChatObserver || msChatWatchTimer) return; if (MergeDanmaku_watchChatList()) return; let tries = 0; msChatWatchTimer = setInterval(function() { if (tries++, MergeDanmaku_watchChatList() || tries >= 60) clearInterval(msChatWatchTimer), msChatWatchTimer = 0; }, 1e3); } function MergeDanmaku_watchChatList() { if (msChatObserver) return true; const list = document.getElementById("js-barrage-list"); if (!list) return false; return msChatObserver = new MutationObserver(function(muts) { for (let i = 0; i < muts.length; i++) { const added = muts[i].addedNodes; for (let j = 0; j < added.length; j++) { const n = added[j]; if (n && 1 === n.nodeType && "LI" === n.tagName) try { MergeDanmaku_badgeNativeChatItem(n); } catch (e) {} } } }), msChatObserver.observe(list, { childList: true }), true; } function MergeDanmaku_badgeNativeChatItem(li) { if (!li || li.querySelector(".ex-ms-badge--chat")) return; const content = li.querySelector("[data-chatid]") || li.querySelector('[class*="Barrage-content"]'); const cid = content ? content.getAttribute("data-chatid") : null; const nick = li.querySelector('[class*="Barrage-nickName"]'); const uid = nick ? nick.getAttribute("data-uid") : null; const text = content ? String(content.textContent || "").trim() : ""; let hit = cid ? MergeDanmaku_claimInjected("chat", function(x) { return x.cid && x.cid === cid; }) : null; if (!hit && uid) hit = MergeDanmaku_claimInjected("chat", function(x) { return x.uid === String(uid) && x.text === text; }); if (!hit || !hit.srcName) return; const badge = document.createElement("span"); badge.className = "ex-ms-badge ex-ms-badge--chat", badge.textContent = MergeDanmaku_badgeText(hit.srcName), badge.title = String(hit.srcName); const host = nick && nick.parentNode ? nick.parentNode : li; if (host) host.insertBefore(badge, host.firstChild); } function MergeDanmaku_parseChatmsg(room, raw) { if ("string" !== typeof raw) return null; let ret = raw; const rawText = raw; if (!getFieldValue(ret, "type")) { const at = ret.indexOf("type@="); if (at < 0) return null; ret = ret.slice(at); } if ("chatmsg" !== getFieldValue(ret, "type")) return null; const txt = stt_unescape(getFieldValue(ret, "txt")); if (!txt) return null; const nn = stt_unescape(getFieldValue(ret, "nn")) || "观众"; const uid = getFieldValue(ret, "uid"); const col = parseInt(getFieldValue(ret, "col"), 10) || 0; let full = null; try { full = "function" === typeof stt_deserialize ? stt_deserialize(ret) : null; } catch (e) { full = null; } return { text: txt, color: MS_MERGE_COLORS[col] || MS_MERGE_COLORS[0], nn: nn, uid: uid, cid: getFieldValue(ret, "cid"), packet: ret, raw: full || { nn: nn, txt: txt, uid: uid, col: String(col), __raw: rawText }, srcRid: room.rid, srcName: room.nn || "房间 " + room.rid }; } function MergeDanmaku_handleMsg(room, ret) { const item = MergeDanmaku_parseChatmsg(room, ret); if (item) MergeDanmaku_push(item); } function MergeDanmaku_push(item) { if (!item || !item.text) return false; if (MergeDanmaku_retryBootstrap(), !MergeDanmaku_allowRate()) { msMergeStats.dropped++; const now = Date.now(); if (now - msRateWarnAt >= MS_MERGE_RATE_WARN_MS) msRateWarnAt = now, console.warn("[DouyuEx] 其它房间的弹幕超过 " + MS_MERGE_RATE_MAX + " 条/秒,超出的已丢弃(累计 " + msMergeStats.dropped + " 条);比例与入口状态看 MergeDanmaku_getStats() 的 pushed / viaBus / streamVia。"); return false; } if (msBusHold.length) return MergeDanmaku_holdForBus(item), false; if (MergeDanmaku_injectNative(item)) return msMergeStats.pushed++, true; return MergeDanmaku_holdForBus(item), false; } function MergeDanmaku_allowRate() { const now = Date.now(); const hits = msMergeRate.hits; while (hits.length && now - hits[0] >= MS_MERGE_RATE_WINDOW_MS) hits.shift(); if (hits.length >= MS_MERGE_RATE_MAX) return false; return hits.push(now), true; } function MergeDanmaku_feedViaHost(item) { const host = MergeDanmaku_getHost(); if (!host) return null; const raw = item.raw; if (!raw) return null; try { const data = host.convertComment(raw); if (!data) return false; if (!data.extraData) data.extraData = {}; data.extraData.exMsSrcRid = String(item.srcRid), data.extraData.exMsSrcName = item.srcName; const ok = host.cm.send(data); if (!ok) msMergeStats.noTrack++; return !!ok; } catch (e) { return null; } } window.MergeDanmaku_getStats = function() { return { pushed: msMergeStats.pushed, dropped: msMergeStats.dropped, noTrack: msMergeStats.noTrack, viaBus: msMergeStats.viaBus, viaHost: msMergeStats.viaHost, held: msBusHold.length, expired: msMergeStats.expired, stream: !!MergeDanmaku_getStream(), streamVia: msBusVia, ready: !!(msMergeSpace && msMergeCtors.scroll), rendererWrapped: !!msRendererProto, bootTries: msBootTries, bootExpired: msBootExpired, pushError: msMergeStats.pushError, host: !!msMergeHost, hostVia: msMergeHostVia, conns: msMergeConns.map(function(c) { return c.rid; }) }; }, window.MergeDanmaku_push = MergeDanmaku_push, window.MergeDanmaku_parseChatmsg = MergeDanmaku_parseChatmsg, window.MergeDanmaku_badgeText = MergeDanmaku_badgeText, window.MergeDanmaku_attachBadge = MergeDanmaku_attachBadge, window.MergeDanmaku_bootstrap = MergeDanmaku_bootstrap, window.MergeDanmaku_patchFirstqueue = MergeDanmaku_patchFirstqueue, window.MergeDanmaku_patchBarrageGroup = MergeDanmaku_patchBarrageGroup, window.MergeDanmaku_feedViaHost = MergeDanmaku_feedViaHost, window.MergeDanmaku_getHost = MergeDanmaku_getHost, window.MergeDanmaku_findHostByFiber = MergeDanmaku_findHostByFiber, window.MergeDanmaku_walkFiberForHost = MergeDanmaku_walkFiberForHost, window.MergeDanmaku_getStream = MergeDanmaku_getStream, window.MergeDanmaku_flushBusHold = MergeDanmaku_flushBusHold, window.MergeDanmaku_dropHeld = MergeDanmaku_dropHeld, window.MergeDanmaku_injectNative = MergeDanmaku_injectNative, window.MergeDanmaku_badgeNativeChatItem = MergeDanmaku_badgeNativeChatItem, window.MergeDanmaku_watchChatList = MergeDanmaku_watchChatList; const MS_SLOT_COUNT = 5; const MS_MAX_ROOMS = 5; const MS_MIN_ROOMS = 2; const MS_LONGPRESS_MS = 300; const MS_SWAP_DELAY_MS = 500; const MS_RELOAD_BTN_MS = 3e3; const MS_START_TIMEOUT_MS = 8e3; const MS_BOTTOM_GUARD_PX = 42; const MS_STALL_TICK_MS = 2e3; const MS_STALL_QUIET_MS = 6e3; const MS_CLICK_SWAP_TO_MAIN = false; const MS_QUALITY_LIST = [ { v: "auto", name: "自动", short: "自动" }, { v: "0", name: "蓝光4M", short: "蓝光" }, { v: "3", name: "超清", short: "超清" }, { v: "2", name: "高清", short: "高清" }, { v: "1", name: "流畅", short: "流畅" } ]; const MS_QN_BIG_SHARE = .45; const MS_QN_AUTO_TIERS = [ { minH: 280, qn: "0" }, { minH: 180, qn: "2" }, { minH: 0, qn: "1" } ]; const MS_LAYOUT = { 2: [ [ 25, 0, 50, 50 ], [ 25, 50, 50, 50 ] ], 3: [ [ 17.2, .8, 65.6, 65.6 ], [ 17.2, 66.4, 32.8, 32.8 ], [ 50, 66.4, 32.8, 32.8 ] ], 4: [ [ 0, 0, 50, 50 ], [ 0, 50, 50, 50 ], [ 50, 50, 50, 50 ], [ 50, 0, 50, 50 ] ], 5: [ [ 1.4, 9.5, 48.6, 48.6 ], [ 1.4, 58.1, 32.4, 32.4 ], [ 33.8, 58.1, 32.4, 32.4 ], [ 50, 9.5, 48.6, 48.6 ], [ 66.2, 58.1, 32.4, 32.4 ] ] }; const MS_SLOT_Z = [ 5, 4, 3, 2, 1 ]; let msContainer = null; let msActiveList = []; let msHistoryList = []; let msMultiType = 0; let msSlotPlayers = new Map(); let msPosOwner = [ 0, 1, 2, 3, 4 ]; let msQn = "auto"; let msEpoch = 0; let msDrag = null; let msDragTimer = null; let msStallTickMs = MS_STALL_TICK_MS; let msStallQuietMs = MS_STALL_QUIET_MS; let msStartTimeoutMs = MS_START_TIMEOUT_MS; let msOnChangeList = []; let msOnSlotClick = null; let msMetaCache = new Map(); function MultiScreen_notifyChange(list) { msOnChangeList.forEach(function(fn) { try { fn(list); } catch (e) {} }); } function MultiScreen_getContainer() { if (msContainer && msContainer.isConnected) return msContainer; return msContainer = document.getElementById("js-player-multiContainer"), msContainer; } function MultiScreen_cells() { const c = MultiScreen_getContainer(); const out = []; if (!c) return out; out[0] = c.querySelector(".layout-Player-videoEntity"); for (let i = 1; i < MS_SLOT_COUNT; i++) out[i] = c.querySelector(".layout-Player-multiPlayer.is-multi" + (i + 1) + ":not(.ms-slot--placeholder)"); return out; } function MultiScreen_slots() { const cells = MultiScreen_cells(); const out = []; for (let pos = 0; pos < MS_SLOT_COUNT; pos++) out[pos] = cells[msPosOwner[pos]] || null; return out; } function MultiScreen_resetPosOwner() { msPosOwner = [ 0, 1, 2, 3, 4 ]; } function MultiScreen_visiblePositions() { const out = []; for (let i = 0; i < msActiveList.length && i < MS_SLOT_COUNT; i++) { const r = msActiveList[i]; if (r && false !== r.video) out.push(i); } return out; } function MultiScreen_externalPositions() { const mainRid = String(MultiScreen_getMainRid()); return MultiScreen_visiblePositions().filter(function(pos) { const r = msActiveList[pos]; return !!r && String(r.rid) !== mainRid && 0 !== msPosOwner[pos]; }); } function MultiScreen_layoutSlotOf(pos) { return MultiScreen_visiblePositions().indexOf(pos); } function MultiScreen_positionOfCell(cellId) { return msPosOwner.indexOf(cellId); } function MultiScreen_positionOfRecord(rec) { let found = -1; return msSlotPlayers.forEach(function(r, pos) { if (r === rec && found < 0) found = pos; }), found; } function MultiScreen_slotDom(idx) { return MultiScreen_slots()[idx] || null; } function MultiScreen_isSupported() { if (!MultiScreen_getContainer()) return false; const s = MultiScreen_slots(); for (let i = 0; i < MS_SLOT_COUNT; i++) if (!s[i]) return false; return true; } const MS_RID_RE = /^\d{1,20}$/; function MultiScreen_saneRid(v) { if ("number" === typeof v && isFinite(v) && v > 0) return String(Math.floor(v)); if ("string" === typeof v) { const s = v.trim(); if (MS_RID_RE.test(s) && "0" !== s) return s; } return ""; } function MultiScreen_saneText(v, max) { return "string" === typeof v ? v.slice(0, max || 60) : ""; } function MultiScreen_saneItem(r) { if (!r || "object" !== typeof r) return null; const rid = MultiScreen_saneRid(r.rid); if (!rid) return null; const o = { rid: rid, nn: MultiScreen_saneText(r.nn), avatar: MultiScreen_saneText(r.avatar, 300) }; if (false === r.video) o.video = false; if (false === r.danmaku) o.danmaku = false; return o; } function MultiScreen_saneQn(q) { if ("string" !== typeof q) return ""; for (let i = 0; i < MS_QUALITY_LIST.length; i++) if (MS_QUALITY_LIST[i].v === q) return q; return ""; } function MultiScreen_getMainRid() { return String("undefined" !== typeof rid && rid || ""); } function MultiScreen_getHost() { const c = MultiScreen_getContainer(); if (!c) return null; let p = c.parentElement; for (let i = 0; i < 6 && p && p !== document.body; i++) { try { if ("static" !== getComputedStyle(p).position) return p; } catch (e) { break; } p = p.parentElement; } return c; } function MultiScreen_applyLayout() { const c = MultiScreen_getContainer(); if (!c || msDrag) return; MultiScreen_watchContainer(); const vis = MultiScreen_visiblePositions(); if (msMultiType = vis.length, msMultiType > 1) c.className = "layout-Player-multiContainer is-multi is-multi" + msMultiType; else c.className = "layout-Player-multiContainer"; const table = MS_LAYOUT[msMultiType]; if (msMultiType <= 1) return MultiScreen_slots().forEach(function(slot) { if (!slot) return; [ "top", "left", "width", "height", "z-index", "transform", "visibility" ].forEach(function(prop) { slot.style.removeProperty(prop); }); }), void 0; const slots = MultiScreen_slots(); slots.forEach(function(slot, pos) { if (!slot) return; const k = vis.indexOf(pos); if (k < 0 || !table || k >= table.length) return slot.style.top = "0px", slot.style.left = "0px", slot.style.width = "0px", slot.style.height = "0px", slot.style.zIndex = "0", slot.style.transform = "scale(0)", slot.style.visibility = "hidden", void 0; const g = table[k]; slot.style.top = g[0] + "%", slot.style.left = g[1] + "%", slot.style.width = g[2] + "%", slot.style.height = g[3] + "%", slot.style.zIndex = String(MS_SLOT_Z[k]), slot.style.transform = "scale(1)", slot.style.visibility = "visible"; }), MultiScreen_reapplyAutoQuality(), MultiScreen_notifyChange(msActiveList.slice()); } function MultiScreen_clearSlot(idx) { const rec = msSlotPlayers.get(idx); if (rec) { if (rec.prev) MultiScreen_destroyPlayer(rec.prev); MultiScreen_destroyPlayer(rec), msSlotPlayers.delete(idx); } const slot = MultiScreen_slotDom(idx); if (!slot) return; if (0 !== msPosOwner[idx]) slot.innerHTML = ""; slot.classList.remove("ms-slot--loading", "ms-slot--notice", "ms-slot--novideo", "is-cur", "cur", "dragging-player", "ms-slot--dragging"); } function MultiScreen_renderSlotNoVideo(idx, room) { const slot = MultiScreen_slotDom(idx); if (!slot) return; MultiScreen_clearSlot(idx), slot.innerHTML = '' + '' + '' + "" + '
    ' + '
    ' + '
    画面已关闭(弹幕照常显示)
    ' + '
    开启画面
    ' + "
    " + "
    ", MultiScreen_fillName(slot, room), slot.classList.add("ms-slot--novideo", "ms-slot--notice"); const btn = slot.querySelector(".ms-slot__reload"); if (btn) { const cellOwner = msPosOwner[idx]; btn.addEventListener("click", function(e) { e.stopPropagation(); const pos = MultiScreen_positionOfCell(cellOwner); const now = pos >= 0 ? msActiveList[pos] : null; if (now) MultiScreen_setRoomFlag(String(now.rid), "video", true); }); } } function MultiScreen_renderSlot(idx, room) { const slot = MultiScreen_slotDom(idx); if (!slot || !room) return; if (0 === msPosOwner[idx]) return; if (false === room.video) return MultiScreen_renderSlotNoVideo(idx, room); MultiScreen_clearSlot(idx); const epoch = ++msEpoch; const cellOwner = msPosOwner[idx]; slot.classList.add("ms-slot--loading"), slot.innerHTML = '' + '' + '' + '' + "" + '
    ' + '
    ' + '
    ' + '
    努力加载中…
    ' + '
    重新加载
    ' + "
    " + "
    "; const rec = { rid: String(room.rid), epoch: epoch, flv: null, video: slot.querySelector(".ms-slot__video"), reloadTimer: 0, startTimer: 0, url: "", stallTimer: 0, lastTick: 0, stalled: 0 }; if (rec.video) rec.video.muted = true, rec.video.volume = 0, rec.video.setAttribute("muted", ""); rec.vol = MultiScreen_getVolume(rec.rid, false), msSlotPlayers.set(idx, rec), MultiScreen_fillName(slot, room); const reloadEl = slot.querySelector(".ms-slot__reload"); if (reloadEl) reloadEl.addEventListener("click", function(e) { e.stopPropagation(); const pos = MultiScreen_positionOfCell(cellOwner); const now = pos >= 0 ? msActiveList[pos] : null; if (!now) return; MultiScreen_renderSlot(pos, now); }); rec.reloadTimer = setTimeout(function() { if (reloadEl && MultiScreen_positionOfRecord(rec) >= 0) reloadEl.classList.add("is-visible"); }, MS_RELOAD_BTN_MS), MultiScreen_resolveRoom(room, function(meta) { const pos = MultiScreen_positionOfRecord(rec); if (pos < 0) return; if (MultiScreen_fillName(slot, meta), 2 === +meta.showStatus) return MultiScreen_setTip(slot, "主播正在赶来中…"), void 0; MultiScreen_fetchStream(pos, meta, epoch); }); } function MultiScreen_resolveRoom(room, cb) { const rid = String(room.rid); if (room.nn) return cb(room), void 0; const hit = msMetaCache.get(rid); if (hit) return cb(Object.assign({}, room, hit)), void 0; fetch("https://www.douyu.com/betard/" + rid, { method: "GET", mode: "no-cors", credentials: "include" }).then(function(r) { return r.json(); }).then(function(ret) { const rm = ret && ret.room || {}; const meta = { nn: rm.nickname || rm.room_name || "", avatar: rm.avatar && rm.avatar.middle || "", showStatus: void 0 === rm.show_status ? void 0 : +rm.show_status }; msMetaCache.set(rid, meta), cb(Object.assign({}, room, meta)); }).catch(function() { cb(room); }); } function MultiScreen_getMetaName(rid) { const hit = msMetaCache.get(String(rid)); return hit && hit.nn || ""; } function MultiScreen_fillName(slot, room) { const nameEl = slot.querySelector(".ms-slot__name-text"); const iconEl = slot.querySelector(".ms-slot__name-icon"); const label = room.nn || "房间 " + room.rid; if (nameEl) nameEl.textContent = label, nameEl.title = label; if (iconEl) if (room.avatar) iconEl.style.backgroundImage = 'url("' + room.avatar + '")', iconEl.style.display = ""; else iconEl.style.display = "none"; } function MultiScreen_setTip(slot, tip) { if (!slot) return; const el = slot.querySelector(".ms-slot__tip"); if (el) el.textContent = tip; slot.classList.remove("ms-slot--loading"), slot.classList.add("ms-slot--notice"); const r = slot.querySelector(".ms-slot__reload"); if (r) r.classList.add("is-visible"); } function MultiScreen_pickQnForSlot(idx) { const slot = MultiScreen_slotDom(idx); let h = 0; try { h = slot ? slot.getBoundingClientRect().height : 0; } catch (e) { h = 0; } let ch = 0; try { const c = MultiScreen_getContainer(); ch = c ? c.getBoundingClientRect().height : 0; } catch (e) { ch = 0; } if (!h) { const k = MultiScreen_layoutSlotOf(idx); const table = MS_LAYOUT[msMultiType]; if (table && k >= 0 && table[k]) h = ch * (table[k][3] / 100); } if (!h) return "2"; if (ch > 0 && h / ch >= MS_QN_BIG_SHARE) return "0"; for (let i = 0; i < MS_QN_AUTO_TIERS.length; i++) if (h >= MS_QN_AUTO_TIERS[i].minH) return MS_QN_AUTO_TIERS[i].qn; return "1"; } function MultiScreen_qnFor(idx) { return "auto" === msQn ? MultiScreen_pickQnForSlot(idx) : msQn; } function MultiScreen_getQnReport() { const c = MultiScreen_getContainer(); let ch = 0; try { ch = c ? c.getBoundingClientRect().height : 0; } catch (e) { ch = 0; } const out = []; for (let i = 0; i < MS_SLOT_COUNT; i++) { const room = msActiveList[i]; if (!room) continue; const isMain = 0 === msPosOwner[i]; const slot = MultiScreen_slotDom(i); let h = 0; try { h = slot ? slot.getBoundingClientRect().height : 0; } catch (e) { h = 0; } const rec = msSlotPlayers.get(i); const v = rec && rec.video; out.push({ pos: i, rid: String(room.rid), isMain: isMain, rectH: Math.round(h), share: ch ? Math.round(h / ch * 100) / 100 : 0, qn: isMain ? "主画面(原生)" : false === room.video ? "(未取流)" : MultiScreen_qnFor(i), mode: "auto" === msQn ? "auto" : "manual(" + msQn + ")", res: v && v.videoWidth ? v.videoWidth + "x" + v.videoHeight : "(未出画面)", vol: MultiScreen_getVolume(room.rid, isMain), video: false !== room.video, danmaku: false !== room.danmaku }); } return out; } function MultiScreen_roomRecord(r) { const o = { rid: String(r.rid), nn: r.nn || "", avatar: r.avatar || "" }; if (false === r.video) o.video = false; if (false === r.danmaku) o.danmaku = false; return o; } function MultiScreen_roomByRid(rid) { const k = String(rid); return msActiveList.filter(function(r) { return String(r.rid) === k; })[0] || null; } function MultiScreen_slotIndexOfRid(rid) { const k = String(rid); for (let i = 0; i < msActiveList.length && i < MS_SLOT_COUNT; i++) if (String(msActiveList[i].rid) === k) return i; return -1; } function MultiScreen_isRoomOn(rid, key) { const room = MultiScreen_roomByRid(rid); if (!room) return true; return false !== room[key]; } function MultiScreen_setRoomFlag(rid, key, on) { if ("video" !== key && "danmaku" !== key) return false; const room = MultiScreen_roomByRid(rid); if (!room) return false; if (String(MultiScreen_getMainRid()) === String(room.rid)) return false; const want = false !== on; if (false !== room[key] === want) return false; if (want) delete room[key]; else room[key] = false; if (MultiScreen_save(), "video" === key) { const idx = MultiScreen_slotIndexOfRid(room.rid); if (idx >= 0) MultiScreen_renderSlot(idx, room); MultiScreen_applyLayout(); } return MultiScreen_notifyChange(msActiveList.slice()), true; } function MultiScreen_roomQnName(rid) { if (msActiveList.length < MS_MIN_ROOMS) return ""; const idx = MultiScreen_slotIndexOfRid(rid); if (idx < 0 || 0 === msPosOwner[idx]) return ""; const room = msActiveList[idx]; if (room && false === room.video) return ""; const qn = MultiScreen_qnFor(idx); const hit = MS_QUALITY_LIST.filter(function(q) { return q.v === String(qn); })[0]; return hit ? hit.name : ""; } const MS_VOLUME_KEY = "ExSave_MultiScreenVol"; let msVolumes = null; function MultiScreen_loadVolumes() { if (msVolumes) return msVolumes; msVolumes = {}; try { const j = JSON.parse(localStorage.getItem(MS_VOLUME_KEY) || "null"); if (j && "object" === typeof j) Object.keys(j).forEach(function(k) { const v = Number(j[k]); if (v >= 0 && v <= 1) msVolumes[String(k)] = v; }); } catch (e) {} return msVolumes; } function MultiScreen_saveVolumes() { try { localStorage.setItem(MS_VOLUME_KEY, JSON.stringify(msVolumes || {})); } catch (e) {} } function MultiScreen_getVolume(rid, isMain) { const vols = MultiScreen_loadVolumes(); const k = String(rid); if (void 0 === vols[k]) return isMain ? 1 : 0; return vols[k]; } function MultiScreen_setVolume(rid, v) { const val = Math.max(0, Math.min(1, Number(v) || 0)); MultiScreen_loadVolumes()[String(rid)] = val, MultiScreen_saveVolumes(), MultiScreen_applyVolume(String(rid), val); } function MultiScreen_nativeVideo() { return document.querySelector("#__video2") || document.querySelector("#__video_container video") || document.querySelector(".layout-Player-videoEntity video") || null; } function MultiScreen_applyVolumeToRec(rec, v) { if (!rec || !rec.video) return; try { rec.video.volume = v, rec.video.muted = 0 === v; } catch (e) {} } function MultiScreen_applyVolume(rid, v) { const k = String(rid); if (k === String(MultiScreen_getMainRid())) { const nv = MultiScreen_nativeVideo(); if (nv) try { nv.volume = v, nv.muted = 0 === v; } catch (e) {} return; } msSlotPlayers.forEach(function(rec) { if (!rec || String(rec.rid) !== k) return; if (rec.vol = v, MultiScreen_applyVolumeToRec(rec, v), rec.prev) rec.prev.vol = v, MultiScreen_applyVolumeToRec(rec.prev, v); }); } function MultiScreen_reapplyAutoQuality() { if ("auto" !== msQn || msMultiType <= 1 || msDrag) return; let changed = false; const positions = MultiScreen_externalPositions(); for (let k = 0; k < positions.length; k++) { const i = positions[k]; const rec = msSlotPlayers.get(i); if (!rec) continue; if (rec.switching) continue; const want = MultiScreen_pickQnForSlot(i); if (rec.qn && rec.qn === want) continue; MultiScreen_switchQuality(i, msActiveList[i]), changed = true; } if (changed) MultiScreen_notifyChange(msActiveList.slice()); } const MS_SWAP_TIMEOUT_MS = 8e3; function MultiScreen_destroyPlayer(rec) { if (!rec) return; clearTimeout(rec.reloadTimer), clearTimeout(rec.startTimer), clearTimeout(rec.swapTimer), clearTimeout(rec.retryTimer), clearInterval(rec.stallTimer), rec.stallTimer = 0; try { if (rec.flv && "function" === typeof rec.flv.destroy) rec.flv.destroy(); } catch (e) {} try { if (rec.video && rec.video.parentNode) rec.video.parentNode.removeChild(rec.video); } catch (e) {} } function MultiScreen_switchQuality(idx, room) { const slot = MultiScreen_slotDom(idx); const old = msSlotPlayers.get(idx); if (!slot || !old || !old.video || !old.video.isConnected || old.video.readyState < 2) return MultiScreen_renderSlot(idx, room); const epoch = ++msEpoch; const fresh = slot.ownerDocument.createElement("video"); fresh.className = "ms-slot__video ms-slot__video--next", fresh.setAttribute("playsinline", ""), fresh.muted = true, fresh.volume = 0, fresh.setAttribute("muted", ""), slot.appendChild(fresh); const rec = { rid: String(room.rid), epoch: epoch, flv: null, video: fresh, reloadTimer: 0, startTimer: 0, url: "", prev: old, switching: true, stallTimer: 0, lastTick: 0, stalled: 0, vol: void 0 === old.vol ? MultiScreen_getVolume(String(room.rid), false) : old.vol }; msSlotPlayers.set(idx, rec), clearTimeout(old.reloadTimer), rec.swapTimer = setTimeout(function() { const pos = MultiScreen_positionOfRecord(rec); if (pos < 0) return; msSlotPlayers.set(pos, old), MultiScreen_destroyPlayer(rec); }, MS_SWAP_TIMEOUT_MS), MultiScreen_fetchStream(idx, room, epoch); } function MultiScreen_finishSwap(idx, rec) { const prev = rec.prev; if (rec.prev = null, rec.switching = false, clearTimeout(rec.swapTimer), rec.swapTimer = 0, rec.video.classList.add("is-live"), !prev) return; setTimeout(function() { MultiScreen_destroyPlayer(prev); }, 320); } let msAutoQnTimer = 0; function MultiScreen_onResize() { clearTimeout(msAutoQnTimer), msAutoQnTimer = setTimeout(function() { msAutoQnTimer = 0, MultiScreen_reapplyAutoQuality(); }, 800); } function MultiScreen_retryOnce(idx, epoch, room) { const rec = msSlotPlayers.get(idx); if (!rec || rec.epoch !== epoch || rec.retried) return false; return rec.retried = 1, rec.retryTimer = setTimeout(function() { const pos = MultiScreen_positionOfRecord(rec); if (pos < 0 || rec.epoch !== epoch || !room) return; MultiScreen_fetchStream(pos, room, epoch); }, 2e3), true; } function MultiScreen_fetchStream(idx, room, epoch) { const rec = msSlotPlayers.get(idx); if (!rec || rec.epoch !== epoch) return; const slot = MultiScreen_slotDom(idx); if ("function" !== typeof getRealLive_Douyu) { if (rec.switching) return MultiScreen_abortSwap(idx, rec), void 0; return MultiScreen_setTip(slot, "取流失败,可点重新加载"), void 0; } const qn = MultiScreen_qnFor(idx); rec.qn = qn, getRealLive_Douyu(String(room.rid), true, false, qn, function(url) { const pos = MultiScreen_positionOfRecord(rec); if (pos < 0 || rec.epoch !== epoch) return; if (!url || "None" === url) { if (rec.switching) return MultiScreen_abortSwap(pos, rec), void 0; if (MultiScreen_retryOnce(pos, epoch, room)) return; return MultiScreen_setTip(MultiScreen_slotDom(pos), "房间未开播或其他错误"), void 0; } MultiScreen_startFlv(pos, url, epoch); }); } function MultiScreen_abortSwap(idx, rec) { if (!rec || !rec.prev) return; const prev = rec.prev; const pos = MultiScreen_positionOfRecord(rec); const back = pos >= 0 ? pos : idx; if (msSlotPlayers.get(back) === rec) msSlotPlayers.set(back, prev); rec.prev = null, rec.switching = false, MultiScreen_destroyPlayer(rec), void prev; } function MultiScreen_startStallWatch(rec) { if (!rec || !rec.video) return; clearInterval(rec.stallTimer), rec.lastTick = Date.now(), rec.stallTimer = setInterval(function() { const pos = MultiScreen_positionOfRecord(rec); if (pos < 0 || rec.switching) return; const v = rec.video; if (!v || !v.isConnected || v.readyState < 2 || v.paused || "hidden" === document.visibilityState) return rec.lastTick = Date.now(), void 0; if (Date.now() - rec.lastTick < msStallQuietMs) return; if (rec.stalled) return; rec.stalled = 1, MultiScreen_setTip(MultiScreen_slotDom(pos), "画面卡住了,点「重新加载」"); }, msStallTickMs); } function MultiScreen_clearStallTip(idx, rec) { if (!rec || !rec.stalled) return; rec.stalled = 0; const slot = MultiScreen_slotDom(idx); if (slot) slot.classList.remove("ms-slot--notice"); } function MultiScreen_startFlv(idx, url, epoch) { const rec = msSlotPlayers.get(idx); if (!rec || rec.epoch !== epoch) return; MultiScreen_noteStreamUrl(rec, url); const slot = MultiScreen_slotDom(idx); if (!rec.video) return MultiScreen_setTip(slot, "播放器未就绪"), void 0; if ("undefined" === typeof flvjs && "function" === typeof ExLoadLib && "undefined" !== typeof EXURL) return ExLoadLib(EXURL.flv, function() { MultiScreen_startFlv(idx, url, epoch); }, function() { MultiScreen_setTip(slot, "播放组件没能加载,刷新页面再试"); }), void 0; if ("undefined" === typeof flvjs || !flvjs.isSupported()) return MultiScreen_setTip(slot, "当前浏览器放不了这个流"), void 0; try { if (rec.flv) { try { if ("function" === typeof rec.flv.destroy) rec.flv.destroy(); } catch (e) {} rec.flv = null; } const player = flvjs.createPlayer({ type: "flv", url: url }, { fixAudioTimestampGap: false }); player.attachMediaElement(rec.video), player.load(), player.play(), rec.flv = player, rec.video.addEventListener("playing", function() { const pos = MultiScreen_positionOfRecord(rec); if (pos < 0 || rec.epoch !== epoch) return; if (clearTimeout(rec.startTimer), rec.switching) return MultiScreen_finishSwap(pos, rec), MultiScreen_applyVolumeToRec(rec, void 0 === rec.vol ? MultiScreen_getVolume(rec.rid, false) : rec.vol), MultiScreen_startStallWatch(rec), void 0; const s = MultiScreen_slotDom(pos); if (s) s.classList.remove("ms-slot--loading", "ms-slot--notice"); MultiScreen_applyVolumeToRec(rec, void 0 === rec.vol ? MultiScreen_getVolume(rec.rid, false) : rec.vol), MultiScreen_startStallWatch(rec); }, { once: true }), rec.video.addEventListener("timeupdate", function() { const pos = MultiScreen_positionOfRecord(rec); if (pos < 0 || rec.switching) return; rec.lastTick = Date.now(), MultiScreen_clearStallTip(pos, rec); }), rec.startTimer = setTimeout(function() { const pos = MultiScreen_positionOfRecord(rec); if (pos < 0 || rec.switching || !rec.video || rec.video.readyState >= 2) return; if (MultiScreen_retryOnce(pos, epoch, msActiveList[pos])) return; MultiScreen_setTip(MultiScreen_slotDom(pos), "加载缓慢,可点重新加载"); }, msStartTimeoutMs); } catch (e) { if (rec.switching) MultiScreen_abortSwap(idx, rec); else MultiScreen_setTip(slot, "播放器创建失败"); } } function MultiScreen_renderAll() { for (let i = 0; i < MS_SLOT_COUNT; i++) { const want = msActiveList[i] || null; const rec = msSlotPlayers.get(i); const have = rec ? rec.rid : null; if (!want) { const stale = MultiScreen_slotDom(i); if (have || stale && stale.classList.contains("ms-slot--novideo")) MultiScreen_clearSlot(i); continue; } if (have === String(want.rid)) continue; MultiScreen_renderSlot(i, want); } MultiScreen_applyLayout(); } function MultiScreen_normalize(list) { const mainRid = MultiScreen_getMainRid(); let arr = (Array.isArray(list) ? list : []).map(MultiScreen_saneItem).filter(Boolean); if (mainRid && !arr.some(function(r) { return String(r.rid) === mainRid; })) arr = [ { rid: mainRid, nn: "", avatar: "" } ].concat(arr); const seen = {}; const out = []; for (let i = 0; i < arr.length && out.length < MS_MAX_ROOMS; i++) { const k = String(arr[i].rid); if (seen[k]) continue; seen[k] = 1, out.push(arr[i]); } return out; } function MultiScreen_normalizeMainFirst(list) { const mainRid = MultiScreen_getMainRid(); const arr = MultiScreen_normalize(list); if (!mainRid) return arr; const main = arr.filter(function(r) { return String(r.rid) === mainRid; })[0] || { rid: mainRid, nn: "", avatar: "" }; const rest = arr.filter(function(r) { return String(r.rid) !== mainRid; }); return [ main ].concat(rest); } function MultiScreen_settleSingleScreen() { if (MultiScreen_visiblePositions().length > 1) return; for (let i = 0; i < MS_SLOT_COUNT; i++) MultiScreen_clearSlot(i); MultiScreen_resetPosOwner(), MultiScreen_clearPendingWork(); } function MultiScreen_apply(action, payload) { if ("close" === action) { msHistoryList = msActiveList.slice(), msActiveList = MultiScreen_normalize([]); for (let i = 0; i < MS_SLOT_COUNT; i++) MultiScreen_clearSlot(i); } else if ("add" === action) { if (msActiveList.length >= MS_MAX_ROOMS) return MultiScreen_after(); msActiveList = MultiScreen_normalize(msActiveList.concat([ payload ])); } else if ("delete" === action) MultiScreen_forgetQnCompare(payload), msActiveList = MultiScreen_normalize(msActiveList.filter(function(r) { return String(r.rid) !== String(payload); })); else msActiveList = MultiScreen_normalize(payload); return MultiScreen_renderAll(), MultiScreen_settleSingleScreen(), MultiScreen_applyLayout(), MultiScreen_after(); } function MultiScreen_clearPendingWork() { if (msQnCompare) clearTimeout(msQnCompare.timer), msQnCompare = null; clearTimeout(msAutoQnTimer), msAutoQnTimer = 0, clearTimeout(msDragTimer), msDragTimer = null, clearTimeout(msHealTimer), msHealTimer = 0; } function MultiScreen_forgetQnCompare(rid) { if (!msQnCompare) return; const k = String(rid); if (!(k in msQnCompare.prev)) return; delete msQnCompare.prev[k], delete msQnCompare.done[k], delete msQnCompare.same[k]; const total = Object.keys(msQnCompare.prev).length; if (!total) return clearTimeout(msQnCompare.timer), msQnCompare = null, void 0; if (Object.keys(msQnCompare.done).length >= total) MultiScreen_finishQnCompare(); } function MultiScreen_after() { msMultiType = MultiScreen_visiblePositions().length, MultiScreen_save(); const mrAfter = String(MultiScreen_getMainRid()); for (let i = 0; i < msActiveList.length && i < MS_SLOT_COUNT; i++) { const rr = msActiveList[i]; if (rr && String(rr.rid) !== mrAfter) MultiScreen_pushRecent(rr); } return MultiScreen_notifyChange(msActiveList.slice()), msActiveList; } function MultiScreen_enter(list) { let target = list; if (!target && msHistoryList && msHistoryList.length > 1) target = msHistoryList.slice(), msHistoryList = []; if (!target || !target.length) target = MultiScreen_load() || []; return msActiveList = MultiScreen_normalizeMainFirst(target), MultiScreen_resetPosOwner(), MultiScreen_renderAll(), MultiScreen_after(); } function MultiScreen_exit() { return MultiScreen_apply("close"); } function MultiScreen_addRoom(room) { return MultiScreen_apply("add", room); } function MultiScreen_removeRoom(rid) { return MultiScreen_apply("delete", rid); } let msQnCompare = null; function MultiScreen_beginQnCompare(idxList) { const prev = {}; idxList.forEach(function(i) { const rec = msSlotPlayers.get(i); if (!rec) return; prev[String(rec.rid)] = rec.url || ""; }), msQnCompare = { prev: prev, same: {}, done: {}, timer: 0 }, msQnCompare.timer = setTimeout(function() { MultiScreen_finishQnCompare(); }, 15e3); } function MultiScreen_noteStreamUrl(rec, url) { if (rec) rec.url = url || ""; const k = rec ? String(rec.rid) : ""; if (!msQnCompare || !k || !(k in msQnCompare.prev)) return; if (msQnCompare.done[k]) return; if (msQnCompare.done[k] = 1, (url || "") === msQnCompare.prev[k]) msQnCompare.same[k] = 1; const total = Object.keys(msQnCompare.prev).length; if (Object.keys(msQnCompare.done).length >= total) MultiScreen_finishQnCompare(); } function MultiScreen_qnLabel(qn) { for (let i = 0; i < MS_QUALITY_LIST.length; i++) if (String(MS_QUALITY_LIST[i].v) === String(qn)) return MS_QUALITY_LIST[i].name; return String(qn); } function MultiScreen_finishQnCompare() { if (!msQnCompare) return; const total = Object.keys(msQnCompare.prev).length; const doneCount = Object.keys(msQnCompare.done).length; const sameCount = Object.keys(msQnCompare.same).length; if (clearTimeout(msQnCompare.timer), msQnCompare = null, !total) return; const qnName = MultiScreen_qnLabel(msQn); if (doneCount < total) showMessage("清晰度切换:已确认 " + doneCount + "/" + total + " 个房间" + (sameCount ? ",其中 " + sameCount + " 个本来就是这一档" : ""), "info"); else if (sameCount >= total) showMessage("其它房间本来就是「" + qnName + "」,画面没变", "info"); else if (sameCount > 0) showMessage("其它房间已切到「" + qnName + "」,其中 " + sameCount + " 个本来就是这一档", "info"); else showMessage("其它房间已切到「" + qnName + "」", "success"); } function MultiScreen_setQuality(qn) { if (!qn || String(qn) === msQn) return; msQn = String(qn), MultiScreen_save(), MultiScreen_notifyChange(msActiveList.slice()); const targets = MultiScreen_externalPositions(); if ("auto" === msQn) { for (let k = 0; k < targets.length; k++) MultiScreen_switchQuality(targets[k], msActiveList[targets[k]]); return showMessage("其它房间清晰度:按每块画面的大小自动选", "success"), void 0; } MultiScreen_beginQnCompare(targets); for (let k = 0; k < targets.length; k++) MultiScreen_switchQuality(targets[k], msActiveList[targets[k]]); } function MultiScreen_hitTest(clientX, clientY, forLongPress) { const c = MultiScreen_getContainer(); if (!c) return -1; const n = msMultiType; if (n < MS_MIN_ROOMS || n > MS_MAX_ROOMS) return -1; const r = c.getBoundingClientRect(); if (!r.width || !r.height) return -1; if (clientX < r.left || clientX > r.right || clientY < r.top || clientY > r.bottom) return -1; const yRel = clientY - r.top; if (forLongPress && yRel >= r.height - MS_BOTTOM_GUARD_PX) return -1; const d = (clientX - r.left) / r.width; const p = yRel / r.height; let hit = -1; if (2 === n) { if (p < .25 || p > .75) return -1; hit = d > .5 ? 1 : 0; } else if (3 === n) { if (p < .1713 || p > .8287 || d < .008 || d > .992) return -1; if (d < .664) hit = 0; else hit = p < .4993 ? 1 : 2; } else if (4 === n) if (d < .5) hit = p < .5 ? 0 : 3; else hit = p < .5 ? 1 : 2; else if (5 === n) { if (p < .014 || p > .986 || d < .095 || d > .905) return -1; if (d < .581) hit = p < .5 ? 0 : 3; else if (p < .338) hit = 1; else if (p < .662) hit = 2; else hit = 4; } if (hit < 0) return -1; const vis = MultiScreen_visiblePositions(); return void 0 === vis[hit] ? -1 : vis[hit]; } function MultiScreen_onMouseDown(e) { if (MultiScreen_isOwnUi(e.target)) return; if (0 !== e.button) return; if (!MultiScreen_isSupported() || msActiveList.length < MS_MIN_ROOMS) return; if (MultiScreen_hitTest(e.clientX, e.clientY, true) < 0) return; if (e.target && e.target.closest && e.target.closest(".ms-slot__reload")) return; clearTimeout(msDragTimer); const x = e.clientX, y = e.clientY; msDragTimer = setTimeout(function() { if (msDrag) return; const slot = MultiScreen_hitTest(x, y, true); if (slot < 0) return; MultiScreen_beginDrag(x, y, slot); }, MS_LONGPRESS_MS); } function MultiScreen_beginDrag(startX, startY, idx) { const c = MultiScreen_getContainer(); if (!c || msDrag) return; const slotEl = MultiScreen_slotDom(idx); if (!slotEl) return; const r = c.getBoundingClientRect(); const ph = document.createElement("div"); ph.className = "layout-Player-multiPlayer ms-slot--placeholder"; const k = MultiScreen_layoutSlotOf(idx); const g = k >= 0 ? (MS_LAYOUT[msMultiType] || [])[k] : null; if (g) ph.style.top = g[0] + "%", ph.style.left = g[1] + "%", ph.style.width = g[2] + "%", ph.style.height = g[3] + "%", ph.style.zIndex = String(MS_SLOT_Z[k]); c.insertBefore(ph, slotEl), msDrag = { from: idx, to: idx, slotEl: slotEl, placeholder: ph }, slotEl.classList.add("dragging-player", "ms-slot--dragging"), c.classList.add("is-dragging"), MultiScreen_moveDragEl(startX - r.left, startY - r.top), c.appendChild(slotEl); } function MultiScreen_moveDragEl(x, y) { const c = MultiScreen_getContainer(); if (!msDrag || !c) return; if (!c.contains(msDrag.slotEl)) c.appendChild(msDrag.slotEl); msDrag.slotEl.style.left = x + "px", msDrag.slotEl.style.top = y + "px"; } function MultiScreen_setCur(idx, on) { if (idx < 0) return; const slot = MultiScreen_slotDom(idx); if (!slot) return; if (on) slot.classList.add("is-cur", "cur"); else slot.classList.remove("is-cur", "cur"); } function MultiScreen_onMouseMove(e) { if (!msDrag) return; const c = MultiScreen_getContainer(); if (!c) return; const r = c.getBoundingClientRect(); MultiScreen_moveDragEl(e.clientX - r.left, e.clientY - r.top); const hit = MultiScreen_hitTest(e.clientX, e.clientY, false); if (hit === msDrag.to) return; MultiScreen_setCur(msDrag.to, false), msDrag.to = hit, MultiScreen_setCur(hit, true); } function MultiScreen_onPointerLost() { clearTimeout(msDragTimer); const drag = msDrag; if (!drag) return; msDrag = null, MultiScreen_cancelDrag(drag); } function MultiScreen_onMouseUp(e) { if (clearTimeout(msDragTimer), 0 !== e.button) return; if (!msDrag) return; const drag = msDrag; msDrag = null; const to = MultiScreen_hitTest(e.clientX, e.clientY, false); if (to < 0 || to === drag.from) return MultiScreen_cancelDrag(drag), void 0; MultiScreen_swapSlots(drag.from, to, drag); } function MultiScreen_cancelDrag(drag) { const c = MultiScreen_getContainer(); if (MultiScreen_setCur(drag.to, false), drag.slotEl.classList.remove("dragging-player", "ms-slot--dragging"), drag.slotEl.style.removeProperty("left"), drag.slotEl.style.removeProperty("top"), drag.placeholder && drag.placeholder.parentNode) drag.placeholder.parentNode.insertBefore(drag.slotEl, drag.placeholder), drag.placeholder.remove(); if (c) c.classList.remove("is-dragging"); MultiScreen_applyLayout(); } function MultiScreen_swapSlots(a, b, drag) { const c = MultiScreen_getContainer(); const slotA = drag.slotEl; const slotB = MultiScreen_slotDom(b); if (!c || !slotA || !slotB || slotA === slotB) return MultiScreen_cancelDrag(drag), void 0; if (MultiScreen_setCur(b, false), slotA.classList.remove("dragging-player", "ms-slot--dragging"), slotB.classList.remove("dragging-player", "ms-slot--dragging"), slotA.style.removeProperty("left"), slotA.style.removeProperty("top"), drag.placeholder && drag.placeholder.parentNode) drag.placeholder.parentNode.insertBefore(slotA, drag.placeholder), drag.placeholder.remove(); c.classList.remove("is-dragging"), MultiScreen_swapSlotState(a, b), MultiScreen_applyLayout(), setTimeout(function() { MultiScreen_save(), MultiScreen_notifyChange(msActiveList.slice()); }, MS_SWAP_DELAY_MS); } function MultiScreen_swapSlotState(a, b) { const tmpOwner = msPosOwner[a]; msPosOwner[a] = msPosOwner[b], msPosOwner[b] = tmpOwner; const ra = msSlotPlayers.get(a); const rb = msSlotPlayers.get(b); if (rb) msSlotPlayers.set(a, rb); else msSlotPlayers.delete(a); if (ra) msSlotPlayers.set(b, ra); else msSlotPlayers.delete(b); const arr = msActiveList.slice(); const tmp = arr[a]; arr[a] = arr[b], arr[b] = tmp, msActiveList = arr; } const MS_OWN_UI_SELECTOR = [ ".ms-slot__reload", ".popup-player-panel", ".popup-player", ".ex-panel__wrap", ".miuix-modal" ].join(","); function MultiScreen_isOwnUi(target) { if (!target) return false; if (target.closest) return !!target.closest(MS_OWN_UI_SELECTOR); return !!(target.querySelector && target.querySelector(MS_OWN_UI_SELECTOR)); } function MultiScreen_onClick(e) { if (void 0 !== e.button && 0 !== e.button) return; if (!MultiScreen_isSupported()) return; if (MultiScreen_isOwnUi(e.target)) return; const idx = MultiScreen_hitTest(e.clientX, e.clientY, false); if (idx < 0) return; if (MultiScreen_handleReloadHit(idx, e.clientX, e.clientY)) return; if (0 === idx) return; const room = msActiveList[idx]; if (!room) return; if ("function" === typeof msOnSlotClick) msOnSlotClick(idx, room); if (MS_CLICK_SWAP_TO_MAIN) MultiScreen_swapSlotState(0, idx), MultiScreen_applyLayout(), MultiScreen_save(), MultiScreen_notifyChange(msActiveList.slice()); } function MultiScreen_handleReloadHit(idx, x, y) { const slot = MultiScreen_slotDom(idx); if (!slot) return false; const btn = slot.querySelector(".ms-slot__reload.is-visible"); if (!btn) return false; const r = btn.getBoundingClientRect(); if (!r.width || !r.height) return false; if (x < r.left || x > r.right || y < r.top || y > r.bottom) return false; return btn.click(), true; } const MS_RECENT_MAX = 20; function MultiScreen_pushRecent(room) { if (!room || !room.rid) return; let list = MultiScreen_getRecentRaw(); const rid = String(room.rid); if (list = list.filter(function(r) { return String(r.rid) !== rid; }), list.unshift({ rid: rid, nn: room.nn || "", avatar: room.avatar || "" }), list.length > MS_RECENT_MAX) list = list.slice(0, MS_RECENT_MAX); try { localStorage.setItem("ExSave_MultiScreenRecent", JSON.stringify(list)); } catch (e) {} } function MultiScreen_getRecentRaw() { try { const raw = localStorage.getItem("ExSave_MultiScreenRecent"); const j = raw ? JSON.parse(raw) : null; return Array.isArray(j) ? j.filter(function(r) { return r && r.rid; }) : []; } catch (e) { return []; } } function MultiScreen_getRecent() { const active = {}; return msActiveList.forEach(function(r) { active[String(r.rid)] = 1; }), MultiScreen_getRecentRaw().filter(function(r) { return !active[String(r.rid)]; }); } function MultiScreen_save() { try { localStorage.setItem("ExSave_MultiScreen", JSON.stringify({ list: msActiveList.map(MultiScreen_roomRecord), qn: msQn })); } catch (e) {} MultiScreen_rememberForMainRoom(); } const MS_BYROOM_KEY = "ExSave_MultiScreenByRoom"; let msByRoom = null; function MultiScreen_loadByRoom() { if (msByRoom) return msByRoom; msByRoom = {}; try { const j = JSON.parse(localStorage.getItem(MS_BYROOM_KEY) || "null"); if (j && "object" === typeof j && !Array.isArray(j)) msByRoom = j; } catch (e) { msByRoom = {}; } return msByRoom; } function MultiScreen_rememberForMainRoom() { const main = MultiScreen_getMainRid(); if (!main) return; const foreign = msActiveList.slice(1).map(MultiScreen_roomRecord); if (!foreign.length) return; const store = MultiScreen_loadByRoom(); store[main] = { rooms: foreign, at: Date.now() }; try { localStorage.setItem(MS_BYROOM_KEY, JSON.stringify(store)); } catch (e) {} } function MultiScreen_savedConfigForMainRoom() { const main = MultiScreen_getMainRid(); const rec = MultiScreen_loadByRoom()[main]; if (!rec || !Array.isArray(rec.rooms) || !rec.rooms.length) return null; const cur = msActiveList.slice(1).map(function(r) { return String(r.rid); }).join(","); const saved = rec.rooms.map(function(r) { return String(r.rid); }).join(","); if (cur === saved) return null; return rec.rooms.slice(0, MS_MAX_ROOMS - 1); } function MultiScreen_load() { try { const raw = localStorage.getItem("ExSave_MultiScreen"); if (!raw) return null; const j = JSON.parse(raw); const qn = MultiScreen_saneQn(j && j.qn); if (qn) msQn = qn; const rawList = j && Array.isArray(j.list) ? j.list : []; const list = []; return rawList.forEach(function(r) { const it = MultiScreen_saneItem(r); if (it) list.push(it); }), list.length > 1 ? list.slice(0, MS_MAX_ROOMS) : null; } catch (e) { return null; } } let msHealTimer = 0; let msWatched = null; let msObserver = null; let msListenersBound = false; function MultiScreen_watchContainer() { const c = MultiScreen_getContainer(); if (!c || "undefined" === typeof MutationObserver || msWatched === c) return; if (msObserver) msObserver.disconnect(); msWatched = c, msObserver = new MutationObserver(function() { if (msDrag) return; if (msHealTimer) return; msHealTimer = setTimeout(function() { msHealTimer = 0, MultiScreen_heal(); }, 250); }), msObserver.observe(c, { childList: true }); } function MultiScreen_heal() { if (msDrag) return; if (!MultiScreen_getContainer() || msActiveList.length < MS_MIN_ROOMS) return; const slots = MultiScreen_slots(); const needRender = []; for (let i = 0; i < msActiveList.length && i < MS_SLOT_COUNT; i++) { if (0 === msPosOwner[i]) continue; if (slots[i] && slots[i].classList.contains("ms-slot--novideo")) continue; if (!slots[i] || !slots[i].querySelector(".ms-slot__video")) needRender.push(i); } MultiScreen_applyLayout(), needRender.forEach(function(i) { MultiScreen_renderSlot(i, msActiveList[i]); }); } let msSwapPatrolTimer = 0; let msSwapMissTicks = 0; function MultiScreen_teardownForPlayerSwap() { for (let i = 0; i < MS_SLOT_COUNT; i++) MultiScreen_clearSlot(i); if (MultiScreen_clearPendingWork(), msObserver) try { msObserver.disconnect(); } catch (e) {} msObserver = null, msWatched = null, msContainer = null, msActiveList = MultiScreen_normalize([]), msHistoryList = [], MultiScreen_resetPosOwner(), msMultiType = 0, MultiScreen_save(), MultiScreen_notifyChange(msActiveList.slice()); } function MultiScreen_checkPlayerSwap() { if (!msContainer) return false; if (msContainer.isConnected) return msSwapMissTicks = 0, false; const fresh = document.getElementById("js-player-multiContainer"); if (fresh && fresh !== msContainer) { if (msSwapMissTicks = 0, msActiveList.length >= MS_MIN_ROOMS) return MultiScreen_teardownForPlayerSwap(), true; return msContainer = null, false; } if (msSwapMissTicks++, msSwapMissTicks < 3) return false; if (msSwapMissTicks = 0, msActiveList.length < MS_MIN_ROOMS) return msContainer = null, false; return MultiScreen_teardownForPlayerSwap(), true; } function MultiScreen_bindOnce() { if (!msListenersBound) msListenersBound = true, document.addEventListener("mousedown", MultiScreen_onMouseDown), document.addEventListener("click", MultiScreen_onClick), document.addEventListener("mousemove", MultiScreen_onMouseMove), document.addEventListener("mouseup", MultiScreen_onMouseUp), document.addEventListener("mouseleave", MultiScreen_onPointerLost), window.addEventListener("blur", MultiScreen_onPointerLost), window.addEventListener("resize", MultiScreen_onResize); if (MultiScreen_watchContainer(), !msSwapPatrolTimer) msSwapPatrolTimer = setInterval(MultiScreen_checkPlayerSwap, 500); MultiScreen_applyLayout(); const mainCell = MultiScreen_slots()[msPosOwner.indexOf(0)]; if (mainCell) mainCell.classList.remove("ms-slot--loading", "ms-slot--notice"); if (msActiveList.length > 1) MultiScreen_renderAll(); } function initPkg_PopupPlayer_MultiScreen() { MultiScreen_load(); try { const W = "undefined" !== typeof unsafeWindow ? unsafeWindow : window; W.MultiScreen_getQnReport = window.MultiScreen_getQnReport; } catch (e) {} if (MultiScreen_isSupported()) return MultiScreen_bindOnce(), void 0; let tries = 0; const timer = setInterval(function() { if (tries++, MultiScreen_isSupported()) return clearInterval(timer), MultiScreen_bindOnce(), void 0; if (tries >= 40) clearInterval(timer); }, 1e3); } window.MultiScreen_enter = MultiScreen_enter, window.MultiScreen_exit = MultiScreen_exit, window.MultiScreen_addRoom = MultiScreen_addRoom, window.MultiScreen_removeRoom = MultiScreen_removeRoom, window.MultiScreen_setQuality = MultiScreen_setQuality, window.MultiScreen_isSupported = MultiScreen_isSupported, window.MultiScreen_getList = function() { return msActiveList.slice(); }, window.MultiScreen_getQuality = function() { return msQn; }, window.MultiScreen_pickQnForSlot = MultiScreen_pickQnForSlot, window.MultiScreen_qnFor = MultiScreen_qnFor, window.MultiScreen_getVolume = MultiScreen_getVolume, window.MultiScreen_setVolume = MultiScreen_setVolume, window.MultiScreen_setRoomFlag = MultiScreen_setRoomFlag, window.MultiScreen_isRoomOn = MultiScreen_isRoomOn, window.MultiScreen_roomQnName = MultiScreen_roomQnName, window.MultiScreen_getMetaName = MultiScreen_getMetaName, window.MultiScreen_resolveRoom = MultiScreen_resolveRoom, window.MultiScreen_slotIndexOfRid = MultiScreen_slotIndexOfRid, window.MultiScreen_roomRecord = MultiScreen_roomRecord, window.MultiScreen_savedConfigForMainRoom = MultiScreen_savedConfigForMainRoom, window.MultiScreen_getQnReport = MultiScreen_getQnReport, window.MultiScreen_reapplyAutoQuality = MultiScreen_reapplyAutoQuality, window.MultiScreen_switchQuality = MultiScreen_switchQuality, window.MultiScreen_finishSwap = MultiScreen_finishSwap, window.MultiScreen_abortSwap = MultiScreen_abortSwap, window.MultiScreen_destroyPlayer = MultiScreen_destroyPlayer, window.MS_SWAP_TIMEOUT_MS = MS_SWAP_TIMEOUT_MS, window.MS_QN_AUTO_TIERS = MS_QN_AUTO_TIERS, window.MS_STALL_TICK_MS = MS_STALL_TICK_MS, window.MS_STALL_QUIET_MS = MS_STALL_QUIET_MS, window.MultiScreen_setStallTimings = function(tickMs, quietMs) { const t = Number(tickMs); const q = Number(quietMs); if (t >= 20) msStallTickMs = t; if (q >= 40) msStallQuietMs = q; }, window.MultiScreen_getInternalState = function() { let players = 0, stallTimers = 0, startTimers = 0, retryTimers = 0, swapTimers = 0; return msSlotPlayers.forEach(function(rec) { if (!rec) return; if (players++, rec.stallTimer) stallTimers++; if (rec.startTimer) startTimers++; if (rec.retryTimer) retryTimers++; if (rec.swapTimer) swapTimers++; }), { list: msActiveList.length, players: players, stallTimers: stallTimers, startTimers: startTimers, retryTimers: retryTimers, swapTimers: swapTimers, qnCompare: !!msQnCompare, qnCompareTotal: msQnCompare ? Object.keys(msQnCompare.prev).length : 0, autoQnTimer: !!msAutoQnTimer, healTimer: !!msHealTimer, dragging: !!msDrag, posOwner: msPosOwner.slice(), quality: msQn, supported: MultiScreen_isSupported() }; }, window.MultiScreen_setStartTimeout = function(ms) { const n = Number(ms); if (n >= 20) msStartTimeoutMs = n; }, window.MultiScreen_onChange = function(fn) { if ("function" === typeof fn && msOnChangeList.indexOf(fn) < 0) msOnChangeList.push(fn); }, window.MultiScreen_onSlotClick = function(fn) { msOnSlotClick = fn; }, window.MultiScreen_hitTest = MultiScreen_hitTest, window.MultiScreen_getHost = MultiScreen_getHost, window.MultiScreen_slots = MultiScreen_slots, window.MultiScreen_getRecent = MultiScreen_getRecent, window.MultiScreen_pushRecent = MultiScreen_pushRecent, window.MultiScreen_QUALITY_LIST = MS_QUALITY_LIST; let hasAvatarBottom = false; function initPkg_RealAudience() { initPkg_RealAudience_StyleHook(), setAvatarVideo(), hideMatchSystemChatEntry(); } function initPkg_RealAudience_StyleHook() { StyleHook_set("Ex_Style_RealAudience", ` .VideoEntry{display:none !important;} `); } function hideMatchSystemChatEntry() { const hide = function(el) { if (el && el.style) el.style.display = "none"; return el; }; if (hide(document.querySelector(".MatchSystemChatRoomEntry"))) return true; if ("function" !== typeof ExGuard_pollFor) return false; return ExGuard_pollFor(".MatchSystemChatRoomEntry", hide, { intervalMs: 500, maxTries: 20, tag: "赛事聊天室入口", quiet: true }), false; } function setAvatarVideo() { let homeDom = document.querySelectorAll(".VideoEntry-tabItem>a")[0]; if (void 0 == homeDom) return; let videoUrl = homeDom.href + "?type=video"; let videoReplayUrl = homeDom.href + "?type=liveReplay"; setAvatarVideo_Dom(), setAvatarYuba_Dom(), setAvatarVideo_Func(videoUrl, videoReplayUrl); let bottomPic = document.getElementsByClassName("Title-anchorPic-bottom")[0]; if (bottomPic) bottomPic.style.display = "none", bottomPic.style.height = hasAvatarBottom ? "66px" : "22px"; let picBack = document.getElementsByClassName("Title-anchorPicBack")[0]; if (picBack && bottomPic) picBack.addEventListener("mouseenter", () => { bottomPic.style.display = "block"; }), picBack.addEventListener("mouseleave", () => { bottomPic.style.display = "none"; }); } function setAvatarVideo_Dom() { let a = document.createElement("div"); hasAvatarBottom = !!document.getElementsByClassName("Title-anchorPic-bottom")[0], a.className = hasAvatarBottom ? "" : "Title-anchorPic-bottom", a.innerHTML = `
    回看
    投稿
    `; let b = document.getElementsByClassName("Title-anchorPic-bottom")[0] || document.getElementsByClassName("Title-anchorPicBack")[0]; if (!b) return; b.insertBefore(a, b.childNodes[0]); } function setAvatarYuba_Dom() { let a = document.createElement("div"); hasAvatarBottom = !!document.getElementsByClassName("Title-anchorPic-bottom")[0], a.className = hasAvatarBottom ? "" : "Title-anchorPic-bottom", a.innerHTML = `
    打开鱼吧
    `; let b = document.getElementsByClassName("Title-anchorPic-bottom")[0] || document.getElementsByClassName("Title-anchorPicBack")[0]; if (!b) return; b.insertBefore(a, b.childNodes[0]); } function setAvatarVideo_Func(videoUrl, videoReplayUrl) { const submitDom = document.getElementById("Ex_VideoSubmit"); if (submitDom) submitDom.addEventListener("click", () => { openPage(videoUrl, true); }); const reviewDom = document.getElementById("Ex_VideoReview"); if (reviewDom) reviewDom.addEventListener("click", () => { openPage(videoReplayUrl, true); }); const yubaDom = document.getElementById("Ex_EnterYuba"); if (yubaDom) yubaDom.addEventListener("click", async () => { const data = await getBindYuba(rid); const url = data.data.group_url; openPage(url, true); }); } function getBindYuba(rid) { return new Promise((resolve, reject) => { fetch("https://www.douyu.com/wgapi/yubanc/api/group/getBindGroup?room_id=" + rid).then(response => response.json()).then(data => { resolve(data); }).catch(error => { reject(error); }); }); } function initPkg_Refresh() { Refresh_tryInit("拉高", initPkg_Refresh_BarrageFrame), Refresh_tryInit("隐藏礼物栏", initPkg_Refresh_Video), Refresh_tryInit("前缀", initPkg_Refresh_Barrage); } function Refresh_tryInit(name, fn) { try { if ("function" === typeof fn) fn(); return true; } catch (e) { if ("function" === typeof ExGuard_warn) ExGuard_warn("简洁模式", name + " 初始化失败(其余功能照常)", e); else console.error("[DouyuEx] 简洁模式初始化失败:" + name + " —— " + (e && e.message || e)); return false; } } function Refresh_readSave() { if ("undefined" === typeof localStorage || !localStorage) return {}; let raw = null; try { raw = localStorage.getItem("ExSave_Refresh"); } catch (e) { raw = null; } if (null == raw) return {}; let parsed = null; try { parsed = JSON.parse(raw); } catch (e) { if ("function" === typeof ExGuard_warn) ExGuard_warn("简洁模式", "本地配置已损坏,已按默认值处理", e); return {}; } return parsed && "object" === typeof parsed ? parsed : {}; } function Refresh_readFullScreen() { if ("undefined" === typeof localStorage || !localStorage) return false; let raw = null; try { raw = localStorage.getItem("ExSave_FullScreen"); } catch (e) { return false; } if (null == raw) return false; try { const parsed = JSON.parse(raw); return !!(parsed && parsed.isFullScreen); } catch (e) { return false; } } function saveData_Refresh() { let data = { barrageFrame: { status: refresh_BarrageFrame_getStatus() }, video: { status: refresh_Video_getStatus() }, barrage: { status: refresh_Barrage_getStatus() } }; localStorage.setItem("ExSave_Refresh", JSON.stringify(data)); } let current_barrage_status = 0; function initPkg_Refresh_Barrage() { initPkg_Refresh_Barrage_Dom(), initPkg_Refresh_Barrage_Func(), initPkg_Refresh_Barrage_Set(); } function initPkg_Refresh_Barrage_Dom() { Refresh_Barrage_insertIcon(); } function Refresh_Barrage_insertIcon() { let a = document.createElement("a"); a.className = "refresh-barrage", a.id = "refresh-barrage", a.innerHTML = '前缀'; let b = document.getElementsByClassName("Barrage-toolbar")[0]; if (b) b.insertBefore(a, b.childNodes[0]); } function Refresh_Barrage_applyUI(active) { const btn = document.getElementById("refresh-barrage"); if (btn) btn.classList[active ? "add" : "remove"]("ex-active"); const text = document.getElementById("refresh-barrage__text"); if (text) text.style.color = active ? "#fff" : "", text.innerText = active ? "恢复前缀" : "前缀"; const svg = document.getElementById("refresh-barrage__svg"); if (svg) { const p = svg.getElementsByTagName("path")[0]; if (p) p.setAttribute("fill", active ? "#ffffff" : "#AFAFAF"); } } function initPkg_Refresh_Barrage_Func() { let btn = document.getElementById("refresh-barrage"); if (btn) btn.addEventListener("click", function() { if (0 == current_barrage_status) { const doApply = function() { setRefreshBarrage(), saveData_Refresh(); }; if ("undefined" === typeof PostbirdAlertBox) { if ("function" === typeof showMessage) showMessage("【简洁模式】缺少确认弹窗组件,已直接屏蔽弹幕前缀", "warning"); return doApply(), void 0; } PostbirdAlertBox.confirm({ title: "提示", content: "是否屏蔽弹幕前缀(如粉丝牌、钻粉、贵族等标志)", okBtn: "确定", cancelBtn: "取消", onConfirm: doApply, onCancel: function() {} }); } else cancelRefreshBarrage(), saveData_Refresh(); }); else if ("function" === typeof ExGuard_warn) ExGuard_warn("简洁模式", "「前缀」按钮不存在,本次不绑定"); } function refresh_Barrage_getStatus() { if (1 == current_barrage_status) return true; else return false; } function initPkg_Refresh_Barrage_Set() { const saved = Refresh_readSave(); const cfg = saved.barrage; if (!cfg || true != cfg.status) return; setRefreshBarrage(); } function setRefreshBarrage() { let cssText = ` .UserCsgoGameDataMedal,.Barrage-honor,.Barrage-listItem .Barrage-icon,.Barrage-listItem .FansMedal.is-made,.Barrage-listItem .RoomLevel,.Barrage-listItem .Motor,.Barrage-listItem .ChatAchievement,.Barrage-listItem .Barrage-hiIcon,.Barrage-listItem .Medal,.Barrage-listItem .MatchSystemTeamMedal{display:none !important;} /*.Barrage-listItem .UserLevel{display:none !important;}*/ .Barrage-listItem .Baby{display:none !important;} .FansMedalWrap{display:none !important;} `; StyleHook_set("Ex_Style_RefreshBarrage", cssText), current_barrage_status = 1, Refresh_Barrage_applyUI(true); } function cancelRefreshBarrage() { StyleHook_remove("Ex_Style_RefreshBarrage"), current_barrage_status = 0, Refresh_Barrage_applyUI(false); } function initPkg_Refresh_BarrageFrame() { initPkg_Refresh_BarrageFrame_Dom(), initPkg_Refresh_BarrageFrame_Func(), initPkg_Refresh_BarrageFrame_Set(); } function initPkg_Refresh_BarrageFrame_Dom() { Refresh_BarrageFrame_insertIcon(); } function Refresh_BarrageFrame_insertIcon() { let a = document.createElement("a"); a.className = "refresh-barrage", a.id = "refresh-barrage-frame", a.innerHTML = '拉高'; let b = document.getElementsByClassName("Barrage-toolbar")[0]; if (b) b.insertBefore(a, b.childNodes[0]); } function Refresh_BarrageFrame_applyUI(active) { const btn = document.getElementById("refresh-barrage-frame"); if (btn) btn.classList[active ? "add" : "remove"]("ex-active"); const text = document.getElementById("refresh-barrage-frame__text"); if (text) text.style.color = active ? "#fff" : "", text.innerText = active ? "恢复高度" : "拉高"; const svg = document.getElementById("refresh-barrage-frame__svg"); if (svg) { const p = svg.getElementsByTagName("path")[0]; if (p) p.setAttribute("fill", active ? "#ffffff" : "#AFAFAF"); } } function Refresh_BarrageFrame_apply(raise, scene) { const dom_rank = document.getElementsByClassName("layout-Player-rank")[0]; const dom_activity = document.getElementById("js-room-activity"); const dom_topBarrage = document.getElementsByClassName("Barrage")[0]; if (!dom_rank || !dom_topBarrage) { if ("function" === typeof ExGuard_warn) ExGuard_warn("简洁模式", "缺少日榜或弹幕区节点,「拉高」本次未执行(场景:" + (scene || "未知") + ")"); return false; } if (raise) { if (dom_rank.style.display = "none", dom_activity) dom_activity.style.display = "none"; dom_topBarrage.className = "Barrage top-0-important"; } else { if (dom_rank.style.display = "block", dom_activity) dom_activity.style.display = "block"; dom_topBarrage.className = "Barrage"; } return Refresh_BarrageFrame_applyUI(raise), true; } function initPkg_Refresh_BarrageFrame_Func() { let btn = document.getElementById("refresh-barrage-frame"); if (!btn) { if ("function" === typeof ExGuard_warn) ExGuard_warn("简洁模式", "「拉高」按钮不存在,本次不绑定"); return; } btn.addEventListener("click", function() { if (refresh_BarrageFrame_getStatus()) { if (Refresh_BarrageFrame_apply(false, "点击")) saveData_Refresh(); return; } const doRaise = function() { if (Refresh_BarrageFrame_apply(true, "点击")) saveData_Refresh(); }; if ("undefined" === typeof PostbirdAlertBox) { if ("function" === typeof showMessage) showMessage("【简洁模式】缺少确认弹窗组件,已直接拉高弹幕框", "warning"); return doRaise(), void 0; } PostbirdAlertBox.confirm({ title: "提示", content: "是否拉高弹幕框,隐藏日榜周榜", okBtn: "确定", cancelBtn: "取消", onConfirm: doRaise, onCancel: function() {} }); }); } function refresh_BarrageFrame_getStatus() { const dom_rank = document.getElementsByClassName("layout-Player-rank")[0]; if (!dom_rank) return false; return "none" == dom_rank.style.display; } function initPkg_Refresh_BarrageFrame_Set() { const saved = Refresh_readSave(); const cfg = saved.barrageFrame; if (!cfg || true != cfg.status) return; Refresh_BarrageFrame_apply(true, "恢复"); } let video_num = 0; function initPkg_Refresh_Video() { let timer = setInterval(() => { const controlbar = getValidDom([ ".right-e7ea5d", ".right-17e251" ]); if (controlbar) clearInterval(timer), initPkg_Refresh_Video_Dom(), initPkg_Refresh_Video_Func(), initPkg_Refresh_Video_Set(); if (video_num++, video_num >= 100) clearInterval(timer); }, 1500); } function initPkg_Refresh_Video_Dom() { Refresh_Video_insertIcon(); } function Refresh_Video_insertIcon() { let a = document.createElement("li"); a.id = "refresh-video", a.innerText = "隐藏礼物栏"; let b = document.getElementsByClassName("menu-da2a9e")[0]; if (b) b.insertBefore(a, b.childNodes[b.childNodes.length - 1]); if (!document.getElementById("refresh-video3")) if (a = document.createElement("div"), a.id = "refresh-video3", a.title = "点击隐藏礼物栏", a.innerHTML = `
    隐藏礼物栏
    `, a.style = "position:absolute;left:18px;bottom:58px;padding:0 10px;height:28px;border-radius:14px;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.55);color:#fff;z-index:9999;cursor:pointer;user-select:none;opacity:0;transform:scale(.9);transition:opacity .15s ease,transform .15s ease,background-color .15s ease,box-shadow .3s ease;pointer-events:none;", b = document.getElementById("js-player-dialog"), b) b.insertBefore(a, b.childNodes[0]); } function initPkg_Refresh_Video_Func() { function changeToolBarZIndex() { let video_fullPage = false; let video_fullScreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement); let chatPanel_isHidden = false; if (document.querySelector(".wfs-2a8e83.removed-9d4c42")) video_fullPage = true; else if (document.querySelector(".toggle__P8TKM")) video_fullPage = true; if (document.querySelector(".shrink__Sd0uK")) chatPanel_isHidden = true; const dom_player_toolbar = document.getElementById("js-player-toolbar"); if (dom_player_toolbar) dom_player_toolbar.style = video_fullPage ? "z-index:20" : "z-index:30"; else if ("function" === typeof ExGuard_warn) ExGuard_warn("简洁模式", "播放器工具条不存在,本次跳过 z-index 调整"); const dom_casebar = document.getElementsByClassName("case__f4yex")[0]; if (dom_casebar) dom_casebar.style = (video_fullScreen || video_fullPage && chatPanel_isHidden) && refresh_Video_getStatus() ? "bottom: -84px;" : "bottom: 0;"; const isBeta = !!document.getElementsByClassName("live-next-body")[0]; if (isBeta && dom_player_toolbar && dom_player_toolbar.parentElement) dom_player_toolbar.parentElement.style = "z-index:20"; } new DomHook(".right-17e251", true, () => { changeToolBarZIndex(); }), new DomHook(".video__VfhVg", true, m => { for (var record of m) if (record.target.className.includes("toggle__P8TKM")) changeToolBarZIndex(); }); let dom = getValidDom([ ".layout-Player-video", ".stream__T55I3" ]); let dom_video = document.getElementsByClassName("room-Player-Box")[0]; let refresh_video3 = document.getElementById("refresh-video3"); let timer_timeout = 0; let isHoveringRefresh3 = false; function hideRefreshVideo3() { if (!refresh_video3) return; if (isHoveringRefresh3) return; refresh_video3.style.transition = "opacity .15s ease,transform .15s ease,background-color .15s ease,box-shadow .3s ease", refresh_video3.style.opacity = "0", refresh_video3.style.transform = "scale(.9)", refresh_video3.style.pointerEvents = "none", clearTimeout(timer_timeout); } function setRefreshVideo3Show() { if (!refresh_video3) return; refresh_video3.style.transition = "opacity .15s ease,transform .15s ease,background-color .15s ease,box-shadow .3s ease", refresh_video3.style.opacity = "1", refresh_video3.style.transform = "scale(1)", refresh_video3.style.pointerEvents = "auto", clearTimeout(timer_timeout), timer_timeout = setTimeout(() => { hideRefreshVideo3(); }, 2e3); } if (dom && refresh_video3) dom.addEventListener("mouseenter", () => { setRefreshVideo3Show(); }), dom.addEventListener("mouseleave", () => { hideRefreshVideo3(); }); if (dom_video && refresh_video3) dom_video.addEventListener("mousemove", () => { setRefreshVideo3Show(); }); if (refresh_video3) refresh_video3.addEventListener("mouseenter", () => { isHoveringRefresh3 = true, refresh_video3.style.transition = "opacity .15s ease,transform .15s ease,background-color .15s ease,box-shadow .3s ease", refresh_video3.style.opacity = "1", refresh_video3.style.transform = "scale(1.08)", refresh_video3.style.pointerEvents = "auto", refresh_video3.style.backgroundColor = "rgba(0,0,0,.7)", clearTimeout(timer_timeout); }), refresh_video3.addEventListener("mouseleave", () => { isHoveringRefresh3 = false, refresh_video3.style.transform = "scale(1)", refresh_video3.style.backgroundColor = "rgba(0,0,0,.55)", setRefreshVideo3Show(); }); function toggleRefreshVideo() { let dom_toolbar = document.getElementsByClassName("PlayerToolbar-ContentRow")[0]; let dom_video = getValidDom([ ".layout-Player-video", ".stream__T55I3" ]); let dom_refresh = document.getElementById("refresh-video"); let dom_refresh3 = document.getElementById("refresh-video3"); if (!dom_toolbar || !dom_video || !dom_refresh) return; if ("hidden" == dom_toolbar.style.visibility) { if (dom_toolbar.style.visibility = "visible", "function" === typeof ExPanel_onGiftBarShow) ExPanel_onGiftBarShow(); if (dom_video.style = "", dom_refresh3) dom_refresh3.style.opacity = "0", dom_refresh3.style.transform = "scale(.9)", dom_refresh3.style.pointerEvents = "none", dom_refresh3.title = "点击隐藏礼物栏"; dom_refresh.innerText = "隐藏礼物栏", updateRefreshSwitchUI(false), refresh_Video_removeStyle(); } else { if (dom_toolbar.style.visibility = "hidden", "function" === typeof ExPanel_onGiftBarHide) ExPanel_onGiftBarHide(); if (dom_video.style = "bottom:0;z-index:25", dom_refresh.innerText = "✓ 隐藏礼物栏", dom_refresh3) dom_refresh3.title = "点击显示礼物栏"; if (updateRefreshSwitchUI(true), dom_refresh3) dom_refresh3.style.transition = "opacity .3s ease,transform .3s cubic-bezier(0.175, 0.885, 0.32, 1.275),background-color .3s ease,box-shadow .3s ease", dom_refresh3.style.opacity = "1", dom_refresh3.style.transform = "scale(1.1)", dom_refresh3.style.pointerEvents = "auto", dom_refresh3.style.backgroundColor = "rgba(0,0,0,.8)", dom_refresh3.style.boxShadow = "0 0 15px rgba(255, 102, 0, 0.6)", clearTimeout(timer_timeout), timer_timeout = setTimeout(() => { dom_refresh3.style.transition = "opacity .15s ease,transform .15s ease,background-color .15s ease,box-shadow .15s ease", dom_refresh3.style.transform = "scale(1)", dom_refresh3.style.backgroundColor = "rgba(0,0,0,.55)", dom_refresh3.style.boxShadow = "none", timer_timeout = setTimeout(() => { hideRefreshVideo3(); }, 1500); }, 800); refresh_Video_setStyle(); } changeToolBarZIndex(), saveData_Refresh(), resizeWindow(); } let refreshVideoBtn = document.getElementById("refresh-video"); if (refreshVideoBtn) refreshVideoBtn.addEventListener("click", e => { toggleRefreshVideo(); }); if (refresh_video3) refresh_video3.addEventListener("click", e => { e.stopPropagation(), toggleRefreshVideo(); }); } function updateRefreshSwitchUI(isSimpleMode) { let swBg = document.getElementById("ex-refresh-switch"); let swCircle = document.getElementById("ex-refresh-switch-circle"); if (swBg && swCircle) if (isSimpleMode) swBg.style.background = "#f60", swCircle.style.left = "14px"; else swBg.style.background = "rgba(255,255,255,0.3)", swCircle.style.left = "2px"; } function refresh_Video_getStatus() { let dom_toolbar = document.getElementsByClassName("PlayerToolbar-ContentRow")[0]; if (dom_toolbar && "hidden" == dom_toolbar.style.visibility) return true; else return false; } function initPkg_Refresh_Video_Set() { const saved = Refresh_readSave(); const cfg = saved.video; if (!cfg || true != cfg.status) return; const dom_toolbar = document.getElementsByClassName("PlayerToolbar-ContentRow")[0]; const dom_video = getValidDom([ ".layout-Player-video", ".stream__T55I3" ]); const dom_refresh = document.getElementById("refresh-video"); const dom_refresh3 = document.getElementById("refresh-video3"); const dom_player_toolbar = document.getElementById("js-player-toolbar"); const missing = []; if (!dom_toolbar) missing.push(".PlayerToolbar-ContentRow"); if (!dom_video) missing.push(".layout-Player-video"); if (missing.length && "function" === typeof ExGuard_warn) ExGuard_warn("简洁模式", "「隐藏礼物栏」恢复时缺少节点:" + missing.join(" / ") + ",这几步跳过(样式与状态照常恢复)"); if (dom_toolbar) dom_toolbar.style.visibility = "hidden"; if (dom_video) dom_video.style = "bottom:0;z-index:25"; if (dom_player_toolbar) { dom_player_toolbar.style = Refresh_readFullScreen() ? "z-index:20" : "z-index:30"; const isBeta = !!document.getElementsByClassName("live-next-body")[0]; if (isBeta && dom_player_toolbar.parentElement) dom_player_toolbar.parentElement.style = "z-index:20"; } if (dom_refresh3) dom_refresh3.style.opacity = "0", dom_refresh3.style.transform = "scale(.9)", dom_refresh3.style.pointerEvents = "none", dom_refresh3.title = "点击显示礼物栏"; if (dom_refresh) dom_refresh.innerText = "✓ 隐藏礼物栏"; refresh_Video_setStyle(), resizeWindow(), setTimeout(() => { updateRefreshSwitchUI(true); }, 500); } function refresh_Video_setStyle() { StyleHook_set("Ex_Style_VideoRefresh", ` .PELact,.pushTower-wrapper-gf1HG,.PkView-9f6a2c,.MorePk,.RandomPKBar,.LiveRoomLoopVideo,.LiveRoomDianzan,.maiMaitView-68e80c,.PkView{display:none !important;} `); } function refresh_Video_removeStyle() { StyleHook_remove("Ex_Style_VideoRefresh"); } function initPkg_RemoveAD() { if (optimizePageStyle(), "function" === typeof initPkg_RemoveMsgNotice) initPkg_RemoveMsgNotice(); } function removeAD() { StyleHook_set("Ex_Style_RemoveAD", ` .ScreenBannerAd,.XinghaiAd,.UserInfo-tryEnterHiddenLead,.AnchorPocketTips,.FishShopTip,.FollowGuide,.RoomText-icon-horn,.RoomText-list,.noHandlerAd-0566b9,.DropMenuList-ad,.DropPane-ad,.igl_bg-b0724a,.closure-ab91fb,.VideoAboveVivoAd,.watermark-442a18,.FollowGuide-FadeOut,.FansMedalDialog-normal,.recommendAD-54569e,.recommendApp-0e23eb,.Bottom-ad,.SignBarrage,.SignBaseComponent-sign-ad,.SuperFansBubble,.Frawdroom,.HeaderGif-right,.HeaderGif-left{display:none !important;} .Barrage-topFloater{z-index:999} .danmuAuthor-3d7b4a, .danmuContent-25f266{overflow: initial} .LastLiveTime,.recommendView-3e8b62{display:none !important;} .feedback-e36e9d{display:none !important;} .FansMedalEnter-maxFlag{display:none !important;} .Header-follow-listBox{max-height:640px !important;} .GuessGameMiniPanelB-wrapper{display:none !important;} /*优化页面*/ #js-barrage-list-parent{scrollbar-width: none;-ms-overflow-style: none;width:98%;height:100%} #js-barrage-list-parent::-webkit-scrollbar{display: none;} /*陪玩*/ .InteractPlayWithEnter-enterTips1{display:none !important;} /*恢复emoji彩色 chrome加粗情况下emoji会变灰,需要找一个fontweight起始值在500的字体库才可以兼容*/ /*去除还在电脑面前的mask*/ .mask1-63237a,.mask2-a8df6e,.panel1-1484c9,.panel2-5ece0e{ display: none!important; } /*左侧悬浮二维码广告*/ .IconCardAdCard{ display: none!important; } /*视频右侧的游戏手柄按钮AD*/ .IconCardAd { display: none!important; } /*视频区视频广告*/ .IconCardAdBoundsBox{ display: none!important; } /*直播间顶部广告*/ .room-top-banner-box { display: none!important; } /*弹幕框底部进场弹幕信息:显隐由 Shield/RemoveEnter 控制,勿在此写 display 覆盖*/ /*弹幕框顶部广告*/ .aside-top-uspension-box { display: none!important; } #js-player-asideMain { top: 0!important; } #js-player-asideTopSuspension{display:none !important;} .Search-Panel-Advert{display:none !important;} /*推广位:德语 werbung = 广告。类名带构建哈希会轮换,故用包含匹配*/ [class*="werbungText"]{display:none !important;} /*播放器工具条:任务大厅 / 免费火箭。 dataid 比类名稳定,且工具条(.ToolBarCardProxyItem)与展开面板(.InteractItem) 用的是同一套 dataid,所以这里通吃两处,不必分别写选择器。*/ .PlayerToolbar-Task, [dataid="taskPanel"], [dataid="webGame"]{display:none !important;} /*以下用 :has() 连同外层槽位一起收起,否则只藏内容会留空白。 单独成条:不支持 :has() 的浏览器会整条丢弃,上面那条仍生效(内容照藏,只是留白)。 槽位不是只装广告,故必须 :has 精确匹配—— 工具条同排还有选手评分/异域商人/至臻殿堂/全民星推,activeItem 槽还有游戏榜/挑战进度。*/ .ToolbarCardModule:has(.PlayerToolbar-Task), .ToolbarCardModule:has(.ToolBarCardProxyItem[dataid="taskPanel"]), .ToolbarCardModule:has(.ToolBarCardProxyItem[dataid="webGame"]), [class*="activeItem__"]:has([class*="werbungText"]){display:none !important;} /*上面那排是绝对定位 + 写死的 right 偏移(0/78/156…),藏掉一个不会自动补位,会留空白。 改成正常流并反向排列:既让剩下的贴右对齐,又保持原来「从右往左」的顺序。 作用域限定在该容器内,避免误伤其它同名类。*/ [class*="activeContainer__"] [class*="activeBar__"]{width:auto !important;display:flex !important;flex-direction:row-reverse !important;align-items:center !important;} [class*="activeContainer__"] [class*="activeItem__"]{position:static !important;left:auto !important;right:auto !important;} `); } function optimizePageStyle() { let list = document.getElementById("js-barrage-list"); if (list && list.parentNode) list.parentNode.id = "js-barrage-list-parent"; } let isRemoveMsgNotice = 0; function initPkg_RemoveMsgNotice() { initPkg_RemoveMsgNotice_Dom(), initPkg_RemoveMsgNotice_Func(), initPkg_RemoveMsgNotice_Set(); } function initPkg_RemoveMsgNotice_Dom() { let a = document.createElement("div"); a.style = "position: absolute;right: 5px;top: 40px;cursor: pointer;", a.id = "ex-removeMsgNotice", a.innerHTML = ``, a.title = "关闭角标提醒"; let b = document.getElementsByClassName("PrivateLetter-frame")[0]; if (b) b.appendChild(a); } function initPkg_RemoveMsgNotice_Func() { let dom = document.getElementById("msg-removeNotice"); if (!dom) return; let checkbox = dom.querySelector("input"); dom.addEventListener("click", () => { let ischecked = checkbox.checked; if (true == ischecked) isRemoveMsgNotice = 1, removeMsgNotice(); else isRemoveMsgNotice = 0, removeMsgNoticeCanel(); saveData_removeMsgNotice(); }); } function initPkg_RemoveMsgNotice_Set() { let ret = localStorage.getItem("ExSave_isRemoveMsgNotice"); if (ret && "1" == ret) { isRemoveMsgNotice = 1, removeMsgNotice(); let dom = document.getElementById("msg-removeNotice"); if (dom) dom.querySelector("input").checked = true; } } function removeMsgNotice() { StyleHook_set("Ex_Style_RemoveMsgNotice", `.UserInfo .Badge,.ChatLetter-PopUnread{display:none!important;}`); } function removeMsgNoticeCanel() { StyleHook_remove("Ex_Style_RemoveMsgNotice"); } function saveData_removeMsgNotice() { localStorage.setItem("ExSave_isRemoveMsgNotice", isRemoveMsgNotice); } function initPkg_ChangeDanmakuLengthLimit() { let count = 0; let timer = setInterval(() => { if (count++, count > 100) return clearInterval(timer), void 0; let dom = document.getElementsByClassName("ChatSend-txt")[0]; if (void 0 != dom) changeChatTextareaMaxLength(), clearInterval(timer); }, 1e3); } function changeChatTextareaMaxLength() { let a; if (a = document.getElementsByClassName("ChatSend-button")[0], void 0 != a) a.className = "ChatSend-button"; if (a = document.getElementsByClassName("ChatSend-txt")[0], void 0 != a) a.maxLength = a.maxLength + 20; } function initPkg_Reset() { GM_registerMenuCommand("重置所有设置", () => { void Reset_run(); }); } function Reset_isPluginLocalStorageKey(key) { return key.startsWith("ExSave_") || key.startsWith("Ex_") || "Ex_isJoysound" === key || "freetimed" === key; } function Reset_clearPluginLocalStorage() { try { const keys = []; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && Reset_isPluginLocalStorageKey(k)) keys.push(k); } keys.forEach(k => localStorage.removeItem(k)); } catch (e) {} } async function Reset_clearGMStorage() { if ("function" !== typeof GM_deleteValue) return; let keys = []; try { if ("function" === typeof GM_listValues) { const ret = GM_listValues(); keys = ret && "function" === typeof ret.then ? await ret : ret || []; } else if ("undefined" !== typeof GM && GM.listValues) keys = await GM.listValues(); } catch (e) { return; } if (!Array.isArray(keys)) keys = []; for (let i = 0; i < keys.length; i++) try { GM_deleteValue(keys[i]); } catch (e2) {} } async function Reset_run() { if (!confirm("确定要清空 DouyuEx 的所有本地设置吗?\n\n包括:油猴存储(GM)与本站 localStorage 中的插件数据。\n此操作不可恢复。")) return; await Reset_clearGMStorage(), Reset_clearPluginLocalStorage(), alert("已清空。请刷新斗鱼页面以使界面与功能恢复默认状态。"); } function initPkg_RestoreYuba() { responseHook((url, text) => { if (-1 !== url.indexOf("group/getBindGroup")) return text.replace('"group_status":4', '"group_status":0'); return text; }); } function initPkg_RestoreYuba_restore() { let oldId = null; let newId = null; oldId = RestoreYuba_getYubaId(window.location.href); const urlParams = new URLSearchParams(window.location.search); const exRestore = urlParams.get("exRestore"); if (!exRestore) return; if (newId = Number(exRestore), oldId === newId) return; RestoreYuba_initHook(oldId, newId), RestoreYuba_changeDom(oldId, newId); } function RestoreYuba_checkRedirect() { const oldId = RestoreYuba_getYubaId(window.location.href); if (!oldId) return; getYubaStatus(oldId).then(data => { if (3002 == data.status_code) { const url = "https://yuba.douyu.com/discussion/4815048/posts?exRestore=" + oldId; window.location.href = url; } }); } function RestoreYuba_getYubaId(url) { const match = url.match(/\/discussion\/(\d+)/); if (match && match[1]) return match[1]; return null; } function RestoreYuba_initHook(oldId, newId) { const excludeUrl = [ "web/group/head", "/follow/topic", "group/unfollowGroup" ]; function shouldExcludeUrl(url) { if ("string" !== typeof url) return false; return excludeUrl.some(keyword => url.includes(keyword)); } const originalOpen = unsafeWindow.XMLHttpRequest.prototype.open; const originalSend = unsafeWindow.XMLHttpRequest.prototype.send; unsafeWindow.XMLHttpRequest.prototype.open = function(method, url, async, user, password) { if ("string" === typeof url && url.includes(oldId) && !shouldExcludeUrl(url)) url = url.replace(new RegExp(oldId, "g"), newId); return originalOpen.call(this, method, url, async, user, password); }, unsafeWindow.XMLHttpRequest.prototype.send = function(body) { const url = this.responseURL || this._url || ""; if (shouldExcludeUrl(url)) return originalSend.call(this, body); if (body && "string" === typeof body && body.includes(oldId)) body = body.replace(new RegExp(oldId, "g"), newId); else if (body && body instanceof FormData) { const newFormData = new FormData(); for (var pair of body.entries()) { const key = pair[0]; let value = pair[1]; if ("string" === typeof value && value.includes(oldId)) value = value.replace(new RegExp(oldId, "g"), newId); newFormData.append(key, value); } body = newFormData; } return originalSend.call(this, body); }; const originalFetch = unsafeWindow.fetch; unsafeWindow.fetch = function(input, init) { let url = ""; if ("string" === typeof input) { if (url = input, input.includes(oldId) && !shouldExcludeUrl(input)) input = input.replace(new RegExp(oldId, "g"), newId); } else if (input instanceof Request) if (url = input.url, url.includes(oldId) && !shouldExcludeUrl(url)) input = new Request(url.replace(new RegExp(oldId, "g"), newId), input); if (shouldExcludeUrl(url)) return originalFetch.call(unsafeWindow, input, init); if (init && init.body) if ("string" === typeof init.body && init.body.includes(oldId)) init.body = init.body.replace(new RegExp(oldId, "g"), newId); else if (init.body instanceof FormData) { const newFormData = new FormData(); for (var pair of init.body.entries()) { const key = pair[0]; let value = pair[1]; if ("string" === typeof value && value.includes(oldId)) value = value.replace(new RegExp(oldId, "g"), newId); newFormData.append(key, value); } init.body = newFormData; } return originalFetch.call(unsafeWindow, input, init); }; } async function RestoreYuba_changeDom(oldId, newId) { const anchorInfo = await getYubaAnchorInfo(newId); if (anchorInfo) { const info = anchorInfo.data.generalOP[0]; const avatar = info.avatar; const nickname = info.nick_name; function changeTitle() { const avatarNode = document.querySelector(".groupavatar__9mD1S .image__GNnZC"); if (avatarNode) avatarNode.src = avatar; const nameNode = document.getElementsByClassName("groupname__BUzOM")[0]; if (nameNode) nameNode.innerText = nickname; const descNode = document.getElementsByClassName("groupdesc__b8-53")[0]; if (descNode) descNode.innerText = nickname + `的鱼吧`; if (!avatarNode && !nameNode && !descNode) ExGuard_warn("鱼吧恢复", "群头像/群名/群简介节点都不在,本次只改标题"); document.title = nickname + `的鱼吧`; } changeTitle(), new DomHook(".groupavatar__9mD1S", false, () => { changeTitle(); }); } } function getYubaAnchorInfo(groupId) { return new Promise(resolve => { fetch("https://yuba.douyu.com/wbapi/web/group/managersdetail?group_id=" + groupId).then(response => response.json()).then(data => { resolve(data); }).catch(() => { resolve(null); }); }); } function getYubaStatus(groupId) { return new Promise((resolve, reject) => { fetch("https://yuba.douyu.com/wbapi/web/group/head?group_id=" + groupId).then(response => response.json()).then(data => { resolve(data); }).catch(error => { reject(error); }); }); } const ROOM_REVENUE_INTERVAL = 15e4; const ROOM_REVENUE_API = "https://dy.roozj.com/api/ranking/anchors"; let roomRevenueState = { initialized: false, enabled: true, timer: null, waitTimer: null, waitTries: 0, request: null, context: null, blockedRid: "", cache: new Map(), income: null, donors: null, paidDonors: null, speakers: null, updatedAt: null, loading: false, stale: false, message: "" }; let roomRevenueRankNode = null; function RoomRevenue_id(value) { const text = void 0 === value || null === value ? "" : String(value).trim(); return /^[1-9]\d*$/.test(text) ? text : ""; } function RoomRevenue_today() { const now = new Date(); return now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0") + "-" + String(now.getDate()).padStart(2, "0"); } function RoomRevenue_abort() { const request = roomRevenueState.request; if (roomRevenueState.request = null, roomRevenueState.loading = false, request) { request.done = true; try { if (request.handle && request.handle.abort) request.handle.abort(); } catch (error) {} } } function RoomRevenue_syncContext() { const state = roomRevenueState; const path = window.location.pathname; const shortId = RoomRevenue_id((path.match(/^\/(?:beta\/)?(\d+)(?:\/|$)/) || [])[1]); let realId = RoomRevenue_id("undefined" === typeof rid ? "" : rid); if (state.context && state.context.path !== path) state.blockedRid = state.context.realId || state.blockedRid; if (state.blockedRid && realId === state.blockedRid && shortId !== realId) realId = ""; else if (realId !== state.blockedRid || realId === shortId) state.blockedRid = ""; const date = RoomRevenue_today(); const context = { path: path, realId: realId, shortId: shortId, date: date }; if (context.key = [ path, realId, shortId, date ].join("|"), !state.context || state.context.key !== context.key) { RoomRevenue_abort(), state.context = context; const cached = state.cache.get(context.key); state.income = cached ? cached.income : null, state.donors = cached ? cached.donors : null, state.paidDonors = cached ? cached.paidDonors : null, state.speakers = cached ? cached.speakers : null, state.updatedAt = cached ? cached.updatedAt : null, state.stale = !!(cached && Date.now() - cached.updatedAt >= ROOM_REVENUE_INTERVAL), state.message = ""; } return state.context; } function RoomRevenue_visible() { try { let prototype = Object.getPrototypeOf(document); while (prototype) { const hidden = Object.getOwnPropertyDescriptor(prototype, "hidden"); if (hidden && hidden.get) return !hidden.get.call(document); prototype = Object.getPrototypeOf(prototype); } } catch (error) {} return !document.hidden && "hidden" !== document.visibilityState; } function RoomRevenue_render() { const node = document.querySelector(".ex-room-revenue"); if (!node) return; const state = roomRevenueState; const value = node.querySelector(".ex-room-revenue__value"); let text = ""; if (null !== state.income) { if (text += (state.income > 0 && state.income < .01 ? "<0.01" : state.income.toLocaleString("zh-CN", { maximumFractionDigits: 2 })) + "鱼翅", state.stale) text += "(上次)"; } else text = state.loading ? "…" : "—"; if (value) value.textContent = text; const refresh = node.querySelector(".ex-room-revenue__refresh"); if (refresh) refresh.disabled = state.loading; const date = state.context ? state.context.date : RoomRevenue_today(); const detail = [ "统计日期:" + date + "(本地今日)", "来源:dy.roozj.com 公开榜单;单位:鱼翅", "第三方统计流水,不代表主播实际到账收入" ]; if (null !== state.income) detail.push("公开流水数值:" + state.income + "鱼翅"); if (null !== state.donors) detail.push("送礼人数:" + state.donors); if (null !== state.paidDonors) detail.push("付费送礼人数:" + state.paidDonors); if (null !== state.speakers && void 0 !== state.speakers) detail.push("今日发言人数:" + state.speakers); if (null !== state.updatedAt) detail.push("获取时间:" + new Date(state.updatedAt).toLocaleString()); if (state.stale) detail.push("显示上次成功结果"); if (state.loading) detail.push("正在刷新"); if (state.message) detail.push(state.message); node.title = detail.join("\n"); const metrics = [ [ "donors", state.donors, "今日送礼人数", "total_fans" ], [ "speakers", state.speakers, "今日发言人数", "danmu_users" ] ]; for (var [ name, count, label, field ] of metrics) { const item = node.querySelector('[data-revenue-metric="' + name + '"]'); const number = node.querySelector(".ex-room-revenue__" + name); const formatted = null === count || void 0 === count ? "—" : count.toLocaleString("zh-CN"); if (number) number.textContent = formatted; if (item) item.title = label + "(公开榜单 " + field + "):" + formatted + "\n" + node.title, item.setAttribute("aria-label", label + " " + formatted); } const money = node.querySelector('[data-revenue-metric="income"]'); if (money) money.title = "今日流水(公开榜单 total_income,单位:鱼翅)\n" + node.title, money.setAttribute("aria-label", "今日流水 " + text); } const ROOM_REVENUE_ICONS = { donors: '', speakers: '', income: '', refresh: '', source: '' }; function RoomRevenue_icon(kind) { const span = document.createElement("span"); span.className = "ex-room-revenue__icon ex-room-revenue__icon--" + kind, span.setAttribute("aria-hidden", "true"); const svg = ROOM_REVENUE_ICONS[kind]; if (svg) span.innerHTML = svg; else span.textContent = { donors: "礼", speakers: "言", income: "¥" }[kind] || ""; return span; } function RoomRevenue_reserveSpace(host) { let rank = null; const region = host && (host.closest(".layout-Player-asideMainTop") || host.closest(".layout-Player-asideMain") || host.parentElement); if (region) { const candidates = Array.from(region.querySelectorAll(".layout-Player-rank")).filter(function(item) { return !item.contains(host); }); if (1 === candidates.length) { const position = window.getComputedStyle(candidates[0]).position; if ("absolute" === position || "fixed" === position) rank = candidates[0]; } } if (roomRevenueRankNode && roomRevenueRankNode !== rank) roomRevenueRankNode.classList.remove("ex-room-revenue-rank-offset"); if (roomRevenueRankNode = rank, rank) rank.classList.add("ex-room-revenue-rank-offset"); } function RoomRevenue_mount() { const host = document.querySelector(".layout-Player-announce") || document.querySelector(".layout-Player-rankAll"); let node = document.querySelector(".ex-room-revenue"); if (!host) { if (node) node.remove(); return RoomRevenue_reserveSpace(null), false; } if (!node) node = document.createElement("div"); if (node.className = "ex-room-revenue", !node.querySelector(".ex-room-revenue__metrics")) { node.replaceChildren(); const metrics = document.createElement("div"); metrics.className = "ex-room-revenue__metrics"; for (var name of [ "donors", "speakers", "income" ]) { const item = document.createElement("span"); item.className = "ex-room-revenue__metric", item.dataset.revenueMetric = name, item.appendChild(RoomRevenue_icon(name)); const value = document.createElement("span"); value.className = "income" === name ? "ex-room-revenue__value" : "ex-room-revenue__" + name, item.appendChild(value), metrics.appendChild(item); } const refresh = document.createElement("button"); refresh.type = "button", refresh.className = "ex-room-revenue__refresh", refresh.innerHTML = ROOM_REVENUE_ICONS.refresh || "↻", refresh.title = "刷新今日流水", refresh.setAttribute("aria-label", "刷新今日流水"), refresh.addEventListener("click", function(event) { event.stopPropagation(), RoomRevenue_refresh(true); }); const source = document.createElement("a"); source.className = "ex-room-revenue__source", source.href = "https://dy.roozj.com/", source.target = "_blank", source.rel = "noopener noreferrer", source.innerHTML = ROOM_REVENUE_ICONS.source || "↗", source.title = "来源:dy.roozj.com 公开榜单", source.setAttribute("aria-label", "查看流水来源 dy.roozj.com"), node.append(metrics, refresh, source); } if (node.parentNode !== host || host.firstElementChild !== node) host.insertBefore(node, host.firstChild); return RoomRevenue_reserveSpace(host), RoomRevenue_render(), true; } function RoomRevenue_masked(value) { return true === value || 1 === value || "true" === value || "1" === value; } function RoomRevenue_count(value) { if ("number" !== typeof value && !("string" === typeof value && /^\d+$/.test(value.trim()))) return null; const count = Number(value); return Number.isSafeInteger(count) && count >= 0 ? count : null; } function RoomRevenue_parse(response, context) { if (!response || !Number.isFinite(Number(response.status)) || response.status < 200 || response.status >= 300) return { error: "数据源暂不可用(HTTP错误)" }; if (response.finalUrl) try { const finalUrl = new URL(response.finalUrl); if ("https://dy.roozj.com" !== finalUrl.origin || "/api/ranking/anchors" !== finalUrl.pathname) return { error: "数据源返回了非榜单页面" }; } catch (error) { return { error: "数据源地址不可用" }; } let payload = response.response; if (void 0 === payload || null === payload) payload = response.responseText; try { if ("string" === typeof payload) payload = JSON.parse(payload); } catch (error) { return { error: "数据源未返回有效JSON(可能是HTML页面)" }; } if (!payload || "object" !== typeof payload) return { error: "数据格式不可用" }; if (RoomRevenue_masked(payload.isMasked)) return { error: "来源未公开此数据" }; if (0 !== payload.code && "0" !== payload.code) return { error: "数据源返回错误" }; if (!Array.isArray(payload.data)) return { error: "榜单数据格式不可用" }; const matches = payload.data.filter(function(row) { if (!row || "object" !== typeof row) return false; return context.realId && RoomRevenue_id(row.room_id) === context.realId || context.shortId && RoomRevenue_id(row.vip_id) === context.shortId || !context.realId && context.shortId && RoomRevenue_id(row.room_id) === context.shortId; }); const exact = context.realId ? matches.filter(function(row) { return RoomRevenue_id(row.room_id) === context.realId; }) : []; const rows = exact.length ? exact : matches; if (!rows.length) return { error: "公开榜单未找到本房间" }; if (1 !== rows.length) return { error: "榜单房间匹配不唯一" }; const row = rows[0]; if (RoomRevenue_masked(row.isMasked)) return { error: "来源未公开此房间流水" }; const amount = row.total_income; if ("number" !== typeof amount && !("string" === typeof amount && /^\d+(?:\.\d+)?$/.test(amount.trim()))) return { error: "来源暂无有效流水数值" }; const income = Number(amount); if (!Number.isFinite(income) || income < 0) return { error: "来源流水数值不可用" }; return { income: income, donors: RoomRevenue_count(row.total_fans), paidDonors: RoomRevenue_count(row.paid_fans), speakers: RoomRevenue_count(row.danmu_users) }; } function RoomRevenue_refresh(force) { const state = roomRevenueState; if (!state.enabled) return; const context = RoomRevenue_syncContext(); if (!RoomRevenue_mount() || !RoomRevenue_visible()) return; if (state.request) return; const cached = state.cache.get(context.key); if (!force && cached && Date.now() - cached.updatedAt < ROOM_REVENUE_INTERVAL) return state.income = cached.income, state.donors = cached.donors, state.paidDonors = cached.paidDonors, state.speakers = cached.speakers, state.updatedAt = cached.updatedAt, state.stale = false, state.message = "", RoomRevenue_render(), void 0; const search = context.shortId || context.realId; if (!search) return state.message = "无法确定当前房间", RoomRevenue_render(), void 0; const request = { key: context.key, handle: null, done: false }; function finish(result) { if (request.done || !state.enabled || state.request !== request) return; const current = RoomRevenue_syncContext(); if (request.done || current.key !== request.key) return RoomRevenue_render(), void 0; if (!RoomRevenue_visible()) return RoomRevenue_abort(), RoomRevenue_render(), void 0; if (request.done = true, state.request = null, state.loading = false, result.error) state.message = result.error, state.stale = null !== state.income; else { state.income = result.income, state.donors = result.donors, state.paidDonors = result.paidDonors, state.speakers = result.speakers, state.updatedAt = Date.now(), state.stale = false, state.message = "", state.cache.delete(context.key), state.cache.set(context.key, { income: result.income, donors: result.donors, paidDonors: result.paidDonors, speakers: result.speakers, updatedAt: state.updatedAt }); while (state.cache.size > 8) state.cache.delete(state.cache.keys().next().value); } RoomRevenue_render(); } if (state.request = request, state.loading = true, state.stale = null !== state.income && null !== state.updatedAt && Date.now() - state.updatedAt >= ROOM_REVENUE_INTERVAL, RoomRevenue_render(), "function" !== typeof GM_xmlhttpRequest) return finish({ error: "当前环境无法请求流水来源" }), void 0; try { request.handle = GM_xmlhttpRequest({ method: "GET", url: ROOM_REVENUE_API + "?startDate=" + context.date + "&endDate=" + context.date + "&clubName=all&search=" + encodeURIComponent(search) + "&limit=50", anonymous: true, timeout: 8e3, responseType: "json", onload: function(response) { finish(RoomRevenue_parse(response, context)); }, onerror: function() { finish({ error: "流水请求失败" }); }, ontimeout: function() { finish({ error: "流水请求超时" }); }, onabort: function() { finish({ error: "流水请求已取消" }); } }); } catch (error) { finish({ error: "流水请求失败" }); } } function RoomRevenue_stop() { const state = roomRevenueState; if (null !== state.timer) clearInterval(state.timer); if (null !== state.waitTimer) clearTimeout(state.waitTimer); state.timer = null, state.waitTimer = null, state.waitTries = 0, RoomRevenue_abort(); } function RoomRevenue_start() { const state = roomRevenueState; if (!state.enabled) return; if (RoomRevenue_syncContext(), !RoomRevenue_mount()) { if (null !== state.waitTimer || state.waitTries >= 20) return; return state.waitTimer = setTimeout(function() { state.waitTimer = null, state.waitTries++, RoomRevenue_start(); }, 500), void 0; } if (null !== state.waitTimer) clearTimeout(state.waitTimer); if (state.waitTimer = null, state.waitTries = 0, !RoomRevenue_visible()) return; if (null === state.timer) state.timer = setInterval(function() { RoomRevenue_refresh(true); }, ROOM_REVENUE_INTERVAL); RoomRevenue_refresh(false); } function RoomRevenue_setEnabled(enabled) { roomRevenueState.enabled = !!enabled; try { localStorage.setItem("ExSave_RoomRevenue", JSON.stringify({ enabled: !!enabled })); } catch (error) {} if (RoomRevenue_stop(), enabled) RoomRevenue_start(); else { const node = document.querySelector(".ex-room-revenue"); if (node) node.remove(); RoomRevenue_reserveSpace(null); } } function initPkg_RoomRevenue() { const state = roomRevenueState; if (!state.initialized) { state.initialized = true; try { const saved = JSON.parse(localStorage.getItem("ExSave_RoomRevenue") || "null"); state.enabled = false !== saved && !(saved && false === saved.enabled); } catch (error) {} if ("function" === typeof GM_registerMenuCommand) GM_registerMenuCommand("切换今日流水显示", function() { RoomRevenue_setEnabled(!state.enabled); }); if (window.addEventListener("visibilitychange", function() { if (RoomRevenue_visible()) RoomRevenue_start(); else RoomRevenue_stop(), RoomRevenue_render(); }, true), window.addEventListener("popstate", RoomRevenue_start), window.addEventListener("hashchange", RoomRevenue_start), window.addEventListener("pageshow", RoomRevenue_start), window.addEventListener("pagehide", RoomRevenue_stop), window.navigation && window.navigation.addEventListener) window.navigation.addEventListener("navigatesuccess", RoomRevenue_start); } if (null === state.waitTimer) state.waitTries = 0; RoomRevenue_start(); } const roomVipExpireDayLimit = 5; function initPkg_RoomVip() { setRoomVipExpireDays(); } const ROOM_VIP_ANCHOR_SELECTORS = [ "#js-backpack-enter", ".BackpackButton", ".PlayerToolbar-ContentRow .PlayerToolbar-ContentCell" ]; let roomVipAnchorWaiting = false; function RoomVip_findAnchor() { return getValidDom(ROOM_VIP_ANCHOR_SELECTORS); } function initPkg_RoomVip_Dom() { const existing = document.querySelector(".room-vip"); if (existing) return existing; let a = document.createElement("span"); a.className = "room-vip", a.innerHTML = ` 距VIP到期 ** 天 `; const anchor = RoomVip_findAnchor(); const parent = anchor ? anchor.parentElement : null; if (!parent) return null; return parent.insertBefore(a, parent.childNodes[0]), a; } function RoomVip_writeDays(badge, days) { if (!badge) return false; if (days < 0) return badge.innerHTML = "VIP 已过期 " + Math.abs(days) + " 天", true; const dayDom = badge.querySelector("#room-vip-expire-days") || document.getElementById("room-vip-expire-days"); if (dayDom) dayDom.innerText = days; return true; } function RoomVip_showDays(days) { const badge = initPkg_RoomVip_Dom(); if (badge) return RoomVip_writeDays(badge, days), true; if (roomVipAnchorWaiting) return false; if ("function" !== typeof ExGuard_pollFor) { if ("function" === typeof ExGuard_warn) ExGuard_warn("房间VIP", "没找到背包入口(" + ROOM_VIP_ANCHOR_SELECTORS.join(" / ") + "),本次不显示 VIP 到期天数"); return false; } return roomVipAnchorWaiting = true, ExGuard_pollFor(RoomVip_findAnchor, function() { roomVipAnchorWaiting = false; const late = initPkg_RoomVip_Dom(); if (late) RoomVip_writeDays(late, days); }, { intervalMs: 500, maxTries: 20, tag: "房间VIP" }), false; } function setRoomVipExpireDays() { fetch("https://www.douyu.com/member/platform_task/effect_list", { method: "GET", mode: "no-cors", cache: "default", credentials: "include" }).then(res => res.text()).then(text => { let doc = null; try { doc = new DOMParser().parseFromString(String(text || ""), "text/html"); } catch (e) { if ("function" === typeof ExGuard_warn) ExGuard_warn("房间VIP", "特效列表页面解析失败", e); return; } const enterEffectDom = doc.getElementsByClassName("enter-wraper is-effect"); if (!enterEffectDom || 0 == enterEffectDom.length) return; const showEffectMoreDom = enterEffectDom[0].getElementsByClassName("show-effect-more"); if (!showEffectMoreDom || 0 == showEffectMoreDom.length) return; for (let i = 0; i < showEffectMoreDom.length; i++) { let detail = null; try { detail = JSON.parse(showEffectMoreDom[i].getAttribute("data-detail") || "{}"); } catch (e) { if ("function" === typeof ExGuard_warn) ExGuard_warn("房间VIP", "有一条特效数据不是合法 JSON,已跳过", e); continue; } if (!detail || "1646" !== String(detail.property_id)) continue; if (String(detail.show_id_list) !== String(rid)) continue; const expireTime = 1e3 * Number(detail.expire_time); if (!isFinite(expireTime)) continue; const days = Math.floor((expireTime - Date.now()) / (1e3 * 60 * 60 * 24)); if (days <= roomVipExpireDayLimit) RoomVip_showDays(days); } }).catch(err => { if ("function" === typeof ExGuard_warn) ExGuard_warn("房间VIP", "读取特效列表失败,本次不显示 VIP 到期天数", err); else console.log("请求失败!", err); }); } let isRemoveDanmakuBackground = getLocalIsRemoveDanmakuBackground(); try { if (isRemoveDanmakuBackground) removeDanmakuBackground(); } catch (e) { ExGuard_warn("屏蔽弹幕背景", "启动时应用样式失败,稍后随面板重绘再试", e); } function getLocalIsRemoveDanmakuBackground() { const ret = localStorage.getItem("ExSave_isRemoveDanmakuBackground"); return ret ? 1 === Number(ret) : false; } function saveRemoveDanmakuBackground() { localStorage.setItem("ExSave_isRemoveDanmakuBackground", isRemoveDanmakuBackground ? 1 : 0); } function removeDanmakuBackground() { StyleHook_set("Ex_Style_RemoveDanmakuBackground", ` .danmuItem-a8616a { background: none !important; } .danmuItem-a8616a div{ background: none; } .danmuItem-a8616a > img { display: none; } .danmuItem-a8616a div > img { display: none; } .super-text-f60bfa { background: none !important; } .danmuItem-a8616a .noble-d35c82 { background: none !important; } .customBarrage { background: none !important; text-shadow: none !important; } .customBarrage > div { background: none !important; } .PlayerCustomBarrage-prefixPlugin--text { display: none !important; } `); } let isRemoveDanmakuImage = getLocalIsRemoveDanmakuImage(); function initPkg_Shield_RemoveDanmakuImage() { const shieldTool = document.getElementsByClassName("ShieldTool-list")[0]; if (!shieldTool) return ExGuard_warn("屏蔽弹幕图片", "弹幕屏蔽工具条不存在,本次不注入开关"), void 0; if (document.getElementById("ex-RemoveDanmakuImage")) return; if (shieldTool.insertAdjacentHTML("beforeend", `
    屏蔽DouyuEx图片
    `), isRemoveDanmakuImage) removeDanmakuImage(); const dom = document.getElementById("ex-RemoveDanmakuImage"); if (!dom) return; dom.addEventListener("click", () => { if (isRemoveDanmakuImage = !isRemoveDanmakuImage, isRemoveDanmakuImage) removeDanmakuImage(), dom.className = dom.className.replace("is-noChecked", "is-checked"); else StyleHook_remove("Ex_Style_RemoveDanmakuImage"), dom.className = dom.className.replace("is-checked", "is-noChecked"); saveRemoveDanmakuImage(); }); } function getLocalIsRemoveDanmakuImage() { const ret = localStorage.getItem("ExSave_isRemoveDanmakuImage"); return ret ? 1 === Number(ret) : false; } function saveRemoveDanmakuImage() { localStorage.setItem("ExSave_isRemoveDanmakuImage", isRemoveDanmakuImage ? 1 : 0); } function removeDanmakuImage() { StyleHook_set("Ex_Style_RemoveDanmakuImage", ` .danmuItem-a8616a a { display: none !important; } `); } let isRemoveEnterBarrage = getLocalIsRemoveEnterBarrage(); function Shield_applyRemoveEnter() { const el = document.getElementById("js-barrage-extend-container"); if (!el) return false; if (isRemoveEnterBarrage) el.style.setProperty("display", "none", "important"); else el.style.removeProperty("display"); return true; } function Shield_applyRemoveEnterWhenReady(tries) { const n = tries || 0; if (Shield_applyRemoveEnter()) return true; if (n >= 120) return false; return setTimeout(function() { Shield_applyRemoveEnterWhenReady(n + 1); }, 500), false; } function getLocalIsRemoveEnterBarrage() { const ret = localStorage.getItem("ExSave_isRemoveEnterBarrage"); return ret ? 1 === Number(ret) : true; } function saveRemoveEnterBarrage() { localStorage.setItem("ExSave_isRemoveEnterBarrage", isRemoveEnterBarrage ? 1 : 0); } let isRemoveRepeatedDanmaku = getLocalIsRemoveRepeatedDanmaku(); let repeatedDanmakuSeconds = getLocalRepeatedDanmakuSeconds(); let isEnlargeDanmaku = getLocalIsEnlargeDanmaku(); let repeatedDanmakuMap = {}; let repeatedDanmakuUuidMap = {}; let repeatedDanmakuDomMap = {}; let repeatedDanmakuCountMap = {}; let repeatedDanmakuOriginalFontSizeMap = new WeakMap(); let repeatedDanmakuCleanupTimer = null; let repeatedDanmakuDomHook = null; let RemoveRepeatedDanmaku_pollOpts = { intervalMs: 1e3, maxTries: 60, tag: "屏蔽重复弹幕" }; try { if (isRemoveRepeatedDanmaku) removeRepeatedDanmaku(); } catch (e) { ExGuard_warn("屏蔽重复弹幕", "启动时挂载失败,稍后随面板重绘再试", e); } function initPkg_Shield_RemoveRepeatedDanmaku_ScriptHook() { scriptHook({ url: "/firstqueue", callback: content => { let newContent = content; return newContent = newContent.replace(`e.display=new e.renderer(e);`, `e.display=new e.renderer(e);e.display.raw.comment=e;`), newContent; } }); } function RemoveRepeatedDanmaku_getKey(dom) { const c = dom && dom.comment; const d = c && c.data; let text = d && "string" === typeof d.text ? d.text.trim() : ""; if (!text) text = dom && dom.textContent ? dom.textContent.trim() : ""; if (!text) return ""; const src = d && d.extraData && d.extraData.exMsSrcRid; return src ? "r" + src + "|" + text : text; } function removeRepeatedDanmaku() { StyleHook_set("Ex_Style_RemoveRepeatedDanmaku_Count", ` /* 弹幕计数显示样式 */ [data-repeat-count]::before { content: "x" attr(data-repeat-count); font-weight: bold; display: inline-block; position: absolute; right: -18px; bottom: 0; font-size: 16px; font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif; color: inherit; } /* 计数跳动动画 */ @keyframes danmaku-combo-bounce { 0% { transform: scale(1); } 50% { transform: scale(1.5); } 100% { transform: scale(1); } } /* 应用动画的类 */ .danmaku-combo-animation::before { animation: danmaku-combo-bounce 0.2s ease-out; } `), ExGuard_pollFor(".danmu-fbb2a3", function() { startRepeatedDanmakuCleanupTimer(), repeatedDanmakuDomHook = new DomHook(".danmu-fbb2a3", false, m => { try { if (!m || m.length <= 0) return; if (!isRemoveRepeatedDanmaku) return; if (m[0].addedNodes.length <= 0 && m[0].removedNodes.length > 0) { const removedDom = m[0].removedNodes[0]; const removedComment = removedDom && removedDom.comment; if (!removedComment) return; const uuid = removedComment.uuid; const endTime = removedComment.startTime + removedComment.duration; const now = Date.now(); if (now > endTime) return; repeatedDanmakuUuidMap[uuid] = now + 1e3 * repeatedDanmakuSeconds; const danmakuText = RemoveRepeatedDanmaku_getKey(removedDom); if (danmakuText && repeatedDanmakuDomMap[danmakuText] === removedDom) delete repeatedDanmakuDomMap[danmakuText], delete repeatedDanmakuCountMap[danmakuText], delete repeatedDanmakuMap[danmakuText]; return; } if (m[0].addedNodes.length <= 0) return; const dom = m[0].addedNodes[0]; if (!dom || !dom.comment) return; const now = Date.now(); const uuid = dom.comment.uuid; const uuidExpireTime = repeatedDanmakuUuidMap[uuid]; if (uuidExpireTime && now <= uuidExpireTime) return; const danmakuText = RemoveRepeatedDanmaku_getKey(dom); if (!danmakuText || 0 === danmakuText.length) return; const expireTime = repeatedDanmakuMap[danmakuText]; if (expireTime && now <= expireTime) { if (dom.className += " repeated-danmaku", repeatedDanmakuCountMap[danmakuText] = (repeatedDanmakuCountMap[danmakuText] || 1) + 1, isEnlargeDanmaku) { const firstDom = repeatedDanmakuDomMap[danmakuText]; if (firstDom && firstDom.parentNode) requestAnimationFrame(() => { if (!firstDom.parentNode) return; if (!repeatedDanmakuOriginalFontSizeMap.has(firstDom)) { const computedStyle = window.getComputedStyle(firstDom); repeatedDanmakuOriginalFontSizeMap.set(firstDom, computedStyle.fontSize); } const originalFontSize = repeatedDanmakuOriginalFontSizeMap.get(firstDom); const baseFontSize = parseFloat(originalFontSize) || 20; const repeatCount = repeatedDanmakuCountMap[danmakuText]; const newFontSize = Math.min(baseFontSize + 2 * (repeatCount - 1), 40); firstDom.style.fontSize = newFontSize + "px", firstDom.setAttribute("data-repeat-count", repeatCount), firstDom.classList.remove("danmaku-combo-animation"), requestAnimationFrame(() => { if (!firstDom.parentNode) return; firstDom.classList.add("danmaku-combo-animation"); }); }); else if (!firstDom || !firstDom.parentNode) delete repeatedDanmakuDomMap[danmakuText], delete repeatedDanmakuCountMap[danmakuText]; } } else repeatedDanmakuMap[danmakuText] = now + 1e3 * repeatedDanmakuSeconds, repeatedDanmakuDomMap[danmakuText] = dom, repeatedDanmakuCountMap[danmakuText] = 1; } catch (e) { ExGuard_warn("屏蔽重复弹幕", "处理弹幕节点失败(多半是页面结构变化),跳过这一条", e); } }); }, Object.assign({ tag: "屏蔽重复弹幕" }, RemoveRepeatedDanmaku_pollOpts)); } function cleanupExpiredRepeatedDanmaku() { const now = Date.now(); for (var [ key, expireTime ] of Object.entries(repeatedDanmakuMap)) if (expireTime <= now) delete repeatedDanmakuMap[key], delete repeatedDanmakuDomMap[key], delete repeatedDanmakuCountMap[key]; for (const [ uuid, expireTime ] of Object.entries(repeatedDanmakuUuidMap)) if (expireTime <= now) delete repeatedDanmakuUuidMap[uuid]; } function startRepeatedDanmakuCleanupTimer() { if (repeatedDanmakuCleanupTimer) return; repeatedDanmakuCleanupTimer = setInterval(cleanupExpiredRepeatedDanmaku, 2e4); } function stopRepeatedDanmakuCleanupTimer() { if (repeatedDanmakuCleanupTimer) clearInterval(repeatedDanmakuCleanupTimer), repeatedDanmakuCleanupTimer = null; repeatedDanmakuMap = {}, repeatedDanmakuUuidMap = {}, repeatedDanmakuDomMap = {}, repeatedDanmakuCountMap = {}; } function saveRemoveRepeatedDanmaku() { setLocalIsRemoveRepeatedDanmaku(isRemoveRepeatedDanmaku); } function getLocalIsRemoveRepeatedDanmaku() { return "1" === localStorage.getItem("ExSave_isRemoveRepeatedDanmaku"); } function setLocalIsRemoveRepeatedDanmaku(value) { localStorage.setItem("ExSave_isRemoveRepeatedDanmaku", value ? "1" : "0"); } function getLocalRepeatedDanmakuSeconds() { const saved = localStorage.getItem("ExSave_repeatedDanmakuSeconds"); if (saved) { const value = parseInt(saved); if (!isNaN(value) && value >= 1 && value <= 300) return value; } return 5; } function setLocalRepeatedDanmakuSeconds(value) { localStorage.setItem("ExSave_repeatedDanmakuSeconds", value.toString()); } function getLocalIsEnlargeDanmaku() { const saved = localStorage.getItem("ExSave_isEnlargeDanmaku"); if (null === saved) return false; return "1" === saved; } function setLocalIsEnlargeDanmaku(value) { localStorage.setItem("ExSave_isEnlargeDanmaku", value ? "1" : "0"); } function initPkg_Shield() { Shield_applyRemoveEnterWhenReady(); } function replacePlayerBarrage(text) { if (-1 === text.indexOf("player_barrage")) return text; return text.replace(/player_barrage\\":0/g, 'player_barrage\\":1').replace(/"player_barrage":0/g, '"player_barrage":1'); } function initPkg_ShowDanmaku_ScriptHook() { scriptHook({ inline: true, callback: replacePlayerBarrage }); } function initPkg_ShowDanmaku() { responseHook((url, text) => { if (-1 !== url.indexOf("/betard")) return replacePlayerBarrage(text); return text; }); } function initPkg_Sign() { if (initPkg_Sign_Dom(), initPkg_Sign_Func(), "function" === typeof Sign_WatchPoints_boot) Sign_WatchPoints_boot(); } function initPkg_Sign_Dom() { Sign_insertIcon(); } function Sign_insertIcon() { let a = document.createElement("div"); a.className = "ex-sign", a.innerHTML = ''; let b = document.getElementsByClassName("ex-panel__wrap")[0]; if (b) b.insertBefore(a, b.childNodes[0]); } function initPkg_Sign_Func() { let btn = document.getElementsByClassName("ex-sign")[0]; if (btn) btn.addEventListener("click", function(e) { e.stopPropagation(); let panel = document.querySelector(".sign-panel"); if (!panel) createSignPanel(), panel = document.querySelector(".sign-panel"); if (panel) { let isShowing = "flex" === panel.style.display || "block" === panel.style.display; if (isShowing) { if (panel.style.removeProperty("display"), panel.style.setProperty("display", "none", "important"), panel.classList.remove("miuix-modal-in"), "function" === typeof updateDockActiveIndicator) updateDockActiveIndicator(); } else if ("function" === typeof openMiuixPanelCentered) openMiuixPanelCentered(panel, btn); else panel.style.setProperty("display", "flex", "important"); } }); } function createSignPanel() { var oldPanel = document.querySelector(".sign-panel"); if (oldPanel) return; oldPanel = document.createElement("div"), oldPanel.className = "sign-panel miuix-modal"; var signRows = [ { key: "client", title: "客户端模拟签到", desc: "模拟手机客户端领取每日免费礼盒" }, { key: "yuba", title: "关注鱼吧签到", desc: "关注的所有鱼吧一键打卡领经验" }, { key: "stardiscover", title: "星推日常任务", desc: "打卡、口令弹幕、关注任务(自动安全取关)" }, { key: "fanshome", title: "粉丝家园与日常", desc: "粉丝家园打卡与钻粉日常领奖" }, { key: "points", title: "活动中心签到领积分", desc: "每日签到并领取签到礼包(需登录)" }, { key: "watchpoints", title: "看直播领积分(自动)", desc: "本页观看时长达标后自动领取,仅页面可见时计时" } ]; if (signRows = signRows.map(function(r) { return ExUI.row({ title: r.title, desc: r.desc, trail: '', legacy: ExUI.lg("row") }); }).join(""), oldPanel.innerHTML = ExUI.card({ title: "签到选项", asideHtml: '按需勾选日常任务', body: signRows, legacy: ExUI.lg("card", { body: "sign-options-list" }) }) + ExUI.card({ title: "执行状态", asideHtml: '就绪', body: "勾选上方选项后,点击下方按钮开始签到。", bodyId: "sign-log-box", legacy: ExUI.lg("card", { body: "sign-log-box" }) }) + ExUI.stack({ html: ExUI.btn("开始签到", { variant: "primary", id: "sign-panel-start-btn", legacy: ExUI.LG.submitBtn }), cls: "ui-actions" }), document.body.appendChild(oldPanel), "function" === typeof ensureMiuixPanelHeader) ensureMiuixPanelHeader(oldPanel, "一键签到"); if ("function" === typeof ExUI_stickActionBar) ExUI_stickActionBar(oldPanel); signRows = { client: true, yuba: true, fanshome: true, stardiscover: true, points: true, watchpoints: false }; var currentCfg = signRows; try { var saved = JSON.parse(localStorage.getItem("ExSave_SignConfig")); if (saved && "object" === typeof saved) currentCfg = Object.assign({}, signRows, saved); } catch (e) {} if (signRows = oldPanel.querySelectorAll(".sign-checkbox"), signRows.forEach(function(cb) { var key = cb.getAttribute("data-key"); if (key && "undefined" !== typeof currentCfg[key]) cb.checked = !!currentCfg[key]; cb.addEventListener("change", function() { currentCfg[key] = this.checked; try { localStorage.setItem("ExSave_SignConfig", JSON.stringify(currentCfg)); } catch (e) {} if ("watchpoints" === key && "function" === typeof Sign_WatchPoints_setEnabled) Sign_WatchPoints_setEnabled(this.checked); }); }), saved = oldPanel.querySelector("#sign-panel-start-btn"), saved) saved.addEventListener("click", async function(e) { e.stopPropagation(), e = document.getElementById("sign-panel-start-btn"); var logBox = document.getElementById("sign-log-box"); var statusTag = document.getElementById("sign-status-tag"); if (e) e.disabled = true; if (statusTag) statusTag.textContent = "正在执行", statusTag.style.color = "#ff7700"; if (logBox) logBox.innerHTML = "正在启动已选签到任务...
    "; var logs = []; function appendLog(msg, isSuccess) { if (logs.push(msg), logBox) logBox.innerHTML = logs.map(function(l) { return "• " + l; }).join("
    "), logBox.scrollTop = logBox.scrollHeight; if ("function" === typeof showMessage) showMessage(msg, isSuccess ? "success" : "info"); } try { var summary = await executeSignEngine(currentCfg, appendLog); var failedCount = summary && summary.fail ? summary.fail : 0; if (statusTag) statusTag.textContent = failedCount > 0 ? "部分失败" : "已完成", statusTag.style.color = failedCount > 0 ? "#ef4444" : "#10b981"; } catch (err) { if (appendLog("签到执行异常: " + (err.message || "未知错误"), false), statusTag) statusTag.textContent = "异常中止", statusTag.style.color = "#ef4444"; } finally { if (e) e.disabled = false; } }); } async function executeSignEngine(options, onLog) { if ("function" !== typeof onLog) onLog = function(msg, isSuccess) { showMessage(msg, isSuccess ? "success" : "info"); }; options = options || { client: true, yuba: true, fanshome: true, stardiscover: true, points: true, watchpoints: false }, onLog("正在准备签到环境...", true); var results = []; function pushResult(name, ok, fail, skipped) { results.push({ name: name, ok: Number(ok) || 0, fail: Number(fail) || 0, skipped: Number(skipped) || 0 }); } if (options.yuba) try { onLog("正在执行【鱼吧签到】..."); var yubaRes = "function" === typeof signYubaList ? await signYubaList() : null; if (!yubaRes) pushResult("鱼吧签到", 0, 1), onLog("【鱼吧签到】模块未加载,已跳过", false); else if (yubaRes.listFailed) pushResult("鱼吧签到", 0, 1); else pushResult("鱼吧签到", yubaRes.signed, yubaRes.failed), onLog("【鱼吧签到】成功 " + yubaRes.signed + " 个,已签到 " + yubaRes.already + " 个" + (yubaRes.failed > 0 ? ",失败 " + yubaRes.failed + " 个" : ""), 0 === yubaRes.failed); } catch (err) { pushResult("鱼吧签到", 0, 1), onLog("【鱼吧签到】异常: " + (err.message || "未知错误"), false); } if (options.client) try { onLog("正在执行【客户端模拟签到】..."); var signRes = await ExGuard_withTimeout(ExGuard_xhr({ method: "POST", url: "https://apiv2.douyucdn.cn/h5nc/sign/sendSign", data: "token=" + ("undefined" !== typeof dyToken ? dyToken : getToken()), responseType: "json", headers: { "Content-Type": "application/x-www-form-urlencoded" }, tag: "客户端签到" }, 15e3), 16e3, "客户端签到"); if (!signRes) pushResult("客户端签到", 0, 1), onLog("【客户端签到】请求失败或超时(已跳过,不影响其它签到项)", false); else if (signRes.data && signRes.data.sign_pl && signRes.data.sign_pl.length > 0) { var items = signRes.data.sign_pl.map(function(it) { return it.cnt + "个" + it.name; }).join(", "); pushResult("客户端签到", 1, 0), onLog("【客户端签到】获得: " + items, true); } else if (signRes.data && 0 === signRes.data.length) pushResult("客户端签到", 1, 0), onLog("【客户端签到】今日已签到", true); else pushResult("客户端签到", 1, 0), onLog("【客户端签到】打卡成功", true); } catch (err) { pushResult("客户端签到", 0, 1), onLog("【客户端签到】执行异常", false); } if (options.stardiscover) try { if ("function" === typeof executeStarDiscoverSign) { var sdRes = await executeStarDiscoverSign(onLog); pushResult("星推任务", sdRes && sdRes.ok, sdRes && sdRes.fail); } else pushResult("星推任务", 0, 1), onLog("【星推任务】模块未加载,已跳过", false); } catch (err) { pushResult("星推任务", 0, 1), onLog("【星推任务】执行异常: " + (err.message || "未知错误"), false); } if (options.fanshome) try { var fansOk = 0; var fansFail = 0; var fansSkip = 0; if ("function" === typeof initPkg_Sign_FansTree) { var treeRes = await initPkg_Sign_FansTree(); fansOk += treeRes && treeRes.ok || 0, fansFail += treeRes && treeRes.fail || 0, fansSkip += treeRes && treeRes.skipped || 0; } pushResult("粉丝家园", fansOk, fansFail, fansSkip), onLog("【粉丝家园】成功 " + fansOk + " 个" + (fansFail > 0 ? ",失败 " + fansFail + " 个" : ""), 0 === fansFail); } catch (err) { pushResult("粉丝家园", 0, 1), onLog("【粉丝家园】执行异常", false); } if (options.points) try { if ("function" === typeof executeSignPoints) { var pointsOk = await executeSignPoints(onLog); pushResult("积分签到", pointsOk ? 1 : 0, pointsOk ? 0 : 1); } else pushResult("积分签到", 0, 1), onLog("【积分签到】模块未加载,已跳过", false); } catch (err) { pushResult("积分签到", 0, 1), onLog("【积分签到】执行异常: " + (err.message || "未知错误"), false); } try { if ("function" === typeof Sign_WatchPoints_setEnabled) if (Sign_WatchPoints_setEnabled(!!options.watchpoints), options.watchpoints) { if (onLog("【看播积分】自动领取已启动(仅页面可见时轮询)", true), "function" === typeof Sign_WatchPoints_checkNow) Sign_WatchPoints_checkNow(); } else onLog("【看播积分】未勾选,本次不启用", false); } catch (err) { onLog("【看播积分】启动异常: " + (err.message || "未知错误"), false); } var totalOk = 0; var totalFail = 0; var totalSkip = 0; for (var ri = 0; ri < results.length; ri++) totalOk += results[ri].ok, totalFail += results[ri].fail, totalSkip += results[ri].skipped || 0; if (yubaRes = results.filter(function(x) { return x.fail > 0; }).map(function(x) { return x.name; }), items = totalSkip > 0 ? ",跳过 " + totalSkip + " 项(已签到/已下线/非粉丝)" : "", totalFail > 0) onLog("签到结束:成功 " + totalOk + " 项,失败 " + totalFail + " 项(" + yubaRes.join("、") + ")" + items, false); else if (totalSkip > 0) onLog("签到结束:成功 " + totalOk + " 项" + items + ",没有失败项", true); else onLog("所有已选签到任务执行完毕!", true); return { ok: totalOk, fail: totalFail, skipped: totalSkip, results: results }; } async function initPkg_Sign_FansTree() { if (!Array.isArray(myFansBadgeList) || !myFansBadgeList.length) return ExGuard_warn("粉丝家园", "没有可签到的粉丝牌房间,本次跳过"), { ok: 0, fail: 0 }; let ok = 0; let fail = 0; let skipped = 0; for (let i = 0; i < myFansBadgeList.length; i++) { const roomId = myFansBadgeList[i]; try { const ret = await signRoomTree(roomId); if (ret && 0 === Number(ret.error)) ok++, showMessage(`【粉丝家园】${roomId}签到成功!`, "success"); else if (ret && 1006 === Number(ret.error)) skipped++, ExGuard_warn("粉丝家园", roomId + ` 非房间粉丝,跳过(1006)`); else fail++, showMessage(`【粉丝家园】` + roomId + (ret && ret.msg || "签到失败"), "error"); } catch (err) { fail++, ExGuard_warn("粉丝家园", roomId + ` 签到异常,继续下一个`, err); } } if (fail > 0) showMessage(`【粉丝家园】完成 ${ok} 个,失败 ${fail} 个` + (skipped > 0 ? `,跳过 ${skipped} 个` : ""), "warning"); return { ok: ok, fail: fail, skipped: skipped }; } function signRoomTree(rid) { return new Promise((resolve, reject) => { fetch("https://www.douyu.com/japi/interactnc/web/fanshome/sign", { method: "POST", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `ctn=${getCCN()}&rid=` + rid }).then(res => res.json()).then(ret => { resolve(ret); }).catch(err => { console.log("请求失败!", err), reject(err); }); }); } window.createSignPanel = createSignPanel; let signPointsConfigCache = { at: 0, data: null }; const SIGN_POINTS_CONFIG_TTL = 5 * 60 * 1e3; const SIGN_POINTS_BENIGN_CODES = [ 31202, 31200, 31e3, 31203, 31204 ]; function Sign_Points_fetchConfig(force) { const now = Date.now(); if (!force && signPointsConfigCache.data && now - signPointsConfigCache.at < SIGN_POINTS_CONFIG_TTL) return Promise.resolve(signPointsConfigCache.data); return fetch("https://www.douyu.com/wgapi/activity/gametask/config", { method: "GET", credentials: "include" }).then(function(res) { return res.json(); }).then(function(json) { const data = json && json.data || null; if (data && data.signAlias) signPointsConfigCache = { at: Date.now(), data: data }; return data; }).catch(function() { return null; }); } function Sign_Points_inPeriod(cfg) { if (!cfg) return { ok: false, why: "拿不到活动配置" }; const now = Math.floor(Date.now() / 1e3); if (cfg.startTime && now < Number(cfg.startTime)) return { ok: false, why: "活动尚未开始" }; if (cfg.endTime && now > Number(cfg.endTime)) return { ok: false, why: "活动已结束" }; return { ok: true }; } function Sign_Points_isLoginError(r) { if (!r) return false; if (300 === Number(r.error) || 1006900016 === Number(r.error)) return true; return /登录/.test(String(r.msg || r.message || "")); } function Sign_Points_isVerifyError(r) { if (!r) return false; return /验证|极验|滑块/.test(String(r.msg || r.message || r.data || "")); } function Sign_Points_apiError(r, fallback) { if (!r) return fallback || "请求失败"; if (Sign_Points_isLoginError(r)) return "未登录(请先在斗鱼登录后再试)"; if (Sign_Points_isVerifyError(r)) return "需要滑块验证,请到「任务中心」手动签到一次"; if (r.msg) return r.msg; if (r.message) return r.message; return fallback || "错误码 " + r.error; } function Sign_Points_cookie(name) { try { const m = String(document.cookie || "").match(new RegExp("(?:^|; )" + name + "=([^;]*)")); return m ? decodeURIComponent(m[1]) : null; } catch (e) { return null; } } function Sign_Points_ctn() { try { if ("function" === typeof getCCN) return getCCN(); } catch (e) {} return Sign_Points_cookie("acf_ccn") || "1"; } function Sign_Points_ensureCsrf() { const got = Sign_Points_cookie("cvl_csrf_token"); if (got) return Promise.resolve(got); return fetch("https://www.douyu.com/japi/carnival/nc/common/generateCsrf", { method: "POST", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: "" }).then(function() { return Sign_Points_cookie("cvl_csrf_token"); }).catch(function() { return null; }); } function Sign_Points_rawPost(url, parts) { return fetch(url, { method: "POST", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: parts.join("&") }).then(function(res) { return res.json(); }).catch(function() { return null; }); } function Sign_Points_postForm(url, pairs) { const keys = Object.keys(pairs); const build = function(tokenName, tokenValue) { const parts = []; if (tokenName && tokenValue) parts.push(encodeURIComponent(tokenName) + "=" + encodeURIComponent(tokenValue)); for (let i = 0; i < keys.length; i++) parts.push(encodeURIComponent(keys[i]) + "=" + encodeURIComponent(pairs[keys[i]])); return parts; }; if (String(url).indexOf("carnivalApi") >= 0) return Sign_Points_ensureCsrf().then(function(token) { return Sign_Points_rawPost(url, build("csrfToken", token)); }); return Sign_Points_rawPost(url, build("ctn", Sign_Points_ctn())); } function Sign_Points_getStatus(signAlias) { return fetch("https://www.douyu.com/japi/carnivalApi/nc/sign/getStatus?signAlias=" + encodeURIComponent(signAlias), { method: "GET", credentials: "include" }).then(function(res) { return res.json(); }).catch(function() { return null; }); } function Sign_Points_doSign(signAlias) { return Sign_Points_postForm("https://www.douyu.com/japi/carnivalApi/sign/doSign", { signAlias: signAlias, useJiYan: "true" }); } function Sign_Points_takeBag(signAlias, offset) { return Sign_Points_postForm("https://www.douyu.com/japi/carnivalApi/sign/takeBag", { signAlias: signAlias, offset: String(offset), useJiYan: "true" }); } function Sign_Points_prizeText(cfg, offset) { try { const table = cfg && cfg.signPrize; if (!table) return ""; const day = table[String((Number(offset) || 0) + 1)] || null; if (!day || !day.length) return ""; return day.map(function(it) { return (it.name || "奖励") + "x" + (it.num || 1); }).join("、"); } catch (e) { return ""; } } async function executeSignPoints(onLog) { const log = function(m, ok) { if ("function" === typeof onLog) onLog(m, !!ok); else if ("function" === typeof showMessage) showMessage(m, ok ? "success" : "info"); }; const cfg = await Sign_Points_fetchConfig(true); if (!cfg || !cfg.signAlias) return log("【积分签到】拿不到活动配置(可能活动已下线),已跳过", false), false; const period = Sign_Points_inPeriod(cfg); if (!period.ok) return log("【积分签到】" + period.why + ",已跳过", false), false; const signAlias = cfg.signAlias; const st0 = await Sign_Points_getStatus(signAlias); if (!st0 || 0 !== Number(st0.error)) return log("【积分签到】状态查询失败:" + Sign_Points_apiError(st0, "接口无响应"), false), false; let d = st0.data || {}; if (0 !== Number(d.todaySigned)) log("【积分签到】今日已签到(已连签 " + (d.signDays || "?") + " 天)", true); else { const r = await Sign_Points_doSign(signAlias); const st1 = await Sign_Points_getStatus(signAlias); const d1 = st1 && 0 === Number(st1.error) && st1.data ? st1.data : null; if (d1 && 0 !== Number(d1.todaySigned)) { d = d1; const prize = Sign_Points_prizeText(cfg, d1.todayOffset); log("【积分签到】签到成功(已连签 " + (d1.signDays || "?") + " 天)" + (prize ? ",获得 " + prize : ""), true); } else return log("【积分签到】签到失败:" + Sign_Points_apiError(r, "状态回读仍显示今日未签到"), false), false; } if (void 0 === d.todayOffset || null === d.todayOffset) return log("【积分签到】未取到今日偏移,跳过礼包领取", false), true; const bag = await Sign_Points_takeBag(signAlias, d.todayOffset); if (bag && 0 === Number(bag.error)) log("【积分签到】签到礼包已领取", true); else if (bag && SIGN_POINTS_BENIGN_CODES.indexOf(Number(bag.error)) >= 0) log("【积分签到】今日没有新的签到礼包", true); else log("【积分签到】礼包领取失败:" + Sign_Points_apiError(bag, "接口无响应"), false); return true; } window.executeSignPoints = executeSignPoints; const STAR_PENDING_KEY = "ExSave_StarFollowPending"; const STAR_UNFOLLOW_ATTEMPTS = 3; const STAR_UNFOLLOW_GAP_MS = 400; function StarDiscover_pendingRead() { try { const raw = JSON.parse(localStorage.getItem(STAR_PENDING_KEY) || "[]"); return Array.isArray(raw) ? raw.map(String) : []; } catch (e) { return []; } } function StarDiscover_pendingWrite(list) { try { localStorage.setItem(STAR_PENDING_KEY, JSON.stringify(list)); } catch (e) { ExGuard_warn("星推关注", "待取关清单写入失败(下次无法自动补偿)", e); } } function StarDiscover_pendingAdd(rid) { const key = String(rid); const list = StarDiscover_pendingRead(); if (-1 === list.indexOf(key)) list.push(key), StarDiscover_pendingWrite(list); } function StarDiscover_pendingRemove(rid) { const key = String(rid); const list = StarDiscover_pendingRead().filter(function(x) { return x !== key; }); StarDiscover_pendingWrite(list); } async function executeStarDiscoverSign(onLog) { if ("function" !== typeof onLog) onLog = function(msg, ok) { showMessage(msg, ok ? "success" : "info"); }; const stats = { ok: 0, fail: 0 }; function report(msg, ok) { if (ok) stats.ok++; else stats.fail++; onLog(msg, !!ok); } const ownedSet = Object.create(null); onLog("正在获取星推榜单与任务配置..."); let curNumericRid = ""; try { const rCandidate = String(window.room_id || window.$ROOM && window.$ROOM.room_id || ("undefined" !== typeof rid ? rid : "") || ""); if (/^\d+$/.test(rCandidate)) curNumericRid = rCandidate; else { const mPath = window.location.pathname.match(/\/(\d+)/); if (mPath && mPath[1]) curNumericRid = mPath[1]; } } catch (e) {} const queryRid = curNumericRid || "9999"; let realCtn = await getDouyuCtn(); let csrf = ""; try { const mCsrf = document.cookie.match(/(^| )post-csrfToken=([^;]*)(;|$)/); if (csrf = mCsrf ? unescape(mCsrf[2]) : "", !csrf) csrf = Math.random().toString(36).substr(2), document.cookie = "post-csrfToken=" + escape(csrf) + ";path=/"; } catch (e) {} function postStarReport(r, type) { return fetch("https://www.douyu.com/japi/livebiznc/web/anchorstardiscover/user/task/report", { method: "POST", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded", "dy-csrf-token": csrf }, body: "ctn=" + encodeURIComponent(realCtn || "1") + "&type=" + type + "&rid=" + encodeURIComponent(r) }).then(res => res.json()).catch(() => null); } async function followAnchorApi(r, token) { return fetch("/wgapi/livenc/liveweb/follow/add", { method: "POST", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: "rid=" + encodeURIComponent(r) + "&ctn=" + encodeURIComponent(token || "") }).then(res => res.json()).catch(e => ({ error: -1, msg: e.message })); } async function unfollowAnchorApi(r, token) { return fetch("/wgapi/livenc/liveweb/follow/rm", { method: "POST", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: "rid=" + encodeURIComponent(r) + "&ctn=" + encodeURIComponent(token || "") }).then(res => res.json()).catch(e => ({ error: -1, msg: e.message })); } async function fetchStarTaskList() { try { const res = await fetch("https://www.douyu.com/japi/livebiznc/web/anchorstardiscover/user/task/list?rid=" + queryRid, { credentials: "include" }).then(r => r.json()); if (res && res.data && Array.isArray(res.data.taskList)) return res.data.taskList; } catch (e) { ExGuard_warn("星推任务", "任务列表拉取失败", e); } return []; } async function tryUnfollowOwned(targetRid) { const key = String(targetRid); if (!ownedSet[key]) return false; let token = realCtn; for (let attempt = 0; attempt < STAR_UNFOLLOW_ATTEMPTS; attempt++) { const rmRes = await unfollowAnchorApi(key, token); if (rmRes && (0 === Number(rmRes.error) || 0 === Number(rmRes.code))) return delete ownedSet[key], StarDiscover_pendingRemove(key), true; await sleep(STAR_UNFOLLOW_GAP_MS), token = await getDouyuCtn(); } return ExGuard_warn("星推关注", "主播 " + key + " 取关未成功,已记入待办(下次打开直播间自动补偿)"), false; } const leftover = StarDiscover_pendingRead(); if (leftover.length) { leftover.forEach(function(x) { ownedSet[String(x)] = true; }); let cleaned = 0; for (let i = 0; i < leftover.length; i++) if (await tryUnfollowOwned(leftover[i])) cleaned++; report("【星推关注】已清理上次未取关的 " + cleaned + "/" + leftover.length + " 个主播", cleaned === leftover.length); } const report6 = await postStarReport(queryRid, 6); if (report6 && 0 === Number(report6.error)) report("【星推任务】已完成每日活动页打卡 (+10金币)", true); else report("【星推任务】每日活动页打卡未成功(" + (report6 && (report6.msg || report6.error) || "接口无响应") + ")", false); const rankUrl = "https://www.douyu.com/japi/livebiznc/web/anchorstardiscover/rank/info?rid=" + queryRid + "&type=5&track=3"; const rankRes = await fetch(rankUrl, { method: "GET", credentials: "include" }).then(r => r.json()).catch(() => null); const rankList = rankRes && rankRes.data && Array.isArray(rankRes.data.rankItemList) ? rankRes.data.rankItemList : []; const starRids = rankList.map(item => String(item.rid || item.rId || "")).filter(Boolean); if (!starRids.length) onLog("【星推任务】榜单为空(活动可能未开或接口异常),后续依赖榜单的任务将自动跳过"); const signRids = starRids.slice(0, 3); let sign5ok = 0; let sign5fail = 0; for (let si = 0; si < signRids.length; si++) { const srid = signRids[si]; const r5 = await postStarReport(srid, 5); if (r5 && 0 === Number(r5.error)) sign5ok++; else sign5fail++; await sleep(200); } if (signRids.length > 0) if (sign5fail > 0) report("【星推签到】" + sign5ok + " 个成功、" + sign5fail + " 个失败(共 " + signRids.length + " 个星推直播间)", false); else report("【星推签到】完成 " + signRids.length + " 个星推直播间打卡 (+9金币)", true); let isStarCompetitionRoom = false; if (curNumericRid && rankRes && rankRes.data && rankRes.data.memberInfo) { const mInfo = rankRes.data.memberInfo; if (0 === mInfo.hide && Number(mInfo.rank) > 0 && String(mInfo.rid) === String(curNumericRid)) isStarCompetitionRoom = true; } if (isStarCompetitionRoom) { const txtEl = document.querySelector("textarea.ChatSend-txt") || document.querySelector("div.ChatSend-txt"); const sendBtn = document.querySelector(".ChatSend-button"); if (!txtEl || !sendBtn) report("【星推弹幕】聊天区输入框/发送按钮不在,口令未发送(不影响其它任务)", false); else { const isDiv = "div" === txtEl.tagName.toLowerCase(); const origVal = isDiv ? txtEl.innerText : txtEl.value; try { if (isDiv) txtEl.innerText = "全民星推荐助力主播成长"; else txtEl.value = "全民星推荐助力主播成长"; txtEl.dispatchEvent(new Event("input", { bubbles: true })), sendBtn.click(), await sleep(400), report("【星推弹幕】当前为星推参赛直播间,已自动发送指定助力口令 (+5金币)", true); } catch (e) { report("【星推弹幕】口令弹幕发送异常(" + (e.message || "未知错误") + ")", false); } finally { if (isDiv) txtEl.innerText = origVal || ""; else txtEl.value = origVal || ""; txtEl.dispatchEvent(new Event("input", { bubbles: true })); } } } else onLog("【星推弹幕】当前房间非指定星推参赛直播间,已安全跳过口令发送(避免打扰主播)", true); try { const currentTaskList = await fetchStarTaskList(); const task4Info = currentTaskList.find(t => 4 === Number(t.type)); let completedCount = task4Info ? Number(task4Info.curCompleteNum || 0) : 0; const targetCount = 5; if (completedCount >= targetCount) onLog("【星推关注】今日关注任务已全部达成 (" + completedCount + "/5)", true); else { const needed = targetCount - completedCount; onLog("正在执行【星推关注任务】(当前进度 " + completedCount + "/5,需完成 " + needed + " 位)..."), realCtn = await getDouyuCtn(); const existingFollowSet = new Set(); try { const followRes = await fetch("https://www.douyu.com/wgapi/livenc/liveweb/follow/list?page=1428", { credentials: "include" }).then(r => r.json()).catch(() => null); if (followRes && followRes.data && Array.isArray(followRes.data.list)) followRes.data.list.forEach(item => { if (item.room_id) existingFollowSet.add(String(item.room_id)); }); } catch (e) { ExGuard_warn("星推关注", "既有关注列表拉取失败,本次按空白名单处理(宁可少关注)", e); } const processedSet = new Set(); if (curNumericRid) processedSet.add(curNumericRid); const candidateQueue = []; starRids.forEach(r => { if (r && !existingFollowSet.has(r) && !processedSet.has(r)) candidateQueue.push(r); }); let sourceIndex = 0; const maxAttempts = 25; let attemptCount = 0; async function pullMoreCandidates() { const srcRid = sourceIndex < starRids.length ? starRids[sourceIndex] : queryRid; sourceIndex++; try { const introRes = await fetch("https://www.douyu.com/japi/livebiznc/web/anchorstardiscover/user/task/follow/introduce?rid=" + srcRid, { credentials: "include" }).then(r => r.json()).catch(() => null); if (introRes && introRes.data && Array.isArray(introRes.data.list)) introRes.data.list.forEach(item => { const candidateRid = String(item.rid || item.rId || ""); if (candidateRid && !existingFollowSet.has(candidateRid) && !processedSet.has(candidateRid) && !candidateQueue.includes(candidateRid)) candidateQueue.push(candidateRid); }); } catch (e) { ExGuard_warn("星推关注", "补候选主播失败(跳过这个源房间)", e); } } await pullMoreCandidates(); let followOk = 0; let followFail = 0; while (completedCount < targetCount && attemptCount < maxAttempts) { if (attemptCount++, 0 === candidateQueue.length) await pullMoreCandidates(); if (0 === candidateQueue.length) if (sourceIndex < starRids.length + 3) { if (await pullMoreCandidates(), 0 === candidateQueue.length) { await sleep(500); continue; } } else break; const targetRid = candidateQueue.shift(); if (!targetRid || processedSet.has(targetRid) || existingFollowSet.has(targetRid)) continue; processedSet.add(targetRid); try { realCtn = await getDouyuCtn(); const addRes = await followAnchorApi(targetRid, realCtn); if (addRes && (0 === Number(addRes.error) || 0 === Number(addRes.code))) { ownedSet[targetRid] = true, StarDiscover_pendingAdd(targetRid), await sleep(1800); const cleaned = await tryUnfollowOwned(targetRid); if (!cleaned) followFail++; const updatedList = await fetchStarTaskList(); const updatedT4 = updatedList.find(t => 4 === Number(t.type)); const newCompleted = updatedT4 ? Number(updatedT4.curCompleteNum || 0) : completedCount; if (newCompleted > completedCount) completedCount = newCompleted, followOk++, onLog("【星推关注】主播 " + targetRid + " 关注成功并已安全取关 (" + completedCount + "/5)", true); else onLog("【星推关注】主播 " + targetRid + " 已处理并安全取关 (当前进度 " + completedCount + "/5)", true); } else if (addRes && 1 === Number(addRes.error)) existingFollowSet.add(targetRid); else followFail++, ExGuard_warn("星推关注", "主播 " + targetRid + " 关注失败(" + (addRes && (addRes.msg || addRes.error) || "无响应") + ")"); } catch (err) { followFail++, ExGuard_warn("星推关注", "主播 " + targetRid + " 关注单步异常", err); } await sleep(800); } let residue = 0; const stillOwned = Object.keys(ownedSet); for (let ci = 0; ci < stillOwned.length; ci++) if (!await tryUnfollowOwned(stillOwned[ci])) residue++; if (residue > 0) report("【星推关注】仍有 " + residue + " 个主播取关未成功,已记入待办(下次打开直播间自动补偿)", false); else if (followFail > 0) report("【星推关注】当前进度 " + completedCount + "/5,其中 " + followFail + " 次关注未成功(可稍后重试)", false); else report("【星推关注】已达成 " + completedCount + "/5,本次新增关注均已确认取关", true); } } catch (e) { report("【星推关注】任务异常: " + (e.message || "未知错误"), false); } let actOk = 0; let actFail = 0; for (let si2 = 0; si2 < Math.min(3, starRids.length); si2++) { const r1 = await postStarReport(starRids[si2], 1); if (r1 && 0 === Number(r1.error)) actOk++; else actFail++; await sleep(150); } if (actFail > 0) ExGuard_warn("星推任务", "互动上报 " + actOk + " 个成功、" + actFail + " 个失败"); const allOk = 0 === stats.fail; return onLog("【星推任务】" + (allOk ? "星推日常任务已全部执行完毕!" : "星推日常任务执行结束,其中 " + stats.fail + " 项未成功(详见上面的提示)"), allOk), { ok: stats.ok, fail: stats.fail }; } const WATCH_POINTS_POLL_MS = 3 * 60 * 1e3; const WATCH_POINTS_REARM_MS = 5 * 60 * 1e3; const WATCH_POINTS_BOOT_DELAY_MS = 5e3; const WATCH_POINTS_MAX_CLAIMS = 10; const WATCH_POINTS_CLAIM_COOLDOWN_MS = 20 * 1e3; const WATCH_POINTS_STATUS = { UNWATCHED: 1, WATCHED: 2, CLAIMED: 3 }; let watchPointsState = { date: "", enabled: false, actAlias: "", tiers: [], statusById: {}, startTime: 0, claims: 0, lastClaimAt: 0, loginBlocked: false, allDone: false, warned: false, timer: 0, rearm: 0, running: false }; function WatchPoints_today() { return "function" === typeof getLocalDateStr ? getLocalDateStr() : new Date().toDateString(); } function WatchPoints_log(msg, ok) { if ("function" === typeof showMessage) showMessage(msg, ok ? "success" : "info"); } function WatchPoints_resetDay() { watchPointsState.statusById = {}, watchPointsState.claims = 0, watchPointsState.lastClaimAt = 0, watchPointsState.loginBlocked = false, watchPointsState.allDone = false, watchPointsState.date = WatchPoints_today(); } function WatchPoints_visible() { try { return "string" !== typeof document.visibilityState || "visible" === document.visibilityState; } catch (e) { return true; } } function WatchPoints_allClaimed() { const tiers = watchPointsState.tiers; if (!tiers.length) return false; return tiers.every(function(t) { return Number(watchPointsState.statusById[t.id]) === WATCH_POINTS_STATUS.CLAIMED; }); } function WatchPoints_goIdle() { if (watchPointsState.allDone = true, watchPointsState.timer) clearInterval(watchPointsState.timer), watchPointsState.timer = 0; if (!watchPointsState.rearm) watchPointsState.rearm = setInterval(WatchPoints_rearmTick, WATCH_POINTS_REARM_MS); } function WatchPoints_rearmTick() { if (!watchPointsState.enabled) return; if (watchPointsState.date === WatchPoints_today()) return; if (WatchPoints_resetDay(), watchPointsState.rearm) clearInterval(watchPointsState.rearm), watchPointsState.rearm = 0; if (WatchPoints_loadConfig(), !watchPointsState.timer) watchPointsState.timer = setInterval(WatchPoints_poll, WATCH_POINTS_POLL_MS); WatchPoints_poll(); } function WatchPoints_loadConfig() { const fetcher = "function" === typeof Sign_Points_fetchConfig ? Sign_Points_fetchConfig : null; if (!fetcher) return Promise.resolve(null); return fetcher(false).then(function(cfg) { if (!cfg) return null; const period = "function" === typeof Sign_Points_inPeriod ? Sign_Points_inPeriod(cfg) : { ok: true }; if (!period.ok) { if (!watchPointsState.warned) watchPointsState.warned = true, WatchPoints_log("【看播积分】" + period.why + ",看播任务已停用", false); return null; } const tiers = (cfg.viewTask || []).map(function(v) { return { id: String(v.id), time: Number(v.time) || 0, num: Number(v.num) || 0 }; }).filter(function(t) { return t.time > 0; }); if (!tiers.length) return null; return watchPointsState.tiers = tiers, watchPointsState.actAlias = cfg.actAlias || "", tiers; }); } function WatchPoints_viewStatus() { if (!watchPointsState.actAlias) return Promise.resolve(null); if ("function" === typeof Sign_Points_postForm) return Sign_Points_postForm("https://www.douyu.com/wgapi/activitync/gametask/viewStatus", { actAlias: watchPointsState.actAlias }); return Promise.resolve(null); } function WatchPoints_takeGift(id) { if ("function" === typeof Sign_Points_postForm) return Sign_Points_postForm("https://www.douyu.com/wgapi/activitync/gametask/takeGift", { id: String(id), actAlias: watchPointsState.actAlias, startTime: String(watchPointsState.startTime) }); return Promise.resolve(null); } async function WatchPoints_poll() { if (!watchPointsState.enabled || !watchPointsState.running) return; if (watchPointsState.date !== WatchPoints_today()) return WatchPoints_rearmTick(), void 0; if (watchPointsState.allDone || watchPointsState.loginBlocked) return; if (!WatchPoints_visible()) return; if (!watchPointsState.tiers.length) if (await WatchPoints_loadConfig(), !watchPointsState.tiers.length) return; const st = await WatchPoints_viewStatus(); if ("function" === typeof Sign_Points_isLoginError && Sign_Points_isLoginError(st)) return watchPointsState.loginBlocked = true, WatchPoints_log("【看播积分】未登录,已停止自动领取(登录后刷新页面即可恢复)", false), void 0; const data = st && st.data || null; const list = data && data.taskList || null; if (!list) return; if (data.startTime) watchPointsState.startTime = data.startTime; let claimable = []; if (Object.keys(list).forEach(function(id) { if (watchPointsState.statusById[id] = Number(list[id].status), Number(list[id].status) === WATCH_POINTS_STATUS.WATCHED) claimable.push(id); }), !claimable.length) { if (WatchPoints_allClaimed()) WatchPoints_goIdle(); return; } let got = 0, failed = 0; for (let i = 0; i < claimable.length; i++) { if (watchPointsState.claims >= WATCH_POINTS_MAX_CLAIMS) { failed++; continue; } const wait = WATCH_POINTS_CLAIM_COOLDOWN_MS - (Date.now() - watchPointsState.lastClaimAt); if (wait > 0) { failed++; continue; } watchPointsState.claims++, watchPointsState.lastClaimAt = Date.now(); const r = await WatchPoints_takeGift(claimable[i]); if (r && 0 === Number(r.error)) got++; else if (failed++, "undefined" !== typeof console && console.warn) console.warn("[DouyuEx] 看播积分领取失败:", claimable[i], r && (r.msg || r.error)); } if (got) { const st2 = await WatchPoints_viewStatus(); const list2 = st2 && st2.data && st2.data.taskList || null; if (list2) Object.keys(list2).forEach(function(id) { watchPointsState.statusById[id] = Number(list2[id].status); }); WatchPoints_log("【看播积分】已领取 " + got + " 档积分", true); } if (WatchPoints_allClaimed()) WatchPoints_goIdle(); if (failed && !got && "undefined" !== typeof console && console.warn) console.warn("[DouyuEx] 看播积分本次没有领取成功,下一轮再试"); } function WatchPoints_start() { if (watchPointsState.running) return; watchPointsState.running = true, WatchPoints_resetDay(), WatchPoints_loadConfig(), watchPointsState.timer = setInterval(WatchPoints_poll, WATCH_POINTS_POLL_MS), setTimeout(function() { if (watchPointsState.enabled && watchPointsState.running) WatchPoints_poll(); }, WATCH_POINTS_BOOT_DELAY_MS); } function WatchPoints_stop() { if (!watchPointsState.running) return; watchPointsState.running = false, clearInterval(watchPointsState.timer), clearInterval(watchPointsState.rearm), watchPointsState.timer = 0, watchPointsState.rearm = 0; } function Sign_WatchPoints_setEnabled(on) { if (watchPointsState.enabled = !!on, watchPointsState.enabled) WatchPoints_start(); else WatchPoints_stop(); } function Sign_WatchPoints_checkNow() { return WatchPoints_poll(); } function Sign_WatchPoints_boot() { try { const saved = JSON.parse(localStorage.getItem("ExSave_SignConfig") || "{}") || {}; if (saved.watchpoints) Sign_WatchPoints_setEnabled(true); } catch (e) {} } window.Sign_WatchPoints_setEnabled = Sign_WatchPoints_setEnabled, window.Sign_WatchPoints_boot = Sign_WatchPoints_boot, window.Sign_WatchPoints_checkNow = Sign_WatchPoints_checkNow, window.Sign_WatchPoints_state = function() { return watchPointsState; }; const SIGN_YUBA_MAX_RETRIES = 10; const SIGN_YUBA_XHR_TIMEOUT = 15e3; const SIGN_YUBA_PAGE_LIMIT = 30; let signYubaRun = null; function Sign_Yuba_createRun(total) { const run = { total: total, done: 0, signed: 0, already: 0, failed: 0, finished: false, countMap: {} }; return run.finishedPromise = new Promise(function(resolve) { run.resolve = resolve; }), run; } function Sign_Yuba_summary(run, listFailed) { return { total: run ? run.total : 0, signed: run ? run.signed : 0, already: run ? run.already : 0, failed: listFailed ? 1 : run ? run.failed : 0, listFailed: !!listFailed }; } function Sign_Yuba_finishRun(run) { if (!run || run.finished || run.done < run.total) return; if (run.finished = true, signYubaRun === run) signYubaRun = null; let rest = run.total - run.signed; if (run.signed > 0) { let text = "【鱼吧】" + run.signed + "个鱼吧签到完成"; if (rest > 0) text += "," + rest + "个未签成功"; if (run.failed > 0) text += "(其中 " + run.failed + " 个连续失败已放弃)"; showMessage(text, "success"); } else if (run.failed > 0) showMessage("【鱼吧】" + run.total + "个鱼吧均未签成功(" + run.failed + " 个连续失败已放弃)", "error"); else showMessage("【鱼吧】" + run.total + "个鱼吧今日均已签到", "warning"); if ("function" === typeof run.resolve) run.resolve(Sign_Yuba_summary(run, false)); } function Sign_Yuba_markFailed(run, group_id, why, err) { if (!run || run.finished) return; run.failed++, run.done++, ExGuard_warn("鱼吧签到", "(" + group_id + ") " + why + ",本批次记为失败", err), Sign_Yuba_finishRun(run); } function signYubaFast() { GM_xmlhttpRequest({ method: "POST", url: "https://mapi-yuba.douyu.com/wb/v3/fastSign", responseType: "json", headers: { "Content-Type": "application/x-www-form-urlencoded", client: "android", token: dyToken }, onload: function(response) { if ("" == response.response.message && 0 != response.response.data); else if (0 == response.response.data); }, onerror: function(err) { ExGuard_warn("鱼吧签到", "极速签到请求失败", err); }, timeout: SIGN_YUBA_XHR_TIMEOUT }); } async function signYuba(group_id, t, run) { const state = run || signYubaRun; if (!state || state.finished) return; GM_xmlhttpRequest({ method: "POST", url: "https://yuba.douyu.com/ybapi/topic/sign", data: "group_id=" + group_id, responseType: "json", headers: { "Content-Type": "application/x-www-form-urlencoded", "dy-client": "pc", "dy-token": t, Referer: "https://yuba.douyu.com/group/" + group_id }, timeout: SIGN_YUBA_XHR_TIMEOUT, onload: async function(response) { if (state.finished) return; const body = response && response.response || {}; const message = null == body.message ? "" : String(body.message); const tries = state.countMap[group_id] || 0; if ("签到失败" == message) { if (state.countMap[group_id] = tries + 1, tries + 1 >= SIGN_YUBA_MAX_RETRIES) return state.failed++, state.done++, ExGuard_warn("鱼吧签到", "(" + group_id + ") 连续 " + SIGN_YUBA_MAX_RETRIES + " 次签到失败,已放弃"), Sign_Yuba_finishRun(state), void 0; return await sleep(2e3), signYuba(group_id, t, state), void 0; } if (state.countMap[group_id] = tries + 1, state.done++, "" == message) state.signed++; else state.already++; try { await signYubaSupplementary(group_id); } catch (e) { ExGuard_warn("鱼吧签到", "(" + group_id + ") 补充签到异常", e); } Sign_Yuba_finishRun(state); }, onerror: function(err) { Sign_Yuba_markFailed(state, group_id, "请求失败", err); }, ontimeout: function() { Sign_Yuba_markFailed(state, group_id, "请求超时"); } }); } async function signYubaSupplementary(group_id) { const result = await signSupplementary(group_id); const extra = result && result.data ? Number(result.data.supplementary_cards) || 0 : 0; for (let i = 0; i < extra; i++) await signSupplementary(group_id); return extra; } async function signYubaList() { let yubaList = []; const first = await getYubaPage(1); if (!first) return showMessage("【鱼吧】获取关注鱼吧列表失败(登录态失效或网络异常),本次跳过", "error"), Sign_Yuba_summary(null, true); yubaList = yubaList.concat(Array.isArray(first.list) ? first.list : []); const pageNum = Number(first.count_page) - 1; if (pageNum >= 1) for (let i = 0; i < pageNum; i++) { const curPage = 2 + i; const ret = await getYubaPage(curPage); if (!ret) { ExGuard_warn("鱼吧签到", "第 " + curPage + " 页鱼吧列表获取失败,已跳过该页"); continue; } yubaList = yubaList.concat(Array.isArray(ret.list) ? ret.list : []); } if (!yubaList.length) return showMessage("【鱼吧】没有可签到的鱼吧(需 7 级以上鱼吧关注)", "warning"), Sign_Yuba_summary(null, false); const run = Sign_Yuba_createRun(yubaList.length); signYubaRun = run, signYubaFast(); for (let i = 0; i < yubaList.length; i++) { const item = yubaList[i] || {}; signYuba(item.group_id, dyToken, run); } return run.finishedPromise; } function getYubaPage(page) { return ExGuard_xhr({ method: "GET", url: "https://yuba.douyu.com/wbapi/web/group/myFollow?page=" + String(page) + "&limit=" + SIGN_YUBA_PAGE_LIMIT, responseType: "json", headers: { "Content-Type": "application/x-www-form-urlencoded", "dy-client": "pc", "dy-token": dyToken }, tag: "鱼吧签到" }, SIGN_YUBA_XHR_TIMEOUT).then(function(ret) { return ret && ret.data ? ret.data : null; }); } function getSupplementaryNums(group_id) { return ExGuard_xhr({ method: "GET", url: "https://yuba.douyu.com/wbapi/web/signDetail?group_id=" + group_id, responseType: "json", headers: { "Content-Type": "application/x-www-form-urlencoded", "dy-client": "pc", "dy-token": dyToken }, tag: "鱼吧签到" }, SIGN_YUBA_XHR_TIMEOUT); } function signSupplementary(group_id) { return ExGuard_xhr({ method: "POST", url: "https://mapi-yuba.douyu.com/wb/v3/supplement", responseType: "json", headers: { "Content-Type": "application/x-www-form-urlencoded", client: "android", token: dyToken }, data: "group_id=" + group_id, tag: "鱼吧签到" }, SIGN_YUBA_XHR_TIMEOUT); } const TITLE_TIME_REFRESH_MS = 15e4; const TITLE_TIME_POLL_INTERVAL_MS = 500; const TITLE_TIME_POLL_MAX_TRIES = 20; const TITLE_TIME_WAIT_TAG = "标题栏观看时间"; let titleTimeState = { showinfo: false, isShow: 2, showtime: 0, watchOk: false, watchSeconds: 0 }; let titleTimeBooted = false; let titleTimeBusy = false; let titleTimeTicks = 0; let titleTimePoll = null; function initPkg_TitleTime() { if (TitleTime_insert()) return; TitleTime_waitAnchor(); } function TitleTime_findAnchor() { return document.getElementById("copy-real-live"); } function TitleTime_buildNode() { const node = document.createElement("div"); return node.className = "ex-title-time Title-blockInline", node.innerHTML = '观看时间 —' + '开播时间 —', node; } function TitleTime_insert() { const anchor = TitleTime_findAnchor(); const parent = anchor ? anchor.parentNode : null; if (!parent) return false; let node = document.querySelector(".ex-title-time"); if (!node) node = TitleTime_buildNode(); if (node.parentNode !== parent || anchor.nextElementSibling !== node) parent.insertBefore(node, anchor.nextSibling); return TitleTime_render(), TitleTime_boot(), true; } function TitleTime_waitAnchor() { if (titleTimePoll && !titleTimePoll.done) return true; const onFound = function() { titleTimePoll = null, TitleTime_insert(); }; if ("function" === typeof ExGuard_pollFor) return titleTimePoll = ExGuard_pollFor(TitleTime_findAnchor, onFound, { intervalMs: TITLE_TIME_POLL_INTERVAL_MS, maxTries: TITLE_TIME_POLL_MAX_TRIES, tag: TITLE_TIME_WAIT_TAG }), true; return titleTimePoll = TitleTime_pollByTimer(onFound), true; } function TitleTime_pollByTimer(onFound) { const state = { done: false, cancelled: false, tries: 0, timer: 0 }; state.cancel = function() { state.cancelled = true, state.timer = 0; }; const tick = function() { if (state.cancelled || state.done) return; if (TitleTime_findAnchor()) return state.done = true, onFound(), void 0; if (state.tries++, state.tries >= TITLE_TIME_POLL_MAX_TRIES) { if (state.done = true, "function" === typeof ExGuard_warn) ExGuard_warn(TITLE_TIME_WAIT_TAG, "等待「复制直播流」入口 " + Math.round(TITLE_TIME_POLL_INTERVAL_MS * TITLE_TIME_POLL_MAX_TRIES / 1e3) + " 秒仍未出现,已放弃(不影响其它功能)"); return; } state.timer = setTimeout(tick, TITLE_TIME_POLL_INTERVAL_MS); }; return tick(), state; } function TitleTime_formatShowTime(showtime, fmt) { const ts = Number(showtime); if (!ts) return ""; const date = new Date(1e3 * ts); if (isNaN(date.getTime())) return ""; return dateFormat(fmt || "MM-dd hh:mm", date); } function TitleTime_render() { const node = document.querySelector(".ex-title-time"); if (!node) return false; const state = titleTimeState; const watchText = state.watchOk ? formatSeconds(state.watchSeconds) : "—"; let startText = "—"; let startFull = "—"; if (state.showinfo) if (2 === state.isShow) startText = "未开播", startFull = "未开播"; else startText = TitleTime_formatShowTime(state.showtime) || "—", startFull = TitleTime_formatShowTime(state.showtime, "yyyy-MM-dd hh:mm") || "—"; const watchItem = node.querySelector('[data-title-time="watch"]'); const startItem = node.querySelector('[data-title-time="start"]'); if (watchItem) { const value = watchItem.querySelector(".ex-title-time__value"); if (value) value.textContent = watchText; watchItem.title = "观看时间(今日累计,官方观看任务接口,需登录):" + watchText; } if (startItem) { const value = startItem.querySelector(".ex-title-time__value"); if (value) value.textContent = startText; startItem.title = "开播时间:" + startFull; } return node.title = "观看时间(今日累计,官方观看任务接口,需登录):" + watchText + "\n开播时间:" + startFull, true; } function TitleTime_queryData(url) { return new Promise(resolve => { fetch(url, { method: "GET", mode: "no-cors", credentials: "include" }).then(res => res.json()).then(ret => { resolve(ret); }).catch(() => { resolve({ error: -1, data: null }); }); }); } async function TitleTime_queryShowInfo() { const ret = await TitleTime_queryData("https://www.douyu.com/swf_api/h5room/" + rid); const data = ret && ret.data || null; if (!data || void 0 === data.show_time || null === data.show_time) return null; return data; } function TitleTime_queryTodayWatch() { return TitleTime_queryData("https://www.douyu.com/japi/interactnc/web/fsjk/getCardTaskInfo?rid=" + rid); } function TitleTime_refresh() { if (titleTimeBusy) return; titleTimeBusy = true, titleTimeTicks++; const needShowInfo = !titleTimeState.showinfo || 2 === titleTimeState.isShow || titleTimeTicks % 10 === 1; Promise.resolve().then(function() { return needShowInfo ? TitleTime_queryShowInfo() : null; }).then(function(showInfo) { if (showInfo) titleTimeState.showinfo = true, titleTimeState.showtime = Number(showInfo.show_time) || 0, titleTimeState.isShow = void 0 === showInfo.show_status ? 2 : Number(showInfo.show_status); return TitleTime_queryTodayWatch(); }).then(function(ret) { if (titleTimeState.watchOk = !!(ret && 0 == ret.error && ret.data && void 0 !== ret.data.todayWatch), titleTimeState.watchOk) titleTimeState.watchSeconds = Number(ret.data.todayWatch) || 0; }).catch(function() {}).then(function() { titleTimeBusy = false, TitleTime_render(); }); } function TitleTime_boot() { if (titleTimeBooted) return false; return titleTimeBooted = true, TitleTime_refresh(), setInterval(TitleTime_refresh, TITLE_TIME_REFRESH_MS), true; } var curVersion = "undefined" !== typeof GM_info && GM_info.script && GM_info.script.version ? GM_info.script.version : "2026.11.14.01"; var isNeedUpdate = false; var lastestVersion = ""; const UPDATE_SEEN_KEY = "Ex_LastSeenRemoteVersion"; const UPDATE_KNOWN_KEY = "Ex_LastKnownRemoteVersion"; const UPDATE_CHECK_TIME_KEY = "Ex_LastUpdateCheckTime"; const UPDATE_CHECK_INTERVAL_MS = 12 * 3600 * 1e3; function Update_noteRemoteVersion(remoteVer) { remoteVer = String(remoteVer || ""); var newer = isNewerVersion(remoteVer, curVersion); if (isNeedUpdate = newer, lastestVersion = newer ? remoteVer : "", newer) GM_setValue(UPDATE_KNOWN_KEY, remoteVer); return Update_refreshTip(), newer; } function Update_refreshTip() { var tip = document.getElementById("ex-update__tip"); if (!tip) return; var seen = String(GM_getValue(UPDATE_SEEN_KEY) || ""); seen = !!(isNeedUpdate && lastestVersion && lastestVersion !== seen), tip.style.display = seen ? "block" : "none"; } let Update_seenBound = false; function Update_bindSeenOnOpen() { if (Update_seenBound) return; Update_seenBound = true, document.addEventListener("DouyuEx:thirdLevelOpened", function(ev) { if (ev = ev && ev.detail && ev.detail.panel, ev && ev.classList && ev.classList.contains("exupdate-panel") && isNeedUpdate) Update_markRemoteSeen(); }); } function Update_markRemoteSeen() { if (lastestVersion) GM_setValue(UPDATE_SEEN_KEY, lastestVersion); Update_refreshTip(); } function Update_stampCheckTime() { try { GM_setValue(UPDATE_CHECK_TIME_KEY, String(Date.now())); } catch (e) {} } function isNewerVersion(remote, local) { if (!remote || !local) return false; var rParts = String(remote).split(".").map(Number); var lParts = String(local).split(".").map(Number); for (var i = 0; i < Math.max(rParts.length, lParts.length); i++) { var r = rParts[i] || 0; var l = lParts[i] || 0; if (r > l) return true; if (r < l) return false; } return false; } function initPkg_Update() { initPkg_Update_Dom(), initPkg_Update_Func(), Update_bindSeenOnOpen(), initVersionLifecycleNotice(); var knownRemote = String(GM_getValue(UPDATE_KNOWN_KEY) || ""); if (knownRemote && isNewerVersion(knownRemote, curVersion)) Update_noteRemoteVersion(knownRemote); } function initPkg_Update_Dom() { Update_insertIcon(); } function Update_insertIcon() { let a = document.createElement("div"); a.className = "ex-update", a.innerHTML = ''; let b = document.getElementsByClassName("ex-panel__wrap")[0]; if (b) b.insertBefore(a, b.childNodes[0]); } function initPkg_Update_Func() { let btn = document.getElementsByClassName("ex-update")[0]; if (btn) btn.addEventListener("click", function(e) { e.stopPropagation(); let panel = document.querySelector(".exupdate-panel"); if (!panel) createExUpdatePanel(), panel = document.querySelector(".exupdate-panel"); if (panel) if ("block" === panel.style.display) panel.style.display = "none"; else { if ("function" === typeof openMiuixPanelCentered) openMiuixPanelCentered(panel, btn); else panel.style.display = "block"; if (isNeedUpdate) Update_markRemoteSeen(); } }); } const UPDATE_VERSION_SOURCES = [ "https://update.greasyfork.org/scripts/595575.meta.js", "https://scriptcat.org/scripts/code/9071/DouyuEx-RL.meta.js", "https://raw.githubusercontent.com/xayahcore/DouyuEx-RL/main/package.json", "https://fastly.jsdelivr.net/gh/xayahcore/DouyuEx-RL@main/package.json", "https://cdn.jsdelivr.net/gh/xayahcore/DouyuEx-RL@main/package.json" ]; const UPDATE_VERSION_TIMEOUT = 4e3; const UPDATE_VERSION_TOTAL_TIMEOUT = 6e3; const EX_UPDATE_PAGE_GREASYFORK = "https://greasyfork.org/zh-CN/scripts/595575-douyuex-rl-%E6%96%97%E9%B1%BC%E7%9B%B4%E6%92%AD%E9%97%B4%E5%A2%9E%E5%BC%BA%E6%8F%92%E4%BB%B6-reborn-lite"; const EX_UPDATE_PAGE_SCRIPTCAT = "https://scriptcat.org/zh-CN/script-show-page/9071"; function getUpdatePageUrl() { var source = ""; try { if ("undefined" !== typeof GM_info && GM_info) { source = String(GM_info.scriptUpdateURL || ""); var meta = GM_info.script || {}; if (!source) source = String(meta.updateURL || meta.downloadURL || meta.installURL || ""); } } catch (e) {} if (/scriptcat\.org/i.test(source)) return EX_UPDATE_PAGE_SCRIPTCAT; if (/greasyfork\.org/i.test(source)) return EX_UPDATE_PAGE_GREASYFORK; return EX_UPDATE_PAGE_SCRIPTCAT; } function isVersionLike(v) { return /^\d{4}\.\d{2}\.\d{2}\.\d{2}$/.test(String(null == v ? "" : v).trim()); } function extractVersionFromText(text) { if (!text) return null; const raw = String(text); if (-1 !== raw.indexOf("{")) try { const json = JSON.parse(raw); if (json && isVersionLike(json.version)) return String(json.version).trim(); } catch (e) {} const m = raw.match(/@version\s+([0-9][0-9.]*)/); if (m && isVersionLike(m[1])) return m[1].trim(); return null; } function requestVersionSource(url, onDone) { let settled = false; const done = text => { if (settled) return; settled = true, onDone(text); }; if ("function" === typeof GM_xmlhttpRequest) GM_xmlhttpRequest({ method: "GET", url: url, timeout: UPDATE_VERSION_TIMEOUT, onload: function(res) { if (res && 200 === res.status) done(res.responseText || res.response || ""); else done(null); }, onerror: function() { done(null); }, ontimeout: function() { done(null); }, onabort: function() { done(null); } }); else fetch(url, { cache: "no-store" }).then(function(res) { return res && res.ok ? res.text() : null; }).then(function(text) { done(text); }).catch(function() { done(null); }); } function fetchLatestVersion(callback) { const versions = []; let pending = UPDATE_VERSION_SOURCES.length; let finished = false; const finish = () => { if (finished) return; if (finished = true, clearTimeout(totalTimer), 0 === versions.length) return callback(null); let best = versions[0]; for (let i = 1; i < versions.length; i++) if (isNewerVersion(versions[i], best)) best = versions[i]; callback(best); }; const totalTimer = setTimeout(finish, UPDATE_VERSION_TOTAL_TIMEOUT); UPDATE_VERSION_SOURCES.forEach(url => { requestVersionSource(url, text => { const ver = extractVersionFromText(text); if (ver && -1 === versions.indexOf(ver)) versions.push(ver); if (pending--, pending <= 0) finish(); }); }); } function getExUpdateLogFor(version) { var log = "undefined" !== typeof EX_UPDATE_LOG && EX_UPDATE_LOG ? EX_UPDATE_LOG : null; if (!log) return null; if (log[version]) return log[version]; if (version = Object.keys(log), 0 === version.length) return null; return version.sort(function(a, b) { var pa = String(a).split(".").map(Number); var pb = String(b).split(".").map(Number); for (var i = 0; i < Math.max(pa.length, pb.length); i++) { var x = pa[i] || 0; var y = pb[i] || 0; if (x !== y) return x - y; } return 0; }), log[version[version.length - 1]]; } function getExUpdateLogSections() { return "undefined" !== typeof EX_UPDATE_LOG_SECTIONS && Array.isArray(EX_UPDATE_LOG_SECTIONS) ? EX_UPDATE_LOG_SECTIONS : [ "新增功能", "改进与修复", "其它" ]; } function escapeUpdateLogText(str) { return String(str).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); } function buildExUpdateCardsHtml() { var entry = getExUpdateLogFor(curVersion); var sections = getExUpdateLogSections(); if (!entry) return ""; var log = "undefined" !== typeof EX_UPDATE_LOG && EX_UPDATE_LOG ? EX_UPDATE_LOG : null; log = !(log && log[curVersion]), log = log ? ExUI.prose("当前版本 " + escapeUpdateLogText(curVersion) + " 暂无独立日志条目,以下为最近一条更新日志。", "exupdate-panel__note") : ""; var firstCard = true; return log + sections.map(function(name) { var items = Array.isArray(entry[name]) ? entry[name] : []; items = items.map(function(text) { return "
  • • " + formatUpdateLogItem(text) + "
  • "; }).join(""); var asideHtml = firstCard ? 'v' + escapeUpdateLogText(curVersion) + "" : ""; return firstCard = false, ExUI.card({ title: name, asideHtml: asideHtml, body: '
      ' + items + "
    ", legacy: { card: ExUI.LG.updateCard, head: ExUI.LG.updateHead, title: ExUI.LG.updateTitle } }); }).join(""); } function createExUpdatePanel() { var existing = document.querySelector(".exupdate-panel"); if (existing) { if (existing.dataset.version === curVersion) return; existing.remove(); } existing = document.createElement("div"), existing.className = "exupdate-panel miuix-modal", existing.dataset.version = curVersion; var actionBtnHtml = ExUI.btn("我已收到", { variant: "primary", id: "exupdate-action-btn" }); if (actionBtnHtml = ExUI.stack({ html: "\n " + actionBtnHtml + "\n ", cls: "ui-actions" }), existing.innerHTML = `
    ${buildExUpdateCardsHtml()} ${actionBtnHtml}
    `, document.body.appendChild(existing), "function" === typeof ensureMiuixPanelHeader) ensureMiuixPanelHeader(existing, "版本更新"); if ("function" === typeof ExUI_stickActionBar) ExUI_stickActionBar(existing); var btn = existing.querySelector("#exupdate-action-btn"); if (!btn) return; var btnBaseCls = btn.className; function setBtnState(state, text) { if (btn.className = btnBaseCls, btn.dataset.state = state, btn.disabled = false, "ack" === state) btn.classList.add("exupdate-state-btn--ack"), btn.textContent = text || "我已收到"; else if ("check" === state) btn.classList.add("exupdate-state-btn--check"), btn.textContent = text || "检查更新"; else if ("checking" === state) btn.classList.add("exupdate-state-btn--checking"), btn.textContent = text || "正在检查更新...", btn.disabled = true; else if ("latest" === state) btn.classList.add("exupdate-state-btn--latest"), btn.textContent = text || "已是最新"; else if ("upgrade" === state) btn.classList.add("exupdate-state-btn--upgrade"), btn.textContent = text || "前往更新"; else if ("error" === state) btn.classList.add("exupdate-state-btn--error"), btn.textContent = text || "检查失败,点击重试"; } if (isNeedUpdate && lastestVersion) setBtnState("upgrade", "前往更新 (v" + lastestVersion + ")"); else setBtnState("check", "检查更新"); btn.onclick = function(e) { if (e.stopPropagation(), e = btn.dataset.state, "check" === e || "latest" === e || "error" === e) setBtnState("checking", "正在检查更新..."), fetchLatestVersion(function(remoteVer) { if (!remoteVer) { if (setBtnState("error", "检查失败,点击重试"), "function" === typeof showMessage) showMessage("【版本更新】检查更新失败,无法连接到更新服务器", "error"); return; } if (isNewerVersion(remoteVer, curVersion)) { if (setBtnState("upgrade", "前往更新 (v" + remoteVer + ")"), Update_noteRemoteVersion(remoteVer), "function" === typeof showMessage) showMessage("【版本更新】检测到新版本 v" + remoteVer + ",点击前往更新", "info"); } else if (setBtnState("latest", "已是最新 (v" + curVersion + ")"), "function" === typeof showMessage) showMessage("【版本更新】当前版本 v" + curVersion + " 已是最新版本", "success"); }); else if ("upgrade" === e) if (Update_markRemoteSeen(), e = getUpdatePageUrl(), "function" === typeof GM_openInTab) GM_openInTab(e, { active: true }); else window.open(e, "_blank"); }; } function initVersionLifecycleNotice() { try { if ("function" === typeof GM_deleteValue && void 0 !== GM_getValue("Ex_LastNotifiedVersion")) GM_deleteValue("Ex_LastNotifiedVersion"); } catch (e) {} var lastCheckTime = Number(GM_getValue(UPDATE_CHECK_TIME_KEY) || 0); var now = Date.now(); if (now - lastCheckTime > UPDATE_CHECK_INTERVAL_MS) setTimeout(() => { fetchLatestVersion(function(remoteVer) { if (!remoteVer) return ExGuard_warn("版本更新", "例行探测失败(可能是网络或更新源不可达),下次启动会重试"), void 0; Update_stampCheckTime(), Update_noteRemoteVersion(remoteVer); }); }, 5e3); } function Update_checkVersion() {} function formatUpdateLogItem(text) { return text = escapeUpdateLogText(text), text = text.replace(/\b([A-Za-z][A-Za-z0-9]*(?:_[A-Za-z0-9]+)+|[A-Za-z][A-Za-z0-9]*\.js)\b/g, '$1'), text = text.replace(/【([^】]{1,14})】/g, '【$1】'), text; } const EX_UPDATE_LOG_SECTIONS = [ "新增功能", "改进与修复", "其它" ]; const EX_UPDATE_LOG = { "2026.11.14.01": { "新增功能": [ "【ScriptCat】新增 ScriptCat 安装渠道与版本源:检查更新会并发探测 Greasy Fork、ScriptCat、GitHub 与 jsDelivr 并取最大值;从 ScriptCat 安装的用户会直接回到 ScriptCat 更新页。", "【更新入口】「前往更新」按安装来源自动跳转:ScriptCat 装的开 ScriptCat,Greasy Fork 装的开 Greasy Fork;来源信息缺失时默认 ScriptCat。" ], "改进与修复": [ "【构建】所有用户脚本产物统一改为未压缩可读版本(保留空白与变量名),不再交付压缩代码,符合 Greasy Fork 与 ScriptCat 的公开脚本规则;体积约 1.16 MiB,仍在 2 MB 限制内。" ], "其它": [ "【测试】补充 ScriptCat 版本源探测与更新入口三条路由(ScriptCat / Greasy Fork / 未知来源)回归,npm test 保持全绿。", "【文档】README 增加 ScriptCat 安装入口与版本检查说明,并把安装顺序调整为 ScriptCat 优先。" ] }, "2026.11.13.01": { "新增功能": [ "【关注列表·失败重试】关注列表读取失败后不再只能刷新页面:鼠标移到顶部「关注」入口会自动补渲染一次(下拉没展开不发请求);确实没有人在播时显示「当前没有在播的关注」,不再是一片空白。" ], "改进与修复": [ "【关注列表·白屏】读取失败不再让关注列表永久空白:只有增强列表渲染成功后才隐藏原生列表,失败一律还原原生列表并说明原因(登录态失效、接口返回 error 码、数据不完整、渲染异常);请求 8 秒没有回音按失败处理,界面一定会往下走。", "【关注列表·数据】开播状态 / 轮播字段缺失时不再把整批条目筛成空(缺字段不等于没开播);昵称、房名、头像等接口文本统一转义,特殊字符按字面量显示;缺少开播时间不再出现「已播NaN秒」;切到「视频动态」等其它 tab 时增强列表自动收起,不再盖住原生内容。", "【关注列表·点击】长按与单击改用标准事件监听,不再覆盖页面自己的 mousedown / mouseup;没有双击回调时单击立即执行,回调里的「阻止默认」真正生效。", "【自动网页全屏】开启后进房没有全屏:旧主选择器在真机上已 0 命中,兜底又按图标固定下标盲点,且在页面刚开始解析时观察还不存在的 body 会当场抛错。现在按「进入网页全屏」按钮的 SVG 图标签名定位,命中不到再退旧选择器与控制条尾部候选,每点一次都校验 body.is-fullScreen,成功即停;同一个按钮不重复点击,已经全屏直接跳过,本地存储被写坏按未开启处理。", "【弹幕·连发误标】连发两条弹幕时,上一条的回执晚到会把新一条误标「可能发送失败」。现在保留最近 5 条 / 10 秒的回执,迟到的回执也能对上;首次判定没有拿到本条回执时再等 1.2 秒复核一次,不再急着下结论。", "【移除·查弹幕】「查弹幕」按钮与面板整体移除:doseeing、fz996 两个数据源都已停服,点开也拿不到内容。", "【启动·空 DOM】页面刚开始解析时还没有 body / head,样式注入、脚本注入与自动网页全屏会失败甚至抛错,连带排在后面的初始化不再执行(表现为「某几个功能没了,刷新一次又好了」)。现在按 body → head → documentElement 兜底,全都没有就等 DOMContentLoaded 再做;画中画入口的等待加上限并保证清掉定时器,等不到只留一条日志。", "【启动·就绪门】自动网页全屏与去广告的初始化移到页面就绪之后,不再在空 DOM 上踩空;init() 只保留协议钩子与不写 DOM 的项。" ], "其它": [ "【测试】新增自动网页全屏回归 3 组(图标签名优先 / 尾部候选兜底 / 已全屏不重复点击与坏存储)并接入 npm test;补齐关注列表失败路径(请求失败 / 接口 error 码 / 字段缺失 / 特殊字符转义)、无 DOM 时的初始化兜底与样式补挂、画中画等待上限、连发回执误标等回归;移除只服务已停服数据源的用例。当前 npm test 为 33 条链。", "【构建】DouyuEx_RL.user.js 与 dist 产物随版本同步重建。" ] }, "2026.11.07.01": { "新增功能": [ "【直播流水】右侧弹幕区顶部恢复统计条:今日送礼人数、发言人数与鱼翅流水,数据来自 ROZJ 公开主播榜单(dy.roozj.com),每 150 秒更新,可手动刷新或打开来源站核对;未收录、数据受限或请求失败会如实显示状态,不填假零值。图标用 QingIcon 线稿,鼠标悬停给出统计日期、来源与口径说明,开关在油猴菜单里。", "【最高画质·基线】起播档位改成「按房间记基线」:基线 = 该房间上次实测到的最高档,存 ExSave_QualityBaseline(按房号记 7 天、最多 30 间,最久未用先淘汰)。只有两个场景补取一次:① 基线 0 起播、档位表首档更高(房间新开 2K);② 基线已不在档位表(主播降档、请求被系统回落)。其余情况一条流播到底,不再二次切流。" ], "改进与修复": [ "【界面·重做】八个三级面板统一到同一套组件与动效(结构、间距、过渡同一份来源):墨色从 #64748b / 10.5px 加深到 #1e293b / 11.5px,字号收成 11 / 11.5 / 12 / 14 / 16 五档、圆角收成 8 档;面板底部操作条改成面板自己的 footer,不再压住内容;次级按钮全站一套 —— 白底 92% + 1px 深色 14% 描边、高 28、字 11.5·600、文字 #0047b3,覆盖 佩戴/卸下、送出/打开、关注在播/最近看过、刷新、保存/删除、抽屉动作行;主按钮统一 44 高。", "【图标】Dock 磁贴、三级面板标题、四级卡片头换成同一套 Phosphor 图标:常态线稿、选中填充,平时中性墨 #334155、选中走产品蓝,不再每个入口各配一种颜色。", "【直播间工具】每个工具改成「标题 + 一句说明 + 开关」,导出 / 导入 / 名单 / 面板收进抽屉(默认收起),重复投票改为抽屉里的带标签设置行,五个单元格按 回复 → 禁言 → 欢迎 → 礼物 → 投票 排列。", "【移除】「弹幕历史」面板整体退役:第三方数据源停服后它已没有数据来源,入口、包与测试一并删除。", "【直播间工具·卡死】打开「直播间工具」面板就整页无响应:面板排序挂在 MutationObserver(childList) 上,却又无条件重排 DOM,重排触发同一个观察器,微任务里无限自我重入把主线程占满。现在顺序没变就一个字都不动,排序期间先摘观察器、排完再挂回;展开精灵球、鼠标划过「直播间工具」磁贴不再卡死。", "【画质·注入】页面刚开始解析时还没有可挂脚本的 DOM,旧实现一次失败就静默放弃、整套画质系统不生效;现在会等 DOM 出现再注入(最多 5 秒),并加幂等守卫,重复注入只装一次。", "【画质·开销】取流响应里的档位表只在起播 12 秒窗口内解析,且每个 XHR 只挂一次监听;补取后的响应补回原请求地址,避免播放器/缓存按空 url 认流。补取失败保留原流并留一条告警,不影响播放。", "【测试】修掉构建产物用例里「流水金额」的时序抖动:原来固定等 3.4 秒赌异步回包,机器忙时会偶发假红,现在等值落地再断言。" ], "其它": [ "【界面门禁】新增 CSS 棘轮(规则数 / !important / 重复选择器)并接在 npm test 首位:旧类名产出点 7 → 0、内联样式 93 → 1、重复选择器 11 → 0、!important 1778 → 1768;同时把 855 个采集产物移出 git 索引,docs 下的 PNG 与 snapshot 加入忽略。", "【测试】新增画质基线回归 7 组(命中不补取 / 场景 A / 场景 B / 首档未变不切流 / 切档后内存基线同步 / 补取超时保留原响应 / 保护期改写发包)、直播间流水回归、直播间工具排序死循环回归 3 组,全部接入 npm test(33 条链、239 条断言)。", "【真机】画质两条路径(6657 首进 0→15 二次切流、252140 基线 15→回落 4→切 0,二次进房零切流)、流水条真实数据与 4 倍放大观感、Dock 八个磁贴逐个 hover,均用 ego 浏览器在真实直播间验收;卡死另有骨架页复现 + Debugger 抓栈对照。", "【构建】DouyuEx_RL.user.js 与 dist 产物随版本同步重建。" ] }, "2026.11.01.01": { "新增功能": [ "【工具条】点菜单外面就收:二级胶囊菜单、所有三级面板,以及面板里再开出来的子菜单,只要鼠标点在它们之外,一次全部收起 —— 以前只有右上角那个 × 能关,面板开着占住半屏时还得先找到 ×。" ], "改进与修复": [ "【工具条】点菜单外面的判定放在鼠标按下那一刻(捕获阶段):「点外面先收菜单、目标自己的点击再打开新面板」这个顺序天然成立,不会出现刚点开的面板被同一次点击立刻收掉;在面板里按下、拖到外面再松手(框选 / 拖拽)也不算点外面。", "【工具条】只认左键:右键按下不会误收菜单;在面板里按下、拖到外面再松手(框选 / 拖拽)同样不算点外面。", "【工具条】精灵球的「全部收敛」与二级菜单的 × 改走同一个收敛出口:运行期再开出来的子菜单层(礼物选择器、画中画菜单等)也会一并收起;三级面板清单仍然只有一份,新增面板自动纳入判定。", "【工具条】修掉「收起动画没走完就再点精灵球」时点了没反应:160 毫秒的收起动画期间重新展开会真正打开,而不是再收一次。", "【稳定性】修掉一处会让插件整体消失的隐患:构建产物是所有包拼进同一个作用域的一份脚本,只要某个包在自身声明语句执行之前就调到告警函数,计数器就会撞上 TDZ(Cannot access ExGuard_warnCounts before initialization)—— 这条异常没有任何兜底能接,会直接掐断顶层脚本,后面的初始化全部不执行(页面里连精灵球都不出现),日志里却只有一条看不懂的 ReferenceError。现在计数器改为挂在函数属性上惰性创建、上限常量改用 var,顶层提前调用只会照常打一条告警,不再中断脚本。", "【阻止P2P上传】修掉开启后进未开播房间会抛一条未捕获异常(NotSupportedError: WebRTC P2P disabled by user policy):拦截器的 createOffer / createAnswer 仍然让调用方拿到失败(P2P 起不来),但这个失败 Promise 已经挂好处理器,浏览器不再把它记为「未处理的拒绝」;本地档位被写坏时按「不拦截」处理并留痕,不再让整个包初始化失败。", "【粉丝牌页】房间加载不再白等 10 秒:粉丝牌表格只在独立页或房间内点开浮层后才出现,旧实现每次加载都轮询 20 次、等不到再报一条 warning;现在改成盯 body 子树(400ms 节流),浮层什么时候打开就什么时候接住。", "【赛事聊天室入口】这个节点只在赛事 / 活动房间才有,普通直播间等 10 秒注定空转;补藏逻辑保留,但等不到不再打 warning,只留一条 debug。", "【隐藏礼物栏】删掉已经消失的旧选择器 .right-e7ea5d 的 DomHook(真机 DOM 里命中 0 个,只在每份文档初始化时白发一条「目标不存在」)。" ], "其它": [ "【测试】新增「点外面就关」回归 11 组场景(外部关闭 / 面板内点击 / 二级菜单内点击 / 精灵球与弹幕小尾巴入口不被抢断 / 子菜单层纳入判定 / 无菜单时无副作用 / 拖拽 / 右键 / 监听幂等 / 源码护栏)与 ExGuard 顶层 TDZ 回归 5 组(声明前调用不抛 / 计数不被冲掉 / 上限与按 tag 计数不变 / 源码与产物护栏),均接入 npm test;隐藏礼物栏的 DomHook 用例同步改为现行的两个挂点。", "【真机】本轮改动全部在斗鱼真实直播间(开播 9999 / 未开播 74751、登录态)用 ego-browser 验收:点外面收二级+三级、面板内点击不收、精灵球全部收敛、子菜单层内外、右键不误收,并跑了「不绑新代码」的对照组;P2P 修复用新旧拦截器同页 A/B 验证未处理拒绝清零。", "【构建】DouyuEx_RL.user.js 与 dist 产物随版本同步重建。" ] }, "2026.10.30.01": { "新增功能": [ "【一键续牌】面板新增「粉丝牌明细」:每块粉丝牌列出等级、亲密值、今日亲密度与排名,每行右侧一个「佩戴 / 卸下」按钮,点击直接调斗鱼 operBadge 接口切换佩戴,成功后列表自动刷新(服务端会把原来那块牌一并改为未佩戴)。", "【扩展功能】新增「弹幕屏蔽」卡片:屏蔽进场弹幕(发言框上方那行「XXX 来了」)、屏蔽重复弹幕(窗口秒数 + 放大重复弹幕)、屏蔽弹幕背景。三个开关改挂在插件自己的面板里,不再往斗鱼原生菜单塞节点。" ], "改进与修复": [ "【签到】移除已停服的「房间与粉丝牌签到」:斗鱼 checkIn 接口实测对所有房间返回 error=8「功能已下线」,旧逻辑会给每个关注房间发一条注定失败的请求(900 个房间就是 900 条失败);同时移除「钻粉联赛签到」(userSign 接口 404)。签到面板不再出现这两项。", "【签到】粉丝家园遇到 1006「非房间粉丝用户」按跳过计,不再算失败;一键签到汇总按「成功 / 失败 / 跳过」分别计数。", "【背包】修复礼物到期时间显示成 1970-01-01:接口 expiry 给的是剩余天数(实测 29 / 3 / 1),旧实现当成秒级时间戳乘 1000;现在显示「剩 N 天」,写死的 20×20 方块改为按内容自适应,不再被裁成看不清的黑块。", "【一键续牌】修复从二级菜单悬停 / 快捷键打开面板时读不到数据:旧实现只在图标点击那条路刷新,面板会一直停在占位符;现在所有入口都走统一事件,建面板时也会先读一次。", "【进场弹幕】未设置过的用户默认屏蔽(恢复旧观感),开关文案与真实显隐保持一致。", "【工具条】点精灵球新增「全部收敛」:有三级面板展开时一次收起所有三级面板,并把二级菜单一并折起;三级面板清单收敛成单一常量,避免新增面板时漏登记。", "【同屏播放】面板由悬停 / 快捷键打开时也会重画房间列表与档位角标(以前只在点击路径重画)。", "【版本更新】面板由悬停 / 快捷键打开时同样会熄灭版本红点。" ], "其它": [ "【测试】新增进场弹幕屏蔽、精灵球全部收敛、一键续牌(数据加载 / 佩戴接口)等回归用例并接入 npm test;签到链路用例同步移除已下线模块。", "【构建】DouyuEx_RL.user.js 与 dist 产物随版本同步重建。" ] }, "2026.10.27.01": { "新增功能": [ "【同屏播放】新增「多屏观看」:在 Dock 里点「同屏播放」,面板顶部切到「多屏观看」,就能把播放区切成 2 / 3 / 4 / 5 格,最多同时看 5 个直播间(本房间固定占第 1 格,永远是它自己的播放器)。加房有三条路:搜索房间号 / 直播链接 / 主播名,一键列出「关注在播」,以及插件自己记着的「最近看过」(最多 20 个)。格子的布局比例与前后层级全部照斗鱼原生多屏的实测原值复刻,退出后页面恢复原样。", "【同屏播放】长按拖拽换位:按住任意一格 300 毫秒进入拖拽(贴着播放器控制条的那 42 像素不会误触),拖动时原地留占位块、落点实时高亮,松手两格就对调。换位只改「哪个房间在哪一格」,视频实例本身不搬家,所以换位过程中画面不闪断、不重新缓冲;拖到空白处或原地松手等于取消。", "【同屏播放】每格独立音量、按房间记忆:房间列表里每间房一条音量滑块(外房默认静音,主房间默认 100%),按房间号记住、跨主房间通用;面板顶部会提示「上次配置:N 个房间」,点「一键恢复」就能把上次那几间房一次加回来。", "【同屏播放】外房清晰度「自动」档:默认自动,按格子实际大小选档 —— 格子高度占到播放区 45% 以上(两房、四房那种大格)给蓝光 4M,其余按像素高度依次降到高清或流畅,多屏之后不会再给几百像素高的小格喂 2K。也可以手动锁定蓝光 4M / 超清 / 高清 / 流畅,每行都带档位角标;换档是两路画面交叉过渡(新流先静音起播、出画面 0.25 秒淡入、320 毫秒撤掉旧流),不黑屏、不转圈;换完还会对比实际取到的流地址,如实告诉你「画质已切换」「该直播间只提供同一档画质」或「部分直播间未变化」。", "【同屏播放】每间房两枚「弹幕」「画面」开关(悬停有说明)。关掉画面的房间不占格子、也不取流(列表里标「未取流」);关掉弹幕的房间不建弹幕连接,一条都不发。两个开关都跟着房间持久化。", "【弹幕合并】外房弹幕并入主画面:外房弹幕进入主房间同一个原生弹幕引擎,颜色、渐变、贵族与活动样式全部按斗鱼自己的规则渲染,每条前面加一条「来源横条 + 主播名」,一眼能分清是谁家的弹幕;同一条同时出现在主房间聊天区,带的是那位观众自己的等级徽章与粉丝牌,点昵称能打开原生用户卡片。上屏按每秒最多 12 条的滑动窗口限流,弹幕连接断了会自动重建(连「重连次数用完、socket 已是 null 但列表里还留着」这种最隐蔽的死法也能认出来)。", "【标题栏】观看时间与开播时间搬进标题栏,紧挨「复制直播流」右侧:观看时间取自官方「今日观看」接口(需登录,只代表你本人),开播时间取自房间信息;取不到写「—」、未开播写「未开播」,不拿 0 或估算值凑数。标题栏窄的时候两项各自省略,悬停可看完整值,不会把标题挤成两行。", "【截图】截图与 GIF 重做:按钮固定在播放器左侧中部(垂直居中,避开浏览器画中画、侧边栏、播放器控制条与弹幕框),单击保存 PNG、按住录制 GIF,文件名带主播名与时间;抓不到画面、跨域污染、编码失败都会给出明确原因,不再静默失败。", "【扩展功能】新增「在线工具 · 在线弹幕助手」入口:扩展功能面板最后一张卡里点「打开」,就能在新标签页查看本房间的弹幕数据(第三方站点 douyuex.com);二级菜单上原来那个常驻图标不再出现,缺房间号、弹窗被拦都只提示不报错。" ], "改进与修复": [ "【功能调整 · 第三方数据源停服】右上角「在看直播」的数据源(第三方数据站 doseeing.com)自 2026-09-30 起全线停止服务,此后那条横条只剩一个「贵宾数」,既没有参照也没有对比,本版整条撤下:贵宾数解析、已播 / 已观看的 5 秒切换、150 秒写值定时器、自愈重建与停服说明一并移除,排行榜也不再被横条顶开 34 像素,脚本头部对那个域名的请求授权也摘掉了;弹幕历史面板保留,但不再引导去第三方站点登录,查询失败时统一提示「第三方数据源已停止服务」。停服之后我们试过两条替代路线:一是把客户端能看到的弹幕数据落到本地自建档案,二是让观众之间互相补齐(点对点共享);两条路实测下来都不理想 —— 本地档案只能覆盖自己看过的房间,口径也复刻不了原站;共享缺少可靠的发现与校验机制,数据质量无法保证。因此这套「本地获取数据 + 共享数据」的相关功能整体移除,不再保留半成品入口;本机算得出来的两项没有丢,观看时间与开播时间搬到了标题栏。", "【同屏播放】面板口径统一:房间列表叫「同屏里的房间」,清晰度叫「其它房间清晰度」,底部主按钮按状态在「先加一个房间 / 退出同屏」之间切换;还没开同屏时面板第一行也显示当前直播间并标「1 / 5」,主房间的音量条终于有地方可调。房间一多(5 间房时内容比面板高出一截)主按钮常驻面板底部、房间列表自己滚动,增删房间后不会被弹回列表顶部;搜索无结果、主播没开播、同屏已满、链接解析不到房间号都有明确文案,「关注在播」读不到时直接写「读不到关注列表」,不会一直停在「正在加载」。", "【同屏播放】面板第一行不再显示成一串房间号,改为显示主播名:先取本页标题栏的主播名,取不到才向后端查一次并记住;直播间链接也不会再被分享参数里的数字拼出错误的房间号。", "【同屏播放】点击与原生交互:多屏期间点格子不再跳转新标签页,点「重新加载」真的能点到,原生弹幕的悬停菜单与用户卡片恢复可点(此前整棵原生弹幕树被一层透明覆盖吃掉,看得见点不动);我们自己的面板、编辑区、弹窗都不参与「点格子」判定。", "【同屏播放】某一格画面断了但播放器没有报错时(多屏最常见的静默黑屏),那一格会提示「画面卡住了,点「重新加载」」,画面自己恢复后提示自动撤掉;3 秒还没出画面才露出重新加载按钮,8 秒未出画面给「加载缓慢」的提示;拖拽换位、把鼠标拖出窗口再松手这类操作也不会再把某一格永久留在转圈上。", "【同屏播放】退出同屏、或把外房删光回到单屏时,外房的流与「哪一格属于谁」一并复位:以前换过位之后再退出,会在 0×0 的隐藏格里给当前直播间偷偷再建一份流,一路播到刷新页面(白耗带宽)。斗鱼是单页应用,切房时同屏会干净退回单屏:销毁全部外房流与定时器、断开外房弹幕连接,不再把旧房间的流挂在游离节点上继续拉。", "【同屏播放】换位之后所有延迟回调(掉线看门狗、取流回包、换档超时、起播重试、重载按钮的计时)都改成按「这间房此刻在哪一格」定位:以前换位后看门狗与自动重试会静默失效,极端情况下还会把重试打到隔壁房间;换位之后再改清晰度也真正生效了(以前角标变了、画面没换,因为代码假定「第一格一定是主画面」)。", "【同屏播放】本地保存的同屏配置若被写坏(非法清晰度值、对象型房间号等),加载时清洗后忽略,不会拿脏档位去取流。", "【同屏播放】底部主按钮的操作区去掉那层白色渐变遮罩,滚动内容直接从按钮后面透出来;「检查更新 / 开始签到 / 立即开始续牌」三个面板的主按钮也改成常驻面板底部、压在最上层,房间或列表再长也点得到。", "【同屏播放】编辑条整个退场:加房、删房、切换「弹幕 / 画面」、改外房清晰度全部收进同屏面板,不再在播放器上盖一条编辑条;同时按用户决定删掉「单窗播放」那套老实现(地址输入与载入同屏流那一页、虎牙 / B站 / iframe 内嵌播放、以及标题栏「切换音频线路」按钮与工具条里的同名菜单一并退役),同屏只保留一条路径。", "【弹幕合并】外房弹幕所依赖的总线被整扇替换后能重新发现入口:以前会一直往死对象里推报文,表现为「开着同屏却没有外房弹幕」;超限提示与自检文案统一成「其它房间」口径。", "【工具条】修复工具条按钮底部的蓝色指示条一旦亮起就不再熄灭:现在它跟着面板打开 / 关闭实时亮灭;鼠标划过图标只负责打开并刷新面板,关闭统一走右上角 × 或 ESC。", "【进场弹幕】修复弹幕框底部的进场提示被永久隐藏:那条提示原先靠 CSS 变量的兜底值 none,而写变量的代码要等屏蔽面板渲染过才执行;现在不依赖面板,开页即按真实设置生效,开关文案与实际状态一致。", "【多账号】修复切换账号会把本域 cookie 删掉却写回空列表(恢复列表被注释成空数组),以及「一条 cookie 都没有时回调不执行」导致点「+」重新登录没反应;账号数据损坏也不再中断整个切换流程。", "【弹幕工具】修复关键词 / 屏蔽规则里一个正则不命中就抛错、把整条派发链(贵宾数、回执、谢礼物、投票、时速…)一起掐断:现在逐项隔离,坏正则与半截规则只留一条 warn;工具条缺失时的初始化也有兜底,不再连坐。", "【关注 / 背包 / 弹幕收藏】读取关注列表失败不再永久空白;背包到期时间不再显示成一串裸数字;弹幕收藏的提示点击、搜索框与轮询都补了判空与上限。", "【标签任务 / 图片弹幕 / 抽奖 / 屏蔽 / 弹幕面板】一批轮询与请求补上兜底与上限:标签任务接口字段缺失或返回异常不再让整块崩掉;图片弹幕上传失败不再永久卡在「上传中」占位符;抽奖读取粉丝牌列表失败时不再抛错中断整块初始化(被广告拦截插件拦掉请求也算失败,这是常见状态);屏蔽与弹幕面板里那些 1 秒一次、永不停止的轮询,现在到点会自己收工并留一条可诊断的日志。", "【DomHook】钩子失败不再静默:会记录失败原因(找不到节点 / 观察失败)并留一条 warn,回调抛异常也不会把观察器带死,关闭钩子可重复调用。", "【菜单 / 版本更新】右键菜单里的「检查更新」以前点了毫无反应(它调的是为「去弹窗」刻意留空的函数),现在直接打开「版本更新」面板,面板打不开时也会明确说一声。", "【本月消费】明细接口被限流或报错时,原来每 2 秒重试一次、面板永远停在「加载中」;现在最多重试 3 次,取不到就显示「—」并留一条可诊断的日志,坏条目跳过不再抛错。", "【房间 VIP / 粉丝牌 / 弹幕小尾巴 / 聊天记忆 / 上次开播】一批「数据不完整就整块崩」的边界:VIP 特效列表里坏一条只跳过那一条、粉丝牌表格缺列不再从某行起整表中断、小尾巴设置被写坏或聊天框晚到会自动重试、聊天记忆在聊天框晚到时不再空转、「上次开播」页 body 未就绪时不再静默。", "【直播间工具 / 弹幕小助手 / 账号管理 / 鱼吧恢复 / 视频工具】把 51 处「取不到节点就直接用」的写法全部加上判空与留痕:节点不在时该功能安静地不启用,而不是抛错把整块初始化带崩(以前会出现「初始化失败:自动抢宝箱」这种一个模块连坐一片的情况)。", "【签到 / 鱼吧】鱼吧签到失败不再每 2 秒无限重试(重试前先计次、封顶 10 次,放弃时明确告知),分页与补签请求都补上了失败回调,汇总消息一定会出现且只出现一次。", "【签到 / 客户端】客户端签到加了超时双保险:请求卡住时按钮不再永久变灰(以前这种情况只能刷新页面)。", "【签到 / 房间与粉丝家园】房间签到给出真实成败与「未开播跳过」数量(以前只有一句「N 个房间正在签到中…」);粉丝家园单个房间出错不再让整批中止,失败提示也恢复了。", "【扩展功能 / 自动钓鱼】活动信息请求加 15 秒超时且失败一定落地:勾选后不再卡死、模式不再被锁死,连续失败三次会自己停手并提示。", "【扩展功能 / 送礼】完成判定改成真实计数(以前第一个请求回来就宣布「赠送完毕」,「执行中」还排在后面),现在按「成功 N/M、失败 K」汇总,全失败用错误样式。", "【扩展功能 / 红包与宝箱】红包轮询补上失败处理与自动关闭(连续三次失败就停),补抢递归封顶 5 次;宝箱延迟把 0、负数、乱码归一成合理值(以前填 0 会疯狂抢宝箱)。", "【签到 / 星推】执行结果按真实成败汇报(不再「失败全静默却宣布执行完毕」),取关前会确认关注确实是本脚本发起的,避免残留陌生主播;签到引擎也要等子任务真正结束才宣布完成。", "【工具条自愈】自愈巡检加了后台跳过与两档节流(长时间无事从 3 秒降到 30 秒一次,恢复正常立刻回快档):切房换节点时以新节点为准、曾经飘到浮动态的工具条也能自动归位,不再出现「切房后工具条飘在中间 / 同时两套工具条」。", "【关注列表】切 tab 或面板重绘不再让条目翻倍;渲染改成只替换自己插的节点,不再重解析整个容器把页面自己的节点与监听器一起冲掉。", "【弹幕记忆】切房后新聊天框立即可以按 ↑ 回忆:补绑发生在按键到达之前,同一次按键就生效,也不需要常驻定时器;回忆游标改成显式位置(原来指向数组长度、靠「按 ↑ 先减一」对齐),连续回忆不再错位、越过最后一条后能清空输入框。", "【工具条 / 本月消费 / 房间 VIP / 粉丝牌】几处「只认某一个固定锚点」的插入改成多选择器 + 有上限等待(等到就补插并幂等绑定):斗鱼改版后这些功能不再整块无声消失;粉丝牌现在房间里的浮层也能用(以前只在独立页生效),重复渲染也不会注释两遍。", "【扩展功能】初始化链路改成逐项隔离(与其它包同口径):单个模块出错只报模块名并继续,不会再让其余开关一起失效;主流程那 16 步也不再是裸平铺直调 —— 任何一处抛异常不再把排在后面的控制台、右键菜单、重置、关注列表一起带走。", "【上次开播】不再把整个页面串成字符串去找判据:先读页面全局变量,取不到才只扫内联脚本文本,页面很大时开销明显下降,取值语义不变。", "【版本红点】以前只要没点过「我已收到」就恒亮(真正探测到新版本时也是同一个小点);现在只有确实探测到更高的版本、且这个版本没被看过才亮,看过即熄灭;探测失败也不再白占 12 小时窗口。" ], "其它": [ "【测试纪律】测试脚本增至 26 个文件,npm test 一条链依次跑完;新增「构建产物四落点验收」并并入 npm test —— 它执行的是 dist 里真正的产物,专盯标题栏时间、同屏主按钮粘底且无渐变、三面板主按钮粘底、扩展功能里的在线弹幕助手入口这四处,防「源码有、产物被摇树删掉」。", "【测试纪律】同屏专项 1580 行:掉线看门狗、换位后的回调定位、坐标越界、指针丢失收尾、二次取流先销毁旧实例、脏值容错、退出清定时器、切房收手、过渡中不重复取流、操作区不得再带渐变。", "【测试纪律】弹幕合并专项 868 行 10 组:补丁顺序无关、注入恒一次、横条避让渐变、色表逐档、真实报文锚定解析(含 bnn 干扰、@S 反转义、二进制残字符)、限流滑动窗口边界、死连接重建、总线被摘后重新发现。", "【测试纪律】新增五套测试:截图抓帧链路(含 toBlob 缺失、跨域污染、长按与单击互不吞并)、弹幕派发隔离(正则不命中 / 坏正则 / async 拒绝)、进场弹幕(没有屏蔽面板时也必须生效)、死源依赖清零、多账号 cookie 恢复与 0 cookie 回调。", "【测试纪律】新增「面板死路」1968 行与「DOM 守卫」487 行两套场景,并给 common.js 两段历史死代码补了源码级保险(把门禁那行临时摘掉再跑一遍,证明守卫真的能兜住);签到链路 440 行与扩展功能 4 组场景把失败路径逐一钉住:重试封顶、汇总只出一次、超时归还按钮、请求必 settle、完成判定按真实计数。", "【测试纪律】新增公共守卫 ExGuard(带节流的 warn、有上限的轮询、带超时的请求与 Promise)与配套 9 组场景;账号侧 5 组(证据不符不写、空快照不覆盖、幂等、垃圾条目过滤);另新增切房补绑、自愈节流与后台跳过、浮动态归位、覆盖式渲染四类场景。", "【测试纪律】本次末轮新增三套专项:标题栏时间 323 行(位置、两个数据来源、三条诚实降级路径)、面板粘底 236 行、扩展入口 289 行;右上角横条撤下的回归改成只钉「横条不再出现」+「头像框三个入口与赛事入口隐藏不跟着丢」,原来的点击跳转用例随横条一起删除。" ] } }; let videoStartTime = 0; let videoTime_domhook_videoChange = null; let videoTime_domhook_showtime = null; let videoTime_domhook_hashidChange = null; let videoTime_timeout = 0; let videoTime_currentVid = ""; function VideoTime_getVideoId() { try { let hashid = document.getElementsByTagName("demand-video-toolbar")[0].shadowRoot.querySelector("share-hover").getAttribute("hashid"); if (hashid) return hashid; } catch (e) {} let pathnameArr = String(window.location.pathname).split("/"); return pathnameArr[pathnameArr.length - 1]; } function initPkg_VideoTime() { let timer = setInterval(() => { let demandVideo = document.getElementsByTagName("demand-video")[0]; if (!demandVideo || !demandVideo.shadowRoot) return; let videoDom = demandVideo.shadowRoot.getElementById("__video"); let showtimeDom = demandVideo.shadowRoot.getElementById("demandcontroller-bar").shadowRoot.querySelector("demand-video-controller-progress").shadowRoot.querySelector("demand-video-controller-preview"); let shareHoverDom = document.getElementsByTagName("demand-video-toolbar")[0].shadowRoot.querySelector("share-hover"); if (void 0 !== videoDom && null !== videoDom && showtimeDom && shareHoverDom) clearInterval(timer), VideoTime_setData(), videoTime_domhook_videoChange = new MutationObserver(function(mutations) { VideoTime_setData(); }), videoTime_domhook_videoChange.observe(videoDom, { attributes: true, childList: true, subtree: false }), videoTime_domhook_hashidChange = new MutationObserver(function(mutations) { for (let i = 0; i < mutations.length; i++) if ("hashid" === mutations[i].attributeName) { VideoTime_setData(); break; } }), videoTime_domhook_hashidChange.observe(shareHoverDom, { attributes: true, childList: false, subtree: false }), videoTime_domhook_showtime = new MutationObserver(function(mutations) { for (let i = 0; i < mutations.length; i++) { let item = mutations[i]; if ("showtime" == item.attributeName) { let showtime = Number(VideoTime_getShowTime()); VideoTime_setShowTime(String(dateFormat("yyyy-MM-dd hh:mm:ss", new Date(Number(videoStartTime + 1e3 * showtime)))) + "
    " + formatSeconds2(showtime)); break; } else if ("isshow" == item.attributeName) { clearTimeout(videoTime_timeout); let showtime = Number(VideoTime_getShowTime()); videoTime_timeout = setTimeout(() => { VideoTime_setShowTime(String(dateFormat("yyyy-MM-dd hh:mm:ss", new Date(Number(videoStartTime + 1e3 * showtime)))) + "
    " + formatSeconds2(showtime)); }, 0); break; } } }), videoTime_domhook_showtime.observe(showtimeDom, { attributes: true, childList: true, subtree: false }); }, 1e3); } function VideoTime_setData() { let videoId = VideoTime_getVideoId(); if (!videoId) return; videoTime_currentVid = videoId; let requestVid = videoId; fetch("https://v.douyu.com/video/video/getVideoUrl?vid=" + videoId, { method: "GET", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" } }).then(res => res.json()).then(ret => { if (requestVid !== videoTime_currentVid) return; let imgUrl = ret.data.viewthumb[0].url; let timeStr = getStrMiddle(imgUrl, "--", "/"); videoStartTime = new Date(timeStr.replace(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/, "$1-$2-$3 $4:$5:$6")).getTime(); }).catch(err => { console.log("请求失败!", err); }); } function VideoTime_getShowTime() { let t = document.getElementsByTagName("demand-video")[0].shadowRoot.getElementById("demandcontroller-bar").shadowRoot.querySelector("demand-video-controller-progress").shadowRoot.querySelector("demand-video-controller-preview").getAttribute("showtime"); return Number(t).toFixed(0); } function VideoTime_setShowTime(timeStr) { let dom = document.getElementsByTagName("demand-video")[0].shadowRoot.getElementById("demandcontroller-bar").shadowRoot.querySelector("demand-video-controller-progress").shadowRoot.querySelector("demand-video-controller-preview").shadowRoot.querySelector(".Preview label"); if (dom) dom.style.position = "relative", dom.style.bottom = "60px", dom.style.backgroundColor = "rgba(0,0,0,0.4)", dom.innerHTML = timeStr; } var CameraCapture_MIME_PNG = "image/png"; var CameraCapture_MIME_GIF = "image/gif"; var CameraCapture_MIN_READY_STATE = 2; var CameraCapture_REASON_TEXT = { "no-video": "没有找到视频元素(播放器还没渲染出来,等画面出来再点)", "video-not-ready": "视频还没出首帧,浏览器不允许抓帧(等画面出来再点一次)", "video-no-dimension": "视频尺寸是 0×0(视频未加载或已被隐藏)", "no-2d-context": "浏览器没有给出 2d 画布上下文", "draw-image-failed": "抓帧失败(drawImage 抛错)", "canvas-tainted": "画布被跨域视频污染,读不到像素(这类流只能改用 flv.js/MSE 播放)", "encode-failed": "图片编码没有返回数据", "download-blocked": "下载没有触发(浏览器拦截了自动下载)" }; function CameraCapture_padNumber(value) { return value < 10 ? "0" + value : "" + value; } function CameraCapture_timestamp(date) { const moment = date || new Date(); return moment.getFullYear() + "-" + CameraCapture_padNumber(moment.getMonth() + 1) + "-" + CameraCapture_padNumber(moment.getDate()) + " " + CameraCapture_padNumber(moment.getHours()) + "-" + CameraCapture_padNumber(moment.getMinutes()) + "-" + CameraCapture_padNumber(moment.getSeconds()); } function CameraCapture_sanitizeName(anchorName) { let text = String(null == anchorName ? "" : anchorName).replace(/[\\/:*?"<>|\r\n\t]+/g, "_").replace(/\s+/g, " ").trim(); if (!text) text = "斗鱼"; if (text.length > 40) text = text.slice(0, 40); return text; } function CameraCapture_buildFileName(anchorName, date, extension) { const suffix = extension || "png"; return "【" + CameraCapture_sanitizeName(anchorName) + "】" + CameraCapture_timestamp(date) + "." + suffix; } function CameraCapture_reasonText(outcome) { const result = outcome || {}; return CameraCapture_REASON_TEXT[result.reason] || "未知失败(" + (result.reason || "no-reason") + ")"; } function CameraCapture_failureText(outcome) { const result = outcome || {}; return result.detail ? CameraCapture_reasonText(result) + "|" + result.detail : CameraCapture_reasonText(result); } function CameraCapture_errorDetail(error) { if (!error) return ""; const name = error.name || "Error"; const message = error.message || String(error); return name + ": " + message; } function CameraCapture_isTaintError(error) { if (!error) return false; if ("SecurityError" === error.name) return true; return /taint|cross-origin|not allowed to export/i.test(error.message || ""); } function CameraCapture_captureFrame(video, canvas) { if (!video) return { ok: false, reason: "no-video" }; const width = video.videoWidth || 0; const height = video.videoHeight || 0; if (!width || !height) return { ok: false, reason: "video-no-dimension", detail: width + "×" + height }; const readyState = "number" === typeof video.readyState ? video.readyState : CameraCapture_MIN_READY_STATE; if (readyState < CameraCapture_MIN_READY_STATE) return { ok: false, reason: "video-not-ready", detail: "readyState=" + readyState }; const target = canvas || document.createElement("canvas"); target.width = width, target.height = height; let context = null; try { context = target.getContext("2d"); } catch (error) { context = null; } if (!context) return { ok: false, reason: "no-2d-context" }; try { context.drawImage(video, 0, 0, width, height); } catch (error) { return { ok: false, reason: CameraCapture_isTaintError(error) ? "canvas-tainted" : "draw-image-failed", detail: CameraCapture_errorDetail(error) }; } return { ok: true, canvas: target, width: width, height: height }; } function CameraCapture_clickAnchor(href, fileName) { if (!href) return false; const host = document.body || document.documentElement; if (!host) return false; const anchor = document.createElement("a"); anchor.href = href, anchor.download = fileName, anchor.rel = "noopener", anchor.style.display = "none", host.appendChild(anchor); let clicked = false; try { if ("function" === typeof anchor.click) anchor.click(), clicked = true; else { const event = document.createEvent("MouseEvents"); event.initEvent("click", false, false), anchor.dispatchEvent(event), clicked = true; } } catch (error) { clicked = false; } if (anchor.parentNode) anchor.parentNode.removeChild(anchor); return clicked; } function CameraCapture_saveBlob(blob, fileName) { if (!blob) return { ok: false, reason: "encode-failed", detail: "blob 为空" }; const canCreateObjectUrl = "undefined" !== typeof URL && URL && "function" === typeof URL.createObjectURL; if (!canCreateObjectUrl) return { ok: false, reason: "download-blocked", detail: "URL.createObjectURL 不可用" }; let objectUrl = ""; try { objectUrl = URL.createObjectURL(blob); } catch (error) { return { ok: false, reason: "download-blocked", detail: CameraCapture_errorDetail(error) }; } const clicked = CameraCapture_clickAnchor(objectUrl, fileName); if (!clicked) { try { URL.revokeObjectURL(objectUrl); } catch (error) {} return { ok: false, reason: "download-blocked", detail: fileName }; } return setTimeout(function() { try { URL.revokeObjectURL(objectUrl); } catch (error) {} }, 1500), { ok: true, fileName: fileName, mime: blob.type || "" }; } function CameraCapture_saveDataUrl(dataUrl, fileName) { if (!dataUrl || 0 !== dataUrl.indexOf("data:")) return { ok: false, reason: "encode-failed", detail: "dataURL 为空" }; const clicked = CameraCapture_clickAnchor(dataUrl, fileName); if (!clicked) return { ok: false, reason: "download-blocked", detail: fileName }; return { ok: true, fileName: fileName, mime: dataUrl.slice(5, dataUrl.indexOf(";")) }; } function CameraCapture_encodeDataUrl(canvas, mime) { try { const dataUrl = canvas.toDataURL(mime || CameraCapture_MIME_PNG); if (!dataUrl || 0 !== dataUrl.indexOf("data:")) return { ok: false, reason: "encode-failed" }; return { ok: true, dataUrl: dataUrl }; } catch (error) { return { ok: false, reason: CameraCapture_isTaintError(error) ? "canvas-tainted" : "encode-failed", detail: CameraCapture_errorDetail(error) }; } } function CameraCapture_captureAndSave(settings) { const config = settings || {}; const notify = "function" === typeof config.onResult ? config.onResult : function() {}; let delivered = false; const deliver = function(result) { if (delivered) return; delivered = true, notify(result); }; const fileName = config.fileName || CameraCapture_buildFileName(config.anchorName, config.now, "png"); const frame = CameraCapture_captureFrame(config.video, config.canvas); if (!frame.ok) return deliver(frame), void 0; const mime = config.mime || CameraCapture_MIME_PNG; const canvas = frame.canvas; if ("function" === typeof canvas.toBlob) { try { canvas.toBlob(function(blob) { if (blob) return deliver(CameraCapture_saveBlob(blob, fileName)), void 0; const encoded = CameraCapture_encodeDataUrl(canvas, mime); deliver(encoded.ok ? CameraCapture_saveDataUrl(encoded.dataUrl, fileName) : encoded); }, mime); } catch (error) { deliver({ ok: false, reason: CameraCapture_isTaintError(error) ? "canvas-tainted" : "encode-failed", detail: CameraCapture_errorDetail(error) }); } return; } const encoded = CameraCapture_encodeDataUrl(canvas, mime); deliver(encoded.ok ? CameraCapture_saveDataUrl(encoded.dataUrl, fileName) : encoded); } var CameraControl_LONG_PRESS_MS = 250; var CameraControl_HIDE_DELAY_MS = 2e3; var CameraControl_HOVER_HIT_INTERVAL_MS = 60; var CameraControl_SUPPRESS_CLICK_MS = 600; var CameraControl_EDGE_GAP = 16; var CameraControl_BUTTON_SIZE = 60; var CameraControl_TOP_RESERVE = 56; var CameraControl_TOP_RESERVE_FULLSCREEN = 72; var CameraControl_BOTTOM_RESERVE = 64; function CameraControl_isWebFullscreen() { return !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement); } function CameraControl_create(options) { const config = options || {}; const cameraId = config.cameraId || "ex-camera"; let cameraNode = null; let attached = false; let gifInstance = null; let frameTimer = 0; let longPressTimer = 0; let hideTimer = 0; let recording = false; let suppressClickUntil = 0; let lastHitTestAt = 0; function getCameraNode() { if (!cameraNode || !cameraNode.isConnected) cameraNode = document.getElementById(cameraId); return cameraNode; } function getVideoNode() { return "function" === typeof config.resolveVideo ? config.resolveVideo() : null; } function getAnchorName() { return "function" === typeof config.getAnchorName ? config.getAnchorName() : ""; } function isHidden() { return "function" === typeof config.isHidden ? !!config.isHidden() : false; } function notifyUser(message, type) { if ("function" === typeof showMessage) showMessage(message, type); } function reportFailure(outcome, prefix) { const label = prefix || "截图"; const text = CameraCapture_failureText(outcome); if ("undefined" !== typeof console && console.error) console.error("[DouyuEx][" + label + "] 失败:" + text); notifyUser("【" + label + "】失败:" + text, "error"); } function clearHideTimer() { if (!hideTimer) return; clearTimeout(hideTimer), hideTimer = 0; } function positionCameraButton() { const camera = getCameraNode(); if (!camera) return null; const video = getVideoNode(); if (!video || "function" !== typeof video.getBoundingClientRect) return null; const videoRect = video.getBoundingClientRect(); if (!videoRect || videoRect.width < 80 || videoRect.height < 80) return null; const host = camera.offsetParent || document.documentElement; if (!host || "function" !== typeof host.getBoundingClientRect) return null; const hostRect = host.getBoundingClientRect(); const size = CameraControl_BUTTON_SIZE; const fullscreen = CameraControl_isWebFullscreen(); const topReserve = fullscreen ? CameraControl_TOP_RESERVE_FULLSCREEN : CameraControl_TOP_RESERVE; const left = videoRect.left + CameraControl_EDGE_GAP; const centeredTop = videoRect.top + (videoRect.height - size) / 2; const minTop = videoRect.top + topReserve; const maxTop = videoRect.bottom - CameraControl_BOTTOM_RESERVE - size; const top = maxTop >= minTop ? Math.min(Math.max(centeredTop, minTop), maxTop) : minTop; const offsetX = Math.round(left - hostRect.left - (host.clientLeft || 0)); const offsetY = Math.round(top - hostRect.top - (host.clientTop || 0)); if (camera.style.position = "absolute", camera.style.left = Math.max(0, offsetX) + "px", camera.style.top = Math.max(0, offsetY) + "px", camera.style.right = "auto", camera.style.bottom = "auto", "function" === typeof camera.getBoundingClientRect) { const actual = camera.getBoundingClientRect(); if (actual && actual.width > 0 && actual.height > 0) { const deltaX = Math.round(left - actual.left); const deltaY = Math.round(top - actual.top); if (Math.abs(deltaX) > 1 || Math.abs(deltaY) > 1) camera.style.left = Math.max(0, offsetX + deltaX) + "px", camera.style.top = Math.max(0, offsetY + deltaY) + "px"; } } return { left: Math.round(left), top: Math.round(top), size: size, fullscreen: fullscreen, videoRect: videoRect }; } function show() { const camera = getCameraNode(); if (!camera || isHidden()) return; camera.style.display = "flex", positionCameraButton(); } function hide() { const camera = getCameraNode(); if (camera) camera.style.display = "none"; } function hideSoon(delay) { clearHideTimer(); const wait = "number" === typeof delay ? delay : CameraControl_HIDE_DELAY_MS; hideTimer = setTimeout(function() { hideTimer = 0, hide(); }, wait); } function isPointerInsidePlayer(clientX, clientY) { const video = getVideoNode(); if (!video || "function" !== typeof video.getBoundingClientRect) return false; const rect = video.getBoundingClientRect(); if (!rect || rect.width < 1 || rect.height < 1) return false; const slack = 4; return clientX >= rect.left - slack && clientX <= rect.right + slack && clientY >= rect.top - slack && clientY <= rect.bottom + slack; } function onDocumentMouseMove(event) { if (isHidden()) return; const now = Date.now(); if (now - lastHitTestAt < CameraControl_HOVER_HIT_INTERVAL_MS) return; if (lastHitTestAt = now, isPointerInsidePlayer(event.clientX, event.clientY)) return show(), hideSoon(), void 0; const camera = getCameraNode(); if (camera && "none" !== camera.style.display) hideSoon(400); } function onViewportChange() { const camera = getCameraNode(); if (camera && "none" !== camera.style.display) positionCameraButton(); } function takeScreenshot() { const video = getVideoNode(); const canvas = "function" === typeof config.getScreenshotCanvas ? config.getScreenshotCanvas(video) : null; CameraCapture_captureAndSave({ video: video, canvas: canvas, anchorName: getAnchorName(), onResult: function(outcome) { if (outcome && outcome.ok) { if (notifyUser("【截图】已保存:" + outcome.fileName, "success"), "function" === typeof config.onCaptured) config.onCaptured(outcome); } else reportFailure(outcome, "截图"); } }); } function startRecording() { const video = getVideoNode(); if (!video) return reportFailure({ reason: "no-video" }, "录制"), false; if ("function" !== typeof GIF) return notifyUser("【录制】gif 组件未加载(gif.js 被拦截?),本次只支持单击截图", "error"), false; if ("function" !== typeof config.prepareRecording) return false; const prepared = config.prepareRecording(video); if (!prepared || !prepared.canvas) return false; return abortRecording(), gifInstance = new GIF({ workers: 5, quality: 3, width: prepared.width, height: prepared.height, workerScript: config.workerScript }), recording = true, config.addFrame(video, prepared.canvas, gifInstance), frameTimer = setInterval(function() { if (gifInstance) config.addFrame(video, prepared.canvas, gifInstance); }, config.fps || 83), true; } function abortRecording() { if (frameTimer) clearInterval(frameTimer), frameTimer = 0; const instance = gifInstance; if (gifInstance = null, recording = false, instance && "function" === typeof instance.abort) try { instance.abort(); } catch (error) {} return instance; } function finishRecording() { if (frameTimer) clearInterval(frameTimer), frameTimer = 0; const instance = gifInstance; if (gifInstance = null, recording = false, !instance) return; notifyUser("【录制】正在生成 gif ...", "info"), instance.on("finished", function(blob) { const saved = CameraCapture_saveBlob(blob, CameraCapture_buildFileName(getAnchorName(), new Date(), "gif")); if (saved.ok) notifyUser("【录制】gif 已保存:" + saved.fileName, "success"); else reportFailure(saved, "录制"); }); try { instance.render(); } catch (error) { reportFailure({ reason: "encode-failed", detail: CameraCapture_errorDetail(error) }, "录制"); } } function isCloseButton(target) { return !!(target && "ex-camera-close" === target.id); } function onCameraMouseEnter() { if (isHidden()) return; clearHideTimer(), show(); } function onCameraMouseDown(event) { if (isCloseButton(event.target)) return; if (event.stopPropagation(), clearHideTimer(), suppressClickUntil = 0, longPressTimer) clearTimeout(longPressTimer); longPressTimer = setTimeout(function() { longPressTimer = 0, startRecording(); }, CameraControl_LONG_PRESS_MS); } function onCameraMouseUp(event) { if (isCloseButton(event.target)) return; if (event.stopPropagation(), longPressTimer) clearTimeout(longPressTimer), longPressTimer = 0; if (recording) finishRecording(), suppressClickUntil = Date.now() + CameraControl_SUPPRESS_CLICK_MS; hideSoon(); } function onCameraClick(event) { if (isCloseButton(event.target)) return; if (event.stopPropagation(), "function" === typeof event.preventDefault) event.preventDefault(); if (Date.now() < suppressClickUntil) return suppressClickUntil = 0, void 0; takeScreenshot(); } function onCameraMouseLeave() { if (longPressTimer) clearTimeout(longPressTimer), longPressTimer = 0; if (recording) finishRecording(), suppressClickUntil = Date.now() + CameraControl_SUPPRESS_CLICK_MS; hideSoon(400); } function attach() { if (attached) return false; const camera = getCameraNode(); if (!camera) return false; return camera.addEventListener("mouseenter", onCameraMouseEnter), camera.addEventListener("mousedown", onCameraMouseDown), camera.addEventListener("mouseup", onCameraMouseUp), camera.addEventListener("click", onCameraClick), camera.addEventListener("mouseleave", onCameraMouseLeave), document.addEventListener("mousemove", onDocumentMouseMove, true), document.addEventListener("fullscreenchange", onViewportChange), document.addEventListener("webkitfullscreenchange", onViewportChange), window.addEventListener("resize", onViewportChange), attached = true, true; } function detach() { if (!attached) return; const camera = getCameraNode(); if (camera) camera.removeEventListener("mouseenter", onCameraMouseEnter), camera.removeEventListener("mousedown", onCameraMouseDown), camera.removeEventListener("mouseup", onCameraMouseUp), camera.removeEventListener("click", onCameraClick), camera.removeEventListener("mouseleave", onCameraMouseLeave); document.removeEventListener("mousemove", onDocumentMouseMove, true), document.removeEventListener("fullscreenchange", onViewportChange), document.removeEventListener("webkitfullscreenchange", onViewportChange), window.removeEventListener("resize", onViewportChange), abortRecording(), clearHideTimer(), attached = false; } return { attach: attach, detach: detach, show: show, hide: hide, hideSoon: hideSoon, position: positionCameraButton, takeScreenshot: takeScreenshot, isRecording: function() { return recording; }, getCameraNode: getCameraNode, getVideoNode: getVideoNode }; } var gifworkerStr = `(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o=ByteArray.pageSize)this.newPage();this.pages[this.page][this.cursor++]=val};ByteArray.prototype.writeUTFBytes=function(string){for(var l=string.length,i=0;i=0)this.dispose=disposalCode};GIFEncoder.prototype.setRepeat=function(repeat){this.repeat=repeat};GIFEncoder.prototype.setTransparent=function(color){this.transparent=color};GIFEncoder.prototype.addFrame=function(imageData){this.image=imageData;this.colorTab=this.globalPavarte&&this.globalPavarte.slice?this.globalPavarte:null;this.getImagePixels();this.analyzePixels();if(this.globalPavarte===true)this.globalPavarte=this.colorTab;if(this.firstFrame){this.writeLSD();this.writePavarte();if(this.repeat>=0){this.writeNetscapeExt()}}this.writeGraphicCtrlExt();this.writeImageDesc();if(!this.firstFrame&&!this.globalPavarte)this.writePavarte();this.writePixels();this.firstFrame=false};GIFEncoder.prototype.finish=function(){this.out.writeByte(59)};GIFEncoder.prototype.setQuality=function(quality){if(quality<1)quality=1;this.sample=quality};GIFEncoder.prototype.setDither=function(dither){if(dither===true)dither="FloydSteinberg";this.dither=dither};GIFEncoder.prototype.setGlobalPavarte=function(pavarte){this.globalPavarte=pavarte};GIFEncoder.prototype.getGlobalPavarte=function(){return this.globalPavarte&&this.globalPavarte.slice&&this.globalPavarte.slice(0)||this.globalPavarte};GIFEncoder.prototype.writeHeader=function(){this.out.writeUTFBytes("GIF89a")};GIFEncoder.prototype.analyzePixels=function(){if(!this.colorTab){this.neuQuant=new NeuQuant(this.pixels,this.sample);this.neuQuant.buildColormap();this.colorTab=this.neuQuant.getColormap()}if(this.dither){this.ditherPixels(this.dither.replace("-serpentine",""),this.dither.match(/-serpentine/)!==null)}else{this.indexPixels()}this.pixels=null;this.colorDepth=8;this.palSize=7;if(this.transparent!==null){this.transIndex=this.findClosest(this.transparent,true)}};GIFEncoder.prototype.indexPixels=function(imgq){var nPix=this.pixels.length/3;this.indexedPixels=new Uint8Array(nPix);var k=0;for(var j=0;j=0&&x1+x=0&&y1+y>16,(c&65280)>>8,c&255,used)};GIFEncoder.prototype.findClosestRGB=function(r,g,b,used){if(this.colorTab===null)return-1;if(this.neuQuant&&!used){return this.neuQuant.lookupRGB(r,g,b)}var c=b|g<<8|r<<16;var minpos=0;var dmin=256*256*256;var len=this.colorTab.length;for(var i=0,index=0;i=0){disp=dispose&7}disp<<=2;this.out.writeByte(0|disp|0|transp);this.writeShort(this.delay);this.out.writeByte(this.transIndex);this.out.writeByte(0)};GIFEncoder.prototype.writeImageDesc=function(){this.out.writeByte(44);this.writeShort(0);this.writeShort(0);this.writeShort(this.width);this.writeShort(this.height);if(this.firstFrame||this.globalPavarte){this.out.writeByte(0)}else{this.out.writeByte(128|0|0|0|this.palSize)}};GIFEncoder.prototype.writeLSD=function(){this.writeShort(this.width);this.writeShort(this.height);this.out.writeByte(128|112|0|this.palSize);this.out.writeByte(0);this.out.writeByte(0)};GIFEncoder.prototype.writeNetscapeExt=function(){this.out.writeByte(33);this.out.writeByte(255);this.out.writeByte(11);this.out.writeUTFBytes("NETSCAPE2.0");this.out.writeByte(3);this.out.writeByte(1);this.writeShort(this.repeat);this.out.writeByte(0)};GIFEncoder.prototype.writePavarte=function(){this.out.writeBytes(this.colorTab);var n=3*256-this.colorTab.length;for(var i=0;i>8&255)};GIFEncoder.prototype.writePixels=function(){var enc=new LZWEncoder(this.width,this.height,this.indexedPixels,this.colorDepth);enc.encode(this.out)};GIFEncoder.prototype.stream=function(){return this.out};module.exports=GIFEncoder},{"./LZWEncoder.js":2,"./TypedNeuQuant.js":3}],2:[function(require,module,exports){var EOF=-1;var BITS=12;var HSIZE=5003;var masks=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535];function LZWEncoder(width,height,pixels,colorDepth){var initCodeSize=Math.max(2,colorDepth);var accum=new Uint8Array(256);var htab=new Int32Array(HSIZE);var codetab=new Int32Array(HSIZE);var cur_accum,cur_bits=0;var a_count;var free_ent=0;var maxcode;var clear_flg=false;var g_init_bits,ClearCode,EOFCode;function char_out(c,outs){accum[a_count++]=c;if(a_count>=254)flush_char(outs)}function cl_block(outs){cl_hash(HSIZE);free_ent=ClearCode+2;clear_flg=true;output(ClearCode,outs)}function cl_hash(hsize){for(var i=0;i=0){disp=hsize_reg-i;if(i===0)disp=1;do{if((i-=disp)<0)i+=hsize_reg;if(htab[i]===fcode){ent=codetab[i];continue outer_loop}}while(htab[i]>=0)}output(ent,outs);ent=c;if(free_ent<1<0){outs.writeByte(a_count);outs.writeBytes(accum,0,a_count);a_count=0}}function MAXCODE(n_bits){return(1<0)cur_accum|=code<=8){char_out(cur_accum&255,outs);cur_accum>>=8;cur_bits-=8}if(free_ent>maxcode||clear_flg){if(clear_flg){maxcode=MAXCODE(n_bits=g_init_bits);clear_flg=false}else{++n_bits;if(n_bits==BITS)maxcode=1<0){char_out(cur_accum&255,outs);cur_accum>>=8;cur_bits-=8}flush_char(outs)}}this.encode=encode}module.exports=LZWEncoder},{}],3:[function(require,module,exports){var ncycles=100;var netsize=256;var maxnetpos=netsize-1;var netbiasshift=4;var intbiasshift=16;var intbias=1<>betashift;var betagamma=intbias<>3;var radiusbiasshift=6;var radiusbias=1<>3);var i,v;for(i=0;i>=netbiasshift;network[i][1]>>=netbiasshift;network[i][2]>>=netbiasshift;network[i][3]=i}}function altersingle(alpha,i,b,g,r){network[i][0]-=alpha*(network[i][0]-b)/initalpha;network[i][1]-=alpha*(network[i][1]-g)/initalpha;network[i][2]-=alpha*(network[i][2]-r)/initalpha}function alterneigh(radius,i,b,g,r){var lo=Math.abs(i-radius);var hi=Math.min(i+radius,netsize);var j=i+1;var k=i-1;var m=1;var p,a;while(jlo){a=radpower[m++];if(jlo){p=network[k--];p[0]-=a*(p[0]-b)/alpharadbias;p[1]-=a*(p[1]-g)/alpharadbias;p[2]-=a*(p[2]-r)/alpharadbias}}}function contest(b,g,r){var bestd=~(1<<31);var bestbiasd=bestd;var bestpos=-1;var bestbiaspos=bestpos;var i,n,dist,biasdist,betafreq;for(i=0;i>intbiasshift-netbiasshift);if(biasdist>betashift;freq[i]-=betafreq;bias[i]+=betafreq<>1;for(j=previouscol+1;j>1;for(j=previouscol+1;j<256;j++)netindex[j]=maxnetpos}function inxsearch(b,g,r){var a,p,dist;var bestd=1e3;var best=-1;var i=netindex[g];var j=i-1;while(i=0){if(i=bestd)i=netsize;else{i++;if(dist<0)dist=-dist;a=p[0]-b;if(a<0)a=-a;dist+=a;if(dist=0){p=network[j];dist=g-p[1];if(dist>=bestd)j=-1;else{j--;if(dist<0)dist=-dist;a=p[0]-b;if(a<0)a=-a;dist+=a;if(dist>radiusbiasshift;if(rad<=1)rad=0;for(i=0;i=lengthcount)pix-=lengthcount;i++;if(delta===0)delta=1;if(i%delta===0){alpha-=alpha/alphadec;radius-=radius/radiusdec;rad=radius>>radiusbiasshift;if(rad<=1)rad=0;for(j=0;j 0 && node.videoHeight > 0); } function Camera_isVisibleVideoNode(node) { if (!Camera_isUsableVideoNode(node)) return false; if ("function" === typeof node.getClientRects && 0 === node.getClientRects().length) return false; return true; } function Camera_resolveVideoNode() { const cached = camera_videoCacheNode; if (camera_videoCacheNode && Date.now() - camera_videoCacheAt < Camera_VIDEO_CACHE_MS && Camera_isUsableVideoNode(cached)) return cached; const candidates = []; const pushCandidate = function(node) { if (node && candidates.indexOf(node) < 0) candidates.push(node); }; if (pushCandidate(document.querySelector("#js-player-dialog video")), "undefined" !== typeof liveVideoNode) pushCandidate(liveVideoNode); const selectors = [ ".layout-Player-videoEntity video", ".layout-Player-video video", "#js-player-main video", "video#__video2" ]; for (let i = 0; i < selectors.length; i++) pushCandidate(document.querySelector(selectors[i])); const embeddedFrame = document.getElementsByTagName("demand-video")[0]; if (embeddedFrame && embeddedFrame.shadowRoot) pushCandidate(embeddedFrame.shadowRoot.getElementById("__video")); let resolved = null; for (let i = 0; i < candidates.length && !resolved; i++) if (Camera_isVisibleVideoNode(candidates[i])) resolved = candidates[i]; for (let i = 0; i < candidates.length && !resolved; i++) if (Camera_isUsableVideoNode(candidates[i])) resolved = candidates[i]; if (!resolved && candidates.length > 0) resolved = candidates[0]; return camera_videoCacheNode = resolved, camera_videoCacheAt = Date.now(), resolved; } function Camera_getScreenshotCanvas(video) { if (!camera_canvas_img) camera_canvas_img = document.createElement("canvas"); const width = video && video.videoWidth || 0; const height = video && video.videoHeight || 0; if (width > 0 && height > 0) camera_canvas_img.width = width, camera_canvas_img.height = height; return camera_canvas_img; } function Camera_prepareRecordingCanvas(video) { const width = Math.max(2, Math.round(.25 * (video && video.videoWidth || 640))); const height = Math.max(2, Math.round(.25 * (video && video.videoHeight || 360))); if (!camera_canvas) camera_canvas = document.createElement("canvas"); return camera_canvas.width = width, camera_canvas.height = height, camera_width = width, camera_height = height, { canvas: camera_canvas, width: width, height: height }; } function Camera_insertIcon() { if (document.getElementById("ex-camera")) return; let a = document.createElement("div"); a.id = "ex-camera", a.title = "单击截图 长按录制gif", a.innerHTML = `
    ×
    `; let host = getValidDom([ "#js-player-dialog", ".layout-Player", ".player__jsy1T", "#js-player-main", ".layout-Player-videoEntity" ]) || document.body; if (!host) return; if (host.firstChild) host.insertBefore(a, host.firstChild); else host.appendChild(a); } function Camera_isHidden() { let saved = localStorage.getItem("ExSave_Camera_Hidden"); if (!saved) return false; let time = parseInt(saved); return Date.now() < time; } function initPkg_VideoTools_Camera_Func() { let camera = document.getElementById("ex-camera"); let closeBtn = document.getElementById("ex-camera-close"); let isClosed = false; if (!camera || !closeBtn) return; if (closeBtn.addEventListener("click", e => { if (e.stopPropagation(), confirm("是否7天内不再显示?")) localStorage.setItem("ExSave_Camera_Hidden", Date.now() + 7 * 24 * 60 * 60 * 1e3); else isClosed = true; camera.style.display = "none"; }), Camera_isHidden()) return camera.style.display = "none", void 0; if (camera_controller) camera_controller.detach(), camera_controller = null; if (camera_controller = CameraControl_create({ cameraId: "ex-camera", resolveVideo: Camera_resolveVideoNode, getAnchorName: function() { return camera_anchorName; }, isHidden: function() { return isClosed || Camera_isHidden(); }, getScreenshotCanvas: Camera_getScreenshotCanvas, prepareRecording: Camera_prepareRecordingCanvas, addFrame: function(video, canvas, gif) { cameraAddFrame(video, canvas, gif, camera_fps); }, workerScript: gifworkerBlob, fps: camera_fps }), camera_controller.attach()) camera_controller.hide(); } function initPkg_VideoTools_Camera_Video() { let attempts = 0; let pollTimer = setInterval(() => { attempts++; const embeddedFrame = document.getElementsByTagName("demand-video")[0]; const shadowRoot = embeddedFrame && embeddedFrame.shadowRoot ? embeddedFrame.shadowRoot : null; if (liveVideoNode = shadowRoot ? shadowRoot.getElementById("__video") : null, liveVideoNode && liveVideoNode.videoWidth) { clearInterval(pollTimer); const anchorNode = shadowRoot.querySelector(".anchor-name"); return camera_anchorName = anchorNode ? String(anchorNode.innerText || anchorNode.textContent || "").trim() : camera_anchorName, initPkg_VideoTools_Camera_Video_Dom(), initPkg_VideoTools_Camera_Video_Func(), void 0; } if (attempts >= 60) clearInterval(pollTimer); }, 1e3); } function initPkg_VideoTools_Camera_Video_Dom() { Camera_Video_insertIcon(); } function Camera_Video_insertIcon() { if (document.getElementById("ex-camera")) return; let a = document.createElement("div"); a.id = "ex-camera", a.title = "单击截图 长按录制gif", a.innerHTML = `
    ×
    `; let host = getValidDom([ ".Video", "#js-player-dialog", "#js-player-main" ]) || document.body; if (!host) return; if (host.firstChild) host.insertBefore(a, host.firstChild); else host.appendChild(a); } function Camera_Video_isHidden() { let saved = localStorage.getItem("ExSave_Camera_Hidden"); if (!saved) return false; let time = parseInt(saved); return Date.now() < time; } function initPkg_VideoTools_Camera_Video_Func() { let camera = document.getElementById("ex-camera"); let closeBtn = document.getElementById("ex-camera-close"); if (!camera || !closeBtn) return; let isClosed = false; if (closeBtn.addEventListener("click", e => { if (e.stopPropagation(), confirm("是否7天内不再显示?")) localStorage.setItem("ExSave_Camera_Hidden", Date.now() + 7 * 24 * 60 * 60 * 1e3); else isClosed = true; camera.style.display = "none"; }), Camera_Video_isHidden()) return camera.style.display = "none", void 0; if (camera_controller) camera_controller.detach(), camera_controller = null; if (camera_controller = CameraControl_create({ cameraId: "ex-camera", resolveVideo: Camera_Video_resolveVideoNode, getAnchorName: function() { return camera_anchorName; }, isHidden: function() { return isClosed || Camera_Video_isHidden(); }, getScreenshotCanvas: Camera_getScreenshotCanvas, prepareRecording: Camera_prepareRecordingCanvas, addFrame: function(video, canvas, gif) { cameraAddFrame(video, canvas, gif, camera_fps); }, workerScript: gifworkerBlob, fps: camera_fps }), camera_controller.attach()) camera_controller.hide(); } function Camera_Video_resolveVideoNode() { if ("undefined" !== typeof liveVideoNode && liveVideoNode && liveVideoNode.videoWidth) return liveVideoNode; const embeddedFrame = document.getElementsByTagName("demand-video")[0]; if (embeddedFrame && embeddedFrame.shadowRoot) { const shadowVideo = embeddedFrame.shadowRoot.getElementById("__video"); if (shadowVideo) return shadowVideo; } const selectors = [ ".Video video", "#js-player-dialog video", "#js-player-main video", "video#__video2" ]; for (let i = 0; i < selectors.length; i++) { const node = document.querySelector(selectors[i]); if (node) return node; } return null; } function initPkg_VideoTools_Cinema() { initPkg_VideoTools_Cinema_Dom(), initPkg_VideoTools_Cinema_Func(); } function initPkg_VideoTools_Cinema_Dom() { Cinema_insertIcon(); } function Cinema_insertIcon() { let a = document.createElement("li"); a.id = "ex-cinema", a.innerHTML = ` 影院比例
    • 默认
    • 剪裁
    • 拉伸
    `; let b = document.getElementsByClassName("menu-da2a9e")[0]; if (b) b.insertBefore(a, b.childNodes[1]); } function initPkg_VideoTools_Cinema_Func() { let btnDef = document.getElementById("cinema__default"); if (btnDef) btnDef.addEventListener("click", () => { StyleHook_remove("Ex_Style_Cinema"); }); let btnCov = document.getElementById("cinema__cover"); if (btnCov) btnCov.addEventListener("click", () => { setVideoCinemaMode("cover"); }); let btnFil = document.getElementById("cinema__fill"); if (btnFil) btnFil.addEventListener("click", () => { setVideoCinemaMode("fill"); }); } function setVideoCinemaMode(fit) { let newHeigth = String(parseInt(liveVideoNode.style.width) / 2.39) + "px"; StyleHook_remove("Ex_Style_Cinema"); let style = ` .layout-Player-videoEntity video{object-fit:${fit} !important;height:${newHeigth} !important;} `; StyleHook_set("Ex_Style_Cinema", style); } let icon_joysound_on = `joysound-on`; let icon_joysound_off = `joysound-off`; function initPkg_VideoTools_Joysound() { initPkg_VideoTools_Joysound_Menu(), initPkg_VideoTools_Joysound_Func(); } function Joysound_isEnabled() { return unsafeWindow.hasInstalledJoysound && 1 == localStorage.getItem("Ex_isJoysound"); } function Joysound_updateMenuUI() { const switchEl = document.getElementById("vtoolbar-joysound-switch"); const iconEl = document.getElementById("vtoolbar-joysound-icon"); if (!switchEl) return; const enabled = Joysound_isEnabled(); if (enabled) { if (switchEl.classList.add("is-on"), iconEl) iconEl.innerHTML = icon_joysound_on; } else if (switchEl.classList.remove("is-on"), iconEl) iconEl.innerHTML = icon_joysound_off; } function initPkg_VideoTools_Joysound_Menu() { Joysound_updateMenuUI(); } function initPkg_VideoTools_Joysound_Func() { const menuBtn = document.getElementById("vtoolbar-menu-joysound"); if (!menuBtn) return; menuBtn.addEventListener("click", e => { if (e.stopPropagation(), unsafeWindow.hasInstalledJoysound) { if (1 == localStorage.getItem("Ex_isJoysound")) unsafeWindow.disableJoysound(); else unsafeWindow.enableJoysound(); Joysound_updateMenuUI(); } else openPage("https://src.douyuex.com/src/joysound.user.js"); }); } const META_FIELDS = [ { key: "dy_cpu_model", label: "CPU" }, { key: "dy_gpu_model", label: "显卡" }, { key: "dy_device_model", label: "设备" }, { key: "dy_os_version", label: "系统" }, { key: "z_canvas_code", label: "场景" } ]; const META_PROBE_TIMEOUT = 6e3; let videoMetaDataCache = { rid: null, data: null }; let isProbingMetaData = false; function initPkg_VideoTools_MetaData() { initPkg_VideoTools_MetaData_Placeholder(); } function initPkg_VideoTools_MetaData_Placeholder() { if (document.getElementById("ex-metadata")) return; let menu = document.getElementsByClassName("menu-da2a9e")[0]; if (!menu) return; let a = document.createElement("li"); a.id = "ex-metadata", a.innerHTML = `主播配置信息`, a.addEventListener("mouseenter", () => MetaData_probeByHover(a)), a.addEventListener("click", e => { if (e.stopPropagation(), null !== MetaData_getCached()) return; MetaData_probeNow(a); }), menu.insertBefore(a, menu.childNodes[1]); } function MetaData_getCurrentRid() { return String("undefined" !== typeof rid && rid || ""); } function MetaData_getCached() { return videoMetaDataCache.rid === MetaData_getCurrentRid() ? videoMetaDataCache.data : null; } function MetaData_probeByHover(container) { let cached = MetaData_getCached(); if (null !== cached) return renderMetaDataContent(container, cached, true), void 0; MetaData_probeNow(container); } function MetaData_probeNow(container) { if (isProbingMetaData) return; isProbingMetaData = true; let wrap = container.querySelector(".metadata__wrap"); if (wrap) wrap.innerHTML = `
  • 探测中...
  • `; let probingRid = MetaData_getCurrentRid(); MetaData_probe((meta, ok) => { if (isProbingMetaData = false, probingRid !== MetaData_getCurrentRid()) return; if (!ok) return renderMetaDataContent(container, null, false), void 0; videoMetaDataCache = { rid: probingRid, data: meta || {} }, renderMetaDataContent(container, meta || {}, true); }); } function MetaData_hasConfig(meta) { return !!meta && META_FIELDS.some(f => meta[f.key]); } function MetaData_escape(value) { return String(null == value ? "" : value).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } function renderMetaDataContent(container, meta, ok) { if (!container) return; let wrap = container.querySelector(".metadata__wrap"); if (!wrap) return; if (!ok) return wrap.innerHTML = `
  • 探测失败
  • `, void 0; if (!MetaData_hasConfig(meta)) return wrap.innerHTML = `
  • 该主播未开启配置上报
  • `, void 0; wrap.innerHTML = META_FIELDS.filter(f => meta[f.key]).map(f => `
  • ${f.label}
    ${MetaData_escape(meta[f.key])}
  • `).join(""); } function MetaData_probe(callback) { const finish = (meta, ok) => { if ("function" === typeof callback) callback(meta || {}, !!ok); }; ExLoadLib(EXURL.flv, () => { getRealLive_Douyu(rid, true, false, "1", lurl => { if (!lurl || "None" === lurl) return finish(null, false); if ("undefined" === typeof flvjs || !flvjs.isSupported()) return finish(null, false); let id = "Fake_" + Date.now(); let host = document.createElement("div"); host.id = "exVideoDiv" + id, host.style.display = "none", host.innerHTML = ``, document.body.appendChild(host); let player = flvjs.createPlayer({ type: "flv", url: lurl }, { fixAudioTimestampGap: false }); let settled = false; let timer = 0; const cleanup = () => { clearTimeout(timer); try { player.destroy(); } catch (e) {} try { host.remove(); } catch (e) {} }; const settle = (meta, ok) => { if (settled) return; settled = true, cleanup(), finish(meta, ok); }; const EV = flvjs.Events || {}; player.on(EV.METADATA_ARRIVED || "metadata_arrived", meta => settle(meta, true)), player.on("media_info", e => { if (e && e.metadata) settle(e.metadata, true); }), player.on(EV.ERROR || "error", () => settle(null, false)), timer = setTimeout(() => settle(null, false), META_PROBE_TIMEOUT), player.attachMediaElement(document.getElementById("exVideoPlayer" + id)), player.load(); }); }, () => finish(null, false)); } let icon_pipcontrol_set = ``; let icon_pipcontrol_send = ``; let pip_ws_instance = null; let pipConfig = { fontSize: 18, speed: 2.5, area: "full", trackHeight: 28, mergeMode: "combo", lowPowerMode: false, filterRobotDanmaku: true, opacity: 1, danmakuVisible: true }; let pipControlbarHook = null; let pipDocObserver = null; let pipControlbarSyncRaf = null; let pipDocSyncRaf = null; let pipNativeTriggerBtn = null; let pipMenuHideTimer = null; let pipMenuAnchorBtn = null; let pipKeepAliveVideo = null; let pipKeepAliveHandle = null; let pipKeepAliveCanvas = null; let pipVisRecoverHandler = null; let pipFrameStallTimer = null; let pipFullscreenListenerBound = false; const PIP_MENU_SVG_NATIVE = ``; const PIP_MENU_SVG_ENHANCED = ``; const PIP_BTN_SVG_RELOAD = ``; function PictureInPictureControl_getDanmakuOpacity() { const v = pipConfig.opacity; if (null == v || Number.isNaN(v)) return 1; return Math.min(1, Math.max(.3, v)); } function PictureInPictureControl_applyDanmakuStyle(pipWindow) { const win = pipWindow || window.__pip_window__; if (!win || win.closed) return; const layer = win.document.getElementById("danmaku"); const combo = win.document.getElementById("combo-container"); const opacity = PictureInPictureControl_getDanmakuOpacity(); const visible = false !== pipConfig.danmakuVisible; if (layer) if (layer.style.opacity = String(opacity), layer.style.visibility = visible ? "visible" : "hidden", !visible) while (layer.firstChild) layer.removeChild(layer.firstChild); if (combo) if (combo.style.opacity = String(opacity), combo.style.display = visible ? "" : "none", !visible) combo.textContent = ""; } function PictureInPictureControl_updateDanmakuToggleBtn(pipWindow) { const win = pipWindow || window.__pip_window__; const btn = win?.document.getElementById("pip-danmaku-toggle"); if (!btn) return; const visible = false !== pipConfig.danmakuVisible; btn.textContent = "弹", btn.title = visible ? "隐藏弹幕" : "显示弹幕", btn.classList.toggle("is-off", !visible); } function PictureInPictureControl_initPipUiText(pipWindow) { const doc = pipWindow.document; const inputField = doc.getElementById("pip-input-field"); const submitBtn = doc.getElementById("pip-submit-btn"); if (inputField) inputField.setAttribute("placeholder", "发条弹幕吧..."); if (submitBtn) submitBtn.textContent = "发送"; const reloadBtn = doc.getElementById("pip-reload"); if (reloadBtn) reloadBtn.title = "刷新画面(恢复卡屏)"; const backBtn = doc.getElementById("pip-back-opener"); if (backBtn) backBtn.textContent = "回到网页", backBtn.title = "退出画中画并返回直播页"; PictureInPictureControl_updateDanmakuToggleBtn(pipWindow); } function PictureInPictureControl_cleanupEnhancedPip(pipWindow, pipVideo) { if (!window.__pip_is_active__) return; if (window.__pip_is_active__ = false, window.__pip_window__ = null, pipWindow?.__pip_keydown_handler__) pipWindow.document.removeEventListener("keydown", pipWindow.__pip_keydown_handler__), pipWindow.__pip_keydown_handler__ = null; if (PictureInPictureControl_stopTabAntiFreeze(), PictureInPictureControl_toggleSourcePagePower(false), PictureInPictureControl_toggleMainVideoVisibility(false), PictureInPictureControl_closePipWebSocket(), PictureInPictureControl_clearWsDedup(), window.__pip_track_state__ = [], comboCleanerTimer) clearInterval(comboCleanerTimer), comboCleanerTimer = null; comboMap.clear(); try { if (pipVideo) pipVideo.srcObject = null; } catch (e) {} if (pipWindow && !pipWindow.closed) try { pipWindow.close(); } catch (e) {} } function PictureInPictureControl_returnToOpener(pipWindow, exitPip = true) { const win = pipWindow || window.__pip_window__; const sourceVideo = win?.__pip_source_video__ || document.getElementById("__video2"); if (exitPip && window.__pip_is_active__) { const pipVideo = win?.document?.getElementById("pip-video"); PictureInPictureControl_cleanupEnhancedPip(win, pipVideo); } try { window.focus(); } catch (e) {} if (sourceVideo) { try { sourceVideo.scrollIntoView({ block: "nearest", behavior: "smooth" }); } catch (e) {} if (!exitPip && window.__pip_is_active__) PictureInPictureControl_wakeSourceVideo(sourceVideo).catch(() => {}); else if (exitPip) sourceVideo.play().catch(() => {}); } } function PictureInPictureControl_stopSourceVideoKeepAlive() { const video = pipKeepAliveVideo; if (null != pipKeepAliveHandle && video && "function" === typeof video.cancelVideoFrameCallback) try { video.cancelVideoFrameCallback(pipKeepAliveHandle); } catch (e) {} else if (null != pipKeepAliveHandle) clearTimeout(pipKeepAliveHandle); pipKeepAliveHandle = null, pipKeepAliveVideo = null; } function PictureInPictureControl_startSourceVideoKeepAlive(video) { if (PictureInPictureControl_stopSourceVideoKeepAlive(), !video || !window.__pip_is_active__) return; if (pipKeepAliveVideo = video, !pipKeepAliveCanvas) pipKeepAliveCanvas = document.createElement("canvas"), pipKeepAliveCanvas.width = 2, pipKeepAliveCanvas.height = 2; const ctx = pipKeepAliveCanvas.getContext("2d", { willReadFrequently: true }); const tick = () => { if (!window.__pip_is_active__ || pipKeepAliveVideo !== video) return; try { if (video.readyState >= 2) ctx.drawImage(video, 0, 0, 2, 2); } catch (e) {} if ("function" === typeof video.requestVideoFrameCallback) pipKeepAliveHandle = video.requestVideoFrameCallback(tick); else pipKeepAliveHandle = setTimeout(tick, 200); }; tick(); } async function PictureInPictureControl_wakeSourceVideo(video) { if (!video) return false; const wasPaused = video.paused; await video.play().catch(() => {}); const hadLowOpacity = "0.01" === video.style.getPropertyValue("opacity"); if (hadLowOpacity) video.style.removeProperty("opacity"); try { window.focus(); } catch (e) {} await new Promise(resolve => { let done = false; const finish = () => { if (done) return; done = true, resolve(); }; if ("function" === typeof video.requestVideoFrameCallback) video.requestVideoFrameCallback(() => { video.requestVideoFrameCallback(finish); }); setTimeout(finish, 150); }); try { if (video.readyState >= 2 && pipKeepAliveCanvas) { const ctx = pipKeepAliveCanvas.getContext("2d", { willReadFrequently: true }); ctx.drawImage(video, 0, 0, 2, 2); } } catch (e) {} if (hadLowOpacity) video.style.setProperty("opacity", "0.01", "important"); if (wasPaused) video.pause(); return true; } async function PictureInPictureControl_reloadPipVideo(pipWindow, pipVideo) { const sourceVideo = pipWindow?.__pip_source_video__ || document.getElementById("__video2"); if (!sourceVideo || !pipVideo) return false; await PictureInPictureControl_wakeSourceVideo(sourceVideo); try { const oldStream = pipVideo.srcObject; if (oldStream) oldStream.getTracks().forEach(track => track.stop()); } catch (e) {} let stream = null; try { stream = sourceVideo.captureStream(); } catch (e) { return false; } if (pipVideo.srcObject = stream, sourceVideo.paused) pipVideo.pause(); else await pipVideo.play().catch(() => {}); try { if (pipWindow && !pipWindow.closed) pipWindow.focus(); } catch (e) {} return true; } function PictureInPictureControl_isTabAntiFreezeEnabled() { return "function" === typeof isTabSwitchEnabled && isTabSwitchEnabled(); } function PictureInPictureControl_startTabAntiFreeze(video, pipWindow, pipVideo) { if (!PictureInPictureControl_isTabAntiFreezeEnabled()) return; PictureInPictureControl_startSourceVideoKeepAlive(video), PictureInPictureControl_bindVisibilityRecover(video, pipWindow, pipVideo), PictureInPictureControl_startFrameStallWatch(video, pipWindow, pipVideo); } function PictureInPictureControl_stopTabAntiFreeze() { if (PictureInPictureControl_stopSourceVideoKeepAlive(), pipVisRecoverHandler) document.removeEventListener("visibilitychange", pipVisRecoverHandler), pipVisRecoverHandler = null; if (pipFrameStallTimer) clearTimeout(pipFrameStallTimer), pipFrameStallTimer = null; } function PictureInPictureControl_bindVisibilityRecover(video, pipWindow, pipVideo) { if (pipVisRecoverHandler) document.removeEventListener("visibilitychange", pipVisRecoverHandler), pipVisRecoverHandler = null; pipVisRecoverHandler = () => { if (!window.__pip_is_active__ || "visible" !== document.visibilityState) return; PictureInPictureControl_reloadPipVideo(pipWindow, pipVideo); }, document.addEventListener("visibilitychange", pipVisRecoverHandler); } function PictureInPictureControl_startFrameStallWatch(video, pipWindow, pipVideo) { if (pipFrameStallTimer) clearTimeout(pipFrameStallTimer), pipFrameStallTimer = null; if ("function" !== typeof video.requestVideoFrameCallback) return; let lastFrameMs = performance.now(); let recovering = false; const onFrame = () => { if (!window.__pip_is_active__) return; lastFrameMs = performance.now(), video.requestVideoFrameCallback(onFrame); }; video.requestVideoFrameCallback(onFrame); const checkStall = () => { if (!window.__pip_is_active__) return; if (pipFrameStallTimer = setTimeout(checkStall, 2500), recovering || video.paused || video.readyState < 2) return; if (performance.now() - lastFrameMs < 4500) return; recovering = true, PictureInPictureControl_reloadPipVideo(pipWindow, pipVideo).finally(() => { lastFrameMs = performance.now(), recovering = false; }); }; pipFrameStallTimer = setTimeout(checkStall, 2500); } function PictureInPictureControl_openPipInputPanel(pipWindow) { const win = pipWindow || window.__pip_window__; if (!win || win.closed) return; const inputPanel = win.document.getElementById("input-panel"); const inputField = win.document.getElementById("pip-input-field"); if (!inputPanel || !inputField) return; inputPanel.classList.add("active"), inputField.focus(); } function PictureInPictureControl_getMenuHtml() { return `
    `; } function initPkg_PictureInPictureControl() { initPkg_PictureInPictureControl_Dom(); } function initPkg_PictureInPictureControl_Dom() { if (PictureInPictureControl_ensureMenuPanel(), PictureInPictureControl_startDocObserver(), !pipFullscreenListenerBound) pipFullscreenListenerBound = true, document.addEventListener("fullscreenchange", PictureInPictureControl_onFullscreenChange, true), document.addEventListener("webkitfullscreenchange", PictureInPictureControl_onFullscreenChange, true), document.addEventListener("mozfullscreenchange", PictureInPictureControl_onFullscreenChange, true), document.addEventListener("MSFullscreenChange", PictureInPictureControl_onFullscreenChange, true); PictureInPictureControl_resetControlbarHook(), PictureInPictureControl_tryBindPipButton(); } function PictureInPictureControl_resetControlbarHook() { if (pipControlbarHook) pipControlbarHook.closeHook(), pipControlbarHook = null; const controlbar = document.getElementById("js-player-controlbar"); if (!controlbar) return; pipControlbarHook = new DomHook("#js-player-controlbar", true, PictureInPictureControl_onControlbarMutation); } function PictureInPictureControl_isPipBindingValid() { const bar = document.getElementById("js-player-controlbar"); return !!(pipNativeTriggerBtn && pipNativeTriggerBtn.isConnected && bar && bar.contains(pipNativeTriggerBtn)); } function PictureInPictureControl_stopDocObserver() { if (pipDocObserver) { if ("function" === typeof pipDocObserver.closeHook) pipDocObserver.closeHook(); else pipDocObserver.disconnect?.(); pipDocObserver = null; } if (null != pipDocSyncRaf) cancelAnimationFrame(pipDocSyncRaf), pipDocSyncRaf = null; } function PictureInPictureControl_startDocObserver() { if (pipDocObserver || PictureInPictureControl_isPipBindingValid()) return; const targetSelector = document.getElementById("js-player-controlbar") ? "#js-player-controlbar" : document.querySelector(".layout-Player") ? ".layout-Player" : null; if (!targetSelector) return setTimeout(PictureInPictureControl_startDocObserver, 1e3), void 0; pipDocObserver = new DomHook(targetSelector, true, records => { if (PictureInPictureControl_isPipBindingValid()) return PictureInPictureControl_stopDocObserver(), void 0; let pipTooltipAdded = false; for (var rec of records) { for (var node of rec.addedNodes) { if (PictureInPictureControl_isPipTooltipNode(node)) { pipTooltipAdded = true, PictureInPictureControl_suppressPipTooltipElement(node); break; } if (1 !== node.nodeType) continue; for (var child of node.children) if (PictureInPictureControl_isPipTooltipNode(child)) { pipTooltipAdded = true, PictureInPictureControl_suppressPipTooltipElement(child); break; } if (pipTooltipAdded) break; } if (pipTooltipAdded) break; } if (!pipTooltipAdded) return; PictureInPictureControl_scheduleDocSync(); }); } function PictureInPictureControl_isPipTooltipNode(node) { return 1 === node?.nodeType && node.classList?.contains("mantine-Tooltip-tooltip") && "开启画中画" === (node.textContent || "").trim(); } function PictureInPictureControl_scheduleDocSync() { if (PictureInPictureControl_isPipBindingValid()) return; if (null != pipDocSyncRaf) return; pipDocSyncRaf = requestAnimationFrame(() => { if (pipDocSyncRaf = null, PictureInPictureControl_isPipBindingValid()) return; PictureInPictureControl_tryBindPipButton(), PictureInPictureControl_showMenuIfAnchorHovered(); }); } function PictureInPictureControl_scheduleControlbarSync() { if (PictureInPictureControl_isPipBindingValid()) return; if (null != pipControlbarSyncRaf) return; pipControlbarSyncRaf = requestAnimationFrame(() => { pipControlbarSyncRaf = null, PictureInPictureControl_syncControlbar(); }); } function PictureInPictureControl_syncControlbar() { if (PictureInPictureControl_isPipBindingValid()) return PictureInPictureControl_stopDocObserver(), void 0; if (pipNativeTriggerBtn && !pipNativeTriggerBtn.isConnected) pipNativeTriggerBtn = null; if (PictureInPictureControl_tryBindPipButton(), PictureInPictureControl_isPipBindingValid()) PictureInPictureControl_stopDocObserver(); } function PictureInPictureControl_onControlbarMutation(mutations) { if (PictureInPictureControl_isPipBindingValid()) return; if (PictureInPictureControl_resetControlbarHook(), pipNativeTriggerBtn && !pipNativeTriggerBtn.isConnected) pipNativeTriggerBtn = null; if (mutations && !PictureInPictureControl_mutationsMayAffectPip(mutations)) return; PictureInPictureControl_startDocObserver(), PictureInPictureControl_scheduleControlbarSync(); } function PictureInPictureControl_mutationsMayAffectPip(mutations) { for (var m of mutations) if ("childList" === m.type) { for (var node of m.addedNodes) if (PictureInPictureControl_nodeMayBePipRelated(node)) return true; for (const node of m.removedNodes) { if (node === pipNativeTriggerBtn) return true; if (1 === node.nodeType && pipNativeTriggerBtn && node.contains(pipNativeTriggerBtn)) return true; } } else if ("attributes" === m.type) { const target = m.target; if (target === pipNativeTriggerBtn) return true; if (1 === target.nodeType && ("aria-label" === m.attributeName || "title" === m.attributeName || "aria-describedby" === m.attributeName)) { const label = (target.getAttribute("aria-label") || target.title || "").trim(); if (label.includes("画中画")) return true; } } return false; } function PictureInPictureControl_nodeMayBePipRelated(node) { if (1 !== node.nodeType) return false; if (node.matches?.("button, [role='button']")) { const label = (node.getAttribute("aria-label") || node.title || "").trim(); if (label.includes("画中画")) return true; } return !!node.querySelector?.("button[aria-label*='画中画'], [role='button'][aria-label*='画中画']"); } function PictureInPictureControl_findNativePipButton() { const bar = document.getElementById("js-player-controlbar"); if (!bar) return null; const candidates = bar.querySelectorAll("button, [role='button']"); for (var btn of candidates) { const label = (btn.getAttribute("aria-label") || btn.title || "").trim(); if (label.includes("画中画")) return btn; } return null; } function PictureInPictureControl_findNativePipButtonFromTooltip() { const tooltips = document.querySelectorAll(".mantine-Tooltip-tooltip"); for (var tooltip of tooltips) { if ("开启画中画" !== (tooltip.textContent || "").trim() || !tooltip.id) continue; const btn = document.querySelector(`[aria-describedby="${tooltip.id}"]`); if (btn) return btn; } return null; } function PictureInPictureControl_tryBindPipButton() { let btn = PictureInPictureControl_findNativePipButton(); if (!btn) btn = PictureInPictureControl_findNativePipButtonFromTooltip(); if (!btn || "1" === btn.dataset.exPipBound) return; btn.dataset.exPipBound = "1", pipNativeTriggerBtn = btn, btn.addEventListener("mouseenter", PictureInPictureControl_onPipBtnEnter), btn.addEventListener("mouseleave", PictureInPictureControl_scheduleHideMenu), btn.addEventListener("focus", PictureInPictureControl_onPipBtnEnter), btn.addEventListener("blur", PictureInPictureControl_scheduleHideMenu), PictureInPictureControl_suppressNativeTooltip(btn), PictureInPictureControl_stopDocObserver(), PictureInPictureControl_showMenuIfAnchorHovered(); } function PictureInPictureControl_showMenuIfAnchorHovered() { const btn = pipNativeTriggerBtn; if (btn && btn.matches(":hover")) pipMenuAnchorBtn = btn, PictureInPictureControl_suppressNativeTooltip(btn), PictureInPictureControl_showMenu(btn); } function PictureInPictureControl_ensureMenuPanel() { if (document.getElementById("ex-pip-menu-panel")) return; const panel = document.createElement("div"); panel.id = "ex-pip-menu-panel", panel.className = "ex-pip-menu-root", panel.setAttribute("role", "menu"), panel.innerHTML = PictureInPictureControl_getMenuHtml(), document.body.appendChild(panel), panel.addEventListener("mouseenter", PictureInPictureControl_cancelHideMenu), panel.addEventListener("mouseleave", PictureInPictureControl_scheduleHideMenu), panel.querySelector('[data-ex-pip-mode="native"]').addEventListener("click", e => { e.preventDefault(), e.stopPropagation(), PictureInPictureControl_hideMenu(), PictureInPictureControl_launchNative(); }), panel.querySelector('[data-ex-pip-mode="enhanced"]').addEventListener("click", e => { e.preventDefault(), e.stopPropagation(), PictureInPictureControl_hideMenu(), PictureInPictureControl_launchEnhanced(); }); } function PictureInPictureControl_onPipBtnEnter(e) { pipMenuAnchorBtn = e.currentTarget, PictureInPictureControl_suppressNativeTooltip(pipMenuAnchorBtn), PictureInPictureControl_showMenu(pipMenuAnchorBtn); } function PictureInPictureControl_onFullscreenChange() { PictureInPictureControl_ensureMenuPanel(), PictureInPictureControl_resetControlbarHook(), PictureInPictureControl_startDocObserver(), PictureInPictureControl_scheduleDocSync(), setTimeout(() => { PictureInPictureControl_scheduleDocSync(); }, 250); } function PictureInPictureControl_showMenu(anchorBtn) { PictureInPictureControl_ensureMenuPanel(), PictureInPictureControl_cancelHideMenu(); const panel = document.getElementById("ex-pip-menu-panel"); if (!panel || !anchorBtn) return; panel.classList.add("is-visible", "is-measuring"), panel.style.removeProperty("visibility"), panel.style.left = "-9999px", panel.style.top = "0"; const rect = anchorBtn.getBoundingClientRect(); const pw = panel.offsetWidth; const ph = panel.offsetHeight; const gap = 4; let left = rect.left + rect.width / 2 - pw / 2; let top = rect.top - ph - gap; if (left = Math.max(8, Math.min(left, window.innerWidth - pw - 8)), top < 8) top = rect.bottom + gap; panel.classList.remove("is-measuring"), panel.style.left = left + `px`, panel.style.top = top + `px`; } function PictureInPictureControl_hideMenu() { const panel = document.getElementById("ex-pip-menu-panel"); if (panel) panel.classList.remove("is-visible", "is-measuring"); pipMenuAnchorBtn = null; } function PictureInPictureControl_cancelHideMenu() { if (pipMenuHideTimer) clearTimeout(pipMenuHideTimer), pipMenuHideTimer = null; } function PictureInPictureControl_scheduleHideMenu() { PictureInPictureControl_cancelHideMenu(), pipMenuHideTimer = setTimeout(() => { const panel = document.getElementById("ex-pip-menu-panel"); if (!panel || !panel.classList.contains("is-visible")) return; if (panel.matches(":hover")) return; if (pipMenuAnchorBtn && pipMenuAnchorBtn.matches(":hover")) return; PictureInPictureControl_hideMenu(); }, 180); } function PictureInPictureControl_suppressPipTooltipElement(el) { if (!el || "1" === el.dataset.exPipTooltipHidden) return; el.dataset.exPipTooltipHidden = "1", el.style.setProperty("display", "none", "important"); } function PictureInPictureControl_suppressNativeTooltip(btn) { if (btn) { const tipId = btn.getAttribute("aria-describedby"); if (tipId) { const tip = document.getElementById(tipId); if (tip && "开启画中画" === (tip.textContent || "").trim()) return PictureInPictureControl_suppressPipTooltipElement(tip), void 0; } } document.querySelectorAll(".mantine-Tooltip-tooltip").forEach(el => { if ("开启画中画" === (el.textContent || "").trim()) PictureInPictureControl_suppressPipTooltipElement(el); }); } function PictureInPictureControl_launchNative() { const btn = pipNativeTriggerBtn || PictureInPictureControl_findNativePipButton(); if (btn) return btn.click(), void 0; const video = document.getElementById("__video2") || document.querySelector(".layout-Player-videoEntity video"); if (video && "function" === typeof video.requestPictureInPicture) return video.requestPictureInPicture().catch(() => { showMessage("【画中画】无法开启原版画中画", "error"); }), void 0; showMessage("【画中画】未找到原版画中画按钮", "error"); } function PictureInPictureControl_launchEnhanced() { const video = document.getElementById("__video2"); if (!video) return showMessage("【画中画增强】当前直播间不支持画中画增强功能", "error"), void 0; if (!window.documentPictureInPicture) return showMessage("【画中画增强】当前浏览器不支持画中画增强功能,建议使用 Chrome 116+ 或 Edge 116+", "error"), void 0; const saved = localStorage.getItem("ExSave_PipSet"); if (saved) try { const obj = JSON.parse(saved); Object.assign(pipConfig, obj); } catch (e) {} PictureInPictureControl_handle(); } function PictureInPictureControl_toggleMainVideoVisibility(hide) { const mainVideo = document.getElementById("__video2"); if (!mainVideo) return; if (hide) mainVideo.style.setProperty("opacity", "0.01", "important"), mainVideo.style.setProperty("pointer-events", "none", "important"); else mainVideo.style.removeProperty("opacity"), mainVideo.style.removeProperty("pointer-events"); } function PictureInPictureControl_toggleSourcePagePower(enable) { const powerHogs = [ ".layout-Player-video", ".layout-Player-videoEntity", ".room-html5-player", ".DiamondsFansRankList", ".wm-view", ".wm-tabv2", ".comment-37342a", ".DanmuEffectDom", ".layout-Player-asideMainTop" ]; const freezeWhitelists = [ ".ChatSend-txt", ".ChatSend-button" ]; const legacyRestoreSelectors = [ ".layout-Player", ".Barrage-list" ]; if (powerHogs.forEach(selector => { const el = document.querySelector(selector); if (!el) return; if (enable) el.style.setProperty("display", "none", "important"); else el.style.removeProperty("display"); }), freezeWhitelists.forEach(selector => { const el = document.querySelector(selector); if (!el) return; if (enable) el.style.setProperty("opacity", "0.01", "important"), el.style.setProperty("pointer-events", "none", "important"); else el.style.removeProperty("opacity"), el.style.removeProperty("pointer-events"); }), !enable) legacyRestoreSelectors.forEach(selector => { const el = document.querySelector(selector); if (el) el.style.removeProperty("display"); }); } function PictureInPictureControl_closePipWebSocket() { if (!pip_ws_instance) return; const ws = pip_ws_instance; pip_ws_instance = null, ws.msgHandler = () => {}; try { ws.close(); } catch (e) {} } async function PictureInPictureControl_handle() { const video = document.getElementById("__video2"); PictureInPictureControl_closePipWebSocket(), PictureInPictureControl_clearWsDedup(), window.__pip_track_state__ = []; const pipWindow = await documentPictureInPicture.requestWindow({ width: 670, height: 380, disallowReturnToOpener: true, preferInitialWindowPlacement: true }); pipWindow.document.body.innerHTML = PictureInPictureControl_getTemplate(), window.__pip_window__ = pipWindow, pipWindow.__pip_source_video__ = video; const pipVideo = pipWindow.document.getElementById("pip-video"); const danmakuLayer = pipWindow.document.getElementById("danmaku"); const mainView = pipWindow.document.getElementById("main-view"); const inputPanel = pipWindow.document.getElementById("input-panel"); const setBtn = pipWindow.document.getElementById("pip-set"); const sendBtn = pipWindow.document.getElementById("pip-send"); const dmToggleBtn = pipWindow.document.getElementById("pip-danmaku-toggle"); const reloadBtn = pipWindow.document.getElementById("pip-reload"); const backOpenerBtn = pipWindow.document.getElementById("pip-back-opener"); const toast = pipWindow.document.getElementById("pip-toast"); if (PictureInPictureControl_applyDanmakuStyle(pipWindow), PictureInPictureControl_initPipUiText(pipWindow), PictureInPictureControl_toggleSourcePagePower(false), PictureInPictureControl_toggleMainVideoVisibility(true), true === pipConfig.lowPowerMode) PictureInPictureControl_toggleSourcePagePower(true); if (pipVideo.srcObject = video.captureStream(), await pipVideo.play().catch(() => {}), dmToggleBtn) dmToggleBtn.addEventListener("click", e => { e.stopPropagation(), pipConfig.danmakuVisible = false === pipConfig.danmakuVisible, localStorage.setItem("ExSave_PipSet", JSON.stringify(pipConfig)), PictureInPictureControl_applyDanmakuStyle(pipWindow), PictureInPictureControl_updateDanmakuToggleBtn(pipWindow); }); if (reloadBtn) reloadBtn.addEventListener("click", async e => { e.stopPropagation(); const ok = await PictureInPictureControl_reloadPipVideo(pipWindow, pipVideo); toast.innerText = ok ? "画面已刷新" : "刷新失败,请检查主页视频", toast.classList.add("show"), clearTimeout(toast._timer), toast._timer = setTimeout(() => toast.classList.remove("show"), 2e3); }); if (backOpenerBtn) backOpenerBtn.addEventListener("click", e => { e.stopPropagation(), PictureInPictureControl_returnToOpener(pipWindow); }); setBtn.addEventListener("click", e => { e.stopPropagation(), PictureInPictureControl_returnToOpener(pipWindow, false), PictureInPictureControl_openSettingPanel(), toast.innerText = "已在斗鱼直播页面打开设置面板", toast.classList.add("show"), clearTimeout(toast._timer), toast._timer = setTimeout(() => toast.classList.remove("show"), 5e3); }), sendBtn.addEventListener("click", e => { if (e.stopPropagation(), inputPanel.classList.contains("active")) inputPanel.classList.remove("active"); else PictureInPictureControl_openPipInputPanel(pipWindow); }); const onPipKeydown = e => { if ("Enter" !== e.key) return; const inputField = pipWindow.document.getElementById("pip-input-field"); if (pipWindow.document.activeElement === inputField) return; e.preventDefault(), PictureInPictureControl_openPipInputPanel(pipWindow); }; pipWindow.document.addEventListener("keydown", onPipKeydown), pipWindow.__pip_keydown_handler__ = onPipKeydown, mainView.addEventListener("click", () => { if (inputPanel.classList.contains("active")) inputPanel.classList.remove("active"); }), PictureInPictureControl_bindSendEvents(pipWindow, danmakuLayer), PictureInPictureControl_bindVideoSync(video, pipVideo), PictureInPictureControl_startTabAntiFreeze(video, pipWindow, pipVideo), PictureInPictureControl_startComboCleaner(), pip_ws_instance = new Ex_WebSocket_UnLogin(rid, ret => { if (!window.__pip_is_active__) return; if (PictureInPictureControl_isDuplicateWsPacket(ret)) return; const msg = PictureInPictureControl_parseWSMsg(ret); PictureInPictureControl_handleComboAndRender(msg, pipWindow, danmakuLayer); }), PictureInPictureControl_bindCleanup(video, pipWindow, pipVideo); } function PictureInPictureControl_bindVideoSync(video, pipVideo) { function syncVideo() { if (video.paused) pipVideo.pause(); else pipVideo.play().catch(() => {}); } video.addEventListener("play", syncVideo), video.addEventListener("pause", syncVideo); } function PictureInPictureControl_bindCleanup(video, pipWindow, pipVideo) { window.__pip_is_active__ = true; const cleanup = () => PictureInPictureControl_cleanupEnhancedPip(pipWindow, pipVideo); pipWindow.addEventListener("pagehide", cleanup); const timer = setInterval(() => { if (pipWindow.closed) clearInterval(timer), cleanup(); }, 2e3); } const danmakuTracks = []; const comboMap = new Map(); let comboCleanerTimer = null; const pipWsPacketDedup = new Map(); const PIP_COMBO_MAX_VISIBLE = 2; const PIP_COMBO_TEXT_MAX = 18; const PIP_WS_DEDUP_MS = 3e3; const PIP_WS_DEDUP_MAX = 800; function PictureInPictureControl_clearWsDedup() { pipWsPacketDedup.clear(); } function PictureInPictureControl_isDuplicateWsPacket(ret) { const now = Date.now(); const last = pipWsPacketDedup.get(ret); if (null != last && now - last < PIP_WS_DEDUP_MS) return true; if (pipWsPacketDedup.set(ret, now), pipWsPacketDedup.size > PIP_WS_DEDUP_MAX) for (var [ key, ts ] of pipWsPacketDedup) if (now - ts > PIP_WS_DEDUP_MS) pipWsPacketDedup.delete(key); return false; } function PictureInPictureControl_getPipWindow() { const w = window.__pip_window__; return w && !w.closed ? w : null; } function PictureInPictureControl_truncateComboText(text, maxLen = PIP_COMBO_TEXT_MAX) { if (!text || text.length <= maxLen) return text || ""; return text.slice(0, maxLen) + "…"; } function PictureInPictureControl_refreshComboDisplay(pipWindow) { const win = pipWindow || PictureInPictureControl_getPipWindow(); const container = win?.document.getElementById("combo-container"); if (!container) return; const now = Date.now(); const active = []; for (var [ key, info ] of comboMap.entries()) { const count = info.timestamps.filter(t => now - t <= 8e3).length; if (count < 2) { info.dom = null; continue; } info.displayCount = count; const lastTs = info.timestamps[info.timestamps.length - 1] || 0; active.push({ key: key, info: info, count: count, lastTs: lastTs }); } active.sort((a, b) => b.count - a.count || b.lastTs - a.lastTs), container.innerHTML = ""; for (const [ , info ] of comboMap) info.dom = null; const visible = active.slice(0, PIP_COMBO_MAX_VISIBLE); const overflow = active.length - visible.length; for (const { key, info, count } of visible) { const el = win.document.createElement("div"); el.className = "combo-item", el.title = key, el.innerHTML = `${PictureInPictureControl_truncateComboText(key)}×${count}`, container.appendChild(el), info.dom = el; } if (overflow > 0) { const more = win.document.createElement("div"); more.className = "combo-item combo-item--more", more.textContent = `+${overflow} 组重复`, container.appendChild(more); } } function PictureInPictureControl_parseWSMsg(ret) { if (!ret || !ret.startsWith("type@=chatmsg/")) return null; const obj = {}; const parts = ret.split("/"); for (let i = 0; i < parts.length; i++) { const p = parts[i]; const idx = p.indexOf("@="); if (-1 === idx) continue; obj[p.substring(0, idx)] = p.substring(idx + 2); } const txt = obj.txt ? decodeURIComponent(obj.txt) : ""; if (!txt) return null; if (false !== pipConfig.filterRobotDanmaku && !obj.dms) return null; const msgId = obj.hash || obj.cid || obj.dmid || ""; return { text: txt, color: obj.col ? parseInt(obj.col) : 0, uid: obj.uid || "", msgId: msgId, dedupKey: msgId || obj.uid + `|` + txt }; } function PictureInPictureControl_getDanmakuColor(col) { switch (col) { case 1: return "#ff3b30"; case 2: return "#0a84ff"; case 3: return "#34c759"; case 4: return "#ff9500"; case 5: return "#af52de"; case 6: return "#ff2d55"; default: return "#ffffff"; } } function PictureInPictureControl_getTrack(pipWindow, textWidth) { let maxTracks = Math.floor(pipWindow.innerHeight / pipConfig.trackHeight); if ("half" === pipConfig.area) maxTracks = Math.floor(maxTracks / 2); else if ("quarter" === pipConfig.area) maxTracks = Math.floor(maxTracks / 4); if (maxTracks = Math.max(1, maxTracks), !window.__pip_track_state__) window.__pip_track_state__ = []; const state = window.__pip_track_state__; const screenWidth = pipWindow.innerWidth; const baseDuration = 15 / pipConfig.speed; const duration = Math.max(.7 * baseDuration, Math.min(1.4 * baseDuration, baseDuration + textWidth / 120 / pipConfig.speed)); const currentSpeed = (screenWidth + textWidth) / duration; let bestTrack = -1; for (let i = 0; i < maxTracks; i++) { const lastDanmaku = state[i]; if (!lastDanmaku) return state[i] = { textWidth: textWidth, speed: currentSpeed, startTime: Date.now(), duration: 1e3 * duration }, i; const elapsed = Date.now() - lastDanmaku.startTime; if (elapsed >= lastDanmaku.duration) return state[i] = { textWidth: textWidth, speed: currentSpeed, startTime: Date.now(), duration: 1e3 * duration }, i; const lastDistance = lastDanmaku.speed * (elapsed / 1e3); const lastHeadX = screenWidth - lastDistance; const lastTailX = lastHeadX + lastDanmaku.textWidth; const safeGap = 16; if (lastTailX > screenWidth - safeGap) continue; if (currentSpeed > lastDanmaku.speed) { const timeLeftForLast = lastDanmaku.duration - elapsed; const catchUpTime = lastHeadX / (currentSpeed - lastDanmaku.speed); if (1e3 * catchUpTime < timeLeftForLast) continue; } bestTrack = i; break; } if (-1 === bestTrack) { let minTailX = 1 / 0; for (let i = 0; i < maxTracks; i++) { const ld = state[i]; if (!ld) { bestTrack = i; break; } const elapsed = Date.now() - ld.startTime; const lastTailX = screenWidth - ld.speed * (elapsed / 1e3) + ld.textWidth; if (lastTailX < minTailX) minTailX = lastTailX, bestTrack = i; } } return state[bestTrack] = { textWidth: textWidth, speed: currentSpeed, startTime: Date.now(), duration: 1e3 * duration }, bestTrack; } function PictureInPictureControl_findMatchKey(text) { if (comboMap.has(text)) return text; const getCleanKey = str => str.replace(/\s+/g, "").split("").filter((v, i, a) => a.indexOf(v) === i).join(""); const currentClean = getCleanKey(text); if (!currentClean) return text; for (var existingKey of comboMap.keys()) { const existingClean = getCleanKey(existingKey); if (currentClean === existingClean) if (Math.abs(text.length - existingKey.length) <= 6) return existingKey; if (text.includes(existingKey) || existingKey.includes(text)) if (Math.abs(text.length - existingKey.length) <= 4) return existingKey; } return text; } function PictureInPictureControl_startComboCleaner() { if (comboCleanerTimer) clearInterval(comboCleanerTimer); comboCleanerTimer = setInterval(() => { const now = Date.now(); for (var [ key, info ] of comboMap.entries()) { const recentCount = info.timestamps.filter(t => now - t <= 8e3).length; if (recentCount < 2) comboMap.delete(key); } PictureInPictureControl_refreshComboDisplay(); }, 1e3); } function PictureInPictureControl_handleComboAndRender(msg, pipWindow, danmakuLayer) { if (!msg || !msg.text) return; if (false === pipConfig.danmakuVisible) return; if (my_uid && msg.uid === my_uid) return; const currentMode = pipConfig.mergeMode || "combo"; if ("all" === currentMode) return PictureInPictureControl_renderDanmaku(msg, pipWindow, danmakuLayer), void 0; const now = Date.now(); const matchedKey = PictureInPictureControl_findMatchKey(msg.text); if (!comboMap.has(matchedKey)) comboMap.set(matchedKey, { timestamps: [], dom: null, displayCount: 0 }); const info = comboMap.get(matchedKey); if (info.timestamps.push(now), "single" === currentMode) { const singleFrequency = info.timestamps.filter(t => now - t <= 4e3).length; if (singleFrequency > 1) return; return PictureInPictureControl_renderDanmaku(msg, pipWindow, danmakuLayer), void 0; } info.timestamps = info.timestamps.filter(t => now - t <= 8e3); const recentFrequency = info.timestamps.length; if (1 === recentFrequency) return info.displayCount = 0, PictureInPictureControl_renderDanmaku(msg, pipWindow, danmakuLayer), PictureInPictureControl_refreshComboDisplay(pipWindow), void 0; info.displayCount = recentFrequency, PictureInPictureControl_refreshComboDisplay(pipWindow); } function PictureInPictureControl_renderDanmaku(msg, pipWindow, danmakuLayer, isSelf = false) { if (!msg || !msg.text) return; const el = pipWindow.document.createElement("div"); el.className = "dm" + (isSelf ? " dm-self" : ""), el.innerText = msg.text, el.style.fontSize = pipConfig.fontSize + "px", el.style.color = isSelf ? "#00ff66" : PictureInPictureControl_getDanmakuColor(msg.color), el.style.visibility = "hidden", danmakuLayer.appendChild(el); const textWidth = el.offsetWidth; const screenWidth = pipWindow.innerWidth; const track = PictureInPictureControl_getTrack(pipWindow, textWidth); const y = track * pipConfig.trackHeight; const currentTrackState = window.__pip_track_state__[track]; const animDuration = currentTrackState.duration / 1e3; el.style.top = y + "px", el.style.left = screenWidth + "px", el.style.visibility = "visible"; const animationName = `exPipMove_` + Math.random().toString(36).substring(2, 9); const pipDoc = pipWindow.document; let styleSheet = pipDoc.getElementById("ex-danmaku-styles"); if (!styleSheet) styleSheet = pipDoc.createElement("style"), styleSheet.id = "ex-danmaku-styles", pipDoc.head.appendChild(styleSheet); styleSheet.sheet.insertRule(` @keyframes ${animationName} { from { transform: translateX(0); } to { transform: translateX(-${screenWidth + textWidth + 30}px); } } `, 0), el.style.animation = `${animationName} ${animDuration}s linear forwards`, el.addEventListener("animationend", () => { el.remove(); try { const sheet = styleSheet.sheet; for (let i = 0; i < sheet.cssRules.length; i++) if (sheet.cssRules[i].name === animationName) { sheet.deleteRule(i); break; } } catch (e) {} }); } function PictureInPictureControl_bindSendEvents(pipWindow, danmakuLayer) { const inputField = pipWindow.document.getElementById("pip-input-field"); const submitBtn = pipWindow.document.getElementById("pip-submit-btn"); const panel = pipWindow.document.getElementById("input-panel"); async function doSubmit() { const content = inputField.value.trim(); if (!content) return; inputField.value = "", panel.classList.remove("active"); const toast = pipWindow.document.getElementById("pip-toast"); toast.innerText = "发送成功", toast.classList.add("show"), clearTimeout(toast._timer), toast._timer = setTimeout(() => toast.classList.remove("show"), 2e3), PictureInPictureControl_renderDanmaku({ text: content, color: 0 }, pipWindow, danmakuLayer, true); try { await PictureInPictureControl_SendDanmaku(content); } catch (err) { console.error("弹幕发送接口调用失败:", err); } } inputField.addEventListener("keydown", e => { if ("Enter" === e.key) doSubmit(); }), submitBtn.addEventListener("click", () => { doSubmit(); }); } async function PictureInPictureControl_SendDanmaku(text) { let textarea = document.querySelector("div.ChatSend-txt"); const button = document.querySelector(".ChatSend-button"); return textarea.innerText = text, button.click(), true; } function PictureInPictureControl_openSettingPanel() { let panel = document.getElementById("pip-setting-panel"); const defaultConfigs = { fontSize: 18, speed: 2.5, area: "full", trackHeight: 28, mergeMode: "combo", lowPowerMode: false, filterRobotDanmaku: true, opacity: 1, danmakuVisible: true }; const save = () => { localStorage.setItem("ExSave_PipSet", JSON.stringify(pipConfig)); }; const refreshPanelUI = () => { document.getElementById("pip-area").value = pipConfig.area, document.getElementById("pip-mergemode").value = pipConfig.mergeMode || "combo", document.getElementById("pip-lowpowermode").value = true === pipConfig.lowPowerMode ? "on" : "off", document.getElementById("pip-filterrobot").value = false !== pipConfig.filterRobotDanmaku ? "on" : "off", document.getElementById("pip-tabswitch").value = isTabSwitchEnabled() ? "on" : "off", document.getElementById("pip-fontsize").value = pipConfig.fontSize, document.getElementById("pip-fontsize-value").innerText = pipConfig.fontSize, document.getElementById("pip-trackheight").value = pipConfig.trackHeight || 28, document.getElementById("pip-trackheight-value").innerText = pipConfig.trackHeight || 28, document.getElementById("pip-speed").value = pipConfig.speed, document.getElementById("pip-speed-value").innerText = pipConfig.speed; const opacityPct = Math.round(100 * (null != pipConfig.opacity ? pipConfig.opacity : 1)); document.getElementById("pip-opacity").value = opacityPct, document.getElementById("pip-opacity-value").innerText = opacityPct + "%"; }; if (panel) return panel.style.display = "block", refreshPanelUI(), void 0; if (panel = document.createElement("div"), panel.id = "pip-setting-panel", panel.innerHTML = `
    画中画弹幕设置
    恢复默认
    弹幕字号 ${pipConfig.fontSize}
    弹幕上下间距 ${pipConfig.trackHeight || 28}
    弹幕速度 ${pipConfig.speed}
    弹幕透明度 ${Math.round(100 * (null != pipConfig.opacity ? pipConfig.opacity : 1))}%
    弹幕显示区域
    重复弹幕合并
    短时间内多条相同弹幕内容时的显示方式
    屏蔽机器人弹幕
    开启后过滤无用户标识的机器人弹幕
    原网页低功耗
    开启后,拉起画中画时将隐藏原网页视频区、飘屏弹幕与礼物动画,保留右侧弹幕列表,以降低 CPU 占用
    页签防冻结
    与扩展工具「防页签冻结」共用设置;开启后画中画期间保持源页解码并自动缓解卡屏(关闭需刷新页面后完全生效)
    `, !document.getElementById("pip-setting-style")) { const style = document.createElement("style"); style.id = "pip-setting-style", style.innerHTML = ` #pip-setting-panel { position: fixed; width: 440px; background: rgba(255, 255, 255, 0.98); border: 1px solid rgba(212, 212, 216, 1); box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15); border-radius: 12px; padding: 24px; z-index: 999999; font-family: "Helvetica Neue", Helvetica, Arial, "Microsoft Yahei", sans-serif; color: #18181b; box-sizing: border-box; backdrop-filter: blur(10px); } .pip-setting-header { display: flex; align-items: center; justify-content: space-between; margin: -24px -24px 24px; padding: 24px 24px 0; cursor: move; user-select: none; } .pip-setting-title { flex: 1; min-width: 0; font-size: 16px; font-weight: 600; color: #000000; letter-spacing: 0.5px; margin: 0; } .pip-setting-header-actions { display: flex; align-items: center; gap: 12px; flex-shrink: 0; } .pip-setting-reset { font-size: 14px; color: #71717a; cursor: pointer; transition: color 0.2s; text-decoration: underline; user-select: none; font-weight: 500; white-space: nowrap; } .pip-setting-reset:hover { color: #ff5d23; } .pip-setting-dismiss { flex-shrink: 0; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: 6px; background: transparent; color: #71717a; font-size: 22px; line-height: 1; cursor: pointer; user-select: none; transition: background 0.2s, color 0.2s; } .pip-setting-dismiss:hover { background: #f4f4f5; color: #18181b; } .pip-setting-item { margin-bottom: 18px; display: flex; align-items: center; font-size: 14px; } .pip-setting-item.item-vertical { margin-bottom: 20px; flex-direction: column; align-items: flex-start; } .item-label-row { width: 100%; display: flex; align-items: center; } .pip-setting-item span:first-child { width: 100px; color: #3f3f46; font-weight: 600; } .pip-setting-item input[type="range"] { flex: 1; margin: 0 14px; -webkit-appearance: none; background: #d4d4d8; height: 4px; border-radius: 2px; outline: none; } .pip-setting-item input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: #ff5d23; cursor: pointer; transition: transform 0.1s; } .pip-setting-item input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.2); } .pip-setting-item select { flex: 1; background: #f4f4f5; color: #18181b; padding: 6px 10px; border-radius: 6px; border: 1px solid #cdcdd6; outline: none; font-size: 14px; cursor: pointer; transition: border-color 0.2s, background 0.2s; font-weight: 500; } .pip-setting-item select:focus { border-color: #ff5d23; background: #ffffff; } .pip-setting-item span:last-child { width: 32px; text-align: right; color: #ff5d23; font-weight: bold; font-family: monospace; } .pip-setting-tip { font-size: 11px; color: #52525b; margin-top: 6px; margin-left: 100px; line-height: 1.4; } `, document.head.appendChild(style); } document.body.appendChild(panel); const left = Math.max(8, (window.innerWidth - panel.offsetWidth) / 2); const top = Math.max(8, (window.innerHeight - panel.offsetHeight) / 2); panel.style.left = left + `px`, panel.style.top = top + `px`, PictureInPictureControl_initSettingPanelDrag(panel); const font = document.getElementById("pip-fontsize"); const track = document.getElementById("pip-trackheight"); const speed = document.getElementById("pip-speed"); const area = document.getElementById("pip-area"); const merge = document.getElementById("pip-mergemode"); const lowPower = document.getElementById("pip-lowpowermode"); const filterRobot = document.getElementById("pip-filterrobot"); const tabSwitch = document.getElementById("pip-tabswitch"); const opacity = document.getElementById("pip-opacity"); area.value = pipConfig.area, merge.value = pipConfig.mergeMode || "combo", lowPower.value = true === pipConfig.lowPowerMode ? "on" : "off", filterRobot.value = false !== pipConfig.filterRobotDanmaku ? "on" : "off", tabSwitch.value = isTabSwitchEnabled() ? "on" : "off", font.addEventListener("input", () => { pipConfig.fontSize = parseInt(font.value), document.getElementById("pip-fontsize-value").innerText = pipConfig.fontSize, save(); }), track.addEventListener("input", () => { pipConfig.trackHeight = parseInt(track.value), document.getElementById("pip-trackheight-value").innerText = pipConfig.trackHeight, save(); }), speed.addEventListener("input", () => { pipConfig.speed = parseFloat(speed.value), document.getElementById("pip-speed-value").innerText = pipConfig.speed, save(); }), opacity.addEventListener("input", () => { if (pipConfig.opacity = parseInt(opacity.value, 10) / 100, document.getElementById("pip-opacity-value").innerText = opacity.value + "%", save(), window.__pip_is_active__) PictureInPictureControl_applyDanmakuStyle(); }), area.addEventListener("change", () => { pipConfig.area = area.value, save(); }), merge.addEventListener("change", () => { pipConfig.mergeMode = merge.value, save(); }), filterRobot.addEventListener("change", () => { pipConfig.filterRobotDanmaku = "on" === filterRobot.value, save(); }), lowPower.addEventListener("change", () => { if (pipConfig.lowPowerMode = "on" === lowPower.value, save(), window.__pip_is_active__) PictureInPictureControl_toggleSourcePagePower(pipConfig.lowPowerMode); }), tabSwitch.addEventListener("change", () => { const enabled = "on" === tabSwitch.value; if (setTabSwitchEnabled(enabled), enabled) enableTabSwitch(); else showMessage("已关闭页签防冻结,请刷新页面后完全生效", "info"); if (window.__pip_is_active__) { const pipWin = window.__pip_window__; const pipVideo = pipWin?.document.getElementById("pip-video"); const sourceVideo = pipWin?.__pip_source_video__; if (enabled) PictureInPictureControl_startTabAntiFreeze(sourceVideo, pipWin, pipVideo); else PictureInPictureControl_stopTabAntiFreeze(); } }), panel.querySelector(".pip-setting-reset").addEventListener("click", () => { if (confirm("确定要将画中画设置恢复为默认配置吗?")) if (Object.assign(pipConfig, defaultConfigs), save(), refreshPanelUI(), window.__pip_is_active__) PictureInPictureControl_toggleSourcePagePower(pipConfig.lowPowerMode), PictureInPictureControl_applyDanmakuStyle(), PictureInPictureControl_updateDanmakuToggleBtn(); }), panel.querySelector(".pip-setting-dismiss").addEventListener("click", () => { panel.style.display = "none"; }); } function PictureInPictureControl_initSettingPanelDrag(panel) { const header = panel.querySelector(".pip-setting-header"); if (!header) return; let dragging = false; let startX = 0; let startY = 0; let startLeft = 0; let startTop = 0; const onMouseMove = e => { if (!dragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; panel.style.left = Math.max(0, startLeft + dx) + `px`, panel.style.top = Math.max(0, startTop + dy) + `px`; }; const onMouseUp = () => { dragging = false, document.removeEventListener("mousemove", onMouseMove), document.removeEventListener("mouseup", onMouseUp); }; header.addEventListener("mousedown", e => { if (0 !== e.button || e.target.closest(".pip-setting-dismiss, .pip-setting-reset")) return; dragging = true; const rect = panel.getBoundingClientRect(); panel.style.left = rect.left + `px`, panel.style.top = rect.top + `px`, startX = e.clientX, startY = e.clientY, startLeft = rect.left, startTop = rect.top, document.addEventListener("mousemove", onMouseMove), document.addEventListener("mouseup", onMouseUp), e.preventDefault(); }); } function PictureInPictureControl_getTemplate() { return `
    ${PIP_BTN_SVG_RELOAD}
    ${icon_pipcontrol_set}
    ${icon_pipcontrol_send}
    `; } let currentBrightness = ""; let currentContrast = ""; let currentSaturate = ""; let liveVideoParentClassName = ""; let isMirror = false; let rotateAngle = 0; let isEnhanceQuality = false; let transformCss = { rotateY: "", rotate: "", scale: "" }; let panorama = null; function isEdgeBrowser() { return /Edg/i.test(navigator.userAgent); } function initPkg_VideoTools_Filter() { liveVideoParentClassName = liveVideoNode.parentNode.className, initPkg_VideoTools_Filter_Dom(), initPkg_VideoTools_Filter_Func(); } function initPkg_VideoTools_Filter_Dom() { if (Filter_insertPanel(), isEdgeBrowser()) Filter_insertEnhanceModal(); } function Filter_getPanelHtml() { return `
    ${isEdgeBrowser() ? `
    画质增强(不掉帧)
    ` : ""}
    明亮度
    对比度
    饱和度

    滤镜

    • 重置
    `; } function Filter_insertPanel() { const host = "function" === typeof VideoToolbarMenu_getFilterHost ? VideoToolbarMenu_getFilterHost() : null; if (!host) return; const wrap = document.createElement("div"); wrap.innerHTML = Filter_getPanelHtml(), host.appendChild(wrap.firstElementChild); let b = document.getElementsByClassName("menu-da2a9e")[0]; if (!b) return; let domPanorama = document.createElement("li"); domPanorama.id = "filter__panorama", domPanorama.innerText = "全景", b.insertBefore(domPanorama, b.childNodes[1]); let domMirror = document.createElement("li"); domMirror.id = "filter__mirror", domMirror.innerText = "镜像画面", b.insertBefore(domMirror, b.childNodes[1]); let domRotate = document.createElement("li"); domRotate.id = "filter__rotate", domRotate.innerText = "旋转画面", b.insertBefore(domRotate, b.childNodes[1]); let domReset = document.createElement("li"); domReset.id = "filter__reset", domReset.innerText = "重置", b.insertBefore(domReset, b.childNodes[1]); let domDivider = document.createElement("div"); domDivider.className = "divider-f9d33d", b.insertBefore(domDivider, b.childNodes[1]); } function Filter_insertEnhanceModal() { let modal = document.createElement("div"); modal.className = "enhance-modal__panel-wrap", modal.innerHTML = `
    ×
    开启后,弹幕飘屏会被遮挡,请将鼠标移入到直播画面中并点击
    完成后,再刷新以恢复弹幕飘屏,该功能会自动保存
    没有增强图标?
    `; let root = document.querySelector("body") || document.body; if (root) root.insertBefore(modal, root.childNodes[0]); let closeBtn = modal.getElementsByClassName("enhance-modal__close")[0]; if (closeBtn) closeBtn.addEventListener("click", () => { modal.style.display = "none"; }); } function initPkg_VideoTools_Filter_Func() { if (isEdgeBrowser()) { const switchEnhance = document.getElementById("switch__enhance"); if (switchEnhance) switchEnhance.addEventListener("click", () => { isEnhanceQuality = !isEnhanceQuality; const switchElement = document.getElementById("switch__enhance"); const slider = document.getElementById("slider__enhance"); const enhanceModal = document.getElementsByClassName("enhance-modal__panel-wrap")[0]; const videoDom = document.querySelector("video"); if (isEnhanceQuality) slider.style.left = "20px", switchElement.style.background = "#369", liveVideoNode.style.imageRendering = "crisp-edges", liveVideoNode.style.imageRendering = "-webkit-optimize-contrast", liveVideoNode.style.imageRendering = "optimize-contrast", enhanceModal.style.display = "flex", videoDom.style.zIndex = "10", videoDom.style.cursor = "auto"; else slider.style.left = "0px", switchElement.style.background = "#ccc", liveVideoNode.style.imageRendering = "", enhanceModal.style.display = "none", videoDom.style.zIndex = "0"; }); } setScrollFunc(document.getElementById("scroll__bright"), document.getElementById("bar__bright"), document.getElementById("mask__bright"), data => { currentBrightness = `brightness(${data}%)`, liveVideoNode.style.filter = `${currentBrightness} ${currentContrast} ` + currentSaturate; }), setScrollFunc(document.getElementById("scroll__contrast"), document.getElementById("bar__contrast"), document.getElementById("mask__contrast"), data => { currentContrast = `contrast(${data}%)`, liveVideoNode.style.filter = `${currentBrightness} ${currentContrast} ` + currentSaturate; }), setScrollFunc(document.getElementById("scroll__saturate"), document.getElementById("bar__saturate"), document.getElementById("mask__saturate"), data => { currentSaturate = `saturate(${data}%)`, liveVideoNode.style.filter = `${currentBrightness} ${currentContrast} ` + currentSaturate; }); let btnReset = document.getElementById("filter__reset"); if (btnReset) btnReset.addEventListener("click", () => { resetVideoFilter(); }); let btnReset2 = document.getElementById("filter__reset2"); if (btnReset2) btnReset2.addEventListener("click", () => { resetVideoFilter(); }); let btnMirror = document.getElementById("filter__mirror"); if (btnMirror) btnMirror.addEventListener("click", () => { if (!isMirror) isMirror = true, transformCss.rotateY = "rotateY(180deg)"; else isMirror = false, transformCss.rotateY = "rotateY(0deg)"; if (liveVideoNode && liveVideoNode.parentNode) liveVideoNode.parentNode.style.transition = "all .5s", liveVideoNode.parentNode.style.transform = transformCss.rotateY + " " + transformCss.rotate + " " + transformCss.scale; }); let btnRotate = document.getElementById("filter__rotate"); if (btnRotate) btnRotate.addEventListener("click", () => { if (rotateAngle += 90, transformCss.rotate = `rotate(${String(rotateAngle)}deg)`, liveVideoNode && liveVideoNode.parentNode) { if (liveVideoNode.parentNode.style.transition = "all .5s", rotateAngle / 90 % 2 !== 0) if (window.innerWidth > window.innerHeight) transformCss.scale = "scale(" + String(liveVideoNode.videoHeight / (liveVideoNode.videoWidth || 1)) + ")"; else transformCss.scale = "scale(" + String(liveVideoNode.videoWidth / (liveVideoNode.videoHeight || 1)) + ")"; else transformCss.scale = ""; liveVideoNode.parentNode.style.transform = transformCss.rotateY + " " + transformCss.rotate + " " + transformCss.scale; } }); let selectFilter = document.getElementById("filter__select"); if (selectFilter) selectFilter.onchange = function() { let option = this.options[this.selectedIndex].text; switch (option) { case "default": StyleHook_remove("Ex_Style_Filter"); break; case "1977": setVideoFilter(`.${liveVideoParentClassName}{position:relative;-webkit-filter:contrast(110%)brightness(110%)saturate(130%);filter:contrast(110%)brightness(110%)saturate(130%)}.${liveVideoParentClassName}::before{content:"";display:block;height:100%;width:100%;top:0;left:0;position:absolute;pointer-events:none;mix-blend-mode:screen;background:rgba(243,106,188,0.3);z-index:10}`); break; case "Aden": setVideoFilter(`.${liveVideoParentClassName}{position:relative;-webkit-filter:contrast(90%)brightness(120%)saturate(85%)hue-rotate(20deg);filter:contrast(90%)brightness(120%)saturate(85%)hue-rotate(20deg)}.${liveVideoParentClassName}::before{content:"";display:block;height:100%;width:100%;top:0;left:0;position:absolute;pointer-events:none;mix-blend-mode:darken;background:-webkit-linear-gradient(to right,rgba(66,10,14,0.2)1,rgba(66,10,14,0));background:linear-gradient(to right,rgba(66,10,14,0.2)1,rgba(66,10,14,0));z-index:10}`); break; case "Amaro": setVideoFilter(`.${liveVideoParentClassName}{position:relative;-webkit-filter:contrast(90%)brightness(110%)saturate(150%)hue-rotate(-10deg);filter:contrast(90%)brightness(110%)saturate(150%)hue-rotate(-10deg)}`); break; case "Brannan": setVideoFilter(`.${liveVideoParentClassName}{position:relative;-webkit-filter:contrast(140%)sepia(50%);filter:contrast(140%)sepia(50%)}.${liveVideoParentClassName}::before{content:"";display:block;height:100%;width:100%;top:0;left:0;position:absolute;pointer-events:none;mix-blend-mode:lighten;background:rgba(161,44,199,0.31);z-index:10}`); break; case "Brooklyn": setVideoFilter(`.${liveVideoParentClassName}{position:relative;-webkit-filter:contrast(90%)brightness(110%);filter:contrast(90%)brightness(110%)}.${liveVideoParentClassName}::before{content:"";display:block;height:100%;width:100%;top:0;left:0;position:absolute;pointer-events:none;mix-blend-mode:overlay;background:-webkit-radial-gradient(50%50%,circle closest-corner,rgba(168,223,193,0.4)1,rgba(183,196,200,0.2));background:radial-gradient(50%50%,circle closest-corner,rgba(168,223,193,0.4)1,rgba(183,196,200,0.2));z-index:10}`); break; case "Claredon": setVideoFilter(`.${liveVideoParentClassName}{position:relative;-webkit-filter:contrast(120%)saturate(125%);filter:contrast(120%)saturate(125%)}.${liveVideoParentClassName}::before{content:"";display:block;height:100%;width:100%;top:0;left:0;position:absolute;pointer-events:none;mix-blend-mode:overlay;background:rgba(127,187,227,0.2);z-index:10}`); break; case "Earlybird": setVideoFilter(`.${liveVideoParentClassName}{position:relative;-webkit-filter:contrast(90%)sepia(20%);filter:contrast(90%)sepia(20%)}.${liveVideoParentClassName}::before{content:"";display:block;height:100%;width:100%;top:0;left:0;position:absolute;pointer-events:none;mix-blend-mode:overlay;background:-webkit-radial-gradient(50%50%,circle closest-corner,rgba(208,186,142,1)20,rgba(29,2,16,0.2));background:radial-gradient(50%50%,circle closest-corner,rgba(208,186,142,1)20,rgba(29,2,16,0.2));z-index:10}`); break; case "Gingham": setVideoFilter(`.${liveVideoParentClassName}{position:relative;-webkit-filter:brightness(105%)hue-rotate(350deg);filter:brightness(105%)hue-rotate(350deg)}.${liveVideoParentClassName}::before{content:"";display:block;height:100%;width:100%;top:0;left:0;position:absolute;pointer-events:none;mix-blend-mode:darken;background:-webkit-linear-gradient(to right,rgba(66,10,14,0.2)1,rgba(0,0,0,0));background:linear-gradient(to right,rgba(66,10,14,0.2)1,rgba(0,0,0,0));z-index:10}`); break; case "Hudson": setVideoFilter(`.${liveVideoParentClassName}{position:relative;-webkit-filter:contrast(90%)brightness(120%)saturate(110%);filter:contrast(90%)brightness(120%)saturate(110%)}.${liveVideoParentClassName}::before{content:"";display:block;height:100%;width:100%;top:0;left:0;position:absolute;pointer-events:none;mix-blend-mode:multiply;opacity:0.5;background:-webkit-radial-gradient(50%50%,circle closest-corner,rgba(255,177,166,1)50,rgba(52,33,52,1));background:radial-gradient(50%50%,circle closest-corner,rgba(255,177,166,1)50,rgba(52,33,52,1));z-index:10}`); break; case "Inkwell": setVideoFilter(`.${liveVideoParentClassName}{position:relative;-webkit-filter:contrast(110%)brightness(110%)sepia(30%)grayscale(100%);filter:contrast(110%)brightness(110%)sepia(30%)grayscale(100%)}.${liveVideoParentClassName}::before{content:"";display:block;height:100%;width:100%;top:0;left:0;position:absolute;pointer-events:none;background:rgba(0,0,0,0);z-index:10}`); break; case "Lofi": setVideoFilter(`.${liveVideoParentClassName}{position:relative;-webkit-filter:contrast(150%)saturate(110%);filter:contrast(150%)saturate(110%)}.${liveVideoParentClassName}::before{content:"";display:block;height:100%;width:100%;top:0;left:0;position:absolute;pointer-events:none;mix-blend-mode:multiply;background:-webkit-radial-gradient(50%50%,circle closest-corner,rgba(0,0,0,0)70,rgba(34,34,34,1));background:radial-gradient(50%50%,circle closest-corner,rgba(0,0,0,0)70,rgba(34,34,34,1));z-index:10}`); break; case "Maven": setVideoFilter(`.${liveVideoParentClassName}{position:relative;-webkit-filter:contrast(95%)brightness(95%)saturate(150%)sepia(25%);filter:contrast(95%)brightness(95%)saturate(150%)sepia(25%)}.${liveVideoParentClassName}::before{content:"";display:block;height:100%;width:100%;top:0;left:0;position:absolute;pointer-events:none;mix-blend-mode:hue;background:rgba(3,230,26,0.2);z-index:10}`); break; case "Perpetua": setVideoFilter(`.${liveVideoParentClassName}{position:relative}.${liveVideoParentClassName}::before{content:"";display:block;height:100%;width:100%;top:0;left:0;position:absolute;pointer-events:none;mix-blend-mode:soft-light;opacity:0.5;background:-webkit-linear-gradient(to bottom,rgba(0,91,154,1)1,rgba(61,193,230,0));background:linear-gradient(to bottom,rgba(0,91,154,1)1,rgba(61,193,230,0));z-index:10}`); break; case "Reyes": setVideoFilter(`.${liveVideoParentClassName}{position:relative;-webkit-filter:contrast(85%)brightness(110%)saturate(75%)sepia(22%);filter:contrast(85%)brightness(110%)saturate(75%)sepia(22%)}.${liveVideoParentClassName}::before{content:"";display:block;height:100%;width:100%;top:0;left:0;position:absolute;pointer-events:none;mix-blend-mode:soft-light;opacity:0.5;background:rgba(173,205,239,1);z-index:10}`); break; case "Stinson": setVideoFilter(`.${liveVideoParentClassName}{position:relative;-webkit-filter:contrast(75%)brightness(115%)saturate(85%);filter:contrast(75%)brightness(115%)saturate(85%)}.${liveVideoParentClassName}::before{content:"";display:block;height:100%;width:100%;top:0;left:0;position:absolute;pointer-events:none;mix-blend-mode:soft-light;background:rgba(240,149,128,0.2);z-index:10}`); break; case "Toaster": setVideoFilter(`.${liveVideoParentClassName}{position:relative;-webkit-filter:contrast(150%)brightness(90%);filter:contrast(150%)brightness(90%)}.${liveVideoParentClassName}::before{content:"";display:block;height:100%;width:100%;top:0;left:0;position:absolute;pointer-events:none;mix-blend-mode:screen;opacity:0.5;background:-webkit-radial-gradient(50%50%,circle closest-corner,rgba(15,78,128,1)1,rgba(59,0,59,1));background:radial-gradient(50%50%,circle closest-corner,rgba(15,78,128,1)1,rgba(59,0,59,1));z-index:10}`); break; case "Walden": setVideoFilter(`.${liveVideoParentClassName}{position:relative;-webkit-filter:brightness(110%)saturate(160%)sepia(30%)hue-rotate(350deg);filter:brightness(110%)saturate(160%)sepia(30%)hue-rotate(350deg)}.${liveVideoParentClassName}::before{content:"";display:block;height:100%;width:100%;top:0;left:0;position:absolute;pointer-events:none;mix-blend-mode:screen;opacity:0.3;background:rgba(204,68,0,1);z-index:10}`); break; case "Valencia": setVideoFilter(`.${liveVideoParentClassName}{position:relative;-webkit-filter:contrast(108%)brightness(108%)sepia(8%);filter:contrast(108%)brightness(108%)sepia(8%)}.${liveVideoParentClassName}::before{content:"";display:block;height:100%;width:100%;top:0;left:0;position:absolute;pointer-events:none;mix-blend-mode:exclusion;opacity:0.5;background:rgba(58,3,57,1);z-index:10}`); break; case "Xpro2": setVideoFilter(`.${liveVideoParentClassName}{position:relative;-webkit-filter:sepia(30%);filter:sepia(30%)}.${liveVideoParentClassName}::before{content:"";display:block;height:100%;width:100%;top:0;left:0;position:absolute;pointer-events:none;mix-blend-mode:color-burn;background:-webkit-radial-gradient(50%50%,circle closest-corner,rgba(224,231,230,1)40,rgba(43,42,161,0.6));background:radial-gradient(50%50%,circle closest-corner,rgba(224,231,230,1)40,rgba(43,42,161,0.6));z-index:10}`); break; default: StyleHook_remove("Ex_Style_Filter"); break; } }; let btnPanorama = document.getElementById("filter__panorama"); if (btnPanorama) btnPanorama.addEventListener("click", () => { let tmp = document.getElementById("ex-panorama"); if (tmp) tmp.remove(), panorama = null; else { let node = document.getElementById("__h5player"); if (node) { let dom = document.createElement("div"); dom.id = "ex-panorama", dom.style = "width:100%;height:100%;z-index:1;background:black;", node.insertBefore(dom, node.childNodes[0]), panorama = new PanoramaVideo(dom, liveVideoNode), panoramaAnimation(panorama); } } }); } function Filter_showPanel() { const panel = document.querySelector("#ex-vtoolbar-filter-host .filter__wrap"); if (panel) panel.style.display = "block"; } function Filter_hidePanel() { const panel = document.querySelector("#ex-vtoolbar-filter-host .filter__wrap"); if (panel) panel.style.display = "none"; } function resetVideoFilter() { if (StyleHook_remove("Ex_Style_Filter"), document.getElementById("filter__select").selectedIndex = 0, liveVideoNode.style.filter = "", rotateAngle = 0, transformCss = { rotateY: "", rotate: "", scale: "" }, liveVideoNode.parentNode.style.transform = "", document.getElementById("bar__bright").style.left = "100px", document.getElementById("bar__contrast").style.left = "100px", document.getElementById("bar__saturate").style.left = "100px", document.getElementById("mask__bright").style.width = "100px", document.getElementById("mask__contrast").style.width = "100px", document.getElementById("mask__saturate").style.width = "100px", isEdgeBrowser()) { isEnhanceQuality = false; const sliderEnhance = document.getElementById("slider__enhance"); const switchEnhance = document.getElementById("switch__enhance"); if (sliderEnhance) sliderEnhance.style.left = "0px"; if (switchEnhance) switchEnhance.style.background = "#ccc"; liveVideoNode.style.imageRendering = ""; const enhanceModal = document.getElementsByClassName("enhance-modal__panel-wrap")[0]; if (enhanceModal) enhanceModal.style.display = "none"; } let domPanorama = document.getElementById("ex-panorama"); if (domPanorama) domPanorama.remove(), panorama = null; let domVideoWrap = document.getElementsByClassName("layout-Player-videoEntity")[0]; domVideoWrap.style.transform = "", domVideoWrap.style.transformOrigin = "", videoScale = 1, StyleHook_remove("Ex_Style_Cinema"), liveVideoNode.playbackRate = 1; } function panoramaAnimation(panorama) { requestAnimationFrame(() => { panoramaAnimation(panorama); }), panorama.update(); } function setVideoFilter(style) { StyleHook_remove("Ex_Style_Filter"), StyleHook_set("Ex_Style_Filter", style), Filter_hidePanel(); } function setScrollFunc(scrollDom, barDom, maskDom, callback) { if (!scrollDom || !barDom || !maskDom) return; let scroll = scrollDom; let bar = barDom; let mask = maskDom; let barleft = 0; bar.onmousedown = function(e) { let event = e || window.event; let leftVal = event.clientX - this.offsetLeft; let that = this; function onMouseMove(e) { let event = e || window.event; if (barleft = event.clientX - leftVal, barleft < 0) barleft = 0; else if (barleft > scroll.offsetWidth - bar.offsetWidth) barleft = scroll.offsetWidth - bar.offsetWidth; mask.style.width = barleft + "px", that.style.left = barleft + "px", callback(parseInt(barleft / (scroll.offsetWidth - bar.offsetWidth) * 255)), window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty(); } function onMouseUp() { document.removeEventListener("mousemove", onMouseMove), document.removeEventListener("mouseup", onMouseUp); } document.addEventListener("mousemove", onMouseMove), document.addEventListener("mouseup", onMouseUp); }; } function initPkg_VideoTools_VideoRecall() { initPkg_VideoTools_VideoRecall_Func(); } var VideoRecall_keydownHandler = null; function initPkg_VideoTools_VideoRecall_Func() { if (VideoRecall_keydownHandler) document.removeEventListener("keydown", VideoRecall_keydownHandler, true); VideoRecall_keydownHandler = e => { if (isInput) return; if (e.target && (e.target.isContentEditable || /^(input|textarea)$/i.test(e.target.tagName))) return; if (37 != e.keyCode && 39 != e.keyCode && "ArrowLeft" !== e.key && "ArrowRight" !== e.key) return; if (liveVideoNode = liveVideoNode || document.querySelector(".layout-Player-videoEntity video"), !liveVideoNode) return; e.preventDefault(), liveVideoNode.currentTime = Math.max(0, (liveVideoNode.currentTime || 0) + (37 == e.keyCode || "ArrowLeft" === e.key ? -3 : 3)); }, document.addEventListener("keydown", VideoRecall_keydownHandler, true); } function initPkg_VideoTools_VideoSpeed() { initPkg_VideoTools_VideoSpeed_Dom(), initPkg_VideoTools_VideoSpeed_Func(); } function initPkg_VideoTools_VideoSpeed_Dom() { VideoSpeed_insertIcon(); } function VideoSpeed_insertIcon() { let a = document.createElement("li"); a.id = "ex-videospeed", a.innerHTML = ` 倍速播放
    • 2.0x
    • 1.5x
    • 1.25x
    • 1.0x
    • 0.75x
    • 0.5x
    `; let b = document.getElementsByClassName("menu-da2a9e")[0]; if (b) b.insertBefore(a, b.childNodes[1]); } function initPkg_VideoTools_VideoSpeed_Func() { const speeds = [ { id: "videospeed__2.0", rate: 2 }, { id: "videospeed__1.5", rate: 1.5 }, { id: "videospeed__1.25", rate: 1.25 }, { id: "videospeed__1.0", rate: 1 }, { id: "videospeed__0.75", rate: .75 }, { id: "videospeed__0.5", rate: .5 } ]; speeds.forEach(item => { let el = document.getElementById(item.id); if (el) el.addEventListener("click", () => { if ("undefined" !== typeof liveVideoNode && liveVideoNode) liveVideoNode.playbackRate = item.rate; }); }); } function initPkg_VideoTools_VideoSync() { initPkg_VideoTools_VideoSync_Dom(), initPkg_VideoTools_VideoSync_Func(); } function initPkg_VideoTools_VideoSync_Dom() { VideoSync_insertIcon(); } function VideoSync_insertIcon() { let a = document.createElement("div"); a.id = "ex-videosync", a.title = "同步时间", a.innerHTML = ` `; let b = document.getElementsByClassName("left-d3671e")[0]; if (b) a.style.marginLeft = "20px", b.insertBefore(a, b.childNodes[3] || b.firstChild); else if (a.style.marginLeft = "8px", b = getValidDom([ ".left-bfab3b" ]), b) b.insertBefore(a, b.childNodes[2] || b.firstChild); } function initPkg_VideoTools_VideoSync_Func() { let btn = document.getElementById("ex-videosync"); if (btn) btn.addEventListener("click", () => { setVideoSync(); }); } function setVideoSync() { let buffered = liveVideoNode.buffered; if (0 == buffered.length) return; liveVideoNode.currentTime = buffered.end(0); } let VideoToolbarMenu_isOpen = false; let VideoToolbarMenu_filterExpanded = false; let VideoToolbarMenu_closeTimer = null; function initPkg_VideoTools_VideoToolbarMenu() { VideoToolbarMenu_insertDom(), VideoToolbarMenu_bindEvents(); } function VideoToolbarMenu_insertDom() { const root = document.createElement("div"); root.id = "ex-vtoolbar-menu", root.className = "vtoolbar-menu", root.innerHTML = `
    `; const toolbar = getValidDom([ ".right-e7ea5d", ".right-17e251" ]); if (toolbar) toolbar.insertBefore(root, toolbar.childNodes[0]); } function VideoToolbarMenu_getRoot() { return document.getElementById("ex-vtoolbar-menu"); } function VideoToolbarMenu_isInsideMenu(target) { const root = VideoToolbarMenu_getRoot(); return !!(root && target && root.contains(target)); } function VideoToolbarMenu_onTriggerEnter() { clearTimeout(VideoToolbarMenu_closeTimer), VideoToolbarMenu_open(); } function VideoToolbarMenu_onMenuZoneEnter() { if (clearTimeout(VideoToolbarMenu_closeTimer), !VideoToolbarMenu_isOpen) VideoToolbarMenu_open(); } function VideoToolbarMenu_scheduleClose() { clearTimeout(VideoToolbarMenu_closeTimer), VideoToolbarMenu_closeTimer = setTimeout(() => { VideoToolbarMenu_close(); }, 80); } function VideoToolbarMenu_onMenuZoneLeave(e) { const related = e.relatedTarget; if (VideoToolbarMenu_isInsideMenu(related)) return; VideoToolbarMenu_scheduleClose(); } function VideoToolbarMenu_bindHoverTarget(el, isTrigger) { if (!el) return; if (isTrigger) el.addEventListener("mouseenter", VideoToolbarMenu_onTriggerEnter), el.addEventListener("pointerenter", VideoToolbarMenu_onTriggerEnter); else el.addEventListener("mouseenter", VideoToolbarMenu_onMenuZoneEnter), el.addEventListener("pointerenter", VideoToolbarMenu_onMenuZoneEnter); el.addEventListener("mouseleave", VideoToolbarMenu_onMenuZoneLeave), el.addEventListener("pointerleave", VideoToolbarMenu_onMenuZoneLeave); } function VideoToolbarMenu_bindEvents() { const root = VideoToolbarMenu_getRoot(); const trigger = root.querySelector(".vtoolbar-menu__trigger"); const dropdown = root.querySelector(".vtoolbar-menu__dropdown"); const filterHost = document.getElementById("ex-vtoolbar-filter-host"); const filterBtn = document.getElementById("vtoolbar-menu-filter"); const copyLiveBtn = document.getElementById("vtoolbar-menu-copy-live"); const enhancedPipBtn = document.getElementById("vtoolbar-menu-enhanced-pip"); const expanelBtn = document.getElementById("vtoolbar-menu-expanel"); VideoToolbarMenu_bindHoverTarget(trigger, true), VideoToolbarMenu_bindHoverTarget(dropdown, false), VideoToolbarMenu_bindHoverTarget(filterHost, false), filterBtn.addEventListener("click", e => { e.stopPropagation(), VideoToolbarMenu_toggleFilterPanel(); }), copyLiveBtn.addEventListener("click", e => { if (e.stopPropagation(), "function" === typeof CopyRealLive_copySelected) CopyRealLive_copySelected(); }), enhancedPipBtn.addEventListener("click", e => { if (e.stopPropagation(), VideoToolbarMenu_close(), "function" === typeof PictureInPictureControl_launchEnhanced) PictureInPictureControl_launchEnhanced(); }), expanelBtn.addEventListener("click", e => { if (e.stopPropagation(), "function" === typeof showExPanel) showExPanel(); }), document.addEventListener("keydown", VideoToolbarMenu_onKeydown); } function VideoToolbarMenu_onKeydown(e) { if ("Escape" === e.key) VideoToolbarMenu_close(), VideoToolbarMenu_hideFilterPanel(); } function VideoToolbarMenu_open() { const root = document.getElementById("ex-vtoolbar-menu"); if (!root) return; VideoToolbarMenu_isOpen = true, root.classList.add("is-open"); const trigger = root.querySelector(".vtoolbar-menu__trigger"); trigger.setAttribute("aria-expanded", "true"); } function VideoToolbarMenu_close() { const root = document.getElementById("ex-vtoolbar-menu"); if (!root) return; clearTimeout(VideoToolbarMenu_closeTimer), VideoToolbarMenu_isOpen = false, root.classList.remove("is-open"); const trigger = root.querySelector(".vtoolbar-menu__trigger"); trigger.setAttribute("aria-expanded", "false"), VideoToolbarMenu_hideFilterPanel(); } function VideoToolbarMenu_toggleFilterPanel() { if (VideoToolbarMenu_filterExpanded) VideoToolbarMenu_hideFilterPanel(); else VideoToolbarMenu_showFilterPanel(); } function VideoToolbarMenu_showFilterPanel() { const host = document.getElementById("ex-vtoolbar-filter-host"); const filterBtn = document.getElementById("vtoolbar-menu-filter"); if (!host) return; if (VideoToolbarMenu_filterExpanded = true, host.classList.add("is-visible"), filterBtn) filterBtn.classList.add("is-active"), filterBtn.setAttribute("aria-expanded", "true"); if ("function" === typeof Filter_showPanel) Filter_showPanel(); } function VideoToolbarMenu_hideFilterPanel() { const host = document.getElementById("ex-vtoolbar-filter-host"); const filterBtn = document.getElementById("vtoolbar-menu-filter"); if (VideoToolbarMenu_filterExpanded = false, host) host.classList.remove("is-visible"); if (filterBtn) filterBtn.classList.remove("is-active"), filterBtn.setAttribute("aria-expanded", "false"); if ("function" === typeof Filter_hidePanel) Filter_hidePanel(); } function VideoToolbarMenu_getFilterHost() { return document.getElementById("ex-vtoolbar-filter-host"); } const VideoToolbarMenu_pokeballSvg = PokeballIcon_svg; const VideoToolbarMenu_iconFilter = ``; const VideoToolbarMenu_iconChevron = ``; const VideoToolbarMenu_iconCopyLive = ``; const VideoToolbarMenu_iconEnhancedPip = ``; var liveVideoNode; var isInput = false; let videotools_num = 0; function initPkg_VideoTools() { let timer = setInterval(() => { const controlbar = getValidDom([ ".right-e7ea5d", ".right-17e251" ]); if (controlbar) { clearInterval(timer), liveVideoNode = document.querySelector(".layout-Player-videoEntity video"); let versionNode = document.getElementsByClassName("disable-23f484")[0]; if (versionNode) versionNode.innerHTML = `DouyuEx_` + curVersion; else ExGuard_warn("视频工具", "未找到版本水印节点(.disable-23f484),版本号未写入(其余功能不受影响)"); initPkg_VideoTools_Module(), initPkg_VideoTools_Func(); } if (videotools_num++, videotools_num >= 100) clearInterval(timer); }, 1500); } function initPkg_VideoTools_Module() { initPkg_VideoTools_VideoToolbarMenu(), initPkg_VideoTools_Joysound(), initPkg_VideoTools_VideoSpeed(), initPkg_VideoTools_Cinema(), initPkg_VideoTools_VideoSync(), initPkg_VideoTools_VideoRecall(), initPkg_VideoTools_Filter(), initPkg_VideoTools_Camera(), initPkg_VideoTools_VideoZoom(), initPkg_VideoTools_MetaData(), initPkg_PictureInPictureControl(); } function initPkg_VideoTools_Func() { const hideFilterOnLeave = () => { if ("function" === typeof VideoToolbarMenu_hideFilterPanel) VideoToolbarMenu_hideFilterPanel(); else if ("function" === typeof Filter_hidePanel) Filter_hidePanel(); }; let tb = document.getElementById("js-player-toolbar"); if (tb) tb.addEventListener("mouseover", hideFilterOnLeave); let aside = document.getElementById("js-player-asideMain"); if (aside) aside.addEventListener("mouseover", hideFilterOnLeave); let inputView = getValidDom([ ".inputView-2a65aa", ".inputView-620ab7" ]); if (inputView) inputView.addEventListener("focus", () => { isInput = true; }), inputView.addEventListener("blur", () => { isInput = false; }); new DomHook(".app-f0f9c7", false, m => { if (m.length > 0) if (m[0].addedNodes.length > 0) isInput = true; else if (m[0].removedNodes.length > 0) isInput = false; }); } let videoScale = 1; var VideoZoom_wheelHandler = null; var VideoZoom_moveHandler = null; var VideoZoom_upHandler = null; var VideoZoom_drag = null; function initPkg_VideoTools_VideoZoom() { let domWrap = getValidDom([ ".layout-Player-videoEntity", ".layout-Player-video" ]); let domVideoWrap = document.getElementsByClassName("layout-Player-videoEntity")[0]; let tx = 0; let ty = 0; if (!domWrap || !domVideoWrap) return; if (domVideoWrap.style.transformOrigin = "0 0", domVideoWrap.style.transition = "transform 0.1s", VideoZoom_wheelHandler) window.removeEventListener("wheel", VideoZoom_wheelHandler, true); if (VideoZoom_wheelHandler = e => { if (!e.ctrlKey) return; if (domWrap = domWrap || getValidDom([ ".layout-Player-videoEntity", ".layout-Player-video" ]), domVideoWrap = domVideoWrap || document.getElementsByClassName("layout-Player-videoEntity")[0], !domWrap || !domVideoWrap) return; const r = domWrap.getBoundingClientRect(); if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) return; e.preventDefault(), e.stopImmediatePropagation(); let px = e.clientX - r.left; let py = e.clientY - r.top; let nextScale = videoScale + (e.deltaY < 0 ? .1 : -.1); if (nextScale < .1) nextScale = .1; let wx = (px - tx) / videoScale; let wy = (py - ty) / videoScale; if (tx = px - wx * nextScale, ty = py - wy * nextScale, videoScale = nextScale, videoScale < .1) videoScale = .1; domVideoWrap.style.transform = `translate(${tx}px, ${ty}px) scale(${videoScale})`; }, window.addEventListener("wheel", VideoZoom_wheelHandler, { capture: true, passive: false }), VideoZoom_moveHandler) window.removeEventListener("mousemove", VideoZoom_moveHandler, true); if (VideoZoom_upHandler) window.removeEventListener("mouseup", VideoZoom_upHandler, true); window.addEventListener("mousedown", e => { if (!e.ctrlKey || 0 !== e.button) return; const r = domWrap.getBoundingClientRect(); if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) return; e.preventDefault(), domVideoWrap.style.transition = "none", VideoZoom_drag = { x: e.clientX, y: e.clientY, tx: tx, ty: ty }; }, true), VideoZoom_moveHandler = e => { if (!VideoZoom_drag) return; tx = VideoZoom_drag.tx + (e.clientX - VideoZoom_drag.x), ty = VideoZoom_drag.ty + (e.clientY - VideoZoom_drag.y), domVideoWrap.style.transform = `translate(${tx}px, ${ty}px) scale(${videoScale})`; }, VideoZoom_upHandler = () => { if (!VideoZoom_drag) return; VideoZoom_drag = null, domVideoWrap.style.transition = "transform 0.1s"; }, window.addEventListener("mousemove", VideoZoom_moveHandler, true), window.addEventListener("mouseup", VideoZoom_upHandler, true); } function initPkg_VolumeMouseScrolling() { const volumeCheckInterval = setInterval(() => { const volumeElement = document.querySelector(".volume-07c230"); const video = document.getElementById("__video2"); if (volumeElement && video) clearInterval(volumeCheckInterval), volumeElement.addEventListener("wheel", function(e) { e.preventDefault(), e.stopPropagation(); let currentVol = video.volume; let nextVol = e.deltaY < 0 ? Math.min(currentVol + .05, 1) : Math.max(currentVol - .05, 0); VolumeMouseScrolling_Handler(nextVol); }, { passive: false, capture: true }), video.addEventListener("volumechange", () => { VolumeMouseScrolling_SyncVolumeUI(video.volume); }), VolumeMouseScrolling_SyncVolumeUI(video.volume); }, 500); } function VolumeMouseScrolling_SyncVolumeUI(volume) { try { const volumeElement = document.querySelector(".volume-07c230"); const volume_front = document.querySelector(".volume-bar-93f0b0 .front-99e2aa"); const volume_point = document.querySelector(".volume-bar-93f0b0 .point-6ef744"); const volume_tips_text = document.querySelector(".volume-bar-93f0b0 .tips2-9bb064"); if (volume_front) volume_front.style.height = 100 * volume + `px`; if (volume_point) volume_point.style.bottom = 100 * volume + 7 + `px`; if (volume_tips_text) volume_tips_text.textContent = `音量${Math.round(100 * volume)}%`; if (volumeElement) if (0 === volume) volumeElement.classList.add("custom-muted"), volumeElement.classList.remove("custom-normal"); else volumeElement.classList.add("custom-normal"), volumeElement.classList.remove("custom-muted"); } catch (e) {} } let _volumeSaveTimer = null; function VolumeMouseScrolling_Handler(volume) { const video = document.getElementById("__video2"); if (!video) return; video.muted = 0 === volume, video.volume = volume, clearTimeout(_volumeSaveTimer), _volumeSaveTimer = setTimeout(() => { try { const keys = [ "volume_muted_before_key", "player_storage_volume_h5p_room" ]; keys.forEach(key => { let raw = localStorage.getItem(key); if (raw) { let data = JSON.parse(raw); data.v = volume, localStorage.setItem(key, JSON.stringify(data)); } }); } catch (e) {} }, 300); } function doSign(alias) { return new Promise(resolve => { fetch("https://www.douyu.com/japi/carnival/nc/hostSnowSign/doSign", { method: "POST", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `actAlias=${alias}&token=${dyToken}&ctn=` + getCCN() }).then(res => res.json()).then(ret => { resolve(ret); }).catch(err => { console.log("请求失败!", err); }); }); } function signAct(alias) { return new Promise(resolve => { fetch("https://www.douyu.com/japi/carnival/nc/signAct/signIn", { method: "POST", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: "token=" + dyToken + "&signAlias=" + alias }).then(res => res.json()).then(ret => { resolve(ret); }); }); } function userStatus(tasks) { return new Promise(resolve => { fetch("https://www.douyu.com/japi/carnival/nc/actTask/userStatus", { method: "POST", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `tasks=${tasks}&token=` + dyToken }).then(res => res.json()).then(ret => { resolve(ret); }).catch(err => { console.log("请求失败!", err); }); }); } function takeActPrize(name) { return new Promise(resolve => { fetch("https://www.douyu.com/japi/carnival/nc/actTask/takePrize", { method: "POST", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `token=${dyToken}&aid=android&taskAlias=` + name }).then(res => res.json()).then(ret => { resolve(ret); }).catch(err => { console.log("请求失败!", err); }); }); } function addFollowRoom(rid) { return new Promise(resolve => { fetch("https://www.douyu.com/wgapi/livenc/liveweb/follow/add", { method: "POST", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `rid=${rid}&ctn=` + getCCN() }).then(res => res.json()).then(ret => { resolve(ret); }).catch(err => { console.log("请求失败!", err); }); }); } function removeFollowRoom(rid) { return new Promise(resolve => { fetch("https://www.douyu.com/wgapi/livenc/liveweb/follow/rm", { method: "POST", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `rid=${rid}&ctn=` + getCCN() }).then(res => res.json()).then(ret => { resolve(ret); }).catch(err => { console.log("请求失败!", err); }); }); } function shareAct(name) { return new Promise(resolve => { fetch("https://www.douyu.com/japi/carnival/common/share", { method: "POST", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `actAlias=${name}&token=` + dyToken }).then(res => res.json()).then(ret => { resolve(ret); }).catch(err => { console.log("请求失败!", err); }); }); } function getJackpot(id) { return new Promise(resolve => { fetch("https://www.douyu.com/japi/carnival/nc/lottery/jackpot", { method: "POST", headers: { "Content-Type": "application/json;charset=UTF-8" }, body: `{"activityId":"${id}","token":"${dyToken}"}` }).then(res => res.json()).then(ret => { resolve(ret); }); }); } function getActRemaining(id) { return new Promise(resolve => { fetch("https://www.douyu.com/japi/carnival/nc/lottery/remaining?activityId=" + id, { method: "GET", mode: "no-cors", credentials: "include", headers: { "Content-Type": "application/json;charset=UTF-8" } }).then(res => res.json()).then(ret => { resolve(ret); }); }); } function getActList() {} class ASS { constructor(options) { let defaultOptions = { width: 1920, height: 1080, fontSize: 36, alpha: this._prefixInteger(Number(0).toString(16), 2), stayTime: 10, title: "Default" }; this.options = { ...defaultOptions, ...options, ...options ? options.alpha ? this._prefixInteger(this.options.alpha.toString(16), 2) : {} : {} }, this.lines = 20, this.lineBase = this.options.height / this.lines, this.currentLine = 0, this.diffTime = 1500; } generate(list) { let sortList = list.sort((a, b) => a.time - b.time); let result = this._getScriptInfo() + this._getV4Styles() + this._getEvents(); for (let i = 0; i < sortList.length; i++) { if (i > 0 && sortList[i].time - sortList[i - 1].time <= this.diffTime) this.currentLine++; else this.currentLine = 0; if (this.currentLine >= this.lines) this.currentLine = 0; let item = sortList[i]; let endTime = Number(item.time) + 1e3 * Number(this.options.stayTime); let heightOffset = this.lineBase * this.currentLine + this.options.fontSize; let fontWidth = this.options.fontSize * item.txt.length; result += `Dialogue: 0,${formatSeconds2(Number(item.time) / 1e3)}.00,${formatSeconds2(endTime / 1e3)}.00,Color${item.color},,0,0,0,,{\\move(${this.options.width + fontWidth},${heightOffset},${-fontWidth},${heightOffset})}${item.txt} `; } return result; } _prefixInteger(num, length) { return num = "" + num, Array(length + 1 - num.length).join("0") + num; } _getScriptInfo() { return `[Script Info] ; DouyuEx -By qianjiachun ; https://github.com/qianjiachun/douyuEx ScriptType: v4.00+ Title: ${this.options.title} PlayResX: ${this.options.width} PlayResY: ${this.options.height} `; } _getV4Styles() { return ` [V4+ Styles] Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding Style: Color0,黑体,${this.options.fontSize},&H${this.options.alpha}FFFFFF,&H80000000,&H80000000,&H80000000,0,0,0,0,100,100,0,0,0,0,0,0,0,0,0,134 Style: Color7,黑体,${this.options.fontSize},&H${this.options.alpha}5456FF,&H80000000,&H80000000,&H80000000,0,0,0,0,100,100,0,0,0,0,0,0,0,0,0,134 Style: Color8,黑体,${this.options.fontSize},&H${this.options.alpha}2375FF,&H80000000,&H80000000,&H80000000,0,0,0,0,100,100,0,0,0,0,0,0,0,0,0,134 Style: Color9,黑体,${this.options.fontSize},&H${this.options.alpha}B369FE,&H80000000,&H80000000,&H80000000,0,0,0,0,100,100,0,0,0,0,0,0,0,0,0,134 Style: Color10,黑体,${this.options.fontSize},&H${this.options.alpha}00BCFF,&H80000000,&H80000000,&H80000000,0,0,0,0,100,100,0,0,0,0,0,0,0,0,0,134 Style: Color11,黑体,${this.options.fontSize},&H${this.options.alpha}46C978,&H80000000,&H80000000,&H80000000,0,0,0,0,100,100,0,0,0,0,0,0,0,0,0,134 Style: Color12,黑体,${this.options.fontSize},&H${this.options.alpha}FF7F9E,&H80000000,&H80000000,&H80000000,0,0,0,0,100,100,0,0,0,0,0,0,0,0,0,134 Style: Color13,黑体,${this.options.fontSize},&H${this.options.alpha}FF9B3D,&H80000000,&H80000000,&H80000000,0,0,0,0,100,100,0,0,0,0,0,0,0,0,0,134 `; } _getEvents() { return ` [Events] Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text `; } } class CClick { constructor(element) { const CONST_LONG_TIME = 700; const CONST_DOUBLE_TIME = 250; this.func_click = null, this.func_dbClick = null, this.func_longClick = null; let isLong = false; let timer_long = null; let clickTimes = 0; let timer_db = null; element.addEventListener("mousedown", event => { if (0 !== event.button) return; isLong = false, timer_long = setTimeout(() => { if (isLong = true, null !== this.func_longClick) this.func_longClick(event); }, CONST_LONG_TIME); }), element.addEventListener("mouseup", event => { if (0 !== event.button) return; if (clearTimeout(timer_long), isLong) return isLong = false, void 0; if (null === this.func_dbClick) { if (null !== this.func_click) this.func_click(event); return; } if (clickTimes++, clickTimes >= 2) return clearTimeout(timer_db), clickTimes = 0, this.func_dbClick(event), void 0; timer_db = setTimeout(() => { if (clickTimes = 0, null !== this.func_click) this.func_click(event); }, CONST_DOUBLE_TIME); }); } click(callback) { this.func_click = callback; } dbClick(callback) { this.func_dbClick = callback; } longClick(callback) { this.func_longClick = callback; } } class DomHook { constructor(selector, isSubtree, callback) { this.selector = selector, this.isSubtree = isSubtree, this.callback = callback, this.observer = null, this.targetNode = null, this.failed = false, this.failReason = "", this.callbackErrors = 0, this.closed = false; let targetNode = null; try { targetNode = document.querySelector(this.selector); } catch (e) { targetNode = null, ExGuard_warn("DomHook", "选择器 " + this.selector + " 查询失败", e); } if (null == targetNode) return this.failed = true, this.failReason = "target-not-found", ExGuard_warn("DomHook", "目标 " + this.selector + " 不存在,本次没有挂上(等下次初始化或刷新后重试)"), void 0; try { const self = this; this.observer = new MutationObserver(function(mutations) { try { callback(mutations); } catch (e) { if (self.callbackErrors++, self.callbackErrors <= 3) ExGuard_warn("DomHook", "回调 " + self.selector + " 抛错(第 " + self.callbackErrors + " 次,observer 继续工作)", e); } }), this.observer.observe(targetNode, { attributes: true, childList: true, subtree: this.isSubtree }), this.targetNode = targetNode; } catch (e) { this.observer = null, this.failed = true, this.failReason = "observe-failed", ExGuard_warn("DomHook", "目标 " + this.selector + " 挂载 observer 失败", e); } } isActive() { if (!this.observer || this.closed) return false; const node = this.targetNode; if (!node) return false; if ("boolean" === typeof node.isConnected) return node.isConnected; return !!node.parentNode; } closeHook() { if (this.observer) this.observer.disconnect(), this.observer = null; this.targetNode = null, this.closed = true; } } class DyVideoSign { constructor(pointId) { this.pointId = pointId, this.decoder = new TextDecoder(); } getSign() { let did = "10000000000000000000000000001501"; let tt = parseInt(new Date().getTime() / 1e3, 10); return unsafeWindow[this.d539fa2cf7732d2a(256042, "9f4f419501570ad13334")](this.pointId, did, tt); } d539fa2cf7732d2a(e, t) { for (var e = CryptoJS.MM(e.toString()).toString(), r = e[0].charCodeAt(0), a = e[16].charCodeAt(0), i = [], o = 0; o < 4; o++) i[o] = r << 24 | r << 16 | r << 8 | r, i[o + 4] = a << 24 | a << 16 | a << 8 | a; var e = Math.floor(t.length / 16) % 4, l = [], d = t.length % 8, c = Math.floor(t.length / 8); for (o = 0; o < c; o++) l[o] = 255 & parseInt(t.substr(8 * o, 2), 16) | parseInt(t.substr(8 * o + 2, 2), 16) << 8 & 65280 | parseInt(t.substr(8 * o + 4, 2), 16) << 24 >>> 8 | parseInt(t.substr(8 * o + 6, 2), 16) << 24; var p = []; p = 0 == e ? e86500e2(l, i) : 1 == e ? this.c30070a4(l, i) : d831eb20(l, i); var h = []; for (o = 0; o < p.length; o++) { var f = 255 & p[o], u = p[o] >>> 8 & 255, m = p[o] >>> 16 & 255, g = p[o] >>> 24 & 255; f && h.push(f), u && h.push(u), m && h.push(m), g && h.push(g); } var b = Math.floor(d / 2); for (o = 0; o < b; o++) h.push(255 & parseInt(t.substr(8 * c + 2 * o, 2), 16)); return this.decoder.decode(new Uint8Array(h)); } c30070a4(e, t) { for (var n = Math.floor(e.length / 2), r = e.slice(0), a = 0; a < n; a++) { var i = this.f5a40d76(e.slice(2 * a, 2 * a + 2), 32, t.slice(4 * a % 8, 4 * a % 8 + 4)); r[2 * a + 0] = i[0], r[2 * a + 1] = i[1]; } return r; } f5a40d76(e, t, n) { for (var r = 0; r < e.length; r += 2) { var a, i = e[r], o = e[r + 1], s = 2654435769 * t; for (a = 0; a < t; a++) i -= ((o -= (i << 4 ^ i >>> 5) + i ^ s + n[s >>> 11 & 3]) << 4 ^ o >>> 5) + o ^ (s -= 2654435769) + n[3 & s]; e[r] = i, e[r + 1] = o; } return e; } } class DyWacthAd { constructor(posid, token, rid) { this.posid = posid, this.token = token, this.rid = rid, this._uid = String(token).split("_")[0], this._mid = 0, this._infoBack = ""; } async start() { let info = await this.getInfo(this.posid, this.token, this.rid); if (false == info) return false; this._mid = info.mid, this._infoBack = info.infoBack; let isStart = await this.getStart(this.posid, this.token, this._uid, this._mid, this._infoBack); return isStart; } async finish() { let isFinish = await this.getFinish(this.posid, this.token, this._uid, this._mid, this._infoBack); return isFinish; } getInfo(posid, token, rid) { return new Promise(resolve => { GM_xmlhttpRequest({ method: "POST", url: "https://rtbapi.douyucdn.cn/japi/sign/app/getinfo?token=" + token + "&mdid=phone" + "&client_sys=android", data: "posid=" + posid + "&roomid=" + rid + "&cate1=1&cate2=1&chanid=30" + '&device={"nt":"1"}', responseType: "json", headers: { "Content-Type": "application/x-www-form-urlencoded" }, onload: function(response) { let ret = response.response; if ("0" == ret.error) { if (0 == ret.data.length) return resolve(false), void 0; let mid = ret.data[0].mid; let infoBack = encodeURIComponent(JSON.stringify(ret.data)); resolve({ mid: mid, infoBack: infoBack }); } } }); }); } getStart(posid, token, uid, mid, infoBack) { return new Promise(resolve => { GM_xmlhttpRequest({ method: "POST", url: "https://apiv2.douyucdn.cn/japi/inspire/api/ad/fishpond/mobile/start?client_sys=android", data: "&uid=" + uid + "&roomId=" + rid + "&posCode=" + posid + "&token=" + token + "&clientType=1&creativeId=" + mid + "&infoBack=" + infoBack, responseType: "json", headers: { "Content-Type": "application/x-www-form-urlencoded" }, onload: function(response) { let ret = response.response; if ("0" == ret.error) resolve(true); } }); }); } getFinish(posid, token, uid, mid, infoBack) { return new Promise(resolve => { GM_xmlhttpRequest({ method: "POST", url: "https://apiv2.douyucdn.cn/japi/inspire/api/ad/fishpond/mobile/finish?client_sys=android", data: "uid=" + uid + "&clientType=1&posCode=" + posid + "&creativeId=" + mid + "&roomId=" + rid + "&token=" + token + "&infoBack=" + infoBack, responseType: "json", headers: { "Content-Type": "application/x-www-form-urlencoded" }, onload: function(response) { let ret = response.response; if ("0" == ret.error) if ("1" == ret.data) resolve(true); else resolve(false); else resolve(false); } }); }); } } var EXGUARD_WARN_MAX = 20; function ExGuard_warnCountsSafe() { if (!ExGuard_warnCountsSafe.map) ExGuard_warnCountsSafe.map = {}; return ExGuard_warnCountsSafe.map; } function ExGuard_warn(tag, message, err) { const key = String(tag || "杂项"); const counts = ExGuard_warnCountsSafe(); const used = counts[key] || 0; if (used >= EXGUARD_WARN_MAX) return; counts[key] = used + 1; let detail = ""; if (err) { const text = err && err.message ? err.message : String(err); if (text) detail = " —— " + text; } try { console.warn("[DouyuEx] " + key + ":" + message + detail); } catch (e) {} } function ExGuard_warnCount(tag) { return ExGuard_warnCountsSafe()[String(tag || "杂项")] || 0; } function ExGuard_asList(value) { return Array.isArray(value) ? value : []; } function ExGuard_pollFor(findFn, onFound, opts) { const config = opts || {}; const intervalMs = config.intervalMs > 0 ? config.intervalMs : 500; const maxTries = config.maxTries > 0 ? config.maxTries : 60; const tag = config.tag || "轮询"; const state = { timer: 0, tries: 0, done: false, cancelled: false }; const stopTimer = function() { if (state.timer) clearInterval(state.timer), state.timer = 0; }; state.cancel = function() { state.cancelled = true, stopTimer(); }; const tick = function() { if (state.cancelled || state.done) return; state.tries++; let found = null; try { if ("function" === typeof findFn) found = findFn(); else if ("string" === typeof findFn) found = document.querySelector(findFn); } catch (e) { ExGuard_warn(tag, "查询目标节点时抛错,继续等待", e); } if (found) { state.done = true, stopTimer(); try { if ("function" === typeof onFound) onFound(found); } catch (e) { ExGuard_warn(tag, "命中后的回调抛错(本次放弃,不影响其它功能)", e); } return; } if (state.tries >= maxTries) { state.done = true, stopTimer(); const seconds = Math.round(intervalMs * maxTries / 1e3); if (config.quiet) try { if ("undefined" !== typeof console && console.debug) console.debug("[DouyuEx] " + tag + ":等待目标节点 " + seconds + " 秒仍未出现(预期内可缺省,已放弃)"); } catch (e) {} else ExGuard_warn(tag, "等待目标节点 " + seconds + " 秒仍未出现,已放弃(不影响其它功能)"); } }; if (false === config.immediate) state.timer = setInterval(tick, intervalMs); else if (tick(), !state.done && !state.cancelled) state.timer = setInterval(tick, intervalMs); return state; } function ExGuard_xhr(options, timeoutMs) { return new Promise(function(resolve) { let settled = false; const finish = function(value) { if (settled) return; settled = true, resolve(void 0 === value ? null : value); }; const config = Object.assign({}, options || {}); const tag = config.tag || "请求"; const innerOnload = config.onload; if (config.onload = function(response) { let value = null; try { if ("function" === typeof innerOnload) { const handled = innerOnload(response); if (void 0 !== handled) value = handled; } if (null === value && response) value = response.response; } catch (e) { ExGuard_warn(tag, "响应处理抛错,按失败处理", e), value = null; } finish(value); }, config.onerror = function(err) { ExGuard_warn(tag, "请求失败:" + (config.url || "(无地址)"), err), finish(null); }, config.ontimeout = function() { ExGuard_warn(tag, "请求超时:" + (config.url || "(无地址)")), finish(null); }, !config.timeout) config.timeout = timeoutMs > 0 ? timeoutMs : 15e3; try { GM_xmlhttpRequest(config); } catch (e) { ExGuard_warn(tag, "请求无法发起:" + (config.url || "(无地址)"), e), finish(null); } }); } function ExGuard_withTimeout(promise, timeoutMs, tag) { return new Promise(function(resolve) { let settled = false; const limit = timeoutMs > 0 ? timeoutMs : 15e3; const timer = setTimeout(function() { if (settled) return; settled = true, ExGuard_warn(tag || "异步", "超过 " + limit + "ms 仍未返回,按失败处理"), resolve(null); }, limit); Promise.resolve(promise).then(function(value) { if (settled) return; settled = true, clearTimeout(timer), resolve(void 0 === value ? null : value); }, function(err) { if (settled) return; settled = true, clearTimeout(timer), ExGuard_warn(tag || "异步", "异步失败", err), resolve(null); }); }); } function M3U8() { var _this = this; function RecurseDownload(arr, cb, i, data) { var _this = this; function recurseDownload(arr, cb, i, data) { let taskList = []; for (let j = 0; j < _this.threadNum; j++) if (arr[i + j]) taskList.push(fetch(arr[i + j]).catch(err => { fetch(arr[i + j]).catch(err => { fetch(arr[i + j]); }); })); else { taskList.push(Promise.resolve()); break; } _this.step = taskList.length, Promise.all(taskList).then(function(d) { return map(filter(d, function(v) { return v && v.blob; }), function(v) { return v.blob(); }); }).then(function(d) { return Promise.all(d); }).then(function(d) { d = map(d, function(v, j) { return new Promise(function(resolve, reject) { var reader = new FileReader(); reader.readAsArrayBuffer(new Blob([ v ], { type: "octet/stream" })), reader.addEventListener("loadend", function(event) { resolve(reader.result), _this.onprogress && _this.onprogress({ segment: i + j + 1, total: arr.length, percentage: ((i + j + 1) / arr.length * 100).toFixed(3), downloaded: formatNumber(+reduce(map(data, function(v) { return v.byteLength; }), function(t, c) { return t + c; }, 0)), status: "Downloading..." }); }); }); }), Promise.all(d).then(function(d) { for (var n = 0; n < d.length; n++) data.push(d[n]); let step = _this.step; if (arr[i + 2], _this.aborted) return data = null, _this.aborted(), void 0; else if (arr[i + step]) if (_this.ie) setTimeout(function() { recurseDownload(arr, cb, i + step, data); }, 500); else recurseDownload(arr, cb, i + step, data); else cb(data); }); }).catch(function(err) { _this.onerror && _this.onerror("Something went wrong when downloading ts file, nr. " + i + ": " + err); }); } this.aborted = false, this.threadNum = 10, this.step = 0, recurseDownload(arr, cb, i, data); } function filter(arr, condition) { var result = []; for (var i = 0; i < arr.length; i++) if (condition(arr[i], i)) result.push(arr[i]); return result; } function map(arr, condition) { var result = arr.slice(0); for (var i = 0; i < arr.length; i++) result[i] = condition(arr[i], i); return result; } function reduce(arr, condition, start) { var result = start; return arr.forEach(function(v, i) { v = +condition(result, v, i), result = v; }), result; } function formatNumber(n) { var ranges = [ { divider: 1e18, suffix: "EB" }, { divider: 1e15, suffix: "PB" }, { divider: 1e12, suffix: "TB" }, { divider: 1e9, suffix: "GB" }, { divider: 1e6, suffix: "MB" }, { divider: 1e3, suffix: "kB" } ]; for (var i = 0; i < ranges.length; i++) if (n >= ranges[i].divider) { var res = (n / ranges[i].divider).toString(); return res.toString().split(".")[0] + ranges[i].suffix; } return n.toString(); } this.ie = navigator.appVersion.toString().indexOf(".NET") > 0, this.ios = navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform), this.start = function(m3u8, options) { if (!options) options = {}; var callbacks = { progress: null, finished: null, error: null, aborted: null }; var recur; function handleCb(key, payload) { if (key && callbacks[key]) callbacks[key](payload); } if (_this.ios) return handleCb("error", "Downloading on IOS is not supported."); var startObj = { on: function(str, cb) { switch (str) { case "progress": callbacks.progress = cb; break; case "finished": callbacks.finished = cb; break; case "error": callbacks.error = cb; break; case "aborted": callbacks.aborted = cb; break; } return startObj; }, abort: function() { recur && (recur.aborted = function() { handleCb("aborted"); }); } }; return new Promise(function(resolve, reject) { var url = new URL(m3u8); fetch(m3u8).then(function(d) { return d.text(); }).then(function(d) { if (d = filter(d.split(/(\r\n|\r|\n)/gi), function(item) { return item.indexOf(".ts") > -1; }), d = map(d, function(v, i) { if (0 === v.indexOf("http") || 0 === v.indexOf("ftp")) return v; return url.protocol + "//" + url.host + url.pathname + "/./../" + v; }), !d.length) return reject("Invalid m3u8 playlist"), handleCb("error", "Invalid m3u8 playlist"); recur = new RecurseDownload(d, function(data) { if (data = new Blob(data, { type: "octet/stream" }), handleCb("progress", { status: "Processing..." }), !options.returnBlob) if (_this.ios); else if (_this.ie) handleCb("progress", { status: "Sending video to Internet Explorer... this may take a while depending on your device's performance." }), window.navigator.msSaveBlob(data, options && options.filename || "video.mp4"); else { handleCb("progress", { status: "Sending video to browser..." }); var a = document.createElementNS("http://www.w3.org/1999/xhtml", "a"); a.href = URL.createObjectURL(data), a.download = options && options.filename || "video.mp4", a.style.display = "none", document.body.appendChild(a), a.click(), handleCb("finished", { status: "Successfully downloaded video", data: data }), resolve(data); } else handleCb("finished", { status: "Successfully downloaded video", data: data }), resolve(data); }, 0, []), recur.onprogress = function(obj) { handleCb("progress", obj); }; }).catch(function(err) { handleCb("error", "Something went wrong when downloading m3u8 playlist: " + err); }); }), startObj; }; } var hexcase = 0; var b64pad = ""; var chrsz = 8; function hex_md5(s) { return binl2hex(core_md5(str2binl(s), s.length * chrsz)); } function b64_md5(s) { return binl2b64(core_md5(str2binl(s), s.length * chrsz)); } function str_md5(s) { return binl2str(core_md5(str2binl(s), s.length * chrsz)); } function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); } function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); } function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); } function md5_vm_test() { return "900150983cd24fb0d6963f7d28e17f72" == hex_md5("abc"); } function core_md5(x, len) { x[len >> 5] |= 128 << len % 32, x[(len + 64 >>> 9 << 4) + 14] = len; var a = 1732584193; var b = -271733879; var c = -1732584194; var d = 271733878; for (var i = 0; i < x.length; i += 16) { var olda = a; var oldb = b; var oldc = c; var oldd = d; a = md5_ff(a, b, c, d, x[i + 0], 7, -680876936), d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586), c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819), b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330), a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897), d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426), c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341), b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983), a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416), d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417), c = md5_ff(c, d, a, b, x[i + 10], 17, -42063), b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162), a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682), d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101), c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290), b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329), a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510), d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632), c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713), b = md5_gg(b, c, d, a, x[i + 0], 20, -373897302), a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691), d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083), c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335), b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848), a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438), d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690), c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961), b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501), a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467), d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784), c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473), b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734), a = md5_hh(a, b, c, d, x[i + 5], 4, -378558), d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463), c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562), b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556), a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060), d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353), c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632), b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640), a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174), d = md5_hh(d, a, b, c, x[i + 0], 11, -358537222), c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979), b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189), a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487), d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835), c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520), b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651), a = md5_ii(a, b, c, d, x[i + 0], 6, -198630844), d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415), c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905), b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055), a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571), d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606), c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523), b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799), a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359), d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744), c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380), b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649), a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070), d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379), c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259), b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551), a = safe_add(a, olda), b = safe_add(b, oldb), c = safe_add(c, oldc), d = safe_add(d, oldd); } return Array(a, b, c, d); } function md5_cmn(q, a, b, x, s, t) { return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b); } function md5_ff(a, b, c, d, x, s, t) { return md5_cmn(b & c | ~b & d, a, b, x, s, t); } function md5_gg(a, b, c, d, x, s, t) { return md5_cmn(b & d | c & ~d, a, b, x, s, t); } function md5_hh(a, b, c, d, x, s, t) { return md5_cmn(b ^ c ^ d, a, b, x, s, t); } function md5_ii(a, b, c, d, x, s, t) { return md5_cmn(c ^ (b | ~d), a, b, x, s, t); } function core_hmac_md5(key, data) { var bkey = str2binl(key); if (bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz); var ipad = Array(16), opad = Array(16); for (var i = 0; i < 16; i++) ipad[i] = 909522486 ^ bkey[i], opad[i] = 1549556828 ^ bkey[i]; return key = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz), core_md5(opad.concat(key), 512 + 128); } function safe_add(x, y) { var lsw = (65535 & x) + (65535 & y); return x = (x >> 16) + (y >> 16) + (lsw >> 16), x << 16 | 65535 & lsw; } function bit_rol(num, cnt) { return num << cnt | num >>> 32 - cnt; } function str2binl(str) { var bin = Array(); var mask = (1 << chrsz) - 1; for (var i = 0; i < str.length * chrsz; i += chrsz) bin[i >> 5] |= (str.charCodeAt(i / chrsz) & mask) << i % 32; return bin; } function binl2str(bin) { var str = ""; var mask = (1 << chrsz) - 1; for (var i = 0; i < 32 * bin.length; i += chrsz) str += String.fromCharCode(bin[i >> 5] >>> i % 32 & mask); return str; } function binl2hex(binarray) { var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; var str = ""; for (var i = 0; i < 4 * binarray.length; i++) str += hex_tab.charAt(binarray[i >> 2] >> i % 4 * 8 + 4 & 15) + hex_tab.charAt(binarray[i >> 2] >> i % 4 * 8 & 15); return str; } function binl2b64(binarray) { var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; var str = ""; for (var i = 0; i < 4 * binarray.length; i += 3) { var triplet = (binarray[i >> 2] >> 8 * (i % 4) & 255) << 16 | (binarray[i + 1 >> 2] >> 8 * ((i + 1) % 4) & 255) << 8 | binarray[i + 2 >> 2] >> 8 * ((i + 2) % 4) & 255; for (var j = 0; j < 4; j++) if (8 * i + 6 * j > 32 * binarray.length) str += b64pad; else str += tab.charAt(triplet >> 6 * (3 - j) & 63); } return str; } ((root, factory) => { if ("object" === typeof exports && "object" === typeof module) module.exports = factory(); else if ("function" === typeof define && define.amd) define("NoticeJs", [], factory); else if ("object" === typeof exports) exports["NoticeJs"] = factory(); else root["NoticeJs"] = factory(); })("undefined" !== typeof self ? self : this, function() { return (modules => { var installedModules = {}; function __webpack_require__(moduleId) { if (installedModules[moduleId]) return installedModules[moduleId].exports; var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; return modules[moduleId].call(module.exports, module, module.exports, __webpack_require__), module.l = true, module.exports; } return __webpack_require__.m = modules, __webpack_require__.c = installedModules, __webpack_require__.d = function(exports, name, getter) { if (!__webpack_require__.o(exports, name)) Object.defineProperty(exports, name, { configurable: false, enumerable: true, get: getter }); }, __webpack_require__.n = function(module) { var getter = module && module.__esModule ? function() { return module["default"]; } : function() { return module; }; return __webpack_require__.d(getter, "a", getter), getter; }, __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }, __webpack_require__.p = "dist/", __webpack_require__(__webpack_require__.s = 2); })([ function(module, exports, __webpack_require__) { Object.defineProperty(exports, "__esModule", { value: true }), exports.noticeJsModalClassName = "noticejs-modal", exports.closeAnimation = "noticejs-fadeOut", exports.Defaults = { title: "", text: "", type: "success", position: "topRight", timeout: 30, progressBar: true, closeWith: [ "button" ], animation: null, modal: false, scroll: { maxHeight: 300, showOnHover: true }, rtl: false, callbacks: { beforeShow: [], onShow: [], afterShow: [], onClose: [], afterClose: [], onClick: [], onHover: [], onTemplate: [] } }; }, function(module, exports, __webpack_require__) { Object.defineProperty(exports, "__esModule", { value: true }), exports.appendNoticeJs = exports.addListener = exports.CloseItem = exports.AddModal = void 0, exports.getCallback = getCallback, __webpack_require__ = __webpack_require__(0); var API = _interopRequireWildcard(__webpack_require__); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) return obj; else { var newObj = {}; if (null != obj) for (var key in obj) if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; return newObj.default = obj, newObj; } } var options = API.Defaults; function getCallback(ref, eventName) { if (ref.callbacks.hasOwnProperty(eventName)) ref.callbacks[eventName].forEach(function(cb) { if ("function" === typeof cb) cb.apply(ref); }); } var AddModal = exports.AddModal = function() { if (document.getElementsByClassName(API.noticeJsModalClassName).length <= 0) { var element = document.createElement("div"); element.classList.add(API.noticeJsModalClassName), element.classList.add("noticejs-modal-open"), document.body.appendChild(element), setTimeout(function() { element.className = API.noticeJsModalClassName; }, 200); } }; var CloseItem = exports.CloseItem = function(item) { if (getCallback(options, "onClose"), null !== options.animation && null !== options.animation.close) item.className += " " + options.animation.close; if (setTimeout(function() { item.remove(); }, 200), true === options.modal && document.querySelectorAll("[noticejs-modal='true']").length >= 1) document.querySelector(".noticejs-modal").className += " noticejs-modal-close", setTimeout(function() { document.querySelector(".noticejs-modal").remove(); }, 500); var position = "." + item.closest(".noticejs").className.replace("noticejs", "").trim(); setTimeout(function() { if (document.querySelectorAll(position + " .item").length <= 0) { let p = document.querySelector(position); if (null != p) p.remove(); } }, 500); }; var addListener = exports.addListener = function(item) { if (options.closeWith.includes("button")) item.querySelector(".close").addEventListener("click", function() { CloseItem(item); }); if (options.closeWith.includes("click")) item.style.cursor = "pointer", item.addEventListener("click", function(e) { if ("close" !== e.target.className) getCallback(options, "onClick"), CloseItem(item); }); else item.addEventListener("click", function(e) { if ("close" !== e.target.className) getCallback(options, "onClick"); }); item.addEventListener("mouseover", function() { getCallback(options, "onHover"); }); }; exports.appendNoticeJs = function(noticeJsHeader, noticeJsBody, noticeJsProgressBar) { var target_class = ".noticejs-" + options.position; var noticeJsItem = document.createElement("div"); if (noticeJsItem.classList.add("item"), noticeJsItem.classList.add(options.type), true === options.rtl) noticeJsItem.classList.add("noticejs-rtl"); if (noticeJsHeader && "" !== noticeJsHeader) noticeJsItem.appendChild(noticeJsHeader); if (noticeJsItem.appendChild(noticeJsBody), noticeJsProgressBar && "" !== noticeJsProgressBar) noticeJsItem.appendChild(noticeJsProgressBar); if ([ "top", "bottom" ].includes(options.position)) document.querySelector(target_class).innerHTML = ""; if (null !== options.animation && null !== options.animation.open) noticeJsItem.className += " " + options.animation.open; if (true === options.modal) noticeJsItem.setAttribute("noticejs-modal", "true"), AddModal(); return addListener(noticeJsItem, options.closeWith), getCallback(options, "beforeShow"), getCallback(options, "onShow"), document.querySelector(target_class).appendChild(noticeJsItem), getCallback(options, "afterShow"), noticeJsItem; }; }, function(module, exports, __webpack_require__) { Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = (() => { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; if (descriptor.enumerable = descriptor.enumerable || false, descriptor.configurable = true, "value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function(Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _noticejs = __webpack_require__(3); _interopRequireDefault(_noticejs), _noticejs = __webpack_require__(0); var API = _interopRequireWildcard(_noticejs); var _components = __webpack_require__(4); _noticejs = __webpack_require__(1); var helper = _interopRequireWildcard(_noticejs); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) return obj; else { var newObj = {}; if (null != obj) for (var key in obj) if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; return newObj.default = obj, newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) throw new TypeError("Cannot call a class as a function"); } __webpack_require__ = (() => { function NoticeJs() { var options = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}; return _classCallCheck(this, NoticeJs), this.options = Object.assign(API.Defaults, options), this.component = new _components.Components(), this.on("beforeShow", this.options.callbacks.beforeShow), this.on("onShow", this.options.callbacks.onShow), this.on("afterShow", this.options.callbacks.afterShow), this.on("onClose", this.options.callbacks.onClose), this.on("afterClose", this.options.callbacks.afterClose), this.on("onClick", this.options.callbacks.onClick), this.on("onHover", this.options.callbacks.onHover), this; } return _createClass(NoticeJs, [ { key: "show", value: function() { var container = this.component.createContainer(); if (null === document.querySelector(".noticejs-" + this.options.position)) document.body.appendChild(container); container = void 0; var noticeJsBody = void 0; var noticeJsProgressBar = void 0; if (container = this.component.createHeader(this.options.title, this.options.closeWith), noticeJsBody = this.component.createBody(this.options.text), true === this.options.progressBar) noticeJsProgressBar = this.component.createProgressBar(); return container = helper.appendNoticeJs(container, noticeJsBody, noticeJsProgressBar), container; } }, { key: "on", value: function(eventName) { var cb = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : function() {}; if ("function" === typeof cb && this.options.callbacks.hasOwnProperty(eventName)) this.options.callbacks[eventName].push(cb); return this; } } ]), NoticeJs; })(), exports.default = __webpack_require__, module.exports = exports["default"]; }, function(module, exports) {}, function(module, exports, __webpack_require__) { Object.defineProperty(exports, "__esModule", { value: true }), exports.Components = void 0; var _createClass = (() => { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; if (descriptor.enumerable = descriptor.enumerable || false, descriptor.configurable = true, "value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function(Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _api = __webpack_require__(0); _api = _interopRequireWildcard(_api), __webpack_require__ = __webpack_require__(1); var helper = _interopRequireWildcard(__webpack_require__); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) return obj; else { var newObj = {}; if (null != obj) for (var key in obj) if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; return newObj.default = obj, newObj; } } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) throw new TypeError("Cannot call a class as a function"); } var options = _api.Defaults; exports.Components = (() => { function Components() { _classCallCheck(this, Components); } return _createClass(Components, [ { key: "createContainer", value: function() { var element_class = "noticejs-" + options.position; var element = document.createElement("div"); return element.classList.add("noticejs"), element.classList.add(element_class), element; } }, { key: "createHeader", value: function() { var element = void 0; if (options.title && "" !== options.title) element = document.createElement("div"), element.setAttribute("class", "noticejs-heading"), element.textContent = options.title; if (options.closeWith.includes("button")) { var close = document.createElement("div"); if (close.setAttribute("class", "close"), close.innerHTML = "×", element) element.appendChild(close); else element = close; } return element; } }, { key: "createBody", value: function() { var element = document.createElement("div"); element.setAttribute("class", "noticejs-body"); var content = document.createElement("div"); if (content.setAttribute("class", "noticejs-content"), content.innerHTML = options.text, element.appendChild(content), null !== options.scroll && "" !== options.scroll.maxHeight) if (element.style.overflowY = "auto", element.style.maxHeight = options.scroll.maxHeight + "px", true === options.scroll.showOnHover) element.style.visibility = "hidden"; return element; } }, { key: "createProgressBar", value: function() { var element = document.createElement("div"); element.setAttribute("class", "noticejs-progressbar"); var bar = document.createElement("div"); if (bar.setAttribute("class", "noticejs-bar"), element.appendChild(bar), true === options.progressBar && "boolean" !== typeof options.timeout && false !== options.timeout) { var frame = function() { if (width <= 0) { clearInterval(id); var item = element.closest("div.item"); if (null !== options.animation && null !== options.animation.close) { item.className = item.className.replace(new RegExp("(?:^|\\s)" + options.animation.open + "(?:\\s|$)"), " "), item.className += " " + options.animation.close; var close_time = parseInt(options.timeout) + 500; setTimeout(function() { helper.CloseItem(item); }, close_time); } else helper.CloseItem(item); } else width--, bar.style.width = width + "%"; }; var width = 100; var id = setInterval(frame, options.timeout); } return element; } } ]), Components; })(); } ]); }); class PanoramaVideo { constructor(domContainer, domVideo) { this.domContainer = domContainer, this.domVideo = domVideo, this.camera = null, this.scene = null, this.renderer = null, this.isUserInteracting = false, this.lon = 0, this.lat = 0, this.phi = 0, this.theta = 0, this.distance = 50, this.onPointerDownPointerX = 0, this.onPointerDownPointerY = 0, this.onPointerDownLon = 0, this.onPointerDownLat = 0, this.onDocumentMouseDown = this.onDocumentMouseDown.bind(this), this.onDocumentMouseMove = this.onDocumentMouseMove.bind(this), this.onDocumentMouseUp = this.onDocumentMouseUp.bind(this), this.onDocumentMouseWheel = this.onDocumentMouseWheel.bind(this), this.onWindowResize = this.onWindowResize.bind(this), this.init(); } init() { var container; container = this.domContainer, this.camera = new THREE.PerspectiveCamera(75, this.domVideo.videoWidth / this.domVideo.videoHeight, 1, 1100), this.camera.target = new THREE.Vector3(0, 0, 0), this.scene = new THREE.Scene(); var geometry = new THREE.SphereBufferGeometry(500, 60, 40); geometry.scale(-1, 1, 1); var texture = new THREE.VideoTexture(this.domVideo); texture.minFilter = THREE.LinearFilter, texture = new THREE.MeshBasicMaterial({ map: texture }), geometry = new THREE.Mesh(geometry, texture), this.scene.add(geometry), this.renderer = new THREE.WebGLRenderer(), this.renderer.setPixelRatio(window.devicePixelRatio), this.renderer.setSize(this.domVideo.clientWidth, this.domVideo.clientHeight), container.appendChild(this.renderer.domElement), container.addEventListener("mousedown", this.onDocumentMouseDown, false), container.addEventListener("mousemove", this.onDocumentMouseMove, false), container.addEventListener("mouseup", this.onDocumentMouseUp, false), container.addEventListener("wheel", this.onDocumentMouseWheel, false), window.addEventListener("resize", this.onWindowResize, false); } onWindowResize() { this.camera.aspect = this.domVideo.videoWidth / this.domVideo.videoHeight, this.camera.updateProjectionMatrix(), this.renderer.setSize(this.domVideo.clientWidth, this.domVideo.clientHeight); } onDocumentMouseDown(event) { this.isUserInteracting = true, this.onPointerDownPointerX = event.clientX, this.onPointerDownPointerY = event.clientY, this.onPointerDownLon = this.lon, this.onPointerDownLat = this.lat; } onDocumentMouseMove(event) { if (true === this.isUserInteracting) this.lon = .1 * (this.onPointerDownPointerX - event.clientX) + this.onPointerDownLon, this.lat = .1 * (event.clientY - this.onPointerDownPointerY) + this.onPointerDownLat; } onDocumentMouseUp() { this.isUserInteracting = false; } onDocumentMouseWheel(event) { this.distance += .05 * event.deltaY, this.distance = THREE.Math.clamp(this.distance, 1, 50); } update() { this.lat = Math.max(-85, Math.min(85, this.lat)), this.phi = THREE.Math.degToRad(90 - this.lat), this.theta = THREE.Math.degToRad(this.lon), this.camera.position.x = this.distance * Math.sin(this.phi) * Math.cos(this.theta), this.camera.position.y = this.distance * Math.cos(this.phi), this.camera.position.z = this.distance * Math.sin(this.phi) * Math.sin(this.theta), this.camera.lookAt(this.camera.target), this.renderer.render(this.scene, this.camera); } } function showAlert(text) { PostbirdAlertBox.alert({ title: "提示", content: text, okBtn: "确定", contentColor: "rgb(51,51,51)" }); } function showConfirm(text, callback_confirm, callback_cancel) { PostbirdAlertBox.confirm({ title: "提示", content: text, okBtn: "确定", contentColor: "rgb(51,51,51)", onConfirm: function() { callback_confirm(); }, onCancel: function() { callback_cancel(); } }); } function showPrompt(text, callback_confirm, callback_cancel) { PostbirdAlertBox.prompt({ title: text, okBtn: "确定", onConfirm: function(data) { callback_confirm(data); }, onCancel: function(data) { callback_cancel(data); } }); } var PostbirdAlertBox = { containerClass: "postbird-box-container active", box: null, textTemplate: { title: "提示信息", content: "提示内容", okBtn: "好的", cancelBtn: "取消", contentColor: "#000000", okBtnColor: "#0e90d2", promptTitle: "请输入内容", promptOkBtn: "确认" }, getAlertTemplate: function() { var temp = '
    ' + '
    ' + '
    ' + '' + "" + this.textTemplate.title + "" + "" + '×' + "
    " + '
    ' + '' + this.textTemplate.content + "" + "
    " + '" + "
    " + "
    "; return temp; }, getConfirmTemplate: function() { return '
    ' + '
    ' + '
    ' + '' + "" + this.textTemplate.title + "" + "" + '×' + "
    " + '
    ' + '' + this.textTemplate.content + "?" + "
    " + '" + "
    " + "
    "; }, getPromptTemplate: function() { return '
    ' + '
    ' + '
    ' + '' + "" + this.textTemplate.title + "" + "" + '×' + "
    " + '
    ' + '' + "
    " + '" + "
    " + "
    "; }, alert: function(opt) { this.textTemplate.title = opt.title || this.textTemplate.title, this.textTemplate.content = opt.content || this.textTemplate.content, this.textTemplate.okBtn = opt.okBtn || this.textTemplate.okBtn, this.textTemplate.okBtnColor = opt.okBtnColor || this.textTemplate.okBtnColor, this.textTemplate.contentColor = opt.contentColor || this.textTemplate.contentColor; var box = document.createElement("div"), _this = this; box.className = this.containerClass, box.innerHTML = this.getAlertTemplate(), this.box = box, document.body.appendChild(this.box); var btn = document.getElementsByClassName("btn-footer-ok"); if (btn[btn.length - 1].focus(), btn[btn.length - 1].onclick = function() { if (opt.onConfirm) opt.onConfirm(); _this.removeBox(); }, btn = box.querySelector(".postbird-box-close-btn"), btn) btn.onclick = function() { _this.removeBox(); }; }, confirm: function(opt) { this.textTemplate.title = opt.title || this.textTemplate.promptTitle, this.textTemplate.promptPlaceholder = opt.promptPlaceholder || this.textTemplate.promptPlaceholder, this.textTemplate.okBtn = opt.okBtn || this.textTemplate.promptOkBtn, this.textTemplate.okBtnColor = opt.okBtnColor || this.textTemplate.okBtnColor, this.textTemplate.cancelBtn = opt.cancelBtn || this.textTemplate.cancelBtn, this.textTemplate.cancelBtnColor = opt.cancelBtnColor || this.textTemplate.cancelBtnColor, this.textTemplate.content = opt.content || this.textTemplate.content; var box = document.createElement("div"), _this = this; this.box = box, box.className = this.containerClass, box.innerHTML = this.getConfirmTemplate(), document.body.appendChild(box); var okBtn = document.getElementsByClassName("btn-footer-ok"); if (okBtn[okBtn.length - 1].focus(), okBtn[okBtn.length - 1].onclick = function() { if (opt.onConfirm) opt.onConfirm(); _this.removeBox(); }, okBtn = document.getElementsByClassName("btn-footer-cancel"), okBtn[okBtn.length - 1].onclick = function() { if (opt.onCancel) opt.onCancel(); _this.removeBox(); }, okBtn = box.querySelector(".postbird-box-close-btn"), okBtn) okBtn.onclick = function() { _this.removeBox(); }; }, prompt: function(opt) { this.textTemplate.title = opt.title || this.textTemplate.title, this.textTemplate.content = opt.content || this.textTemplate.content, this.textTemplate.contentColor = opt.contentColor || this.textTemplate.contentColor, this.textTemplate.okBtn = opt.okBtn || this.textTemplate.okBtn, this.textTemplate.okBtnColor = opt.okBtnColor || this.textTemplate.okBtnColor, this.textTemplate.cancelBtn = opt.cancelBtn || this.textTemplate.cancelBtn, this.textTemplate.cancelBtnColor = opt.cancelBtnColor || this.textTemplate.cancelBtnColor; var box = document.createElement("div"), _this = this; box.className = this.containerClass, box.innerHTML = this.getPromptTemplate(), this.box = box, document.body.appendChild(box); var promptInput = document.getElementsByClassName("postbird-prompt-input"); if (promptInput = promptInput[promptInput.length - 1], null != opt.defaultValue) promptInput.value = opt.defaultValue; promptInput.focus(); var okBtn = document.getElementsByClassName("btn-footer-ok"); if (promptInput.value, okBtn[okBtn.length - 1].focus(), okBtn[okBtn.length - 1].onclick = function() { if (opt.onConfirm) opt.onConfirm(promptInput.value); _this.removeBox(); }, okBtn = document.getElementsByClassName("btn-footer-cancel"), okBtn[okBtn.length - 1].onclick = function() { if (opt.onCancel) opt.onCancel(promptInput.value); _this.removeBox(); }, okBtn = box.querySelector(".postbird-box-close-btn"), okBtn) okBtn.onclick = function() { _this.removeBox(); }; }, colse: function() { this.removeBox(); }, removeBox: function() { var box = document.getElementsByClassName(this.containerClass); if (box && box.length > 0) document.body.removeChild(box[box.length - 1]); } }; function getRealLive_Bilibili(room_id, qn, cdn, reallive_callback) { let qn_data = "80"; switch (qn) { case "1": qn_data = "80"; break; case "2": qn_data = "150"; break; case "3": qn_data = "250"; break; case "4": qn_data = "400"; break; case "5": qn_data = "20000"; break; default: qn_data = "80"; break; } GM_xmlhttpRequest({ method: "GET", url: `https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo?room_id=${room_id}&platform=web&qn=${qn_data}&protocol=0,1&format=0,1,2&codec=0,1`, responseType: "json", onload: function(response) { let ret = response.response; let rurl = ""; for (let i = 0; i < ret.data.playurl_info.playurl.stream.length; i++) { const item = ret.data.playurl_info.playurl.stream[i]; if (String(item.protocol_name).includes("stream") && item.format.length > 0) { let url_info = item.format[0].codec[0].url_info[0]; let base_url = item.format[0].codec[0].base_url; rurl = `` + url_info.host + base_url + url_info.extra; } } let streamList = ret.data.durl; if (streamList) rurl = streamList.length > 0 ? streamList[0].url : ""; reallive_callback(rurl); } }); } function getRealRid_Bilibili(url, realrid_callback) { let arr = url.split("/"); let rid = arr[arr.length - 1]; GM_xmlhttpRequest({ method: "GET", url: "https://api.live.bilibili.com/room/v1/Room/room_init?id=" + rid, responseType: "json", onload: function(response) { let ret = response.response; realrid_callback(ret.data.room_id); } }); } function getRealLive_Douyu(room_id, is_video, is_https, qn, reallive_callback) { let did = getCookieValue("dy_did") || "10000000000000000000000000001501"; GM_xmlhttpRequest({ method: "GET", url: `https://www.douyu.com/wgapi/livenc/liveweb/websec/getEncryption?did=` + did, responseType: "json", onload: function(res) { if (0 !== res.response.error) return reallive_callback("None"); let d = res.response.data; let ts = Math.round(Date.now() / 1e3); let auth = getDouyuRealLiveAuth(room_id, did, ts, d.key, d.rand_str, d.enc_time, d.is_special); let rate = "1428" == qn ? "-1" : qn; let postData = `enc_data=${d.enc_data}&tt=${ts}&did=${did}&auth=${auth}&cdn=&rate=${rate}&hevc=0&fa=0&ive=0`; GM_xmlhttpRequest({ method: "POST", url: `https://www.douyu.com/lapi/live/getH5PlayV1/` + room_id, data: postData, responseType: "json", headers: { "Content-Type": "application/x-www-form-urlencoded" }, onload: function(res) { let ret = res.response; if (0 === ret.error) { let url = ret.data.rtmp_url + `/` + ret.data.rtmp_live; reallive_callback(is_video ? url : url + "&only-audio=1"); } else reallive_callback("None"); } }); } }); } function getDouyuRealLiveAuth(rid, did, ts, key, rand_str, enc_time, is_special) { let i = 1 === is_special ? "" : `` + rid + ts; let f = rand_str; for (let p = 0; p < enc_time; p++) f = hex_md5(f + key); return hex_md5(f + key + i); } function getRealRid_Douyu(url, realrid_callback) { fetch(url, { method: "GET", mode: "no-cors", cache: "default", credentials: "include" }).then(res => res.text()).then(doc => { doc = new DOMParser().parseFromString(doc, "text/html"); let url = doc.getElementsByTagName("html")[0].innerHTML; let urlLen = "$ROOM.room_id =".length; let ridPos = url.indexOf("$ROOM.room_id ="); let rid = ""; if (ridPos > 0) rid = url.substring(ridPos + urlLen, url.indexOf(";", ridPos + urlLen)), rid = rid.trim(); else if (rid = getStrMiddle(url, `roomID:`, `,`), rid) rid = rid.trim(); else { let canonicalLink = doc.querySelector(`link[rel="canonical"]`); if (canonicalLink) { let href = canonicalLink.getAttribute(`href`); rid = href.split("/").pop().trim(); } } if (true == isRid(rid)) realrid_callback(rid); else showMessage("获取直播间失败,请检查直播间地址是否正确!", "error"); }).catch(err => { console.log("请求失败!", err); }); } function getRealLive_Huya(rid, qn, reallive_callback) { let qn_data = "500"; switch (qn) { case "1": qn_data = "500"; break; case "2": qn_data = "2500"; break; case "3": qn_data = "4500"; break; case "4": qn_data = "0"; break; default: qn_data = "500"; break; } GM_xmlhttpRequest({ method: "GET", url: "https://mp.huya.com/cache.php?m=Live&do=profileRoom&roomid=" + rid, responseType: "json", onload: ret => { let msg = ""; let liveUrl = ""; let multiLine = ret.response.data.stream.flv.multiLine; if (multiLine.length && multiLine.length > 0) liveUrl = multiLine[0].url.replace("http", "https"); if (null == liveUrl || "" == liveUrl) msg = "房间暂未开播"; reallive_callback(liveUrl, msg); } }); } let responseHookCallbackList = []; let requestBodyMap = new WeakMap(); function initResponseHook() { const originalSend = unsafeWindow.XMLHttpRequest.prototype.send; unsafeWindow.XMLHttpRequest.prototype.send = function(body) { requestBodyMap.set(this, body), originalSend.call(this, body); }; let accessor = Object.getOwnPropertyDescriptor(unsafeWindow.XMLHttpRequest.prototype, "responseText"); Object.defineProperty(unsafeWindow.XMLHttpRequest.prototype, "responseText", { get: function() { let text = accessor.get.call(this); let requestBody = requestBodyMap.get(this); for (var callback of responseHookCallbackList) { let newText = callback(this.responseURL, text, requestBody); if (void 0 !== newText) text = newText; } return requestBodyMap.delete(this), text; }, configurable: true }); } function responseHook(callback) { responseHookCallbackList.push(callback); } let scriptHookCallbackList = []; function applyInlineScriptHooks(node) { if ("SCRIPT" !== node.tagName || node.src || !node.textContent) return node; const inlineCallbacks = scriptHookCallbackList.filter(item => item.inline); if (0 === inlineCallbacks.length) return node; let content = node.textContent; for (let i = 0; i < inlineCallbacks.length; i++) content = inlineCallbacks[i].callback(content); if (content !== node.textContent) node.textContent = content; return node; } function handleExternalScript(node, insertFn, targetNode) { const src = node.src; const callbacks = []; for (let k = 0; k < scriptHookCallbackList.length; k++) { const item = scriptHookCallbackList[k]; if (!item.inline && src.includes(item.url)) callbacks.push(item); } if (0 === callbacks.length) return false; return fetchAndReplace(src, callbacks, targetNode), true; } function initScriptHook() { const originalAppendChild = Node.prototype.appendChild; const originalInsertBefore = Node.prototype.insertBefore; Node.prototype.appendChild = function(node) { if (node && "SCRIPT" === node.tagName) { if (node.src && handleExternalScript(node, originalAppendChild, this)) return node; node = applyInlineScriptHooks(node); } return originalAppendChild.call(this, node); }, Node.prototype.insertBefore = function(node, referenceNode) { if (node && "SCRIPT" === node.tagName) { if (node.src && handleExternalScript(node, originalInsertBefore, this)) return node; node = applyInlineScriptHooks(node); } return originalInsertBefore.call(this, node, referenceNode); }; } function fetchAndReplace(url, callbacks, targetNode) { GM_xmlhttpRequest({ method: "GET", url: url, onload: function(response) { let modifiedContent = response.responseText; for (let m = 0; m < callbacks.length; m++) modifiedContent = callbacks[m].callback(modifiedContent); const newScript = document.createElement("script"); newScript.type = "text/javascript", newScript.textContent = modifiedContent, targetNode.appendChild(newScript); }, onerror: function(err) { console.error("Error loading script via GM_xmlhttpRequest:", err); } }); } function scriptHook(callback) { scriptHookCallbackList.push(callback); } function stt_unescape(v) { if (!v) return; return v.toString().replace(/@S/g, "/").replace(/@A/g, "@"); } function stt_deserialize(raw) { if (!raw) return; if (raw.includes("//")) return raw.split("//").filter(e => "" !== e).map(item => stt_deserialize(item)); if (raw.includes("@=")) return raw.split("/").filter(e => "" !== e).reduce((o, s) => { const [ k, v ] = s.split("@="); return o[k] = stt_deserialize(stt_unescape(v)), o; }, {}); else if (raw.includes("@A=")) return stt_deserialize(stt_unescape(raw)); else return raw.toString(); } function StyleHook_set(styleName, styleText) { if (null != document.getElementById(styleName)) return; let host = document.body || document.head || document.documentElement; if (!host) return document.addEventListener("DOMContentLoaded", function() { StyleHook_set(styleName, styleText); }, { once: true }), void 0; let styleElement = document.createElement("style"); styleElement.id = styleName, styleElement.innerHTML = styleText, host.append(styleElement); } function StyleHook_remove(styleName) { let e = document.getElementById(styleName); if (null !== e) document.getElementById(styleName).remove(); } function StyleHook_setIframe(dom, styleName, styleText) { if (null == dom.getElementById(styleName)) { let host = dom.body || dom.head || dom.documentElement; if (!host) return; let styleElement = dom.createElement("style"); styleElement.id = styleName, styleElement.innerHTML = styleText, host.append(styleElement); } } function StyleHook_removeIframe(dom, styleName) { let e = dom.getElementById(styleName); if (null !== e) dom.getElementById(styleName).remove(); } function WebSocket_Packet(str) { const MSG_TYPE = 689; let bytesArr = stringToByte(str); let buffer = new Uint8Array(bytesArr.length + 4 + 4 + 2 + 1 + 1 + 1); let p_content = new Uint8Array(bytesArr.length); for (let i = 0; i < p_content.length; i++) p_content[i] = bytesArr[i]; let p_length = new Uint32Array([ bytesArr.length + 4 + 2 + 1 + 1 + 1 ]); let p_type = new Uint32Array([ MSG_TYPE ]); return buffer.set(new Uint8Array(p_length.buffer), 0), buffer.set(new Uint8Array(p_length.buffer), 4), buffer.set(new Uint8Array(p_type.buffer), 8), buffer.set(p_content, 12), buffer; } function stringToByte(str) { let bytes = new Array(); let len, c; len = str.length; for (let i = 0; i < len; i++) if (c = String(str).charCodeAt(i), c >= 65536 && c <= 1114111) bytes.push(c >> 18 & 7 | 240), bytes.push(c >> 12 & 63 | 128), bytes.push(c >> 6 & 63 | 128), bytes.push(63 & c | 128); else if (c >= 2048 && c <= 65535) bytes.push(c >> 12 & 15 | 224), bytes.push(c >> 6 & 63 | 128), bytes.push(63 & c | 128); else if (c >= 128 && c <= 2047) bytes.push(c >> 6 & 31 | 192), bytes.push(63 & c | 128); else bytes.push(255 & c); return bytes; } function byteToString(arr) { if ("string" === typeof arr) return arr; let str = "", _arr = arr; for (let i = 0; i < _arr.length; i++) { let one = _arr[i].toString(2), v = one.match(/^1+?(?=0)/); if (v && 8 == one.length) { let bytesLength = v[0].length; let store = _arr[i].toString(2).slice(7 - bytesLength); for (let st = 1; st < bytesLength; st++) store += _arr[st + i].toString(2).slice(2); str += String.fromCharCode(parseInt(store, 2)), i += bytesLength - 1; } else str += String.fromCharCode(_arr[i]); } return str; } function hex2bin(e) { if ("string" === typeof e && e.length % 8 === 0) { for (let r = [], t = e.length, o = 0; o < t; ) r.push(e.substr(o, 2)), o += 2; for (let n = [], i = r.length, s = 0; s < i; ) n.push(parseInt(r.slice(s, s + 4).reverse().join(""), 16)), s += 4; return n; } return null; } function hex(e) { if (Array.isArray(e)) { let r = "0123456789abcdef".split(""); return e.map(function(e) { for (let t = "", o = 0; o < 4; o++) t += r[e >> 8 * o + 4 & 15] + r[e >> 8 * o & 15]; return t; }).join(""); } return ""; } class Ex_WebSocket_UnLogin { constructor(rid, msgHandler) { if ("WebSocket" in window) this.timer = 0, this.rid = rid, this.msgHandler = msgHandler, this.reconnectCount = 0, this.maxReconnect = 10, this.closed = false, this.connect(); } connect() { this.ws = new WebSocket("wss://danmuproxy.douyu.com:850" + String(getRandom(2, 5))), this.ws.onopen = () => { this.reconnectCount = 0, this.ws.send(WebSocket_Packet("type@=loginreq/roomid@=" + this.rid)), this.ws.send(WebSocket_Packet("type@=joingroup/rid@=" + this.rid + "/gid@=-9999/")), this.timer = setInterval(() => { this.ws.send(WebSocket_Packet("type@=mrkl/")); }, 4e4); }, this.ws.onerror = () => { if (this.closed || !this.ws) return; try { this.ws.close(); } catch (e) {} }, this.ws.onmessage = e => { if (this.closed) return; let reader = new FileReader(); reader.onload = () => { if (this.closed) return; let arr = String(reader.result).split("\0"); reader = null; for (let i = 0; i < arr.length; i++) if (arr[i].length > 12) this.msgHandler(arr[i]); }, reader.readAsText(e.data); }, this.ws.onclose = () => { if (clearInterval(this.timer), this.timer = 0, this.ws = null, !this.closed) this.reconnect(); }; } reconnect() { if (this.closed || this.reconnectCount >= this.maxReconnect) return; this.reconnectCount++; const delay = Math.min(3e3 * Math.pow(1.5, this.reconnectCount - 1), 6e4); setTimeout(() => { if (!this.closed) this.connect(); }, delay); } close() { if (this.closed) return; if (this.closed = true, clearInterval(this.timer), this.timer = 0, !this.ws) return; const socket = this.ws; this.ws = null, socket.onclose = null, socket.onerror = null, socket.onmessage = null; try { if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) socket.close(); } catch (e) {} } } function initRouter(href) { if (-1 !== String(href).indexOf("yuba.douyu.com")) if (-1 !== String(href).indexOf("?exRestore")) initPkg_RestoreYuba_restore(); else if (-1 !== String(href).indexOf("?exClean")) initRouter_CleanYuba(); else initRouter_Yuba(); else if (-1 !== String(href).indexOf("passport.douyu.com") && -1 !== String(href).indexOf("exid=chun")) initRouter_Passport(); else if (-1 !== String(href).indexOf("msg.douyu.com")) { if (-1 !== href.indexOf("?exClean")) initRouter_CleanMsg(); } else if (-1 !== String(href).indexOf("v.douyu.com")) { if (-1 !== String(href).indexOf("?exClean")) initRouter_CleanVideo(); else if (-1 !== String(href).indexOf("show/")) initRouter_Video(); } else if (-1 !== String(href).indexOf("cz.douyu.com")) { if (-1 !== String(href).indexOf("?exClean")) initRouter_CleanCz(); } else if (-1 !== String(href).indexOf("getFansBadgeList")) initRouter_FansBadgeList(); else if (-1 !== String(href).indexOf("exid=chun")) initRouter_DouyuRoom_Popup(); else { if (-1 !== String(href).indexOf("template/") || -1 !== String(href).indexOf("h5/")) return; initRouter_DouyuRoom_Main(); } } function initRouter_DouyuRoom_Popup() { removeAD(); let tries = 0; let intID = setInterval(() => { let wfs = document.querySelector("div.wfs-2a8e83"); let aside = document.querySelector("label.layout-Player-asidetoggleButton"); let items = document.querySelectorAll(".tip-e3420a > ul > li"); if (wfs && aside && items.length > 0) return clearInterval(intID), wfs.click(), aside.click(), items[items.length - 1].click(), void 0; if (++tries > 20) clearInterval(intID), console.warn("[DouyuEx] 画中画入口等不到,本次放弃(不影响其它功能)"); }, 1e3); } function initRouter_DouyuRoom_Main() { document.domain = "douyu.com", init(); let waited = 0; let intID = setInterval(() => { let dom1 = document.getElementsByClassName("BackpackButton")[0]; let dom2 = document.getElementsByClassName("Barrage-main")[0]; let dom3 = document.querySelector("#js-backpack-enter"); waited++; const ready = !!dom2 && (!!dom1 || !!dom3); if (!ready && waited < 15) return; if (!ready) console.warn("[DouyuEx] 未等到预期页面元素(弹幕区: " + !!dom2 + ",背包入口: " + (!!dom1 || !!dom3) + "),已等待 " + waited + " 秒,仍继续初始化"); setTimeout(() => { initStyles(), initPkg(), initPkgSpecial(), initTimer(); }, 1500), clearInterval(intID); }, 1e3); } function initPkgSpecial() {} function initRouter_Yuba() { document.domain = "douyu.com", RestoreYuba_checkRedirect(); } function initRouter_Passport() { let cmd = getStrMiddle(window.location.href, "cmd=", "&"); let uid = getStrMiddle(window.location.href, "uid=", "&"); let domain = getStrMiddle(window.location.href, "domain=", "&"); if ("clean" !== cmd) addAccountPassport(uid); switch (cmd) { case "clean": cleanCookie(() => { window.parent.postMessage("cleanOver", decodeURIComponent(domain)); }); break; case "switch": switchAccountPassport(uid, () => { window.parent.postMessage("switchOver", decodeURIComponent(domain)); }); break; case "delete": deleteAccountPassport(uid, () => { window.parent.postMessage("deleteOver", decodeURIComponent(domain)); }); break; default: break; } return; } function initRouter_CleanMsg() { let domain = getStrMiddle(window.location.href, "domain=", "&"); cleanCookie(() => { window.parent.postMessage("msgCleanOver", decodeURIComponent(domain)); }); } function initRouter_CleanYuba() { let domain = getStrMiddle(window.location.href, "domain=", "&"); cleanCookie(() => { window.parent.postMessage("yubaCleanOver", decodeURIComponent(domain)); }); } function initRouter_CleanVideo() { let domain = getStrMiddle(window.location.href, "domain=", "&"); cleanCookie(() => { window.parent.postMessage("videoCleanOver", decodeURIComponent(domain)); }); } function initRouter_CleanCz() { let domain = getStrMiddle(window.location.href, "domain=", "&"); cleanCookie(() => { window.parent.postMessage("czCleanOver", decodeURIComponent(domain)); }); } function initRouter_Video() { if (unsafeWindow.$DATA && "ROOM" in unsafeWindow.$DATA) initStyles(), initPkg_VideoTime(), initPkg_VideoTools_Camera_Video(), initPkg_DyVideoDownload(), initPkg_DyVideoBarrageLine(); } function initRouter_FansBadgeList() { initPkg_FansBadgeList(); } (async () => { initRouter(window.location.href); })();