Greasy Fork

Greasy Fork is available in English.

哔哩哔哩(bilibili.com)播放页调整

1.自动定位到播放器(进入播放页,可自动定位到播放器,可设置偏移量及是否在点击主播放器时定位);2.可设置是否自动选择最高画质;3.可设置播放器默认模式;

当前为 2024-01-26 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name              哔哩哔哩(bilibili.com)播放页调整
// @license           GPL-3.0 License
// @namespace         http://greasyfork.icu/zh-CN/scripts/415804-bilibili%E6%92%AD%E6%94%BE%E9%A1%B5%E8%B0%83%E6%95%B4-%E8%87%AA%E7%94%A8
// @version           0.39
// @description       1.自动定位到播放器(进入播放页,可自动定位到播放器,可设置偏移量及是否在点击主播放器时定位);2.可设置是否自动选择最高画质;3.可设置播放器默认模式;
// @author            QIAN
// @match             *://*.bilibili.com/video/*
// @match             *://*.bilibili.com/bangumi/play/*
// @match             *://*.bilibili.com/list/*
// @run-at            document-start
// @require           https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// @require           https://unpkg.com/[email protected]/dist/sweetalert2.min.js
// @resource          swalStyle https://unpkg.com/[email protected]/dist/sweetalert2.min.css
// @grant             GM_setValue
// @grant             GM_getValue
// @grant             GM_registerMenuCommand
// @grant             GM_getResourceText
// @grant             GM.info
// @supportURL        https://github.com/QIUZAIYOU/Bilibili-VideoPage-Adjustment
// @homepageURL       https://github.com/QIUZAIYOU/Bilibili-VideoPage-Adjustment
// @icon              https://www.bilibili.com/favicon.ico?v=1
// ==/UserScript==
$(() => {
  'use strict'
  let {
    currentUrl,
    theMainFunctionRunningTimes,
    thePrepFunctionRunningTimes,
    autoSelectScreenModeTimes,
    autoCancelMuteTimes,
    webfullUnlockTimes,
    insertGoToCommentsButtonTimes,
    autoSelectVideoHighestQualityTimes,
    functionExecutionsTimes = 0,
  } = {
    currentUrl: window.location.href,
    theMainFunctionRunningTimes: 0,
    thePrepFunctionRunningTimes: 0,
    autoSelectScreenModeTimes: 0,
    autoCancelMuteTimes: 0,
    webfullUnlockTimes: 0,
    insertGoToCommentsButtonTimes: 0,
    autoSelectVideoHighestQualityTimes: 0,
  }
  const {
    getValue,
    setValue,
    sleep,
    addStyle,
    historyListener,
    checkBrowserHistory,
    throttle,
    getClientHeight,
    checkElementExistence,
    isDocumentHidden,
    isLogin,
    logger,
    checkPageReadyState,
    pageReload,
    scrollToPlayer
  } = {
    getValue (name) {
      return GM_getValue(name)
    },
    setValue (name, value) {
      GM_setValue(name, value)
    },
    sleep (time) {
      return new Promise(resolve => setTimeout(resolve, time))
    },
    addStyle (id, tag, css) {
      tag = tag || 'style'
      const doc = document
      const styleDom = doc.getElementById(id)
      if (styleDom) return
      const style = doc.createElement(tag)
      style.rel = 'stylesheet'
      style.id = id
      tag === 'style' ? (style.innerHTML = css) : (style.href = css)
      document.head.appendChild(style)
    },
    historyListener () {
      class Dep {
        constructor(name) {
          this.id = new Date()
          this.subs = []
        }
        defined () {
          Dep.watch.add(this)
        }
        notify () {
          this.subs.forEach((e, i) => {
            if (typeof e.update === 'function') {
              try {
                e.update.apply(e)
              } catch (err) {
                console.warr(err)
              }
            }
          })
        }
      }
      Dep.watch = null
      class Watch {
        constructor(name, fn) {
          this.name = name
          this.id = new Date()
          this.callBack = fn
        }
        add (dep) {
          dep.subs.push(this)
        }
        update () {
          var cb = this.callBack
          cb(this.name)
        }
      }
      var addHistoryMethod = (function () {
        var historyDep = new Dep()
        return function (name) {
          if (name === 'historychange') {
            return function (name, fn) {
              var event = new Watch(name, fn)
              Dep.watch = event
              historyDep.defined()
              Dep.watch = null
            }
          } else if (name === 'pushState' || name === 'replaceState') {
            var method = history[name]
            return function () {
              method.apply(history, arguments)
              historyDep.notify()
              // logger.info("访问历史|变化")
            }
          }
        }
      })()
      window.addHistoryListener = addHistoryMethod('historychange')
      history.pushState = addHistoryMethod('pushState')
      history.replaceState = addHistoryMethod('replaceState')
      window.addHistoryListener('history', function () {
        const throttleAutoLocation = throttle(m.autoLocation, 500)
        throttleAutoLocation()
      })
    },
    checkBrowserHistory () {
      window.addEventListener('popstate', () => {
        m.autoLocation()
      })
    },
    throttle (func, delay) {
      let wait = false
      return (...args) => {
        if (wait) {
          return
        }
        func(...args)
        wait = true
        setTimeout(() => {
          wait = false
        }, delay)
      }
    },
    getClientHeight () {
      const bodyHeight = document.body.clientHeight || 0
      const docHeight = document.documentElement.clientHeight || 0
      return bodyHeight < docHeight ? bodyHeight : docHeight
    },
    // 检查指定HTML元素是否存在
    checkElementExistence (selector, maxAttempts, interval) {
      // functionExecutionsTimes += 1
      // const funName = (new Error()).stack.split("\n")[2].trim().split(" ")[1].replace('Object.', '')
      // logger.debug(`(调用:${functionExecutionsTimes}) ${funName} -> ${selector}`)
      return new Promise(resolve => {
        let attempts = 0
        const intervalId = setInterval(() => {
          attempts++
          // logger.debug(`(尝试:${attempts}) -> ${selector}`)
          const element = $(selector)
          if (element.length) {
            clearInterval(intervalId)
            resolve(true)
          } else if (attempts === maxAttempts) {
            clearInterval(intervalId)
            resolve(false)
          }
        }, interval)
      })
    },
    isDocumentHidden () {
      const visibilityChangeEventNames = ['visibilitychange', 'mozvisibilitychange', 'webkitvisibilitychange', 'msvisibilitychange']
      const documentHiddenPropertyName = visibilityChangeEventNames.find(name => name in document) || 'onfocusin' in document || 'onpageshow' in window ? 'hidden' : null
      if (documentHiddenPropertyName !== null) {
        const isHidden = () => document[documentHiddenPropertyName]
        const onChange = () => isHidden()
        // 添加各种事件监听器
        visibilityChangeEventNames.forEach(eventName => document.addEventListener(eventName, onChange))
        window.addEventListener('focus', onChange)
        window.addEventListener('blur', onChange)
        window.addEventListener('pageshow', onChange)
        window.addEventListener('pagehide', onChange)
        document.onfocusin = document.onfocusout = onChange
        return isHidden()
      }
      // 如果无法判断是否隐藏,则返回undefined
      return undefined
    },
    isLogin () {
      return Boolean(document.cookie.replace(new RegExp(String.raw`(?:(?:^|.*;\s*)bili_jct\s*=\s*([^;]*).*$)|^.*$`), '$1') || null)
    },
    logger: {
      info (content) {
        console.info('%c播放页调整', 'color:white;background:#006aff;padding:2px;border-radius:2px', content)
      },
      warn (content) {
        console.warn('%c播放页调整', 'color:white;background:#ff6d00;padding:2px;border-radius:2px', content)
      },
      error (content) {
        console.error('%c播放页调整', 'color:white;background:#f33;padding:2px;border-radius:2px', content)
      },
      debug (content) {
        console.info('%c播放页调整(调试)', 'color:white;background:#cc00ff;padding:2px;border-radius:2px', content)
      },
    },
    checkPageReadyState (state) {
      return new Promise((resolve) => {
        const timer = setInterval(() => {
          if (document.readyState === state) {
            clearInterval(timer)
            resolve(true)
          }
        }, 100)
      })
    },
    pageReload () {
      if (auto_reload) location.reload(true)
    },
    scrollToPlayer (offset) {
      $('html,body').scrollTop(offset)
    }
  }
  const {
    is_vip,
    player_type,
    offset_top,
    auto_locate,
    auto_locate_video,
    auto_locate_bangumi,
    click_player_auto_locate,
    player_offset_top,
    current_screen_mode,
    selected_screen_mode,
    auto_select_video_highest_quality,
    contain_quality_4k,
    contain_quality_8k,
    webfull_unlock,
    auto_reload
  } = {
    is_vip: getValue('is_vip'),
    player_type: getValue('player_type'),
    offset_top: Math.trunc(getValue('offset_top')),
    auto_locate: getValue('auto_locate'),
    auto_locate_video: getValue('auto_locate_video'),
    auto_locate_bangumi: getValue('auto_locate_bangumi'),
    click_player_auto_locate: getValue('click_player_auto_locate'),
    player_offset_top: Math.trunc(getValue('player_offset_top')),
    current_screen_mode: getValue('current_screen_mode'),
    selected_screen_mode: getValue('selected_screen_mode'),
    auto_select_video_highest_quality: getValue('auto_select_video_highest_quality'),
    contain_quality_4k: getValue('contain_quality_4k'),
    contain_quality_8k: getValue('contain_quality_8k'),
    webfull_unlock: getValue('webfull_unlock'),
    auto_reload: getValue('auto_reload')
  }
  const m = {
    // 初始化设置参数
    initValue () {
      const value = [{
        name: 'is_vip',
        value: false,
      }, {
        name: 'player_type',
        value: 'video',
      }, {
        name: 'offset_top',
        value: 7,
      }, {
        name: 'player_offset_top',
        value: 160,
      }, {
        name: 'auto_locate',
        value: true,
      }, {
        name: 'auto_locate_video',
        value: true,
      }, {
        name: 'auto_locate_bangumi',
        value: true,
      }, {
        name: 'click_player_auto_locate',
        value: true,
      }, {
        name: 'current_screen_mode',
        value: 'normal',
      }, {
        name: 'selected_screen_mode',
        value: 'wide',
      }, {
        name: 'auto_select_video_highest_quality',
        value: true,
      }, {
        name: 'contain_quality_4k',
        value: false,
      }, {
        name: 'contain_quality_8k',
        value: false,
      }, {
        name: 'webfull_unlock',
        value: false,
      }, {
        name: 'auto_reload',
        value: false,
      }]
      value.forEach(v => {
        if (getValue(v.name) === undefined) {
          setValue(v.name, v.value)
        }
      })
    },
    // 检查视频资源是否加载完毕并处于可播放状态
    async checkVideoCanPlayThrough () {
      const BwpVideoPlayerExists = await checkElementExistence('bwp-video', 10, 10)
      // logger.debug(`bwp-video|${BwpVideoPlayerExists?'存在':'不存在'}`)
      if (BwpVideoPlayerExists) {
        return new Promise(resolve => {
          resolve(true)
        })
      }
      const $video = $('#bilibili-player video')
      const videoReadyState = $video[0].readyState
      // logger.debug(`视频资源|${videoReadyState>=4?'可播放':'不可播放'}`
      if (videoReadyState >= 4) {
        return new Promise(resolve => {
          resolve(true)
        })
      } else {
        return new Promise(resolve => {
          const checkTimeout = setTimeout(() => {
            // logger.error('视频资源|脚本检测失败|重载页面')
            pageReload()
            resolve(false)
          }, 7000)
          $video.on('canplaythrough', () => {
            // logger.info("视频资源加载|成功")
            let attempts = 100
            const timer = setInterval(() => {
              const isHidden = $('#bilibili-player .bpx-player-container').attr('data-ctrl-hidden')
              if (isHidden === 'false') {
                clearInterval(timer)
                clearTimeout(checkTimeout)
                // logger.info(`视频可播放`)
                // logger.info(`控制条|出现(hidden:${isHidden})`)
                resolve(true)
              } else if (attempts <= 0) {
                clearInterval(timer)
                clearTimeout(checkTimeout)
                // logger.error("控制条|检查失败")
                resolve(false)
              }
              // logger.info("控制条|检查中")
              attempts--
            }, 100)
          })
        })
      }
    },
    // 获取当前视频类型(video/bangumi)
    getCurrentPlayerType () {
      const isVideo = currentUrl.includes('www.bilibili.com/video') || currentUrl.includes('www.bilibili.com/list/')
      const isBangumi = currentUrl.includes('www.bilibili.com/bangumi')
      setValue('player_type', isVideo ? 'video' : isBangumi && 'bangumi')
    },
    // 获取当前屏幕模式(normal/wide/web/full)
    async getCurrentScreenMode () {
      const exists = await checkElementExistence('#bilibili-player .bpx-player-container', 10, 100)
      if (exists) {
        const screenMode = $('#bilibili-player .bpx-player-container').attr('data-screen')
        return Promise.resolve(screenMode)
      } else return Promise.resolve(false)
    },
    // 监听屏幕模式变化(normal/wide/web/full)
    watchScreenModeChange () {
      const screenModObserver = new MutationObserver(mutations => {
        const playerDataScreen = $('#bilibili-player .bpx-player-container').attr('data-screen')
        setValue('current_screen_mode', playerDataScreen)
      })
      screenModObserver.observe($('#bilibili-player .bpx-player-container')[0], {
        attributes: true,
        attributeFilter: ['data-screen'],
      })
    },
    // 判断自动切换屏幕模式是否切换成功
    async checkScreenModeSuccess (expect_mode) {
      const current_screen_mode = await this.getCurrentScreenMode()
      const player_data_screen = $('#bilibili-player .bpx-player-container').attr('data-screen')
      const equal = new Set([
        expect_mode,
        selected_screen_mode,
        current_screen_mode,
        player_data_screen,
      ]).size === 1
      return Promise.resolve(equal)
    },
    // 自动选择屏幕模式
    async autoSelectScreenMode () {
      const current_screen_mode = await this.getCurrentScreenMode()
      if (current_screen_mode === 'wide') return { done: true, mode: selected_screen_mode }
      if (current_screen_mode === 'web') return { done: true, mode: selected_screen_mode }
      autoSelectScreenModeTimes++
      if (autoSelectScreenModeTimes === 1) {
        const wideEnterBtn = document.querySelector('.bpx-player-ctrl-wide-enter')
        const webEnterBtn = document.querySelector('.bpx-player-ctrl-web-enter')
        const selectModeBtn = selected_screen_mode === 'wide' ? wideEnterBtn : webEnterBtn
        const expect_mode = selected_screen_mode === 'wide' ? 'wide' : 'web'
        let attempts = 50
        selectModeBtn.click()
        const checkScreenMode = async (expect_mode) => {
          const success = await this.checkScreenModeSuccess(expect_mode)
          if (success) {
            clearInterval(checkScreenModeInterval)
            setValue('current_screen_mode', selected_screen_mode)
            return {
              done: true,
              mode: selected_screen_mode
            }
          } else {
            await sleep(1000)
            selectModeBtn.click()
            logger.warn('自动选择屏幕模式失败正在重试')
            attempts--
            if (attempts === 0) {
              clearInterval(checkScreenModeInterval)
              pageReload()
            }
          }
        }
        let checkScreenModeInterval = setInterval(checkScreenMode, 100, expect_mode)
        return new Promise(resolve => {
          checkScreenMode(expect_mode).then(result => {
            resolve(result)
          })
        })
      }
    },
    // 网页全屏解锁
    fixedWebfullUnlockStyle () {
      webfullUnlockTimes++
      async function resetPlayerLayout () {
        $('body').css({
          'padding-top': 0,
          position: 'auto',
        })
        $('#playerWrap').css('display', 'block')
        $('#bilibili-player').css({
          height: 'auto',
          position: 'unset',
        })
        $('#playerWrap').append($('#bilibili-player'))
        $('.float-nav-exp .mini').css('display', '')
        // 临时设置默认屏幕模式为宽屏用以触发执行自动定位至播放器,定位完后再重新改为网页全屏
        setValue('selected_screen_mode', 'wide')
        const playerDataScreen = await m.getCurrentScreenMode()
        if (playerDataScreen !== 'full') {
          m.autoLocation()
        }
        setValue('selected_screen_mode', 'web')
      }
      if (webfullUnlockTimes === 1) {
        const clientHeight = getClientHeight()
        $('body.webscreen-fix').css({
          'padding-top': clientHeight,
          position: 'unset',
        })
        $('#bilibili-player.mode-webscreen').css({
          height: clientHeight,
          position: 'absolute',
        })
        $('#app').prepend($('#bilibili-player.mode-webscreen'))
        $('#playerWrap').css('display', 'none')
        logger.info('网页全屏解锁|成功')
        setValue('current_screen_mode', 'web')
        this.insertGoToCommentsButton()
        // 退出网页全屏
        $('.bpx-player-ctrl-btn-icon.bpx-player-ctrl-web-leave').click(function () {
          resetPlayerLayout()
        })
        // 再次进入网页全屏
        $('.bpx-player-ctrl-btn-icon.bpx-player-ctrl-web-enter').click(function () {
          $('body').css({
            'padding-top': clientHeight,
            position: 'unset',
          })
          $('#bilibili-player').css({
            height: clientHeight,
            position: 'absolute',
          })
          $('#app').prepend($('#bilibili-player'))
          $('#playerWrap').css('display', 'none')
          $('.float-nav-exp .mini').css('display', 'none')
          $('html,body').scrollTop(0)
        })
        // 进入退出全屏
        $('.bpx-player-ctrl-btn.bpx-player-ctrl-full').click(function () {
          resetPlayerLayout()
        })
        // 进入宽屏
        $('.bpx-player-ctrl-btn-icon.bpx-player-ctrl-wide-enter').click(function () {
          resetPlayerLayout()
        })
      }
    },
    // 插入跳转评论按钮
    insertGoToCommentsButton () {
      insertGoToCommentsButtonTimes++
      if (player_type === 'video' && webfull_unlock && insertGoToCommentsButtonTimes === 1) {
        const goToCommentsBtnHtml = '<div class="bpx-player-ctrl-btn bpx-player-ctrl-comment" role="button" aria-label="前往评论" tabindex="0"><div id="goToComments" class="bpx-player-ctrl-btn-icon"><span class="bpx-common-svg-icon"><svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="88" height="88" preserveAspectRatio="xMidYMid meet" style="width: 100%; height: 100%; transform: translate3d(0px, 0px, 0px);"><path d="M512 85.333c235.637 0 426.667 191.03 426.667 426.667S747.637 938.667 512 938.667a424.779 424.779 0 0 1-219.125-60.502A2786.56 2786.56 0 0 0 272.82 866.4l-104.405 28.48c-23.893 6.507-45.803-15.413-39.285-39.296l28.437-104.288c-11.008-18.688-18.219-31.221-21.803-37.91A424.885 424.885 0 0 1 85.333 512c0-235.637 191.03-426.667 426.667-426.667zm-102.219 549.76a32 32 0 1 0-40.917 49.216A223.179 223.179 0 0 0 512 736c52.97 0 103.19-18.485 143.104-51.67a32 32 0 1 0-40.907-49.215A159.19 159.19 0 0 1 512 672a159.19 159.19 0 0 1-102.219-36.907z" fill="#currentColor"/></svg></span></div></div>'
        $('.bpx-player-control-bottom-right').append(goToCommentsBtnHtml)
        $('#goToComments').on('click', function (event) {
          event.stopPropagation()
          $('body,html').scrollTop($('#comment').offset().top - 10)
          logger.info('到达评论区')
        })
      }
    },
    // 添加返回播放器按钮
    async insertBackToPlayerButton () {
      const playerDataScreen = await this.getCurrentScreenMode()
      if (player_type === 'video') {
        const locateButtonHtml = '<div class="fixed-sidenav-storage-item locate" title="定位至播放器">\n<svg t="1643419779790" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1775" width="200" height="200" style="width: 50%;height: 100%;fill: currentColor;"><path d="M512 352c-88.008 0-160.002 72-160.002 160 0 88.008 71.994 160 160.002 160 88.01 0 159.998-71.992 159.998-160 0-88-71.988-160-159.998-160z m381.876 117.334c-19.21-177.062-162.148-320-339.21-339.198V64h-85.332v66.134c-177.062 19.198-320 162.136-339.208 339.198H64v85.334h66.124c19.208 177.062 162.144 320 339.208 339.208V960h85.332v-66.124c177.062-19.208 320-162.146 339.21-339.208H960v-85.334h-66.124zM512 810.666c-164.274 0-298.668-134.396-298.668-298.666 0-164.272 134.394-298.666 298.668-298.666 164.27 0 298.664 134.396 298.664 298.666S676.27 810.666 512 810.666z" p-id="1776"></path></svg></div>'
        const floatNav = $('.fixed-sidenav-storage .back-to-top-wrap')
        // $('.fixed-sidenav-storage').css('bottom', '274px')
        const dataV = floatNav[0].attributes[1].name
        const locateButtonHtmlDataV = locateButtonHtml.replace('title="定位至播放器"', `title="定位至播放器" ${dataV}`)
        floatNav.prepend(locateButtonHtmlDataV)
        const locateButton = $('.storable-items .fixed-sidenav-storage-item.locate')
        locateButton.not(':first-child').remove()
        floatNav.on('click', '.locate', function () {
          $('html,body').scrollTop(playerDataScreen !== 'web' ? player_offset_top - offset_top : 0)
        })
      }
      if (player_type === 'bangumi') {
        const floatNav = $('[class*="navTools_floatNavExp"] [class*="navTools_navMenu"]')
        const floatNavMenuItemClass = floatNav.children('a').children('div').attr('class').split(' ')[0]
        const locateButtonHtml = `<div class="${floatNavMenuItemClass} locate" style="height:40px;padding:0" title="定位至播放器">\n<svg t="1643419779790" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1775" width="200" height="200" style="width: 50%;height: 100%;fill: currentColor;"><path d="M512 352c-88.008 0-160.002 72-160.002 160 0 88.008 71.994 160 160.002 160 88.01 0 159.998-71.992 159.998-160 0-88-71.988-160-159.998-160z m381.876 117.334c-19.21-177.062-162.148-320-339.21-339.198V64h-85.332v66.134c-177.062 19.198-320 162.136-339.208 339.198H64v85.334h66.124c19.208 177.062 162.144 320 339.208 339.208V960h85.332v-66.124c177.062-19.208 320-162.146 339.21-339.208H960v-85.334h-66.124zM512 810.666c-164.274 0-298.668-134.396-298.668-298.666 0-164.272 134.394-298.666 298.668-298.666 164.27 0 298.664 134.396 298.664 298.666S676.27 810.666 512 810.666z" p-id="1776"></path></svg></div>`
        floatNav.prepend(locateButtonHtml)
        const locateButton = $(`${floatNavMenuItemClass}.locate`)
        locateButton.not(':first-child').remove()
        floatNav.on('click', '.locate', function () {
          $('html,body').scrollTop(playerDataScreen !== 'web' ? player_offset_top - offset_top : 0)
        })
      }
    },
    // 自动定位至播放器
    autoLocation () {
      const $player = $('#bilibili-player')
      const player_offset_top = Math.trunc($player.offset().top)
      setValue('player_offset_top', player_offset_top)
      return new Promise(resolve => {
        const isAutoLocate = auto_locate && ((!auto_locate_video && !auto_locate_bangumi) || (auto_locate_video && player_type === 'video') || (auto_locate_bangumi && player_type === 'bangumi'))

        if (!isAutoLocate || getValue('selected_screen_mode') === 'web') {
          resolve(false)
          // 未开启功能或模式为网页全屏时直接返回,防止代码继续执行进入死循环
          return
        }
        scrollToPlayer(player_offset_top - offset_top)
        const applyAutoLocationInterval = setInterval(() => {
          scrollToPlayer(player_offset_top - offset_top)
          logger.warn(`自动定位失败,继续尝试
                    -----------------
                    当前文档顶部偏移量:${Math.trunc($(document).scrollTop())}
                    期望文档顶部偏移量:${player_offset_top - offset_top}
                    偏移量误差:${(player_offset_top - offset_top) - Math.trunc($(document).scrollTop())}
                    播放器顶部偏移量:${player_offset_top}
                    设置偏移量:${offset_top}`)
        }, 200)
        const checkAutoLocationStatus = setInterval(() => {
          const document_scroll_top = Math.trunc($(document).scrollTop())
          const expect_offset_top = player_offset_top - offset_top
          const offset_deviation = Math.abs(expect_offset_top - document_scroll_top)
          const success = true ? offset_deviation < 5 : false
          if (success) {
            clearInterval(checkAutoLocationStatus)
            clearInterval(applyAutoLocationInterval)
            // logger.info(offset_deviation);
            resolve(true)
          }
        }, 100)
      })
    },
    // 点击播放器自动定位至播放器
    async clickPlayerAutoLocation () {
      if (click_player_auto_locate) {
        const $player = $('#bilibili-player')
        const player_offset_top = Math.trunc($player.offset().top)
        $('#bilibili-player').on('click', handleClick)
        function handleClick (event) {
          event.stopPropagation()
          // logger.info(`1:${player_offset_top}, 2:${offset_top}, 3:${player_offset_top - offset_top}`)
          scrollToPlayer(player_offset_top - offset_top)
        }
      }
    },
    // 点击时间锚点自动返回播放器
    async jumpVideoTime () {
      const clickTarget = player_type === 'video' ? '#comment' : '#comment_module'
      const $clickTarget = $(clickTarget)
      scrollToPlayer(player_offset_top - offset_top)
      $clickTarget.unbind('click').on('click', '.video-time,.video-seek', function (event) {
        event.stopPropagation()
        $('html,body').scrollTop(scrollTop)
        const targetTime = $(this).attr(player_type === 'video' ? 'data-video-time' : 'data-time')
        const video = $('bwp-video')
        video.currentTime = targetTime
        video.play()
      })
    },
    // 自动取消静音
    autoCancelMute () {
      autoCancelMuteTimes++
      const cancelMuteButtn = $('.bpx-player-ctrl-muted-icon')
      const cancelMuteButtnDisplay = cancelMuteButtn.css('display')
      const cancelMuteButtnClass = cancelMuteButtn.attr('class')
      if (autoCancelMuteTimes === 1) {
        if (cancelMuteButtnDisplay === 'block') {
          cancelMuteButtn.click()
          logger.info('已自动取消静音')
        }
      }
    },
    // 自动选择最高画质
    autoSelectVideoHighestQuality () {
      autoSelectVideoHighestQualityTimes++
      if (!auto_select_video_highest_quality) return
      if (autoSelectVideoHighestQualityTimes === 1) {
        let message
        const no4K8K = $('.bpx-player-ctrl-quality ul > li').filter(function () {
          const qualityText = $(this).children('span.bpx-player-ctrl-quality-text').text()
          return (!qualityText.includes('4K') && !qualityText.includes('8K'))
        }).eq(0)
        const yes8K = $('.bpx-player-ctrl-quality ul > li').filter(function () {
          return $(this).children('span.bpx-player-ctrl-quality-text').text().includes('8K')
        }).eq(0)
        const yes4K = $('.bpx-player-ctrl-quality ul > li').filter(function () {
          return $(this).children('span.bpx-player-ctrl-quality-text').text().includes('4K')
        }).eq(0)
        const notVip = $('.bpx-player-ctrl-quality ul > li').eq($('.bpx-player-ctrl-quality ul > li').children('.bpx-player-ctrl-quality-badge-bigvip').length)
        function autoSelectTargetQuality (target) {
          target.length ? target.click() : logger.error('最高画质丨切换失败丨未找到清晰度切换元素')
        }
        if (is_vip) {
          if (!contain_quality_4k && !contain_quality_8k) {
            autoSelectTargetQuality(no4K8K)
            message = '最高画质|VIP|不包含4K及8K|切换成功'
          }
          if (contain_quality_4k && !contain_quality_8k) {
            yes4K.length ? autoSelectTargetQuality(yes4K) : autoSelectTargetQuality(no4K8K)
            message = '最高画质|VIP|4K|切换成功'
          }
          if ((contain_quality_4k && contain_quality_8k) || (!contain_quality_4k && contain_quality_8k)) {
            yes8K.length ? autoSelectTargetQuality(yes8K) : autoSelectTargetQuality(no4K8K)
            message = '最高画质|VIP|8K|切换成功'
          }
        } else {
          autoSelectTargetQuality(notVip)
          message = '最高画质|非VIP|切换成功'
        }
        logger.info(message)
      }
    },
    // 添加样式文件
    addPluginStyle () {
      const style = `
        #playerAdjustment {
          height: 500px;
          overflow: auto;
          overscroll-behavior: contain;
          padding-right: 10px;
        }
        .swal2-popup {
          width: 34em !important;
          padding: 1.25em !important;
        }
        .swal2-html-container {
          margin: 0 !important;
          padding: 16px 5px 0 !important;
          width: 100% !important;
          box-sizing: border-box !important;
        }
        .swal2-footer {
          flex-direction: column !important;
        }
        .swal2-close {
          top: 5px !important;
          right: 3px !important;
        }
        .swal2-actions {
          margin: 7px auto 0 !important;
        }
        .swal2-styled.swal2-confirm {
          background-color: #23ade5 !important;
        }
        .swal2-icon.swal2-info.swal2-icon-show {
          display: none !important;
        }
        .player-adjustment-container, .swal2-container {
          z-index: 999999999 !important;
        }
        .player-adjustment-popup {
          font-size: 14px !important;
        }
        .player-adjustment-setting-label {
          display: flex !important;
          align-items: center !important;
          justify-content: space-between !important;
          padding-top: 10px !important;
        }
        .player-adjustment-setting-checkbox {
          width: 16px !important;
          height: 16px !important;
        }
        .player-adjustment-setting-tips {
          width: 100% !important;
          display: flex !important;
          align-items: center !important;
          padding: 5px !important;
          margin-top: 10px !important;
          background: #f5f5f5 !important;
          box-sizing: border-box !important;
          font-size: 14px !important;
          color: #666 !important;
          border-radius: 2px !important;
          text-align: left !important;
        }
        .player-adjustment-setting-tips svg {
          margin-right: 5px !important;
        }
        label.player-adjustment-setting-label input {
          border: 1px solid #cecece !important;
          background: #fff !important;
        }
        label.player-adjustment-setting-label input[type=checkbox],
        label.player-adjustment-setting-label input[type=radio] {
          width: 16px !important;
          height: 16px !important;
        }
        label.player-adjustment-setting-label input:checked {
          border-color: #1986b3 !important;
          background: #23ade5 !important;
        }
        .auto-quality-sub-options,
        .auto-locate-sub-options {
          display: flex;
          align-items: center;
          padding-left: 15px;
        }
        .auto-quality-sub-options label.player-adjustment-setting-label.fourK,
        .auto-locate-sub-options label.player-adjustment-setting-label.video {
          margin-right: 10px;
        }
        .auto-quality-sub-options .player-adjustment-setting-label input[type="checkbox"] {
          margin-left: 5px !important;
        }
        .player-adjustment-setting-label.screen-mod input {
          margin-right: 5px !important;
        }
        #biliMainHeader {
          height:64px!important;
        }
        #viewbox_report {
          height:106px!important;
          padding-top:24px!important;
        }
        #v_upinfo {
          height:80px!important;
        }
        .members-info-v1 {
          padding-top:0!important;
        }
        .members-info-v1 .wide-members-header {
          height:0!important;
        }
        .members-info-v1 .wide-members-container .up-card .info-tag {
          display:none!important;
        }
        .membersinfo-wide {
          margin-top: -35px!important;
        }
        .fixed-sidenav-storage-item.locate {
          width:40px!important;
          height:40px!important;
        }
        .fixed-header .bili-header__bar{
          position: absolute!important;
          inset-inline: 0!important;
        }
      `
      const addStyleToHead = () => {
        addStyle('swal-pub-style', 'style', GM_getResourceText('swalStyle'))
        addStyle('player-adjustment-style', 'style', style)
      }
      if (document.head) {
        addStyleToHead()
      } else {
        const headObserver = new MutationObserver(() => {
          if (document.head) {
            headObserver.disconnect()
            addStyleToHead()
          }
        })
        headObserver.observe(document.documentElement, {
          childList: true,
          subtree: true,
        })
      }
    },
    // 注册脚本设置控件
    registerMenuCommand () {
      GM_registerMenuCommand('设置', () => {
        const html = `
                <div id="playerAdjustment" style="font-size: 1em;">
                  <label class="player-adjustment-setting-label" style="padding-top:0!important;"> 是否为大会员
                    <input type="checkbox" id="Is-Vip" ${getValue('is_vip') ? 'checked' : ''
          } class="player-adjustment-setting-checkbox">
                  </label>
                  <span class="player-adjustment-setting-tips"> -> 请如实勾选,否则影响自动选择清晰度</span>
                  <label class="player-adjustment-setting-label"> 自动定位至播放器
                    <input type="checkbox" id="Auto-Locate" ${getValue('auto_locate') ? 'checked' : ''
          } class="player-adjustment-setting-checkbox">
                  </label>
                  <div class="auto-locate-sub-options">
                    <label class="player-adjustment-setting-label video"> 普通视频(video)
                      <input type="checkbox" id="Auto-Locate-Video" ${getValue('auto_locate_video') ? 'checked' : ''
          } class="player-adjustment-setting-checkbox">
                    </label>
                    <label class="player-adjustment-setting-label bangumi"> 其他视频(bangumi)
                      <input type="checkbox" id="Auto-Locate-Bangumi" ${getValue('auto_locate_bangumi') ? 'checked' : ''
          } class="player-adjustment-setting-checkbox">
                    </label>
                  </div>
                  <span class="player-adjustment-setting-tips"> -> 只有勾选自动定位至播放器,才会执行自动定位的功能;勾选自动定位至播放器后,video 和 bangumi 两者全选或全不选,默认在这两种类型视频播放页都执行;否则勾选哪种类型,就只在这种类型的播放页才执行。</span>
                  <label class="player-adjustment-setting-label" id="player-adjustment-Range-Wrapper">
                    <span>播放器顶部偏移(px)</span>
                    <input id="Top-Offset" value="${getValue(
            'offset_top'
          )}" style="padding:5px;width: 200px;border: 1px solid #cecece;">
                  </label>
                  <span class="player-adjustment-setting-tips"> -> 播放器距离浏览器窗口默认距离为 ${Math.trunc(
            $('#bilibili-player').offset().top
          )};请填写小于 ${Math.trunc(
            $('#bilibili-player').offset().top
          )} 的正整数或 0;当值为 0 时,播放器上沿将紧贴浏览器窗口上沿、值为 ${Math.trunc(
            $('#bilibili-player').offset().top
          )} 时,将保持B站默认。 </span>
                  <label class="player-adjustment-setting-label"> 点击播放器时定位
                    <input type="checkbox" id="Click-Player-Auto-Location" ${getValue('click_player_auto_locate') ? 'checked' : ''
          } class="player-adjustment-setting-checkbox">
                  </label>
                  <div class="player-adjustment-setting-label screen-mod" style="display: flex;align-items: center;justify-content: space-between;"> 播放器默认模式 <div style="width: 215px;display: flex;align-items: center;justify-content: space-between;">
                      <label class="player-adjustment-setting-label" style="padding-top:0!important;">
                        <input type="radio" name="Screen-Mod" value="close" ${getValue('selected_screen_mode') === 'close'
            ? 'checked'
            : ''
          }>关闭</label>
                      <label class="player-adjustment-setting-label" style="padding-top:0!important;">
                        <input type="radio" name="Screen-Mod" value="wide" ${getValue('selected_screen_mode') === 'wide'
            ? 'checked'
            : ''
          }>宽屏</label>
                      <label class="player-adjustment-setting-label" style="padding-top:0!important;">
                        <input type="radio" name="Screen-Mod" value="web" ${getValue('selected_screen_mode') === 'web'
            ? 'checked'
            : ''
          }>网页全屏</label>
                    </div>
                  </div>
                  <span class="player-adjustment-setting-tips"> -> 若遇到不能自动选择播放器模式可尝试点击重置</span>
                  <label class="player-adjustment-setting-label"> 网页全屏模式解锁
                    <input type="checkbox" id="Webfull-Unlock" ${getValue('webfull_unlock') ? 'checked' : ''
          } class="player-adjustment-setting-checkbox">
                  </label>
                  <span class="player-adjustment-setting-tips"> ->*实验性功能(不稳,可能会有这样或那样的问题):勾选后网页全屏模式下可以滑动滚动条查看下方评论等内容,2秒延迟后解锁(番剧播放页不支持)<br>->新增迷你播放器显示,不过比较简陋,只支持暂停/播放操作,有条件的建议还是直接使用浏览器自带的小窗播放功能。</span>
                  <label class="player-adjustment-setting-label"> 自动选择最高画质
                    <input type="checkbox" id="Auto-Quality" ${getValue('auto_select_video_highest_quality')
            ? 'checked'
            : ''
          } class="player-adjustment-setting-checkbox">
                  </label>
                  <div class="auto-quality-sub-options">
                    <label class="player-adjustment-setting-label fourK"> 是否包含4K画质
                      <input type="checkbox" id="Quality-4K" ${getValue('contain_quality_4k') ? 'checked' : ''
          } class="player-adjustment-setting-checkbox">
                    </label>
                    <label class="player-adjustment-setting-label eightK"> 是否包含8K画质
                      <input type="checkbox" id="Quality-8K" ${getValue('contain_quality_8k') ? 'checked' : ''
          } class="player-adjustment-setting-checkbox">
                    </label>
                  </div>
                  <span class="player-adjustment-setting-tips"> -> 网络条件好时可以启用此项,勾哪项选哪项,都勾选8k,否则选择4k及8k外最高画质。</span>
                  <label class="player-adjustment-setting-label"> 自动刷新
                  <input type="checkbox" id="Auto-Reload" ${getValue('auto_reload') ? 'checked' : ''
          } class="player-adjustment-setting-checkbox">
                </label>
                <span class="player-adjustment-setting-tips"> -> (不建议开启)若脚本执行失败是否自动刷新页面重试,开启后可能会对使用体验起到一定改善作用,但若是因为B站页面改版导致脚本失效,则会陷入页面无限刷新的情况,此时则必须在页面加载时看准时机关闭此项才能恢复正常,请自行选择是否开启。</span>
                </div>
                `
        Swal.fire({
          title: '播放页调整设置',
          html: html,
          icon: 'info',
          showCloseButton: true,
          showDenyButton: true,
          confirmButtonText: '保存',
          denyButtonText: '重置',
          footer: '<div style="text-align: center;">如果发现脚本不能用,可能是播放页更新了,请耐心等待适配。</div><hr style="border: none;height: 1px;margin: 12px 0;background: #eaeaea;"><div style="text-align: center;font-size: 1.25em;"><a href="//userstyles.world/style/241/nightmode-for-bilibili-com" target="_blank">夜间哔哩 - </a><a href="//greasyfork.org/zh-CN/scripts/415804-bilibili%E6%92%AD%E6%94%BE%E9%A1%B5%E8%B0%83%E6%95%B4-%E8%87%AA%E7%94%A8" target="_blank">检查更新</a></div>',
        }).then(res => {
          res.isConfirmed && location.reload(true)
          if (res.isConfirmed) {
            location.reload(true)
          } else if (res.isDenied) {
            setValue('current_screen_mode', 'normal')
            location.reload(true)
          }
        })
        $('#Is-Vip').change(e => {
          setValue('is_vip', e.target.checked)
          $('.fourK,.eightK').css('display', e.target.checked ? 'flex!important' : 'none!important')
        })
        $('#Auto-Locate').change(e => {
          setValue('auto_locate', e.target.checked)
        })
        $('#Auto-Locate-Video').change(e => {
          setValue('auto_locate_video', e.target.checked)
        })
        $('#Auto-Locate-Bangumi').change(e => {
          setValue('auto_locate_bangumi', e.target.checked)
        })
        $('#Top-Offset').change(e => {
          setValue('offset_top', e.target.value * 1)
        })
        $('#Click-Player-Auto-Location').change(e => {
          setValue('click_player_auto_locate', e.target.checked)
        })
        $('#Auto-Quality').change(e => {
          setValue('auto_select_video_highest_quality', e.target.checked)
        })
        $('#Quality-4K').change(e => {
          setValue('contain_quality_4k', e.target.checked)
        })
        $('#Quality-8K').change(e => {
          setValue('contain_quality_8k', e.target.checked)
        })
        $('input[name="Screen-Mod"]').click(function () {
          setValue('selected_screen_mode', $(this).val())
        })
        $('#Webfull-Unlock').change(e => {
          setValue('webfull_unlock', e.target.checked)
        })
        $('#Auto-Reload').change(e => {
          setValue('auto_reload', e.target.checked)
        })
      })
    },
    // 冻结视频标题及UP主信息样式
    freezeHeaderAndVideoTitleStyles () {
      $('#biliMainHeader').attr('style', 'height:64px!important')
      $('#viewbox_report').attr('style', 'height:106px!important;padding-top:24px!important')
      $('#v_upinfo').attr('style', 'height:80px!important')
      $('.members-info-v1').attr('style', 'padding-top:0!important')
      $('.members-info-v1 .wide-members-header').attr('style', 'height:0!important')
      $('.members-info-v1 .wide-members-container .up-card .info-tag').attr('style', 'display:none!important')
    },
    // 判断当前窗口是否在最上方
    isTopWindow () {
      return window.self === window.top
    },
    // 前期准备函数
    thePrepFunction () {
      thePrepFunctionRunningTimes++
      if (thePrepFunctionRunningTimes === 1) {
        isLogin()
        checkBrowserHistory()
        historyListener()
        this.initValue()
        this.addPluginStyle()
        this.isTopWindow() && this.registerMenuCommand()
        this.getCurrentPlayerType()
        this.getCurrentScreenMode()
      }
    },
    // 主函数
    async theMainFunction () {
      try {
        theMainFunctionRunningTimes++
        if (theMainFunctionRunningTimes === 1) {
          const videoPlayerExists = await checkElementExistence('#bilibili-player video', 5, 100) || await checkElementExistence('bwp-video', 5, 100)
          if (videoPlayerExists) {
            logger.info('播放器|存在')
            $('body').css('overflow', 'hidden')
            const isPlayable = await this.checkVideoCanPlayThrough()
            // console.time('播放页调整:判断按钮出现')
            const screenModeBtnExists = await checkElementExistence('#bilibili-player .bpx-player-ctrl-btn', 100, 100)
            // console.timeEnd('播放页调整:判断按钮出现')
            // const pageComplete = await checkPageReadyState('complete')
            if (isPlayable || (!isPlayable && screenModeBtnExists)) {
              logger.info('视频资源|可以播放')
              // console.time('播放页调整:切换模式耗时')
              this.watchScreenModeChange()
              await sleep(100)
              // close 为功能关闭,勿改
              const selectedScreenMode = selected_screen_mode !== 'close' ? await this.autoSelectScreenMode() : 'close'
              // console.timeEnd('播放页调整:切换模式耗时')
              if (selectedScreenMode && selectedScreenMode.done || selectedScreenMode === 'close') {
                if (selectedScreenMode !== 'close') logger.info(`屏幕模式|${selectedScreenMode['mode'].toUpperCase()}|切换成功`)
                this.autoCancelMute()
                // console.time('播放页调整:选择画质耗时')
                this.autoSelectVideoHighestQuality()
                // console.timeEnd('播放页调整:选择画质耗时')
                this.clickPlayerAutoLocation()
                if (webfull_unlock && selectedScreenMode.mode === 'web') {
                  this.fixedWebfullUnlockStyle()
                }
                // console.time('播放页调整:自动定位耗时')
                this.freezeHeaderAndVideoTitleStyles()
                await sleep(500)
                const autoLocationDone = await this.autoLocation()
                // console.timeEnd('播放页调整:自动定位耗时')
                if (auto_locate && autoLocationDone) {
                  $('body').css('overflow', 'auto')
                  logger.info('自动定位|成功')
                  await sleep(100)
                  this.insertBackToPlayerButton()
                  this.jumpVideoTime()
                }
                if (!auto_locate || (auto_locate && auto_locate_bangumi && !auto_locate_video && player_type === 'video') || (auto_locate && auto_locate_video && !auto_locate_bangumi && player_type === 'bangumi')) {
                  $('body').css('overflow', 'auto')
                  logger.info('自动定位|未开启')
                }
                if (player_type === 'video') {
                  const loaded = await checkElementExistence('#comment > .comment > .bili-comment', 10, 100)
                  await sleep(100)
                  if (loaded) {
                    logger.info('页面加载|完毕')
                  } else {
                    pageReload()
                  }
                }
              } else {
                logger.error(`屏幕模式|切换失败|autoSelectScreenMode()`)
                pageReload()
              }
            } else {
              logger.error('视频资源|加载失败')
              pageReload()
            }
          } else {
            logger.error('播放器|不存在')
            pageReload()
          }
        }
      } catch (error) {
        logger.error(error)
        pageReload()
      }
    },
  }
  if (isLogin()) {
    m.thePrepFunction()
    const checkDocumentHidden = setInterval(() => {
      const dicumentHidden = isDocumentHidden()
      if (!dicumentHidden) {
        logger.info('当前标签|已激活|开始应用配置')
        clearInterval(checkDocumentHidden)
        m.theMainFunction()
      } else {
        logger.info('当前标签|未激活|等待激活')
      }
    }, 100)
  } else logger.warn('请登录|本脚本只能在登录状态下使用')
})