Bilibili - 优化未登录(不可用)情况下的移动网页端

优化未登录(不可用)情况下的移动网页端的使用体验 | V0.7 累积更新

  1. // ==UserScript==
  2. // @name Bilibili - 优化未登录(不可用)情况下的移动网页端
  3. // @namespace https://bilibili.com/
  4. // @version 0.7
  5. // @description 优化未登录(不可用)情况下的移动网页端的使用体验 | V0.7 累积更新
  6. // @license GPL-3.0
  7. // @author DD1969
  8. // @match https://m.bilibili.com/*
  9. // @icon https://www.bilibili.com/favicon.ico
  10. // @require https://update.gf.qytechs.cn/scripts/475332/1250588/spark-md5.js
  11. // @require https://update.gf.qytechs.cn/scripts/510239/1454424/viewer.js
  12. // @require https://update.gf.qytechs.cn/scripts/524844/1567651/bilibili-mobile-comment-module.js
  13. // @require https://update.gf.qytechs.cn/scripts/512576/1464552/inject-viewerjs-style.js
  14. // @require https://update.gf.qytechs.cn/scripts/512574/1464548/inject-bilibili-comment-style.js
  15. // @grant none
  16. // @run-at document-end
  17. // ==/UserScript==
  18.  
  19. (async function() {
  20. 'use strict';
  21.  
  22. // no need to continue this script if user already logged in
  23. if (document.cookie.includes('DedeUserID')) return;
  24.  
  25. const blacklist = [];
  26.  
  27. // regular expressions
  28. const re = {
  29. home: /m\.bilibili\.com\/$|m\.bilibili\.com\/channel\/v\/.*/,
  30. video: /m\.bilibili\.com\/video\/.*/,
  31. search: /m\.bilibili\.com\/search.*/,
  32. space: /m\.bilibili\.com\/space\/.*/,
  33. dynamic: /m\.bilibili\.com\/dynamic\/.*/,
  34. opus: /m\.bilibili\.com\/opus\/.*/,
  35. topicDetail: /m\.bilibili\.com\/topic-detail.*/,
  36. }
  37.  
  38. // make sure the document is ready
  39. await new Promise(resolve => {
  40. const timer = setInterval(() => {
  41. if (document.head && document.body) { clearInterval(timer); resolve(); }
  42. }, 50);
  43. });
  44.  
  45. // search and remove elements constantly
  46. setupElementCleaner();
  47.  
  48. // add style patch
  49. addStyle();
  50.  
  51. // nav bar
  52. modifyNavBar();
  53. // home page
  54. if (re.home.test(window.location.href)) modifyHomePage();
  55.  
  56. // video page
  57. if (re.video.test(window.location.href)) modifyVideoPage();
  58.  
  59. // search page
  60. if (re.search.test(window.location.href)) modifySearchPage();
  61.  
  62. // space page
  63. if (re.space.test(window.location.href)) modifySpacePage();
  64.  
  65. // dynamic page
  66. if (re.dynamic.test(window.location.href)) modifyDynamicPage();
  67. // opus page
  68. if (re.opus.test(window.location.href)) modifyOpusPage();
  69.  
  70. // topic detail page
  71. if (re.topicDetail.test(window.location.href)) modifyTopicDetailPage();
  72.  
  73. // ------------ functions below ------------
  74.  
  75. function setupElementCleaner() {
  76. const selectors = [];
  77.  
  78. // home page
  79. if (re.home.test(window.location.href)) {
  80. selectors.push(...[
  81. '.m-nav-openapp', // 右上角的"下载App"按钮
  82. '.m-navbar .face', // 右上角的用户头像
  83. '.fixed-openapp', // 首页底部的打开App横条
  84. '.v-card__stats', // 视频卡片的数据信息
  85. '.reserve-float-btn', // 首页底部的浮动弹窗
  86. ]);
  87. }
  88.  
  89. // video page
  90. if (re.video.test(window.location.href)) {
  91. selectors.push(...[
  92. '.m-nav-openapp', // 右上角的"下载App"按钮
  93. '.m-navbar .face', // 右上角的用户头像
  94. '.video-natural-search .fixed-wrapper', // 顶部遮挡video元素的整个区域
  95. '.m-video-related .list-custom-slot .m-open-app', // 相关视频底部的打开App横条
  96. '.openapp-dialog', // 底部弹出的"浏览方式"
  97. '.caution-dialog', // 底部弹出的"友情提示"
  98. '.play-page-gotop', // 回到顶部按钮
  99. '.reserve-float-btn', // 底部的浮动弹窗
  100. '.fixed-openapp', // URL带'unique_k'参数时右下角才会出现的打开App按钮
  101. '.gsl-callapp-dom', // 视频模块中阻碍点击并唤起app的元素
  102. 'm-open-app.m-video-main-launchapp', // 从b23.tv打开时出现的打开App横条
  103. '.m-video-info', // 从b23.tv打开时出现的视频标题、作者和简介
  104. '.bottom-tab-header', // 从b23.tv打开时出现的下方相关视频header
  105. '.m-video-part', // 从b23.tv打开时出现的视频分P模块#1
  106. '.m-video-part-panel', // 从b23.tv打开时出现的视频分P模块#2
  107. ]);
  108. }
  109.  
  110. // space page
  111. if (re.space.test(window.location.href)) {
  112. selectors.push(...[
  113. '.fixed-openapp', // 底部的打开App按钮
  114. '.bili-dyn-item-header__following', // 动态右上方的关注按钮
  115. '.dyn-orig-author__following', // 转发的动态的作者的关注按钮
  116. ]);
  117. }
  118.  
  119. // dynamic & opus page
  120. if (re.dynamic.test(window.location.href) || re.opus.test(window.location.href)) {
  121. selectors.push(...[
  122. '.fixed-openapp', // 底部的打开App横条
  123. '.dyn-header__following', // 动态右上方的关注按钮
  124. '.dyn-share', // 若干分享按钮
  125. '.openapp-dialog', // 底部弹出的"浏览方式"
  126. '.reserve-float-btn', // 底部的浮动弹窗
  127. '.opus-module-author__action', // opus页右上角的关注按钮
  128. '.stat-openApp', // opus页(原专栏页)底部的stat模块
  129. ]);
  130. }
  131.  
  132. // topic detail page
  133. if (re.topicDetail.test(window.location.href)) {
  134. selectors.push(...[
  135. '.m-topic-float-openapp', // 底部的打开App按钮
  136. ]);
  137. }
  138.  
  139. // start cleaning
  140. setInterval(() => {
  141. for (const selector of selectors) {
  142. document.querySelectorAll(selector).forEach(element => element.remove());
  143. }
  144. }, 100);
  145. }
  146.  
  147. function addStyle() {
  148. // nav bar
  149. const navBarCSS = document.createElement('style');
  150. navBarCSS.textContent = `
  151. .m-navbar {
  152. position: relative !important;
  153. display: flex !important;
  154. justify-content: space-between;
  155. align-items: center;
  156. background: none !important;
  157. background-color: #FFFFFF !important;
  158. border-bottom: 1px solid #EEEEEE;
  159. }
  160.  
  161. .nav-logo {
  162. display: flex;
  163. align-items: center;
  164. height: 100%;
  165. }
  166.  
  167. .nav-logo img {
  168. height: 60%;
  169. }
  170.  
  171. .nav-search {
  172. display: flex;
  173. flex-direction: column;
  174. justify-content: center;
  175. width: 200px;
  176. height: 28px;
  177. border: 1px solid #EEEEEE;
  178. border-radius: 16px;
  179. }
  180.  
  181. .nav-search > svg {
  182. align-self: flex-end;
  183. margin-right: 8px;
  184. }
  185. `;
  186. document.head.appendChild(navBarCSS);
  187.  
  188. // video page
  189. const videoPageCSS = document.createElement('style');
  190. videoPageCSS.textContent = `
  191. .video-share-loading-mask {
  192. position: absolute;
  193. top: 0;
  194. left: 0;
  195. display: none;
  196. justify-content: center;
  197. align-items: center;
  198. width: 100%;
  199. height: 100%;
  200. z-index: 1999;
  201. background-color: #000000;
  202. }
  203.  
  204. .video-share-loading-mask-circle {
  205. width: 30px;
  206. height: 30px;
  207. border: 2px solid #000000;
  208. border-top-color: #ffffff;
  209. border-right-color: #ffffff;
  210. border-bottom-color: #ffffff;
  211. border-radius: 100%;
  212. animation: circle infinite 0.75s linear;
  213. }
  214.  
  215. @keyframes circle {
  216. 0% {
  217. transform: rotate(0);
  218. }
  219. 100% {
  220. transform: rotate(360deg);
  221. }
  222. }
  223.  
  224. .m-video-player {
  225. position: relative !important;
  226. top: initial !important;
  227. }
  228.  
  229. .video-info {
  230. display: flex;
  231. flex-direction: column;
  232. padding: 20px 12px;
  233. }
  234.  
  235. .video-info-title {
  236. font-size: 1.2rem;
  237. word-break: break-all;
  238. }
  239.  
  240. .video-info-author {
  241. display: flex;
  242. align-items: center;
  243. margin-top: 16px;
  244. }
  245.  
  246. .video-info-author-avatar {
  247. width: 32px;
  248. height: 32px;
  249. margin-right: 8px;
  250. border-radius: 100%;
  251. }
  252.  
  253. .video-info-desc {
  254. color: #666666;
  255. font-size: 0.8rem;
  256. word-break: break-all;
  257. }
  258.  
  259. .m-video-related {
  260. margin-top: 0 !important;
  261. padding: 24px 0;
  262. border-top: 1px dashed #aaa;
  263. border-bottom: 1px dashed #aaa;
  264. }
  265.  
  266. .card-box {
  267. justify-content: space-between;
  268. }
  269.  
  270. .card-box > .card {
  271. width: 49%;
  272. }
  273.  
  274. .card-box .card .label,
  275. .card-box .card .open-app.weakened,
  276. .card-box .card .video-card .count {
  277. display: none !important;
  278. }
  279.  
  280. .card-box .card .title {
  281. padding-top: 8px;
  282. padding-bottom: 16px;
  283. font-size: 0.8rem !important;
  284. word-break: break-all;
  285. }
  286.  
  287. .gsl-top-return {
  288. transform: scale(0.5);
  289. }
  290.  
  291. .gsl-buffer-app {
  292. display: none !important;
  293. }
  294.  
  295. .gsl-control-btn.gsl-control-btn-quality {
  296. display: none !important;
  297. }
  298.  
  299. .gsl-control-btn.gsl-control-btn-speed {
  300. display: flex !important;
  301. }
  302.  
  303. .gsl-control-btn.gsl-control-btn-speed .gsl-control-dot {
  304. display: none !important;
  305. }
  306.  
  307. .playback-rate-setting-panel {
  308. position: fixed;
  309. top: 0;
  310. left: 0;
  311. width: 100%;
  312. height: 100%;
  313. display: flex;
  314. justify-content: center;
  315. align-items: center;
  316. background-color: rgba(0, 0, 0, 0.5);
  317. z-index: 999999;
  318. }
  319.  
  320. .playback-rate-option-container {
  321. width: 240px;
  322. padding: 8px;
  323. display: flex;
  324. flex-direction: column;
  325. align-items: center;
  326. background-color: #FFFFFF;
  327. border-radius: 4px;
  328. user-select: none;
  329. }
  330.  
  331. .playback-rate-option {
  332. width: 100%;
  333. margin-top: 2px;
  334. padding: 8px 0;
  335. color: #FFFFFF;
  336. background-color: #00AEEC;
  337. border-top: 1px solid #EEEEEE;
  338. border-radius: 4px;
  339. text-align: center;
  340. }
  341.  
  342. .episode-container {
  343. display: flex;
  344. flex-direction: column;
  345. background-color: #f1f2f3;
  346. }
  347.  
  348. .episode-container-header {
  349. display: flex;
  350. align-items: center;
  351. padding: 12px;
  352. padding-bottom: 10px;
  353. }
  354.  
  355. .episode-container-header-count {
  356. margin-left: 4px;
  357. font-size: 0.8rem;
  358. font-family: monospace;
  359. color: #9499A0;
  360. }
  361.  
  362. .episode-list {
  363. display: flex;
  364. flex-direction: column;
  365. padding: 12px;
  366. padding-top: 0;
  367. }
  368.  
  369. .episode-list-item {
  370. display: flex;
  371. align-items: center;
  372. height: 36px;
  373. margin: 2px 0;
  374. padding: 2px 6px;
  375. }
  376.  
  377. .episode-list-item.is-current-episode {
  378. background-color: #ffffff;
  379. color: #00AEEC;
  380. outline: 1px solid #7bdbfd;
  381. border-radius: 4px;
  382. }
  383.  
  384. .episode-playing-gif {
  385. display: inline-block;
  386. width: 12px;
  387. height: 12px;
  388. margin-right: 5px;
  389. background-image: url('https://i0.hdslb.com/bfs/static/jinkela/playlist-video/asserts/playing.gif');
  390. background-repeat: no-repeat;
  391. background-size: 12px 12px;
  392. background-position: center;
  393. }
  394.  
  395. .episode-list-item-title {
  396. max-width: 250px;
  397. overflow: hidden;
  398. white-space: nowrap;
  399. text-overflow: ellipsis;
  400. line-height: 36px;
  401. font-size: 0.8rem;
  402. }
  403.  
  404. .episode-list-item-duration {
  405. flex-grow: 1;
  406. text-align: right;
  407. font-size: 0.8rem;
  408. font-family: monospace;
  409. color: #9499A0;
  410. }
  411.  
  412. #video-comment-module-wrapper {
  413. position: fixed;
  414. top: 0;
  415. left: 0;
  416. z-index: 2000;
  417. display: none;
  418. width: 100vw;
  419. height: 100vh;
  420. background-color: #fff;
  421. overflow-x: hidden;
  422. }
  423.  
  424. .close-comment-module-btn {
  425. position: fixed;
  426. right: 20px;
  427. bottom: 20px;
  428. z-index: 2001;
  429. display: none;
  430. justify-content: center;
  431. align-items: center;
  432. width: 40px;
  433. height: 40px;
  434. color: #fff;
  435. border-radius: 100%;
  436. background-color: #00AEEC;
  437. }
  438.  
  439. .open-comment-module-btn {
  440. display: flex;
  441. justify-content: center;
  442. align-items: center;
  443. margin: 0 12px 20px 12px;
  444. height: 40px;
  445. color: #fff;
  446. border-radius: 4px;
  447. background-color: #00AEEC;
  448. }
  449. `;
  450. document.head.appendChild(videoPageCSS);
  451.  
  452. // space page
  453. const spacePageCSS = document.createElement('style');
  454. spacePageCSS.textContent = `
  455. .m-space-info {
  456. margin-top: 0 !important;
  457. }
  458.  
  459. .bili-dyn-item {
  460. border-bottom: 1px solid #e7e7e7;
  461. }
  462.  
  463. .video-item-space {
  464. box-sizing: border-box;
  465. margin-right: 3.2vmin;
  466. padding: 2.4vmin 0;
  467. height: 24.26667vmin;
  468. position: relative;
  469. display: block;
  470. border-bottom: 1px solid #ddd;
  471. }
  472.  
  473. .video-item-space .cover {
  474. float: left;
  475. width: 31.2vmin;
  476. height: 19.46667vmin;
  477. position: relative;
  478. border-radius: 1.06667vmin;
  479. overflow: hidden;
  480. }
  481.  
  482. .video-item-space .cover .duration {
  483. padding: 0 0.53333vmin;
  484. position: absolute;
  485. right: 1.06667vmin;
  486. bottom: 1.06667vmin;
  487. border-radius: 0.53333vmin;
  488. background: rgba(0,0,0,.5);
  489. font-size: 3.2vmin;
  490. color: #fff;
  491. }
  492.  
  493. .video-item-space .info {
  494. margin-left: 34.4vmin;
  495. height: 19.46667vmin;
  496. position: relative;
  497. }
  498.  
  499. .video-item-space .info .title {
  500. max-height: 9.06667vmin;
  501. font-size: 3.73333vmin;
  502. color: #212121;
  503. line-height: 4.53333vmin;
  504. overflow: hidden;
  505. text-overflow: ellipsis;
  506. display: -webkit-box;
  507. -webkit-line-clamp: 2;
  508. -webkit-box-orient: vertical;
  509. }
  510.  
  511. .video-item-space .info .state {
  512. display: flex;
  513. align-items: center;
  514. position: absolute;
  515. bottom: 0;
  516. left: 0;
  517. right: 0;
  518. font-size: 2.66667vmin;
  519. color: #999;
  520. line-height: 4.53333vmin;
  521. height: 4.53333vmin;
  522. }
  523.  
  524. .video-item-space .info .state .view,
  525. .video-item-space .info .state .danmaku {
  526. display: flex;
  527. align-items: center;
  528. }
  529.  
  530. .video-item-space .info .state .danmaku {
  531. margin-left: 7.73333vmin;
  532. }
  533.  
  534. .video-item-space .info .state .icon {
  535. margin-right: 1.06667vmin;
  536. }
  537. `;
  538. document.head.appendChild(spacePageCSS);
  539.  
  540. // opus page
  541. const opusPageCSS = document.createElement('style');
  542. opusPageCSS.textContent = `
  543. .show-read-text {
  544. max-height: initial !important;
  545. }
  546. `;
  547. document.head.appendChild(opusPageCSS);
  548.  
  549. // topic detail page
  550. const topicDetailPageCSS = document.createElement('style');
  551. topicDetailPageCSS.textContent = `
  552. .topic-detail-container {
  553. top: 0 !important;
  554. }
  555. `;
  556. document.head.appendChild(topicDetailPageCSS);
  557. }
  558.  
  559. async function modifyNavBar() {
  560. const timer = setInterval(() => {
  561. const navBarElement = document.querySelector('.m-navbar');
  562. if (navBarElement) {
  563. const parentElement = navBarElement.parentElement;
  564. if (parentElement.tagName === 'M-OPEN-APP') {
  565. parentElement.insertAdjacentElement('beforebegin', navBarElement);
  566. parentElement.remove();
  567. }
  568.  
  569. navBarElement.__vue__.$destroy();
  570. navBarElement.innerHTML = `
  571. <a class="nav-logo" href="/"><img src="https://i1.hdslb.com/bfs/static/jinkela/long/mstation/logo-bilibili-pink.png" /></a>
  572. <a class="nav-search" href="/search">
  573. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#CCCCCC" d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"></path></svg>
  574. </a>
  575. `;
  576. clearInterval(timer);
  577. }
  578. }, 100);
  579. }
  580.  
  581. async function modifyHomePage() {
  582. // modify video card
  583. setInterval(() => {
  584. document.querySelectorAll('.card-box .v-card:not(.modified)').forEach(card => {
  585. // blacklist check
  586. const cardTitle = card.querySelector('.v-card__title');
  587. if (blacklist.some(keyword => cardTitle.textContent.includes(keyword))) { card.remove(); return; }
  588.  
  589. // rewrite behavior when video card being clicked
  590. if (card.__vue__?.bvid && card.__vue__?.$destroy) {
  591. const bvid = card.__vue__.bvid;
  592. card.onclick = () => window.location.href = `https://m.bilibili.com/video/${bvid}`;
  593. card.__vue__.$destroy();
  594. card.querySelector('.sleepy')?.classList.remove('sleepy');
  595. card.classList.add('modified');
  596. }
  597. });
  598. }, 100);
  599. }
  600.  
  601. async function modifyVideoPage() {
  602. // show hidden video player
  603. const videoShare = await new Promise(resolve => {
  604. const timer = setInterval(() => {
  605. const videoShare = document.querySelector('.video-share');
  606. if (videoShare) {
  607. videoShare.style.display = 'block';
  608. videoShare.style.position = 'relative';
  609. clearInterval(timer);
  610. resolve(videoShare);
  611. }
  612. }, 100);
  613. });
  614.  
  615. // add loading mask
  616. const loadingMask = document.createElement('div');
  617. loadingMask.classList.add('video-share-loading-mask');
  618. loadingMask.innerHTML = '<span class="video-share-loading-mask-circle"></span>';
  619. videoShare.appendChild(loadingMask);
  620.  
  621. // show loading mask when video share being clicked, but only once
  622. const videoShareOnClickHandler = () => {
  623. videoShare.removeEventListener('click', videoShareOnClickHandler);
  624. loadingMask.style.display = 'flex';
  625.  
  626. // hide the loading mask after the video start playing
  627. const timer = setInterval(() => {
  628. const progressBar = videoShare.querySelector('.gsl-ui-progress-bar');
  629. if (progressBar && progressBar.style.width && progressBar.style.width !== '0%') {
  630. clearInterval(timer);
  631. loadingMask.style.display = 'none';
  632. document.querySelector('.gsl-poster')?.remove();
  633. }
  634. }, 50);
  635. }
  636. videoShare.addEventListener('click', videoShareOnClickHandler);
  637.  
  638. // get video info
  639. const videoID = window.location.pathname.replace('/video/', '');
  640. let param;
  641. if (videoID.startsWith('av')) param = `aid=${videoID.replace('av', '')}`;
  642. if (videoID.startsWith('BV')) param = `bvid=${videoID}`;
  643. const videoInfo = await fetch(`https://api.bilibili.com/x/web-interface/view?${param}`).then(res => res.json()).then(json => json.data);
  644.  
  645. // add info
  646. const infoContainer = document.createElement('div');
  647. infoContainer.classList.add('video-info');
  648. infoContainer.innerHTML = `
  649. <div class="video-info-title">${videoInfo.title}</div>
  650. <a class="video-info-author" href="https://m.bilibili.com/space/${videoInfo.owner.mid}">
  651. <img class="video-info-author-avatar" src="${videoInfo.owner.face}">
  652. <span class="video-info-author-name">${videoInfo.owner.name}</span>
  653. </a>
  654. <div class="video-info-desc" ${videoInfo.desc ? 'style="margin-top: 16px;"' : ''}>${videoInfo.desc.replaceAll('\n', '<br>').replaceAll(/BV[1-9A-HJ-NP-Za-km-z]{10}|av\d+/g, (match) => `<a style="color: #00AEEC" href="https://m.bilibili.com/video/${match}">${match}</a>`)}</div>
  655. `;
  656. document.querySelector('.video-share').insertAdjacentElement('afterend', infoContainer);
  657.  
  658. // add comment module wrapper
  659. const commentModuleWrapper = document.createElement('div');
  660. commentModuleWrapper.id = 'video-comment-module-wrapper';
  661. document.body.appendChild(commentModuleWrapper);
  662. MobileCommentModule.init(commentModuleWrapper);
  663.  
  664. // add button to close comment module
  665. const closeCommentModuleBtn = document.createElement('span');
  666. closeCommentModuleBtn.classList.add('close-comment-module-btn');
  667. closeCommentModuleBtn.textContent = '×';
  668. closeCommentModuleBtn.onclick = function () {
  669. // trigger 'popstate' event
  670. window.history.back();
  671. }
  672. document.body.appendChild(closeCommentModuleBtn);
  673. // add button to open comment module
  674. const openCommentModuleBtn = document.createElement('div');
  675. openCommentModuleBtn.classList.add('open-comment-module-btn');
  676. openCommentModuleBtn.textContent = '查看评论';
  677. openCommentModuleBtn.onclick = function () {
  678. commentModuleWrapper.style.display = 'block';
  679. closeCommentModuleBtn.style.display = 'flex';
  680. document.body.style.overflow = 'hidden';
  681.  
  682. // push state in history
  683. window.history.pushState({}, '');
  684.  
  685. // close comment module when 'popstate' event triggered
  686. const popStateHandler = () => {
  687. commentModuleWrapper.style.display = 'none';
  688. closeCommentModuleBtn.style.display = 'none';
  689. document.body.style.overflow = 'initial';
  690. document.querySelectorAll('.reply-item .preview-image-container').forEach(item => item.viewer?.hide(true));
  691. window.removeEventListener('popstate', popStateHandler);
  692. }
  693. window.addEventListener('popstate', popStateHandler);
  694. }
  695. infoContainer.insertAdjacentElement('afterend', openCommentModuleBtn);
  696.  
  697. // add episodes
  698. const partNum = parseInt((new URLSearchParams(window.location.search)).get('p') || '1');
  699. const episodeContainer = document.createElement('div');
  700. episodeContainer.classList.add('episode-container');
  701. if (videoInfo.pages.length > 1) {
  702. episodeContainer.innerHTML = `
  703. <div class="episode-container-header">
  704. <span>视频选集</span>
  705. <span class="episode-container-header-count">(${partNum}/${videoInfo.pages.length})</span>
  706. </div>
  707. <div class="episode-list">
  708. ${
  709. videoInfo.pages.map((page, index) => {
  710. const isCurrentEpisode = partNum === index + 1;
  711. const second = page.duration % 60;
  712. const minute = (page.duration - second) / 60 % 60;
  713. const hour = Math.floor(page.duration / 3600);
  714. const formattedDuration = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}:${String(second).padStart(2, '0')}`;
  715. return `
  716. <a class="episode-list-item ${isCurrentEpisode ? 'is-current-episode' : ''}" ${isCurrentEpisode ? '' : `href="${window.location.pathname}?p=${index + 1}"`}>
  717. ${isCurrentEpisode ? '<span class="episode-playing-gif"></span>' : ''}
  718. <span class="episode-list-item-title">${page.part}</span>
  719. <span class="episode-list-item-duration">${formattedDuration}</span>
  720. </a>
  721. `;
  722. }).join('')
  723. }
  724. </div>
  725. `;
  726. }
  727. openCommentModuleBtn.insertAdjacentElement('afterend', episodeContainer);
  728.  
  729. // move .bottom-tab to where it should be, usually happens when page open by b23.tv short url
  730. const bottomTab = document.querySelector('.video-share > .bottom-tab');
  731. if (bottomTab) episodeContainer.insertAdjacentElement('afterend', bottomTab);
  732.  
  733. // modify video card
  734. setInterval(() => {
  735. // move the video card out of <m-open-app>
  736. document.querySelectorAll('.card-box > m-open-app').forEach(item => {
  737. const href = item.getAttribute('universallink').replace('.html?', '');
  738. item.outerHTML = item.querySelector('.card').outerHTML.replace('class="card"', `class="card" data-href="${href}"`).replaceAll('sleepy', '');
  739. });
  740.  
  741. // setup click event
  742. document.querySelectorAll('.card-box > .card:not(.modified)').forEach(item => {
  743. // blacklist check
  744. const cardTitle = item.querySelector('.title');
  745. if (blacklist.some(keyword => cardTitle.textContent.includes(keyword))) { item.remove(); return; }
  746.  
  747. item.classList.add('modified');
  748. item.onclick = () => window.location.href = item.dataset.href;
  749. });
  750. }, 100);
  751.  
  752. // modify playback rate button
  753. const timer4PlaybackRateBtn = setInterval(() => {
  754. const playbackRateBtn = document.querySelector('.gsl-control .gsl-control-btn-speed');
  755. if (playbackRateBtn) {
  756. playbackRateBtn.onclick = () => {
  757. const maskElement = document.createElement('div');
  758. maskElement.classList.add('playback-rate-setting-panel');
  759. maskElement.innerHTML = `
  760. <div class="playback-rate-option-container">
  761. <span style="margin-bottom: 6px; padding: 6px 0;">播放倍速</span>
  762. <span class="playback-rate-option" data-rate="5">5.0x</span>
  763. <span class="playback-rate-option" data-rate="3">3.0x</span>
  764. <span class="playback-rate-option" data-rate="2">2.0x</span>
  765. <span class="playback-rate-option" data-rate="1.5">1.5x</span>
  766. <span class="playback-rate-option" data-rate="1">1.0x</span>
  767. <span class="playback-rate-option" data-rate="0.5">0.5x</span>
  768. </div>
  769. `;
  770. maskElement
  771. .querySelectorAll('.playback-rate-option')
  772. .forEach(optionElement => optionElement.addEventListener('click', function() {
  773. const videoElement = document.querySelector('#bilibiliPlayer video');
  774. if (videoElement) videoElement.playbackRate = parseFloat(this.dataset.rate);
  775. }));
  776. maskElement.onclick = () => maskElement.remove();
  777. (document.querySelector('#bilibiliPlayer .gsl-area.gsl-wide') || document.body).appendChild(maskElement);
  778. }
  779.  
  780. clearInterval(timer4PlaybackRateBtn);
  781. }
  782. }, 100);
  783.  
  784. // modify fullscreen button
  785. const timer4FullscreenBtn = setInterval(() => {
  786. const fullscreenBtn = document.querySelector('.gsl-btn-fullscreen');
  787. if (fullscreenBtn) {
  788. // pre-click, whick needs 2 clicks to enter fullscreen naturally
  789. fullscreenBtn.click();
  790. clearInterval(timer4FullscreenBtn);
  791. }
  792. }, 100);
  793.  
  794. // modify ending panel of video
  795. const timer4EndingPanel = setInterval(async () => {
  796. const endingPanel = document.querySelector('.gsl-end.gsl-show .gsl-ending-panel-video');
  797. if (endingPanel) {
  798. clearInterval(timer4EndingPanel);
  799.  
  800. // clean click events
  801. endingPanel.innerHTML = `
  802. <div class="gsl-ending-panel-video">
  803. <div class="gsl-ending-panel-pic">
  804. <img />
  805. </div>
  806. <div class="gsl-ending-panel-content">
  807. <div class="gsl-ending-panel-title"></div>
  808. <div class="gsl-ending-panel-button">点击打开</div>
  809. </div>
  810. </div>
  811. `;
  812.  
  813. // get related video data
  814. const relatedVideoData = await fetch(`https://api.bilibili.com/x/web-interface/archive/related?bvid=${videoInfo.bvid}`).then(res => res.json()).then(json => json.data);
  815.  
  816. // start showing
  817. let currentIndex;
  818. const currentVideoCover = endingPanel.querySelector('.gsl-ending-panel-pic img');
  819. const currentVideoTitle = endingPanel.querySelector('.gsl-ending-panel-title');
  820. const showRelatedVideo = () => {
  821. currentIndex = Math.floor(Math.random() * relatedVideoData.length);
  822. currentVideoCover.src = relatedVideoData[currentIndex].pic + '@460w_280h.webp';
  823. currentVideoTitle.textContent = relatedVideoData[currentIndex].title;
  824. }
  825. showRelatedVideo();
  826. setInterval(showRelatedVideo, 5 * 1000);
  827.  
  828. // setup click event
  829. endingPanel.onclick = () => window.location.href = `https://m.bilibili.com/video/${relatedVideoData[currentIndex].bvid}`;
  830. }
  831. }, 100);
  832.  
  833. // modify charge video page
  834. const timer4ChargeMask = setInterval(() => {
  835. const mask = document.querySelector('.m-video-player m-open-app.charge-mask');
  836. if (mask) {
  837. mask.outerHTML = mask.outerHTML.replace('<m-open-app', '<div').replace('</m-open-app>', '</div>');
  838. }
  839. }, 100);
  840. setTimeout(() => clearInterval(timer4ChargeMask), 3 * 1000);
  841. }
  842.  
  843. async function modifySearchPage() {
  844. // modify cancel button
  845. const timer4CancelBtn = setInterval(() => {
  846. const cancelBtn = document.querySelector('.m-search-search-bar .cancel');
  847. if (cancelBtn) {
  848. cancelBtn.outerHTML = cancelBtn.outerHTML.replace('class="cancel"', 'class="cancel" href="/"');
  849. clearInterval(timer4CancelBtn);
  850. }
  851. }, 100);
  852.  
  853. // modify video card
  854. setInterval(() => {
  855. document.querySelectorAll('.card-box .v-card-single:not(.vue-destroyed)').forEach(card => {
  856. // blacklist check
  857. const cardTitle = card.querySelector('.info .title');
  858. const cardAuthor = card.querySelector('.info .author');
  859. if (blacklist.some(keyword => cardTitle.textContent.includes(keyword) || cardAuthor.textContent.includes(keyword))) { card.remove(); return; }
  860.  
  861. const aid = card.dataset.aid;
  862.  
  863. // remove card which leading to live streaming
  864. if (aid === '0') { card.remove(); return; }
  865.  
  866. // cancel default actions
  867. card.classList.add('vue-destroyed');
  868. card.__vue__.$destroy();
  869.  
  870. // setup click event
  871. card.onclick = () => window.location.href = `https://m.bilibili.com/video/av${aid}`;
  872.  
  873. // show covers
  874. card.querySelector('.sleepy')?.classList.remove('sleepy');
  875. });
  876. }, 100);
  877.  
  878. // modify user card
  879. setInterval(() => {
  880. document.querySelectorAll('.card-box a.m-search-user-item:not(.vue-destroyed)').forEach(card => {
  881. card.classList.add('vue-destroyed');
  882. card.__vue__.$destroy();
  883. card.href = card.href.replace('?from=search', '');
  884. card.querySelector('.sleepy')?.classList.remove('sleepy');
  885. });
  886. }, 100);
  887. }
  888.  
  889. async function modifySpacePage() {
  890. // setup click event on user avatar if live streaming
  891. const timer4LiveStreaming = setInterval(() => {
  892. const face = document.querySelector('.m-space-info .info-main .face:not(.modified):has(.living-wrapper)');
  893. const liveRoomID = window.__INITIAL_STATE__?.space?.info?.live_room?.roomid;
  894. if (face && liveRoomID) {
  895. face.classList.add('modified');
  896. face.onclick = () => window.location.href = `https://live.bilibili.com/${liveRoomID}`;
  897. }
  898. }, 100);
  899. setTimeout(() => clearInterval(timer4LiveStreaming), 3 * 1000);
  900.  
  901. // deactivate follow button
  902. const timer4FollowBtn = setInterval(() => {
  903. const followBtn = document.querySelector('.m-space-info .follow-btn');
  904. if (followBtn) {
  905. followBtn.__vue__.$destroy();
  906. clearInterval(timer4FollowBtn);
  907. }
  908. }, 100);
  909. // modify dynamic items
  910. setInterval(() => {
  911. document.querySelectorAll('.dynamic-list .list-scroll-content-wrap > m-open-app').forEach(item => {
  912. const dynItem = item.querySelector('.bili-dyn-item');
  913. dynItem.onclick = () => window.location.href = item.getAttribute('universallink');
  914. item.insertAdjacentElement('beforebegin', dynItem);
  915. item.remove();
  916. });
  917. }, 100);
  918.  
  919. // modify archive list
  920. const timer4ArchiveList = setInterval(async () => {
  921. const archiveList = document.querySelector('.archive-list');
  922. if (archiveList) {
  923. clearInterval(timer4ArchiveList);
  924.  
  925. // get user id
  926. const mid = window.location.pathname.replace('/space/', '');
  927.  
  928. const getPaginationData = async (pn) => {
  929. const params = { mid, pn, ps: 50, order: 'senddate', wts: Math.floor(Date.now() / 1000) };
  930. return fetch(`https://api.bilibili.com/x/space/wbi/arc/search?${await getWbiQueryString(params)}`, { credentials: 'include' }).then(res => res.json()).then(json => json.data.list?.vlist || []);
  931. };
  932.  
  933. const getVideoItem = (videoData) => {
  934. return `
  935. <a href="/video/${videoData.bvid}" class="video-item-space">
  936. <div class="cover">
  937. <div class="bfs-img-wrap">
  938. <div class="bfs-img b-img">
  939. <img class="b-img__inner" src="${videoData.pic}@468w_292h_1c.webp" alt="${videoData.title}">
  940. </div>
  941. </div>
  942. <span class="duration">${videoData.length}</span>
  943. </div>
  944. <div class="info">
  945. <h3 class="title">${videoData.title}</h3>
  946. <div class="state">
  947. <span class="view">
  948. <svg class="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16" width="16" height="16" style="width: 16px; height: 16px;"><path d="M8 3.3320333333333334C6.321186666666667 3.3320333333333334 4.855333333333333 3.4174399999999996 3.820593333333333 3.5013466666666666C3.1014733333333333 3.5596599999999996 2.5440733333333334 4.109013333333333 2.48 4.821693333333333C2.4040466666666664 5.666533333333334 2.333333333333333 6.780666666666666 2.333333333333333 7.998666666666666C2.333333333333333 9.216733333333334 2.4040466666666664 10.330866666666665 2.48 11.175699999999999C2.5440733333333334 11.888366666666666 3.1014733333333333 12.437733333333334 3.820593333333333 12.496066666666666C4.855333333333333 12.579933333333333 6.321186666666667 12.665333333333333 8 12.665333333333333C9.678999999999998 12.665333333333333 11.144933333333334 12.579933333333333 12.179733333333333 12.496033333333333C12.898733333333332 12.4377 13.456 11.888533333333331 13.520066666666667 11.176033333333333C13.595999999999998 10.331533333333333 13.666666666666666 9.217633333333332 13.666666666666666 7.998666666666666C13.666666666666666 6.779766666666667 13.595999999999998 5.665846666666667 13.520066666666667 4.821366666666666C13.456 4.108866666666666 12.898733333333332 3.55968 12.179733333333333 3.5013666666666663C11.144933333333334 3.417453333333333 9.678999999999998 3.3320333333333334 8 3.3320333333333334zM3.7397666666666667 2.50462C4.794879999999999 2.41906 6.288386666666666 2.3320333333333334 8 2.3320333333333334C9.7118 2.3320333333333334 11.2054 2.4190733333333334 12.260533333333331 2.5046399999999998C13.458733333333331 2.6018133333333333 14.407866666666665 3.5285199999999994 14.516066666666667 4.73182C14.593933333333332 5.597933333333334 14.666666666666666 6.7427 14.666666666666666 7.998666666666666C14.666666666666666 9.2547 14.593933333333332 10.399466666666665 14.516066666666667 11.2656C14.407866666666665 12.468866666666665 13.458733333333331 13.395566666666667 12.260533333333331 13.492766666666665C11.2054 13.578333333333333 9.7118 13.665333333333333 8 13.665333333333333C6.288386666666666 13.665333333333333 4.794879999999999 13.578333333333333 3.7397666666666667 13.492799999999999C2.541373333333333 13.395599999999998 1.5922066666666668 12.468633333333333 1.4840200000000001 11.265266666666665C1.4061199999999998 10.3988 1.3333333333333333 9.253866666666667 1.3333333333333333 7.998666666666666C1.3333333333333333 6.743533333333333 1.4061199999999998 5.598579999999999 1.4840200000000001 4.732153333333333C1.5922066666666668 3.5287466666666667 2.541373333333333 2.601793333333333 3.7397666666666667 2.50462z" fill="currentColor"></path><path d="M9.8092 7.3125C10.338433333333333 7.618066666666666 10.338433333333333 8.382 9.809166666666666 8.687533333333333L7.690799999999999 9.910599999999999C7.161566666666666 10.216133333333332 6.5 9.8342 6.500006666666666 9.223066666666666L6.500006666666666 6.776999999999999C6.500006666666666 6.165873333333334 7.161566666666666 5.783913333333333 7.690799999999999 6.089479999999999L9.8092 7.3125z" fill="currentColor"></path></svg>
  949. <span>${videoData.play < 10000 ? videoData.play : (parseInt(videoData.play) / 10000).toFixed(1) + '万'}</span>
  950. </span>
  951. <span class="danmaku">
  952. <svg class="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16" width="16" height="16" style="width: 16px; height: 16px;"><path d="M8 3.3320333333333334C6.321186666666667 3.3320333333333334 4.855333333333333 3.4174399999999996 3.820593333333333 3.5013466666666666C3.1014733333333333 3.5596599999999996 2.5440733333333334 4.109013333333333 2.48 4.821693333333333C2.4040466666666664 5.666533333333334 2.333333333333333 6.780666666666666 2.333333333333333 7.998666666666666C2.333333333333333 9.216733333333334 2.4040466666666664 10.330866666666665 2.48 11.175699999999999C2.5440733333333334 11.888366666666666 3.1014733333333333 12.437733333333334 3.820593333333333 12.496066666666666C4.855333333333333 12.579933333333333 6.321186666666667 12.665333333333333 8 12.665333333333333C9.678999999999998 12.665333333333333 11.144933333333334 12.579933333333333 12.179733333333333 12.496033333333333C12.898733333333332 12.4377 13.456 11.888533333333331 13.520066666666667 11.176033333333333C13.595999999999998 10.331533333333333 13.666666666666666 9.217633333333332 13.666666666666666 7.998666666666666C13.666666666666666 6.779766666666667 13.595999999999998 5.665846666666667 13.520066666666667 4.821366666666666C13.456 4.108866666666666 12.898733333333332 3.55968 12.179733333333333 3.5013666666666663C11.144933333333334 3.417453333333333 9.678999999999998 3.3320333333333334 8 3.3320333333333334zM3.7397666666666667 2.50462C4.794879999999999 2.41906 6.288386666666666 2.3320333333333334 8 2.3320333333333334C9.7118 2.3320333333333334 11.2054 2.4190733333333334 12.260533333333331 2.5046399999999998C13.458733333333331 2.6018133333333333 14.407866666666665 3.5285199999999994 14.516066666666667 4.73182C14.593933333333332 5.597933333333334 14.666666666666666 6.7427 14.666666666666666 7.998666666666666C14.666666666666666 9.2547 14.593933333333332 10.399466666666665 14.516066666666667 11.2656C14.407866666666665 12.468866666666665 13.458733333333331 13.395566666666667 12.260533333333331 13.492766666666665C11.2054 13.578333333333333 9.7118 13.665333333333333 8 13.665333333333333C6.288386666666666 13.665333333333333 4.794879999999999 13.578333333333333 3.7397666666666667 13.492799999999999C2.541373333333333 13.395599999999998 1.5922066666666668 12.468633333333333 1.4840200000000001 11.265266666666665C1.4061199999999998 10.3988 1.3333333333333333 9.253866666666667 1.3333333333333333 7.998666666666666C1.3333333333333333 6.743533333333333 1.4061199999999998 5.598579999999999 1.4840200000000001 4.732153333333333C1.5922066666666668 3.5287466666666667 2.541373333333333 2.601793333333333 3.7397666666666667 2.50462z" fill="currentColor"></path><path d="M10.583333333333332 7.166666666666666L6.583333333333333 7.166666666666666C6.307193333333332 7.166666666666666 6.083333333333333 6.942799999999999 6.083333333333333 6.666666666666666C6.083333333333333 6.390526666666666 6.307193333333332 6.166666666666666 6.583333333333333 6.166666666666666L10.583333333333332 6.166666666666666C10.859466666666666 6.166666666666666 11.083333333333332 6.390526666666666 11.083333333333332 6.666666666666666C11.083333333333332 6.942799999999999 10.859466666666666 7.166666666666666 10.583333333333332 7.166666666666666z" fill="currentColor"></path><path d="M11.583333333333332 9.833333333333332L7.583333333333333 9.833333333333332C7.3072 9.833333333333332 7.083333333333333 9.609466666666666 7.083333333333333 9.333333333333332C7.083333333333333 9.0572 7.3072 8.833333333333332 7.583333333333333 8.833333333333332L11.583333333333332 8.833333333333332C11.859466666666666 8.833333333333332 12.083333333333332 9.0572 12.083333333333332 9.333333333333332C12.083333333333332 9.609466666666666 11.859466666666666 9.833333333333332 11.583333333333332 9.833333333333332z" fill="currentColor"></path><path d="M5.25 6.666666666666666C5.25 6.942799999999999 5.02614 7.166666666666666 4.75 7.166666666666666L4.416666666666666 7.166666666666666C4.140526666666666 7.166666666666666 3.9166666666666665 6.942799999999999 3.9166666666666665 6.666666666666666C3.9166666666666665 6.390526666666666 4.140526666666666 6.166666666666666 4.416666666666666 6.166666666666666L4.75 6.166666666666666C5.02614 6.166666666666666 5.25 6.390526666666666 5.25 6.666666666666666z" fill="currentColor"></path><path d="M6.25 9.333333333333332C6.25 9.609466666666666 6.02614 9.833333333333332 5.75 9.833333333333332L5.416666666666666 9.833333333333332C5.140526666666666 9.833333333333332 4.916666666666666 9.609466666666666 4.916666666666666 9.333333333333332C4.916666666666666 9.0572 5.140526666666666 8.833333333333332 5.416666666666666 8.833333333333332L5.75 8.833333333333332C6.02614 8.833333333333332 6.25 9.0572 6.25 9.333333333333332z" fill="currentColor"></path></svg>
  953. <span>${videoData.video_review}</span>
  954. </span>
  955. </div>
  956. </div>
  957. </a>
  958. `;
  959. };
  960.  
  961. const addAnchor = (container) => {
  962. const anchorElement = document.createElement('div');
  963. anchorElement.classList.add('anchor-for-loading');
  964. anchorElement.style = `margin-right: 3.2vmin; padding: 10vmin 0; text-align: center; font-size: 0.8rem; color: #61666d;`;
  965. anchorElement.textContent = '正在加载...';
  966. container.appendChild(anchorElement);
  967.  
  968. let paginationCounter = 1;
  969. const ob = new IntersectionObserver(async (entries) => {
  970. if (!entries[0].isIntersecting) return;
  971.  
  972. const newPaginationData = await getPaginationData(++paginationCounter);
  973. if (newPaginationData instanceof Array && newPaginationData.length === 0) {
  974. anchorElement.textContent = '所有投稿已加载完毕';
  975. ob.disconnect();
  976. return;
  977. }
  978.  
  979. for (const videoData of newPaginationData) {
  980. anchorElement.insertAdjacentHTML('beforebegin', getVideoItem(videoData));
  981. }
  982. });
  983.  
  984. ob.observe(anchorElement);
  985. };
  986.  
  987. // get first pagination data
  988. const firstPaginationData = await getPaginationData(1);
  989.  
  990. // return if there are no video archives
  991. if (firstPaginationData instanceof Array && firstPaginationData.length === 0) return;
  992.  
  993. // clear archive list, then add video item
  994. archiveList.innerHTML = '';
  995. for (const videoData of firstPaginationData) {
  996. archiveList.insertAdjacentHTML('beforeend', getVideoItem(videoData));
  997. }
  998.  
  999. // add anchor to the bottom
  1000. addAnchor(archiveList);
  1001. }
  1002. }, 100);
  1003. }
  1004.  
  1005. async function modifyDynamicPage() {
  1006. // if <m-open-app> still exists 1 second after opening the page, reload the page
  1007. const noMoreOpenAppPromise = new Promise(resolve => {
  1008. setTimeout(() => {
  1009. if (document.querySelector('m-open-app')) window.location.reload();
  1010. else resolve();
  1011. }, 1000);
  1012. });
  1013.  
  1014. // get cleaned dynamic card
  1015. const dynamicCard = await new Promise(resolve => {
  1016. const timer = setInterval(() => {
  1017. const dynamicCardWrapper = document.querySelector('m-open-app.card-wrap');
  1018. if (dynamicCardWrapper) {
  1019. clearInterval(timer);
  1020. const dynamicCard = dynamicCardWrapper.querySelector('.dyn-card');
  1021. dynamicCardWrapper.insertAdjacentElement('beforebegin', dynamicCard);
  1022. dynamicCardWrapper.remove();
  1023. resolve(dynamicCard);
  1024. }
  1025. }, 100);
  1026. });
  1027.  
  1028. // setup click event on author avatar and name
  1029. const spaceURL = dynamicCard.querySelector('.dyn-header').dataset.url;
  1030. dynamicCard.querySelector('.dyn-header .dyn-header__author__avatar').onclick = () => window.location.href = spaceURL;
  1031. dynamicCard.querySelector('.dyn-header .dyn-header__author__name').onclick = () => window.location.href = spaceURL;
  1032.  
  1033. // setup click event on topic(original or referenced)
  1034. dynamicCard.querySelectorAll('.dyn-content .bili-dyn-topic').forEach(item => {
  1035. item.onclick = () => window.location.href = item.dataset.url;
  1036. });
  1037.  
  1038. // setup click event on archive(original or referenced)
  1039. dynamicCard.querySelectorAll('.dyn-content .dyn-archive').forEach(item => {
  1040. item.onclick = () => window.location.href = `https://m.bilibili.com/video/${item.dataset.oid}`;
  1041. });
  1042.  
  1043. // setup click event on rich-text-topic
  1044. dynamicCard.querySelectorAll('.dyn-content .bili-richtext .bili-rich-text-topic').forEach(item => {
  1045. item.onclick = () => window.location.href = item.dataset.url;
  1046. });
  1047.  
  1048. // setup click event on rich-text-at
  1049. dynamicCard.querySelectorAll('.dyn-content .bili-richtext .bili-rich-text-module.at').forEach(item => {
  1050. item.onclick = () => window.location.href = `https://m.bilibili.com/space/${item.dataset.oid}`;
  1051. });
  1052.  
  1053. // setup click event on rich-text-link
  1054. dynamicCard.querySelectorAll('.dyn-content .bili-richtext .bili-rich-text-link:not(.viewpic)').forEach(item => {
  1055. item.onclick = () => window.location.href = item.dataset.url;
  1056. });
  1057.  
  1058. // setup click event on rich-text-link for viewing image
  1059. const viewPicElements = dynamicCard.querySelectorAll('.dyn-content .bili-richtext .bili-rich-text-link.viewpic');
  1060. if (viewPicElements.length > 0) {
  1061. let code, dynamicDetail;
  1062. const dynamicID = window.location.pathname.replace('/dynamic/', '');
  1063. const parseResult = JSON.parse(window.localStorage.getItem('dynamicDetail'));
  1064. if (parseResult && parseResult.data.item.id_str === dynamicID) {
  1065. code = parseResult.code;
  1066. dynamicDetail = parseResult.data;
  1067. } else {
  1068. const fetchResult = await fetch(`https://api.bilibili.com/x/polymer/web-dynamic/v1/detail?id=${dynamicID}`).then(res => res.json());
  1069. code = fetchResult.code;
  1070. dynamicDetail = fetchResult.data;
  1071. if (code === 0) window.localStorage.setItem('dynamicDetail', JSON.stringify(fetchResult));
  1072. }
  1073.  
  1074. if (code === 0) {
  1075. const viewPicNodes = [].concat(
  1076. dynamicDetail.item.modules?.module_dynamic?.desc?.rich_text_nodes?.filter(node => node.type === 'RICH_TEXT_NODE_TYPE_VIEW_PICTURE') || [],
  1077. dynamicDetail.item.orig?.modules?.module_dynamic?.desc?.rich_text_nodes?.filter(node => node.type === 'RICH_TEXT_NODE_TYPE_VIEW_PICTURE') || [],
  1078. );
  1079. viewPicElements.forEach((viewPicElement, index) => {
  1080. viewPicElement.insertAdjacentHTML('beforeend', `<div style="display: none;">${viewPicNodes[index].pics.map(pic => `<img src=${pic.src} />`).join('')}</div>`);
  1081. const viewerInstance = new Viewer(viewPicElement, { title: false, toolbar: false, tooltip: false, keyboard: false });
  1082. viewPicElement.onclick = () => viewerInstance.show();
  1083. });
  1084. }
  1085. }
  1086.  
  1087. // setup image viewer on pics-block
  1088. dynamicCard.querySelectorAll('.dyn-content .bm-pics-block').forEach(item => {
  1089. new Viewer(item, { title: false, toolbar: false, tooltip: false, keyboard: false, url: (image) => image.src.slice(0, image.src.lastIndexOf('@')) });
  1090. });
  1091.  
  1092. // setup click event on goods card
  1093. dynamicCard.querySelectorAll('.dyn-content .dyn-goods .dyn-goods__card > [data-url]').forEach(item => {
  1094. item.onclick = () => window.location.href = item.dataset.url;
  1095. });
  1096.  
  1097. // setup click event on goods list item
  1098. dynamicCard.querySelectorAll('.dyn-content .dyn-goods .dyn-goods__card .dyn-goods__list__item > img').forEach(item => {
  1099. item.onclick = () => window.location.href = item.dataset.url;
  1100. });
  1101.  
  1102. // setup click event on live card
  1103. dynamicCard.querySelectorAll('.dyn-content .dyn-live__card').forEach(item => {
  1104. item.onclick = () => window.location.href = item.dataset.url;
  1105. });
  1106.  
  1107. // setup click event on additional common card
  1108. dynamicCard.querySelectorAll('.dyn-content .dyn-add-common').forEach(item => {
  1109. item.onclick = () => window.location.href = item.dataset.url;
  1110. });
  1111.  
  1112. // setup click event on reference author
  1113. const referenceAuthor = dynamicCard.querySelector('.dyn-content .reference .dyn-orig-author');
  1114. if (referenceAuthor) {
  1115. referenceAuthor.querySelector('.dyn-orig-author__following').remove();
  1116. referenceAuthor.onclick = () => window.location.href = referenceAuthor.dataset.url;
  1117. }
  1118.  
  1119. // setup click event on reference article
  1120. const referenceArticle = dynamicCard.querySelector('.dyn-content .reference .dyn-article');
  1121. if (referenceArticle) {
  1122. referenceArticle.onclick = async () => {
  1123. let code, dynamicDetail;
  1124. const dynamicID = window.location.pathname.replace('/dynamic/', '');
  1125. const parseResult = JSON.parse(window.localStorage.getItem('dynamicDetail'));
  1126. if (parseResult && parseResult.data.item.id_str === dynamicID) {
  1127. code = parseResult.code;
  1128. dynamicDetail = parseResult.data;
  1129. } else {
  1130. const fetchResult = await fetch(`https://api.bilibili.com/x/polymer/web-dynamic/v1/detail?id=${dynamicID}`).then(res => res.json());
  1131. code = fetchResult.code;
  1132. dynamicDetail = fetchResult.data;
  1133. if (code === 0) window.localStorage.setItem('dynamicDetail', JSON.stringify(fetchResult));
  1134. }
  1135.  
  1136. if (code === 0) {
  1137. const jump_url = dynamicDetail.item.orig?.modules?.module_dynamic?.major?.article?.jump_url;
  1138. if (jump_url) window.location.href = jump_url;
  1139. }
  1140. }
  1141. }
  1142.  
  1143. // setup comment module
  1144. const timer4CommentModule = setInterval(() => {
  1145. const wrapper = document.querySelector('.m-dynamic > .v-switcher');
  1146. if (wrapper) {
  1147. clearInterval(timer4CommentModule);
  1148. wrapper.className = 'comment-module-wrapper';
  1149. wrapper.style = `background-color: #fff; overflow-x: hidden;`;
  1150. wrapper.innerHTML = '';
  1151. noMoreOpenAppPromise.then(_ => MobileCommentModule.init(wrapper));
  1152. }
  1153. }, 100);
  1154. }
  1155.  
  1156. async function modifyOpusPage() {
  1157. // if <m-open-app> still exists 1 second after opening the page, reload the page
  1158. const noMoreOpenAppPromise = new Promise(resolve => {
  1159. setTimeout(() => {
  1160. if (document.querySelector('m-open-app')) window.location.reload();
  1161. else resolve();
  1162. }, 1000);
  1163. });
  1164.  
  1165. // get data of opus modules
  1166. const opusModules = await new Promise(resolve => {
  1167. const timer = setInterval(() => {
  1168. const opusModules = window.__INITIAL_STATE__?.opus?.detail?.modules;
  1169. if (opusModules instanceof Array && opusModules.length !== 0) {
  1170. clearInterval(timer);
  1171. resolve(opusModules);
  1172. }
  1173. }, 100);
  1174. });
  1175.  
  1176. // remove opus content read more limit
  1177. const timer4OpusReadMoreLimit = setInterval(() => {
  1178. document.querySelectorAll('.opus-module-content.limit').forEach(item => item.classList.remove('limit'));
  1179. document.querySelectorAll('.opus-read-more').forEach(item => item.remove());
  1180. }, 100);
  1181. setTimeout(() => clearInterval(timer4OpusReadMoreLimit), 3 * 1000);
  1182.  
  1183. // setup image viewer on top album
  1184. document.querySelectorAll('.opus-module-top .opus-module-top__album').forEach(item => {
  1185. let previousImageAmount = item.querySelectorAll('.v-swipe__item img').length;
  1186. const viewerInstance = new Viewer(item, { title: false, toolbar: false, tooltip: false, keyboard: false, url: (image) => image.src.slice(0, image.src.lastIndexOf('@')) });
  1187. setInterval(() => {
  1188. const currentImageAmount = item.querySelectorAll('.v-swipe__item img').length;
  1189. if (currentImageAmount > previousImageAmount) {
  1190. previousImageAmount = currentImageAmount;
  1191. viewerInstance.update();
  1192. }
  1193. }, 500);
  1194. });
  1195.  
  1196. // get cleaned author avatar and name
  1197. const wrappedAuthorAvatar = document.querySelector('m-open-app.opus-module-author__avatar');
  1198. const wrappedAuthorName = document.querySelector('m-open-app.opus-module-author__name');
  1199. wrappedAuthorAvatar.outerHTML = wrappedAuthorAvatar.outerHTML.replace('<m-open-app', '<div').replace('</m-open-app', '</div');
  1200. wrappedAuthorName.outerHTML = wrappedAuthorName.outerHTML.replace('<m-open-app', '<div').replace('</m-open-app', '</div');
  1201. const authorAvatar = document.querySelector('.opus-module-author__avatar');
  1202. authorAvatar.querySelector('.sleepy')?.classList.remove('sleepy');
  1203. const authorName = document.querySelector('.opus-module-author__name');
  1204.  
  1205. // setup click event on author avatar and name
  1206. const authorData = opusModules.find(module => module.module_type === 'MODULE_TYPE_AUTHOR')?.module_author;
  1207. authorAvatar.onclick = () => window.location.href = authorData.jump_url;
  1208. authorName.onclick = () => window.location.href = authorData.jump_url;
  1209.  
  1210. // clean and setup click event on topic
  1211. const timer4Topic = setInterval(() => {
  1212. const wrapper = document.querySelector('m-open-app.opus-module-topic');
  1213. if (wrapper) {
  1214. wrapper.outerHTML = wrapper.outerHTML.replace('<m-open-app', '<span').replace('</m-open-app', '</span');
  1215. const jump_url = opusModules.find(module => module.module_type === 'MODULE_TYPE_TOPIC')?.module_topic?.jump_url;
  1216. if (jump_url) document.querySelector('.opus-module-topic').onclick = () => window.location.href = jump_url;
  1217. }
  1218. }, 100);
  1219. setTimeout(() => clearInterval(timer4Topic), 3 * 1000);
  1220.  
  1221. // clean and setup click event on rich-text-at
  1222. document.querySelectorAll('m-open-app > .opus-text-rich-hl.at').forEach(item => {
  1223. const wrapper = item.parentElement;
  1224. wrapper.insertAdjacentElement('beforebegin', item);
  1225. wrapper.remove();
  1226. const rid = opusModules.find(module => module.module_type === 'MODULE_TYPE_CONTENT')?.module_content?.paragraphs.find(paragraph => paragraph.para_type === 1)?.text?.nodes.find(node => node.type === 'TEXT_NODE_TYPE_RICH' && node?.rich?.type === 'RICH_TEXT_NODE_TYPE_AT' && node?.rich?.text === item.textContent)?.rich?.rid;
  1227. if (rid) item.onclick = () => window.location.href = `https://m.bilibili.com/space/${rid}`;
  1228. });
  1229.  
  1230. // clean and setup click event on rich-text-topic
  1231. document.querySelectorAll('m-open-app > .opus-text-rich-hl.topic').forEach((item, index) => {
  1232. const wrapper = item.parentElement;
  1233. wrapper.insertAdjacentElement('beforebegin', item);
  1234. wrapper.remove();
  1235. const jump_url = opusModules.find(module => module.module_type === 'MODULE_TYPE_CONTENT')?.module_content?.paragraphs.find(paragraph => paragraph.para_type === 1)?.text?.nodes.filter(node => node.type === 'TEXT_NODE_TYPE_RICH' && node?.rich?.type === 'RICH_TEXT_NODE_TYPE_TOPIC').at(index)?.rich?.jump_url;
  1236. if (jump_url) item.onclick = () => window.location.href = jump_url;
  1237. });
  1238.  
  1239. // clean and setup click event on rich-text-link
  1240. document.querySelectorAll('m-open-app > .opus-text-rich-hl.link').forEach((item, index) => {
  1241. const wrapper = item.parentElement;
  1242. wrapper.insertAdjacentElement('beforebegin', item);
  1243. wrapper.remove();
  1244. const jump_url = opusModules.find(module => module.module_type === 'MODULE_TYPE_CONTENT')?.module_content?.paragraphs.find(paragraph => paragraph.para_type === 1)?.text?.nodes.filter(node => node.type === 'TEXT_NODE_TYPE_RICH' && node?.rich?.type === 'RICH_TEXT_NODE_TYPE_WEB').at(index)?.rich?.jump_url;
  1245. if (jump_url) item.onclick = () => window.location.href = jump_url;
  1246. });
  1247.  
  1248. // clean and setup click event on rich-text-goods
  1249. document.querySelectorAll('m-open-app > .opus-text-rich-hl.goods').forEach((item, index) => {
  1250. const wrapper = item.parentElement;
  1251. wrapper.insertAdjacentElement('beforebegin', item);
  1252. wrapper.remove();
  1253. const jump_url = opusModules.find(module => module.module_type === 'MODULE_TYPE_CONTENT')?.module_content?.paragraphs.find(paragraph => paragraph.para_type === 1)?.text?.nodes.filter(node => node.type === 'TEXT_NODE_TYPE_RICH' && node?.rich?.type === 'RICH_TEXT_NODE_TYPE_GOODS').at(index)?.rich?.jump_url;
  1254. if (jump_url) item.onclick = () => window.location.href = jump_url;
  1255. });
  1256.  
  1257. // clean rich-text-lottery
  1258. document.querySelectorAll('m-open-app > .opus-text-rich-hl.lottery').forEach((item, index) => {
  1259. const wrapper = item.parentElement;
  1260. wrapper.insertAdjacentElement('beforebegin', item);
  1261. wrapper.remove();
  1262. });
  1263.  
  1264. // setup image viewer on pics-block
  1265. document.querySelectorAll('.opus-module-content .bm-pics-block').forEach(item => {
  1266. const wrapper = item.parentElement;
  1267. if (wrapper.tagName === 'M-OPEN-APP') {
  1268. wrapper.insertAdjacentElement('beforebegin', item);
  1269. wrapper.remove();
  1270. }
  1271. new Viewer(item, { title: false, toolbar: false, tooltip: false, keyboard: false, url: (image) => image.src.slice(0, image.src.lastIndexOf('@')) });
  1272. });
  1273.  
  1274. // clean reserve card and setup click event
  1275. const timer4ReserveCard = setInterval(() => {
  1276. const reserveCard = document.querySelector('m-open-app > .bm-link-card-reserve');
  1277. if (reserveCard) {
  1278. const wrapper = reserveCard.parentElement;
  1279. wrapper.insertAdjacentElement('beforebegin', reserveCard);
  1280. wrapper.remove();
  1281. const jump_url = opusModules.find(module => module.module_type === 'MODULE_TYPE_CONTENT')?.module_content?.paragraphs.find(paragraph => paragraph.para_type === 6)?.link_card?.card?.reserve?.jump_url;
  1282. if (jump_url) reserveCard.onclick = () => window.location.href = jump_url;
  1283. }
  1284. }, 100);
  1285. setTimeout(() => clearInterval(timer4ReserveCard), 3 * 1000);
  1286.  
  1287. // clean goods card and setup click event
  1288. const timer4GoodsCard = setInterval(() => {
  1289. const goodsCard = document.querySelector('m-open-app > .bm-link-card-goods');
  1290. if (goodsCard) {
  1291. const wrapper = goodsCard.parentElement;
  1292. wrapper.insertAdjacentElement('beforebegin', goodsCard);
  1293. wrapper.remove();
  1294. goodsCard.querySelectorAll('.bm-link-card-goods__one[data-url]').forEach(item => {
  1295. item.onclick = () => window.location.href = item.dataset.url;
  1296. });
  1297. goodsCard.querySelectorAll('.bm-link-card-goods__list__item img').forEach(item => {
  1298. item.onclick = () => window.location.href = item.dataset.url;
  1299. });
  1300. }
  1301. }, 100);
  1302. setTimeout(() => clearInterval(timer4GoodsCard), 3 * 1000);
  1303.  
  1304. // clean and setup click event on ugc card
  1305. const timer4UgcCard = setInterval(() => {
  1306. const ugcCard = document.querySelector('m-open-app > .bm-link-card-ugc');
  1307. if (ugcCard) {
  1308. const wrapper = ugcCard.parentElement;
  1309. wrapper.insertAdjacentElement('beforebegin', ugcCard);
  1310. wrapper.remove();
  1311. const jump_url = opusModules.find(module => module.module_type === 'MODULE_TYPE_CONTENT')?.module_content?.paragraphs.find(paragraph => paragraph.para_type === 6)?.link_card?.card?.ugc?.jump_url;
  1312. if (jump_url) ugcCard.onclick = () => window.location.href = jump_url;
  1313. }
  1314. }, 100);
  1315. setTimeout(() => clearInterval(timer4UgcCard), 3 * 1000);
  1316.  
  1317. // clean and setup click event on common card
  1318. const timer4CommonCard = setInterval(() => {
  1319. const commonCard = document.querySelector('m-open-app > .bm-link-card-common');
  1320. if (commonCard) {
  1321. const wrapper = commonCard.parentElement;
  1322. wrapper.insertAdjacentElement('beforebegin', commonCard);
  1323. wrapper.remove();
  1324. commonCard.onclick = () => window.location.href = commonCard.dataset.url;
  1325. }
  1326. }, 100);
  1327. setTimeout(() => clearInterval(timer4CommonCard), 3 * 1000);
  1328.  
  1329. // setup comment module
  1330. const timer4CommentModule = setInterval(() => {
  1331. const wrapper = document.querySelector('.m-opus .v-switcher');
  1332. if (wrapper) {
  1333. clearInterval(timer4CommentModule);
  1334. wrapper.className = 'comment-module-wrapper';
  1335. wrapper.style = `background-color: #fff; overflow-x: hidden;`;
  1336. wrapper.innerHTML = '';
  1337. noMoreOpenAppPromise.then(_ => MobileCommentModule.init(wrapper));
  1338. }
  1339. }, 100);
  1340. }
  1341.  
  1342. async function modifyTopicDetailPage() {
  1343. const timer4OpenApp = setInterval(() => {
  1344. document.querySelectorAll('m-open-app').forEach(item => {
  1345. item.outerHTML = item.outerHTML.replace('<m-open-app', '<div').replace('</m-open-app', '</div');
  1346. });
  1347. }, 100);
  1348. setTimeout(() => clearInterval(timer4OpenApp), 3 * 1000);
  1349. }
  1350.  
  1351. async function getWbiQueryString(params) {
  1352. // get origin key
  1353. const { img_url, sub_url } = await fetch('https://api.bilibili.com/x/web-interface/nav').then(res => res.json()).then(json => json.data.wbi_img);
  1354. const imgKey = img_url.slice(img_url.lastIndexOf('/') + 1, img_url.lastIndexOf('.'));
  1355. const subKey = sub_url.slice(sub_url.lastIndexOf('/') + 1, sub_url.lastIndexOf('.'));
  1356. const originKey = imgKey + subKey;
  1357.  
  1358. // get mixin key
  1359. const mixinKeyEncryptTable = [
  1360. 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
  1361. 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
  1362. 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
  1363. 36, 20, 34, 44, 52
  1364. ];
  1365. const mixinKey = mixinKeyEncryptTable.map(n => originKey[n]).join('').slice(0, 32);
  1366.  
  1367. // generate basic query string
  1368. const query = Object
  1369. .keys(params)
  1370. .sort() // sort properties by key
  1371. .map(key => {
  1372. const value = params[key].toString().replace(/[!'()*]/g, ''); // remove characters !'()* in value
  1373. return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
  1374. })
  1375. .join('&');
  1376. // calculate wbi sign
  1377. const wbiSign = SparkMD5.hash(query + mixinKey);
  1378.  
  1379. return query + '&w_rid=' + wbiSign;
  1380. }
  1381.  
  1382. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址