Greasy Fork

Greasy Fork is available in English.

B站合集倒序播放

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

当前为 2024-08-21 提交的版本,查看 最新版本

// ==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(`#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

    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 VideoOnPlay() {
        if (local.startreverseorder && !document.querySelector('#zaizai-div')) {
            const div = document.createElement('div')
            div.id = 'zaizai-div'
            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 ${local.startreverseorder ? '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')
            function switchReverseoOnClick() {
                local.startreverseorder = !local.startreverseorder
                if (local.startreverseorder) {
                    this.classList.add('on')
                    Video.addEventListener('ended', VideoOnEnded)
                } else {
                    this.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()
    }



    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')
        VideoOnPlay()


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


    window.onload = () => {
        console.log('正式-v2');
        main()
    }
})()