Greasy Fork

Greasy Fork is available in English.

B站合集倒序播放

增强B站功能,支持视频合集倒序播放,还有一些其他小功能

当前为 2024-12-16 提交的版本,查看 最新版本

// ==UserScript==
// @name            B站合集倒序播放
// @description     增强B站功能,支持视频合集倒序播放,还有一些其他小功能
// @version         0.0.3
// @author          Grant Howard, Coulomb-G
// @copyright       2024, Grant Howard
// @license         MIT
// @match           *://*.bilibili.com/video/*
// @exclude         *://api.bilibili.com/*
// @exclude         *://api.*.bilibili.com/*
// @exclude         *://*.bilibili.com/api/*
// @exclude         *://member.bilibili.com/studio/bs-editor/*
// @exclude         *://t.bilibili.com/h5/dynamic/specification
// @exclude         *://bbq.bilibili.com/*
// @exclude         *://message.bilibili.com/pages/nav/header_sync
// @exclude         *://s1.hdslb.com/bfs/seed/jinkela/short/cols/iframe.html
// @exclude         *://open-live.bilibili.com/*
// @run-at          document-start
// @grant           unsafeWindow
// @grant           GM_getValue
// @grant           GM_setValue
// @grant           GM_deleteValue
// @grant           GM_info
// @grant           GM_xmlhttpRequest
// @grant           GM_registerMenuCommand
// @grant           GM_unregisterMenuCommand
// @grant           GM_addStyle
// @connect         raw.githubusercontent.com
// @connect         github.com
// @connect         cdn.jsdelivr.net
// @connect         cn.bing.com
// @connect         www.bing.com
// @connect         translate.google.cn
// @connect         translate.google.com
// @connect         localhost
// @connect         *
// @icon            https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/images/logo-small.png
// @icon64          https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/images/logo.png
// @namespace http://greasyfork.icu/users/734541
// ==/UserScript==

