Greasy Fork

Greasy Fork is available in English.

B站合集倒序播放

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

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==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)
    })
  }
})()