// ==UserScript== // @name B站 · 隐藏右侧推荐栏 + 自定义进度条 // @namespace https://local/bbhr // @version 1.16.9 // @description 隐藏B站播放页右侧推荐栏并放大播放器(16:9);进度条加粗到25px、默认彩虹渐变(可换纯色/透明度)、悬停标记可自定义图片;视频操作栏竖排到播放器左侧;隐藏首页大轮播;滚到评论区小窗正常浮出。页面左上角悬浮按钮(Q版奥特曼)鼠标移上展开设置面板,六个开关互不影响,快捷键 Shift+H/G/P/R/B/U。播放器左上角UP主头像与悬浮卡照常。 // @author 兄弟 // @match *://www.bilibili.com/* // @run-at document-start // @grant none // @license MIT // ==/UserScript== /* ================= 更新日志 ================= (v1.16.9 起从 @description 移到这里 —— GreasyFork 限制 @description 最多 500 字, 原超长描述全文如下,逐版历史完整保留。) 隐藏B站播放页右侧的推荐栏(推荐视频 / UP主卡片 / 广告),左栏与播放器自动放大并保持16:9;播放器进度条加粗到 25px,已播放段默认是彩虹渐变(色谱钉在整条进度条上、随播放从左往右揭开),可换成任意纯色并调透明度(默认半透明,画面能透出来),悬停时的「小电视」标记也换成了自定义图片。视频操作栏(点赞/投币/收藏/分享 + 稿件举报/记笔记/更多)整体搬到播放器左边的空白处竖排成一列,原位置不再占高度;播放器滚出屏幕时(滚到评论区)B站原生的右下角小窗能正常浮出来,不会被竖排栏或放大规则挤坏。首页首屏那个大轮播(bilibili公开课…)也一并隐藏,剩下的视频卡由 CSS Grid 自己往前补满整行,不留空档;设置面板在首页也会出现。页面最左边垂直居中处有悬浮按钮(图标是 Q 版奥特曼宝宝,位置固定不动,竖排操作栏会让开它),鼠标移上去就展开浅色设置面板,鼠标移开自动收起(面板右上角也有关闭按钮),里面是六个互不影响的开关(隐藏右栏 / 播放器放大 / 进度条加粗 / 操作栏竖排 / 首页轮播 / UP主悬浮卡),带中文说明和快捷键提示,也可用 Shift+H、Shift+G、Shift+P、Shift+R、Shift+B、Shift+U。鼠标移到播放器左上角那个小「+关注」按钮上,右侧栏的 UP主卡片(头像/昵称/发消息/签名/充电+关注)会浮到按钮下方,鼠标移开自动收回。进度条圆点(小摩托)稳稳贴在进度条上、不再上下浮动(v1.16.5 起),车尾同步喷出一柱彩虹烟雾、随播放飘散放大,暂停时整体冻结(与播放器中间那辆大摩托的尾气同款效果);小摩托只在进度条出现时出现、进度条收起时随之一并消失;滚到评论区后在右下角浮出的那个小窗,它的进度条颜色也跟着主窗口走(同一套彩虹 / 纯色与透明度),不再是小窗原生的实心蓝。v1.16.4 起小摩托不用再等到鼠标压到进度条:鼠标一进播放器范围它就出来,鼠标离开播放器才跟着进度条一起收走(右下角的小窗保持原样)。v1.16.1 修三个真实浏览器里的回归:设置悬浮按钮被右栏(同为顶格 z-index 且 DOM 顺序靠后)压住而消失 —— 右栏默认态改回 display:none、悬浮按钮改挂到 上不再被 B站 重写 body 拔走;滚到评论区小窗不出 —— 常驻的离屏+顶格 z-index 规则撤掉、只在悬停「关注」时临时启用;UP主卡浮不出 —— 离屏规则显式摁掉 transform/filter,防止右栏变成卡片的包含块把卡片裁掉。v1.16.2 找到三个回归的真正根因(真机控制台的 ReferenceError 实锤):apply() 的顶层同步首次调用踩中「暂时性死区」——它用到的 RAINBOW_TARGETS / upCard / upCardBound 等变量声明在调用点之后,油猴在 已存在的 document-start 注入时脚本从那一行起整体失效(悬浮按钮、UP卡事件绑定、竖排栏循环全没注册,三个现象同源);修法是首次调用挪到文件末尾(所有声明初始化完之后)+ apply() 里五个功能调用各自 try/catch,任何单个功能出错都不再团灭整个脚本。v1.16.5 按需求去掉进度条小摩托的上下浮动(骑行颠簸),彩虹烟雾和其余一切保持原样。v1.16.6 调整设置面板开关顺序:「隐藏右侧推荐栏」「播放器自适应放大」挪到第四、五行,快捷键与开关功能本身不变。v1.16.7 播放器左上角的 UP主头像即使已关注也照常显示——鼠标移进播放器就浮出来(头像 + 关注按钮胶囊,与未关注时同款),移开播放器即收走;显示后把鼠标移到头像上,原有的 UP主悬浮卡功能照常可用。v1.16.8 修复已关注 UP主鼠标进播放器头像仍不显示的问题——根因是已关注时 B站 把整段 follow 区(父 .bpx-player-top-left-follow 乃至 .bpx-player-top-wrap)藏成 display:none,只拉内层会被父级塌缩吃掉,故父级也一并 !important 拉出。v1.16.9 把左侧那颗悬浮的奥特曼设置按钮从「页面左侧垂直居中」挪到「页面左上角、bilibili 顶栏 logo 正下方」(left:10px / top:70px),并配套把 .wrap 改为 align-items:flex-start,保证展开设置面板时按钮不位移、面板向下展开。 =========================================== */ (function () { 'use strict'; /* ================= 配置 ================= */ const KEY_ON = 'bbhr:on'; // 隐藏右栏 + 播放器放大 const KEY_GROW = 'bbhr:grow'; // 是否同时放大播放器 const KEY_THICK = 'bbhr:thick'; // 进度条加粗 const KEY_RAIL = 'bbhr:rail'; // 操作栏改成播放器左侧竖排 const KEY_BANNER = 'bbhr:nobanner'; // 首页首屏那个大轮播隐藏掉 const KEY_UPCARD = 'bbhr:upcard'; // 悬停播放器左上角「关注」→ 浮出 UP主卡片 const STYLE_ID = 'bbhr-style'; // 播放器下方要预留的高度(顶部导航 + 标题/信息区)。 // 窗口越矮,算出来的播放器越小,保证一屏能看全。 const CHROME_RESERVE = 175; // 进度条粗细(px)。想细一点就改这里;B站原生是 4px。 // 建议别低于 20px —— B站的拖拽圆点是固定 20px 高,条比它矮的话圆点会上下溢出去。 const PROGRESS_BAR_H = 25; /* ---------------- 操作栏竖排(播放器左侧) ---------------- B站的视频操作栏(点赞/投币/收藏/分享 + 稿件举报/记笔记/更多)原生是 播放器下方的一整行。开启这项之后整条搬到播放器左边的空白处、竖着排一列。 RAIL_W 竖排栏宽度(px)。必须和下面 CSS 里的 width 对上(同一份常量在写 CSS)。 RAIL_GAP 竖排栏右边缘 → 播放器左边缘留多少空隙。 RAIL_MIN_X 竖排栏离视口边缘的最小留白(上下夹紧时用它),也是绝对下限。 FAB_LEFT 「设置按钮」固定在页面最左边时的 left(px)。 ⚠ 必须和下面 UI_CSS 里 :host 的 left 是同一个值(改一处要改两处)。 FAB_TOP 「设置按钮」固定的 top(px)。v1.16.9 起按钮从「左侧垂直居中」 挪到页面左上角、bilibili 顶栏 logo 正下方(70 ≈ 顶栏 56 + 一点余量)。 ⚠ 必须和下面 UI_CSS 里 :host 的 top 是同一个值。 FAB_SIZE 设置按钮的宽高(px,正方形)。必须和 UI_CSS 里 .fab 的 width/height 一致。 FAB_GAP 设置按钮这一列 → 竖排栏 之间必须留的空隙。 窄窗口里播放器左边挤不出空隙时,竖排栏最多往右压到 「按钮右边缘 + FAB_GAP」,绝不会盖到按钮上。 按钮的位置**恒定不动**:竖排操作栏开也好关也好、设置面板展开也好, 它都待在页面左上角那个固定位置 —— 要让位的是竖排栏,不是按钮。 */ const RAIL_W = 56; const RAIL_GAP = 12; const RAIL_MIN_X = 6; const FAB_LEFT = 10; const FAB_TOP = 70; const FAB_SIZE = 54; const FAB_GAP = 10; /* ---------------- 进度条配色(可自定义) ---------------- 要改进度条颜色 / 透明度,只动下面这几行就行,改完刷新页面生效。 BAR_FILL 已播放部分(进度条走过的那一段)怎么上色: 'rainbow' -> 彩虹渐变(默认) 其它任意串 -> 走纯色模式,用下面的 BAR_COLOR 想换回之前那种纯绿色就把这行改成 '#03de6d' 或 'solid'。 BAR_RAINBOW 彩虹的色带,按顺序从左到右铺开。默认 7 色(红橙黄绿青蓝紫)。 想少几色、或者换成别的色系,直接改这个数组即可(至少 2 个色)。 BAR_COLOR 纯色模式下用的颜色(BAR_FILL 不是 'rainbow' 时生效)。 B站原生是它家主色 #00AEEC(亮蓝);这里前几版用的是 #03de6d(亮绿)。 BAR_ALPHA 已播放部分的不透明度 alpha:0 = 全透明,1 = 完全不透明 默认 0.45 就是「45% 实 + 55% 透」,底下的画面能透出来。 ⚠ 彩虹是好几段颜色叠着半透明,比纯绿色更容易发灰, 觉得不够艳就先把这里调到 0.6~0.7 —— 见效最快的就这一个值。 BAR_ACCENT 拖动进度条时上下冒出来的那两个小三角的颜色。 纯色模式下跟着进度条走;彩虹是渐变、而三角形只能单色(见下面 CSS 里的说明), 所以单独给一个中性白,深色浅色画面上都看得见。 BAR_BUFFER_COLOR 缓冲部分(进度条里偏白的那一段) BAR_TRACK_COLOR 轨道底(还没缓冲到的部分) 这两条不想动就保持默认(就是 B站原生值)。 举例: 想换成半透明粉紫 -> BAR_FILL = '#c56cff'; BAR_ALPHA = 0.35; 想彻底看不见进度条 -> BAR_ALPHA = 0; 想回到原来那种实心 -> BAR_ALPHA = 1; 彩虹想更艳一点 -> BAR_ALPHA = 0.65; ------------------------------------------------------- */ const BAR_FILL = 'rainbow'; const BAR_RAINBOW = ['#ff3b30', '#ff9500', '#ffcc00', '#34c759', '#00c7be', '#007aff', '#af52de']; const BAR_COLOR = '#03de6d'; const BAR_ALPHA = 0.45; const BAR_BUFFER_COLOR = 'rgba(255, 255, 255, .3)'; const BAR_TRACK_COLOR = 'rgba(255, 255, 255, .2)'; /* ---------------- 进度条圆点图标(自定义图) ---------------- 鼠标悬停进度条时,跟着光标跑的那个小标记。B站原生画的是「白色圆角方块 + 深色小电视 Logo」,而且是一段 Lottie 动画,整个(方块带 Logo)都塞在 一段 里。下面这张图就是把它整个换掉。 PROGRESS_ICON_SIZE 图标边长(px)。B站原生的圆点容器固定是 20×20, 这里用「固定宽高 + 负 margin 居中」的方式把它放大, 26 大致和加粗后的进度条(25px)齐高。 设成 0 就整段不生效,回到 B站原生那个小电视。 PROGRESS_ICON 图片本体(base64 data URI): 从 1080×1080 的原始图片抠的 —— 裁掉四周留白、纯白背景 转全透明、去掉白边、再把透明处的颜色向外扩散了一圈 (不做这步,缩放时白色会混进边缘糊出一圈白描边)。 想换图:抠成透明底 PNG -> base64 -> 整串替换掉即可。 ------------------------------------------------------------- */ const PROGRESS_ICON_SIZE = 26; const PROGRESS_ICON = 'data:image/png;base64,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'; /* B站原生的圆点容器就是 20px,我们改尺寸之后要补一个偏移量才能保持居中 */ const ICON_NATIVE_BOX = 20; const ICON_PAD = (ICON_NATIVE_BOX - PROGRESS_ICON_SIZE) / 2; /* ---------------- 悬浮按钮图标(和进度条那张是两码事) ---------------- 用户指定的 Q 版奥特曼宝宝,只喂给页面最左边那颗悬浮按钮。 上面的 PROGRESS_ICON 只喂给进度条上的圆点标记 —— 两边彻底解耦, 以后想单独换哪一张都不会牵连到另一张。 抠图流程同一套:裁掉四周留白 -> 近白背景 floodfill 成透明 -> 去白边(两轮,这张是 JPEG 压噪大)-> 透明区颜色扩散 -> 缩到 160px 高 -> base64。 想换图:抠成透明底 PNG -> base64 -> 整串替换掉即可。 */ const FAB_ICON = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAG0AAACgCAYAAAAGjuI8AABzYklEQVR42uydd3wc1dX+v/fOzFaterFkSe69N1zBNsWAMSUEC9Mh9EAoCSEBArYDBEiB0EvoHQtM78U2BoN7w70Xyept+87Mvb8/VnaAUExL8r6/d/jMR8aWVrvz3HvuOc855zmC/w8urbWorKyU7zW/J1kKS5cu3fdvS5cudcePHy979eolik8q1jMmzFBCCMX/Xf9+kLTWYvr06SYgv+l7fT7fV/79rKlTjb2v89/2+cT/dgAtyyKVSgWWr1s+dvmK5RMWzP+IVas/E02NjcJVjj788COGBvzBbZ06lTX36ddvy6SDJi3MCGV8Fo1E/2s/0/8q0PburBkzZvgA9dq7r5301jtvHrvo04V9Ghqbumql8Pn9GFKSSCbQSmMYJsFAAIXC8njI9mXQvaR0ed8evR+55A9/eAGIzpgxI7x27Voxa9YsJYTQ/wfaj3DNmjXLyMjICE6ePLlNax169sVnb372mWeOXLZ8eZdkMsHIkSOZMH6CGjBogOrcqTM5OTkolT62hBDCdV3d1tpGPBITqz9eaDx5+13Y0QjFPXpEBo4e9uiV1998vRCibu/vqqiocP/vEPqBgLWfYx0/+vTT26edctLmrPxc3atvH339zTfa67euc7TWrm6/lHa1rVPa0bZ2tK1TOqltndJaK621rbXWeseKue4Hf53hXHXgIfqgDln6+PETWp569NE7tNaB/1+OlX8HYGW33X3HU8NGHqA7dOyob7jpJqeuqcnVWuukTuhwsk23Jlp0zI3qmIrquIr9yx1zozqRjOhwJK6rN3+i9bZPtd60Rr39+E3OtBE99biOHfWfrvz1xni85pB2Uyz/D4Hv7iHuBaz3jJuuq8rMzdIjx4xJrVqzxtVa64Rj63A8qiN2WEfscBoUHf+GO6ZtJ6Frd9Xo8w8foZ87e4pefv1VWi15Tre9/5C667ij7UFev77k0rP1rtatIwHmzJlj/h8S+w+Y3AvYpVdeUgXoE6ZNtZvb2rTWWkeTCZ1000bwm4H65x1XUe3olK7ZtFVfPqizfm9sH/16x0K9fMoA3fDnC7Se/ah++NQTnc4S58JfnNrc2tg4+vO7/d95mf8TARNCKK11r6um//792/98R8kZ557u/uO+h0ylIJyI47EsQKKV+pYo7fOvC65Q1NftxEpGKM/yYngFGS011M+eRfPIHVSMG2wEWw5Uf3lyVvYNOYXPaq0PE0Js2vue/g+0rwmaKyorhNbauu3uWx++7dbbSs69+Bzn7jvvMZVWOMpBSkAoEBrQ3+g2i899hwQMTLbvrsJKRbHIIGEoAtLBKxPsXPkBkchuDulULnf16+w8+cRj5Y8M6P2Ax+OZUFlZ+X/n27c5HvMXzru7W5+u+mfTjkvFnZiOObH9OLP+ead0XDs68YU75US11lrfOfNq/XBfv64d1VFv7FeiY2MzdWScpeMHGrpxDLqxordef/I4/YsCv33cQePdpp11B/27zaT5P8wsulrr3sdMPfq8YDCoHn7wYVMIQSIZ/1o6an8vKQ00cWIbFzMwaGHFo2R7TISw8STBQGFJQaRqB3mBCL/o15VrlyyVN93zl5la60MrKir+70z78jVjxgwppVRXz7i6YuXKlWZlZaWTFcwyI3YEr9f7tT+nlEJrjdYapRRSSEyZtmZK632mUkqD5qot+Ks308lvohJRgj4LlI0WaTMqtCboJkhFqunnt4yRfqEXfDjnwN0ku86aNWsz8G852+T/EHpKAsp13eJPPlnw+6OnHM2IQSPMmBPDY3mQUiKE+ErAXNfFY3gImhmEPJkErQyklFiGD68ZxGsG8ZgBTOmj6rM15EcbsCQkPAJh2GhXkDIgYUDKACVAoPDbjWJiWYZr7dwhX7jtrp8JIXRFRYX4/36n7WXYL7nkEuvOO+9MKkv9vKmlyX/Z5Ze5MTtmaDQCgf6Sw6GUQgiBXwbQUrFi7Qo+WbCAFStXUl1VTVtrKwGvj9KyUrp27Ur3bl3p2bMfn334CT0yM/CYiriRAYlWDMcATwqNwMVECYlUCu0m6FOQI7pUh8XS9+dM1Fq/cOutt1ZXVlbG/78G7XPkbFJr3fHQyZOuHDt6nO5e1k20xFvx+Xz7ANOuQGtQysXn8WEgee2t17nnvvvYsWMHfp+fnNwcysrK6dy5B02NjUTjNp8uWs5Lr7xBQ2MTmdVVDLESdM0K0js3kxHBzhQHJdi1aCOJjY0UYAgvIuUQSsVF35DFe9t3DAWC48ePd7TW4qcmlc3/VrZ+5syZSmttAbl//futF5559i9+uXb1ury//e2vWoO0LOsLP+O4LriCDH+ADZs387vf/461n61h8jHHcNppZ5GREUQIgQYMKVFa4zoO0jBIxBPU1tZQu3snWzatZ86WzczZsZ3Z0Tp6BLwML89lYEGAIrsZI9wAJFGGiTa0KM42dcg2Cj/9dK1/9OjhdrsX6f5/AVp7dtmqqKiwZ86c6WitR95wy00XL/h4wdEFBQVZS5csYcjQIXpAn/4iruIYhvH5n0UgCPr9PPL4Y/z2yt8y/qDxPPzoIyANautqCYfDuK6LEAKlFF6vF5/Ph2kY5OXlUl5ehhg9Dke5uE6ctqoq1sz/hDULFzN37SrK1u7kqA4ZHFSSR643iZuKQyouunqDTpGdMuL1uw4FFhYUFIj/7eZRAHq6nr7X60pprQfecvtNPz/iuCP+4JF+efLJJzP1hBOcI448wpg0aZKQSNAgZPrhJ5NJTNPE5wnw69/+lgcf/AczZs5kwoQJbN26ldZwBI/Hg9frJScnh/z8fHJyssnOziEjGEQaBnvhF27a2DpCw+DhHH7kMbQ1NLF67Wd89N5bPP/O6zy/ci3jS3M5rGNnurltdHQkRcmwWLPgnSTA3Llz/9c7Inr69OnmTDHT0Vp7X33n5V8eMvmQXwlJl+OP+7k+47Sz3KDXL5evXmWuX7+BiRMnotH7PEWtFaZh4jW9nHv+ubz95jvMfulFMkOZLF++HL8/QHFxMSUlJRQXFxMKZWKZBrKdCXGVRmtw2s9FlwS2dnA1OFpgShOrIIuxB49hzCETafzDNSx66y2e+9vtfLBwGRWdchjRqSNmKkxtU4OhtRYzZsz4313H8e6Sd7Pa/zz0d9dduXL0hJH6kt9eonfV7LS11rol2qYdx9F3P3CvHjZyhE44KR1z4jqpEzqu4jqcCmullT73onN0WddOesmKFfrdOR/ofzzyiH7ng/f1hq1bdFsirlNa65TWOqFcHXNsHXcdnVBu+tauTmqlk1rphBPXCTui405MR1NxHUsmdTwZ0+FEvY7EWnUq4WittY5HmvVjjz+oD+3WWZ+S4bHPHTlEz7z26osBbr/9du//NqyE1lp8Lg9WVPlK5eUTjpjQeNiUQ/UH899Paa2Vo20dscM6nAprrbU+71fn6VN+cbLWWutwMqyjybgOJ2Jaa61vuPlPurRTmV69foN+/d339XOzX9Sbd+zU0ZSj7fZ8WlzFdVx9O72VVO23Ti+MvX+O65hOqLhOuDEdsyM6qRNaa61rd23Xv7/4QrcoFFAXnnX6R1rrov/VSVKtde6Mm6a/MXT0EP37636nW2MtjtZKR52IjquYTui4bku2aq21Pur4yfq6G6/VWmvdlmjTkURca631ux98oHMLC/SCRQv12k2b9HsffqhbolGdVEonXFfHXXu/+cjvcsdVTEeTbdq20wvn3XffVBMmjNFnnnVKbXO4/mApJdOnTze/KuD/H2UKAdZsXdNJa52htR79y19fuHvgiIH65TdfdrTWKqHiujXRsg+wuIrpSCqsHWXrkeNH6oeefHAfaLFkQtc1NujuvXrqP/7pRt0Wi+pN27dqW2sdc1I6aqd0Qjk6qX8a0JI6oaNuVLckW3VrqlW7Wum6lnrnlHNP1YdNOdR9Z/47FQDjx483/0eDNjVdR+jfsWfH1COPO6JtwuHj9cbtG22ttY7Y4a9czTEnqsOpNt13SB/9ytsvp0GLp03m7665Wg89YIRuamvV9c1NOqVtndQpndQpnWr/mtSpnwS0hI7ruI7raNpw6uZUq47quE5px736j1e5w8cMi7/0xuwb22NJ87+xdvLbWA2YmvaqF65YeMHQUUPafn7S8bot1urYOqWbY01fmVbZW7vREm/WfYf01W+897rWWuukm9QbNm/WHTuV6xdffSV9zsXj6TNH/3vvlErolE7oaCqsw/EWHU22aa21+sdjD+iDDjtQ3/PwXbd8Pp78H0MYn3vuuRaVuPMWzrng0ksvuXfAwAGhZ59+Vnl8HiNhJ/B4PF9J9goEQkksaWEgMUT6w3ukh8efepQu3Tox+YgjiDsJpKH/7We/3JsdAEzDwDAMpJSEk23inNPP1eefd77z6COPXXnz32+6UWudPX369MD/iDOuvXiUlRtWzhg/abw+5eyTU462VSS1H4U2Kq5tx9ZKKT1s6DD92muvpd3tZEwPGzNU3/PQ3el6ECfyb99hX32n/4ur2D4H6tGnH7H7DeunH33m4dtN0/yfc8at2bbmuJEHHuBUnFZh2zqlYk5UtyVbdcQO62T6Y34taMlUUmut9eGHH67/9re/acdx9LKVy/TgkYP1jprtOqkS/3GwvvwZYm5Ux5yobok3a621/ssdf7aHjh6i53867wb4cSq45E/hdMyaNct4a8FbuVrrw2ZeO+PFnNwc8dhDj6UbGtCYpolhGP+SUvnyWWinUgD0H9Cft99+G8MwWLp0Cf369aW0qAxHOf9pBwvHddKpoM+ZZyEFXq+XllgzV/zqN8bgwYOdq//wh2s27do0YeLEic5/ooLrW1371ZtWl2mtx5169slLxx0yVlXXVzlxJ/btu+sLgW5CxxNx7bquXvDJAl1aWqpvuOEG3btfL/3si89opdVXep3/zjuuYl9bU5nUCd2WbNVRO6JjiagaNmaYc9rZp0S01oPaSYb/ngT0/fffbwHc/dDdj4w8aIRevmaZrbX6Qgy2X3e7eUwmkzoej+u77rxTl3bsqH/7+yv03gXwnV7vJwLt67zWuIrpcKpNtySatasdvXTNErtr7876d9de+bJhGPue039FT9i6dUtLVm1aNXrMxFG1z7z4tKO1VmG77Z8f8rucF24ifW65cW3rlG6NtmitlU6o+H8csP0FdS94Wmv959v+Ynfr3V3PXzD/jKq2tvzp06fL7xPDyR85JpO9ew9tvvfuex4++JBDCqcdd6JoS7YKy7T+GbN9l4UgNVpokODgEAgESOgEiO/+Wv+pGFUIgWVaJB2HX55/kVGQV8iDDz3yt5JQyP99X/dHOxBLSkqs119/3SksK/zjjh3bf3b17692TMs0tNYYpvEjJN4ESiv+p/J5yaRNKBAQpeXlzn333pshTLnn91f+7mPAnDdvnvr37rR2s3j++efbSqncuXPnXHjC1BNUQW6hkbATaBR2KoVy05VRP5hd+R+Z2xB4vR4Sjs0Rhx5mDB4yRM2ePfvi9qyA+q5OifwR3pCeMWOGpbXueMW1v76rV+9e2UcdMgWJENn+HELeLDI8IYRMF2Hr/0+bu4TY96jFr3/9a11VVV0+febMSTNnzvzO3aXm93U69v6itdvXFufm5apnX3j6hEhz5KRzzzrXWbj0U3P9hk1EwmGysjLp0bMbvfv3J8sTIqUTOFqlC0b13jrCH34pdLoMAdG+I9OLRLQbV91eVodIV23p9Hprz2HvvX9KL1wjDUHMTjDmgJFiyJDB+u333rxOa127bddmYWZ6F5dnlzdrrfk2EM3vA9ae5j3lWuuqCRMmIK1SuygjNPK9116buXDecrd24wZja9VSsvyaDBOwNfUR8JcM5PTTzuGM008iKQXaVgQQxMy02TP5pnaJ/QBNK0BgYCC0gRDguilSqTCmYaFd0FqCFGhhIqSJYYLEQeO2/3YPCPmTAKalk148wkGh5C/OOcM959xzu89+7fnhR0yZ/GjVpipnfwD7oRlWQ2st29qqjpwx49czlnz04ZCBXbqqQ8eUy5492sjN3EiGYaBifvY0ZvLC21t5/MXdjD/hNG686368WpAhNXFTgZCYiB8EmqFcZDuN62oHaTikS3a87Wvz82Ck0ERJKY1QFlJ7MISJY7iof0PHkp2yEUh92KRDdffu3asff/CJoeu3ru/Qp1uf1fvTNmX+AK/Tferxe09+5bUXHy8stHngH1eq3qUhSf1iYtVLUG0tmMoBrSnPzObXp+bTuQR+c9cTFHbpwfVXXYuTaEsvbEN+L8D21ugLIVCuRmmN5fFjAbaOsWnzelYu2siuLdtpqq3DVVDSqRulPYoZfEA/upX12odlyk6mS+wM8ZM7PEorMr0hMe2kaerOO+/suHzd8mOH9BnybJre+vZVI75PXf2MGTN05XMPP3zHHX8789RTj1PnX3g4xJfKupVzkS3r8FGFFF4ECiFcEo6LML00RnJ44KUkz37o8O77C+neuYi4SiBMH/J7PCilFFoptAaEAdLDzp27WLF4AcsWzqG5ejNWogqnrRVL2UQTDrXNUWoiJjFy6dy1HxMnHcrPph1Lj27l2GiSCRePx0IK+ZN5TFprPMLD7trd7tixY42Tpk37+M83/HVSRUVFcn9kL8R3+EXW0qVLA8OHD2997LEHLnjsoQfuPe+ck9wTTzvBiFa/QNuWp7DaduLVKTzeFCYOCp2+lURi4LgWi7d14KK/7uK4ky7nxj/dQCwVRnp838s4ag2OY+O3fNQ3NLN2zVrC0RqKsmJ0zGklR1bhiy1DxNswXIVtK6LKojYGqzcnee/DOt6YV0vCyOLnp1Zw2eWX0atzH9qSrXi9Pr7Ydvjj7za/8HP8tOPV7t277UUfLR4nhFgyXU+XM8VM9YOCa621mDlzJif/4uTOOQU5fc4/49Te06+e8fTPjznUPfui0434rtdF0/oXCaY2ENLNWLhIbSBtE9MJYLgGpnIxXIEpDRxlUNfs5YOPN3Hiqadh+Iy037a/DoAGrQVaSVw3TtDysHjpcrZu3sbAfgUM7wvFaglqzxska+ei2zYgEzXoWDU6VYepW8nw1tGrPMERY7oy4YDuJKMRnn5yLs/MfpXCsmJGDBxBwkkghUIo4ydxKrXWWHhwUO7s2bM9WKx4+/W3t9ZX1juVlZXuDzrThBC6nR/bAVRNu+TSRX6vKS659CwZa3xP1G98lIzUDrxmEiEUaAHKBctBpbxI10LJBA4uSC85oQTDu2neWdDE0pXbGH/QQFJOFOR++o8a3KTCsCw8ppdnKp8iL6eIww4bDq1zqFv4GEbTSvxWHGEkMfceEVKmW3Z1DNOR6JSD6S5jSHGIsgu7MGxwJnfM2swZJ5/O9i3V/PZ3l6KdBELpH5E3+nL/sGbMqAMlrmTN8nVnAa8AcYRIm5EfElzPmDHDEEK4s1966aoFy5YMvPjqcxzYLMOrXsabqsKwwkgdQ2jazZxAKAtwcI0UtqFwPBrldZEBg6LSTEwjyerlizGxvtPRKpRA6CRe0+Wuex5FOwEOP3Qgbt2z7F56D0RWEvKG8YgoBsmvZCcQAiFMpPSAHSHABo4ZH+dvv+nDpEEm1/3+Cv58y614zQyUSP0kFlIKQcJNUl7WUQwdNowVK1cMB3IGDBhgaqW+UUhtv8zjxIkT0VoHr7v6D/coI5bzx+kni7b1zwpf3XyCnkYggU+DVO2mS3jQykRbUWwrie0pJuIOYNPubGzZEYdy5i+tJphXzhFHTCLlJpDG/mUq3JRNwO/jwSfuoKU+zMUXnk3Lnlm0rX2U7ORGQkYblrKRez/zVzk4Mt1IL10LqV08Moml2ijKMunZtZidOyM88vQ79B3Qk8H9B2Br5/OMxo+21WzbwWd6RW19nXrjjTfIysl96YhJk9b369fP6N+/v/re5nHu3LkG4Gxeu/rQnds2djvl5DFOILbarK2eS7HZCG4SDwLhaoQW6WckXFwMHGGhfKXMW+TngafXsnlnjFAQRo8ciDdYTH1jI6CQUu73OeDzelm+fD7vv/MWjzxwP07NS4Q/e4asxHYCMgaujau9aeZD2F9pctNtiBolBAITocBwJQFPA4O6WFx4Qjl1NZu49rdXMu7A0RQVlGAre7/f537TUaaJBsaMGaMMwzB37dx+KvBeZWXlD+Me9zYUPPzs42OaW2v00eP6EVk/lwxqSXk04Md0PAjXC8qTPtNECkckML3deeOtIL+evpY3l7WyKWqxeAc8+doS1m6pQppewGB/U0oCidI2d99zNxecez5+z27q1z1KfmwtIeJoHFwpSAqwpUKZX3OeuwbS9aClxDYdHMtBW+AKG5MaDhzg49QjerB12y4eePAxTGGhtf5JTGTKcejbp68sLCzUO3fumqS1zq+srFTfZB6/FbR58+ZprbW5cePOcaWlflGau0fYTRvxGy6mayK1gRIa1wDXACWMNM9nZLBidwdm/GMte6IZeDx5KOFg+SRxbVLXEiXD702ff9r46h2hNYaWSG1ixx28wsuct98lYMGEg3rSsHYWvshG/DKOEkkQCoHGEDYSuz1M1f9yCwzQ3jQzaeh95hJHgLAJZLRw8KhcBpdZPPrkk9S31WNK4wdnKb7KciilyAgEZL++fdm2bVsxUND+Rr8faO0N6i6QX7V925BOHUwsdkllN2GgMV2NUAotNKr91kLgOgLDV8Ls93ayucHBMEyUqxFuCu2mUO2ZiMxQ8MsM+Bedq3biV7saj+HBVQ5PPfEMPzv6CJzIEqLViwiKKDYOSrYrFei0fITU+mtB08JFSRu0xnRNDNuDdCy8SYkRB51ooEteM4cN6UDV+u0sWbIUS3hwHPcLBTw/+FSTYh86/QcM0Dt27tRvvvNmGUBFZcX3A61fv34CYNmyT8eEWxp9fTqHlBvbITxE07EXNqC+sCi0VpheP7ubPHz46W6UEDjYaOLoL31vdnb2Xjr1qzz79MnT/u0ej4elSxewp34Xow7oTeP6uWQ4VaDbcEwX9R2epRYuiDQBYCgXrTW21oigDyvUEV9mMZlFNmNGZxKSNvPmzkMg0574j+pKin1mt3v37iqVTIotW3dMBOi7pq/4Xo7ImjVrBMDS5ct7CNumOFspO7pTek0biURIF6G+9DG0xjS9bN1ls2VnEiV9ONppT52IfWYBoLi4+Jt4cVytMAX7BDXfevNlDhjSHb+nnuaaxeSKpnY5pe9YxqAN0AZgo0yHmPZAsISl2xSfrhK0hTX9egTI6lRGh97bWLx0CQ4pTPPHrzWVMv3ey8vLSaZSPPPUU/Fv6yjdr3dRV1OfdFM22QEPpkimH5G2UIYLWu0DY+/jFkISbnNwkiaGFUDoKLgCKTwIFI7jEAwGGTx48DeaRy3SexNp4DgRVq1ezlkV43F3f0yGqsY0bGxCGE4KId39xk6001NK2CSUSdI/gMdebOaBp7azpdFE46HAiHLgYSbZhV2prd1DLBnH4/H8aGeZ1gqtNFoJXNclKytLgqa8vOzIlcHgH+fNm+f+INASqYQQaAIeP1o7KAQIiUa021f9hR2CBOWkkJhIrTFQaPwkk3G8HnBdlx49etCjRw8cx/nKFJbea4iUwGua1FTtobmxgd5dMwhXvYvPSKKlRLsWpuumVXX2n0lFYONqMDJ78/Czjdx87y4cXYDpiWN6FCrp4/231yKyAxR0zMdO2Xg8nn1Zhe+RUgMhcBwHj2lhCX97I0D6nzt37kyHDh3YU1OTG4lE5L6fSP8u/Z1BEx6hlYJ41MGWLrZIYQkLy/EgtNue4NsbyyrQSXLzvGAqpKHwaYGSATr37M6eqo0kUykmT55MMBgkkUggvV8WcGmPpLSLTJkIX4Id2zfjc01y/RuwGzYhhYGJjUkzmB603H8lCFfrNDHiL+WtJZnc/MhqbNOPohWtXOyEog2BYZkkmpvpUFa4z0R/zxworgNaKYLeIJFEjHmffMSSpUuxbZuC/Hwys7JACwzD0JmhTBWOhNmwc0PHjQ0bwz3yeoQ/z/x/s8s/If2ld+9+lrS8VNU0YxpBhHAQwgXhor+c/hECN5WiU75BWaHEUQ5NiSRTTpzAu3Nnc+kll5GRkcHUqVP3Fet8dQwkMIRs/zdJY0MdlkihknUIlUxTOSq9SNJGVH+XOhtcHSKW6sxTT68hFg8AgfS52152oBHt/58OgqUhv9cuS1eRpW2Sx+Pjocce44ARB3DGmWcyZ84cli9bxocffURlZaVsaKjX27Zt6z7jphmVWuteRWVFsS8D9q2gzZgwQwP07tljj/R61LY9cWE7me0/pNJZ3i+BJoTEsVN0LnI44sB8RDKCNA02b93Kpws+paq6ipEjR9K/f39SqdS+2sB/qadAIPXenKBBS3MzQU8SnazHUCkMoZCa9mD+O9ciYfnzWbA8yeKVLXhEAGdvXu4rXszvD+DxePY/E/H5Xa3c9p3mcOGFF3D+uedy7HHH8fFHH/H2628wu/J5nn7scV6sfJ45c+eKi375S+PJp5484dRfnLIg2Zo8QAihP9v2WYfPl92b38KGaIChA0Z8mFOQJ9dt3oxyemhDrRNSm2gh0SLBF9dB2vQ49i7OPqEvK9c0MXdVhE/mL2TdyotQpHjiiScwTRPbsTGkgYP91WtUC4RMe4ephEak4nhUKxIbQ6WLdrTmnzHad7BXjhlgwWctNNggzSgaZ18Z0JdL9kpKigl4AiScxHfyIB3HwVUumZ5MLrv2Nzz0wD944aXZHH/McdhakbBT/xQJlZJ+vXrTr1dvTjypInXK6Sfnnn32L+7XWo/86KOPku31Ofux09KgSSA8auyBtWs3RWioRnstP9oNoHWAfbp6X/D6TGytKc/dye2/H82lJ/Vl8ugSnGgDhx56GEdPORrXdTGk8Y3ngLFPrMzANAI40QQyFcXCwdRq3ybX31liwSISDbFiYwspBJBIhw5fY/pKS0sxMNuLh76DWVSKTE8mla88z+233sYDDz3I8cccRyQex7btNDdjpDuIlNbEHZtIIkFph1LPnXfd6SxbtqzTn+/48xMTJkxorqio2OucfDNoQgg9depUIYRoListv8BRfvvteVUYni4IN4EGbI+FY2mQAoFEK4EQHrQS6FgjBb4qjppQSFEHg6LyAm646QYSThxb2SihUML9mgBY47gK6TpAnIzsDkghMJEoDGwJrilQ0kgnKr92n4l/uYWWJOIWu2uTaCHQQiO1D63M9syN+ILITI8ePdrBFt8pMy0QtMXa+MM113DiKdM496xfEElGMT0SaZDuYNUOQrv4TQu/aZHh89EWb2NAz4HmgP4D1Zuvv3mY4zj9Kysr3ek6LSsv90OCVmmNGDduVJ3IyhT3vLZZb28qRpga7bZguxauDiBcD8I2kNqLq9Klaq3xbD7b2sT7H2/n3fn1/P3eh+nds1caMOniCgdXuOl6/X/h4RWOqxGuA4QpKC0n7kgSCRMMC1sKUobAxUib0W9VK/4caMLFtqO4btokagk4BlKYOK6D7TiYpoWrFKZlMXTY0PQ5+x1YfqUUGZ4QT896mvqGem64cWY6U2CCMDTC0KBdXNvGkJLX3niFa669iocfeZDq3TWgBePHTVQb1m3Wb7739kSACXMnfDtoWk+Xr776qt8wDP3c00/+steAvqbI7qqv/PtC4v5ReL0d8KRcLNtBaAdlKByfJOX3siuSwZLdIWZ9Ao++XsPf7niMKYdPoTXRsh/nQlqgzDAEmCaONunSqRThK6CqRuATEstVWI6B1BJX2u21i/v5QCX4/JATNEFbOK4PVypsN05eXj6FhYU4to2dStK7b18G9h9I1Inu96GptcaUJnE7xv333s8FF1xA9/KexFLRtOO1j74y0Fpy0cWXcOaZZ/OnG25mw8bNlJWWIQT06duXaDQqli5Z2lVKyT333PPN5lFrLTZtGmn1HNC53HWdnms+XvDz44YNYvbDTxs7a0o5+fKFrKjti5HdHRHIJWEWEjbK2dxUwLzVJk+/aTPzvp28sczDbY+9SMVJFUSTrfi8PoQUfFvXv0AgDQmWRcoRFBXlUdi5H0s3aIS3CFcZCCVAG0hT738dhwaNIhRw6VrkwUIhpB9XJSgv78jcufN488036VDcAdAcNXkyWYHsfcKf+/P6Sik8wsu8j+dRW1vLueeei61TWFY6xZNMpXBdF6/h4Te/+x3zP17ArbffzqVXXMELL75EU0szAHl5ecJ1XQzDOsTj8VBZWam+0Xtsjw1sYH3lI4/d6dQ2eosa9rjJhW8Zz/35Bh549V3OmzmLUf0kPUp9SLxsr4qwckMzW6tT2DLA+MOn8uiNN9G9WyeiiSYsS6KFZP9K19OhgCsFaC+moTjk6MnMumcOUyYNQnob8agEpPguVAgIUErjMyIcNqqQVz6uJ2lm4Ngav8+L4zp06dyZDh2Kqaut46ijjvpnrLYf5tF13bSbb2ieevIpxo4bS5eSrsTcaLplWWtcVxE0M3jwiUdYvGwFp//iHBqa2xg+ajRPPfMsO3ftpqy4pH1XguPYyf2K03T60NNa647vvvXGL0PeLHoVFUq98R3q5j/L7y/4BY889Rp23pHMXpzPQ2+38dIim3j2EI4/7w9UvvEhTz/7NN27FJGMt2DhA+Xfb1+vnVrGRYCU2KqBycceRZvRhZc+rkNm55HQ6ThPK/O7uZBa4CZaOWhEAYN7hYjH2vCZHrZu2czko6Zw1JQpLFu6mJ8dfzyjRo4ioeLtDST7F5eZpklNcw3vvvsuP//58Sit9hEIWmss06Sqfhd//dutVEybRmNzM1V79iCkAYZJsr3XXKl0eC+l9OwXjVVZuVYA6rlXnhuzeumn8txhw1QgEZFS5KKTLmvmv8Woy67gHyMfJ+ZGaW1pxLIsQpnZePGjgYibROkkPp+JdDRSOjiuQimdXvHt2SRXetDCQKIxSLv66byXwsBJf6/rIxjI4peXXsVf/nAKowb2pou/FTsVRxsgXIEh/kkHCxQIsIUGTIRON3xI4SJQKLuJ4uwafn9mGdH7NrK6KolCU1u9i+pdu8jKyuTyX1+MNDSO1u0l5+xHrbyBT/p54c3Z2DGH8WMmoNGYhpkOA1xB0PLz3HOzcRxNn369efbpZ+jetSvJaISsQIDCvDwAWlpatM/vo7au9t1UKsXUqVNlZWWl+7XvpKKi0tVam2++88GN0WhEdy3tQKQlTkMqSKsvj7JBA0g5KcJOBMswKcrrSE5mAQJBwo3i2gl8QuA3/QhtpektVyOVgc/04TMDBMwgATOAT5qYIu3eS9cBJwXuXhVGlY6gpY+USnLsz45j7EEnc/NfNuJ6h4HfAp0g5QkQNgMkDQ+OTNfuS037rdNf0Uit0KbC9Tkk7U0cODTBgzcP58pzSjl1UieOHdMPgeLiiy5i1AGjcRznm+PJrxKtQfDm62/Sv09/OhQUo9x0BkIDpmESTSV55ulKRo4chUZRX19Hfl4ujXV1FOXl0alTJzSwe/duJIKA11+llKJv33SO7StBq6qqCmitvbWp5r6rlizrMvm443W3gydJc9Awcg49jAFnnk7JmDE4piedldaKlE5haxul066xiYuhFcpx0cLENANYngxMK0A0blO1u4YdW3dStbOaaHMTHscmYFp4zQAeKy27LrVGOS5IgZIaR2iSTpQZN/2VFjWYK/60kpRvKNLKa3+4LqZwMbSbjpW1gUcJLKUwcZA6nbTVysR1AwjhRzkp/G4zI7uYHDiklHXrN3DQgeO4/LdXEk3EEMrdb5OutcY0LZqbmpk7dy4TJk5ASIHrpoNyrRReabFs2XI2btzAqNGjaWluIRwOU1hYyKZNm+jQoQOhQICU67B582Y00KNnT/MbzaPWWqxYv76wpCQr+fcb/nxQ7e495mn33ueWDu6LbScRHivdc+KC4Wqk2V5H+OV0jkrhKI3fm0XCTfLRx4t49+23WbrwE7Zt2UoiGkXbDqY0MU2B5fWSW1BI5x49GXPgBCZMGE+f3l3xmEHiqQiOUGCaKO3iDRnc/fQjnDTleC64chU3Xz2cThkbccLVaOViCAVS4gqBQbpI1UHjYCEMC6EdfFJjqwy2VcP2epMt1V5ur5xPRqdB3PXk01jZGeC0tXubvv0iyZRS+AyLlStXUl1dzahRI/f9vaHFvuEN7773LoFgkLKyMlatWYWUkuzsbHbt2kWX8k4AxONxduzYYaK1xjDe3NeG91WgCQT0YXswGGTaScdfUpLXgS69+hB1NbYZRLsCocErFJZwUDpdF/JlD8qyAvikwSuvPM/tf72NjatW0aOsI317dGHKz4+iJK8QQwG2Q0u8ler6BtZv3sbqD+fwxnOzUNLk4EmHcMppp3DM8cfh92XSkmjBMAUJu42OnUqY/e7bnD11Gsee9w7XXTSIicN6kOFpRdtVOE4TihRaSxRpL1QbfrT0Y7uKcMRmdyOs3i5YuiHOS29tZdiESTzw6GOUFXQg4TZjmaAc80tFEt/YhQbA/I/mEwgEGDx4CFqT7jkXGqPdmfn444/p3KkTGcEgGzZsSGfwhWBPTQ2HHnwIADW1NezYvp2OpaXilKlTo+ecfvo3OCJiH5sc6tOnR+jAAybgDwRESzxKEAlapD0pQ4PRrj6ACe1pC9u2CVoZNNbVc8WvLmL+O69x1ISxXHfTdHp17oQfcFMOhqPxIMBREFTYElLKoKa5leVrNrBi7UbeX7SMs956mxFjx3D1zD9w6CFHktQRkraHeEKRUxBk1tvPcf11d3HWLX+lT5etnHx4JyYOKaFzcT4+K4ql095yUhhEkhaNrUl27fGzar3Bpyt2sHhtlITK4qqrbuOckyeT3LmZ6jVLycwPIorK8OSWIgBbK4RIn4to2b5Qv2g2DZl26ed/OJ8+ffrQoUMHbNvGMNIpKlOatMTaWL9+A1OOmoKrFDu372DM6NGEW9tobWlh4MCBAOzetZs9NTUMHTok7vP51N5yxpkzZ/4raLNmzZKAmvvRR0MTtirq0q+Hq8GwDIG2nC8U3aSz1+2cnDaJhSNkZ4ZYsmQh55x1LiVeePrPf6R3xwLspEOyuZGEnWYuXCFIta8SlXDTtLA0KBIweWBfRpZ24PBBvVm8ZSvPzVvA5EMn85urr+LaP1xD0J9JJNmGVhKvP5s//fUGjjz+CO78++1cf98r3JRKMahPBt3LQnTKt8GURJTBlqoGtu2OUNcCTY0mxR06cczpR3PBWSeS3dTI6sfvw2xuJsPRNFsW4ewcMnr3os/BEzEKS0hpRVA5KExShsnnu+q01pjCJBxpY+WqlUyePLk97dSernIcPB4PW7ZsJdzWSo+ePdlTVU1zYws9unRnz64qpFb07N4VpWFnVbXbGm4zijoULgd2AoYQaaL2X0C7++67RUVFhf7HIw8dEo5ExOAhg9Ohazux+uVtaZE+O5LKJTszi4/nvs9ZJ1Zw7CEHcvkvTsNoayHS1ALSQgqDryJCDMx9bUVaa7SryM3NxvYIQqEgw3r15c1Fy/jbn25iyaJFPPLoY+QX55NyU2gNtpPgwDGjOXDMONatWss7b7zNwo8XsnzbZuaviSIsEJbAGyqhsFcpIzp3YuLYcYwZPYaCko7UrP6Q5S88TXclCBqgRArXlYiGOPGP61jXuIeePz8Vb1EZCKO9W1T9qxMiTKqqqqmrq2Po0KFftl5IDNatXYthGHTr2pVFi5fi9wco7VjK87Oeo7CwkOKSEppa21i1cpUWGoYOG7aznbhnb+Wx+TVeUIdf//aKHqZp0qVLF1ytv7Z+UjkutlL4PUHenfc2F51wIr855WROPmos0cYacMEQZrrp7xurdNP/ZhgGjuOglCY7L0ie6aFpez0njx5FYYdcbnx4FiccfwKznp9FUWkxKSeO35AkkymQJn0G9qXPwL4ARJIuWqYQQiFxMAywBEgCgIXCJek0E23YQx6QaUBKJEhYKZSAgPKT5bjsXLWUPd0HU35oZ2w3HTJ8uQosHTwLNm7cCPDPoqU0YvvSOkuXLqG8UycCwSCffbaaLl064w/4WbFiOb169sDj8VC1aQvLli0VHUtLGTJ00NPtFpCvzadFekUEEPzo4wUtgUCA3JwcVPuoqq9M9LkuXtPDihULOefnUzn9qMmcdMShJOv2YDlO2nvTAoGL3A9SV6l0bb8hJcpJEQx4KC0tRCajjO3djd+feTyfLfqUs085iVhbFEOY2E4KaRoIE+KqhTa3jlZdi/Y2ErBsMkxBQPjxpPwQ9ZKMu8TsBK5rg2lR1ncgwc49qYtrkq6FsD34lQ+PY+DaYGRmE8jPBamRUvNNFOTmzZsQQlBWXravtkFr9pHk69dvoEOHDqSSSXbt2s2gQYOort7Dxo2bGDtmLGjNjh071Pp16428/Nya0UNHz29fHeoraSyttVj6wFIX2NqpvHyYIQ2CGRnSdRyMr+DdtNYgJalEgt+ddzHjevblgooKYk2NCGmhpAdXWGghMXUSQyX3rc5/DRL0lxKV4JcmUWySuR4KupdgNzUxsU8PLjh2HO/N/5jfXXEFHunD1RIlDbQwMKSBV1j4lBeP68F1YqScOI600V6FDILHLwhYBoaClGtg53Wi54lnk3nE0cQ69SZq5BBO+tijvTR3LKPLMSeQ068vcZ3ElDaynaH/qmvr1m1kZWaRGcr8XGeVQIp0uqmltYWCggJ27d5FKBSiX79+LFiwAK0VBx98MK2trXy2erUKh8MMGjhkkRCipf08099EY2nAb5jC5wt68HhNUqkUaPkvK0wrhd8Kcts9f6V27Rruu+cu7HgblpPANF2SIokrHVwh0TKAqRUqlUS3N8ZLIZAalDaQpGksrRVOuyaWaUuQkqSryQhlUlrYgR21Ozls3AFs293EPx58kLGHTuSME08mbifSrrX2IRQoOz2oy+/PAgQxEtTW19EWacZjGZTndyToy8GLJqZtPKUldCw9lrJYG/GdVSSjYbTfS2ZxB6zsfKIiXSHvaIXG/NolV1VVRWZWJsFgcJ/JBI3rajDSdX7Kddm0cSMjRgxHCnjrzTcYNnw43bt3Y/WaNSxc/Al5BdlMOuKwOffecQfTp09n5syZX00YCyH0+PHjDSFEbMfubQv8QS8aR0mjfel/udnb8NDY1Mw9d97BL34+hY45IaJOGNNQKOXF0ODTCTKEjWkrlKPRhsIVDo5O4bTX9iPTHpaJxBDppKQr00Sw6Zj4bQMj4VJYUEgwmIGIJDjjqCPplBPi+htmUtdQhyWtdN2sFiRtG8Nn4fdbLF61mEuu+BVjDhjJQaPHMuWwKRw+4UjGHDSeX11xMTt2bCRDCBwdxxUCO5iDp89gsoaPJ7vfSHRuRxLSxBQSUxi40kJJkeY2P1982l5i968VW+0qRTrNPfbo1p1lS5aQk53FxAkH8uHcOWxev5YLL74IVwi2796t16xbZfTt2yN53JFTXmx39dV+EcZDhwzJWrxkKSnbTvdRfenNKKUwDIt3XnkFWlo5fPIRNKaSYFokBLhuCEul8KSi+E2B16uJe720mUGkqzBdjVcYGFoQS4ZxVRwPFoYLHm3gSvY9GN1+dlqWhw5ZudTWVJERyubkSQdz03Mv8+ADj3D11b/DtlMYhkHIH2Dj1q1Mn34t7733Nl27d2VqxVQGDhxIVlYWruuwZ08NdbV1aZZCi39WOWsXF7fd+eJrqsW+/urapQvz580nHo/h9Xrbf1bv9Y2pmDaNJx59jI8//phoLM5dd9/NlOOP5/jjfsbmrVvYsGGDampsMs4447S5wM6pU6fuc/W/ErT2ATdq7ty5+b/9w29WpFLJE6PRqMjIyEizKdJoLyLV6PZe5FdmP8/Bw4aRW1REVm4+bdU7sFv2YJn1KAxEdgG7oik+XrGGJWtWs7ummlhrBEsYdC4tZczIkYwe3IUMwyAVdbEQmCrNurjiS/K4dopMf5CwNIlEWhjfvy/vf7yYRx58kPN+eT652dkkUkkeeeIRrr3mGnr36U3lC5UcOObA9rDiKxwpbBxt/xOYvZTcd61vbP/+oUOHcvdd97BnTw1ZWdlpTtQwMEyTpONw5KTDueHmm3jgvvtYtnQZU446ir///e9E4zGqq6tZtGgRpWWl6oQTfn6XEELP0rOoFJVf3zUjhNDtnTJNY8eOXqy11uvWrcNn+JBS/nMqYHuF7p6maj5bu4phA/sTLMrHX1QACY2hQRhxHL/Bg2+/ydQrr+aaex9k3tLVtDVFyAplk5Wdw4r1G7nq5j9z8XV/ZktdGMcbJCUkjlRo6X5llakvIxNPdjYplSTTUBw6bDCbt23muVnP0dDYwNXXXM0F55zLiSedxNw585gwZiJJN0nEDhNzo8RVjLiKEXOjxNwojnZ+sNiLEAJDGigU48ePx+Px8N577yGEIJVKoXTaI5ZS4iiXq373e5atWMGixYt54sknKS0qYtu2bTQ1Nak1az6To0aOauvTtf8HAFOZqr61ln/NmjW6oqJCaa1X/OUvt7a++uqrWRNGT9RKxcXeEgGtFD7Tz6KNS2iNttGjZzeCeTm0RWM4rVGyMrLY44SY8dc7+WzbLs6oOJrDBvWlwBsAZRGOhGmOhmm1E+xqrOWFDxZx2Yy/cvfMaynPzSCVbMWQGuGYn0uHgiEFLbEYSzZupmPHPNxUlN6dSsg2BW+++TqRcJjb/3orvzjvXO647e/EknG0dDBNM+3ByZ9AW+JzvWYplaC8rBMHH3wwd955J6edfjrZWVkkU8m9ycz23ogkoVCIYEYGpmGwu66OmpoaVq5YqSORiBw37qBbAGYtmOUXQsS/tT9t5syZevr06TIQDDaOGjFm0SsvvSbqmhqUiYXQ6baIvUnB7Ws3kukJkVlYhvYFiLY1oXWKmGlw1V2P0NLczOO33sDJR06hJMNHphnHaybIz/FQmhcgTycZkJnBlaecRFlZGTNuu4NoVGGZHhIyhRY2ynBxTNJhg2MQ8oWYs3AlW6tjmNJHSWYGHXOzWb9mLQ/cey8DBvbn77f+DRcXIcGyrJ8csM97kI52+PWVl7GrehdHHT2Fz9avx+vx4jXT9SG24+xr3QXYU1PH8mUriISj6tVXX2PY0OH1F559/s3bt2/3lJaW7ndToe7Xr5+Ix2Kceeo513hMHw899LDwWT5oT3+3G312b99Bts9DZkYQw7RIJBKIYAYvz5nPtvWf8fc/XEmJP0g8pkia2US1SUqCjSaQEaRbl64EPX7sht2cffxkIpEYdz79DDIzB9MVmDgIbBAOynCxsQlkeCno0IElqzYgTR+mIcjOzGLblq1s3bKV6dOnEwqGcFwHj/Xv1XaWUhJ3YoweN5Z/PPwgS5Yu5YADRnDd9TewfuNGBJDh9eH3eInFYmzcuIkVK1dgO7a+9967Hdd1jF9d8qv7otGo1aVLl5Yx5WPi+93qVFFR4bYz/SsnTJyw6IEHHhhx2qknuyUdOhipzwXIVdXVhPwm2QETN5lECi/485j91jxOmHQohTmZhFOaLiNGonWKmpUfIyOtSCFRKY1hmeSUllAXqcGfbOP4o47i1lnPcPj2wxiXFyIRb0VIgcBBC0XSUngNh26dO/PKO3OJuQ5aCnw+L0ppxo8/kCOPPBJHOd+q+/9TXYYwsG2bUytOoWvXntx2623cfffd3P73WxkzejS9+/TB5/WqzKwslUyljIaGBvHeu+8JaQjPJZde/vRh48e/uG7r1i5a603trU5qv/vTKisrZUVFhb1517YZH3/88RtXXXWVfuLRx9CuRrkKTIiEI2RkZqC1m05mGl4WLPmETXtqOX1AD5p0jJwePaAkDy1cUtuCWOFWLCRSa1wtUF4vmXn5JKKKQMBHNJVkxcbNTDxiEm12AiBNSqNwhcJRNjk5mTRFIkQdh4DH2MfWnHjiiQT8QZJOAin/MxNChBSYwiDqxBkzfATDn3qaZ599jkULF/DOO+/w1utvaMvvk127dJWOa6OFol+fvs0nnnzyY6ecMO2Pr776avKYY46J8Q0V7+bX14hUuLNmzTK6lXaef/rpZ3w0c8a14x585AH3nLMuMJrsdF2ez+cl5vFhC4s7br2T2c/NpnHzLmwf5GZmY2iJaycR2kFHWrGjETzSRLppbjGhXIyQn23NKZ55/mXCKZcSy8Mzj89CRROcevRhGPEkTjKFKQAtsYQJQuNoSAqJRwhSqQSmIenbt+++TlT+gwrhSrl4TB8tsQiffroIj8fDkUceycJPFzr5hQXmzBtu+KxXz16VK1Ys+WDiIRMbh/Yf2iqEqD6Vk/jecrh7FVRHjBhRsHnz5oxfX3LJJQs+nvfsZZdd1qNXv37umAPGGa5S5BUUsGDBR1w980beefd9zp5WQd64g7j72acJpgShFEQ37aAxZuJGWvGH4yAttOuiXAd/KMi7i5bypzsfYdKUyVRMPpTwtl289tEn3PHkM6xdu57fX3g+HbxBjFgUrU1wDFK2QxKNqx1StkMkGqWkpJgevXqQcpIIw/jJ1OO+TsRv79hmx3GwTItoNMonny6ipqaWgoIC7r/vHmfFymXmxZdcMu+X5553oRBi3edfaeqsqcasqbP2S8/Y/KZJ7p07d26orKxs6tG9h73hrxvOdW1n3onHTuP1995zhvTrY/YeOIRb/vZ3gskkT95yFWOGDuW9JWtQyqG2YReDi7MI2Qlad6zGUAZ+AqQMQdITw5N0MNwMHn72TQaNGMmvzzwDGuvRpUVYB49g4MCuPPTka1ww/QbuvuZSepoQc7xYIoNtu+pxAwEKTLDtMDUtzZQPGUleQR6OdttbAn9ieWst02V57YDJdjbfa3lobmlh4cKFNDY2UVRURGXlc87rb7xqnnjyiatvu+WvJ86YMaPx9jfe8DYtXGjv7U4SQrj7+56/rWvGqaioSM2qmCV7lff68Prp008aPWqkcdxxx5rLVq9SftOgd0Eed/3pjwzs0YNwUxOGIbBdly27q6htakEbEsuSmFJiKAdTx3GVjen30dAaZnd9I8MHD8b0+1CFOYSDXrJC2XTzh/j9OSdix2L87k930GyGMC2BMl0WbthI1/wCOpjZNDelqI/YFHbsgNfwf6Hb5d+jAJ6eoqZUuraxurqGuXM/JBqNkZWdzSMPP2w/89RT5qlnnLr56YefPmvRukXOzJkz1aWTJydnzpypvo9a+H4Z/6mzpqrp06fL0uLS9fffd+/Phg4b/OFlv75EzrjmKn3uiT+nX+cytO1QtacG5bqUdSxmzdYd1LS0sbu6DuVqPIaJ3wCv4eDxWiSVpiUSQakUu7dvQ2aEyB06GH+P7mTlFFDizyBLx7j0rFNYv7OWvzz7Ct7sTDbv2sraXTsY2aMLPsdiW1WEpIbcvJx0x6Xr/iSSSF/ZmOg47fm/ND+5dt1a5n/0IYYhaQu36T/OmGG/9frr1sW/umT9Y/c/9rPmeHN9UXbRXi1+8ZMOJ29fCXrkyJHrJk+evEJr/fYll174/turVo+acOAo5c8KyqptCWob24hhEYvGWPLZeiomHEIqGqc1FaFDIAdHS1JBRbN2aKuPoLSXo8aP4vXXX6dTv94cdeYpFHTuTmN1PVnFRdRtr6fENDnpuMk8UDmbip9NYuXaLQgnRe+uhTQnkyxYvx2A1qYmtNDfX32A7yrFq9Fa4LEsmltbWLF8OfV1dWRkhPjkk0/UP+69V+bk5lp/vOmmjy7/5S+nCSGqvuoI+snnp02ePDnZPvsra9eOLR37de9Kbsd8EfMKdja10JrUPPDAoxTkFuF4/Mz5+BOCoRDhcITaPXuoqqph2856qmpaSCZsdLiFw4b1YuiIftx8699579X38IsgWWUlNIUscsp64I00MaFvCfmFWTz5znze/XAFEwYNpENxgLXNVSzavhNLwLoVK6hurMbn8X1LWcMPFmsHNKZhoLVi3fq1zJ3zAc3NzSRTSe6++27nz3+6Sfbp1y/1+2uu+f1Vv/71gUKIqlmzZhl7hyj9W4fezZo1y6ioqHDrmqq6Vu/YU94hK0eHQhkikUrQ5tq8Mf9DUq7mrhuv55xjpvDyB/NZuW0buVm5mNpGyzjKjeBx4wgnhtdwybcMxh84Fm0r/vLHW1i4eBmhTt2wLQ+mz4Mv5CcobUYN7cOsV17D0oopY0ehlIf3Vy6jJt7MpMmHUVVTy/wPP8YrfdiO096PrX6kXZUWanFdMPAg8VJT08i8efNZs3YDrhJ88ulC9ZvLf6M+W7XCPPv8c5Y99Mgjw88788xbksmk0FqLiooKVwihf8gO+0FjS1YuXZtqq2vVnQo74hE+lKNpCreyYMVKjq84jsKA5PTDDmXsxLH86ckX+HD5JsjugMzJISuYSYY3hCcrl3BWDot2NVI560XOP3EaGZZF5WuvQCiXoBnCI21URgCtDcoLc5HxGIeOHERBZoDqBs2Hn20iLyeLqWecRa/+Q7j1r7cTScaRGO1nm/oBijr6C382pAeJRV1dKx/NX8Knny4hHI2ycdMmPXPGDPeuO+6UPXr25Nbb//rcg/fcN6lPt26rz0vPSPtRgPoRdPktXIFwpdZaenGVQ01tI41Rm6SdIJ6MEo/F+P1lvyL7yed44s0P+HD5OnoVdyTX8hCTitp4lPV7dtOciHHqkQdyyWnTqG9oYPHHHxOprSeYkUWiVZLtzyRVF6coMxevkGhlob1BXn/nDba0tHHSaSdTlJXPSSdM43dX/Z6bbr6FG6fPIObG9tvtF3vFZlS7fsnn8oZGezBfU1vL9m07aGhowrYVO3ZuVy88P4uFH30ku/XqZVx40S+X/+aKK6aXd+jwrhAi0W6V7J/CRH8v0Lr36W4EskKEk3FsWyG0pLm5jRQw6+XXOOqgEXQMaCLRPVx91s848dAxfPThItZt3MDGlkYMBIU5eUwdPZIJ48aRX+gh3LSHYf1789IDH7J721ZKDJOoCx7Th1eYeE0vjgbtD/LBkpW8MG8hnXt344ijjyHSGqZzpy78/qprePrJJ+jduzcnnVhBykmgtIv8mq5TQVqXynYcPB4Ln0yPNEtqF0MYmAJaohE+W7OOmj3VJG2bbVu36g/ef9/9cO4cs0NxEb/6zWW1U4499opJB054Wgih5syZk9FOTrg/1bn6nUCbOnWqBuhSWhrJLSiI72ls8LqxNiypcJIpigvySHkC3HjPQ/ztstPJNDV2Ww09i/wMPHEySTmZBiKE8JItfXiTGhGzaVKKYH4hBQWN6JRNS3UVnfLz0YZJyk6itKQpGsP2mry/egXvfvIhnlCIc877JX5/CDsaJxmLM3zYCDqWlLBkyXK6du3K8OFDMYQm5aRI9ybKL6jlaQTJVAKPxwta8/p7b/Hqq2+wcuUKLNOksLCQ8k6dCQay2LZtC8uXL2H92jUiryDXPHHa1PWnnnX6/UdOOPwpIUT93nUwceLEyE/tuZrfkVdTgAhmZKw5bOLErTV7dvaLN9eprKBXZoWCxBzFdTP/xB+uvJzT/ng3M889k5IcH1olsJXGqwyypIMwDXbFG0kqga1h+9bd7Ng8h6WrNxIKBLDcFCoWBilpi0ZQUtKSSNBgJ5k9fy7C7+GSCy+iX4++tEUjmJaJ1CatzW1kZ+cweuRoNm3cggB69uxCRmYGUsh9ZtDAaP9/ic/v57PNq7n66mt49cXXKSrqSJ++fQkGA+zeVcWSxcuoqWkgGW3WoZxczr/w/OqDDh5377Rjpz0jhNj6eQft38SdfT/zGI1ExMw//D75zJJFVNXVEuhQQrfSHtQ0zyapI8x+sZKfHT+N0/5wMxf9/EgO7l0GPj+29GAZFton2bq9ikdffp3NtQ24qRjdysvJyM2nLJFLUYaXcKKBhEoQbwljS4vNe6pRWlJQXMw5F5zJwAFDSCTCeL0GWmo8yLQmsnTBFGQGsmiqqWNRpIWiomIKCooo8AcxJNTZYSJNrUTq6/hs3Rr+fsONbNq+nVtmXMthxx2LlZuNiDo01zexYed2dmzYyNwP3mPz5q0kE9HEuANG7/B4PFsBdrTs6NYpu9OWfycpbX6PWTOGEMJ5+L47Z9kwdMX6zapHQbEcWNKRUZ1LuO+OO/lkwUI+nv8O1996J3+f9SJz87IYPWIY3Xp2JNdK4jU85GR4+cXRB6XLrAOFBHI6cNsddzCkTw98QhNzEyTbmpExlxbLZNmGTeQ4imLXYfazlTz92JNYPh9aCKRh4Pdm4LM8GKYiGAxiGV68pgdDujRHE4RjKexIFJ2KkUpEyI/HKDAEW3fWcEjHEk4beQDlpoch/YbQkIqQWehHl5RTXFrM1g4dGDBwkFi5ciUvzp7dberUk5+4ZsZ1B06/+g/XCiG2TJ8+3Zw5c6bzXwna3mqtjRs3ZvboUrbooYceVbPf/8CcetCBlHvhnMMP5vL7n+TJp2dz1tmnc/ykQ9j++tt4Wpp4+6VXafP7yC7IoKywiPyMEDlZIRzHxqGJrdveZfvmzZx74rE4kRgqEqd19x5CMpfqphg1dU0cXppDf9Ngd2sTrqtItbWgANtVpBwHA40hwFEaB0kCyY5UEgo7UNKpN4WdShnSszN1b73EhPwQHTNDbKaVsZ1yUUT5y913MuSII+g0aDjxSJygz0PnsjJyC4pZs2YtrrToN3iorpxV6dx73wPn1dXuOSSWip0W8AQ+uf/++63zzz/f/q8DbS+ddfTR4wRmj13DDhj+3qMPP3zYsnUb1cGdio3RA3tx8pEH8/cbb2bRJ4uofucZflXmZURWFsvrDN7YHWZXbYId26pYjUFNykXk5NAWbUWlXG6+9ByGDuyDmYgTqW4kFXdxsjJZNH8hBa5iWH6AXpFWBvl0WkzNkO2VkRJXWhha4NGkc22Gh4jHzwdRxahTz2HM6CMYPnEEC559hI1vPsXEoJdkeCelXX0E7F3ooJdewRS3XnYxN78wm2BuCdFohKTr4rGCDB0yiOzsTFatXClOrDjB6t2vi/PoPfd0o7Xtzd1b159b2rV35efOtp82O/59fuiBB55IzZw5s+nJpx/dPe/TRWcuX7iIww8eJ0TIpFfPHpjhGA8+9ThDi/I4qHMOWU6MUHY+Pr+fstwAPUqLyMzJpE3ZGKEM7GSSX591EmcccwRWPEJLTQ3Nja3I7ALWtrbw2muvcWBRDj0DLtppQWNjk8JWSRydJEUSmyRKJcFJknKSJExNbTxClelnyvkXMHTAATSEa7ntwrM5xhKUus143TjCiGO5CRLE6FJSzJq1W6h8Zy69Bw6juFMnfKYPr9QY2qEor4DyjgW0tTZRVpgtu2dluE/f9aC/ua1t6ivz3117wNADPps1a5ZRWVmp/+tAA8T0OXPME0eN3XreRReXvfTiS8OQrjto5FApY1EmDx7MoG4deWfpKiqXbOeT6girmxxWNkRZ2dDKmto2lm6vpzaaIjsvm2vOOp1pE8cjwi207qmiurYOFcikUQuuf/Rh3GiUMXl5BI0UykpL0ghhovHiCh8aE4TA1BaG9uK6JtoKsbY2yrCpU/nZeRcT8PuZ9eRj1L79AieW5mNFmvFojSkUPhekEvhiCXqUFbNm6y4eeOAJNm7YAtJFuzGcZJzWpjpqd25jzZJFPH/3HUSWL5UDc4rUux/MpS6RnPrxgk8r+/fv39A+CUv/V3mPgGbuXBzbIWiINweOGf2LO2e/qbMLSzlx5Ciaaus5aOgB9Og/gA+WL2flijXEGlpIxpI4hiAzGOSIkg6MGNCNsUP7UuAVNLfsob6umbbdezCzc9gUS3HHk89S1LknR198JJ+98xaLFy+mb2GQrlkGoXgcXANHePHIKD5bIWw/UemSI1IELC/verKYcMzJeLGIuy7Ln3+dAzI9GLIebWhcbYBSNAlJwlOATyvy26q5oncOS8IZzH/1eZ595nEi2T6kP4jPNLHiYUosODbXRIo2XL8hfZ3z3Nfvvsvwe0MPm6Y1SvzE9Q4/uPplx9bNmZ1KS8W4yy7ntrvugXCCSQcMpWnPdvyG5NShA/jFqMEIU5BSGtc1sYQPy7BIJsKEmxuob2mgtjWKjUFuQS5Lt+/gz7PfplZ1xOPms+Sz3Uy96DJWLpvP8/c/wFgVYHgohN+O49UuEhufcMkQcYqDBiX5+awNS7I696DP4KFIBJs/+4zqVUs5fUABpPbgmg4JbYE3wNZmxfwNdQwZWETv7CD+thoOMrMYPyyfuJ1DfSpKUilMqcjxZJEnFFqk2OD1saKuhu6ZhcaYaMB9+8lHRz7yj3tvPePs83597bXXypkzZ6r/NtDU9OnT5RVXXLxzylHH7WpqaCo99ayz9QPPPCPeXbqU4ycdSK+sXOymWnBi2EGwRRK/1hi2HycJMWWTkElMaaEyC9jV2MZDL7zPnHVVyF6H0LPPRNymOt6dN5+tu2p5/OUH6TlsHH8581y6ZgYZmSPISETwef1kWpJMS+IVNq5lsrq+hZ7HV+ANZeMCH732KkVOmLJALqJxr2Sgi+EmKc3LJzOc4PWl1YiBZQzxSrzhelSqBZ9H0NljIKSF6wBJGwMHaUOhN4MMv0KHGxibmyt3VFW7rz771OWNNbuezCnsuGx/hrL+O8805s6dy9y5c8WkSZM7vvX+WxV+XzBkeXwInyU+XfMZle9+yLL1O2iKK5xAJs3CQ0MyTpMdpxmTOkexJxmjNhZlVU0rT85fyvMLltNiFOLvdTh0m0ibzKagQ1cmjD+cT+d/SJ7fywVnnsru6p1s/vQTjulbSifVRqHhEhAgiWOmUrQaWVTWpzj8sivp3KM7tobHb7yBPuFqRuZozHgcjYU0HIKOjaFMnOw8Zu9oYklNlKKCjhRm+LCSUaStkFoibI3pgKnTg5lNQFmClpRGRW2E0iK3uECt3LpTpEzTeuP9ua/OmDDBmLdjh/qv2WkTZkww5s2c58Q8icPibqrkhSefdpTjmNV7dhGOtLF54zZeee1VXv7gPZ755H3yrQBZPoukcGiJpydi+JQiS7g0kEVGtz787YFbGDtiFM99sJa7X1lEx+IeeIK51PokPcZN4eWXXuOS8yv4+ZlnctlDD7G9MU4JBt5kDFdKoj4Xy8pgZ5tAl3Vm0NgDUGj27Kph97q1HFuUgdeNIXCxDQPbUpiuIGGFeGtNNd5MP9IK8PjyLRQML6Ov6cNjJ1DKg6EVhko3yCsJjpFWJsrWJmEcbNMlx1WyGCm2rVw7CfDMnDcv9flB7j/W9X0PTDFv5jyltfYvW7jipPEHTcA0TOm4Lp07d6Vfv/787PgTuOGmmUw5fSp+06BLXiau6xKNexg+cTKjJx1NyJfNiNLelIZKOP5nJ3Lc4RPIyU5x1OEDOWBEX7w+H9I0qWloxDU9NIUj1DTWU9i9B4kOpayqbUb7vSQkJKWLTmiEN59l9WH6HzSWnMwcwGXbxs24zfWUBjUqlcARBoYEpUxigWxW1IVpSSrGl2YxMc8kQ2pe2lTF7qw8bI+Bo+PYIoVjOGkZXiFJCoErJQHTA4aJY5gY2pFZArWntqogRarPt01n+reCNmfOHANQcz6dMzk7EOp+2PhJrtJaJhyHGTfcRH1zGNdNsn1HDXM+WIBujuMTgt3xViouOImZf7mOM355Bh2HdmODamBHSxV7GltZ3xQnKv2s2xWmMebHkQLDjVG7bhnNW9YghCZgeCjyZZKdW0RL0kH7QriZHbBzSvHm92SVG+C9eJQJx05BO+mE6K7NW8iVNnmmDbaLFh6krfG7HtqS0BhtZVhJkCAxMs0UZWVFzK5P8twum1h2KTLgxTUMUrQ3vdsaU2lcmUCZNkiFFgrXYxOLt6kIyru+atOg9mNE/leYx/r6eq21Ftf88aqx+QX5uk/v3loKQWVlJa+8/DKX/+YywvEIdVVVhKsbGXXYJPr36sTKd2dz+qVn0b2wGx0L8un7yINEkzFuvOFu3lmyCt+rm/CZPrbuaEC5AbKkYsOqpUQbdxBu2czoQYXk5+USaYvSmoyzNi54eF0rTlJhSElUxJjb1MIBvzyPfgeMJ96UxF/gp27nVsqyMsj0WciwQhp2Wj9ZGrSFw3g9ENcJXCWpUQYrIimGTj6SlXt2cf2yTRxS4qNLYQ4FJgQSYUQqhqENAtqLVwg8hovH9OCaAVrsNpK2xmt44/9VLn9FRYU2DVNPPv7IUSOHjxamlGL6Dddzx223MWzECPxeH2s3bmLLhk3IlGLaL8/lrVdmkxcVJBdtoHmgj5zyboQy0rqGZ06bxuyz/8CKVbvx+DvikT50sokN6xdTv2srRRmCpkQdp518NhgWH2/5jNVttVz8178RSqRYv3EbruuQHQhy3UEHMfTww0hoiQpKAkIR37WZzgGJmUxgArZ20YYkqQWtSRcXjVRxAsLDrrhLnePlrutnMnjwEO6ZcR0PPfIkiTXb6Zxh0j3TojzTS4E3iC+VQXUkxqY2TYMWxLRNlfIIM5GkemtVmdbaV1lZaf90dc3fgTRuH6NcMOLA4csvPO+ijk0NLfqBf9wvunfvwdatW7nv/vtIujFuuGomGb4QZ1/9K268/HImNMUZVpjFZ1LgHzaYXmMPpHvvfvTpXMb5F1zNC5/UMnjiicTtMM31W9DJZixlU7djPReePY0bZl6Jx7SYfMxRNIdb+WTOR/vel/s5V9iJJwkLhfT5ENVVXHPwOE7LTzGUJqRtk5QGtmmxK+KwtTVFwrRwhcD1SLZaeTy8uoo+Bx3ELbfdxoCBQ4nHHN5743XmvvoSu9Z/hoxHScUSGB4/2iPx5mSSW9aFnMJyRo4Y4rzw+ivmjvraqe+/+c4LEyZMMObNm+f8R3faDGYIrbVoTjV3CAaCHV966WWWLVkmLrv8ctZvWM/bb77FX/58Cy3hZpCC039xGjsad9JcVc3IriWM6xCmV5vDxnmvsPHNF1lk+QiWluONupREasmP92Jt1XZi4T2EW9vwoLngrFO59qrL8ZgWt99+D0sWLOLVt17GUYpUUmGTBOJYXgtTevD4LXLwopTDjdfNJBhppFN5LrLFQQkTZXhpi9vUtqRISj+OMFHYOPioj0PcFRw6/mBee+FVbvrrfUyePI5jjzmao084tn2BKJykg2VKVq1dQnXdbkxPkGhcs37NRlZ9tp7zL/jlQCHE89OnT2fevHn/2dQMoLfXbS8qyC0oz8zM5NXnX9Ynn3qaGD78ABYvXorSmlVr1zN89EhOOmIynlA2zRt3UhSN47HrsRPQVbsMLbZISosG5VCndlKVkU+bT1OeK7nq8quINO0gkmqj38AR9Oo9HIAnH3iY22/8I3f85WaG9B9Ia0Mths+bHlKk4kRam0nEUiTCbdTv2MEHTzxJy9z3OHFgGZ5YHS5gS0lYm6xvi9EgvUgJwonjekzCVhYffLaDngeN49Jrp2MAb779Fq++OosXZj1Hly7dGTL0AErLypHSJJGI88knH7Ft2xZisTjr1m1g86YtIrcgn2Wrl4+HdGftfzw1o7UWXYq61GitP5CGXA/0/vnPfq58Xp8cPnwEH3zwAZ06deboY44nw2NgBPy4zRGyPBo7FccJZ2CG8ojYNq5yCPr89HMT9NZtlA4McddL9/LInm1M+9VvGTigOzW7q1i36FmWvf4yn73/Kuf26UxT5T+47q5biGtB1NGk2nWPA7aL307h1VFyZZSBGT76DS0nK9WGNxUHy0NK+qhqjtHquGjLQLspXMOk2ZfDvG17GHHkwVx93z/SWsuOw5FHHMGRR0yipm4P69ZuoKamnm2btxJLpti6fTvr1q1LVxm3tLJm1SomTjpSHnjQWP3ySy8esHr96hEDeg9Y/GMzI+b36L/SgGGaZnzqaSc0CEPQsWNHnZuXS3l5Gb+54jfc/vc7uOAX53HUoeOYdMThhAzIL86ixu8w39XU7mxj6+5GXKXJyIC++fkMzJKUZEa4ZGwJb2z/lIfOnUZSGFipKMWmw4AQnDYynwx7D3ZbjFSGl4j0kDD82IYHraL4EwmyXUGWx0uWzMCIJ0m27UE4CTymJKwlmxoj1MZsMMEUKZCCRCCb1zc0MPKkk7j2rjuJaYEKR7H8Jq2pFgwtyC8oYOKEjl94FouWr2LHjh1kZWXx+OOP4/UHmTx5svAHfE7KsX0bNmwaCSxud/vVf5zldxxHnH3R2aY0DJLJJB7LQ1ZWNn379uWPN9zI22+9y9vvvcjrb79O/w6dKHBgQ34WSzfV4QQ6cPzl11HasYQFc9/nH3PewbOpge4h6Frko09pR0Z1CoIbxe/NwkLgVYrWSAtRW2AamURcQV3EpSbcTFwYdM+TdMr2EorHkaqFhCMwhcB0U2jLJGb52NwQYXdS4/o8JFUKaRhIy8PmlhiR7GIuvflvePyZzHr+OVq37OKi316O46SHIDgoUk4C29H4LA9r1m5g9erVZGVlsXHjRl6cPZup005i0OBB+pVXXjK00qpn7+5vAEyYMMH9j7v846ePl0II55Y7bnk1MzNrVFV1le7ZuyebNm3EHwxQXNSBs06bxqHHj2LFpyvY/tFGkmGL9zfvJCOnjCcqn6fvwLQe4hnnnse2zYuZP2cOr730AY98/BGhrVso1qSz0EBSQ1SlJztIwOczMfx+sopK8OflIgMB3v1sDUMsm2P6FhNwq/HIGB5L4MS8xGyoa43REHOxLR/g4noM4tKH5c2gvqENJ+CluTnMS2+/wV//dgt9i8u56MrfYJp+XGykkGgDvKaH+qZGNm7eRMAfwHVdHnvsMY488kiOP/54Gurq9SeffCIPGnfghgE9B1SnhQx+XNL4e4E2Y8IMJs6cyICBA1o6lpaQm5dDh8ICRo8aybOVz7Hus88Y2K8P+Z0KOOroY/GN8VDfVMPFf7iYK676NX0HDqCxbTcBXwZCWZT0HMLpvUdw0jm/Ye2m9WzfuJbaLduprm8jkUqglI1hGfiDPvLz8yjuWEKfvgOJ2Qlmv/IK/QYOoGrzZh6/dibh1THG5HsJxiSWUkStGAkhUVYAkZ2DdsHFpk2naEoYtDXHqI6kaIjs4dILLiSjJA9pGbTGY2gt0I7A5/HREmtJE8WGy6pVK6itq6WkuCN33XkHpmVx6hmnk0wmqa2v01qI+EmnnXZrdXW1nDVrlqioqPjPB9d7x/nWVtcarrDJLcxCoRg6ZBDdepbz/POzeXfufCyPh169ejFwwECWLFqLxxvksCOOIqlcgqHcfcPHlXaIOkksy2RQ334M6tvvW99DMmVzyulnUFZWhkeb5JQU0feoQ3n/xVfJzi8hZAZxiJOSCWK2JprUNETbCCc0CQS2KfGFMggWFdN1bE8O6d2foh6dCWZn8qtLfsW0n52ElmAJzZJli/ntFVcw7aSTGDp0KPPef4dNW3fQ1BJmw9bNTJ8xg0gyjmmaeDMCOiMz5F29Zk39hFGjknvHTv/nWf4JwEzwetNSEMlUEomkJd6Mx+vl3DPO5cgjjubV115n7ty5rFq9kgXzP2bChPGUdygnkozsE2VJC6GANA2UVsR17HNNgf/Kt7q2Q9AT4umnn2HH9l2cc855fPLpfO77xwN4vD5S3hDPb6/FEIqkJfBmZBMKhcjIzCU7r5BOBcXkFmaRUxQiMycXXzCEP5RNRiiL3t27c/0fr6dHtx4ceNBBrN24kR07dvCb31yG1+ulpLiUhZ8s5rFHn6AtFqdrj15ce911ZGVlYds2UkosyyIej8twW0tvIcRL06dPNz9XySbaW3X1TwKa1lpUVlbKNQX/XCkzJsxwhRB6AhOYyUzy83O1ISSRSARNuiNSSknEjlBYVMCFZ5/NGaefxqcLP+WN115l4sETkRjpGRFf0fi3P4pvQqcHer/3/vv0798P13V46qmn6NqjG1NPPplYUxsBRyIsCx30E7BCBP1+gj4vphRo2yFhpBA5XjL8QbJCIYo7lNAhr4gAsHHjRrKzs9mwYQMvvfQSlbNm0dLSzDV/+ANzPpjLY48/hte0+PnPT+DwyVOQUuwTLXNdF9O0RDKZJOgPTJw1a9ZfbrnlFvH5SrbP6zb+qKB9Lgf0Ba9nJjPRWssZM2Ywffp06csKvNPW1mbX19dbAqkt0xJ7hbvAJeakCFh+8vNz8Pk9HHjgWAAsj/W9ujV1u8ZxLB5l+/btHHzIJHbv3s3OnTs56/wzGNC3O4nWKDkiiHI0cUMQNVxQLspOgiHp3L0jufkFSNOPz+MhGg4Trm1ixfrtYEDfvn155OGHef/990FrCgoKKC4u5d577sfr9fCz405g4sEHk5mTTSyZwLadfdOa9so5JZJJmpub7csu/KUrhHDTMhXKC2TvbNhplOeX10gp1fdtMza/gVs0lq1ddtJHH37YHSQTDp6wfWDPgY+3e0J7deKbsnKyjaqq6vaRinpfrkeTnsyrtWb+R/MIBH10LCn5mqGt+03JYAqL+qY6WlpaKC0tpba2hkAgQFtDC++/+wHaVnhSLlpDwoRIIkIqZZNMJEkmbaQ0ScaihFuaMUwLny9ATl4BHTqUUFiQx9ixY+nWrRuRSJic7BzyCwqIhOM4jkN+fj5+v59kIkk4HEFaRlqd73MPXymF6zhI04y//NrLx7755itnFRYUdr/2+mt85Z3Lt02rOPlSIUT1j7bT2mkqobUOPPjEA88+8cSTU0pLSjFMk1nPP8dxx/3sQK31LaQ9cQVk5eXm1lTv3l2SFmlID4fXgNYS0a40sH7dRjqVdybLn01Cx39AT3RaJr01EgUJoawgO3cn8fr8uNrL9s27qKndQzwcRhiQcm0Sda1UV+0hGo9imF46dixn1JjhHHDwULLycvEGMvAFg3j8flQ8iky5dC3vimtZxFMplG2TmZONaRokUymS4ba0Ko9l7puD/XnznkrGRSIew02lBrz88stPWYYMjhk9jkcef5zFi5Z0a6ppflhrPXHVqlV5hmHU9e/fP/WDQJsxY4Yxc+ZM5/2P3r/lwYcemjJt2rTkhedeaNjK5tFHHhWPPvboL957592zmpqblNaa3LxctW71Gp0TykTjSkPI9sku4Gq5b/Bp9e4aCvOKv0Yu9jttNBCCuO1ieb14vB40CqUUA4cO54BxExBuCtd1WL1iGa++/DI791QjLEl2Rj6GlLRFG5n7wQe4ySQ/nzqVYFYmcTuJoRTCb2GGQthJCIejWCYYpsbWDrbtpHW65N4p3/8qDywNg9bWFplMxNm0YUOvgM9Hr5593WeefE401zfrgw8+mLqaupE33HBD/oknnli3YsUK9wftNCEEM2fOdLXW/klHTzquvKxcXXbB5VbUjkjlKi4+71d06dLFnTWr0ijvVG74fD6yQtlGcUERq1auJBoPE/AF0lNn20HZ+7WluZnSsvLP6df/wEtpMjIy2r1XhasU8VgUr9eiprqaO2+/jc3r1zFoyCAuuewSunfvTn5+AeFwGzt37mLb5i28+cYbzP3wQy64+JcUFndgxYoVVNfUYPq9DBjUn4F9+2AJTSppI/Dul9KqaRi0tLTgOA7+QED37Nadbdu2GTt37iQjM6QdxxFCiMS1116rr7322uT3rRzYB9pzzz1nVJxY4d794N0Td+3a1fHmm25SjnbkXschYoc56rApxlGHTfnCU69trBITxk9k+/bt9O8zCKWTaanczwHU1NxMn959vjBE5/uJraS/WpZJIBBIt9eaJsp1UW4KXJe/3HIzedkhXn3lJcaMHU1udv4+J6Yp3MKuXTuprq7l4MMO4+EH/8GNN9xIZmYmbc1NFHmhf1Yur1VW8sbA3kw+5RQGdxuIa6ebEoUQ/7LodPvosb0e5ObNmyksKCAUyqC4uINasXy5HHfggSKYEaS6ulqFw2Hv0Ucfbb366qv/ogL+nWtEKioqBBoWLV70y65du9C/fz+VdBNYlvXP+CMtIytiblREnYiIOhFRkNuB0tKOvPLKK+mpg66LqxSO4yClJJqIs6e6msKiou83SfxLOVtFepd5vB5SqRQBvx/HcRAaXpr9AtFwC6++8hJTjjqaUDBAUidIqARJnSAzFKB/37506dYJ0+fh4ssuZcyYMbQ1NTJ8+AEM7duTMr+Hc8aNJWPTDu6/5nrmvfkOPr8fpdJm+Ou0i6WUKKX0ls2b6dqly/bSjmUfv/7Gm0Z+fr4oLS1VAwcOVCtXrpSGYWx75ZVX6gH5fdM28nMOiKu1ztm6ZUu/Ll264MFnuK77hZ0hpUyLKBvGPyVmhcHUE6by3LPPEYm1ImifXuEqPNLgrbffprZ6z74JED+UHlAoghlBPJaXZDItKyuAHdu3Mn/eHC668ALKOpYTizaDTjdnCpnepY5ycLRN9y496NGrJ7bjcOppp5KZmU00HKHiwktZnxNk3ua13Dh+Emdnd+CZ++5k3odzCQQCXwnaP0ePWbSFw6q2rlZ36dZ13RWXXXZ1bm7Om9FotHXb9q3ypZdeNEaMGLH20ksvPVsIEWnfsd8ftBkzZuydK5WfjCc7dyzpSLsX+a1Kora2+dnxFUSiSZ55thKP6SdpKwwhCYfD3H3HHQQCARrq6r5o476neXRxyMoM4Q94qK+rIzcnj1Awi0ULl2B6LKadcipKawy/D22aX/h9Usi06rhK0KdXdzIzMigpLuXgQyexbv0amlIepv3m13wSibNy/QouGFHG4bl+XvvHP9hT34DH8iCI4woF2sJFkB5vqvFaBls2rMVrCXH88cc9C3z6wH33T77++usHTjhs4thjTzhuxJ133tlvwIABc9vDKvWjlNDtqNnh1O6pVRkZGV/Y+t+kbZhIpSjIzefkU07j5pv/TFVtDYa08Pt83HPPPShXcf7557Nu7bp2oMUPMo+OcvEZFh2KCqmtrSE3J5ec7BwWzP+QDkUdKCsvx8UFmZ6e8ZUVMUJjoOlUXk48luCg8RMwPX4++eQjepb2ZNykE6jctI0mu5UTB/VANTXz3ltvEfQFUMpObw8t//mCWuPzeNylixfJLp07rTj6sKMeF0I4rusaHTt23HnEIUcsOGrSUUuSySTTp0+XP5TG+sKn2rR5k0gk4yIQCOzbSd9aVy4lSdvmggvOJxqL8re//Q2fZbF161buuOMOrrvuOiZOnMju3buIJdMDfb6vBykgPRQVKC0rZ9eu3fi8PsrKykjEInQo7oDP68NR7jea4b2DDQoLC/F4PJSXl9G1W3eWLJxDrDHGuDFT2ObN4e3NO+if5WVUfoiF77xDfU0NhseH1gqpVbqQyFVkBIIsWbSYql27xLHHHH1DIpFg+vTpBuBOnz5dzpo1y2iXp+LHaMr4AipFhUUE/AH2grY/D9cwDFyt6FjUgUsvvZRHHn6Y7bt2MnPmTKZMmcLBBx9Mbm4uLc0t1NTUYmJ9b9A0IA0DBQzo359YLIarXHr07AlAMJiBp33Q3LfFe65WZGRkkBHKQAhJv7592b1jB7U1jZR17URe/758XNWKSMUZUZhDvLGBVctXYFpelE5TY7i6XbPYcSuffVYM6Nd/xcXnXTq7vT/N2QtSRUWF+2N2iH4BtCyfF+tzCttyP9qshBD7gDv99NMpKCjgZ8cfT48ePbj11ltxXZfu3XtgWRbbtm39QeKZov1OqBSDBw+muLiYxoZG+vfvD8JI64Gwf3pmWmu80qAgPx+lNT179kA4Bs3xKP5glL49S1gfg+2RJP0KcsgEVq9YiWontWX7WNUMf4BH//GgDgWC8opLLr9BCKErKyt/0v60L7x4ZmGhNkxTx+Px9hhW7ZcqmxBpTisjlMFpp53CiiVL6d6tG8FgkGQiTl5eDvn5uSxduvSfAmHfY7fpdnPsKoe8zBxGjx5FfUM9GRkZWD7/vh32z2lKX+dACYz20Zg5Odlo7dKxtBTT9LG7cQ9atNC/e2dSQrKlNUGeT5IvYcPadbQ1NeP1eFGOQ05miLffesP94P33zMMOP/ypoSNGvDR16tSfvO/6C6Bl+7KN/KICuWXLlv02j7ZwUUJhCUksFqNLpw50K87m8osvZvmiTwkEA0gp6dajCx99PH8faF/nPu/PZcn0qTR02BC0cEnYaULXaR9UIL5iyOoX+7sEVnvUFwz5EELhDwbQPoO2RC2uDpJd2AevL0R1c5iglaJzyENrYw1tO2swscjKDLFs2afO/fffLY+YfMTOP86Ycb4QQvXt2/cnF4DZ6/LvXZptoVBG8+ZNmwC0YRj7XaKsSZ8T8XicbL+fULyNS39xNtu37kRKg9EHjWXRkk+pbanB5/H9MDMpBQ4OnTp1Ii8vn2QytU/4+btMftdoLNNECInH4yEUDKHjSaSjCWVl4c3NZk8kgkcLumZlklQudXsaCAWCfDD3A/Wn6683p02bJm7569/OE0JEp0+fbvxU3Z//AtresjhDGnU1e2pf2bJtG+Fkm9of71Fo2od/Q2O4hZTtoBNJDu9XTtP6tZw17SQaG5uYdMThNDY3sWTxEgyMH7TT9ppur+UlLzeXVCqJZVm4rvO9TO5e3ayA14+2U0hlEwj4CBbmUxV3EA50zcjEBKp21/DoAw+qO+68VZ5QUVH7u99eeXafrl3fnjVrlvHvEoD5JypT0UorcnJzPtq2baveuGmjNjG/9eEK10VoaIlF2L57V7oszXbJd+McNaALtUs+5ZTJk8kwfBw4bgJPPPkEAvn9O+O+8ObFPnrMHwjQ3Nz8hUE8+wOZaaR1sizLwvJaOMpFOTaGIcjMySEBJG2XQq9FloR335mjXqqs5LQzTtn+5GOPjx08cODD/45z7CtBe+OsN8yqtqr8v/z5L5GcnBzxxBNPYEnP14K2V7zSa5gINMtWraShpRkpJabWWJE2yo0kx/UpYdeihUz72VTKy7uxeMlidtXuxBR7F4T+/jyk1uTn52GnbDIyMqirr8duz+Htr5+Tzjanb9trYGT6cdwUPtMkLzMbW0FcuwS0QlhSN4RbOO/Ms+UjDzxyYrvEkqeysvLfBtgXQOvTp49oqakvG9p/6Ivjx4/f+dxzzxm763a6QTODVCqF47goV+M6CuWCKTyApC3qsHjpanZt34lfSTL8IayMEIbPi1SuLs0M6mNH9NXhjRv1Oy+8qPfs3KOXLlmmvdKXnsyuJFq3Z3xdd59nmZ53TXrynSvQ7V9RIq2JjyRpp8jKzMK2U3g9FvV19bS0tCC/ZRvr9tmBChCmheH1YmsYMngo/bv3JaXBNgShQBChIYJLk08STyo9dtRocdbpv/hVJNWcmD59upwxY4bNv/nal5rp0qVLAlgOsGrDqgu3bNny+hFHTOaZp592+/fuZ6SNUfqgT7oOVbv3UFtXS31bnLa2KLnBLC2SCdXUFtdR02RBU4KASpjajhKVXjJCQRrjCeKRBFf/7g8oW6iJB0+UoVAI202PhUTsjQ1FO1Ft7JvTK8Tn9qRI/61hQNDjZcCAvmg0dXV17Nmzh8LsHJxvTLYKtBAoBNI0MLxeki1tTD32BLwiQTiZQPvTg2ulKWgLZbKottbJC4XMKUcf9syI8SPuuv32273tuvr8x0BrN3mysrJSDOg54J0ZM2Y8MH369PMOPvgQBgzoz4gDDlDdu/RUoVAIx3WpqqqipaWF6roGduzYJZrra42WhjoDxyYnO5PdoW4kmlpjAb+hhMcjYpbU2R6fKEDoaCzmP+2004xBgwepE0+s0P0HDJQdSzuKYDCIx+PBMMy01r1hYkiJlMa+GMxx3bTopgbHcXAdl46dO5GXX8D2zZuoramBPn2/NVbb6/k6roNj26A1KSFAKSzTRCJwHBcZzGBlTOrX1lbT7+CDnYt+c9lzk6ccWWoYRt1P0QT/fQp7dEVFhVqydknxEROPPH/Z2kVP3nHbXVcvWbLkkIceesgSypAp207LmwN2KoXH6yUjI0QoFLS79+i6oH+f3ssHDRmyqtuQA9zGcPjdft272yUlJYQJI8IRIaJCJLTOe/ShB2+b+8Gcw6//4/WYHi9l5Z3UyJEH6PLyctGxYym5ebli72Q41T63zbFtkqkUrlL74HAch2AwSK/evVj6ycds276Nb0vc7TW/CgU6XZAE4AoBhoFKpVCuxlaCJlvw+Cef2WZJiWfAiBHTQ17vFitoZRdkFbQKIVL/CeDMrxL4H953+B5A9Os2aL7f7z8yFot1fuP9t6fU1zVYO7Zu11u2bBHxZJJQMKgGDx0k+/bovafv0L6fFOcU1xqmmdhL6n7DVae1/nnDRRcfcu99901evmJZxZq1a3IefeRBUo5DbnY2+enSNdW1a1dVVlYm8vLzdXZ2lvT5AmC0U2dKCW1p4fN4mTJ5in79lVdYsWKF2B9SQCuFNE0ibWHCbWFMy8QRBtrRGAp8Xh9JLVjX0ubkFhV5Tj31jA9uvvlP9wkh6n6soQg/avvu3tXTLoWn0n+lOwshtn/Ti+3YveXo3Q01C9958Z2mkpIScd555zlf5a3NmjVLVlRUuNuat2X3KOjRYtv2gSs+W+Ffv2H9JYuXLe7y/rvvEYlEe4dbYjKVTJFIJMjJzUUKQTAUwucPYu0d44xwpZSUlHY05s59n379+6qP58yTcTf29YPvNGhXIKTBRwv/X3vXEtNEFEVvp9POiK21IgpIFRGKBMWFf6lp/aAL0+UksnFDWmpIMdGYGI2Zla7cNMYNMVGC0QR/CdWKGmyUAjE20USigQLVUkRjtAh++pmZ48JWkQSj8a+czVu/d1/ufe/ec+69TYNDw6TR8PROYkhPcdJP01DX/QEcOXRYKsjRaXY4aq4d3Ld/ezgcNgeDwaAgCPgZnXh+qOZaFEXGbrervV4viD5x+YmIbDbbx//ut2QDxrkVdYYUq1KpSKfTkSRJFB2Jll88czE7mZLsjyPhorbrbdH5C+ZviL0amfn82XNKphKkn67PM84ysrIs09jrsXeGGQapPzygb2pqVCwr1zOxtzHSshwxjPqzvGNSUkij5SkyGKFARyexGi2xLEtZPE/JN6+VK5e9SmPTGdZiWUc7HY7j1dXVdSqVKvWrehT/UgDjK4NfLQcmog/NmydjJ2XSaUajkQCwADTptRzAVgBbAFQAKKutd4aWLC9H153OFNJIKjLeJhN4k4gjLqUQBzD44iXO+1px9rIP532tOHbipFzjqlVKSs0wl5XBudMVu3bjxgEAhR6PhwPAiKLI0BS+fAEEQVCnBQyM1Wr9olikpaVldl+kb0kSyeWWTZZ7s3KNcO92J/ztt+TY6KiUAiQJkGRAeTQUxTnvJfloQ4O0Z/+BlHVzFeYWmFBkLkHdrvqBq23X9wKYQ0Tk8Xg4v9/PTlmEvruklmFCMwDUANju7m7d3Z6eeUREccQX19Y7I9lzs8HyHBaWFGNV5Vps3FKFFWtWY1GpWckzmWCcnYN8kwlrKitR53bfvNnRsQ1A1rg5BOo/9RD+KTQ3N6sFQVCIKP/0hVOO0IPwtt5QiAmF+jD8dFi1sLBw6UyDQcMwTH9+bu7DimUV7S6ny0dE/em5MHG/38/abDb5d7wM/ztkYs6TJ705gUBAT0Sk1WqJ4zjieZ44jiMAxQCWAeAztL7J4uwUfo8R2YkudWLuVRRFNu1mVVMG+0PdPgAmM4Dub93cezy8pE+XU6RxAAAAAElFTkSuQmCC'; /* 把 #rgb / #rrggbb / rgb() / rgba() 统一揉成「指定透明度」的 rgba()。 认不出来的写法就原样返回,交给浏览器自己解析。 */ function withAlpha(color, alpha) { const s = String(color).trim(); let r, g, b; const hex = s.match(/^#([0-9a-f]{3,8})$/i); if (hex) { let h = hex[1]; // #abc / #abcd 这种缩写先补全成 #aabbcc if (h.length === 3 || h.length === 4) h = h.slice(0, 3).split('').map((c) => c + c).join(''); r = parseInt(h.slice(0, 2), 16); g = parseInt(h.slice(2, 4), 16); b = parseInt(h.slice(4, 6), 16); } else { const fn = s.match(/^rgba?\(([^)]+)\)$/i); if (fn) { const p = fn[1].split(/[,\s/]+/).map(parseFloat); r = p[0]; g = p[1]; b = p[2]; } } if (![r, g, b].every(Number.isFinite)) return s; return `rgba(${r}, ${g}, ${b}, ${alpha})`; } const BAR_PLAYED = withAlpha(BAR_COLOR, BAR_ALPHA); /* 「已播放段」怎么上色:彩虹 or 纯色。 */ const RAINBOW = BAR_FILL === 'rainbow' && BAR_RAINBOW.length >= 2; const barFillCss = RAINBOW ? ` /* 彩虹:把 BAR_RAINBOW 按顺序铺成一条横向渐变。 必须先 background-color: transparent —— B站自己在这个元素上有 background-color(原生蓝),不清掉的话 会从半透明渐变的缝里透出来,整条彩虹都带蓝。 background-size 故意不加 !important:它是给下面 syncRainbow() 动态写的, 这里的 100% 100% 只是脚本还没跑起来时的兜底。 */ background-color: transparent !important; background-image: linear-gradient(90deg, ${BAR_RAINBOW .map((c, i, a) => `${withAlpha(c, BAR_ALPHA)} ${((i / (a.length - 1)) * 100).toFixed(2)}%`) .join(', ')}) !important; background-repeat: no-repeat !important; background-position: left center !important; background-size: 100% 100%;` : `background-color: ${BAR_PLAYED} !important;`; /* 拖动小三角的颜色。彩虹是渐变、而三角形只能单色,所以不分给它渐变,给个中性白。 */ const BAR_ACCENT = RAINBOW ? 'rgba(255, 255, 255, .75)' : BAR_PLAYED; /* 圆点图标替换的那几行 CSS。PROGRESS_ICON_SIZE 为 0 时整段不输出。 ⚠️ 千万不要碰 transform。 B站是靠 .bpx-state-active 把这个元素从 transform: scale(0) 放到 scale(1), 才做出「鼠标移上进度条图标才出现」的效果。一旦覆盖 transform, 要么永远显示、要么永远不显示。 所以放大和居中只能绕开 transform:固定 width/height,再用负 margin 把它 相对原来 20×20 的容器挪回正中间。 另外 background 默认会被裁到元素自己的 border-box, 所以「元素放大」这一步也是必须的,光改 background-size 会被裁掉。 「跟进度条同生共死」怎么实现(v1.14.0): B站隐藏整条控制栏/进度条时,是靠 .bpx-player-control-top 的 opacity:0 + visibility:hidden 往下级联,进度条区域和圆点容器都会 继承到 visibility:hidden。我们的图标和拖尾都在那个子树里, 本来也会跟着不可见。但**光靠继承不够稳**: · 有些版本 B站只降 opacity,transition 期间 visibility 还没翻, 图标会「残影」一下; · 拖尾是 position:absolute + 独立 animation/filter,个别内核下 会短暂脱离父级的可见性判断。 所以这里由 JS 显式判一下「圆点现在到底可不可见」,把结果写成 html.bbhr-dot-off 这个根类;图标和拖尾的显示都挂在这个类上。 这样「进度条出现 → 摩托出现;进度条消失 → 摩托消失」就和 B站 自己的节奏完全同步,且不依赖任何具体版本号。 */ const iconCss = PROGRESS_ICON_SIZE > 0 ? ` /* ---- 7. 进度条圆点图标:换成自定义图 ---- */ html.bbhr-thick .bpx-player-progress-thumb-icon { width: ${PROGRESS_ICON_SIZE}px !important; height: ${PROGRESS_ICON_SIZE}px !important; margin: ${ICON_PAD}px 0 0 ${ICON_PAD}px !important; background-image: url("${PROGRESS_ICON}") !important; background-repeat: no-repeat !important; background-position: center center !important; background-size: contain !important; } /* 进度条/圆点被 B站收起时,我们的图标也立即收掉,不留残影。 ⚠️ 这里只用 opacity 藏,绝不能再用 visibility:hidden —— 门控 syncDotVis() 第一条判据就是读这个图标的 computed visibility;一旦我们用 visibility 把它 按成 hidden,门控下一帧读到的永远是自己写的 hidden,于是 dot-off 类被 永久锁死:B站 之后把圆点 scale 回 1 也摘不掉类,圆点就再也浮不出来 (v1.16.2「圆点消失」的真因:v1.14.0 起就埋着,脚本复活后才发作)。 opacity 不在门控判据里,所以藏得干净、也不会自锁。 */ html.bbhr-thick.bbhr-dot-off .bpx-player-progress-thumb-icon { opacity: 0 !important; pointer-events: none !important; } /* v1.16.4 鼠标一进播放器,小摩托就显形。 B站 圆点的显隐全靠这两条: .bpx-player-progress-area .bpx-player-progress-wrap .bpx-player-progress-thumb-icon → scale(0) .bpx-player-progress-area .bpx-player-progress-wrap.bpx-state-active ...-thumb-icon → scale(1) 而 bpx-state-active 只在鼠标压到进度条那一小条时才加 —— 所以「鼠标在播放器里 但没碰进度条」的时候摩托是不出来的。想要「鼠标一进播放器就出来」, 得把 B站 那条 scale(1) 补上:由 JS 维护 html.bbhr-hover-player, 类在手上的时候就压住 B站 那条 scale(0),强制 scale(1)。 ⚠️ 这是全场唯一一处覆盖 transform 的地方,注释里原话说「千万不要碰 transform」 是因为碰了会变成永远显示 / 永远不显示;现在我们是**只在 hover-player 时**碰, 类一摘,B站自己的规则立刻接管,所以「鼠标离开播放器 → 摩托跟着进度条收走」 这条没变。 ⚠️ 选择器比 B站 那条多一级根类(0-4-0 对 0-3-0),再带 !important,稳赢。 */ html.bbhr-thick.bbhr-hover-player .bpx-player-progress-thumb-icon { transform: scale(1) !important; } /* 藏掉 B站原生那段 Lottie(白方块 + 小电视 Logo 都在里面)*/ html.bbhr-thick .bpx-player-progress-thumb-icon > * { display: none !important; } /* v1.16.5:骑行颠簸(上下浮动)已按需求移除——小摩托现在稳稳贴在进度条上, 彩虹烟雾照旧从车尾喷出(见下方 trailCss),其余一切不变。 */ ` : ''; /* 进度条圆点的彩虹烟雾(小摩托身后喷出的那柱彩烟)——「大图同款」。 与中间大图那辆摩托的尾气用同一套坐标系/锚点/参数,只是整体缩放。 用 子元素承载动画,绝不碰 .bpx-player-progress-thumb-icon 自己的 transform —— 否则会破坏 B站 .bpx-state-active 控制的「移上去才显出来」。 锚点经像素分析确定:图标 90×96,车尾最左点 (13,78),contain 进 130 框后 留白 (20,17),故车尾 ≈ 参考框的 x25% / y73%(相对圆点容器解析)。 */ const trailCss = PROGRESS_ICON_SIZE > 0 ? ` /* ---- 8. 进度条圆点的彩虹烟雾(大图同款)---- */ .bbhr-trail{ display:none; position:absolute; left:0; top:0; width:100%; height:100%; pointer-events:none; /* v1.16.5:摩托不再上下浮动,烟无需跟随——只保留 bbhrSmoke 喷烟动画。 */ } html.bbhr-thick .bbhr-trail{ display:block; } /* 进度条收起时,整柱烟也一起收掉(与圆点同步,见 iconCss 里的说明) */ html.bbhr-thick.bbhr-dot-off .bbhr-trail{ display:none !important; } .bbhr-trail i{ position:absolute; width:9px; height:9px; border-radius:50%; transform: translate(-50%,-50%); background: radial-gradient(circle at 50% 50%, hsla(var(--h,0),90%,72%,.95) 0%, hsla(var(--h,0),90%,62%,.5) 38%, hsla(var(--h,0),92%,58%,0) 72%); opacity:0; filter:blur(1.1px) saturate(1.25); animation: bbhrSmoke 2.2s linear infinite; will-change:transform,opacity,filter; } /* 锚点与大图 .exhaust 同一份百分比(left:25% / top:73%),逐团带轻微纵向抖动 */ .bbhr-trail i:nth-child(1){ --h:0; --dx:-18px; --dy:-11px; --rot:-22deg; left:25%; top:73%; animation-delay:0s; } .bbhr-trail i:nth-child(2){ --h:45; --dx:-23px; --dy:-6px; --rot:18deg; left:25%; top:76%; animation-delay:.28s; } .bbhr-trail i:nth-child(3){ --h:90; --dx:-28px; --dy:-12px; --rot:-30deg; left:25%; top:70%; animation-delay:.56s; } .bbhr-trail i:nth-child(4){ --h:135; --dx:-16px; --dy:-4px; --rot:25deg; left:25%; top:77%; animation-delay:.84s; } .bbhr-trail i:nth-child(5){ --h:180; --dx:-25px; --dy:-9px; --rot:-15deg; left:25%; top:73%; animation-delay:1.12s; } .bbhr-trail i:nth-child(6){ --h:225; --dx:-31px; --dy:-14px; --rot:30deg; left:25%; top:72%; animation-delay:1.4s; } .bbhr-trail i:nth-child(7){ --h:270; --dx:-21px; --dy:-10px; --rot:-26deg; left:25%; top:74%; animation-delay:1.68s; } .bbhr-trail i:nth-child(8){ --h:315; --dx:-34px; --dy:-17px; --rot:20deg; left:25%; top:69%; animation-delay:1.96s; } @keyframes bbhrSmoke{ 0%{ transform: translate(-50%,-50%) translate(0,0) scale(.3) rotate(0deg); opacity:0; filter:blur(.7px) saturate(1.25); } 12%{ opacity:.85; } 55%{ opacity:.5; } 100%{ transform: translate(-50%,-50%) translate(var(--dx),var(--dy)) scale(2.6) rotate(var(--rot)); opacity:0; filter:blur(2.8px) saturate(1.25); } } /* 暂停(或视频还没播)时整柱烟冻结 */ html.bbhr-paused .bbhr-trail, html.bbhr-paused .bbhr-trail i{ animation-play-state:paused; } ` : ''; /* ================= 播放器左上角 UP主头像(v1.16.7,即使已关注也照常显示) ================= 需求:就算已经关注了 UP主,鼠标移进播放器时左上角也要浮出 UP主头像, 移开播放器就收走。 B站 的 .bpx-player-follow(头像 img + 「+」号 + 文字,89×32 圆角胶囊) DOM 一直都在,平时被内联 style="display:none" 压着:未关注时鼠标进播放器 B站 自己会把它拉出来,已关注后则永远不再显示 —— 这是 B站 的 JS 行为。 做法:复用 v1.16.4 的 bindPlayerHover 维护的 html.bbhr-hover-player 根类 (鼠标进播放器加、离开摘、小窗 data-screen=mini 排除),类在时用 !important 压过内联 display:none 强制显示;类一摘,内联样式自动接管, 隐藏如常。原生显示态就是 display:flex(B站 样式表实测),照抄即可。 内联 style 无 !important,0-3-0 的选择器 + !important 稳赢。 与「UP主悬浮卡」功能天然配合:头像显出来后把鼠标移上去,原有悬浮卡 逻辑(upCardTrigger 找的就是 .bpx-player-top-left-follow)照常把右栏 UP卡浮出来;反之 display:none 时按钮矩形宽高为 0,placeUpCard 的 宽高容差判断会自动跳过,不会误摆位。 */ const followCss = ` /* ---- 9. 播放器左上角 UP主头像(鼠标进播放器显示,即使已关注)---- */ /* ⚠️ 已关注时 B站 会把整段 follow 区(父 .bpx-player-top-left-follow 乃至 .bpx-player-top-wrap)藏成 display:none,只拉内层 .bpx-player-follow 会被父级塌缩吃掉、出不来。所以父级也得一并 !important 拉出来。 */ html.bbhr-hover-player .bpx-player-top-wrap { display: flex !important; opacity: 1 !important; } html.bbhr-hover-player .bpx-player-top-left-follow { display: block !important; } html.bbhr-hover-player .bpx-player-top-left-follow .bpx-player-follow { display: flex !important; opacity: 1 !important; } `; /* ================= UP主悬浮卡(悬停播放器左上角「关注」) ================= 需求:鼠标移到播放器左上角那个小「+关注」按钮上时,把右侧栏里的 UP主卡片(头像 / 昵称 / 发消息 / 签名 / 充电+关注按钮)浮现出来; 鼠标移开就收起。 为什么不能用纯 CSS :hover: 触发方(播放器左上角的 .bpx-player-top-left-follow)和 被显示方(右侧栏里的 .up-info-container)**分属两棵不相交的子树**, 没有共同祖先可以挂 :hover … descendant 选择器。所以只能靠 JS: 监听触发方的进出,把状态写成一个根类 html.bbhr-upcard-open, CSS 挂在这个根类上控制显隐。 为什么「节点不搬家」(沿用竖排栏那套): .up-info-container 是 Vue 渲染的,一旦 appendChild 挪走, 「关注 / 充电」按钮的计数更新会找不到节点。所以照旧只加样式: · position: fixed 把它从右栏文档流里抽出来,钉成浮层; · 每帧用 JS 把它摆到「关注」按钮正下方; · visibility / opacity 用一个根类统一切换显隐。 右栏(.right-container)本身也被收了起来,但用的是 position:absolute + left:-99999px(**既不是 display:none,也不是 visibility:hidden**)——原因见 css 里第 1 节的说明:display:none 会把后代尺寸全归零;而 visibility:hidden 会把整棵子树从绘制/命中里摘掉,后代 visibility:visible 也救不回来。 只有纯几何挪出视口,才能既藏住右栏、又让卡片浮得回来。 锚点垂直位置:贴在关注按钮下方 6px;水平左对齐按钮左缘。 */ const upCardCss = ` /* ---- 9. UP主悬浮卡 ---- 常态:彻底移出视口 + visibility:hidden,不占位、不吃点击、不闪。 只有 html.bbhr-upcard-open 时(且开关开着),才浮现到 JS 写好的位置。 ⚠ 不用 display:none —— display 由 B站 自己控着,覆盖它风险大; visibility + opacity + translate 这套组合足够干净,回退也不留残影。 */ html.bbhr-upcard:is(.bbhr-on) .up-info-container { position: fixed !important; /* left / top 由 JS 每帧写内联值;这里给个兜底(万一 JS 还没跑到) */ left: 0 !important; top: -9999px !important; z-index: 2147483000 !important; width: 350px !important; max-width: calc(100vw - 24px) !important; box-sizing: border-box !important; /* 浮层观感:圆角 + 阴影 + 不透明底,避免和底下视频内容糊在一起 */ background: var(--bg1, #fff) !important; border-radius: 8px !important; box-shadow: 0 8px 28px rgba(0, 0, 0, .18), 0 2px 8px rgba(0, 0, 0, .1) !important; padding: 12px !important; /* 收起态:靠 visibility + opacity 藏,同时往上挪一点点做入场位移 */ visibility: hidden !important; opacity: 0 !important; transform: translateY(-6px) !important; transition: opacity .16s ease, transform .16s ease, visibility 0s linear .16s !important; pointer-events: none !important; } /* 展开态 */ html.bbhr-upcard:is(.bbhr-on).bbhr-upcard-open .up-info-container { visibility: visible !important; opacity: 1 !important; transform: translateY(0) !important; transition: opacity .16s ease, transform .16s ease, visibility 0s !important; pointer-events: auto !important; } /* 悬浮卡展开时,里面的按钮恢复可点(B站 原生布局照旧,只补圆角) */ html.bbhr-upcard:is(.bbhr-on) .up-info-container .up-info--right { width: auto !important; } `; const css = ` /* ---- 1. 隐藏右侧推荐栏 ---- 推荐视频 / 广告卡片这些**直接 display:none** 就够,干净利落。 */ html.bbhr-on :is(#mirror-vdcon, .video-container-v1) .video-page-special-card, html.bbhr-on :is(#mirror-vdcon, .video-container-v1) .video-page-special-card-small, html.bbhr-on :is(#mirror-vdcon, .video-container-v1) .recommend-list-v1, html.bbhr-on :is(#mirror-vdcon, .video-container-v1) .rec-list, html.bbhr-on :is(#mirror-vdcon, .video-container-v1) .next-play, html.bbhr-on #reco_list { display: none !important; } /* 右栏容器本身:默认用 **display:none** —— 这是 v1.11~v1.14 一直在用、 被 140+ 项回归验证过的「无害基准态」。 ⚠ v1.15.0 曾把它改成「永久离屏」(position:absolute + left:-99999px + z-index:2147483000,整条规则常驻生效),结果在真实浏览器里带出三个回归: 设置按钮消失、滚到评论区小窗不出、UP主卡不浮出。原因: 1. 设置按钮 #bbhr-ui 与右栏从此**同为 z-index:2147483000**,同层里 谁的 DOM 顺序靠后谁赢 —— B站 是 SPA,会往 body 末尾追加自己的 层(#app 重渲染),右栏就排到了我们按钮的后面,把按钮压住了; display:none 的右栏不参与绘制/堆叠,压根没这场竞赛。 2. 那套「抽离文档流 + 顶格 z-index + overflow」常驻在页面上,还会 干扰 B站 自己对「视频在不在屏幕上」的判定链路(小窗由此不出)。 所以默认态回到 display:none;离屏那套**只在悬停「关注」按钮期间**临时 启用(见下一条规则)—— 那是 UP主卡片唯一需要它的时候。 为什么悬停时还是不能用 display:none:它会把卡片(.up-info-container) 的 offsetWidth/getBoundingClientRect 全归零,position:fixed 的子孙也 救不回来,卡片永远是 0×0(v1.15.0 实测)。 也**不能加 visibility:hidden** —— 祖先 visibility:hidden 会把整棵子树 从命中/绘制里摘掉,后代自己写 visibility:visible 也救不回(CDP 实测)。 只有「抽离文档流 + 挪出视口」能既藏住右栏又让卡片浮得回来: · position:absolute 不占位(左栏照旧铺满); · left:-99999px 挪出视口 —— 子树绘制/命中都还活着,卡片靠 position:fixed 回到视口内正常浮现; · overflow:hidden + height:0 干掉抽离后残留的高度; · z-index 给到顶 —— 左栏 .left-container 是 position:sticky 且 z-index:1,自成堆叠上下文;卡片自己的 z-index 只在右栏这一层里 称王,跨不过左栏,所以右栏必须同时抬到左栏之上。 */ html.bbhr-on :is(#mirror-vdcon, .video-container-v1) .right-container { display: none !important; } /* 悬停「关注」→ 临时把右栏切成离屏态(见上一条规则的解释)。 ⚠ transform / filter 必须显式摁成 none:右栏一旦带 transform(或 filter / contain),它就会变成卡片(position:fixed)的**包含块**, overflow:hidden + height:0 会把卡片整个裁掉,卡片就浮不出来了。 */ html.bbhr-upcard:is(.bbhr-on).bbhr-upcard-open :is(#mirror-vdcon, .video-container-v1) .right-container { display: block !important; position: absolute !important; left: -99999px !important; top: 0 !important; overflow: hidden !important; height: 0 !important; transform: none !important; filter: none !important; z-index: 2147483000 !important; } /* 右栏里那两块内容(弹幕列表 / 推荐列表)直接收掉,免得抽离后还残留 */ html.bbhr-on :is(#mirror-vdcon, .video-container-v1) .right-container .rcmd-tab, html.bbhr-on :is(#mirror-vdcon, .video-container-v1) .right-container .video-pod-above-modules { display: none !important; } /* ---- 2. 左栏按 16:9 + 视口高度自适应,整体居中 ---- */ html.bbhr-on.bbhr-grow :is(#mirror-vdcon, .video-container-v1) .left-container { width: min(100%, calc((100vh - ${CHROME_RESERVE}px) * 16 / 9)) !important; flex: 0 1 auto !important; max-width: none !important; margin-left: auto !important; margin-right: auto !important; } /* ---- 3. 播放器跟随左栏铺满,高度由 16:9 推出 ---- */ html.bbhr-on.bbhr-grow :is(#mirror-vdcon, .video-container-v1) #playerWrap { width: 100% !important; height: auto !important; } html.bbhr-on.bbhr-grow :is(#mirror-vdcon, .video-container-v1) #bilibili-player { width: 100% !important; height: auto !important; aspect-ratio: 16 / 9; } /* ⚠ 必须把**小窗**放行(data-screen="mini")。 滚到评论区时 B站 会把播放器收成右下角的 320×180 小窗:它 position:fixed, 于是这里的 height:100% 是相对**视口**解析的 —— 被我们顶成 900px 高的一条, 小窗直接报废(实测)。 B站 自己给小窗的 height:180px **没有 !important**(见 core.css 的 .bpx-player-container[data-screen='mini'] 那条:position 带 !important, width/height 不带,再加 [data-revision] 收成 320×180), 权重压不过我们这条带 !important 的规则,所以只能靠选择器把它排除掉。 正常状态(data-screen 实测是 "normal",CSS 里另有 web / full)照旧铺满。 */ html.bbhr-on.bbhr-grow :is(#mirror-vdcon, .video-container-v1) .bpx-player-container:not([data-screen="mini"]) { height: 100% !important; } /* ---- 4. 播放器进度条加粗 ---- 进度条是一层层嵌的 div,最里层的“已播放/缓冲/轨道”靠 transform:scaleX 控制长度, 所以这里只改 height,不碰 transform。 */ html.bbhr-thick .bpx-player-progress-area, html.bbhr-thick .bpx-player-progress-wrap, html.bbhr-thick .bpx-player-progress, html.bbhr-thick .bpx-player-progress-schedule-wrap, html.bbhr-thick .bpx-player-progress-schedule, html.bbhr-thick .bpx-player-progress-schedule-buffer, html.bbhr-thick .bpx-player-progress-schedule-current, html.bbhr-thick .bpx-player-progress-point-wrap { height: ${PROGRESS_BAR_H}px !important; } /* 包裹层原本靠上下 padding 把细进度条顶到垂直居中,加粗后要去掉 */ html.bbhr-thick .bpx-player-progress-wrap { padding-top: 0 !important; padding-bottom: 0 !important; } /* B站鼠标悬停时会用 scaleY 把进度条放大 1.5 倍(原生 4px→6px 的“变粗”反馈)。 我们加粗之后会被撑成 1.5 倍,跳一下很难看,这里直接把缩放压掉。 */ html.bbhr-thick .bpx-player-progress-schedule-wrap { transform: none !important; } html.bbhr-thick .bpx-player-progress-schedule, html.bbhr-thick .bpx-player-progress-schedule-buffer, html.bbhr-thick .bpx-player-progress-schedule-current { border-radius: 4px !important; } /* 拖拽圆点 / 悬停光标垂直居中。 用 absolute + top/bottom:0 + margin:auto 的经典居中写法: 不依赖父容器高度,也不碰 transform(B站拿它做水平定位,改了圆点会横着跑偏)。 */ html.bbhr-thick .bpx-player-progress-thumb, html.bbhr-thick .bpx-player-progress-cursor { top: 0 !important; bottom: 0 !important; margin-top: auto !important; margin-bottom: auto !important; translate: none !important; } /* ---- 5. 进度条配色 ---- 只改颜色,不碰尺寸和长度。 「已播放」那段是 .bpx-player-progress-schedule-current, B站原生是它家主色 #00AEEC 的实心蓝,完全不透明。 加粗之后一整块实心色很挡画面,所以默认给了 45% 不透明度。 颜色 / 渐变 / 透明度都在文件顶部的 BAR_FILL / BAR_RAINBOW / BAR_COLOR / BAR_ALPHA 里改。 */ html.bbhr-thick .bpx-player-progress-schedule-current { ${barFillCss} } html.bbhr-thick .bpx-player-progress-schedule-buffer { background-color: ${BAR_BUFFER_COLOR} !important; } html.bbhr-thick .bpx-player-progress-schedule { background-color: ${BAR_TRACK_COLOR} !important; } /* ---- 5b. 小窗(tiny view)的进度条 ---- 滚到评论区后 B站 会在右下角浮出一个 320×180 的小窗。此时播放器容器 走进 tiny 布局(内部判据 isTinyView:容器宽 ≤480 或高 ≤320), 进度条换成**另一套 class**,跟主条完全不同: .bpx-player-mini-progress 轨道(原生 rgba(255,255,255,.2)) .bpx-player-mini-progress-buffer 缓冲(原生 rgba(255,255,255,.3)) .bpx-player-mini-progress-tempo 已播放段(原生实心蓝 #00AEEC)← 要改的就是它 它的机制和主条一模一样:元素 width 是整条 320px,靠 transform:scaleX(f) 缩到播放比例 —— 所以同样需要 syncRainbow() 写 background-size 补偿, 否则彩虹会被压进那一小段里、颜色看着和主窗口对不上。 ⚠ 高度(3px)和位置都保持 B站 原生,这里只改颜色,不碰尺寸。 */ html.bbhr-thick .bpx-player-mini-progress-tempo { ${barFillCss} } html.bbhr-thick .bpx-player-mini-progress-buffer { background-color: ${BAR_BUFFER_COLOR} !important; } html.bbhr-thick .bpx-player-mini-progress { background-color: ${BAR_TRACK_COLOR} !important; } /* 拖动时冒出来的小箭头,原本是实心蓝,跟主体走同一个色, 不然一个半透一个实心,看着割裂。(彩虹模式下给的是中性白,见顶部 BAR_ACCENT 说明) ⚠ 这两个箭头是用 CSS 三角形画的:三边有边框宽度,但只有一边有颜色, 另外两条斜边是 transparent。所以绝对不能整条 border-color 一起设 —— 透明斜边会被一起染上色,8×4 的三角形直接糊成一块实心方块。 只能点名单改「有色」的那一边: · -down(向下箭头)有色的是 border-top · -up (向上箭头)有色的是 border-bottom 这两个值是对着 B站原生 computed style 一个一个核过的。 也正因为是 border 画的,它上不了渐变 —— 这就是彩虹模式下小三角只能单色的原因。 */ html.bbhr-thick .bpx-player-progress-move-indicator-down { border-top-color: ${BAR_ACCENT} !important; } html.bbhr-thick .bpx-player-progress-move-indicator-up { border-bottom-color: ${BAR_ACCENT} !important; } /* ---- 6. 高能进度条(内部代号 pbp)---- B站只在鼠标悬停或「常驻」时才让它显形,它原生贴着进度条上沿、且 z-index 是 -1。 进度条加粗后向上长出来的那一段会把它整个压住,连带右上角那个「常驻」图钉开关也点不到。 这里让它跟着一起变高,并整体上移让出位置。 */ html.bbhr-thick .bpx-player-pbp { height: ${PROGRESS_BAR_H}px !important; line-height: ${PROGRESS_BAR_H}px !important; } /* .show 是「悬停显示」态,原本 bottom:100% 紧贴进度条上沿; 进度条长高多少就往上让多少,同时抬层级(原生 z-index 是 -1,会被进度条整个压住)。 注意 z-index 只加在 .show 上:常驻态 .pin 是待在控制栏里的,抬层反而会挡住控制栏按钮。 */ html.bbhr-thick .bpx-player-pbp.show { bottom: calc(100% + ${PROGRESS_BAR_H - 10}px) !important; z-index: 1 !important; } html.bbhr-thick .bpx-player-pbp svg { height: 100% !important; } /* ---- 7. 操作栏 → 播放器左侧的竖排栏 ---- 实现思路:**节点不搬家**,只给 #arc_toolbar_report 加 position:fixed, 再用 JS 每帧写 left/top 把它贴到播放器左边的空白处。 为什么不直接把整条搬到别的位置上去(appendChild): B站是 Vue 渲染的,节点一旦被挪到别的父节点下,它那套「谁是谁的孩子」的 记账就对不上了 —— 点赞/投币会更新计数、触发重渲染,重渲染时按旧路径去找 节点会找不到甚至报错。原地改样式就没这个问题:事件、数据绑定、四个弹层 (分享 / 更多 / 记笔记 / 举报)全部原样保留。 为什么用 fixed 是安全的:已实测这条祖先链上(.left-container → #mirror-vdcon → #app → body → html)没有任何 transform / filter / perspective / contain, 不会把 fixed 的定位基准劫持成某个祖先,就是老老实实相对视口。 ⚠ position / left / top 这三条**故意不加 !important**: · position 靠选择器权重就能压过 B站的 .video-toolbar-container · left / top 要让位给 JS 写的内联样式(内联样式赢不了带 !important 的规则), 下面这两个值只是脚本还没跑到时的兜底。 · 兜底的 left 也必须**让开设置按钮那一列**,否则脚本还没跑起来的那 一瞬间,竖排栏会正正压在按钮上(按钮在 left:10、宽 54)。 */ html.bbhr-rail #arc_toolbar_report { position: fixed; left: ${FAB_LEFT + FAB_SIZE + FAB_GAP}px; top: 160px; z-index: 2000; /* 压过播放器,但远低于设置面板(2147483000) */ display: flex !important; flex-direction: column !important; align-items: center !important; justify-content: flex-start !important; gap: 0 !important; width: ${RAIL_W}px !important; height: auto !important; min-height: 0 !important; margin: 0 !important; padding: 5px 0 !important; border: 0 !important; /* B站原生带一条 1px 底边(下方那条分隔线),竖排后要去掉 */ border-radius: 16px !important; background: rgba(255, 255, 255, .93) !important; box-shadow: 0 6px 22px rgba(0, 0, 0, .16), 0 0 0 1px rgba(0, 0, 0, .07) !important; -webkit-backdrop-filter: blur(12px) saturate(1.25); backdrop-filter: blur(12px) saturate(1.25); box-sizing: border-box !important; /* 这里**不能**加 max-height / overflow: 分享、更多、记笔记这三处的弹层都是这条栏的后代,一旦栏上出现 overflow 就会被裁掉,弹层直接点不到。所以宁可让它在极矮的窗口里溢出一点。 */ } /* 左右两组各变一列,中间的竖线当分隔 */ html.bbhr-rail #arc_toolbar_report > .video-toolbar-left, html.bbhr-rail #arc_toolbar_report > .video-toolbar-right, html.bbhr-rail #arc_toolbar_report .video-toolbar-left-main { display: flex !important; flex-direction: column !important; align-items: center !important; justify-content: flex-start !important; width: 100% !important; height: auto !important; gap: 0 !important; } html.bbhr-rail #arc_toolbar_report > .video-toolbar-left { padding-bottom: 4px !important; } html.bbhr-rail #arc_toolbar_report > .video-toolbar-right { margin-top: 4px !important; padding-top: 4px !important; border-top: 1px solid rgba(0, 0, 0, .08) !important; } /* 每一个按钮:从「图标在左、数字在右」翻成「图标在上、数字在下」。 ⚠ 选择器一律走「直接子代」到底(#arc_toolbar_report > … > …), 因为弹层里的「用手机看 / 稍后再看」这些条目**也带 .video-toolbar-right-item / .video-toolbar-item-text 类**,用后代选择器会把弹层里的一起改烂。 */ html.bbhr-rail #arc_toolbar_report .video-toolbar-left-main > .toolbar-left-item-wrap, html.bbhr-rail #arc_toolbar_report .video-toolbar-left-main > .toolbar-left-item-wrap > .video-toolbar-left-item, html.bbhr-rail #arc_toolbar_report > .video-toolbar-right > .video-toolbar-right-item:not(.dropdown-item) { display: flex !important; flex-direction: column !important; align-items: center !important; justify-content: center !important; width: 100% !important; height: auto !important; min-height: 0 !important; margin: 0 !important; text-align: center !important; border-radius: 10px !important; box-sizing: border-box !important; transition: background .14s ease; } /* 外层只做容器,内层才是可点的那块,hover 底色画在内层上 */ html.bbhr-rail #arc_toolbar_report .video-toolbar-left-main > .toolbar-left-item-wrap { padding: 0 !important; } html.bbhr-rail #arc_toolbar_report .video-toolbar-left-main > .toolbar-left-item-wrap > .video-toolbar-left-item { padding: 4px 0 !important; } html.bbhr-rail #arc_toolbar_report > .video-toolbar-right > .video-toolbar-right-item:not(.dropdown-item) { padding: 6px 0 !important; } html.bbhr-rail #arc_toolbar_report .video-toolbar-left-main > .toolbar-left-item-wrap:hover > .video-toolbar-left-item, html.bbhr-rail #arc_toolbar_report > .video-toolbar-right > .video-toolbar-right-item:not(.dropdown-item):hover { background: rgba(0, 174, 236, .1) !important; } /* 分享那一条的结构特殊:外面还套了 .video-share-wrap > span > .video-share, 要一层层都掰成竖列,图标和计数才会上下排。 */ html.bbhr-rail #arc_toolbar_report .video-share-wrap, html.bbhr-rail #arc_toolbar_report .video-share-wrap > span, html.bbhr-rail #arc_toolbar_report .video-share { display: flex !important; flex-direction: column !important; align-items: center !important; justify-content: center !important; width: 100% !important; gap: 1px !important; } /* 更多(⋮)外面套了一层 .video-tool-more-reference */ html.bbhr-rail #arc_toolbar_report > .video-toolbar-right > .video-tool-more > .video-tool-more-reference { display: flex !important; flex-direction: column !important; align-items: center !important; justify-content: center !important; gap: 1px !important; } /* 图标统一成 20px(原生 24 / 28 / 36 三种尺寸混着,竖排后要一样大才整齐) */ html.bbhr-rail #arc_toolbar_report .video-toolbar-item-icon { width: 20px !important; height: 20px !important; margin: 0 !important; flex: 0 0 auto !important; } /* 数字 / 计数:缩小一号,居中,不换行。 **只改字号不改颜色** —— 已点赞/已投币的状态色是 B站按状态给的, 覆盖掉就不显示「已选」状态了。 */ html.bbhr-rail #arc_toolbar_report .video-toolbar-item-text, html.bbhr-rail #arc_toolbar_report .video-share-info, html.bbhr-rail #arc_toolbar_report .video-share-info-text { font-size: 11px !important; line-height: 1.15 !important; margin: 0 !important; padding: 0 !important; white-space: nowrap !important; } /* 分享那个计数(div.video-share-info)B站写死了 height:28px + overflow:hidden, 而且里层的文字是脱流定位的 —— 横排时正好和图标左右齐平, 竖排叠成「图标在上、数字在下」之后,容器高度是 0、数字却飘在外面, 于是这一格的底部比别的按钮少十几像素,数字离分隔线特别近。 这里把这一对拉回正常文档流,四个按钮的高度就一致了。 */ html.bbhr-rail #arc_toolbar_report .video-share-info, html.bbhr-rail #arc_toolbar_report .video-share-info-text { position: static !important; display: block !important; height: auto !important; min-height: 0 !important; overflow: visible !important; } /* 竖排之后「稿件举报 / 记笔记」只留图标:文字藏掉(56px 宽也塞不下)。 标题属性 B站原生就带,鼠标停上去仍有提示。 ⚠ 这里必须一路写死「直接子代」: · 用后代选择器的话,⋮更多 的弹层会一起中招 —— 探针实测过, 「稿件举报 / 用手机看 / 稍后再看」在没登录时是被 B站塞进 .video-tool-more-dropdown 里当菜单项渲染的(class 带 dropdown-item), 它们同样挂 .video-toolbar-item-text,后代选择器会把菜单文字全藏掉。 */ html.bbhr-rail #arc_toolbar_report > .video-toolbar-right > .video-toolbar-right-item:not(.dropdown-item) > .video-toolbar-item-text, html.bbhr-rail #arc_toolbar_report > .video-toolbar-right > .video-note > .video-note-inner > .video-toolbar-item-text { display: none !important; } /* B站会按「登录态 / 浏览器能力」决定「稿件举报 / 记笔记」要不要真的长出来: 不长出来的时候会留一个空壳(标签还在、里面图标是 0 宽)。 横排时看不出来,竖排成一条之后就是一块很显眼的空白,所以整条收掉。 标记(data-bbhr-empty)由 JS 打 —— 只有真的量出图标是 0 宽才算数。 */ html.bbhr-rail #arc_toolbar_report > .video-toolbar-left > .video-toolbar-left-main > .toolbar-left-item-wrap[data-bbhr-empty="1"], html.bbhr-rail #arc_toolbar_report > .video-toolbar-right > .video-toolbar-right-item[data-bbhr-empty="1"]:not(.dropdown-item) { display: none !important; } /* ---- 8. 首页首屏的大轮播 ---- 首页推荐流的容器是 div.container.is-version8 —— display:grid、 5 列 × 245px + 20px 间距。那个大轮播 .recommended-swipe 是该容器的 第一个 grid item,横跨 2 列 × 2 行(所以量出来是 510 × 446)。 因此这里**只要把它 display:none 就够了** —— 剩下的卡片由 CSS Grid 自己往前补位:实测首屏从「左边一大块轮播 + 右边 3 列」直接变成 整齐的 5 列,不留任何空档,不需要任何额外补偿。 (一度想写 flex:1 让兄弟撑满 —— 探针实测父级是 grid,那套完全不起作用。) .recommended-swipe 这个类只在首页推荐流里有,不用再按路径判断。 */ html.bbhr-nobanner .recommended-swipe { display: none !important; } ${iconCss} ${trailCss} ${followCss} ${upCardCss} `; /* ================= 状态 ================= */ let on = localStorage.getItem(KEY_ON) !== '0'; let grow = localStorage.getItem(KEY_GROW) !== '0'; let thick = localStorage.getItem(KEY_THICK) !== '0'; let rail = localStorage.getItem(KEY_RAIL) !== '0'; /* 首页首屏那个大轮播("bilibili公开课…"那张)。 默认开着 —— 用户要的就是"去掉它"。 */ let noBanner = localStorage.getItem(KEY_BANNER) !== '0'; /* UP主悬浮卡:悬停播放器左上角「关注」按钮 → 浮出右侧栏的 UP主卡片。 默认开着。 */ let upCard = localStorage.getItem(KEY_UPCARD) !== '0'; /* 悬浮按钮 + 设置面板的引用。 ⚠ 这个声明必须放在 apply() 第一次被调用之前 —— apply() 结尾要同步面板状态, 声明写在后面会踩 let 的暂时性死区(ReferenceError: Cannot access 'ui' before initialization)。 */ let ui = null; /* ---- 竖排操作栏的运行时状态 ---- ⚠ 同样必须在 apply() 第一次被调用之前声明 —— apply() 里会调 ensureRailLoop()。 具体的定位函数(placeRail 等)写在文件后半段,函数声明会提升,不受影响。 */ let railStyled = false; // 有没有往 B站的操作栏节点上写过内联 left/top/visibility let railRaf = 0; // rAF 句柄 let railSeenL = -1; // 上一帧写过的 left(-1 = 还没写过) let railSeenT = -1; // 上一帧写过的 top let railSeenV = ''; // 上一帧的可见性('' / 'hidden') let railTidyNext = 0; // 「收拾空壳按钮」的节流时间戳(每帧都做没必要) let railTimer = 0; // 定时器句柄(rAF 的保活兜底,见下面 ensureRailLoop) let railLastError = ''; // 循环里最后一次异常,供 __bbhrRail() 自检 function apply() { const root = document.documentElement; if (!root) return false; // document-start 时可能还没 html 节点 root.classList.toggle('bbhr-on', on); root.classList.toggle('bbhr-grow', on && grow); root.classList.toggle('bbhr-thick', thick); root.classList.toggle('bbhr-rail', rail); root.classList.toggle('bbhr-nobanner', noBanner); root.classList.toggle('bbhr-upcard', upCard); let style = document.getElementById(STYLE_ID); if (!style) { style = document.createElement('style'); style.id = STYLE_ID; (document.head || root).appendChild(style); } if (style.textContent !== css) style.textContent = css; /* 每个功能各自兜住异常:任何一个环节出错都只损失它自己, 绝不允许把 apply()(以及调用它的顶层代码)整个炸掉 —— v1.15.0~v1.16.1 的真机三回归,就是一次 TDZ 异常团灭了整个脚本。 */ try { syncUI(); } catch (e) { console.warn('[bbhr] syncUI:', e); } // 快捷键改出来的状态也要反映到面板上(ui 还没建出来时它会自己跳过) try { if (RAINBOW) syncRainbow(); } catch (e) { console.warn('[bbhr] syncRainbow:', e); } // 加粗刚被打开时立刻补一次,不用等下一个 120ms 轮询 try { ensureRailLoop(); } catch (e) { console.warn('[bbhr] ensureRailLoop:', e); } // 竖排栏:开了就起 rAF 循环,关了就把上一帧留的痕迹擦干净 try { ensureTrail(); } catch (e) { console.warn('[bbhr] ensureTrail:', e); } // 进度条圆点的彩虹拖尾:SPA 重建后靠每秒守护自动补回 try { ensureUpCard(); } catch (e) { console.warn('[bbhr] ensureUpCard:', e); } // UP主悬浮卡:绑「关注」按钮的进出事件 + 每帧摆位 return true; } /* ⚠ 首次 apply() 的同步调用挪到了脚本最末尾(文件尾注释): 顶层在这里直接调它的话,函数体里用到的 const/let(RAINBOW_TARGETS、 upCard / upCardBound 等)当时还没初始化、正在「暂时性死区」—— 油猴在 已存在的 document-start 注入时,apply() 一进来就 ReferenceError,顶层代码从这一行起全部失效(v1.15.0~v1.16.1 的 三个真机回归共同根因)。放到末尾,所有声明都已就绪,同步调用才安全。 */ // 守护:B站部分页面会用 document.write 重建 head,样式节点会丢 setInterval(apply, 1000); document.addEventListener('DOMContentLoaded', apply); /* ================= 彩虹进度条的补偿 ================= */ /* 为什么需要这段: 「已播放」那段(.bpx-player-progress-schedule-current)B站是把它从整条宽度 压到实际播放比例来显示的(transform: scaleX(f),或直接改 width,视版本而定)。 而 background 会跟着元素一起被缩放 —— 渐变要是直接贴上去,整条彩虹会被 压进已播放的那一小段里,越往后压得越狠,看着像在变焦,而不是彩虹随播放揭开。 补偿办法:把 background-size 设成 (100/f)%。 也就是在元素自己的坐标系里先把渐变拉宽 1/f 倍,被缩放之后在屏幕上正好还原成 「色谱钉在整条进度条上、随播放从左往右揭开」。 f 直接拿两个元素的实际渲染宽度相除(已播放段 / 整条轨道), 这样不管 B站用的是 transform 还是 width,算出来都对,不用去猜它的实现。 ⚠ 必须遍历**所有**画面上的进度条,不能只取第一个: 同一个页面上同时存在好几条 —— 主控制条一条、`.bpx-player-shadow-progress-*` 里的窄影条一条、以及滚到评论区后右下角小窗(tiny view)里的 `.bpx-player-mini-progress-tempo` 一条。它们用的是**互不相同的 class**, 但都靠「元素本身是整条宽度、用 transform:scaleX(f) 缩到播放比例」来显示。 以前用 document.querySelector 只改到第一条,结果小窗那条拿不到补偿: 渐变虽然贴上了,整条彩虹却被压进那一小段已播放宽度里, 颜色看着就和主窗口对不上。改成把所有已知的「已播放段」一起处理。 小窗那条的优点:它的 width 就是整条 320px,rect.width 已经被 scaleX 缩过, 所以 done/full 直接相除就得到 f,和主条同一套算法,无需分支。 */ const RAINBOW_TARGETS = [ '.bpx-player-progress-schedule-current', // 主控制条 '.bpx-player-mini-progress-tempo', // 小窗(tiny view) ]; function syncRainbow() { const curs = document.querySelectorAll(RAINBOW_TARGETS.join(',')); /* 加粗关掉时,这段补偿逻辑必须彻底让路: 否则会一直往 B站自己的节点上写内联 background-size(虽然此时没有渐变、 肉眼看不出区别),但「关掉」就该恢复成原生,不留任何残留样式。 */ if (!thick) { curs.forEach((cur) => { if (cur.__bbhrSize) { cur.__bbhrSize = ''; cur.style.backgroundSize = ''; } }); return; } curs.forEach((cur) => { /* 「整条」的宽度怎么取: · 小窗那条(tempo):它自己的 width 就是整条(320px), 而 rect.width 已被 scaleX 缩过 → 分子分母都是它自己,取父元素即可。 · 主条:轨道在 .bpx-player-progress-schedule-wrap 上。 统一先试 wrap、拿不到就退回 parentElement —— 两种结构都能覆盖。 */ const wrap = cur.closest('.bpx-player-progress-schedule-wrap') || cur.closest('.bpx-player-mini-progress') || cur.parentElement; if (!wrap) return; // 两个宽度一次性读完再写,避免中间插入样式、触发多余的重排 const full = wrap.getBoundingClientRect().width; const done = cur.getBoundingClientRect().width; /* 这条条当前不可见(宽高归零 —— 例如小窗还没浮出来、或正处在切换过程中), 就别写样式:留着上一次的补偿值即可,等它真正有尺寸了下一轮自然会算对。 顺带把「没尺寸」这条排除掉,避免算出 f 的假值。 */ if (!(full > 1)) return; let f = done / full; if (f > 1) f = 1; // 下限:刚开始播放时 f 接近 0,100/f 会大得离谱(f=0 直接 Infinity) if (f < 0.005) f = 0.005; const size = `${(100 / f).toFixed(3)}% 100%`; // 只在真的变化时才写;否则每 120ms 写一次样式会白白触发重算/重绘 if (cur.__bbhrSize !== size) { cur.__bbhrSize = size; cur.style.backgroundSize = size; } }); } if (RAINBOW) { setInterval(syncRainbow, 120); document.addEventListener('DOMContentLoaded', syncRainbow); } /* ================= 进度条圆点的彩虹拖尾 ================= */ /* 把一段彩虹烟柱挂到进度条圆点(小摩托)身后。 关键约束: · B站用 .bpx-state-active 的 transform:scale 控制图标「移上去才显出来」, 动画绝对不能动图标本身的 transform → 用子元素承载。 · .bpx-player-progress-thumb-icon > * { display:none } 会干掉图标内所有子节点 → 所以我们把拖尾插到「图标的父节点」上(那个随播放移动的圆点容器)。 · 用 host.__bbhrTrail 标记防重复插入;apply() 每秒守护一次,SPA 重建后自动补回。 */ function ensureTrail() { if (PROGRESS_ICON_SIZE <= 0) return; const icon = document.querySelector('.bpx-player-progress-thumb-icon'); if (!icon || !icon.parentElement) return; const host = icon.parentElement; if (host.__bbhrTrail) return; host.__bbhrTrail = true; const t = document.createElement('div'); t.className = 'bbhr-trail'; for (let i = 0; i < 8; i++) t.appendChild(document.createElement('i')); host.insertBefore(t, icon); // 插在图标前 → 同层先画,垫在图标底下,不会盖住小摩托 // 同步一次播放态(视频可能开局就暂停) const v = document.querySelector('video'); if (v) document.documentElement.classList.toggle('bbhr-paused', v.paused); } /* 让「小摩托」和进度条同生共死。 判据(三条任一成立 → 认为进度条/圆点已收起): 1. 找不到圆点图标(SPA 切页 / 播放器还没起来); 2. 图标自身 computed visibility 不是 visible —— B站隐藏控制栏时 .bpx-player-control-top 的 visibility:hidden 会级联下来; 3. 图标的可视矩形是 0 —— B站把圆点藏起来时用的是 transform:scale(0), 此时元素还在、visibility 也可能是 visible(transition 期间尤甚), 但宽高会被缩成 0,这是最准的一条。 只要命中就把 html.bbhr-dot-off 加上,CSS 那边据此同时收掉图标和彩虹烟。 用 rAF 而不是 setInterval:跟着渲染节奏走,页面看不见时不空转。 ⚠️ 只读不写 B站节点的样式,纯加类,不会和它自己的显隐逻辑打架。 */ let dotWasOff = null; function syncDotVis() { const root = document.documentElement; if (!root) return; const icon = document.querySelector('.bpx-player-progress-thumb-icon'); let off = false; if (!icon) { off = true; } else { const cs = getComputedStyle(icon); if (cs.visibility !== 'visible' || cs.display === 'none') { off = true; } else { const r = icon.getBoundingClientRect(); // scale(0) 之后宽高会归零;留 0.5px 容差,避免浮点误差误判 if (r.width < 0.5 || r.height < 0.5) off = true; } } if (off !== dotWasOff) { dotWasOff = off; root.classList.toggle('bbhr-dot-off', off); } } /* rAF 常驻循环:只在「进度条加粗」开着时才需要判断,关掉时把类摘掉即可。 每帧只读一点布局信息,开销可忽略。 */ (function loopDotVis() { if (thick) syncDotVis(); else if (dotWasOff) { dotWasOff = false; document.documentElement.classList.remove('bbhr-dot-off'); } requestAnimationFrame(loopDotVis); })(); /* ================= v1.16.4 鼠标在播放器内 → 小摩托常显 ================= 上面 CSS 里那条 scale(1) 需要一个「鼠标现在在不在播放器里」的信号,这里负责产生。 为什么用 document 上的 capture 委托:B站是 SPA,播放器节点随时重建, 逐个 addEventListener 会在重建后失效;委托到 document 就永远有效。 为什么用 pointerover:鼠标在播放器内部移动时,每跨进一个子元素都会触发, 天然可以用来持续刷新状态(顺带覆盖触摸 / 手写笔)。 右下角那个 mini 小窗不算「播放器」:小窗的进度条只有几十像素宽, 26px 的大摩托糊上去会把进度条整个盖住,所以命中 mini 容器时一律不挂类, 让它保持 B站 自己的「碰到进度条才出来」。 整个模块自带作用域、只用 DOM API,不引用任何外部变量 —— 避免在脚本顶层 额外制造跨声明顺序的依赖(v1.16.2 的 TDZ 团灭就是这么来的)。 */ (function bindPlayerHover() { const HOST = '#bilibili-player, .bpx-player-container, .bilibili-player-area'; const setPlayerHover = (on) => { document.documentElement.classList.toggle('bbhr-hover-player', !!on); }; document.addEventListener('pointerover', (e) => { const t = e.target; const host = t && t.closest ? t.closest(HOST) : null; if (!host) { setPlayerHover(false); return; } /* ⚠️ 认 mini 一定要从 target 往上找,写 host.closest 是错的: 小窗的真实结构是 .bpx-player-container[data-screen=mini] > primary-area > video-area > .bpx-player-mini-warp mini-warp 长在 container 的**里面**,所以从 container 往上 closest 永远够不到它(v1.16.4 第一版就栽在这里:鼠标一进小窗摩托也跟着显形)。 双保险:① 从 target 往上找带 mini 的祖先;② 直接看容器的 data-screen。 */ const inMini = (t.closest && t.closest('[class*="mini"]')) || host.getAttribute('data-screen') === 'mini'; setPlayerHover(!inMini); }, true); // 鼠标离开整个文档 / 窗口失焦:兜底摘类,不然摩托会卡在屏幕上 document.addEventListener('mouseleave', () => setPlayerHover(false)); window.addEventListener('blur', () => setPlayerHover(false)); })(); /* 播放 / 暂停 → 拖尾冻结。 play/pause 不冒泡,用 capture 在 document 上抓视频元素的事件。 */ document.addEventListener('play', () => document.documentElement.classList.remove('bbhr-paused'), true); document.addEventListener('pause', () => document.documentElement.classList.add('bbhr-paused'), true); /* ================= UP主悬浮卡 ================= 悬停播放器左上角的「+关注」小按钮 → 右侧栏那张 UP主卡片浮到按钮下方; 鼠标离开(按钮和卡片都没碰到)→ 收起。 三个关键点: 1. **触发方与目标无共同祖先**,所以用 JS 监听进出,不靠 CSS :hover。 2. 卡片在 .right-container 里,而那个容器默认是 display:none(见 css 第 1 节)。 display:none 会让卡片尺寸归零、根本浮不出来 —— 所以悬停期间 JS 会把根类 html.bbhr-upcard-open 加上,CSS 借这个类把右栏临时切成「离屏态」 (抽离文档流 + 挪出视口),卡片靠 position:fixed 回到视口内正常浮现; 鼠标移开、类摘掉,右栏回到 display:none。这样「右栏整体隐藏 + 卡片单独浮出」可以共存,且离屏那套高 z-index 规则只在悬停的一瞬间存在, 不会常年压在页面上(v1.15.0 把它做成常驻,压出过一批回归)。 3. 收起判定用「按钮 hover ∪ 卡片 hover」:鼠标从按钮移到卡片上时 中间会经过一段缝隙,只判按钮会误收;所以两边都挂着, 再给一个很短的延时(grace)兜住这段过渡。 */ const UPCARD_GRACE = 160; // 按钮→卡片之间留的宽限(ms) let upCardBound = false; // 事件只绑一次 let upCardTimer = 0; // 收起延时句柄 let upCardHitBtn = false; // 当前是否悬停在按钮上 let upCardHitCard = false; // 当前是否悬停在卡片上 let upCardShown = false; // 当前是否已展开(避免每帧重复写类) function upCardEl() { return document.querySelector('.up-info-container'); } function upCardTrigger() { return document.querySelector('.bpx-player-top-left-follow') || document.querySelector('.bpx-player-follow'); } /* 展开 / 收起。只写根类,位置由下面的每帧循环负责。 */ function setUpCardOpen(open) { if (open === upCardShown) return; upCardShown = open; document.documentElement.classList.toggle('bbhr-upcard-open', open); } /* 每次「应该保持展开」的信号到达就取消待执行的收起; 信号停了 GRACE 毫秒后才真正收起。 */ function holdUpCard() { if (upCardTimer) { clearTimeout(upCardTimer); upCardTimer = 0; } if (!upCard) return; setUpCardOpen(true); } function releaseUpCard() { if (upCardTimer) clearTimeout(upCardTimer); upCardTimer = setTimeout(() => { upCardTimer = 0; // 宽限期内若又贴回了任一侧,就不收 if (!upCardHitBtn && !upCardHitCard) setUpCardOpen(false); }, UPCARD_GRACE); } /* 每帧把卡片摆到「关注」按钮正下方。位置用按钮的实时矩形算, 这样播放器缩放、窗口改变、页面滚动都自动跟随,不用额外监听 resize/scroll。 */ function placeUpCard() { if (!upCard || !upCardShown) return; const btn = upCardTrigger(); const card = upCardEl(); if (!btn || !card) return; const b = btn.getBoundingClientRect(); if (b.width < 1 || b.height < 1) return; // 按钮被 B站 收起来了,先不动 // 期望位置:按钮左缘对齐,贴按钮下方 6px let left = b.left; let top = b.bottom + 6; // 夹进视口:右侧不越界(留 12px),下方不够就翻到按钮上方 const cw = card.offsetWidth || 350; const ch = card.offsetHeight || 104; const vw = window.innerWidth, vh = window.innerHeight; if (left + cw > vw - 12) left = Math.max(12, vw - 12 - cw); if (top + ch > vh - 12) top = Math.max(12, b.top - 6 - ch); // 只在真的变了才写(每帧写样式会白触发重绘) const L = Math.round(left), T = Math.round(top); if (card.__bbhrL !== L || card.__bbhrT !== T) { card.__bbhrL = L; card.__bbhrT = T; card.style.setProperty('left', L + 'px', 'important'); card.style.setProperty('top', T + 'px', 'important'); } } /* 关掉开关 / 页面切走时,把卡片恢复原状(清掉内联定位,交给 CSS 收起态)。 */ function resetUpCard() { const card = upCardEl(); if (card) { card.style.removeProperty('left'); card.style.removeProperty('top'); card.__bbhrL = card.__bbhrT = null; } upCardHitBtn = upCardHitCard = false; if (upCardTimer) { clearTimeout(upCardTimer); upCardTimer = 0; } setUpCardOpen(false); upCardShown = false; document.documentElement.classList.remove('bbhr-upcard-open'); } function ensureUpCard() { if (!upCard) { if (upCardShown) resetUpCard(); return; } if (upCardBound) return; const btn = upCardTrigger(); if (!btn) return; // 播放器还没起来,下一轮守护再来 upCardBound = true; /* 用 pointerenter / pointerleave 而不是 mouseover/mouseout: 后者会在子元素之间来回冒泡,鼠标在按钮内部移动也会触发一堆进出。 */ btn.addEventListener('pointerenter', () => { upCardHitBtn = true; holdUpCard(); }, { passive: true }); btn.addEventListener('pointerleave', () => { upCardHitBtn = false; releaseUpCard(); }, { passive: true }); /* 卡片自己也要能「续命」——鼠标从按钮移到卡片上时不能收。 卡片是后插进 DOM 的,用事件委托挂在 document 上更稳。 */ if (!document.__bbhrUpCardDelegated) { document.__bbhrUpCardDelegated = true; document.addEventListener('pointerover', (e) => { const card = e.target && e.target.closest && e.target.closest('.up-info-container'); if (card) { upCardHitCard = true; holdUpCard(); } }, true); document.addEventListener('pointerout', (e) => { const card = e.target && e.target.closest && e.target.closest('.up-info-container'); if (card) { upCardHitCard = false; releaseUpCard(); } }, true); // 页面失焦 / 滚走时收掉,避免残留 window.addEventListener('blur', () => { upCardHitBtn = upCardHitCard = false; releaseUpCard(); }); } } /* 每帧摆位循环(与 dot 那条共用同样的「常驻 rAF」思路)。 只在展开时才真正干活,收起时直接跳过,开销为 0。 */ (function loopUpCard() { placeUpCard(); requestAnimationFrame(loopUpCard); })(); /* ================= 提示条 ================= */ let toastEl = null; let toastTimer = 0; function toast(text) { if (!document.body) return; if (!toastEl) { toastEl = document.createElement('div'); toastEl.id = 'bbhr-toast'; Object.assign(toastEl.style, { position: 'fixed', left: '50%', bottom: '48px', transform: 'translate(-50%, 0)', padding: '8px 16px', borderRadius: '8px', background: 'rgba(0, 0, 0, .78)', color: '#fff', fontSize: '13px', lineHeight: '1.5', zIndex: '2147483647', pointerEvents: 'none', opacity: '0', transition: 'opacity .2s ease, transform .2s ease', fontFamily: 'system-ui, -apple-system, "Microsoft YaHei", sans-serif', }); document.body.appendChild(toastEl); } toastEl.textContent = text; toastEl.style.opacity = '1'; toastEl.style.transform = 'translate(-50%, 0)'; clearTimeout(toastTimer); toastTimer = setTimeout(() => { toastEl.style.opacity = '0'; toastEl.style.transform = 'translate(-50%, -10px)'; }, 1600); } /* ================= 各个开关的动作(快捷键和面板共用) ================= 抽出来是为了让快捷键和界面按钮走同一条路, 免得以后改一处忘一处、两边行为对不上。 */ function toggleOn() { on = !on; localStorage.setItem(KEY_ON, on ? '1' : '0'); apply(); toast(on ? '已隐藏右侧推荐栏' : '已恢复右侧推荐栏'); } function toggleGrow() { grow = !grow; localStorage.setItem(KEY_GROW, grow ? '1' : '0'); apply(); /* 「放大」那组 CSS 要求 .bbhr-on.bbhr-grow 同时成立,右栏没隐藏时它必然不生效。 这里**不去替用户改另一个开关** —— 三个开关各自独立,点了哪个就只动哪个; 暂不生效只用提示条说清楚,面板上那一行也会变暗。 */ if (grow && !on) toast('已打开播放器放大 · 需同时打开「隐藏右侧推荐栏」才生效'); else toast(grow ? '播放器:自适应放大' : '播放器:保持原尺寸'); } function toggleThick() { thick = !thick; localStorage.setItem(KEY_THICK, thick ? '1' : '0'); apply(); const alphaTip = BAR_ALPHA >= 1 ? '实心' : `不透明度 ${Math.round(BAR_ALPHA * 100)}%`; toast(thick ? `进度条:${PROGRESS_BAR_H}px · ${alphaTip}` : '进度条:恢复 B站原生'); } function toggleRail() { rail = !rail; localStorage.setItem(KEY_RAIL, rail ? '1' : '0'); // 关掉时先把上一帧写给 B站节点的内联 left/top/visibility 擦干净, // 否则操作栏会留着 fixed 定位、永远回不到播放器下方。 if (!rail) resetRail(); apply(); toast(rail ? '操作栏:已移到播放器左侧竖排' : '操作栏:已还回播放器下方'); } function toggleBanner() { noBanner = !noBanner; localStorage.setItem(KEY_BANNER, noBanner ? '1' : '0'); apply(); toast(noBanner ? '首页轮播:已隐藏(视频卡自动补满整行)' : '首页轮播:已恢复'); } function toggleUpCard() { upCard = !upCard; localStorage.setItem(KEY_UPCARD, upCard ? '1' : '0'); if (!upCard) resetUpCard(); // 关掉时把内联定位和展开类都清干净 apply(); toast(upCard ? 'UP主悬浮卡:鼠标移到播放器左上角「关注」上即浮出' : 'UP主悬浮卡:已关闭'); } /* ================= 快捷键 ================= */ window.addEventListener('keydown', (e) => { // 只认 Shift + 单键,避开 B站自身的单键快捷键 if (!e.shiftKey || e.ctrlKey || e.altKey || e.metaKey) return; const t = e.target; if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return; const key = (e.key || '').toLowerCase(); const act = key === 'h' ? toggleOn : key === 'g' ? toggleGrow : key === 'p' ? toggleThick : key === 'r' ? toggleRail : key === 'b' ? toggleBanner : key === 'u' ? toggleUpCard : null; if (!act) return; e.preventDefault(); e.stopPropagation(); act(); }, true); /* ================= 悬浮按钮 + 设置面板 ================= 放在页面左上角、bilibili 顶栏 logo 正下方(v1.16.9 起,原先在左侧垂直居中)。 整块 UI 走 Shadow DOM —— B站的全局样式非常霸道(大量 `div { }` / 高特异性选择器), 用普通节点挂进页面,按钮的圆角、字号、行高随时可能被覆盖或继承污染。 挂进 shadow root 之后,两边的样式天然互相隔离,也不用写 !important 军备竞赛。 */ const UI_ID = 'bbhr-ui'; const UI_Z = '2147483000'; // 比 B站播放器高,但比提示条(2147483647)低,提示条要压在最上面 /* ---- 鼠标悬浮展开的两个时间参数 ---- HOVER_OPEN_MS 鼠标压上来后等这么久才展开。给一点延迟,是为了 光标只是「路过」按钮上方时不要闪一下面板。 HOVER_CLOSE_MS 鼠标离开后的宽限期。从按钮横着滑到面板要跨过中间那 10px 间隔, 这一下会短暂地「离开」再「进入」(实测约 20ms), 260ms 的宽限给足了余量;手抖划出边界又回来也不会误关。 */ const HOVER_OPEN_MS = 80; const HOVER_CLOSE_MS = 260; /* 按钮和面板之间的间隔(px)。UI_CSS 里 .wrap 的 gap 和 .fab::after 那道「桥」 共用这一个值,改一处就够。 */ const UI_GAP = 10; let hoverTimer = 0; let hoverMode = false; // 是否是「鼠标悬浮」这套交互(触摸设备退回点击) /* 当前这次展开是不是悬浮触发的。 兜底判断需要的是「这次是鼠标悬浮开的」,光看 hoverMode 不够 —— 鼠标挪过之后改成键盘打开面板(hoverMode 仍是 true), 兜底会看到指针在远处,把键盘开的这个面板顺手关掉。 */ let hoverOpenPanel = false; function clearHoverTimer() { if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = 0; } } const UI_CSS = ` /* 宿主的落位:页面左上角、bilibili 顶栏 logo 正下方(v1.16.9 起从「左侧垂直居中」挪上来)。 **开了竖排操作栏也在原地不动** —— 竖排栏会自己往右让开这一列(见 placeRail 里的 minLeft)。 位置写死在 CSS(而不是由 JS 内联)是为了让「按钮永远在这儿」只有一个出处; 上面配置里的 FAB_LEFT / FAB_TOP 就是这两个值,改一处等于两处都跟着走。 ⚠ 顶部锚定(top 固定、不再 translateY(-50%))后,配套把下面 .wrap 改成 align-items:flex-start —— 否则设置面板一展开、wrap 被撑高,按钮会被居中而往下跳。 */ :host { position: fixed; left: ${FAB_LEFT}px; top: ${FAB_TOP}px; z-index: ${UI_Z}; /* ⚠ 必须是 none,只给 .fab / .panel 单独开 auto,原因: 宿主是个 flex 容器,盒子里除了按钮还框着一大片**透明空区** (面板占满高度后,按钮上方/下方、面板下方全在里面)。 这里留 auto 的话,鼠标挪进空区仍算「还在 UI 上」,pointerleave 不触发, 面板就再也关不掉 —— 实测从按钮往下挪 120px 面板就是不关。 交给 .fab / .panel 各自负责命中之后,命中区就等于它们的真实轮廓。 */ pointer-events: none; /* ⚠ 这里**故意不加 left/top 的过渡**: 竖排栏是每帧跟着播放器走的,加了过渡会让整条栏在滚动时慢半拍、黏在旧位置上。 */ } /* 显式写死字体 / 颜色 / 行高:shadow root 里虽然不会被外面污染, 但 :host 本身还是会从页面继承字体和颜色,这里一律断开。 */ .wrap { /* ⚠ 顶部锚定:align-items:flex-start 让按钮永远贴在 wrap 顶部。 面板展开把 wrap 撑高时,按钮不会跟着被居中而往下跑(配合 :host 的 top 定值)。 */ display: flex; align-items: flex-start; gap: ${UI_GAP}px; font: 13px/1.5 system-ui, -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; color: #1b1c1f; text-align: left; letter-spacing: normal; /* 只负责布局,不参与命中测试(命中区由 .fab / .panel 自己划)。 原因见 :host 的 pointer-events 注释。 */ pointer-events: none; } .wrap * { box-sizing: border-box; margin: 0; padding: 0; } /* ---------- 悬浮按钮(浅色) ---------- */ .fab { flex: 0 0 auto; width: 54px; height: 54px; display: flex; align-items: center; justify-content: center; padding: 0; border: 1px solid rgba(0, 0, 0, .12); border-radius: 50%; background: rgba(255, 255, 255, .9); box-shadow: 0 4px 14px rgba(0, 0, 0, .18), 0 1px 3px rgba(0, 0, 0, .1); -webkit-backdrop-filter: blur(10px) saturate(1.3); backdrop-filter: blur(10px) saturate(1.3); cursor: pointer; opacity: .94; transition: transform .18s ease, opacity .18s ease, box-shadow .18s ease, background .18s ease; -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent; position: relative; /* 给下面那道 ::after 当定位基准 */ pointer-events: auto; /* 从 :host 的 none 里把自己择出来 */ } /* 按钮 → 面板之间那道 ${UI_GAP}px 的「桥」。 空区不再参与命中之后,横着从按钮滑到面板会「离开再进入」一下; 拿按钮的伪元素把这段补上,命中链就一直是「按钮 → .wrap」,不会闪断。 它不画任何东西,只是把有效区域从按钮右边再往外延 ${UI_GAP}px。 (伪元素上的 hover 算在 .fab 头上,所以鼠标在桥上按钮也会微微放大,正常。) */ .fab::after { content: ''; position: absolute; pointer-events: auto; left: 100%; top: 0; bottom: 0; width: ${UI_GAP}px; } .fab:hover { opacity: 1; transform: scale(1.07); background: #fff; box-shadow: 0 8px 22px rgba(0, 0, 0, .24), 0 1px 3px rgba(0, 0, 0, .12); } .fab:active { transform: scale(.97); } .fab:focus-visible { outline: 2px solid #00aeec; outline-offset: 2px; } /* 竖构图图标(奥特曼)用「定高、宽自适应」,定宽正方形会在左右留一大块空白; max-width 只是兜底,防止以后换成横构图把圆按钮撑变形。 */ .fab img { height: 42px; width: auto; max-width: 46px; object-fit: contain; display: block; pointer-events: none; } .fab.open { box-shadow: 0 0 0 2px rgba(0, 174, 236, .9), 0 8px 22px rgba(0, 0, 0, .2); } /* ---------- 设置面板(浅色) ---------- */ .panel { width: 340px; overflow: hidden; pointer-events: auto; /* 同样从 :host 的 none 里择出来 */ border: 1px solid rgba(0, 0, 0, .08); border-radius: 12px; background: rgba(255, 255, 255, .95); -webkit-backdrop-filter: blur(14px) saturate(1.3); backdrop-filter: blur(14px) saturate(1.3); box-shadow: 0 10px 30px rgba(0, 0, 0, .16), 0 1px 2px rgba(0, 0, 0, .06); animation: bbhr-pop .16s ease; } .panel[hidden] { display: none; } /* 从按钮那一侧展开,方向感更自然 */ @keyframes bbhr-pop { from { opacity: 0; transform: translateX(-6px) scale(.97); } to { opacity: 1; transform: none; } } /* 头部:左标题块 + 右上角关闭按钮 */ .hd { display: flex; align-items: flex-start; gap: 8px; padding: 12px 9px 9px 14px; font-size: 13px; font-weight: 600; color: #17181b; } .hd .hdtxt { flex: 1 1 auto; min-width: 0; } .hd small { display: block; margin-top: 3px; font-size: 11px; font-weight: 400; color: #8a9099; } /* 关闭按钮:淡灰的 ×,hover 才显出来。 用 svg 画而不是文字 ×,跨字体不会歪。 */ .cl { flex: 0 0 auto; width: 22px; height: 22px; display: flex; align-items: center; justify-content: center; padding: 0; border: 0; border-radius: 6px; background: transparent; color: #9aa0a9; cursor: pointer; transition: background .14s ease, color .14s ease; -webkit-tap-highlight-color: transparent; } .cl:hover { background: rgba(0, 0, 0, .07); color: #1b1c1f; } .cl:active { background: rgba(0, 0, 0, .13); } .cl:focus-visible { outline: 2px solid #00aeec; outline-offset: 1px; } .cl svg { display: block; pointer-events: none; } .rows { padding: 2px 6px 6px; } .row { display: flex; align-items: center; gap: 10px; padding: 9px 8px; border-radius: 8px; cursor: pointer; transition: background .14s ease; } .row:hover { background: rgba(0, 0, 0, .05); } .row:focus-visible { outline: 2px solid #00aeec; outline-offset: -2px; } .row.dim { opacity: .42; } .sw { flex: 0 0 auto; position: relative; width: 38px; height: 21px; border-radius: 999px; background: rgba(0, 0, 0, .16); transition: background .18s ease; } .sw::after { content: ''; position: absolute; top: 2px; left: 2px; width: 17px; height: 17px; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, .3); transition: transform .18s cubic-bezier(.4, 1.5, .6, 1); } .row.on .sw { background: linear-gradient(90deg, #00c7be, #007aff); } .row.on .sw::after { transform: translateX(17px); } .tx { flex: 1 1 auto; min-width: 0; } .t { font-size: 13px; color: #1b1c1f; } .d { margin-top: 2px; font-size: 11px; color: #8a9099; } .keys { flex: 0 0 auto; display: flex; align-items: center; gap: 3px; } .keys kbd { font: 10px/1 ui-monospace, Consolas, monospace; padding: 4px 5px; border-radius: 4px; color: #4b5563; background: #f2f3f5; border: 1px solid rgba(0, 0, 0, .1); border-bottom-width: 2px; } .ft { padding: 9px 14px 11px; font-size: 11px; line-height: 1.5; color: #8a9099; border-top: 1px solid rgba(0, 0, 0, .07); } `; const UI_ROWS = [ { k: 'thick', act: toggleThick, name: '进度条加粗', desc: `${PROGRESS_BAR_H}px 半透明彩虹条 + 自定义图标`, key: 'P' }, { k: 'rail', act: toggleRail, name: '操作栏竖排到左侧', desc: '赞/币/藏/享 + 举报/记笔记/更多 竖排在播放器左边', key: 'R' }, { k: 'nobanner', act: toggleBanner, name: '隐藏首页轮播', desc: '首屏那个大轮播去掉,右边视频卡自动补满整行', key: 'B' }, { k: 'on', act: toggleOn, name: '隐藏右侧推荐栏', desc: '收起推荐视频 / UP主卡片 / 广告', key: 'H' }, { k: 'grow', act: toggleGrow, name: '播放器自适应放大', desc: '按 16:9 占满一屏(需上一项打开)', key: 'G' }, { k: 'upcard', act: toggleUpCard, name: 'UP主悬浮卡', desc: '鼠标移到播放器左上角「关注」上,浮出 UP主卡片;移开即收', key: 'U' }, ]; function syncUI() { if (!ui) return; // 每个开关各自显示自己存的状态,互不代填: // 「放大」在右栏没隐藏时不生效,但那也只是「暂不生效」,不是「被关掉了」, // 所以开关照原样亮着,只用 dim 变暗提示。 ui.rows.on.classList.toggle('on', on); ui.rows.grow.classList.toggle('on', grow); ui.rows.thick.classList.toggle('on', thick); ui.rows.rail.classList.toggle('on', rail); ui.rows.nobanner.classList.toggle('on', noBanner); ui.rows.upcard.classList.toggle('on', upCard); ui.rows.grow.classList.toggle('dim', !on); for (const def of UI_ROWS) { ui.rows[def.k].setAttribute('aria-checked', String(ui.rows[def.k].classList.contains('on'))); } } /* byHover 只有悬浮那条路会传 true,其余(点击 / 键盘 / 触摸)都是 undefined, 兜底的自动关闭据此判断「这次该不该跟着鼠标走」。 */ function setPanelOpen(open, byHover) { if (!ui) return; if (open) hoverOpenPanel = byHover === true; ui.panel.hidden = !open; ui.btn.classList.toggle('open', open); } function buildUI() { if (ui || !document.body) return; // document-start 时 body 还没出来 const host = document.createElement('div'); host.id = UI_ID; // 落位 / 层级 / pointer-events 全部交给 UI_CSS 里的 :host,按钮位置恒定不动。 // ⚠ 别在这里写内联 left/top —— 内联会盖住 :host 的落位,而且 resetRail() // 清空时会连默认值一起清掉,按钮就飘了。 const root = host.attachShadow({ mode: 'open' }); const style = document.createElement('style'); style.textContent = UI_CSS; root.appendChild(style); const wrap = document.createElement('div'); wrap.className = 'wrap'; root.appendChild(wrap); // ---- 悬浮按钮:图标用 FAB_ICON(Q版奥特曼);万一被清空了再退回进度条那张 ---- const btn = document.createElement('div'); btn.className = 'fab'; btn.setAttribute('role', 'button'); btn.setAttribute('tabindex', '0'); btn.setAttribute('aria-label', 'B站播放页增强设置'); btn.title = 'B站播放页增强 · 鼠标移上来打开设置'; const fabIcon = FAB_ICON || PROGRESS_ICON; if (fabIcon) { const img = document.createElement('img'); img.src = fabIcon; img.alt = ''; btn.appendChild(img); } else { btn.textContent = 'B'; // 两张图都被清掉时的兜底 } wrap.appendChild(btn); // ---- 面板 ---- const panel = document.createElement('div'); panel.className = 'panel'; panel.hidden = true; panel.setAttribute('role', 'dialog'); panel.setAttribute('aria-label', 'B站播放页增强'); panel.innerHTML = '
' + '
B站播放页增强五项各自独立,点一下立即生效
' + '' + '
' + '
' + '
设置会自动记住。快捷键在任何位置都能用。
鼠标移开或按 Esc 收起,也可以点右上角关闭。
'; const rowsEl = panel.querySelector('.rows'); const rows = {}; for (const def of UI_ROWS) { const row = document.createElement('div'); row.className = 'row'; row.setAttribute('role', 'switch'); row.setAttribute('tabindex', '0'); row.title = `快捷键 Shift + ${def.key}`; row.innerHTML = '
' + '
' + `
Shift${def.key}
`; row.querySelector('.t').textContent = def.name; row.querySelector('.d').textContent = def.desc; row.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); def.act(); }); row.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); def.act(); } }); rowsEl.appendChild(row); rows[def.k] = row; } wrap.appendChild(panel); // ---- 右上角关闭按钮 ---- const cl = panel.querySelector('.cl'); cl.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); /* 关之前先把待执行的悬浮定时器掐掉,否则「刚要展开」的那一下 会在关掉之后又把面板弹回来。 关掉之后鼠标若还在面板原来的位置上,那已经在 54px 的按钮之外了, 不会再触发 pointerenter,所以不会自己弹回 —— 移动鼠标回到按钮上才会重新展开。 */ clearHoverTimer(); setPanelOpen(false); }); cl.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); clearHoverTimer(); setPanelOpen(false); } }); /* ---- 鼠标悬浮展开 ---- 监听挂在 .wrap(按钮 + 面板的整体)上,不挂在按钮上: 挂按钮的话,鼠标一滑进面板就离开了按钮,面板立刻收起,根本点不到开关。 */ wrap.addEventListener('pointerenter', (e) => { if (e.pointerType !== 'mouse') return; // 触摸 / 笔:不吃悬浮,走点击 hoverMode = true; clearHoverTimer(); hoverTimer = setTimeout(() => setPanelOpen(true, true), HOVER_OPEN_MS); }); wrap.addEventListener('pointerleave', (e) => { if (e.pointerType !== 'mouse') return; clearHoverTimer(); hoverTimer = setTimeout(() => setPanelOpen(false), HOVER_CLOSE_MS); }); /* 触屏笔记本这种混合设备:鼠标挪过之后 hoverMode 是 true, 这时候直接拿手指点按钮,点击会被上面那条「鼠标用户不响应点击」吞掉、面板打不开。 所以非鼠标的按下事件要把 hoverMode 清掉,让随后的 click 走触摸分支。 */ wrap.addEventListener('pointerdown', (e) => { if (e.pointerType !== 'mouse') hoverMode = false; }); btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); // 鼠标用户的展开/收起由悬浮负责:这时候再点一下把面板关掉会很反直觉 // (刚悬浮出来、想点开关却手滑点到按钮上)。键盘和触摸才用点击切换。 if (hoverMode) return; setPanelOpen(panel.hidden); }); btn.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); setPanelOpen(panel.hidden); } }); ui = { host, root, wrap, btn, panel, cl, rows }; syncUI(); /* ⚠ 挂到 documentElement()而不是 body —— 这是 v1.16.1 修「左下角 设置按钮消失」的关键之一。B站 是 SPA,页面加载/切页过程中会**整批重写 body 的子节点**(包括往末尾追加自己的层),挂在 body 上的宿主节点轻则 被拔走(靠 1 秒守护重挂,期间按钮闪没)、重则被压到别人新加的层底下。 挂到 上,body 怎么重写都碰不到它。 */ (document.documentElement || document.body).appendChild(host); } // 点面板外收起。用 composedPath 判断,才能在 shadow DOM 下正确识别「点在自己的 UI 上」 document.addEventListener('click', (e) => { if (!ui || ui.panel.hidden) return; const path = typeof e.composedPath === 'function' ? e.composedPath() : []; if (path.indexOf(ui.host) !== -1 || path.indexOf(ui.wrap) !== -1) return; setPanelOpen(false); }, true); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && ui && !ui.panel.hidden) setPanelOpen(false); }, true); /* ================= 操作栏竖排到播放器左侧 ================= 为什么节点不搬家、为什么 position:fixed 在这里是安全的,都写在文件上方 CSS 第 7 节。 这一节只干一件事:每帧算出竖排栏该在的坐标,写进 B站操作栏的内联样式。 几何规则(全部以「播放器左边缘」为锚,所以三种状态下都对得上): 竖排栏.right = 播放器.left − RAIL_GAP 但左边这一列的最左端是「设置按钮」的地盘,竖排栏不许压上去: left 的下限 = 按钮那一列的右边缘 + FAB_GAP。 窄窗口里播放器左边挤不出空隙时,竖排栏就宁可往右压一点(盖住画面左边一条), 也绝不去挤按钮 —— 按钮固定在页面左上角(top 定值锚定),位置永远不动。 垂直方向:在播放器范围内居中,再用视口上下各留 RAIL_MIN_X 夹一下 播放器几乎滚出视口时整条隐藏,不然会孤零零挂在屏幕边上 */ const RAIL_BAR_ID = 'arc_toolbar_report'; /* 设置面板展开后的高度参考(六个开关 + 头部 ≈ 500px)。 ⚠ v1.16.9 按钮改为「top 定值 + .wrap align-items:flex-start」顶部锚定后, 面板是**从按钮顶部向下**展开的(不再是以按钮中心上下各展开一半), 所以只需保证按钮下方留得下整个面板,而不是上下各留一半。 (本常量现已无引用,保留作参考。) */ const FAB_PANEL_RESERVE = 350; const RAIL_EMPTY_ATTR = 'data-bbhr-empty'; // 空壳按钮的标记(CSS 靠它整条隐藏) /* 收起竖排栏时把 top 推到这个值 —— 视口外,矩形彻底出界(见 nudgeSiteScroll 注释) */ const RAIL_OFFSCREEN_TOP = -9999; function railBar() { return document.getElementById(RAIL_BAR_ID); } /* ============ 催 B站 重判一次「视频还在不在屏幕上」 ============ 背景(2026-09-28 用户报「界面拉到评论区,画中画没了,原先是有的」): B站 判断视频有没有滚出屏幕时,**把这条操作栏(#arc_toolbar_report)的矩形也算进去了**。 而 rail 模式下它是 position:fixed + JS 每帧写 top:160 —— 矩形永远停在视口里, 于是 B站 永远认为视频还在,data-screen 一直卡在 web,右下角那个小窗(320×180) 永远不出现。原来的收起只写了 visibility:hidden:矩形一点没动,所以照样挡着小窗。 修法见 placeRail:收起时把 top 推到视口外(矩形跟着出界),visibility 顺手遮一下。 ⚠ 光改矩形还不够 —— B站 **只在 scroll 事件里**重新判定: 实测把工具栏挪出视口之后静等 6 秒,data-screen 一动不动; 再轻轻抖一下滚动,立刻变成 mini。 所以「收起 / 展开」这两个状态切换一发生就得补一发合成 scroll 催它。 (合成事件对 window 上的 scroll 监听有效。document 上也补一发,因为有的代码 挂在 document 上;一次状态切换最多发一次,不会持续骚扰页面。) ⚠ 事件是**同步**派发的,B站 的处理函数当场量矩形 —— 所以发之前必须先把 坐标写完。收起那侧顺序天然是对的(先写 top 再发);展开那侧则是先把 left/top 算出来写好,再在函数末尾发(见 placeRail)。 */ function nudgeSiteScroll() { try { window.dispatchEvent(new Event('scroll')); document.dispatchEvent(new Event('scroll')); } catch (err) { /* 极老的浏览器没有 Event 构造器,就当没这回事 */ } } /* 收拾「空壳」按钮。 B站按登录态 / 浏览器能力决定 稿件举报、记笔记 要不要渲染: 不渲染时不是把节点删掉,而是留一个空壳(比如 .video-note 还在,但里面的 .video-note-inner 被 display:none,于是一个图标都没有)。 竖排成一条之后这些空壳就是一块块很显眼的空白,所以在这里标出来让 CSS 收掉。 ⚠ 找图标时必须把弹层里的排除掉 —— .video-tool-more 底下就塞着 记笔记/用手机看/稍后再看 三个菜单项的图标,它们平时是 0 宽, 不加排除会把好好的「更多」按钮也判成空壳。 */ function tidyRailItems(bar) { const items = bar.querySelectorAll( ':scope > .video-toolbar-left > .video-toolbar-left-main > .toolbar-left-item-wrap, ' + ':scope > .video-toolbar-right > .video-toolbar-right-item'); for (const it of items) { const icons = [...it.querySelectorAll('.video-toolbar-item-icon')] .filter((s) => !s.closest('.van-popover')); const empty = !icons.length || icons[0].getBoundingClientRect().width < 1; const marked = it.getAttribute(RAIL_EMPTY_ATTR) === '1'; if (empty === marked) continue; // 只在真的变了才写,避免每帧都去动 B站的节点 if (empty) it.setAttribute(RAIL_EMPTY_ATTR, '1'); else it.removeAttribute(RAIL_EMPTY_ATTR); } } function clearRailEmpty() { const bar = railBar(); if (!bar) return; for (const el of bar.querySelectorAll('[' + RAIL_EMPTY_ATTR + ']')) el.removeAttribute(RAIL_EMPTY_ATTR); } /* 把写给 B站节点的内联样式全部撤掉,操作栏就回到「播放器下方一整行」的原生状态。 关了开关却不擦干净的话,那条栏会一直留着 fixed 定位飘在左边。 */ function resetRail() { const bar = railBar(); if (bar && railStyled) { bar.style.left = ''; bar.style.top = ''; bar.style.visibility = ''; } railStyled = false; railSeenL = -1; railSeenT = -1; railSeenV = ''; clearRailEmpty(); clearFabInlinePos(); } /* 设置按钮不再跟着竖排栏跑了 —— 它钉在页面左上角(UI_CSS 的 :host 里,top 定值锚定)。 这里只负责把内联 left/top 擦掉,让 CSS 那套落位说了算; 主要是兼容从旧版本(按钮会被挪到栏下方)升上来的页面,不留残影。 */ function clearFabInlinePos() { if (!ui || !ui.host) return; if (ui.host.style.left) ui.host.style.left = ''; if (ui.host.style.top) ui.host.style.top = ''; } function placeRail() { const bar = railBar(); const player = document.querySelector('#bilibili-player') || document.querySelector('#playerWrap'); /* SPA 换页时操作栏会被整批重建,中间有几百毫秒两头都找不到。 找不到就什么都不做:新节点一长出来,下一帧自然会重新贴上。 按钮不受影响(它的位置跟竖排栏无关)。 */ if (!bar || !player) return; const bw = bar.offsetWidth; const bh = bar.offsetHeight; if (!bw || !bh) return; // 还在排版的中间态,等下一帧 const vh = window.innerHeight; const pr = player.getBoundingClientRect(); /* 小窗状态:B站 把播放器收成右下角那块 320×180 时,.bpx-player-container 的 data-screen 变成 mini。 ⚠ 注意换成小窗**不会挪动 #bilibili-player**(那是外层的大框,探针实测它的 矩形一直待在页面原位),缩小的只是里面的容器。所以「视频还在不在屏幕上」 照旧可以用 #bilibili-player 的矩形判断,不必为小窗单开一条分支 —— 事实上也不能:见下面「收起 / 展开」那段注释,单开分支会把回程堵死。 */ const pc = document.querySelector('.bpx-player-container'); const miniOn = !!pc && pc.getAttribute('data-screen') === 'mini'; let needNudge = false; /* 播放器露出来的部分太少就收起整条栏。 阈值取「栏高的六成」和 150px 里更小的那个 —— 窗口很矮时栏本身就不高。 ⚠ 收起/展开只看**播放器**的矩形,绝不看 data-screen 是不是 mini —— B站 判定「视频在不在屏幕上」时把这条操作栏的矩形也算进去了(探针实测): 栏在视口里 → 认为视频还在 → 不进小窗;栏离开视口 → 才进小窗。 于是一旦写成「是 mini 就收起」,回程就没了:栏收起 → B站 认为视频不在屏幕上 → 永远停在 mini → 栏永远不展开(实测滚回顶部也切不回来,死锁)。 正确做法就是让栏的进出跟着播放器走:播放器走了栏走、播放器回来栏回来, 两个方向的判定都用同一份矩形说话。 */ const visible = Math.min(pr.bottom, vh) - Math.max(pr.top, 0); if (visible < Math.min(bh * 0.6, 150)) { if (railSeenV !== 'hidden') { /* 收起要「人走开」,不能只「闭上眼」——只写 visibility 的话矩形还停在视口里, B站 会一直认为视频还在,小窗永远不出现。见 nudgeSiteScroll 上方的长注释。 */ bar.style.top = RAIL_OFFSCREEN_TOP + 'px'; bar.style.visibility = 'hidden'; railSeenT = RAIL_OFFSCREEN_TOP; railSeenV = 'hidden'; railStyled = true; needNudge = true; // 视频和操作栏都出去了:催 B站 把播放器收成小窗 } if (needNudge) nudgeSiteScroll(); // 坐标已经写完了,这里可以直接发 return; } if (railSeenV !== 'shown') { bar.style.visibility = ''; railSeenV = 'shown'; railSeenL = -1; // 收起时 top 被推到了视口外,位置要整组重算 railSeenT = -1; /* 播放器回到屏幕上,但 B站 可能还停在小窗里 —— 它上一次判定看到的是 「操作栏已经出视口」,所以得等我们这一侧先展开、矩形回到视口, 再催它重判一次,它才会把小窗收回去。 ⚠ 这一发不能在这儿发:合成事件是**同步**派发的,B站 的处理函数会当场 量矩形,而 left/top 要到下面几行才写 —— 先发事件等于让它量到 -9999, 白催。所以只记个记号,放到写完坐标之后再发。 */ needNudge = miniOn; } /* 竖排栏往左压的下限。设置按钮固定在 FAB_LEFT、宽 FAB_SIZE,位置恒定, 所以这里直接算就行 —— 别去量它的 rect,hover 时的 scale(1.07) 会让宽度抖动。 */ const minLeft = Math.max(RAIL_MIN_X, FAB_LEFT + FAB_SIZE + FAB_GAP); let left = Math.round(pr.left - bw - RAIL_GAP); if (left < minLeft) left = minLeft; let top = Math.round(pr.top + (pr.height - bh) / 2); const maxTop = vh - bh - RAIL_MIN_X; if (top > maxTop) top = maxTop; if (top < RAIL_MIN_X) top = RAIL_MIN_X; // 只在真的变了才写:每帧无脑写一遍会让浏览器每帧都重算一次样式 if (left !== railSeenL) { bar.style.left = left + 'px'; railSeenL = left; railStyled = true; } if (top !== railSeenT) { bar.style.top = top + 'px'; railSeenT = top; railStyled = true; } /* 展开这一侧的 nudge 放这儿 —— 必须等 left/top 都写完了再发, 否则 B站 当场量到的还是 -9999(见上面 show 分支的注释)。 */ if (needNudge) nudgeSiteScroll(); /* 收拾空壳按钮。节流到 400ms 一次 —— 每帧都查一遍太浪费, 而且它一改就会让整条栏重新排版(高度变了要重新居中)。 */ const now = Date.now(); if (now >= railTidyNext) { railTidyNext = now + 400; tidyRailItems(bar); } } /* ⚠ railRaf / railTimer 已在文件上方声明过(apply() 第一次被调用之前) */ function railTick() { railRaf = 0; /* 兜一层 try:万一这里抛了异常,rAF 链就断在这儿、再也不会重新排下一帧, 整条竖排栏会冻在上一帧的位置(而且一点提示都没有)。 异常存起来给 __bbhrRail() 看,循环本身照常续上。 */ try { placeRail(); } catch (err) { railLastError = String((err && err.message) || err); } if (rail) railRaf = requestAnimationFrame(railTick); } /* 开关一变就调它:开了起循环,关了停循环再把痕迹擦干净。 为什么 rAF 和定时器**两个都要**: rAF 负责顺滑(跟着播放器一帧一帧走,滚动时不会掉队); 但它并不保证一定会被调度 —— 实测就遇到过 rAF 句柄发出去了、回调一直不来的情况 (后台标签页、某些 headless / 显卡降级场景都会这样)。 只靠 rAF 的话竖排栏会一直待在 CSS 的兜底坐标上、位置也不跟着播放器走, 而且没有任何报错,非常难查。 所以再挂一个 200ms 的定时器兜底:rAF 正常时它只是重复算一遍(很便宜), rAF 不工作时它就是唯一的那根线,至少保证能贴上去。 */ function ensureRailLoop() { if (rail) { if (!railTimer) { // 给首次排版留点时间:刚上来就量图标尺寸的话, // 很可能把「还没排完」当成「0 宽的空壳」,按钮会闪一下再回来 railTidyNext = Date.now() + 700; railTimer = setInterval(() => { try { placeRail(); } catch (err) { railLastError = String((err && err.message) || err); } }, 200); } if (!railRaf) railRaf = requestAnimationFrame(railTick); } else { if (railRaf) { cancelAnimationFrame(railRaf); railRaf = 0; } if (railTimer) { clearInterval(railTimer); railTimer = 0; } resetRail(); } } /* 自检口子:控制台敲 __bbhrRail() 就能看到竖排栏的内部状态与最后一条异常, 排查「怎么没贴上去 / 栏怎么被挤到别处」时不用靠猜。只读,不改任何东西。 */ window.__bbhrRail = () => ({ on: rail, styled: railStyled, /* 竖排栏往左能压到哪儿(= 按钮那一列右边 + FAB_GAP)。 栏的 left 永远 ≥ 这个值,拿它和 bar.rect.x 一比就知道有没有挤到按钮。 */ minLeft: Math.max(RAIL_MIN_X, FAB_LEFT + FAB_SIZE + FAB_GAP), fab: (() => { if (!ui || !ui.host || !ui.host.shadowRoot) return null; const f = ui.host.shadowRoot.querySelector('.fab'); if (!f) return null; const q = f.getBoundingClientRect(); return { left: Math.round(q.left), centerY: Math.round(q.top + q.height / 2), hostInline: { left: ui.host.style.left, top: ui.host.style.top } }; })(), raf: railRaf, timer: !!railTimer, seen: { l: railSeenL, t: railSeenT, v: railSeenV }, lastError: railLastError, bar: (() => { const b = railBar(); if (!b) return null; const q = b.getBoundingClientRect(); return { rect: { x: Math.round(q.x), y: Math.round(q.y), w: Math.round(q.width), h: Math.round(q.height) }, inline: { left: b.style.left, top: b.style.top, vis: b.style.visibility }, }; })(), }); /* ================= 兜底:指针真的走开了就关 ================= 正常路径是 wrap 上的 pointerleave,但它不是万无一失的: 页面上任何一层只要在**捕获阶段**调一次 stopPropagation,事件就传不到我们这儿, 面板会一直挂在屏幕上不走。B站这种全局监听一堆的站点,值得防一手。 所以再补一层纯几何的判断:用 document 上的 pointermove 记下指针位置 (passive + capture,不改页面行为),每 150ms 和 wrap 的矩形比一次, 连续落在外面超过 350ms 就收。350 > 上面 260ms 的宽限期, 所以正常路径永远先动手,这层只在它失效时兜底。 ⚠ 只在 hoverMode 下生效:键盘打开的面板旁边根本没有鼠标, 不加这个判断的话,键盘用户刚按回车打开就会被这层立刻关掉。 */ let lastPtr = null; let outsideSince = 0; /* 指针是不是还压在我们的 UI 上。 ⚠ 不能拿 ui.wrap 的矩形来判 —— .wrap 是 flex 容器,盒子里除了按钮还框着 「按钮上方/下方、面板下方」那一大片透明空区,鼠标挪进空区会被误判成 「还在里面」,这层兜底就跟着一起失效(和 :host 的 pointer-events 是同一个坑的两面)。 所以逐块判:按钮 ∪ 面板 ∪ 按钮右边那道 ${UI_GAP}px 的桥。 */ function pointerOverUI(x, y) { if (!ui) return false; const inRect = (q) => !!q && q.width > 0 && q.height > 0 && x >= q.left && x <= q.right && y >= q.top && y <= q.bottom; const btn = ui.btn ? ui.btn.getBoundingClientRect() : null; if (inRect(btn)) return true; /* 桥的位置和 CSS 里 .fab::after 一致:按钮右边 ${UI_GAP}px、上下与按钮齐平 */ if (btn && x > btn.right && x <= btn.right + UI_GAP && y >= btn.top && y <= btn.bottom) return true; if (ui.panel && !ui.panel.hidden && inRect(ui.panel.getBoundingClientRect())) return true; return false; } document.addEventListener('pointermove', (e) => { lastPtr = { x: e.clientX, y: e.clientY }; }, { passive: true, capture: true }); setInterval(() => { if (!ui || ui.panel.hidden || !hoverMode || !hoverOpenPanel || !lastPtr) { outsideSince = 0; return; } const inside = pointerOverUI(lastPtr.x, lastPtr.y); if (inside) { outsideSince = 0; return; } if (!outsideSince) { outsideSince = Date.now(); return; } if (Date.now() - outsideSince > 350) { outsideSince = 0; clearHoverTimer(); setPanelOpen(false); } }, 150); if (document.body) buildUI(); else document.addEventListener('DOMContentLoaded', buildUI); // 守护:B站是 SPA,切页/重建 DOM 时 body 里的节点会整批换掉,UI 得能自己回来。 setInterval(() => { if (!ui || !ui.host.isConnected) { ui = null; buildUI(); return; } /* 面板在哪些页面露出来: · 播放页 —— 隐藏右栏 / 放大 / 进度条 / 操作栏竖排 都在这儿用得上; · 首页 —— 「首页轮播」这一项用得上(其它四项在首页没东西可作用)。 其余页面(动态、频道、番剧索引…)没有可调的东西,藏起来不碍事。 */ const isVideoPage = location.pathname.indexOf('/video/') === 0 || !!document.querySelector('#bilibili-player, .bpx-player-container'); const isHomePage = location.pathname === '/' || location.pathname === '/index.html'; ui.host.style.display = (isVideoPage || isHomePage) ? '' : 'none'; }, 1200); /* ===== 首次 apply():必须放在所有顶层 const/let 初始化之后同步调用 ===== apply() 的函数体里会用到 RAINBOW_TARGETS(syncRainbow 用)、upCard / upCardBound(ensureUpCard 用)等声明在本文件更靠后位置的变量; 把调用放到这里(文件末尾),它们全部已初始化,不会再踩「暂时性死区」。 document-start 时 html 节点若还没出生(极早注入的环境),返回 false, 就 10ms 一轮等它出现。 */ if (!apply()) { const timer = setInterval(() => { if (apply()) clearInterval(timer); }, 10); } })();