Bilibili - 优化未登录情况下的移动网页端

优化未登录情况下的移动网页端的使用体验 | V0.4 代码重写

// ==UserScript==
// @name         Bilibili - 优化未登录情况下的移动网页端
// @namespace    https://bilibili.com/
// @version      0.4
// @description  优化未登录情况下的移动网页端的使用体验 | V0.4 代码重写
// @license      GPL-3.0
// @author       DD1969
// @match        https://m.bilibili.com/*
// @icon         https://www.bilibili.com/favicon.ico
// @require      https://update.greasyfork.org/scripts/475332/1250588/spark-md5.js
// @require      https://update.greasyfork.org/scripts/510239/1454424/viewer.js
// @require      https://update.greasyfork.org/scripts/524844/1527492/bilibili-mobile-comment-module.js
// @require      https://update.greasyfork.org/scripts/512576/1464552/inject-viewerjs-style.js
// @require      https://update.greasyfork.org/scripts/512574/1464548/inject-bilibili-comment-style.js
// @grant        none
// ==/UserScript==

(async function() {
  'use strict';

  // no need to continue this script if user already logged in
  if (document.cookie.includes('DedeUserID')) return;

  const blacklist = [];

  // regular expressions
  const re = {
    home: /m\.bilibili\.com\/$|m\.bilibili\.com\/channel\/v\/.*/,
    video: /m\.bilibili\.com\/video\/.*/,
    search: /m\.bilibili\.com\/search.*/,
    space: /m\.bilibili\.com\/space\/.*/,
    dynamic: /m\.bilibili\.com\/dynamic\/.*/,
    opus: /m\.bilibili\.com\/opus\/.*/,
    topicDetail: /m\.bilibili\.com\/topic-detail.*/,
  }

  // make sure the document is ready
  await new Promise(resolve => {
    const timer = setInterval(() => {
      if (document.head && document.body) { clearInterval(timer); resolve(); }
    }, 50);
  });

  // search and remove elements constantly
  setupElementCleaner();

  // add style patch
  addStyle();

  // nav bar
  modifyNavBar();
  
  // home page
  if (re.home.test(window.location.href)) modifyHomePage();

  // video page
  if (re.video.test(window.location.href)) modifyVideoPage();

  // search page
  if (re.search.test(window.location.href)) modifySearchPage();

  // space page
  if (re.space.test(window.location.href)) modifySpacePage();

  // dynamic page
  if (re.dynamic.test(window.location.href)) modifyDynamicPage();
  
  // opus page
  if (re.opus.test(window.location.href)) modifyOpusPage();

  // topic detail page
  if (re.topicDetail.test(window.location.href)) modifyTopicDetailPage();

  // ------------ functions below ------------

  function setupElementCleaner() {
    const selectors = [];

    // home page
    if (re.home.test(window.location.href)) {
      selectors.push(...[
        '.m-nav-openapp',     // 右上角的"下载App"按钮
        '.m-navbar .face',    // 右上角的用户头像
        '.fixed-openapp',     // 首页底部的打开App横条
        '.v-card__stats',     // 视频卡片的数据信息
        '.reserve-float-btn', // 首页底部的浮动弹窗
      ]);
    }

    // video page
    if (re.video.test(window.location.href)) {
      selectors.push(...[
        '.m-nav-openapp',                                 // 右上角的"下载App"按钮
        '.m-navbar .face',                                // 右上角的用户头像
        '.video-natural-search .fixed-wrapper',           // 顶部遮挡video元素的整个区域
        '.m-video-related .list-custom-slot .m-open-app', // 相关视频底部的打开App横条
        '.openapp-dialog',                                // 底部弹出的"浏览方式"
        '.caution-dialog',                                // 底部弹出的"友情提示"
        '.play-page-gotop',                               // 回到顶部按钮
        '.reserve-float-btn',                             // 底部的浮动弹窗
        '.gsl-callapp-dom',                               // 视频模块中阻碍点击并唤起app的元素
        'm-open-app.m-video-main-launchapp',              // 从b23.tv打开时出现的打开App横条
        '.m-video-info',                                  // 从b23.tv打开时出现的视频标题、作者和简介
        '.bottom-tab-header',                             // 从b23.tv打开时出现的下方相关视频header
        '.m-video-part',                                  // 从b23.tv打开时出现的视频分P模块#1
        '.m-video-part-panel',                            // 从b23.tv打开时出现的视频分P模块#2
      ]);
    }

    // space page
    if (re.space.test(window.location.href)) {
      selectors.push(...[
        '.fixed-openapp',                   // 底部的打开App按钮
        '.bili-dyn-item-header__following', // 动态右上方的关注按钮
        '.dyn-orig-author__following',      // 转发的动态的作者的关注按钮
      ]);
    }

    // dynamic & opus page
    if (re.dynamic.test(window.location.href) || re.opus.test(window.location.href)) {
      selectors.push(...[
        '.fixed-openapp',                   // 底部的打开App横条
        '.dyn-header__following',           // 动态右上方的关注按钮
        '.dyn-share',                       // 若干分享按钮
        '.openapp-dialog',                  // 底部弹出的"浏览方式"
        '.reserve-float-btn',               // 底部的浮动弹窗
        '.opus-module-author__action',      // opus页右上角的关注按钮
        '.stat-openApp',                    // opus页(原专栏页)底部的stat模块
      ]);
    }

    // topic detail page
    if (re.topicDetail.test(window.location.href)) {
      selectors.push(...[
        '.m-topic-float-openapp', // 底部的打开App按钮
      ]);
    }

    // start cleaning
    setInterval(() => {
      for (const selector of selectors) {
        document.querySelectorAll(selector).forEach(element => element.remove());
      }
    }, 100);
  }

  function addStyle() {
    // nav bar
    const navBarCSS = document.createElement('style');
    navBarCSS.textContent = `
      .m-navbar {
        position: relative !important;
        display: flex !important;
        justify-content: space-between;
        align-items: center;
        background: none !important;
        background-color: #FFFFFF !important;
        border-bottom: 1px solid #EEEEEE;
      }

      .nav-logo {
        display: flex;
        align-items: center;
        height: 100%;
      }

      .nav-logo img {
        height: 60%;
      }

      .nav-search {
        display: flex;
        flex-direction: column;
        justify-content: center;
        width: 200px;
        height: 28px;
        border: 1px solid #EEEEEE;
        border-radius: 16px;
      }

      .nav-search > svg {
        align-self: flex-end;
        margin-right: 8px;
      }
    `;
    document.head.appendChild(navBarCSS);

    // video page
    const videoPageCSS = document.createElement('style');
    videoPageCSS.textContent = `
      .video-share-loading-mask {
        position: absolute;
        top: 0;
        left: 0;
        display: none;
        justify-content: center;
        align-items: center;
        width: 100%;
        height: 100%;
        z-index: 1999;
        background-color: #000000;
      }

      .video-share-loading-mask-circle {
        width: 30px;
        height: 30px;
        border: 2px solid #000000;
        border-top-color: #ffffff;
        border-right-color: #ffffff;
        border-bottom-color: #ffffff;
        border-radius: 100%;
        animation: circle infinite 0.75s linear;
      }

      @keyframes circle {
        0% {
          transform: rotate(0);
        }
        100% {
          transform: rotate(360deg);
        }
      }

      .m-video-player {
        position: relative !important;
        top: initial !important;
      }

      .video-info {
        display: flex;
        flex-direction: column;
        padding: 20px 12px;
      }

      .video-info-title {
        font-size: 1.2rem;
        word-break: break-all;
      }

      .video-info-author {
        display: flex;
        align-items: center;
        margin-top: 16px;
      }

      .video-info-author-avatar {
        width: 32px;
        height: 32px;
        margin-right: 8px;
        border-radius: 100%;
      }

      .video-info-desc {
        color: #666666;
        font-size: 0.8rem;
        word-break: break-all;
      }

      .m-video-related {
        margin-top: 0 !important;
        padding: 24px 0;
        border-top: 1px dashed #aaa;
        border-bottom: 1px dashed #aaa;
      }

      .card-box {
        justify-content: space-between;
      }

      .card-box > .card {
        width: 49%;
      }

      .card-box .card .label,
      .card-box .card .open-app.weakened,
      .card-box .card .video-card .count {
        display: none !important;
      }

      .card-box .card .title {
        padding-top: 8px;
        padding-bottom: 16px;
        font-size: 0.8rem !important;
        word-break: break-all;
      }

      .gsl-top-return {
        transform: scale(0.5);
      }

      .gsl-buffer-app {
        display: none !important;
      }

      .gsl-control-btn.gsl-control-btn-quality {
        display: none !important;
      }

      .gsl-control-btn.gsl-control-btn-speed {
        display: flex !important;
      }

      .gsl-control-btn.gsl-control-btn-speed .gsl-control-dot {
        display: none !important;
      }

      .playback-rate-setting-panel {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        display: flex;
        justify-content: center;
        align-items: center;
        background-color: rgba(0, 0, 0, 0.5);
        z-index: 999999;
      }

      .playback-rate-option-container {
        width: 240px;
        padding: 8px;
        display: flex;
        flex-direction: column;
        align-items: center;
        background-color: #FFFFFF;
        border-radius: 4px;
        user-select: none;
      }

      .playback-rate-option {
        width: 100%;
        margin-top: 2px;
        padding: 8px 0;
        color: #FFFFFF;
        background-color: #00AEEC;
        border-top: 1px solid #EEEEEE;
        border-radius: 4px;
        text-align: center;
      }

      .episode-container {
        display: flex;
        flex-direction: column;
        background-color: #f1f2f3;
      }

      .episode-container-header {
        display: flex;
        align-items: center;
        padding: 12px;
        padding-bottom: 10px;
      }

      .episode-container-header-count {
        margin-left: 4px;
        font-size: 0.8rem;
        font-family: monospace;
        color: #9499A0;
      }

      .episode-list {
        display: flex;
        flex-direction: column;
        padding: 12px;
        padding-top: 0;
      }

      .episode-list-item {
        display: flex;
        align-items: center;
        height: 36px;
        margin: 2px 0;
        padding: 2px 6px;
      }

      .episode-list-item.is-current-episode {
        background-color: #ffffff;
        color: #00AEEC;
        outline: 1px solid #7bdbfd;
        border-radius: 4px;
      }

      .episode-playing-gif {
        display: inline-block;
        width: 12px;
        height: 12px;
        margin-right: 5px;
        background-image: url('https://i0.hdslb.com/bfs/static/jinkela/playlist-video/asserts/playing.gif');
        background-repeat: no-repeat;
        background-size: 12px 12px;
        background-position: center;
      }

      .episode-list-item-title {
        max-width: 250px;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
        line-height: 36px;
        font-size: 0.8rem;
      }

      .episode-list-item-duration {
        flex-grow: 1;
        text-align: right;
        font-size: 0.8rem;
        font-family: monospace;
        color: #9499A0;
      }

      #video-comment-module-wrapper {
        position: fixed;
        top: 0;
        left: 0;
        z-index: 2000;
        display: none;
        width: 100vw;
        height: 100vh;
        background-color: #fff;
        overflow-x: hidden;
      }

      .close-comment-module-btn {
        position: fixed;
        right: 20px;
        bottom: 20px;
        z-index: 2001;
        display: none;
        justify-content: center;
        align-items: center;
        width: 40px;
        height: 40px;
        color: #fff;
        border-radius: 100%;
        background-color: #00AEEC;
      }

      .open-comment-module-btn {
        display: flex;
        justify-content: center;
        align-items: center;
        margin: 0 12px 20px 12px;
        height: 40px;
        color: #fff;
        border-radius: 4px;
        background-color: #00AEEC;
      }
    `;
    document.head.appendChild(videoPageCSS);

    // space page
    const spacePageCSS = document.createElement('style');
    spacePageCSS.textContent = `
      .m-space-info {
        margin-top: 0 !important;
      }

      .bili-dyn-item {
        border-bottom: 1px solid #e7e7e7;
      }

      .video-item-space {
        box-sizing: border-box;
        margin-right: 3.2vmin;
        padding: 2.4vmin 0;
        height: 24.26667vmin;
        position: relative;
        display: block;
        border-bottom: 1px solid #ddd;
      }

      .video-item-space .cover {
        float: left;
        width: 31.2vmin;
        height: 19.46667vmin;
        position: relative;
        border-radius: 1.06667vmin;
        overflow: hidden;
      }

      .video-item-space .cover .duration {
        padding: 0 0.53333vmin;
        position: absolute;
        right: 1.06667vmin;
        bottom: 1.06667vmin;
        border-radius: 0.53333vmin;
        background: rgba(0,0,0,.5);
        font-size: 3.2vmin;
        color: #fff;
      }

      .video-item-space .info {
        margin-left: 34.4vmin;
        height: 19.46667vmin;
        position: relative;
      }

      .video-item-space .info .title {
        max-height: 9.06667vmin;
        font-size: 3.73333vmin;
        color: #212121;
        line-height: 4.53333vmin;
        overflow: hidden;
        text-overflow: ellipsis;
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
      }

      .video-item-space .info .state {
        display: flex;
        align-items: center;
        position: absolute;
        bottom: 0;
        left: 0;
        right: 0;
        font-size: 2.66667vmin;
        color: #999;
        line-height: 4.53333vmin;
        height: 4.53333vmin;
      }

      .video-item-space .info .state .view,
      .video-item-space .info .state .danmaku {
        display: flex;
        align-items: center;
      }

      .video-item-space .info .state .danmaku {
        margin-left: 7.73333vmin;
      }

      .video-item-space .info .state .icon {
        margin-right: 1.06667vmin;
      }
    `;
    document.head.appendChild(spacePageCSS);

    // opus page
    const opusPageCSS = document.createElement('style');
    opusPageCSS.textContent = `
      .show-read-text {
        max-height: initial !important;
      }
    `;
    document.head.appendChild(opusPageCSS);

    // topic detail page
    const topicDetailPageCSS = document.createElement('style');
    topicDetailPageCSS.textContent = `
      .topic-detail-container {
        top: 0 !important;
      }
    `;
    document.head.appendChild(topicDetailPageCSS);
  }

  async function modifyNavBar() {
    const timer = setInterval(() => {
      const navBarElement = document.querySelector('.m-navbar');
      if (navBarElement) {
        navBarElement.__vue__.$destroy();
        navBarElement.innerHTML = `
          <a class="nav-logo" href="/"><img src="https://i1.hdslb.com/bfs/static/jinkela/long/mstation/logo-bilibili-pink.png" /></a>
          <a class="nav-search" href="/search">
            <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>
          </a>
        `;
        clearInterval(timer);
      }
    }, 100);
  }

  async function modifyHomePage() {
    // modify video card
    setInterval(() => {
      document.querySelectorAll('.card-box .v-card:not(.modified)').forEach(card => {
        // blacklist check
        const cardTitle = card.querySelector('.v-card__title');
        if (blacklist.some(keyword => cardTitle.textContent.includes(keyword))) { card.remove(); return; }

        card.classList.add('modified');
        card.__vue__.$destroy();
        card.querySelector('.sleepy')?.classList.remove('sleepy');
      });
    }, 100);
  }

  async function modifyVideoPage() {
    // show hidden video player
    const videoShare = await new Promise(resolve => {
      const timer = setInterval(() => {
        const videoShare = document.querySelector('.video-share');
        if (videoShare) {
          videoShare.style.display = 'block';
          videoShare.style.position = 'relative';
          clearInterval(timer);
          resolve(videoShare);
        }
      }, 100);
    });

    // add loading mask
    const loadingMask = document.createElement('div');
    loadingMask.classList.add('video-share-loading-mask');
    loadingMask.innerHTML = '<span class="video-share-loading-mask-circle"></span>';
    videoShare.appendChild(loadingMask);

    // show loading mask when video share being clicked, but only once
    const videoShareOnClickHandler = () => {
      videoShare.removeEventListener('click', videoShareOnClickHandler);
      loadingMask.style.display = 'flex';

      // hide the loading mask after the video start playing
      const timer = setInterval(() => {
        const progressBar = videoShare.querySelector('.gsl-ui-progress-bar');
        if (progressBar && progressBar.style.width && progressBar.style.width !== '0%') {
          clearInterval(timer);
          loadingMask.style.display = 'none';
        }
      }, 50);
    }
    videoShare.addEventListener('click', videoShareOnClickHandler);

    // get video info
    const videoID = window.location.pathname.replace('/video/', '');
    let param;
    if (videoID.startsWith('av')) param = `aid=${videoID.replace('av', '')}`;
    if (videoID.startsWith('BV')) param = `bvid=${videoID}`;
    const videoInfo = await fetch(`https://api.bilibili.com/x/web-interface/view?${param}`).then(res => res.json()).then(json => json.data);

    // add info
    const infoContainer = document.createElement('div');
    infoContainer.classList.add('video-info');
    infoContainer.innerHTML = `
      <div class="video-info-title">${videoInfo.title}</div>
      <a class="video-info-author" href="https://m.bilibili.com/space/${videoInfo.owner.mid}">
        <img class="video-info-author-avatar" src="${videoInfo.owner.face}">
        <span class="video-info-author-name">${videoInfo.owner.name}</span>
      </a>
      <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}/g, (match) => `<a style="color: #00AEEC" href="https://m.bilibili.com/video/${match}">${match}</a>`)}</div>
    `;
    document.querySelector('.video-share').insertAdjacentElement('afterend', infoContainer);

    // add comment module wrapper
    const commentModuleWrapper = document.createElement('div');
    commentModuleWrapper.id = 'video-comment-module-wrapper';
    document.body.appendChild(commentModuleWrapper);
    MobileCommentModule.init(commentModuleWrapper);

    // add button to close comment module
    const closeCommentModuleBtn = document.createElement('span');
    closeCommentModuleBtn.classList.add('close-comment-module-btn');
    closeCommentModuleBtn.textContent = '×';
    closeCommentModuleBtn.onclick = function () {
      commentModuleWrapper.style.display = 'none';
      closeCommentModuleBtn.style.display = 'none';
      document.body.style.overflow = 'initial';
    }
    document.body.appendChild(closeCommentModuleBtn);
    
    // add button to open comment module
    const openCommentModuleBtn = document.createElement('div');
    openCommentModuleBtn.classList.add('open-comment-module-btn');
    openCommentModuleBtn.textContent = '查看评论';
    openCommentModuleBtn.onclick = function () {
      commentModuleWrapper.style.display = 'block';
      closeCommentModuleBtn.style.display = 'flex';
      document.body.style.overflow = 'hidden';
    }
    infoContainer.insertAdjacentElement('afterend', openCommentModuleBtn);

    // add episodes
    const partNum = parseInt((new URLSearchParams(window.location.search)).get('p') || '1');
    const episodeContainer = document.createElement('div');
    episodeContainer.classList.add('episode-container');
    if (videoInfo.pages.length > 1) {
      episodeContainer.innerHTML = `
        <div class="episode-container-header">
          <span>视频选集</span>
          <span class="episode-container-header-count">(${partNum}/${videoInfo.pages.length})</span>
        </div>
        <div class="episode-list">
          ${
            videoInfo.pages.map((page, index) => {
              const isCurrentEpisode = partNum === index + 1;
              const second = page.duration % 60;
              const minute = (page.duration - second) / 60 % 60;
              const hour = Math.floor(page.duration / 3600);
              const formattedDuration =  `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}:${String(second).padStart(2, '0')}`;
              return `
                <a class="episode-list-item ${isCurrentEpisode ? 'is-current-episode' : ''}" ${isCurrentEpisode ? '' : `href="${window.location.pathname}?p=${index + 1}"`}>
                  ${isCurrentEpisode ? '<span class="episode-playing-gif"></span>' : ''}
                  <span class="episode-list-item-title">${page.part}</span>
                  <span class="episode-list-item-duration">${formattedDuration}</span>
                </a>
              `;
            }).join('')
          }
        </div>
      `;
    }
    openCommentModuleBtn.insertAdjacentElement('afterend', episodeContainer);

    // move .bottom-tab to where it should be, usually happens when page open by b23.tv short url
    const bottomTab = document.querySelector('.video-share > .bottom-tab');
    if (bottomTab) episodeContainer.insertAdjacentElement('afterend', bottomTab);

    // modify video card
    setInterval(() => {
      // move the video card out of <m-open-app>
      document.querySelectorAll('.card-box > m-open-app').forEach(item => {
        const href = item.getAttribute('universallink').replace('.html?', '');
        item.outerHTML = item.querySelector('.card').outerHTML.replace('class="card"', `class="card" data-href="${href}"`).replaceAll('sleepy', '');
      });

      // setup click event
      document.querySelectorAll('.card-box > .card:not(.modified)').forEach(item => {
        // blacklist check
        const cardTitle = item.querySelector('.title');
        if (blacklist.some(keyword => cardTitle.textContent.includes(keyword))) { item.remove(); return; }

        item.classList.add('modified');
        item.onclick = () => window.location.href = item.dataset.href;
      });
    }, 100);

    // modify playback rate button
    const timer4PlaybackRateBtn = setInterval(() => {
      const playbackRateBtn = document.querySelector('.gsl-control .gsl-control-btn-speed');
      if (playbackRateBtn) {
        playbackRateBtn.onclick = () => {
          const maskElement = document.createElement('div');
          maskElement.classList.add('playback-rate-setting-panel');
          maskElement.innerHTML = `
            <div class="playback-rate-option-container">
              <span style="margin-bottom: 6px; padding: 6px 0;">播放倍速</span>
              <span class="playback-rate-option" data-rate="5">5.0x</span>
              <span class="playback-rate-option" data-rate="3">3.0x</span>
              <span class="playback-rate-option" data-rate="2">2.0x</span>
              <span class="playback-rate-option" data-rate="1.5">1.5x</span>
              <span class="playback-rate-option" data-rate="1">1.0x</span>
              <span class="playback-rate-option" data-rate="0.5">0.5x</span>
            </div>
          `;
    
          maskElement
            .querySelectorAll('.playback-rate-option')
            .forEach(optionElement => optionElement.addEventListener('click', function() {
              const videoElement = document.querySelector('#bilibiliPlayer video');
              if (videoElement) videoElement.playbackRate = parseFloat(this.dataset.rate);
            }));
          
          maskElement.onclick = () => maskElement.remove();
    
          (document.querySelector('#bilibiliPlayer .gsl-area.gsl-wide') || document.body).appendChild(maskElement);
        }

        clearInterval(timer4PlaybackRateBtn);
      }
    }, 100);

    // modify fullscreen button
    const timer4FullscreenBtn = setInterval(() => {
      const fullscreenBtn = document.querySelector('.gsl-btn-fullscreen');
      if (fullscreenBtn) {
        // pre-click, whick needs 2 clicks to enter fullscreen naturally
        fullscreenBtn.click();
        clearInterval(timer4FullscreenBtn);
      }
    }, 100);

    // modify ending panel of video
    const timer4EndingPanel = setInterval(async () => {
      const endingPanel = document.querySelector('.gsl-end.gsl-show .gsl-ending-panel-video');
      if (endingPanel) {
        clearInterval(timer4EndingPanel);

        // clean click events
        endingPanel.innerHTML = `
          <div class="gsl-ending-panel-video">
            <div class="gsl-ending-panel-pic">
              <img />
            </div>
            <div class="gsl-ending-panel-content">
              <div class="gsl-ending-panel-title"></div>
              <div class="gsl-ending-panel-button">点击打开</div>
            </div>
          </div>
        `;

        // get related video data
        const relatedVideoData = await fetch(`https://api.bilibili.com/x/web-interface/archive/related?bvid=${videoInfo.bvid}`).then(res => res.json()).then(json => json.data);

        // start showing
        let currentIndex;
        const currentVideoCover = endingPanel.querySelector('.gsl-ending-panel-pic img');
        const currentVideoTitle = endingPanel.querySelector('.gsl-ending-panel-title');
        const showRelatedVideo = () => {
          currentIndex = Math.floor(Math.random() * relatedVideoData.length);
          currentVideoCover.src = relatedVideoData[currentIndex].pic + '@460w_280h.webp';
          currentVideoTitle.textContent = relatedVideoData[currentIndex].title;
        }
        showRelatedVideo();
        setInterval(showRelatedVideo, 5 * 1000);

        // setup click event
        endingPanel.onclick = () => window.location.href = `https://m.bilibili.com/video/${relatedVideoData[currentIndex].bvid}`;
      }
    }, 100);

    // modify charge video page
    const timer4ChargeMask = setInterval(() => {
      const mask = document.querySelector('.m-video-player m-open-app.charge-mask');
      if (mask) {
        mask.outerHTML = mask.outerHTML.replace('<m-open-app', '<div').replace('</m-open-app>', '</div>');
      }
    }, 100);
    setTimeout(() => clearInterval(timer4ChargeMask), 3 * 1000);
  }

  async function modifySearchPage() {
    // modify cancel button
    const timer4CancelBtn = setInterval(() => {
      const cancelBtn = document.querySelector('.m-search-search-bar .cancel');
      if (cancelBtn) {
        cancelBtn.outerHTML = cancelBtn.outerHTML.replace('class="cancel"', 'class="cancel" href="/"');
        clearInterval(timer4CancelBtn);
      }
    }, 100);

    // modify video card
    setInterval(() => {
      document.querySelectorAll('.card-box .v-card-single:not(.vue-destroyed)').forEach(card => {
        // blacklist check
        const cardTitle = card.querySelector('.info .title');
        const cardAuthor = card.querySelector('.info .author');
        if (blacklist.some(keyword => cardTitle.textContent.includes(keyword) || cardAuthor.textContent.includes(keyword))) { card.remove(); return; }

        const aid = card.dataset.aid;

        // remove card which leading to live streaming
        if (aid === '0') { card.remove(); return; }

        // cancel default actions
        card.classList.add('vue-destroyed');
        card.__vue__.$destroy();

        // setup click event
        card.onclick = () => window.location.href = `https://m.bilibili.com/video/av${aid}`;

        // show covers
        card.querySelector('.sleepy')?.classList.remove('sleepy');
      });
    }, 100);

    // modify user card
    setInterval(() => {
      document.querySelectorAll('.card-box a.m-search-user-item:not(.vue-destroyed)').forEach(card => {
        card.classList.add('vue-destroyed');
        card.__vue__.$destroy();
        card.href = card.href.replace('?from=search', '');
        card.querySelector('.sleepy')?.classList.remove('sleepy');
      });
    }, 100);
  }

  async function modifySpacePage() {
    // setup click event on user avatar if live streaming
    const timer4LiveStreaming = setInterval(() => {
      const face = document.querySelector('.m-space-info .info-main .face:not(.modified):has(.living-wrapper)');
      const liveRoomID = window.__INITIAL_STATE__?.space?.info?.live_room?.roomid;
      if (face && liveRoomID) {
        face.classList.add('modified');
        face.onclick = () => window.location.href = `https://live.bilibili.com/${liveRoomID}`;
      }
    }, 100);
    setTimeout(() => clearInterval(timer4LiveStreaming), 3 * 1000);

    // deactivate follow button
    const timer4FollowBtn = setInterval(() => {
      const followBtn = document.querySelector('.m-space-info .follow-btn');
      if (followBtn) {
        followBtn.__vue__.$destroy();
        clearInterval(timer4FollowBtn);
      }
    }, 100);
    
    // modify dynamic items
    setInterval(() => {
      document.querySelectorAll('.dynamic-list .list-scroll-content-wrap > m-open-app').forEach(item => {
        const dynItem = item.querySelector('.bili-dyn-item');
        dynItem.onclick = () => window.location.href = item.getAttribute('universallink');
        item.insertAdjacentElement('beforebegin', dynItem);
        item.remove();
      });
    }, 100);

    // modify archive list
    const timer4ArchiveList = setInterval(async () => {
      const archiveList = document.querySelector('.archive-list');
      if (archiveList) {
        clearInterval(timer4ArchiveList);

        // get user id
        const mid = window.location.pathname.replace('/space/', '');

        const getPaginationData = async (pn) => {
          const params = { mid, pn, ps: 50, order: 'senddate', wts: Math.floor(Date.now() / 1000) };
          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 || []);
        };

        const getVideoItem = (videoData) => {
          return `
            <a href="/video/${videoData.bvid}" class="video-item-space">
              <div class="cover">
                <div class="bfs-img-wrap">
                  <div class="bfs-img b-img">
                    <img class="b-img__inner" src="${videoData.pic}@468w_292h_1c.webp" alt="${videoData.title}">
                  </div>
                </div>
                <span class="duration">${videoData.length}</span>
              </div>
              <div class="info">
                <h3 class="title">${videoData.title}</h3>
                <div class="state">
                  <span class="view">
                    <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>
                    <span>${videoData.play < 10000 ? videoData.play : (parseInt(videoData.play) / 10000).toFixed(1) + '万'}</span>
                  </span>
                  <span class="danmaku">
                    <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>
                    <span>${videoData.video_review}</span>
                  </span>
                </div>
              </div>
            </a>
          `;
        };

        const addAnchor = (container) => {
          const anchorElement = document.createElement('div');
          anchorElement.classList.add('anchor-for-loading');
          anchorElement.style = `margin-right: 3.2vmin; padding: 10vmin 0; text-align: center; font-size: 0.8rem; color: #61666d;`;
          anchorElement.textContent = '正在加载...';
          container.appendChild(anchorElement);

          let paginationCounter = 1;
          const ob = new IntersectionObserver(async (entries) => {
            if (!entries[0].isIntersecting) return;

            const newPaginationData = await getPaginationData(++paginationCounter);
            if (newPaginationData instanceof Array && newPaginationData.length === 0) {
              anchorElement.textContent = '所有投稿已加载完毕';
              ob.disconnect();
              return;
            }

            for (const videoData of newPaginationData) {
              anchorElement.insertAdjacentHTML('beforebegin', getVideoItem(videoData));
            }
          });

          ob.observe(anchorElement);
        };

        // get first pagination data
        const firstPaginationData = await getPaginationData(1);

        // return if there are no video archives
        if (firstPaginationData instanceof Array && firstPaginationData.length === 0) return;

        // clear archive list, then add video item
        archiveList.innerHTML = '';
        for (const videoData of firstPaginationData) {
          archiveList.insertAdjacentHTML('beforeend', getVideoItem(videoData));
        }

        // add anchor to the bottom
        addAnchor(archiveList);
      }
    }, 100);
  }

  async function modifyDynamicPage() {
    // if <m-open-app> still exists 1 second after opening the page, reload the page
    const noMoreOpenAppPromise = new Promise(resolve => {
      setTimeout(() => {
        if (document.querySelector('m-open-app')) window.location.reload();
        else resolve();
      }, 1000);
    });

    // get cleaned dynamic card
    const dynamicCard = await new Promise(resolve => {
      const timer = setInterval(() => {
        const dynamicCardWrapper = document.querySelector('m-open-app.card-wrap');
        if (dynamicCardWrapper) {
          clearInterval(timer);
          const dynamicCard = dynamicCardWrapper.querySelector('.dyn-card');
          dynamicCardWrapper.insertAdjacentElement('beforebegin', dynamicCard);
          dynamicCardWrapper.remove();
          resolve(dynamicCard);
        }
      }, 100);
    });

    // setup click event on author avatar and name
    const spaceURL = dynamicCard.querySelector('.dyn-header').dataset.url;
    dynamicCard.querySelector('.dyn-header .dyn-header__author__avatar').onclick = () => window.location.href = spaceURL;
    dynamicCard.querySelector('.dyn-header .dyn-header__author__name').onclick = () => window.location.href = spaceURL;

    // setup click event on topic(original or referenced)
    dynamicCard.querySelectorAll('.dyn-content .bili-dyn-topic').forEach(item => {
      item.onclick = () => window.location.href = item.dataset.url;
    });

    // setup click event on archive(original or referenced)
    dynamicCard.querySelectorAll('.dyn-content .dyn-archive').forEach(item => {
      item.onclick = () => window.location.href = `https://m.bilibili.com/video/${item.dataset.oid}`;
    });

    // setup click event on rich-text-topic
    dynamicCard.querySelectorAll('.dyn-content .bili-richtext .bili-rich-text-topic').forEach(item => {
      item.onclick = () => window.location.href = item.dataset.url;
    });

    // setup click event on rich-text-at
    dynamicCard.querySelectorAll('.dyn-content .bili-richtext .bili-rich-text-module.at').forEach(item => {
      item.onclick = () => window.location.href = `https://m.bilibili.com/space/${item.dataset.oid}`;
    });

    // setup click event on rich-text-link
    dynamicCard.querySelectorAll('.dyn-content .bili-richtext .bili-rich-text-link').forEach(item => {
      item.onclick = () => window.location.href = item.dataset.url;
    });

    // setup click event on referenced author
    const referencedAuthor = dynamicCard.querySelector('.dyn-content .reference .dyn-orig-author');
    if (referencedAuthor) {
      referencedAuthor.querySelector('.dyn-orig-author__following').remove();
      referencedAuthor.onclick = () => window.location.href = referencedAuthor.dataset.url;
    }

    // setup image viewer on pics-block
    dynamicCard.querySelectorAll('.dyn-content .bm-pics-block').forEach(item => {
      new Viewer(item, { title: false, toolbar: false, tooltip: false, keyboard: false, url: (image) => image.src.slice(0, image.src.lastIndexOf('@')) });
    });

    // setup click event on goods card
    dynamicCard.querySelectorAll('.dyn-content .dyn-goods .dyn-goods__card > [data-url]').forEach(item => {
      item.onclick = () => window.location.href = item.dataset.url;
    });

    // setup click event on goods list item
    dynamicCard.querySelectorAll('.dyn-content .dyn-goods .dyn-goods__card .dyn-goods__list__item > img').forEach(item => {
      item.onclick = () => window.location.href = item.dataset.url;
    });

    // setup click event on live card
    dynamicCard.querySelectorAll('.dyn-content .dyn-live__card').forEach(item => {
      item.onclick = () => window.location.href = item.dataset.url;
    });

    // setup click event on additional common card
    dynamicCard.querySelectorAll('.dyn-content .dyn-add-common').forEach(item => {
      item.onclick = () => window.location.href = item.dataset.url;
    });

    // setup comment module
    const timer4CommentModule = setInterval(() => {
      const wrapper = document.querySelector('.m-dynamic > .v-switcher');
      if (wrapper) {
        clearInterval(timer4CommentModule);
        wrapper.className = 'comment-module-wrapper';
        wrapper.style = `background-color: #fff; overflow-x: hidden;`;
        wrapper.innerHTML = '';
        noMoreOpenAppPromise.then(_ => MobileCommentModule.init(wrapper));
      }
    }, 100);
  }

  async function modifyOpusPage() {
    // if <m-open-app> still exists 1 second after opening the page, reload the page
    const noMoreOpenAppPromise = new Promise(resolve => {
      setTimeout(() => {
        if (document.querySelector('m-open-app')) window.location.reload();
        else resolve();
      }, 1000);
    });

    // get data of opus modules
    const opusModules = await new Promise(resolve => {
      const timer = setInterval(() => {
        const opusModules = window.__INITIAL_STATE__?.opus?.detail?.modules;
        if (opusModules instanceof Array && opusModules.length !== 0) {
          clearInterval(timer);
          resolve(opusModules);
        }
      }, 100);
    });

    // remove opus content read more limit
    const timer4OpusReadMoreLimit = setInterval(() => {
      document.querySelectorAll('.opus-module-content.limit').forEach(item => item.classList.remove('limit'));
      document.querySelectorAll('.opus-read-more').forEach(item => item.remove());
    }, 100);
    setTimeout(() => clearInterval(timer4OpusReadMoreLimit), 3 * 1000);

    // setup image viewer on top album
    document.querySelectorAll('.opus-module-top .opus-module-top__album').forEach(item => {
      let previousImageAmount = item.querySelectorAll('.v-swipe__item img').length;
      const viewerInstance = new Viewer(item, { title: false, toolbar: false, tooltip: false, keyboard: false, url: (image) => image.src.slice(0, image.src.lastIndexOf('@')) });
      setInterval(() => {
        const currentImageAmount = item.querySelectorAll('.v-swipe__item img').length;
        if (currentImageAmount > previousImageAmount) {
          previousImageAmount = currentImageAmount;
          viewerInstance.update();
        }
      }, 500);
    });

    // get cleaned author avatar and name
    const wrappedAuthorAvatar = document.querySelector('m-open-app.opus-module-author__avatar');
    const wrappedAuthorName = document.querySelector('m-open-app.opus-module-author__name');
    wrappedAuthorAvatar.outerHTML = wrappedAuthorAvatar.outerHTML.replace('<m-open-app', '<div').replace('</m-open-app', '</div');
    wrappedAuthorName.outerHTML = wrappedAuthorName.outerHTML.replace('<m-open-app', '<div').replace('</m-open-app', '</div');
    const authorAvatar = document.querySelector('.opus-module-author__avatar');
    authorAvatar.querySelector('.sleepy')?.classList.remove('sleepy');
    const authorName = document.querySelector('.opus-module-author__name');

    // setup click event on author avatar and name
    const authorData = opusModules.find(module => module.module_type === 'MODULE_TYPE_AUTHOR')?.module_author;
    authorAvatar.onclick = () => window.location.href = authorData.jump_url;
    authorName.onclick = () => window.location.href = authorData.jump_url;

    // clean and setup click event on topic
    const timer4Topic = setInterval(() => {
      const wrapper = document.querySelector('m-open-app.opus-module-topic');
      if (wrapper) {
        wrapper.outerHTML = wrapper.outerHTML.replace('<m-open-app', '<span').replace('</m-open-app', '</span');
        const jump_url = opusModules.find(module => module.module_type === 'MODULE_TYPE_TOPIC')?.module_topic?.jump_url;
        if (jump_url) document.querySelector('.opus-module-topic').onclick = () => window.location.href = jump_url;
      }
    }, 100);
    setTimeout(() => clearInterval(timer4Topic), 3 * 1000);

    // clean and setup click event on rich-text-at
    document.querySelectorAll('m-open-app > .opus-text-rich-hl.at').forEach(item => {
      const wrapper = item.parentElement;
      wrapper.insertAdjacentElement('beforebegin', item);
      wrapper.remove();
      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;
      if (rid) item.onclick = () => window.location.href = `https://m.bilibili.com/space/${rid}`;
    });

    // clean and setup click event on rich-text-topic
    document.querySelectorAll('m-open-app > .opus-text-rich-hl.topic').forEach((item, index) => {
      const wrapper = item.parentElement;
      wrapper.insertAdjacentElement('beforebegin', item);
      wrapper.remove();
      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;
      if (jump_url) item.onclick = () => window.location.href = jump_url;
    });

    // clean and setup click event on rich-text-link
    document.querySelectorAll('m-open-app > .opus-text-rich-hl.link').forEach((item, index) => {
      const wrapper = item.parentElement;
      wrapper.insertAdjacentElement('beforebegin', item);
      wrapper.remove();
      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;
      if (jump_url) item.onclick = () => window.location.href = jump_url;
    });

    // clean and setup click event on rich-text-goods
    document.querySelectorAll('m-open-app > .opus-text-rich-hl.goods').forEach((item, index) => {
      const wrapper = item.parentElement;
      wrapper.insertAdjacentElement('beforebegin', item);
      wrapper.remove();
      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;
      if (jump_url) item.onclick = () => window.location.href = jump_url;
    });

    // clean rich-text-lottery
    document.querySelectorAll('m-open-app > .opus-text-rich-hl.lottery').forEach((item, index) => {
      const wrapper = item.parentElement;
      wrapper.insertAdjacentElement('beforebegin', item);
      wrapper.remove();
    });

    // setup image viewer on pics-block
    document.querySelectorAll('.opus-module-content .bm-pics-block').forEach(item => {
      const wrapper = item.parentElement;
      if (wrapper.tagName === 'M-OPEN-APP') {
        wrapper.insertAdjacentElement('beforebegin', item);
        wrapper.remove();
      }
      new Viewer(item, { title: false, toolbar: false, tooltip: false, keyboard: false, url: (image) => image.src.slice(0, image.src.lastIndexOf('@')) });
    });

    // clean reserve card and setup click event
    const timer4ReserveCard = setInterval(() => {
      const reserveCard = document.querySelector('m-open-app > .bm-link-card-reserve');
      if (reserveCard) {
        const wrapper = reserveCard.parentElement;
        wrapper.insertAdjacentElement('beforebegin', reserveCard);
        wrapper.remove();
        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;
        if (jump_url) reserveCard.onclick = () => window.location.href = jump_url;
      }
    }, 100);
    setTimeout(() => clearInterval(timer4ReserveCard), 3 * 1000);

    // clean goods card and setup click event
    const timer4GoodsCard = setInterval(() => {
      const goodsCard = document.querySelector('m-open-app > .bm-link-card-goods');
      if (goodsCard) {
        const wrapper = goodsCard.parentElement;
        wrapper.insertAdjacentElement('beforebegin', goodsCard);
        wrapper.remove();
        goodsCard.querySelectorAll('.bm-link-card-goods__one[data-url]').forEach(item => {
          item.onclick = () => window.location.href = item.dataset.url;
        });
        goodsCard.querySelectorAll('.bm-link-card-goods__list__item img').forEach(item => {
          item.onclick = () => window.location.href = item.dataset.url;
        });
      }
    }, 100);
    setTimeout(() => clearInterval(timer4GoodsCard), 3 * 1000);

    // clean and setup click event on ugc card
    const timer4UgcCard = setInterval(() => {
      const ugcCard = document.querySelector('m-open-app > .bm-link-card-ugc');
      if (ugcCard) {
        const wrapper = ugcCard.parentElement;
        wrapper.insertAdjacentElement('beforebegin', ugcCard);
        wrapper.remove();
        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;
        if (jump_url) ugcCard.onclick = () => window.location.href = jump_url;
      }
    }, 100);
    setTimeout(() => clearInterval(timer4UgcCard), 3 * 1000);

    // clean and setup click event on common card
    const timer4CommonCard = setInterval(() => {
      const commonCard = document.querySelector('m-open-app > .bm-link-card-common');
      if (commonCard) {
        const wrapper = commonCard.parentElement;
        wrapper.insertAdjacentElement('beforebegin', commonCard);
        wrapper.remove();
        commonCard.onclick = () => window.location.href = commonCard.dataset.url;
      }
    }, 100);
    setTimeout(() => clearInterval(timer4CommonCard), 3 * 1000);

    // setup comment module
    const timer4CommentModule = setInterval(() => {
      const wrapper = document.querySelector('.m-opus .v-switcher');
      if (wrapper) {
        clearInterval(timer4CommentModule);
        wrapper.className = 'comment-module-wrapper';
        wrapper.style = `background-color: #fff; overflow-x: hidden;`;
        wrapper.innerHTML = '';
        noMoreOpenAppPromise.then(_ => MobileCommentModule.init(wrapper));
      }
    }, 100);
  }

  async function modifyTopicDetailPage() {
    const timer4OpenApp = setInterval(() => {
      document.querySelectorAll('m-open-app').forEach(item => {
        item.outerHTML = item.outerHTML.replace('<m-open-app', '<div').replace('</m-open-app', '</div');
      });
    }, 100);
    setTimeout(() => clearInterval(timer4OpenApp), 3 * 1000);
  }

  async function getWbiQueryString(params) {
    // get origin key
    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);
    const imgKey = img_url.slice(img_url.lastIndexOf('/') + 1, img_url.lastIndexOf('.'));
    const subKey = sub_url.slice(sub_url.lastIndexOf('/') + 1, sub_url.lastIndexOf('.'));
    const originKey = imgKey + subKey;

    // get mixin key
    const mixinKeyEncryptTable = [
      46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
      33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
      61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
      36, 20, 34, 44, 52
    ];
    const mixinKey = mixinKeyEncryptTable.map(n => originKey[n]).join('').slice(0, 32);

    // generate basic query string
    const query = Object
      .keys(params)
      .sort() // sort properties by key
      .map(key => {
        const value = params[key].toString().replace(/[!'()*]/g, ''); // remove characters !'()* in value
        return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
      })
      .join('&');
 
    // calculate wbi sign
    const wbiSign = SparkMD5.hash(query + mixinKey);

    return query + '&w_rid=' + wbiSign;
  }

})();