;(() => {
  GM_addStyle(`.qmsg.qmsg-wrapper {
    position: fixed;
    top: calc(50vh - (53px));
    left: 0;
    z-index: 1010;
    width: 100%;
    pointer-events: none;
    color: rgba(0, 0, 0, 0.55);
    font-size: 13px;
    font-variant: tabular-nums;
    font-feature-settings: 'tnum';
  }
  .qmsg .qmsg-item {
    padding: 8px;
    text-align: center;
    animation-duration: 0.3s;
  }
  .qmsg .qmsg-item .qmsg-content {
    text-align: left;
    position: relative;
    display: inline-block;
    padding: 10px 12px;
    background: #fff;
    border-radius: 4px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    pointer-events: all;
    max-width: 80%;
    min-width: 80px;
  }
  .qmsg .qmsg-item .qmsg-content [class^='qmsg-content-'] {
    display: flex;
    align-items: center;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .qmsg .qmsg-item .qmsg-content [class^='qmsg-content-'] .qmsg-icon {
    display: inline-block;
    height: 16px;
  }
  .qmsg .qmsg-item .qmsg-content [class^='qmsg-content-'] .qmsg-icon:first-child {
    margin-right: 8px;
  }
  .qmsg .qmsg-item .qmsg-content [class^='qmsg-content-'] .qmsg-icon-close {
    cursor: pointer;
    color: rgba(0, 0, 0, 0.45);
    transition: color 0.3s;
    margin-left: 6px;
  }
  .qmsg .qmsg-item .qmsg-content [class^='qmsg-content-'] .qmsg-icon-close:hover > svg path {
    stroke: #555;
  }
  .qmsg .qmsg-item .qmsg-content [class^='qmsg-content-'] .qmsg-count {
    display: inline-block;
    position: absolute;
    left: -8px;
    top: -8px;
    color: #fff;
    font-size: 12px;
    text-align: center;
    height: 16px;
    line-height: 16px;
    border-radius: 3px;
    min-width: 16px;
    animation-duration: 0.3s;
  }
  .qmsg .qmsg-item .qmsg-content-info {
    color: #909399;
  }
  .qmsg .qmsg-item .qmsg-content-info .qmsg-count {
    background-color: #909399;
  }
  .qmsg .qmsg-item .qmsg-content-warning {
    color: #e6a23c;
  }
  .qmsg .qmsg-item .qmsg-content-warning .qmsg-count {
    background-color: #e6a23c;
  }
  .qmsg .qmsg-item .qmsg-content-error {
    color: #f56c6c;
  }
  .qmsg .qmsg-item .qmsg-content-error .qmsg-count {
    background-color: #f56c6c;
  }
  .qmsg .qmsg-item .qmsg-content-success {
    color: #67c23a;
  }
  .qmsg .qmsg-item .qmsg-content-success .qmsg-count {
    background-color: #67c23a;
  }
  .qmsg .qmsg-item .qmsg-content-loading {
    color: #409eff;
  }
  .qmsg .qmsg-item .qmsg-content-loading .qmsg-count {
    background-color: #409eff;
  }
  .qmsg .animate-turn {
    animation: MessageTurn 1s linear infinite;
  }
  @keyframes MessageTurn {
    0% {
      transform: rotate(0deg);
    }
    25% {
      transform: rotate(90deg);
    }
    50% {
      transform: rotate(180deg);
    }
    75% {
      transform: rotate(270deg);
    }
    100% {
      transform: rotate(360deg);
    }
  }
  @keyframes MessageMoveOut {
    0% {
      max-height: 150px;
      padding: 8px;
      opacity: 1;
    }
    to {
      max-height: 0;
      padding: 0;
      opacity: 0;
    }
  }
  @keyframes MessageMoveIn {
    0% {
      transform: translateY(-100%);
      transform-origin: 0 0;
      opacity: 0;
    }
    to {
      transform: translateY(0);
      transform-origin: 0 0;
      opacity: 1;
    }
  }
  @keyframes MessageShake {
    0%,
    100% {
      transform: translateX(0px);
      opacity: 1;
    }
    25%,
    75% {
      transform: translateX(-4px);
      opacity: 0.75;
    }
    50% {
      transform: translateX(4px);
      opacity: 0.25;
    }
  }`)

  GM_addStyle(`#zaizai-div .video-sections-head_second-line {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        margin: 12px 16px 0;
        color: var(--text3);
        color: var(--text3);
        padding-bottom: 12px;
        font-size: 14px;
        line-height: 16px;
        gap: 10px 20px;
    }

    #zaizai-div .border-bottom-line {
        height: 1px;
        background: var(--line_regular);
        margin: 0 15px;
    }

    #zaizai-div .switch-button {
        margin: 0;
        display: inline-block;
        position: relative;
        width: 30px;
        height: 20px;
        border: 1px solid #ccc;
        outline: none;
        border-radius: 10px;
        box-sizing: border-box;
        background: #ccc;
        cursor: pointer;
        transition: border-color .2s, background-color .2s;
        vertical-align: middle;
    }

    #zaizai-div .switch-button.on:after {
        left: 11px;
    }

    #zaizai-div .switch-button:after {
        content: "";
        position: absolute;
        top: 1px;
        left: 1px;
        border-radius: 100%;
        width: 16px;
        height: 16px;
        background-color: #fff;
        transition: all .2s;
    }

    #zaizai-div .switch-button.on {
        border: 1px solid var(--brand_blue);
        background-color: var(--brand_blue);
    }

    #zaizai-div .txt {
        margin-right: 4px;
        vertical-align: middle;
    }
    `)

  const console = (() => {
    const _console = window.console
    return {
      log: (...args) => {
        _console.log(`%c ZAIZAI `, 'padding: 2px 1px; border-radius: 3px; color: #fff; background: #42c02e; font-weight: bold;', ...args)
      }
    }
  })()

  // 全局变量
  const local = useReactiveLocalStorage({
    defaultreverseorder: false,
    // 开启倒序播放
    startreverseorder: false,
    addsectionslistheigth: false
  })

  let Video = null
  let videoSections = null
  let keyupCodeFn = {}

  function useReactiveLocalStorage(obj) {
    let data = {}
    let zaizaiStore = window.localStorage.getItem('zaizai-store')
    if (zaizaiStore) {
      zaizaiStore = JSON.parse(zaizaiStore)
      for (const key in obj) {
        data[key] = zaizaiStore[key] || obj[key]
      }
    } else {
      data = obj
    }

    let handler = {
      set(target, key, value) {
        let res = Reflect.set(target, key, value)
        try {
          window.localStorage.setItem(`zaizai-store`, JSON.stringify(data))
        } catch (error) {
          console.log('存储失败,请检查浏览器设置', error)
        }
        return res
      },
      get(target, key) {
        let ret = Reflect.get(target, key)
        return typeof ret === 'object' ? new Proxy(ret, handler) : ret
      }
    }
    data = new Proxy(data, handler)
    return data
  }

  function waitTime(callback, options = { time: 500, isSetup: false }) {
    let timeout = null
    return new Promise(resolve => {
      if (options.isSetup) {
        let res = callback()
        if (res) resolve(res)
      }
      timeout = setInterval(() => {
        let res = callback()
        if (res) {
          clearInterval(timeout)
          resolve(res)
        }
      }, options.time)
    })
  }

  async function selectVideo() {
    await waitTime(
      () => {
        let video = document.querySelector('video')
        if (video) {
          Video = video
          return true
        }
      },
      {
        isSetup: true
      }
    )
  }

  async function switchAddsectionslistheigthOnClick(action) {
    if (typeof action !== 'string') {
      local.addsectionslistheigth = !local.addsectionslistheigth
    }
    const ListEl = await waitTime(
      () => {
        return document.querySelector('.video-sections-content-list')
      },
      { isSetup: true }
    )

    if (local.addsectionslistheigth) {
      typeof action !== 'string' && this.classList.add('on')
      ListEl.style.maxHeight = '40vh'
      ListEl.style.height = '40vh'
    } else {
      typeof action !== 'string' && this.classList.remove('on')
      ListEl.style.height = '150px'
      ListEl.style.maxHeight = '150px'
    }
  }

  async function getisReverseorder() {
    const playerloop_checkbox = await waitTime(
      () => {
        let checkbo = document.querySelector(`.bui-switch-input[aria-label="洗脑循环"]`)
        if (checkbo) {
          return checkbo
        }
      },
      { isSetup: true }
    )

    return playerloop_checkbox
  }

  async function bindWatch() {
    const playerloop_checkbox = await getisReverseorder()
    playerloop_checkbox.addEventListener('change', () => {
      if (playerloop_checkbox.checked) {
        local.startreverseorder = false
        document.querySelector('#startreverseorder').classList.remove('on')
        Video.removeEventListener('ended', VideoOnEnded)
      }
    })
  }

  async function VideoOnPlay() {
    // local.startreverseorder &&
    if (!document.querySelector('#zaizai-div')) {
      const div = document.createElement('div')
      div.id = 'zaizai-div'
      let isstartreverseorder = await getisReverseorder()
      div.innerHTML = `
                <div class="video-sections-head">
        <div class="border-bottom-line"></div>
        <div class="video-sections-head_second-line">
            <div>
                <span class="txt">默认开启倒序播放</span>
                <span id="defaultreverseorder" class="switch-button ${local.defaultreverseorder ? 'on' : ''}"></span>
            </div>
            <div>
                <span class="txt">倒序播放</span>
                <span id="startreverseorder" class="switch-button ${isstartreverseorder.checked ? 'on' : ''}"></span>
            </div>
            <div>
                <span class="txt">增高合集列表</span>
                <span id="addsectionslistheigth" class="switch-button ${local.addsectionslistheigth ? 'on' : ''}"></span>
            </div>
        </div>
    </div>
            `

      videoSections.appendChild(div)

      // 默认开启倒序播放
      let defaultreverseorder = document.querySelector('#defaultreverseorder')
      function defaultSwitchClick() {
        local.defaultreverseorder = !local.defaultreverseorder
        if (local.defaultreverseorder) {
          this.classList.add('on')
        } else {
          this.classList.remove('on')
        }
      }
      defaultreverseorder.addEventListener('click', defaultSwitchClick)

      // 倒序播放
      let startreverseorder = document.querySelector('#startreverseorder')
      async function switchReverseoOnClick() {
        const playerloop_checkbox = await getisReverseorder()
        if (playerloop_checkbox.checked) {
          Qmsg && Qmsg.warning('请关闭"洗脑循环"后再开启倒序播放')
          return
        }
        local.startreverseorder = !local.startreverseorder
        if (local.startreverseorder) {
          startreverseorder.classList.add('on')
          Video.addEventListener('ended', VideoOnEnded)
        } else {
          startreverseorder.classList.remove('on')
          Video.removeEventListener('ended', VideoOnEnded)
        }
      }
      startreverseorder.addEventListener('click', switchReverseoOnClick)

      const button = document.querySelector('.video-sections-head_second-line button').cloneNode()
      button.textContent = '滚动到当前播放'
      button.style.width = '100%'
      function scrollToCurrent() {
        let { currentEl } = getCurrentcard()
        const sectionsListEl = document.querySelector('.video-sections-content-list')
        // 42 = currentEl.clientHeight + margin     4 = 列表第一个有4px的margin-top   12是自定义
        let scrollToPosition = currentEl.offsetTop - sectionsListEl.clientHeight / 2 - 42 - 4 - 12
        sectionsListEl.scrollTo({
          top: scrollToPosition
        })
      }
      button.addEventListener('click', scrollToCurrent)
      const newdiv = document.createElement('div')
      newdiv.style.width = '100%'
      newdiv.appendChild(button)
      div.querySelector('.video-sections-head_second-line').appendChild(newdiv)

      let addsectionslistheigth = document.querySelector('#addsectionslistheigth')
      addsectionslistheigth.addEventListener('click', switchAddsectionslistheigthOnClick)
    }
  }

  function getCurrentcard() {
    const episodecards = document.querySelectorAll('.video-episode-card')
    let i = 0
    for (const element of episodecards) {
      let curicon = element.querySelector('.cur-play-icon')
      if (curicon.style.display !== 'none') {
        break
      }
      i++
    }
    // 顺序上一个
    let previous = i - 1 <= 0 ? episodecards.length - 1 : i - 1
    // 顺序下一个
    let next = i + 1 >= episodecards.length - 1 ? episodecards.length - 1 : i + 1
    return {
      elements: episodecards,
      current: i,
      currentEl: episodecards[i],
      next,
      nextEl: episodecards[next],
      previous,
      previousEl: episodecards[previous]
    }
  }

  function VideoOnEnded() {
    /* let curpage = document.querySelector('.cur-page').textContent
            curpage = curpage.match(/\d+/g).at(-1)
            curpage = parseInt(curpage) */
    const { previousEl } = getCurrentcard()
    previousEl.click()
  }

  function keyup_key_g() {
    document.querySelector(`.bpx-player-ctrl-btn[aria-label="网页全屏"]`).click()
  }
  keyupCodeFn['g'] = keyup_key_g

  function keyup_key_h() {
    document.querySelector(`.bpx-player-ctrl-btn[aria-label="画中画"]`).click()
  }
  keyupCodeFn['h'] = keyup_key_h

  async function main() {
    console.log('mian start')

    await waitTime(() => {
      let progress = document.querySelector('.bpx-player-progress-schedule-current')
      if (progress) {
        let transform = progress.style.transform.replace('scaleX(', '').replace(')', '')
        if (transform > 0) {
          videoSections = document.querySelector('.base-video-sections-v1')
          if (!videoSections) {
            videoSections = document.querySelector('.video-sections-v1')
          }
          return true
        }
      }
    })

    if (!videoSections) {
      console.log('mian stop 没有合集')
      return
    }

    await selectVideo()

    Video.addEventListener('play', VideoOnPlay)
    if (local.defaultreverseorder) {
      Video.addEventListener('ended', VideoOnEnded)
    }

    await switchAddsectionslistheigthOnClick('0')
    await VideoOnPlay()
    await bindWatch()

    window.addEventListener('keyup', e => {
      console.log('keyup', e)

      keyupCodeFn[e.key] && keyupCodeFn[e.key]()
    })

    console.log('mian stop 成功开启')
  }

  window.onload = async () => {
    console.log('正式-v3')

    let addscript = new Promise(resolve => {
      const script = document.createElement('script')
      script.src = 'https://cdn.jsdelivr.net/gh/yaohaixiao/message.js/message.min.js'
      script.onload = () => {
        resolve()
      }
      document.body.appendChild(script)
    })

    try {
      await addscript
    } catch (e) {
      console.log('添加 message 失败')
    }
    console.log('unsafeWindow', unsafeWindow.Qmsg)

    main().catch(err => {
      console.log(err)
    })
  }
})()